UNPKG

@atomiqlabs/sdk

Version:

atomiq labs SDK for cross-chain swaps between smart chains and bitcoin

1,106 lines (1,020 loc) 131 kB
import {ISwapPrice} from "../prices/abstract/ISwapPrice"; import { BitcoinNetwork, BitcoinRpc, BitcoinRpcWithAddressIndex, BtcBlock, BtcRelay, ChainData, ChainSwapType, ChainType, LightningNetworkApi, Messenger, RelaySynchronizer, SpvWithdrawalClaimedState, SpvWithdrawalFrontedState, SwapCommitState, SwapContract, SwapData } from "@atomiqlabs/base"; import { ToBTCLNOptions, ToBTCLNWrapper } from "../swaps/escrow_swaps/tobtc/ln/ToBTCLNWrapper"; import {ToBTCOptions, ToBTCWrapper} from "../swaps/escrow_swaps/tobtc/onchain/ToBTCWrapper"; import {FromBTCLNOptions, FromBTCLNWrapper} from "../swaps/escrow_swaps/frombtc/ln/FromBTCLNWrapper"; import {FromBTCOptions, FromBTCWrapper} from "../swaps/escrow_swaps/frombtc/onchain/FromBTCWrapper"; import {IntermediaryDiscovery, MultichainSwapBounds, SwapBounds} from "../intermediaries/IntermediaryDiscovery"; import {decode as bolt11Decode} from "@atomiqlabs/bolt11"; import {ISwap} from "../swaps/ISwap"; import {IntermediaryError} from "../errors/IntermediaryError"; import {SwapType} from "../enums/SwapType"; import {FromBTCLNSwap} from "../swaps/escrow_swaps/frombtc/ln/FromBTCLNSwap"; import {FromBTCSwap} from "../swaps/escrow_swaps/frombtc/onchain/FromBTCSwap"; import {ToBTCLNSwap} from "../swaps/escrow_swaps/tobtc/ln/ToBTCLNSwap"; import {ToBTCSwap} from "../swaps/escrow_swaps/tobtc/onchain/ToBTCSwap"; import {LnForGasWrapper} from "../swaps/trusted/ln/LnForGasWrapper"; import {LnForGasSwap} from "../swaps/trusted/ln/LnForGasSwap"; import {EventEmitter} from "events"; import {Intermediary} from "../intermediaries/Intermediary"; import {ISwapWrapper, WrapperCtorTokens} from "../swaps/ISwapWrapper"; import {bigIntCompare, bigIntMax, bigIntMin, fromDecimal, objectMap, randomBytes} from "../utils/Utils"; import {OutOfBoundsError} from "../errors/RequestError"; import {SwapperWithChain} from "./SwapperWithChain"; import {OnchainForGasSwap} from "../swaps/trusted/onchain/OnchainForGasSwap"; import {OnchainForGasWrapper} from "../swaps/trusted/onchain/OnchainForGasWrapper"; import {BTC_NETWORK, NETWORK, TEST_NETWORK} from "@scure/btc-signer/utils"; import {IUnifiedStorage, QueryParams} from "../storage/IUnifiedStorage"; import { UnifiedSwapStorage, UnifiedSwapStorageCompositeIndexes, UnifiedSwapStorageIndexes } from "../storage/UnifiedSwapStorage"; import {UnifiedSwapEventListener} from "../events/UnifiedSwapEventListener"; import {IToBTCSwap} from "../swaps/escrow_swaps/tobtc/IToBTCSwap"; import {SpvFromBTCOptions, SpvFromBTCWrapper} from "../swaps/spv_swaps/SpvFromBTCWrapper"; import {SpvFromBTCSwap} from "../swaps/spv_swaps/SpvFromBTCSwap"; import {SwapperUtils} from "./SwapperUtils"; import {FromBTCLNAutoOptions, FromBTCLNAutoWrapper} from "../swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoWrapper"; import {FromBTCLNAutoSwap} from "../swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoSwap"; import {UserError} from "../errors/UserError"; import {SwapAmountType} from "../enums/SwapAmountType"; import {IClaimableSwap} from "../swaps/IClaimableSwap"; import {correctClock} from "../utils/AutomaticClockDriftCorrection"; import {isSwapType, SwapProtocolInfo, SwapTypeMapping} from "../utils/SwapUtils"; import {IndexedDBUnifiedStorage} from "../storage-browser/IndexedDBUnifiedStorage"; import {TokenAmount, toTokenAmount} from "../types/TokenAmount"; import {BitcoinTokens, BtcToken, isBtcToken, isSCToken, SCToken, Token} from "../types/Token"; import {AmountData} from "../types/AmountData"; import {getLogger} from "../utils/Logger"; import {isLNURLWithdraw, LNURLWithdraw} from "../types/lnurl/LNURLWithdraw"; import {isLNURLPay, LNURLPay} from "../types/lnurl/LNURLPay"; import {tryWithRetries} from "../utils/RetryUtils"; import {NotNever} from "../utils/TypeUtils"; import {IEscrowSwap} from "../swaps/escrow_swaps/IEscrowSwap"; import {LightningInvoiceCreateService, isLightningInvoiceCreateService} from "../types/wallets/LightningInvoiceCreateService"; import {SwapSide} from "../enums/SwapSide"; import {IntermediaryAPI} from "../intermediaries/apis/IntermediaryAPI"; import {BitcoinWalletUtxo, BitcoinWalletUtxoBase, IBitcoinWallet} from "../bitcoin/wallet/IBitcoinWallet"; import {MinimalBitcoinWalletInterface} from "../types/wallets/MinimalBitcoinWalletInterface"; import {toBitcoinWallet} from "../utils/BitcoinWalletUtils"; import {getSignedKeyBasedAuthHandler} from "../intermediaries/auth/SignedKeyBasedAuth"; /** * Configuration options for the Swapper * @category Core */ export type SwapperOptions = { /** * Manual override for the intermediary (LP) URLs for the SDK to use, by default these are fetched automatically * from the registry */ intermediaryUrl?: string | string[], /** * Registry URL for where to look for active intermediary (LP) endpoint URLs */ registryUrl?: string, /** * Bitcoin network to use for the swaps, */ bitcoinNetwork?: BitcoinNetwork, /** * Timeout (in milliseconds) for HTTP GET requests done by the SDK */ getRequestTimeout?: number, /** * Timeout (in milliseconds) for HTTP POST requests done by the SDK */ postRequestTimeout?: number, /** * Additional parameters to be sent to the intermediaries (LPs), when requesting quotes from them */ defaultAdditionalParameters?: {[key: string]: any}, /** * Optional name prefix to use when creating a swap storage, you can use this to create separate storage * instances that don't overlap. */ storagePrefix?: string, /** * Sets the default intermediary (LP) to use for the trusted gas swaps, if not set the SDK uses a default one */ defaultTrustedIntermediaryUrl?: string, /** * A function callback to retrieve a specific named storage container for swap persistency. If not present, the * default IndexedDB storage adapter is used. When you use the SDK in non-browser based environments you need to * provide this callback such that the SDK is able to use a custom storage adapter. * * @param storageName Name of the container to retrieve */ swapStorage?: (storageName: string) => IUnifiedStorage<UnifiedSwapStorageIndexes, UnifiedSwapStorageCompositeIndexes>, /** * By setting this flag, the swapper doesn't schedule automatic tick timers. To make sure the swap states are * properly updated (e.g. the expired swaps properly move to the expired state), you should call the * {@link Swapper._syncSwaps} function periodically. This flag should be set when you run an environment that * doesn't support long-running timers - e.g. serverless environments like Azure Function Apps or AWS Lambda */ noTimers?: boolean, /** * By setting this flag, the swapper doesn't subscribe to on-chain events. To make sure the swap states are * properly updated you should either call the {@link Swapper._syncSwaps} function periodically, or use the * {@link Swapper._pollChainEvents} function to manually poll for on-chain events. This flag should be set * when you run an environment that doesn't support long-running timers and websocket connections - e.g. * serverless environments like Azure Function Apps or AWS Lambda */ noEvents?: boolean, /** * By setting this flag, the swap objects will not be cached in the SDK and instead will always be loaded from * the persistent storage. By default, the SDK uses a `WeakRef` mapping of swaps, to ensure that when the same * swap is loaded concurrently, it returns the same object reference to both, making the changes on the object * atomic. This flag should be set to `true` when running in an environment where multiple instances of the SDK * access the same swap database - e.g. serverless environments like Azure Function Apps or AWS Lambda */ noSwapCache?: boolean, /** * Skip checking past swaps when the swapper is initiated with {@link Swapper.init}, you can call the * {@link Swapper._syncSwaps} function later, to check the swaps. By default, the SDK checks the state * of all the known swaps during init. */ dontCheckPastSwaps?: boolean, /** * Skip fetching the LPs when the swapper is initiated with {@link Swapper.init}, this means the list of available * tokens and swap limits won't be available immediately. LPs will be fetched automatically later, when a swap * is requested */ dontFetchLPs?: boolean, /** * Defaults to `true`, this means every swap regardless of it being initiated (i.e. when `commit()`, `execute()` or * `waitTillPayment` is called) is saved to the persistent storage. This is a reasonable default for when you * want to only create a swap, and then later on retrieve it with the `swapper.getSwapById()` function. * * Setting this to `false` means the SDK only saves and persists swaps that are considered initiated, i.e. when * `commit()`, `execute()` or `waitTillPayment()` is called (or their respective txs... prefixed variations). This * might save calls to the persistent storage for swaps that are never initiated. This is useful in e.g. * frontend implementations where the frontend holds the swap object reference until it is initiated anyway, not * necessitating the saving of the swap data to the persistent storage until it is actually initiated. */ saveUninitializedSwaps?: boolean, /** * Automatically checks system time on initialize, if the system time drifts too far from the actual time * (as checked from multiple server sources) it adjusts the `Date.now()` function to return proper actual time. */ automaticClockDriftCorrection?: boolean, /** * Used in centralized API deployments to allow higher rate limits from LPs */ signedKeyBasedAuth?: { certificate: string, privateKey: string }, /** * If you set the option to `true` the chains for which the RPC is unresponsive are skipped and not initialized * letting the swapper continue with only the available chains with responsive RPCs */ gracefullyHandleChainErrors?: boolean, }; /** * Type representing multiple blockchain configurations * @category Core */ export type MultiChain = { [chainIdentifier in string]: ChainType; }; type ChainSpecificData<T extends ChainType> = { wrappers: { [SwapType.TO_BTCLN]: ToBTCLNWrapper<T>, [SwapType.TO_BTC]: ToBTCWrapper<T>, [SwapType.FROM_BTCLN]: FromBTCLNWrapper<T>, [SwapType.FROM_BTC]: FromBTCWrapper<T>, [SwapType.TRUSTED_FROM_BTCLN]: LnForGasWrapper<T>, [SwapType.TRUSTED_FROM_BTC]: OnchainForGasWrapper<T>, [SwapType.SPV_VAULT_FROM_BTC]: SpvFromBTCWrapper<T>, [SwapType.FROM_BTCLN_AUTO]: FromBTCLNAutoWrapper<T> } chainEvents: T["Events"], chainInterface: T["ChainInterface"], unifiedChainEvents: UnifiedSwapEventListener<T>, unifiedSwapStorage: UnifiedSwapStorage<T>, reviver: (val: any) => ISwap<T>, defaultVersion: string, versionedContracts: { [contractVersion: string]: { swapContract: T["Contract"], spvVaultContract: T["SpvVaultContract"], btcRelay: BtcRelay<any, T["TX"], BtcBlock, T["Signer"]>, synchronizer: RelaySynchronizer<any, T["TX"], BtcBlock>, } } }; type MultiChainData<T extends MultiChain> = { [chainIdentifier in keyof T]: ChainSpecificData<T[chainIdentifier]> }; type CtorMultiChainData<T extends MultiChain> = { [chainIdentifier in keyof T]: ChainData<T[chainIdentifier]> }; type SwapperCtorTokens<T extends MultiChain = MultiChain> = { ticker: string, name: string, chains: {[chainId in ChainIds<T>]?: { address: string, decimals: number, displayDecimals?: number }} }[]; /** * Type extracting chain identifiers from a MultiChain type * @category Core */ export type ChainIds<T extends MultiChain> = keyof T & string; /** * Type helper to check if a chain supports a specific swap type * @category Core */ export type SupportsSwapType< C extends ChainType, Type extends SwapType > = Type extends SwapType.SPV_VAULT_FROM_BTC ? NotNever<C["SpvVaultContract"]> : Type extends (SwapType.TRUSTED_FROM_BTCLN | SwapType.TRUSTED_FROM_BTC) ? true : Type extends SwapType.FROM_BTCLN_AUTO ? (C["Contract"]["supportsInitWithoutClaimer"] extends true ? true : false) : NotNever<C["Contract"]>; /** * Core orchestrator for all atomiq swap operations * * @category Core */ export class Swapper<T extends MultiChain> extends EventEmitter<{ lpsRemoved: [Intermediary[]], lpsAdded: [Intermediary[]], swapState: [ISwap], swapLimitsChanged: [] }> { private readonly logger = getLogger(this.constructor.name+": "); private readonly swapStateListener: (swap: ISwap) => void; private defaultTrustedIntermediary?: Intermediary; private readonly bitcoinNetwork: BitcoinNetwork; private readonly options: SwapperOptions; /** * Data propagation layer used for broadcasting messages to watchtowers */ private readonly messenger: Messenger; /** * A dictionary of smart chains used by the SDK * @internal */ readonly _chains: MultiChainData<T>; /** * Bitcoin RPC for fetching bitcoin chain data * @internal */ readonly _bitcoinRpc: BitcoinRpcWithAddressIndex<any>; /** * Bitcoin network specification * @internal */ readonly _btcNetwork: BTC_NETWORK; /** * Token data indexed by chain identifier and token addresses * @internal */ readonly _tokens: { [chainId: string]: { [tokenAddress: string]: SCToken } }; /** * Token data indexed by chain identifier and token tickers * @internal */ readonly _tokensByTicker: { [chainId: string]: { [tokenTicker: string]: SCToken } }; /** * Pricing API used by the SDK */ readonly prices: ISwapPrice<T>; /** * API for contacting LPs */ readonly lpApi: IntermediaryAPI; /** * Intermediary discovery instance */ readonly intermediaryDiscovery: IntermediaryDiscovery; /** * Miscellaneous utility functions */ readonly Utils: SwapperUtils<T>; /** * @internal */ constructor( bitcoinRpc: BitcoinRpcWithAddressIndex<any>, lightningApi: LightningNetworkApi, bitcoinSynchronizer: (btcRelay: BtcRelay<any, any, any>) => RelaySynchronizer<any, any, any>, chainsData: CtorMultiChainData<T>, pricing: ISwapPrice<T>, tokens: SCToken[], messenger: Messenger, options?: SwapperOptions ) { super(); const storagePrefix = options?.storagePrefix ?? "atomiq-"; options ??= {}; options.saveUninitializedSwaps ??= true; options.bitcoinNetwork = options.bitcoinNetwork==null ? BitcoinNetwork.TESTNET : options.bitcoinNetwork; const swapStorage = options.swapStorage ??= (name: string) => new IndexedDBUnifiedStorage(name); this.options = options; this.bitcoinNetwork = options.bitcoinNetwork; this._btcNetwork = options.bitcoinNetwork===BitcoinNetwork.MAINNET ? NETWORK : (options.bitcoinNetwork===BitcoinNetwork.TESTNET || options.bitcoinNetwork===BitcoinNetwork.TESTNET4) ? TEST_NETWORK : { bech32: 'bcrt', pubKeyHash: 111, scriptHash: 196, wif: 239 }; this.Utils = new SwapperUtils(this); this.prices = pricing; this._bitcoinRpc = bitcoinRpc; this.messenger = messenger; this._tokens = {}; this._tokensByTicker = {}; for(let tokenData of tokens) { const chainId = tokenData.chainId; this._tokens[chainId] ??= {}; this._tokensByTicker[chainId] ??= {}; this._tokens[chainId][tokenData.address] = this._tokensByTicker[chainId][tokenData.ticker] = tokenData; } const lpApi = new IntermediaryAPI( this.options.signedKeyBasedAuth!=null ? getSignedKeyBasedAuthHandler(this.options.signedKeyBasedAuth.certificate, this.options.signedKeyBasedAuth.privateKey) : undefined ); this.lpApi = lpApi; this.swapStateListener = (swap: ISwap) => { this.emit("swapState", swap); }; this._chains = objectMap<CtorMultiChainData<T>, MultiChainData<T>>(chainsData, <InputKey extends keyof CtorMultiChainData<T>>(chainData: CtorMultiChainData<T>[InputKey], key: string) => { let { chainInterface, chainEvents, chainId, btcRelay, swapContract, swapDataConstructor, spvVaultContract, spvVaultWithdrawalDataConstructor, spvVaultDataConstructor, defaultVersion, versions } = chainData; defaultVersion ??= "v1"; if(versions==null) { versions = { [defaultVersion]: { btcRelay, swapContract, swapDataConstructor, spvVaultContract, spvVaultDataConstructor, spvVaultWithdrawalDataConstructor } } } const versionedContracts = objectMap(versions, (value, key) => { return { swapContract: value.swapContract, spvVaultContract: value.spvVaultContract, btcRelay: value.btcRelay, synchronizer: bitcoinSynchronizer(value.btcRelay) }; }); const storageHandler = swapStorage(storagePrefix + chainId); const unifiedSwapStorage = new UnifiedSwapStorage<T[InputKey]>(storageHandler, this.options.noSwapCache); const unifiedChainEvents = new UnifiedSwapEventListener<T[InputKey]>(unifiedSwapStorage, chainEvents); const wrappers: any = {}; wrappers[SwapType.TO_BTCLN] = new ToBTCLNWrapper<T[InputKey]>( key, unifiedSwapStorage, unifiedChainEvents, chainInterface, pricing, this._tokens[chainId], versions, lpApi, { getRequestTimeout: this.options.getRequestTimeout, postRequestTimeout: this.options.postRequestTimeout, saveUninitializedSwaps: this.options.saveUninitializedSwaps, } ); wrappers[SwapType.TO_BTC] = new ToBTCWrapper<T[InputKey]>( key, unifiedSwapStorage, unifiedChainEvents, chainInterface, pricing, this._tokens[chainId], versions, this._bitcoinRpc, lpApi, { getRequestTimeout: this.options.getRequestTimeout, postRequestTimeout: this.options.postRequestTimeout, saveUninitializedSwaps: this.options.saveUninitializedSwaps, bitcoinNetwork: this._btcNetwork } ); wrappers[SwapType.FROM_BTCLN] = new FromBTCLNWrapper<T[InputKey]>( key, unifiedSwapStorage, unifiedChainEvents, chainInterface, pricing, this._tokens[chainId], versions, lightningApi, lpApi, { getRequestTimeout: this.options.getRequestTimeout, postRequestTimeout: this.options.postRequestTimeout, saveUninitializedSwaps: this.options.saveUninitializedSwaps, unsafeSkipLnNodeCheck: this.bitcoinNetwork===BitcoinNetwork.TESTNET4 || this.bitcoinNetwork===BitcoinNetwork.REGTEST } ); wrappers[SwapType.FROM_BTC] = new FromBTCWrapper<T[InputKey]>( key, unifiedSwapStorage, unifiedChainEvents, chainInterface, pricing, this._tokens[chainId], versions, versionedContracts, this._bitcoinRpc, lpApi, { getRequestTimeout: this.options.getRequestTimeout, postRequestTimeout: this.options.postRequestTimeout, saveUninitializedSwaps: this.options.saveUninitializedSwaps, bitcoinNetwork: this._btcNetwork } ); wrappers[SwapType.TRUSTED_FROM_BTCLN] = new LnForGasWrapper<T[InputKey]>( key, unifiedSwapStorage, unifiedChainEvents, chainInterface, pricing, this._tokens[chainId], lpApi, { getRequestTimeout: this.options.getRequestTimeout, postRequestTimeout: this.options.postRequestTimeout, saveUninitializedSwaps: this.options.saveUninitializedSwaps, } ); wrappers[SwapType.TRUSTED_FROM_BTC] = new OnchainForGasWrapper<T[InputKey]>( key, unifiedSwapStorage, unifiedChainEvents, chainInterface, pricing, this._tokens[chainId], bitcoinRpc, lpApi, { getRequestTimeout: this.options.getRequestTimeout, postRequestTimeout: this.options.postRequestTimeout, saveUninitializedSwaps: this.options.saveUninitializedSwaps, bitcoinNetwork: this._btcNetwork } ); // This is gated on the default version of the contracts if(spvVaultContract!=null) { wrappers[SwapType.SPV_VAULT_FROM_BTC] = new SpvFromBTCWrapper<T[InputKey]>( key, unifiedSwapStorage, unifiedChainEvents, chainInterface, pricing, this._tokens[chainId], versions, versionedContracts, bitcoinRpc, lpApi, { getRequestTimeout: this.options.getRequestTimeout, postRequestTimeout: this.options.postRequestTimeout, saveUninitializedSwaps: this.options.saveUninitializedSwaps, bitcoinNetwork: this._btcNetwork } ); } // This is gated on the default version of the contracts if(swapContract.supportsInitWithoutClaimer) { wrappers[SwapType.FROM_BTCLN_AUTO] = new FromBTCLNAutoWrapper<T[InputKey]>( key, unifiedSwapStorage, unifiedChainEvents, chainInterface, pricing, this._tokens[chainId], versions, lightningApi, this.messenger, lpApi, { getRequestTimeout: this.options.getRequestTimeout, postRequestTimeout: this.options.postRequestTimeout, saveUninitializedSwaps: this.options.saveUninitializedSwaps, unsafeSkipLnNodeCheck: this.bitcoinNetwork===BitcoinNetwork.TESTNET4 || this.bitcoinNetwork===BitcoinNetwork.REGTEST } ); } Object.keys(wrappers).forEach(key => wrappers[key].events.on("swapState", this.swapStateListener)); const reviver = (val: any) => { const wrapper: ISwapWrapper<any, any> = wrappers[val.type]; if(wrapper==null) return null; return new wrapper._swapDeserializer(wrapper, val); }; return { chainEvents, chainInterface, wrappers, unifiedChainEvents, unifiedSwapStorage, defaultVersion, reviver, versionedContracts } }); const contracts = objectMap(chainsData, (data) => data.versions ?? {[data.defaultVersion ?? "v1"]: {swapContract: data.swapContract, spvVaultContract: data.spvVaultContract}}); if(options.intermediaryUrl!=null) { this.intermediaryDiscovery = new IntermediaryDiscovery(contracts, lpApi, options.registryUrl, Array.isArray(options.intermediaryUrl) ? options.intermediaryUrl : [options.intermediaryUrl], options.getRequestTimeout); } else { this.intermediaryDiscovery = new IntermediaryDiscovery(contracts, lpApi, options.registryUrl, undefined, options.getRequestTimeout); } this.intermediaryDiscovery.on("removed", (intermediaries: Intermediary[]) => { this.emit("lpsRemoved", intermediaries); }); this.intermediaryDiscovery.on("added", (intermediaries: Intermediary[]) => { this.emit("lpsAdded", intermediaries); }); } private async _init(): Promise<void> { this.logger.debug("init(): Initializing swapper"); const abortController = new AbortController(); const promises: Promise<void>[] = []; let automaticClockDriftCorrectionPromise: Promise<void> | undefined = undefined; if(this.options.automaticClockDriftCorrection) { promises.push(automaticClockDriftCorrectionPromise = tryWithRetries(correctClock, undefined, undefined, abortController.signal).catch((err) => { abortController.abort(err); })); } this.logger.debug("init(): Initializing intermediary discovery"); if(!this.options.dontFetchLPs) promises.push(this.intermediaryDiscovery.init(abortController.signal).catch(err => { if(abortController.signal.aborted) return; this.logger.error("init(): Failed to fetch intermediaries/LPs: ", err); })); if(this.options.defaultTrustedIntermediaryUrl!=null) { promises.push( this.intermediaryDiscovery.getIntermediary(this.options.defaultTrustedIntermediaryUrl, abortController.signal) .then(val => { if(val==null) throw new Error("Cannot get trusted LP"); this.defaultTrustedIntermediary = val; }) .catch(err => { if(abortController.signal.aborted) return; this.logger.error("init(): Failed to contact trusted LP url: ", err); }) ); } if(automaticClockDriftCorrectionPromise!=null) { //We should await the promises here before checking the swaps await automaticClockDriftCorrectionPromise; } const chainPromises = []; for(let chainIdentifier in this._chains) { chainPromises.push((async() => { const { chainInterface, versionedContracts, unifiedChainEvents, unifiedSwapStorage, wrappers, reviver } = this._chains[chainIdentifier]; try { const _chainInterface: any = chainInterface; if(_chainInterface.verifyNetwork!=null) { await _chainInterface.verifyNetwork(this.bitcoinNetwork); } for(let contractVersion in versionedContracts) { await versionedContracts[contractVersion].swapContract.start(); this.logger.debug("init(): Intialized swap contract: "+chainIdentifier+` version: ${contractVersion}`); } await unifiedSwapStorage.init(); if(unifiedSwapStorage.storage instanceof IndexedDBUnifiedStorage) { //Try to migrate the data here const storagePrefix = chainIdentifier==="SOLANA" ? "SOLv4-"+this.bitcoinNetwork+"-Swaps-" : "atomiqsdk-"+this.bitcoinNetwork+chainIdentifier+"-Swaps-"; await unifiedSwapStorage.storage.tryMigrate( [ [storagePrefix+"FromBTC", SwapType.FROM_BTC], [storagePrefix+"FromBTCLN", SwapType.FROM_BTCLN], [storagePrefix+"ToBTC", SwapType.TO_BTC], [storagePrefix+"ToBTCLN", SwapType.TO_BTCLN] ], (obj: any) => { const swap = reviver(obj); if(swap._randomNonce==null) { const oldIdentifierHash = swap.getId(); swap._randomNonce = randomBytes(16).toString("hex"); const newIdentifierHash = swap.getId(); this.logger.info("init(): Found older swap version without randomNonce, replacing, old hash: "+oldIdentifierHash+ " new hash: "+newIdentifierHash); } return swap; } ) } await unifiedChainEvents.start(this.options.noEvents); this.logger.debug("init(): Initialized events: "+chainIdentifier); } catch (e) { if(!this.options.gracefullyHandleChainErrors) throw e; this.logger.error(`init(): Failed to initialize ${chainIdentifier} (skipped): `, e); delete this._chains[chainIdentifier]; return; } for(let key in wrappers) { // this.logger.debug("init(): Initializing "+SwapType[key]+": "+chainIdentifier); await wrappers[key as unknown as SwapType].init(this.options.noTimers, this.options.dontCheckPastSwaps); } })()); } await Promise.all(chainPromises); await Promise.all(promises); this.logger.debug("init(): Initializing messenger"); await this.messenger.init(); } private initPromise?: Promise<void>; private initialized: boolean = false; /** * Initializes the swap storage and loads existing swaps, needs to be called before any other action */ async init(): Promise<void> { if(this.initialized) return; if(this.initPromise!=null) { await this.initPromise; return; } try { const promise = this._init(); this.initPromise = promise; await promise; delete this.initPromise; this.initialized = true; } catch (e) { delete this.initPromise; throw e; } } /** * Whether the SDK is initialized (after {@link init} is called) */ isInitialized(): boolean { return this.initialized; } /** * Stops listening for onchain events and closes this Swapper instance */ async stop() { if(this.initPromise) await this.initPromise; for(let chainIdentifier in this._chains) { const { wrappers, unifiedChainEvents } = this._chains[chainIdentifier]; for(let key in wrappers) { const wrapper = wrappers[key as unknown as SwapType]; wrapper.events.removeListener("swapState", this.swapStateListener); await wrapper.stop(); } await unifiedChainEvents.stop(); await this.messenger.stop(); } this.initialized = false; } /** * Creates swap & handles intermediary, quote selection * * @param chainIdentifier * @param create Callback to create the * @param amountData Amount data as passed to the function * @param swapType Swap type of the execution * @param maxWaitTimeMS Maximum waiting time after the first intermediary returns the quote * @private * @throws {Error} when no intermediary was found * @throws {Error} if the chain with the provided identifier cannot be found */ private async createSwap<ChainIdentifier extends ChainIds<T>, S extends ISwap<T[ChainIdentifier]>>( chainIdentifier: ChainIdentifier, create: (candidates: Intermediary[], abortSignal: AbortSignal, chain: ChainSpecificData<T[ChainIdentifier]>) => Promise<{ quote: Promise<S>, intermediary: Intermediary }[]>, amountData: { amount?: bigint, token: string, exactIn: boolean }, swapType: SwapType, maxWaitTimeMS: number = 2000 ): Promise<S> { if(!this.initialized) throw new Error("Swapper not initialized, init first with swapper.init()!"); if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier); let candidates: Intermediary[]; const inBtc: boolean = swapType===SwapType.TO_BTCLN || swapType===SwapType.TO_BTC ? !amountData.exactIn : amountData.exactIn; if(!inBtc || amountData.amount==null) { //Get candidates not based on the amount candidates = this.intermediaryDiscovery.getSwapCandidates(chainIdentifier, swapType, amountData.token); } else { candidates = this.intermediaryDiscovery.getSwapCandidates(chainIdentifier, swapType, amountData.token, amountData.amount); } let swapLimitsChanged = false; if(candidates.length===0) { this.logger.warn("createSwap(): No valid intermediary found to execute the swap with, reloading intermediary database..."); await this.intermediaryDiscovery.reloadIntermediaries(); swapLimitsChanged = true; if(!inBtc || amountData.amount==null) { //Get candidates not based on the amount candidates = this.intermediaryDiscovery.getSwapCandidates(chainIdentifier, swapType, amountData.token); } else { candidates = this.intermediaryDiscovery.getSwapCandidates(chainIdentifier, swapType, amountData.token, amountData.amount); if(candidates.length===0) { const min = this.intermediaryDiscovery.getSwapMinimum(chainIdentifier, swapType, amountData.token); const max = this.intermediaryDiscovery.getSwapMaximum(chainIdentifier, swapType, amountData.token); if(min!=null && max!=null) { if(amountData.amount < BigInt(min)) throw new OutOfBoundsError("Swap amount too low! Try swapping a higher amount.", 200, BigInt(min), BigInt(max)); if(amountData.amount > BigInt(max)) throw new OutOfBoundsError("Swap amount too high! Try swapping a lower amount.", 200, BigInt(min), BigInt(max)); } } } if(candidates.length===0) throw new Error("No intermediary found for the requested pair and amount! You can try swapping different pair or higher/lower amount."); } const abortController = new AbortController(); this.logger.debug("createSwap() Swap candidates: ", candidates.map(lp => lp.url).join()); const quotePromises: {quote: Promise<S>, intermediary: Intermediary}[] = await create(candidates, abortController.signal, this._chains[chainIdentifier]); const promiseAll = new Promise<{ quote: S, intermediary: Intermediary }[]>((resolve, reject) => { let min: bigint; let max: bigint; let error: Error; let numResolved = 0; let quotes: { quote: S, intermediary: Intermediary }[] = []; let timeout: NodeJS.Timeout; quotePromises.forEach(data => { data.quote.then(quote => { if(numResolved===0) { timeout = setTimeout(() => { abortController.abort(new Error("Timed out waiting for quote!")); resolve(quotes); }, maxWaitTimeMS); } numResolved++; quotes.push({ quote, intermediary: data.intermediary }); if(numResolved===quotePromises.length) { clearTimeout(timeout); resolve(quotes); return; } }).catch(e => { numResolved++; if(e instanceof IntermediaryError) { //Blacklist that node this.intermediaryDiscovery.removeIntermediary(data.intermediary); swapLimitsChanged = true; } else if(e instanceof OutOfBoundsError) { if(min==null || max==null) { min = e.min; max = e.max; } else { min = bigIntMin(min, e.min); max = bigIntMax(max, e.max); } data.intermediary.swapBounds[swapType] ??= {}; data.intermediary.swapBounds[swapType]![chainIdentifier] ??= {}; const tokenBoundsData = (data.intermediary.swapBounds[swapType]![chainIdentifier]![amountData.token] ??= {input: {}, output: {}}); if(amountData.exactIn) { tokenBoundsData.input = {min: e.min, max: e.max}; } else { tokenBoundsData.output = {min: e.min, max: e.max}; } swapLimitsChanged = true; } this.logger.warn("createSwap(): Intermediary "+data.intermediary.url+" error: ", e); error = e; if(numResolved===quotePromises.length) { if(timeout!=null) clearTimeout(timeout); if(quotes.length>0) { resolve(quotes); return; } if(min!=null && max!=null) { let msg = "Swap amount too high or too low! Try swapping a different amount."; if(amountData.amount!=null) { if(min > amountData.amount) msg = "Swap amount too low! Try swapping a higher amount."; if(max < amountData.amount) msg = "Swap amount too high! Try swapping a lower amount."; } reject(new OutOfBoundsError(msg, 400, min, max)); return; } reject(error); } }); }); }); try { const quotes = await promiseAll; //TODO: Intermediary's reputation is not taken into account! quotes.sort((a, b) => { if(amountData.exactIn) { //Compare outputs return bigIntCompare(b.quote.getOutput().rawAmount!, a.quote.getOutput().rawAmount!); } else { //Compare inputs return bigIntCompare(a.quote.getInput().rawAmount!, b.quote.getInput().rawAmount!); } }); this.logger.debug("createSwap(): Sorted quotes, best price to worst: ", quotes); if(swapLimitsChanged) this.emit("swapLimitsChanged"); const quote = quotes[0].quote; await quote._save(); return quote; } catch (e) { if(swapLimitsChanged) this.emit("swapLimitsChanged"); throw e; } } /** * Creates Smart chain -> Bitcoin ({@link SwapType.TO_BTC}) swap * * @param chainIdentifier Chain identifier string of the source smart chain * @param signer Signer's address on the source chain * @param tokenAddress Token address to pay with * @param address Recipient's bitcoin address * @param amount Amount to send in token based units (if `exactIn=true`) or receive in satoshis (if `exactIn=false`) * @param exactIn Whether to use exact in instead of exact out * @param additionalParams Additional parameters sent to the LP when creating the swap * @param options Additional options for the swap */ createToBTCSwap<ChainIdentifier extends ChainIds<T>>( chainIdentifier: ChainIdentifier, signer: string, tokenAddress: string, address: string, amount: bigint, exactIn: boolean = false, additionalParams: Record<string, any> | undefined = this.options.defaultAdditionalParameters, options?: ToBTCOptions ): Promise<ToBTCSwap<T[ChainIdentifier]>> { if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier); if(address.startsWith("bitcoin:")) { address = address.substring(8).split("?")[0]; } if(!this.Utils.isValidBitcoinAddress(address)) throw new Error("Invalid bitcoin address"); if(!this._chains[chainIdentifier].chainInterface.isValidAddress(signer, true)) throw new Error("Invalid "+chainIdentifier+" address"); signer = this._chains[chainIdentifier].chainInterface.normalizeAddress(signer); const amountData = { amount, token: tokenAddress, exactIn }; return this.createSwap( chainIdentifier as ChainIdentifier, (candidates: Intermediary[], abortSignal, chain) => Promise.resolve(chain.wrappers[SwapType.TO_BTC].create( signer, address, amountData, candidates, options, additionalParams, abortSignal )), amountData, SwapType.TO_BTC ); } /** * Creates Smart chain -> Bitcoin Lightning ({@link SwapType.TO_BTCLN}) swap * * @param chainIdentifier Chain identifier string of the source smart chain * @param signer Signer's address on the source chain * @param tokenAddress Token address to pay with * @param paymentRequest BOLT11 lightning network invoice to be paid (needs to have a fixed amount), and the swap * amount is taken from this fixed amount, hence only exact output swaps are supported * @param additionalParams Additional parameters sent to the LP when creating the swap * @param options Additional options for the swap */ async createToBTCLNSwap<ChainIdentifier extends ChainIds<T>>( chainIdentifier: ChainIdentifier, signer: string, tokenAddress: string, paymentRequest: string, additionalParams: Record<string, any> | undefined = this.options.defaultAdditionalParameters, options?: ToBTCLNOptions ): Promise<ToBTCLNSwap<T[ChainIdentifier]>> { if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier); if(paymentRequest.startsWith("lightning:")) paymentRequest = paymentRequest.substring(10); if(!this.Utils.isValidLightningInvoice(paymentRequest)) throw new Error("Invalid lightning network invoice"); if(!this._chains[chainIdentifier].chainInterface.isValidAddress(signer, true)) throw new Error("Invalid "+chainIdentifier+" address"); signer = this._chains[chainIdentifier].chainInterface.normalizeAddress(signer); const parsedPR = bolt11Decode(paymentRequest); if(parsedPR.millisatoshis==null) throw new Error("Invalid lightning network invoice, no msat value field!"); const amountData = { amount: (BigInt(parsedPR.millisatoshis) + 999n) / 1000n, token: tokenAddress, exactIn: false as const }; return this.createSwap( chainIdentifier as ChainIdentifier, (candidates: Intermediary[], abortSignal: AbortSignal, chain) => chain.wrappers[SwapType.TO_BTCLN].create( signer, paymentRequest, amountData, candidates, options, additionalParams, abortSignal ), amountData, SwapType.TO_BTCLN ); } /** * Creates Smart chain -> Bitcoin Lightning ({@link SwapType.TO_BTCLN}) swap via LNURL-pay link * * @param chainIdentifier Chain identifier string of the source smart chain * @param signer Signer's address on the source chain * @param tokenAddress Token address to pay with * @param lnurlPay LNURL-pay link to use for the payment * @param amount Amount to send in token based units (if `exactIn=true`) or receive in satoshis (if `exactIn=false`) * @param exactIn Whether to do an exact in swap instead of exact out * @param additionalParams Additional parameters sent to the LP when creating the swap * @param options Additional options for the swap */ async createToBTCLNSwapViaLNURL<ChainIdentifier extends ChainIds<T>>( chainIdentifier: ChainIdentifier, signer: string, tokenAddress: string, lnurlPay: string | LNURLPay, amount: bigint, exactIn: boolean = false, additionalParams: Record<string, any> | undefined = this.options.defaultAdditionalParameters, options?: ToBTCLNOptions & {comment?: string} ): Promise<ToBTCLNSwap<T[ChainIdentifier]>> { if(this._chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier); if(typeof(lnurlPay)==="string" && !this.Utils.isValidLNURL(lnurlPay)) throw new Error("Invalid LNURL-pay link"); if(!this._chains[chainIdentifier].chainInterface.isValidAddress(signer, true)) throw new Error("Invalid "+chainIdentifier+" address"); signer = this._chains[chainIdentifier].chainInterface.normalizeAddress(signer); const amountData = { amount, token: tokenAddress, exactIn }; return this.createSwap( chainIdentifier as ChainIdentifier, (candidates: Intermediary[], abortSignal: AbortSignal, chain) => chain.wrappers[SwapType.TO_BTCLN].createViaLNURL( signer, typeof(lnurlPay)==="string" ? (lnurlPay.startsWith("lightning:") ? lnurlPay.substring(10): lnurlPay) : lnurlPay.params, amountData, candidates, options, additionalParams, abortSignal ), amountData, SwapType.TO_BTCLN ); } /** * Creates Smart chain -> Bitcoin Lightning ({@link SwapType.TO_BTCLN}) swap via {@link LightningInvoiceCreateService} * * @param chainIdentifier Chain identifier string of the source smart chain * @param signer Signer's address on the source chain * @param tokenAddress Token address to pay with * @param service Invoice create service object which facilitates the creation of fixed amount LN invoices * @param amount Amount to send in token based units (if `exactIn=true`) or receive in satoshis (if `exactIn=false`) * @param exactIn Whether to do an exact in swap instead