From daaa324abd281ba62d34c0080a5108f3b813936e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Fri, 14 Jun 2024 12:43:32 +0300 Subject: [PATCH] Commit dist --- .gitignore | 3 +- dist/libswap.d.ts | 1 + dist/libswap.js | 1 + dist/src/BitcoinAssetAdapter.d.ts | 25 ++++++ dist/src/BitcoinAssetAdapter.js | 98 ++++++++++++++++++++++++ dist/src/Erc20AssetAdapter.d.ts | 63 ++++++++++++++++ dist/src/Erc20AssetAdapter.js | 85 +++++++++++++++++++++ dist/src/FiatAssetAdapter.d.ts | 21 ++++++ dist/src/FiatAssetAdapter.js | 99 ++++++++++++++++++++++++ dist/src/IAssetAdapter.d.ts | 26 +++++++ dist/src/IAssetAdapter.js | 10 +++ dist/src/NimiqAssetAdapter.d.ts | 27 +++++++ dist/src/NimiqAssetAdapter.js | 121 ++++++++++++++++++++++++++++++ dist/src/SwapHandler.d.ts | 42 +++++++++++ dist/src/SwapHandler.js | 60 +++++++++++++++ 15 files changed, 680 insertions(+), 2 deletions(-) create mode 100644 dist/libswap.d.ts create mode 100644 dist/libswap.js create mode 100644 dist/src/BitcoinAssetAdapter.d.ts create mode 100644 dist/src/BitcoinAssetAdapter.js create mode 100644 dist/src/Erc20AssetAdapter.d.ts create mode 100644 dist/src/Erc20AssetAdapter.js create mode 100644 dist/src/FiatAssetAdapter.d.ts create mode 100644 dist/src/FiatAssetAdapter.js create mode 100644 dist/src/IAssetAdapter.d.ts create mode 100644 dist/src/IAssetAdapter.js create mode 100644 dist/src/NimiqAssetAdapter.d.ts create mode 100644 dist/src/NimiqAssetAdapter.js create mode 100644 dist/src/SwapHandler.d.ts create mode 100644 dist/src/SwapHandler.js diff --git a/.gitignore b/.gitignore index fe15aae..1e8ce36 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ node_modules .DS_Store -dist +#dist *.local - diff --git a/dist/libswap.d.ts b/dist/libswap.d.ts new file mode 100644 index 0000000..a162344 --- /dev/null +++ b/dist/libswap.d.ts @@ -0,0 +1 @@ +export * from './src/SwapHandler'; diff --git a/dist/libswap.js b/dist/libswap.js new file mode 100644 index 0000000..a162344 --- /dev/null +++ b/dist/libswap.js @@ -0,0 +1 @@ +export * from './src/SwapHandler'; diff --git a/dist/src/BitcoinAssetAdapter.d.ts b/dist/src/BitcoinAssetAdapter.d.ts new file mode 100644 index 0000000..ea7d8e3 --- /dev/null +++ b/dist/src/BitcoinAssetAdapter.d.ts @@ -0,0 +1,25 @@ +import type { ConsensusState, TransactionDetails } from '@nimiq/electrum-client'; +import { AssetAdapter, SwapAsset } from './IAssetAdapter'; +export { ConsensusState, TransactionDetails }; +export interface BitcoinClient { + addTransactionListener(listener: (tx: TransactionDetails) => any, addresses: string[]): number | Promise; + getTransactionsByAddress(address: string, sinceBlockHeight?: number, knownTransactions?: TransactionDetails[]): Promise; + removeListener(handle: number): void | Promise; + sendTransaction(tx: TransactionDetails | string): Promise; + addConsensusChangedListener(listener: (consensusState: ConsensusState) => any): number | Promise; +} +export declare class BitcoinAssetAdapter implements AssetAdapter { + client: BitcoinClient; + private cancelCallback; + private stopped; + constructor(client: BitcoinClient); + private findTransaction; + awaitHtlcFunding(address: string, value: number, data?: string, confirmations?: number, onPending?: (tx: TransactionDetails) => any): Promise; + fundHtlc(serializedTx: string): Promise; + awaitHtlcSettlement(address: string, data: string): Promise; + awaitSwapSecret(address: string, data: string): Promise; + settleHtlc(serializedTx: string, secret: string): Promise; + awaitSettlementConfirmation(address: string, onUpdate?: (tx: TransactionDetails) => any): Promise; + stop(reason: Error): void; + private sendTransaction; +} diff --git a/dist/src/BitcoinAssetAdapter.js b/dist/src/BitcoinAssetAdapter.js new file mode 100644 index 0000000..ddfa6ce --- /dev/null +++ b/dist/src/BitcoinAssetAdapter.js @@ -0,0 +1,98 @@ +export class BitcoinAssetAdapter { + constructor(client) { + this.client = client; + this.cancelCallback = null; + this.stopped = false; + } + async findTransaction(address, test) { + return new Promise(async (resolve, reject) => { + const listener = (tx) => { + if (!test(tx)) + return false; + cleanup(); + resolve(tx); + return true; + }; + const transactionListener = await this.client.addTransactionListener(listener, [address]); + let history = []; + const checkHistory = async () => { + history = await this.client.getTransactionsByAddress(address, 0, history); + for (const tx of history) { + if (listener(tx)) + break; + } + }; + checkHistory(); + const consensusListener = await this.client.addConsensusChangedListener((consensusState) => consensusState === 'established' && checkHistory()); + const historyCheckInterval = window.setInterval(checkHistory, 60 * 1000); + const cleanup = () => { + this.client.removeListener(transactionListener); + this.client.removeListener(consensusListener); + window.clearInterval(historyCheckInterval); + this.cancelCallback = null; + }; + this.cancelCallback = (reason) => { + cleanup(); + reject(reason); + }; + }); + } + async awaitHtlcFunding(address, value, data, confirmations = 0, onPending) { + return this.findTransaction(address, (tx) => { + const htlcOutput = tx.outputs.find((out) => out.address === address); + if (!htlcOutput) + return false; + if (htlcOutput.value !== value) + return false; + if (tx.confirmations < confirmations) { + if (typeof onPending === 'function') + onPending(tx); + return false; + } + if (tx.replaceByFee) { + if (tx.state === 'mined' || tx.state === 'confirmed') + return true; + if (typeof onPending === 'function') + onPending(tx); + return false; + } + return true; + }); + } + async fundHtlc(serializedTx) { + if (this.stopped) + throw new Error('BitcoinAssetAdapter called while stopped'); + return this.sendTransaction(serializedTx); + } + async awaitHtlcSettlement(address, data) { + return this.findTransaction(address, (tx) => tx.inputs.some((input) => input.address === address + && typeof input.witness[4] === 'string' && input.witness[4] === data)); + } + async awaitSwapSecret(address, data) { + const tx = await this.awaitHtlcSettlement(address, data); + return tx.inputs[0].witness[2]; + } + async settleHtlc(serializedTx, secret) { + serializedTx = serializedTx.replace('000000000000000000000000000000000000000000000000000000000000000001', `${secret}01`); + return this.sendTransaction(serializedTx); + } + async awaitSettlementConfirmation(address, onUpdate) { + return this.findTransaction(address, (tx) => { + if (!tx.inputs.some((input) => input.address === address && input.witness.length === 5)) + return false; + if (tx.state === 'mined' || tx.state === 'confirmed') + return true; + if (typeof onUpdate === 'function') + onUpdate(tx); + return false; + }); + } + stop(reason) { + if (this.cancelCallback) + this.cancelCallback(reason); + this.stopped = true; + } + async sendTransaction(serializedTx) { + return this.client.sendTransaction(serializedTx); + } +} diff --git a/dist/src/Erc20AssetAdapter.d.ts b/dist/src/Erc20AssetAdapter.d.ts new file mode 100644 index 0000000..604fd60 --- /dev/null +++ b/dist/src/Erc20AssetAdapter.d.ts @@ -0,0 +1,63 @@ +import { BigNumber, Contract, Event as EthersEvent, EventFilter } from 'ethers'; +import { AssetAdapter, SwapAsset } from './IAssetAdapter'; +export declare enum EventType { + OPEN = "Open", + REDEEM = "Redeem", + REFUND = "Refund" +} +type OpenEventArgs = [ + string, + string, + BigNumber, + string, + string, + BigNumber +]; +type RedeemEventArgs = [ + string, + string +]; +type RefundEventArgs = [ + string +]; +type EventArgs = T extends EventType.OPEN ? OpenEventArgs : T extends EventType.REDEEM ? RedeemEventArgs : T extends EventType.REFUND ? RefundEventArgs : never; +type OpenResult = ReadonlyArray & OpenEventArgs & { + id: string; + token: string; + amount: BigNumber; + recipient: string; + hash: string; + timeout: BigNumber; +}; +type RedeemResult = ReadonlyArray & RedeemEventArgs & { + id: string; + secret: string; +}; +type RefundResult = ReadonlyArray & RefundEventArgs & { + id: string; +}; +export interface Event extends EthersEvent { + args: T extends EventType.OPEN ? OpenResult : T extends EventType.REDEEM ? RedeemResult : T extends EventType.REFUND ? RefundResult : never; +} +export type GenericEvent = Event; +export interface Web3Client { + htlcContract: Contract; + currentBlock: () => number | Promise; + startBlock: number; + endBlock?: number; +} +export declare class Erc20AssetAdapter implements AssetAdapter { + client: Web3Client; + private cancelCallback; + private stopped; + constructor(client: Web3Client); + findLog(filter: EventFilter, test?: (...args: [...EventArgs, Event]) => boolean | Promise): Promise>; + awaitHtlcFunding(htlcId: string, value: number, data?: string, confirmations?: number, onPending?: (tx: GenericEvent) => any): Promise>; + fundHtlc(_serializedTx: string): Promise>; + awaitHtlcSettlement(htlcId: string): Promise>; + awaitSwapSecret(htlcId: string): Promise; + settleHtlc(_serializedTx: string, _secret: string): Promise>; + awaitSettlementConfirmation(htlcId: string): Promise>; + stop(reason: Error): void; +} +export {}; diff --git a/dist/src/Erc20AssetAdapter.js b/dist/src/Erc20AssetAdapter.js new file mode 100644 index 0000000..863a9e0 --- /dev/null +++ b/dist/src/Erc20AssetAdapter.js @@ -0,0 +1,85 @@ +export var EventType; +(function (EventType) { + EventType["OPEN"] = "Open"; + EventType["REDEEM"] = "Redeem"; + EventType["REFUND"] = "Refund"; +})(EventType || (EventType = {})); +export class Erc20AssetAdapter { + constructor(client) { + this.client = client; + this.cancelCallback = null; + this.stopped = false; + } + async findLog(filter, test) { + return new Promise(async (resolve, reject) => { + const listener = async (...args) => { + if (test && !(await test.apply(this, args))) + return false; + cleanup(); + resolve(args[args.length - 1]); + return true; + }; + this.client.htlcContract.on(filter, listener); + const checkHistory = async () => { + const history = await this.client.htlcContract.queryFilter(filter, this.client.startBlock, this.client.endBlock); + for (const event of history) { + if (!event.args) + continue; + if (await listener(...event.args, event)) + break; + } + }; + checkHistory(); + const historyCheckInterval = window.setInterval(checkHistory, 60 * 1000); + const cleanup = () => { + this.client.htlcContract.off(filter, listener); + window.clearInterval(historyCheckInterval); + this.cancelCallback = null; + }; + this.cancelCallback = (reason) => { + cleanup(); + reject(reason); + }; + }); + } + async awaitHtlcFunding(htlcId, value, data, confirmations = 0, onPending) { + const filter = this.client.htlcContract.filters.Open(htlcId); + return this.findLog(filter, async (id, token, amount, recipient, hash, timeout, log) => { + if (amount.toNumber() !== value) { + console.warn(`Found ERC-20 HTLC, but amount does not match. Expected ${value}, found ${amount.toNumber()}`); + return false; + } + if (confirmations > 0) { + const logConfirmations = await this.client.currentBlock() - log.blockNumber + 1; + if (logConfirmations < confirmations) { + if (typeof onPending === 'function') + onPending(log); + return false; + } + } + return true; + }); + } + async fundHtlc(_serializedTx) { + throw new Error('Method "fundHtlc" not available for ERC-20 HTLCs'); + } + async awaitHtlcSettlement(htlcId) { + const filter = this.client.htlcContract.filters.Redeem(htlcId); + return this.findLog(filter); + } + async awaitSwapSecret(htlcId) { + const log = await this.awaitHtlcSettlement(htlcId); + return log.args.secret.substring(2); + } + async settleHtlc(_serializedTx, _secret) { + throw new Error('Method "settleHtlc" not available for ERC-20 HTLCs'); + } + async awaitSettlementConfirmation(htlcId) { + return this.awaitHtlcSettlement(htlcId); + } + stop(reason) { + if (this.cancelCallback) + this.cancelCallback(reason); + this.stopped = true; + } +} diff --git a/dist/src/FiatAssetAdapter.d.ts b/dist/src/FiatAssetAdapter.d.ts new file mode 100644 index 0000000..b4cc529 --- /dev/null +++ b/dist/src/FiatAssetAdapter.d.ts @@ -0,0 +1,21 @@ +import { Htlc, HtlcStatus, SettlementTokens } from '@nimiq/oasis-api'; +import type { AssetAdapter, FiatSwapAsset } from './IAssetAdapter'; +export { Htlc as OasisHtlcDetails, SettlementTokens as OasisSettlementTokens }; +export interface OasisClient { + getHtlc(id: string): Promise; + settleHtlc(id: string, secret: string, settlementJWS: string, tokens?: SettlementTokens): Promise; +} +export declare class FiatAssetAdapter implements AssetAdapter { + client: OasisClient; + private cancelCallback; + private stopped; + constructor(client: OasisClient); + private findTransaction; + awaitHtlcFunding(id: string, value: number, data?: string, confirmations?: number, onUpdate?: (htlc: Htlc) => any): Promise; + fundHtlc(): Promise; + awaitHtlcSettlement(id: string): Promise>; + awaitSwapSecret(id: string): Promise; + settleHtlc(settlementJWS: string, secret: string, hash: string, tokens?: SettlementTokens): Promise; + awaitSettlementConfirmation(id: string, onUpdate?: (tx: Htlc) => any): Promise; + stop(reason: Error): void; +} diff --git a/dist/src/FiatAssetAdapter.js b/dist/src/FiatAssetAdapter.js new file mode 100644 index 0000000..7f834e2 --- /dev/null +++ b/dist/src/FiatAssetAdapter.js @@ -0,0 +1,99 @@ +import { HtlcStatus, SettlementStatus } from '@nimiq/oasis-api'; +export class FiatAssetAdapter { + constructor(client) { + this.client = client; + this.cancelCallback = null; + this.stopped = false; + } + async findTransaction(id, test) { + const check = async () => { + try { + const htlc = await this.client.getHtlc(id); + if (test(htlc)) + return htlc; + } + catch (error) { + console.error(error); + if (error.message !== 'HTLC not found') { + } + } + return null; + }; + const htlc = await check(); + if (htlc) + return htlc; + return new Promise((resolve, reject) => { + const interval = window.setInterval(() => { + check().then((htlc) => { + if (!htlc) + return; + cleanup(); + resolve(htlc); + }); + }, 5 * 1000); + const cleanup = () => { + window.clearInterval(interval); + this.cancelCallback = null; + }; + this.cancelCallback = (reason) => { + cleanup(); + reject(reason); + }; + }); + } + async awaitHtlcFunding(id, value, data, confirmations, onUpdate) { + return this.findTransaction(id, (htlc) => { + if (htlc.status === HtlcStatus.CLEARED || htlc.status === HtlcStatus.SETTLED) + return true; + if (typeof onUpdate === 'function') + onUpdate(htlc); + return false; + }); + } + async fundHtlc() { + throw new Error('Method "fundHtlc" not available for EUR/CRC HTLCs'); + } + async awaitHtlcSettlement(id) { + return this.findTransaction(id, (htlc) => typeof htlc.preimage.value === 'string'); + } + async awaitSwapSecret(id) { + const tx = await this.awaitHtlcSettlement(id); + return tx.preimage.value; + } + async settleHtlc(settlementJWS, secret, hash, tokens) { + if (this.stopped) + throw new Error('FiatAssetAdapter called while stopped'); + const jwsBody = settlementJWS.split('.')[1]; + const jsonBody = atob(jwsBody.replace(/_/g, '/').replace(/-/g, '+')); + const payload = JSON.parse(jsonBody); + let htlc; + try { + htlc = await this.client.settleHtlc(payload.contractId, secret, settlementJWS, tokens); + } + catch (error) { + console.error(error); + htlc = await this.client.getHtlc(payload.contractId); + } + if (htlc.status !== HtlcStatus.SETTLED || htlc.settlement.status === SettlementStatus.WAITING) { + throw new Error('Could not settle OASIS HTLC (invalid secret or authorization token?)'); + } + return htlc; + } + async awaitSettlementConfirmation(id, onUpdate) { + return this.findTransaction(id, (htlc) => { + if (htlc.status !== HtlcStatus.SETTLED) + return false; + if (htlc.settlement.status === SettlementStatus.ACCEPTED + || htlc.settlement.status === SettlementStatus.CONFIRMED) + return true; + if (typeof onUpdate === 'function') + onUpdate(htlc); + return false; + }); + } + stop(reason) { + if (this.cancelCallback) + this.cancelCallback(reason); + this.stopped = true; + } +} diff --git a/dist/src/IAssetAdapter.d.ts b/dist/src/IAssetAdapter.d.ts new file mode 100644 index 0000000..7a33867 --- /dev/null +++ b/dist/src/IAssetAdapter.d.ts @@ -0,0 +1,26 @@ +import type { BitcoinClient, TransactionDetails as BitcoinTransactionDetails } from './BitcoinAssetAdapter'; +import type { GenericEvent as Erc20TransactionDetails, Web3Client } from './Erc20AssetAdapter'; +import type { OasisClient, OasisHtlcDetails, OasisSettlementTokens } from './FiatAssetAdapter'; +import type { NimiqClient, TransactionDetails as NimiqTransactionDetails } from './NimiqAssetAdapter'; +export declare enum SwapAsset { + NIM = "NIM", + BTC = "BTC", + USDC = "USDC", + USDC_MATIC = "USDC_MATIC", + USDT = "USDT", + EUR = "EUR", + CRC = "CRC" +} +export type FiatSwapAsset = SwapAsset.EUR | SwapAsset.CRC; +export type Transaction = TAsset extends SwapAsset.NIM ? NimiqTransactionDetails : TAsset extends SwapAsset.BTC ? BitcoinTransactionDetails : TAsset extends SwapAsset.USDC | SwapAsset.USDC_MATIC | SwapAsset.USDT ? Erc20TransactionDetails : TAsset extends FiatSwapAsset ? OasisHtlcDetails : never; +export type Client = TAsset extends SwapAsset.NIM ? NimiqClient : TAsset extends SwapAsset.BTC ? BitcoinClient : TAsset extends SwapAsset.USDC | SwapAsset.USDC_MATIC | SwapAsset.USDT ? Web3Client : TAsset extends FiatSwapAsset ? OasisClient : never; +export interface AssetAdapter { + client: Client; + awaitHtlcFunding(address: string, value: number, data: string, confirmations: number, onPending?: (tx: Transaction) => any): Promise>; + fundHtlc(serializedTx: string, onPending: (tx: Transaction) => any, serializedProxyTx?: string): Promise>; + awaitHtlcSettlement(address: string, data: string): Promise>; + awaitSwapSecret(address: string, data: string): Promise; + settleHtlc(serializedTx: string, secret: string, hash: string, tokens?: OasisSettlementTokens): Promise>; + awaitSettlementConfirmation(address: string, onUpdate?: (tx: Transaction) => any): Promise>; + stop(reason: Error): void; +} diff --git a/dist/src/IAssetAdapter.js b/dist/src/IAssetAdapter.js new file mode 100644 index 0000000..9ec4084 --- /dev/null +++ b/dist/src/IAssetAdapter.js @@ -0,0 +1,10 @@ +export var SwapAsset; +(function (SwapAsset) { + SwapAsset["NIM"] = "NIM"; + SwapAsset["BTC"] = "BTC"; + SwapAsset["USDC"] = "USDC"; + SwapAsset["USDC_MATIC"] = "USDC_MATIC"; + SwapAsset["USDT"] = "USDT"; + SwapAsset["EUR"] = "EUR"; + SwapAsset["CRC"] = "CRC"; +})(SwapAsset || (SwapAsset = {})); diff --git a/dist/src/NimiqAssetAdapter.d.ts b/dist/src/NimiqAssetAdapter.d.ts new file mode 100644 index 0000000..17da857 --- /dev/null +++ b/dist/src/NimiqAssetAdapter.d.ts @@ -0,0 +1,27 @@ +import { AssetAdapter, SwapAsset } from './IAssetAdapter'; +type RawTransactionDetails = import('@nimiq/core-web').Client.TransactionDetails; +export type TransactionDetails = ReturnType; +export type ConsensusState = import('@nimiq/core-web').Client.ConsensusState; +export interface NimiqClient { + addTransactionListener(listener: (tx: TransactionDetails | RawTransactionDetails) => any, addresses: string[]): number | Promise; + getTransactionsByAddress(address: string, sinceBlockHeight?: number, knownTransactions?: TransactionDetails[] | RawTransactionDetails[]): Promise; + removeListener(handle: number): void | Promise; + sendTransaction(tx: TransactionDetails | RawTransactionDetails | string): Promise; + addConsensusChangedListener(listener: (consensusState: ConsensusState) => any): number | Promise; +} +export declare class NimiqAssetAdapter implements AssetAdapter { + client: NimiqClient; + private cancelCallback; + private stopped; + constructor(client: NimiqClient); + private findTransaction; + awaitHtlcFunding(address: string, value: number, data: string, confirmations?: number, onPending?: (tx: TransactionDetails) => any): Promise; + fundHtlc(serializedTx: string, onPending?: (tx: TransactionDetails) => any, serializedProxyTx?: string): Promise; + awaitHtlcSettlement(address: string): Promise; + awaitSwapSecret(address: string): Promise; + settleHtlc(serializedTx: string, secret: string, hash: string): Promise; + awaitSettlementConfirmation(address: string, onUpdate?: (tx: TransactionDetails) => any): Promise; + stop(reason: Error): void; + private sendTransaction; +} +export {}; diff --git a/dist/src/NimiqAssetAdapter.js b/dist/src/NimiqAssetAdapter.js new file mode 100644 index 0000000..9bd8814 --- /dev/null +++ b/dist/src/NimiqAssetAdapter.js @@ -0,0 +1,121 @@ +import { shim as shimPromiseFinally } from 'promise.prototype.finally'; +shimPromiseFinally(); +export class NimiqAssetAdapter { + constructor(client) { + this.client = client; + this.cancelCallback = null; + this.stopped = false; + } + async findTransaction(address, test) { + return new Promise(async (resolve, reject) => { + const listener = (tx) => { + if ('toPlain' in tx) + tx = tx.toPlain(); + if (!test(tx)) + return false; + cleanup(); + resolve(tx); + return true; + }; + const transactionListener = await this.client.addTransactionListener(listener, [address]); + let history = []; + const checkHistory = async () => { + history = await this.client.getTransactionsByAddress(address, 0, history); + for (const tx of history) { + if (listener(tx)) + break; + } + }; + checkHistory(); + const consensusListener = await this.client.addConsensusChangedListener((consensusState) => consensusState === 'established' && checkHistory()); + const historyCheckInterval = window.setInterval(checkHistory, 60 * 1000); + const cleanup = () => { + this.client.removeListener(transactionListener); + this.client.removeListener(consensusListener); + window.clearInterval(historyCheckInterval); + this.cancelCallback = null; + }; + this.cancelCallback = (reason) => { + cleanup(); + reject(reason); + }; + }); + } + async awaitHtlcFunding(address, value, data, confirmations = 0, onPending) { + return this.findTransaction(address, (tx) => { + if (tx.recipient !== address) + return false; + if (tx.value !== value) + return false; + if (typeof tx.data.raw !== 'string' || tx.data.raw !== data) + return false; + if (tx.state === 'mined' || tx.state === 'confirmed') { + if (tx.confirmations >= confirmations) + return true; + } + if (typeof onPending === 'function') + onPending(tx); + return false; + }); + } + async fundHtlc(serializedTx, onPending, serializedProxyTx) { + if (serializedProxyTx) { + const proxyTx = await this.sendTransaction(serializedProxyTx, false); + const resendInterval = window.setInterval(() => this.sendTransaction(serializedProxyTx, false), 60 * 1000); + await this.findTransaction(proxyTx.recipient, (tx) => tx.transactionHash === proxyTx.transactionHash + && (tx.state === 'mined' || tx.state === 'confirmed')).finally(() => window.clearInterval(resendInterval)); + } + const htlcTx = await this.sendTransaction(serializedTx, false); + if (htlcTx.state === 'new' || htlcTx.state === 'pending') { + if (typeof onPending === 'function') + onPending(htlcTx); + const resendInterval = window.setInterval(() => this.sendTransaction(serializedTx, false), 60 * 1000); + return this.awaitHtlcFunding(htlcTx.recipient, htlcTx.value, htlcTx.data.raw) + .finally(() => window.clearInterval(resendInterval)); + } + return htlcTx; + } + async awaitHtlcSettlement(address) { + return this.findTransaction(address, (tx) => tx.sender === address + && typeof tx.proof.preImage === 'string'); + } + async awaitSwapSecret(address) { + const tx = await this.awaitHtlcSettlement(address); + return tx.proof.preImage; + } + async settleHtlc(serializedTx, secret, hash) { + serializedTx = serializedTx + .replace(`${hash}0000000000000000000000000000000000000000000000000000000000000000`, `${hash}${secret}`) + .replace('66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925' + + '0000000000000000000000000000000000000000000000000000000000000000', `${hash}${secret}`); + return this.sendTransaction(serializedTx); + } + async awaitSettlementConfirmation(address, onUpdate) { + return this.findTransaction(address, (tx) => { + if (tx.sender !== address) + return false; + if (typeof tx.proof.preImage !== 'string') + return false; + if (tx.state === 'mined' || tx.state === 'confirmed') + return true; + if (typeof onUpdate === 'function') + onUpdate(tx); + return false; + }); + } + stop(reason) { + if (this.cancelCallback) + this.cancelCallback(reason); + this.stopped = true; + } + async sendTransaction(serializedTx, throwOnFailure = true) { + if (this.stopped) + throw new Error('NimiqAssetAdapter called while stopped'); + let tx = await this.client.sendTransaction(serializedTx); + if ('toPlain' in tx) + tx = tx.toPlain(); + if (throwOnFailure && tx.state === 'new') + throw new Error('Failed to send transaction'); + return tx; + } +} diff --git a/dist/src/SwapHandler.d.ts b/dist/src/SwapHandler.d.ts new file mode 100644 index 0000000..5a3d7b8 --- /dev/null +++ b/dist/src/SwapHandler.d.ts @@ -0,0 +1,42 @@ +import type { OasisSettlementTokens } from './FiatAssetAdapter'; +import { SwapAsset } from './IAssetAdapter'; +import type { AssetAdapter, Client, Transaction } from './IAssetAdapter'; +export { Client, SwapAsset, Transaction }; +export type Contract = { + htlc: { + address: string; + data: TAsset extends SwapAsset.NIM ? string : never; + script: TAsset extends SwapAsset.BTC ? string : never; + contract: TAsset extends SwapAsset.USDC | SwapAsset.USDC_MATIC | SwapAsset.USDT ? string : never; + }; +}; +export type Swap = { + from: { + asset: FromAsset; + amount: number; + }; + to: { + asset: ToAsset; + amount: number; + serviceEscrowFee: number; + }; + hash: string; + contracts: { + [asset in FromAsset | ToAsset]: Contract; + }; +}; +export declare class SwapHandler { + private swap; + fromAssetAdapter: AssetAdapter; + toAssetAdapter: AssetAdapter; + private static makeAssetAdapter; + constructor(swap: Swap, fromClient: Client, toClient: Client); + setSwap(swap: Swap): void; + awaitIncoming(onUpdate: (tx: Transaction) => any, confirmations?: number): Promise>; + createOutgoing(serializedTx: string, onPending: (tx: Transaction) => any, serializedProxyTx?: string): Promise>; + awaitOutgoing(onUpdate: (tx: Transaction) => any, confirmations?: number): Promise>; + awaitSecret(): Promise; + settleIncoming(serializedTx: string, secret: string, tokens?: OasisSettlementTokens): Promise>; + awaitIncomingConfirmation(onUpdate?: (tx: Transaction) => any): Promise>; + stop(reason: Error): void; +} diff --git a/dist/src/SwapHandler.js b/dist/src/SwapHandler.js new file mode 100644 index 0000000..8dd0252 --- /dev/null +++ b/dist/src/SwapHandler.js @@ -0,0 +1,60 @@ +import { BitcoinAssetAdapter } from './BitcoinAssetAdapter'; +import { Erc20AssetAdapter } from './Erc20AssetAdapter'; +import { FiatAssetAdapter } from './FiatAssetAdapter'; +import { SwapAsset } from './IAssetAdapter'; +import { NimiqAssetAdapter } from './NimiqAssetAdapter'; +export { SwapAsset }; +export class SwapHandler { + static makeAssetAdapter(asset, client) { + switch (asset) { + case SwapAsset.NIM: + return new NimiqAssetAdapter(client); + case SwapAsset.BTC: + return new BitcoinAssetAdapter(client); + case SwapAsset.USDC: + case SwapAsset.USDC_MATIC: + case SwapAsset.USDT: + return new Erc20AssetAdapter(client); + case SwapAsset.EUR: + return new FiatAssetAdapter(client); + case SwapAsset.CRC: + return new FiatAssetAdapter(client); + default: + throw new Error(`Unsupported asset: ${asset}`); + } + } + constructor(swap, fromClient, toClient) { + this.swap = swap; + this.fromAssetAdapter = SwapHandler.makeAssetAdapter(this.swap.from.asset, fromClient); + this.toAssetAdapter = SwapHandler.makeAssetAdapter(this.swap.to.asset, toClient); + } + setSwap(swap) { + this.swap = swap; + } + async awaitIncoming(onUpdate, confirmations = 0) { + const contract = this.swap.contracts[this.swap.to.asset]; + return this.toAssetAdapter.awaitHtlcFunding(contract.htlc.address, this.swap.to.amount + this.swap.to.serviceEscrowFee, this.swap.to.asset === SwapAsset.NIM ? contract.htlc.data : '', confirmations, onUpdate); + } + async createOutgoing(serializedTx, onPending, serializedProxyTx) { + return this.fromAssetAdapter.fundHtlc(serializedTx, onPending, serializedProxyTx); + } + async awaitOutgoing(onUpdate, confirmations = 0) { + const contract = this.swap.contracts[this.swap.from.asset]; + return this.fromAssetAdapter.awaitHtlcFunding(contract.htlc.address, this.swap.from.amount, this.swap.from.asset === SwapAsset.NIM ? contract.htlc.data : '', confirmations, onUpdate); + } + async awaitSecret() { + const contract = this.swap.contracts[this.swap.from.asset]; + return this.fromAssetAdapter.awaitSwapSecret(contract.htlc.address, this.swap.from.asset === SwapAsset.BTC ? contract.htlc.script : ''); + } + async settleIncoming(serializedTx, secret, tokens) { + return this.toAssetAdapter.settleHtlc(serializedTx, secret, this.swap.hash, tokens); + } + async awaitIncomingConfirmation(onUpdate) { + const contract = this.swap.contracts[this.swap.to.asset]; + return this.toAssetAdapter.awaitSettlementConfirmation(contract.htlc.address, onUpdate); + } + stop(reason) { + this.fromAssetAdapter.stop(reason); + this.toAssetAdapter.stop(reason); + } +}