UNPKG

@atomiqlabs/sdk-lib

Version:

Basic SDK functionality library for atomiq

1,162 lines (1,076 loc) 100 kB
import {ISwapPrice} from "../../prices/abstract/ISwapPrice"; import { BitcoinNetwork, BtcRelay, ChainData, ChainSwapType, ChainType, Messenger, RelaySynchronizer } from "@atomiqlabs/base"; import { InvoiceCreateService, isInvoiceCreateService, ToBTCLNOptions, ToBTCLNWrapper } from "../escrow_swaps/tobtc/ln/ToBTCLNWrapper"; import {ToBTCOptions, ToBTCWrapper} from "../escrow_swaps/tobtc/onchain/ToBTCWrapper"; import {FromBTCLNOptions, FromBTCLNWrapper} from "../escrow_swaps/frombtc/ln/FromBTCLNWrapper"; import {FromBTCOptions, FromBTCWrapper} from "../escrow_swaps/frombtc/onchain/FromBTCWrapper"; import {IntermediaryDiscovery, MultichainSwapBounds, SwapBounds} from "../../intermediaries/IntermediaryDiscovery"; import {decode as bolt11Decode} from "@atomiqlabs/bolt11"; import {ISwap} from "../ISwap"; import {IntermediaryError} from "../../errors/IntermediaryError"; import {SwapType} from "../enums/SwapType"; import {FromBTCLNSwap} from "../escrow_swaps/frombtc/ln/FromBTCLNSwap"; import {FromBTCSwap} from "../escrow_swaps/frombtc/onchain/FromBTCSwap"; import {ToBTCLNSwap} from "../escrow_swaps/tobtc/ln/ToBTCLNSwap"; import {ToBTCSwap} from "../escrow_swaps/tobtc/onchain/ToBTCSwap"; import {MempoolApi} from "../../btc/mempool/MempoolApi"; import {MempoolBitcoinRpc} from "../../btc/mempool/MempoolBitcoinRpc"; import {MempoolBtcRelaySynchronizer} from "../../btc/mempool/synchronizer/MempoolBtcRelaySynchronizer"; import {LnForGasWrapper} from "../trusted/ln/LnForGasWrapper"; import {LnForGasSwap} from "../trusted/ln/LnForGasSwap"; import {EventEmitter} from "events"; import {MempoolBitcoinBlock} from "../../btc/mempool/MempoolBitcoinBlock"; import {Intermediary} from "../../intermediaries/Intermediary"; import {isLNURLPay, isLNURLWithdraw, LNURLPay, LNURLWithdraw} from "../../utils/LNURL"; import {AmountData, ISwapWrapper, WrapperCtorTokens} from "../ISwapWrapper"; import { bigIntCompare, bigIntMax, bigIntMin, getLogger, objectMap, randomBytes, tryWithRetries } from "../../utils/Utils"; import {OutOfBoundsError} from "../../errors/RequestError"; import {SwapperWithChain} from "./SwapperWithChain"; import { BitcoinTokens, BtcToken, fromDecimal, isBtcToken, isSCToken, SCToken, Token, TokenAmount, toTokenAmount } from "../../Tokens"; import {OnchainForGasSwap} from "../trusted/onchain/OnchainForGasSwap"; import {OnchainForGasWrapper} from "../trusted/onchain/OnchainForGasWrapper"; import {BTC_NETWORK, NETWORK, TEST_NETWORK} from "@scure/btc-signer/utils"; import {IUnifiedStorage, QueryParams} from "../../storage/IUnifiedStorage"; import {IndexedDBUnifiedStorage} from "../../browser-storage/IndexedDBUnifiedStorage"; import { UnifiedSwapStorage, UnifiedSwapStorageCompositeIndexes, UnifiedSwapStorageIndexes } from "../../storage/UnifiedSwapStorage"; import {UnifiedSwapEventListener} from "../../events/UnifiedSwapEventListener"; import {IToBTCSwap} from "../escrow_swaps/tobtc/IToBTCSwap"; import {SpvFromBTCOptions, SpvFromBTCWrapper} from "../spv_swaps/SpvFromBTCWrapper"; import {SpvFromBTCSwap} from "../spv_swaps/SpvFromBTCSwap"; import {SwapperUtils} from "./utils/SwapperUtils"; import {FromBTCLNAutoOptions, FromBTCLNAutoWrapper} from "../escrow_swaps/frombtc/ln_auto/FromBTCLNAutoWrapper"; import {FromBTCLNAutoSwap} from "../escrow_swaps/frombtc/ln_auto/FromBTCLNAutoSwap"; import {UserError} from "../../errors/UserError"; import {SwapAmountType} from "../enums/SwapAmountType"; import {IClaimableSwap} from "../IClaimableSwap"; import {correctClock} from "../../utils/AutomaticClockDriftCorrection"; export type SwapperOptions = { intermediaryUrl?: string | string[], registryUrl?: string, bitcoinNetwork?: BitcoinNetwork, getRequestTimeout?: number, postRequestTimeout?: number, defaultAdditionalParameters?: {[key: string]: any}, storagePrefix?: string defaultTrustedIntermediaryUrl?: string, swapStorage?: <T extends ChainType>(chainId: T["ChainId"]) => IUnifiedStorage<UnifiedSwapStorageIndexes, UnifiedSwapStorageCompositeIndexes>, noTimers?: boolean, noEvents?: boolean, noSwapCache?: boolean, dontCheckPastSwaps?: boolean, dontFetchLPs?: boolean, saveUninitializedSwaps?: boolean, //automatically persist all created swaps - by default only initiated swaps are persisted automaticClockDriftCorrection?: boolean }; export type MultiChain = { [chainIdentifier in string]: ChainType; }; export 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"], swapContract: T["Contract"], spvVaultContract: T["SpvVaultContract"], chainInterface: T["ChainInterface"], btcRelay: BtcRelay<any, T["TX"], MempoolBitcoinBlock, T["Signer"]>, synchronizer: RelaySynchronizer<any, T["TX"], MempoolBitcoinBlock>, unifiedChainEvents: UnifiedSwapEventListener<T>, unifiedSwapStorage: UnifiedSwapStorage<T>, reviver: (val: any) => ISwap<T> }; export type MultiChainData<T extends MultiChain> = { [chainIdentifier in keyof T]: ChainSpecificData<T[chainIdentifier]> }; export type CtorMultiChainData<T extends MultiChain> = { [chainIdentifier in keyof T]: ChainData<T[chainIdentifier]> }; export type ChainIds<T extends MultiChain> = keyof T & string; type NotNever<T> = [T] extends [never] ? false : true; 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"]>; export class Swapper<T extends MultiChain> extends EventEmitter<{ lpsRemoved: [Intermediary[]], lpsAdded: [Intermediary[]], swapState: [ISwap], swapLimitsChanged: [] }> { protected readonly logger = getLogger(this.constructor.name+": "); protected readonly swapStateListener: (swap: ISwap) => void; private defaultTrustedIntermediary: Intermediary; readonly chains: MultiChainData<T>; readonly prices: ISwapPrice<T>; readonly intermediaryDiscovery: IntermediaryDiscovery; readonly options: SwapperOptions; readonly mempoolApi: MempoolApi; readonly bitcoinRpc: MempoolBitcoinRpc; readonly bitcoinNetwork: BTC_NETWORK; private readonly _bitcoinNetwork: BitcoinNetwork; readonly tokens: { [chainId: string]: { [tokenAddress: string]: SCToken } }; readonly tokensByTicker: { [chainId: string]: { [tokenTicker: string]: SCToken } }; readonly Utils: SwapperUtils<T>; readonly messenger: Messenger; constructor( bitcoinRpc: MempoolBitcoinRpc, chainsData: CtorMultiChainData<T>, pricing: ISwapPrice<T>, tokens: WrapperCtorTokens<T>, messenger: Messenger, options?: SwapperOptions ) { super(); const storagePrefix = options?.storagePrefix ?? "atomiq-"; options.bitcoinNetwork = options.bitcoinNetwork==null ? BitcoinNetwork.TESTNET : options.bitcoinNetwork; options.swapStorage ??= (name: string) => new IndexedDBUnifiedStorage(name); this._bitcoinNetwork = options.bitcoinNetwork; this.bitcoinNetwork = 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.mempoolApi = bitcoinRpc.api; this.messenger = messenger; this.options = options; this.tokens = {}; this.tokensByTicker = {}; for(let tokenData of tokens) { for(let chainId in tokenData.chains) { const chainData = tokenData.chains[chainId]; this.tokens[chainId] ??= {}; this.tokensByTicker[chainId] ??= {}; this.tokens[chainId][chainData.address] = this.tokensByTicker[chainId][tokenData.ticker] = { chain: "SC", chainId, ticker: tokenData.ticker, name: tokenData.name, decimals: chainData.decimals, displayDecimals: chainData.displayDecimals, address: chainData.address } } } 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) => { const { swapContract, chainEvents, btcRelay, chainInterface, spvVaultContract, spvVaultWithdrawalDataConstructor } = chainData; const synchronizer = new MempoolBtcRelaySynchronizer(btcRelay, bitcoinRpc); const storageHandler = options.swapStorage(storagePrefix + chainData.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, swapContract, pricing, tokens, chainData.swapDataConstructor, { getRequestTimeout: options.getRequestTimeout, postRequestTimeout: options.postRequestTimeout, } ); wrappers[SwapType.TO_BTC] = new ToBTCWrapper<T[InputKey]>( key, unifiedSwapStorage, unifiedChainEvents, chainInterface, swapContract, pricing, tokens, chainData.swapDataConstructor, this.bitcoinRpc, { getRequestTimeout: options.getRequestTimeout, postRequestTimeout: options.postRequestTimeout, bitcoinNetwork: this.bitcoinNetwork } ); wrappers[SwapType.FROM_BTCLN] = new FromBTCLNWrapper<T[InputKey]>( key, unifiedSwapStorage, unifiedChainEvents, chainInterface, swapContract, pricing, tokens, chainData.swapDataConstructor, bitcoinRpc, { getRequestTimeout: options.getRequestTimeout, postRequestTimeout: options.postRequestTimeout, unsafeSkipLnNodeCheck: this._bitcoinNetwork===BitcoinNetwork.TESTNET4 || this._bitcoinNetwork===BitcoinNetwork.REGTEST } ); wrappers[SwapType.FROM_BTC] = new FromBTCWrapper<T[InputKey]>( key, unifiedSwapStorage, unifiedChainEvents, chainInterface, swapContract, pricing, tokens, chainData.swapDataConstructor, btcRelay, synchronizer, this.bitcoinRpc, { getRequestTimeout: options.getRequestTimeout, postRequestTimeout: options.postRequestTimeout, bitcoinNetwork: this.bitcoinNetwork } ); wrappers[SwapType.TRUSTED_FROM_BTCLN] = new LnForGasWrapper<T[InputKey]>( key, unifiedSwapStorage, unifiedChainEvents, chainInterface, pricing, tokens, { getRequestTimeout: options.getRequestTimeout, postRequestTimeout: options.postRequestTimeout } ); wrappers[SwapType.TRUSTED_FROM_BTC] = new OnchainForGasWrapper<T[InputKey]>( key, unifiedSwapStorage, unifiedChainEvents, chainInterface, pricing, tokens, bitcoinRpc, { getRequestTimeout: options.getRequestTimeout, postRequestTimeout: options.postRequestTimeout, bitcoinNetwork: this.bitcoinNetwork } ); if(spvVaultContract!=null) { wrappers[SwapType.SPV_VAULT_FROM_BTC] = new SpvFromBTCWrapper<T[InputKey]>( key, unifiedSwapStorage, unifiedChainEvents, chainInterface, spvVaultContract, pricing, tokens, spvVaultWithdrawalDataConstructor, btcRelay, synchronizer, bitcoinRpc, { getRequestTimeout: options.getRequestTimeout, postRequestTimeout: options.postRequestTimeout, bitcoinNetwork: this.bitcoinNetwork } ); } if(swapContract.supportsInitWithoutClaimer) { wrappers[SwapType.FROM_BTCLN_AUTO] = new FromBTCLNAutoWrapper<T[InputKey]>( key, unifiedSwapStorage, unifiedChainEvents, chainInterface, swapContract, pricing, tokens, chainData.swapDataConstructor, bitcoinRpc, this.messenger, { getRequestTimeout: options.getRequestTimeout, postRequestTimeout: options.postRequestTimeout, 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 = wrappers[val.type]; if(wrapper==null) return null; return new wrapper.swapDeserializer(wrapper, val); }; return { chainEvents, spvVaultContract, swapContract, chainInterface, btcRelay, synchronizer, wrappers, unifiedChainEvents, unifiedSwapStorage, reviver } }); const contracts = objectMap(chainsData, (data) => data.swapContract); if(options.intermediaryUrl!=null) { this.intermediaryDiscovery = new IntermediaryDiscovery(contracts, options.registryUrl, Array.isArray(options.intermediaryUrl) ? options.intermediaryUrl : [options.intermediaryUrl], options.getRequestTimeout); } else { this.intermediaryDiscovery = new IntermediaryDiscovery(contracts, options.registryUrl, null, 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, sdk-lib version 16.1.3"); const abortController = new AbortController(); const promises: Promise<void>[] = []; let automaticClockDriftCorrectionPromise: Promise<void>; 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 => { 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 { swapContract, unifiedChainEvents, unifiedSwapStorage, wrappers, reviver } = this.chains[chainIdentifier]; await swapContract.start(); this.logger.debug("init(): Intialized swap contract: "+chainIdentifier); 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; } ) } if(!this.options.noEvents) await unifiedChainEvents.start(); this.logger.debug("init(): Intialized events: "+chainIdentifier); for(let key in wrappers) { // this.logger.debug("init(): Initializing "+SwapType[key]+": "+chainIdentifier); await wrappers[key].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; try { const promise = this._init(); this.initPromise = promise; await promise; delete this.initPromise; this.initialized = true; } catch (e) { delete this.initPromise; throw e; } } /** * 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) { (wrappers[key] as ISwapWrapper<any, any>).events.removeListener("swapState", this.swapStateListener); await wrappers[key].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: AmountData, 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) { //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, reloading intermediary database..."); await this.intermediaryDiscovery.reloadIntermediaries(); swapLimitsChanged = true; if(!inBtc) { //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("Amount too low!", 200, BigInt(min), BigInt(max)); if(amountData.amount > BigInt(max)) throw new OutOfBoundsError("Amount too high!", 200, BigInt(min), BigInt(max)); } } } if(candidates.length===0) throw new Error("No intermediary found!"); } 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: null, output: null}); 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) { reject(new OutOfBoundsError("Out of bounds", 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; if(this.options.saveUninitializedSwaps) { quote._setInitiated(); await quote._save(); } return quote; } catch (e) { if(swapLimitsChanged) this.emit("swapLimitsChanged"); throw e; } } /** * Creates To BTC swap * * @param chainIdentifier * @param signer * @param tokenAddress Token address to pay with * @param address Recipient's bitcoin address * @param amount Amount to send in satoshis (bitcoin's smallest denomination) * @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 */ createToBTCSwap<ChainIdentifier extends ChainIds<T>>( chainIdentifier: ChainIdentifier, signer: string, tokenAddress: string, address: string, amount: bigint, exactIn?: boolean, additionalParams: Record<string, any> = 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); options ??= {}; options.confirmationTarget ??= 3; options.confirmations ??= 2; 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 To BTCLN swap * * @param chainIdentifier * @param signer * @param tokenAddress Token address to pay with * @param paymentRequest BOLT11 lightning network invoice to be paid (needs to have a fixed amount) * @param additionalParams Additional parameters sent to the LP when creating the swap * @param options */ async createToBTCLNSwap<ChainIdentifier extends ChainIds<T>>( chainIdentifier: ChainIdentifier, signer: string, tokenAddress: string, paymentRequest: string, additionalParams: Record<string, any> = this.options.defaultAdditionalParameters, options?: ToBTCLNOptions ): Promise<ToBTCLNSwap<T[ChainIdentifier]>> { if(this.chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier); options ??= {}; 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); const amountData = { amount: (BigInt(parsedPR.millisatoshis) + 999n) / 1000n, token: tokenAddress, exactIn: false }; options.expirySeconds ??= 5*24*3600; 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 To BTCLN swap via LNURL-pay * * @param chainIdentifier * @param signer * @param tokenAddress Token address to pay with * @param lnurlPay LNURL-pay link to use for the payment * @param amount Amount to be paid in sats * @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 */ async createToBTCLNSwapViaLNURL<ChainIdentifier extends ChainIds<T>>( chainIdentifier: ChainIdentifier, signer: string, tokenAddress: string, lnurlPay: string | LNURLPay, amount: bigint, exactIn?: boolean, additionalParams: Record<string, any> = 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); options ??= {}; const amountData = { amount, token: tokenAddress, exactIn }; options.expirySeconds ??= 5*24*3600; 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 To BTCLN swap via InvoiceCreationService * * @param chainIdentifier * @param signer * @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 be paid in sats * @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 */ async createToBTCLNSwapViaInvoiceCreateService<ChainIdentifier extends ChainIds<T>>( chainIdentifier: ChainIdentifier, signer: string, tokenAddress: string, service: InvoiceCreateService, amount: bigint, exactIn?: boolean, additionalParams: Record<string, any> = this.options.defaultAdditionalParameters, options?: ToBTCLNOptions ): Promise<ToBTCLNSwap<T[ChainIdentifier]>> { if(this.chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier); if(!this.chains[chainIdentifier].chainInterface.isValidAddress(signer, true)) throw new Error("Invalid "+chainIdentifier+" address"); signer = this.chains[chainIdentifier].chainInterface.normalizeAddress(signer); options ??= {}; const amountData = { amount, token: tokenAddress, exactIn }; options.expirySeconds ??= 5*24*3600; return this.createSwap( chainIdentifier as ChainIdentifier, (candidates: Intermediary[], abortSignal: AbortSignal, chain) => chain.wrappers[SwapType.TO_BTCLN].createViaInvoiceCreateService( signer, Promise.resolve(service), amountData, candidates, options, additionalParams, abortSignal ), amountData, SwapType.TO_BTCLN ); } /** * Creates From BTC swap * * @param chainIdentifier * @param signer * @param tokenAddress Token address to receive * @param amount Amount to receive, in satoshis (bitcoin's smallest denomination) * @param exactOut Whether to use a exact out instead of exact in * @param additionalParams Additional parameters sent to the LP when creating the swap * @param options */ async createFromBTCSwapNew<ChainIdentifier extends ChainIds<T>>( chainIdentifier: ChainIdentifier, signer: string, tokenAddress: string, amount: bigint, exactOut?: boolean, additionalParams: Record<string, any> = this.options.defaultAdditionalParameters, options?: SpvFromBTCOptions ): Promise<SpvFromBTCSwap<T[ChainIdentifier]>> { if(this.chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier); 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: !exactOut }; return this.createSwap( chainIdentifier as ChainIdentifier, (candidates: Intermediary[], abortSignal: AbortSignal, chain) => Promise.resolve(chain.wrappers[SwapType.SPV_VAULT_FROM_BTC].create( signer, amountData, candidates, options, additionalParams, abortSignal )), amountData, SwapType.SPV_VAULT_FROM_BTC ); } /** * Creates From BTC swap * * @param chainIdentifier * @param signer * @param tokenAddress Token address to receive * @param amount Amount to receive, in satoshis (bitcoin's smallest denomination) * @param exactOut Whether to use a exact out instead of exact in * @param additionalParams Additional parameters sent to the LP when creating the swap * @param options */ async createFromBTCSwap<ChainIdentifier extends ChainIds<T>>( chainIdentifier: ChainIdentifier, signer: string, tokenAddress: string, amount: bigint, exactOut?: boolean, additionalParams: Record<string, any> = this.options.defaultAdditionalParameters, options?: FromBTCOptions ): Promise<FromBTCSwap<T[ChainIdentifier]>> { if(this.chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier); 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: !exactOut }; return this.createSwap( chainIdentifier as ChainIdentifier, (candidates: Intermediary[], abortSignal: AbortSignal, chain) => Promise.resolve(chain.wrappers[SwapType.FROM_BTC].create( signer, amountData, candidates, options, additionalParams, abortSignal )), amountData, SwapType.FROM_BTC ); } /** * Creates From BTCLN swap * * @param chainIdentifier * @param signer * @param tokenAddress Token address to receive * @param amount Amount to receive, in satoshis (bitcoin's smallest denomination) * @param exactOut Whether to use exact out instead of exact in * @param additionalParams Additional parameters sent to the LP when creating the swap * @param options */ async createFromBTCLNSwap<ChainIdentifier extends ChainIds<T>>( chainIdentifier: ChainIdentifier, signer: string, tokenAddress: string, amount: bigint, exactOut?: boolean, additionalParams: Record<string, any> = this.options.defaultAdditionalParameters, options?: FromBTCLNOptions ): Promise<FromBTCLNSwap<T[ChainIdentifier]>> { if(this.chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier); 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: !exactOut }; return this.createSwap( chainIdentifier as ChainIdentifier, (candidates: Intermediary[], abortSignal: AbortSignal, chain) => Promise.resolve(chain.wrappers[SwapType.FROM_BTCLN].create( signer, amountData, candidates, options, additionalParams, abortSignal )), amountData, SwapType.FROM_BTCLN ); } /** * Creates From BTCLN swap, withdrawing from LNURL-withdraw * * @param chainIdentifier * @param signer * @param tokenAddress Token address to receive * @param lnurl LNURL-withdraw to pull the funds from * @param amount Amount to receive, in satoshis (bitcoin's smallest denomination) * @param exactOut Whether to use exact out instead of exact in * @param additionalParams Additional parameters sent to the LP when creating the swap */ async createFromBTCLNSwapViaLNURL<ChainIdentifier extends ChainIds<T>>( chainIdentifier: ChainIdentifier, signer: string, tokenAddress: string, lnurl: string | LNURLWithdraw, amount: bigint, exactOut?: boolean, additionalParams: Record<string, any> = this.options.defaultAdditionalParameters ): Promise<FromBTCLNSwap<T[ChainIdentifier]>> { if(this.chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier); if(typeof(lnurl)==="string" && !this.Utils.isValidLNURL(lnurl)) throw new Error("Invalid LNURL-withdraw 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: !exactOut }; return this.createSwap( chainIdentifier as ChainIdentifier, (candidates: Intermediary[], abortSignal: AbortSignal, chain) => chain.wrappers[SwapType.FROM_BTCLN].createViaLNURL( signer, typeof(lnurl)==="string" ? (lnurl.startsWith("lightning:") ? lnurl.substring(10): lnurl) : lnurl.params, amountData, candidates, additionalParams, abortSignal ), amountData, SwapType.FROM_BTCLN ); } /** * Creates From BTCLN swap using new protocol * * @param chainIdentifier * @param signer * @param tokenAddress Token address to receive * @param amount Amount to receive, in satoshis (bitcoin's smallest denomination) * @param exactOut Whether to use exact out instead of exact in * @param additionalParams Additional parameters sent to the LP when creating the swap * @param options */ async createFromBTCLNSwapNew<ChainIdentifier extends ChainIds<T>>( chainIdentifier: ChainIdentifier, signer: string, tokenAddress: string, amount: bigint, exactOut?: boolean, additionalParams: Record<string, any> = this.options.defaultAdditionalParameters, options?: FromBTCLNAutoOptions ): Promise<FromBTCLNAutoSwap<T[ChainIdentifier]>> { if(this.chains[chainIdentifier]==null) throw new Error("Invalid chain identifier! Unknown chain: "+chainIdentifier); 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: !exactOut }; return this.createSwap( chainIdentifier as ChainIdentifier, (candidates: Intermediary[], abortSignal: AbortSignal, chain) => Promise.resolve(chain.wrappers[SwapType.FROM_BTCLN_AUTO].create( signer, amountData, candidates, options, additionalParams, abortSignal )), amountData, SwapType.FROM_BTCLN_AUTO ); } /** * Creates From BTCLN swap using new protocol, withdrawing from LNURL-withdraw * * @param chainIdentifier * @param signer * @param tokenAddress Token address to receive * @param lnurl LNURL-withdraw to pull the funds from * @param amount Amount to receive, in satoshis (bitcoin's smallest denomination) * @param exactOut Whether to use exact out instead of exact in * @param additionalParams Additional parameters sent to the LP when creating the swap * @param options */