UNPKG

@atomiqlabs/sdk

Version:

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

852 lines (779 loc) 39.8 kB
import {decode as bolt11Decode, PaymentRequestObject, TagsObject} from "@atomiqlabs/bolt11"; import {ToBTCLNSwap, ToBTCLNSwapInit} from "./ToBTCLNSwap"; import {IToBTCDefinition, IToBTCWrapper} from "../IToBTCWrapper"; import {UserError} from "../../../../errors/UserError"; import {ChainSwapType, ChainType, SwapCommitState, SwapCommitStateType} from "@atomiqlabs/base"; import {Intermediary} from "../../../../intermediaries/Intermediary"; import {ISwapWrapperOptions, WrapperCtorTokens} from "../../../ISwapWrapper"; import {ISwapPrice} from "../../../../prices/abstract/ISwapPrice"; import {EventEmitter} from "events"; import {IntermediaryError} from "../../../../errors/IntermediaryError"; import {SwapType} from "../../../../enums/SwapType"; import {extendAbortController, mapArrayToObject, throwIfUndefined} from "../../../../utils/Utils"; import {IntermediaryAPI, ToBTCLNResponseType} from "../../../../intermediaries/apis/IntermediaryAPI"; import {RequestError} from "../../../../errors/RequestError"; import {LNURL, LNURLPaySuccessAction} from "../../../../lnurl/LNURL"; import {IToBTCSwapInit, ToBTCSwapState} from "../IToBTCSwap"; import {UnifiedSwapEventListener} from "../../../../events/UnifiedSwapEventListener"; import {UnifiedSwapStorage} from "../../../../storage/UnifiedSwapStorage"; import {ISwap} from "../../../ISwap"; import {sha256} from "@noble/hashes/sha2"; import {AmountData} from "../../../../types/AmountData"; import {LNURLPayParamsWithUrl} from "../../../../types/lnurl/LNURLPay"; import {tryWithRetries} from "../../../../utils/RetryUtils"; import {AllOptional} from "../../../../utils/TypeUtils"; import {LightningInvoiceCreateService} from "../../../../types/wallets/LightningInvoiceCreateService"; export type ToBTCLNOptions = { /** * HTLC expiration timeout in seconds to use when offering the HTLC to the LP. Larger expirations mean that more * lightning network payment paths can be considered (every hop in the lightning network payment adds additional * timeout requirement). On the other side, larger expiration also means that user's funds are locked for longer * in case of a non-cooperative LP. Might be extended up to `maxExpirySeconds` if the lightning invoice that is * being paid explictly requires longer timeouts due to `min_final_cltv_expiry` > 144. If the resulting expiration * is longer than the `expirySeconds` due to this, the swap will be flagged with * {@link ToBTCLNSwap.hasLongExpiration}. * * Uses 5 days as default. */ expirySeconds?: number, /** * Allow the increase of the default `expirySeconds` parameter up to the number of seconds specified here if * necessary, this is only applied to invoices with `min_final_cltv_expiry` > 144, this is usually the case when * sending lightning payments to systems with long potential settlement times (like Arkade or Bark). If the * resulting expiration is longer than the `expirySeconds` due to this, the swap will be flagged with * {@link ToBTCLNSwap.hasLongExpiration}. * * Uses 10 days as default. */ maxExpirySeconds?: number, /** * Maximum fee for routing the swap output payment through the lightning network. Higher fee percentages means that * more payment routes can be considered (every hop in the lightning network payment adds additional fee * requirements). * * The fee is express as percentage of the swap value, uses `0.2` by default which means the maximum * routing fee is capped at 0.2% of the swap value. * * The full fee also contains the base component (set by `maxRoutingBaseFee` option), the resulting maximum routing * fee rate is: * * `maxRoutingFee` = `maxRoutingBaseFee` sats + `value` * `maxRoutingFeePercentage`% */ maxRoutingFeePercentage?: number, /** * * Maximum base fee (in sats) for routing the swap output payment through the lightning network. Higher fee * percentages means that more payment routes can be considered (every hop in the lightning network payment adds additional fee * requirements). * * Uses 10 sats as a default. * * The full fee also contains the value percentage component (set by `maxRoutingFeePercentage` option), the * resulting maximum routing fee rate is: * * `maxRoutingFee` = `maxRoutingBaseFee` sats + (`value` * `maxRoutingFeePercentage`%) */ maxRoutingBaseFee?: bigint, /** * @deprecated Use `maxRoutingFeePercentage` and express the routing fee in percentage instead! */ maxRoutingPPM?: bigint, /** * @deprecated Adjust fee with `maxRoutingFeePercentage` & `maxRoutingBaseFee` params! */ maxFee?: bigint | Promise<bigint>, /** * @deprecated Pass desired HTLC expiration timeout as `expirySeconds` */ expiryTimestamp?: bigint } export type ToBTCLNWrapperOptions = ISwapWrapperOptions & { lightningBaseFee: number, lightningFeePPM: number, paymentTimeoutSeconds: number, maxPaymentTimeoutSeconds: number }; export type ToBTCLNDefinition<T extends ChainType> = IToBTCDefinition<T, ToBTCLNWrapper<T>, ToBTCLNSwap<T>>; /** * Escrow based (HTLC) swap for Smart chains -> Bitcoin lightning * * @category Swaps/Smart chain → Lightning */ export class ToBTCLNWrapper<T extends ChainType> extends IToBTCWrapper<T, ToBTCLNDefinition<T>, ToBTCLNWrapperOptions> { public readonly TYPE: SwapType.TO_BTCLN = SwapType.TO_BTCLN; /** * @internal */ readonly _swapDeserializer = ToBTCLNSwap; constructor( chainIdentifier: string, unifiedStorage: UnifiedSwapStorage<T>, unifiedChainEvents: UnifiedSwapEventListener<T>, chain: T["ChainInterface"], prices: ISwapPrice, tokens: WrapperCtorTokens, versionedContracts: { [version: string]: { swapContract: T["Contract"], swapDataConstructor: new (data: any) => T["Data"] } }, lpApi: IntermediaryAPI, options?: AllOptional<ToBTCLNWrapperOptions>, events?: EventEmitter<{swapState: [ISwap]}> ) { super( chainIdentifier, unifiedStorage, unifiedChainEvents, chain, prices, tokens, lpApi, { ...options, paymentTimeoutSeconds: options?.paymentTimeoutSeconds ?? 5*24*60*60, maxPaymentTimeoutSeconds: options?.maxPaymentTimeoutSeconds ?? 10*24*60*60, lightningBaseFee: options?.lightningBaseFee ?? 10, lightningFeePPM: options?.lightningFeePPM ?? 2000 }, versionedContracts, events ); } private toRequiredSwapOptions(amountData: AmountData, options?: ToBTCLNOptions, pricePreFetchPromise?: Promise<bigint | undefined>, abortSignal?: AbortSignal) { const expirySeconds = options?.expirySeconds ?? this._options.paymentTimeoutSeconds; const maxRoutingBaseFee = options?.maxRoutingBaseFee ?? BigInt(this._options.lightningBaseFee); const maxRoutingPPM = options?.maxRoutingFeePercentage!=null ? BigInt(Math.floor(options.maxRoutingFeePercentage * 10_000)) : options?.maxRoutingPPM ?? BigInt(this._options.lightningFeePPM); let maxFee: bigint | Promise<bigint>; if(options?.maxFee!=null) { maxFee = options.maxFee; } else if(amountData.exactIn) { if(pricePreFetchPromise!=null) { maxFee = pricePreFetchPromise .then(val => this._prices.getFromBtcSwapAmount(this.chainIdentifier, maxRoutingBaseFee, amountData.token, abortSignal, val)) .then(_maxBaseFee => this.calculateFeeForAmount(amountData.amount, _maxBaseFee, maxRoutingPPM)) } else { maxFee = this._prices.getFromBtcSwapAmount(this.chainIdentifier, maxRoutingBaseFee, amountData.token, abortSignal) .then(_maxBaseFee => this.calculateFeeForAmount(amountData.amount, _maxBaseFee, maxRoutingPPM)) } } else { maxFee = this.calculateFeeForAmount(amountData.amount, maxRoutingBaseFee, maxRoutingPPM) } return { expiryTimestamp: options?.expiryTimestamp ?? BigInt(Math.floor(Date.now()/1000)+expirySeconds), maxFee } } /** * Verifies whether a given payment hash was already paid by checking the local * storage of known swaps * * @param paymentHash Payment hash to check * * @private */ private async checkPaymentHashWasPaid(paymentHash: string) { const swaps = await this.unifiedStorage.query( [[{key: "type", value: this.TYPE}, {key: "paymentHash", value: paymentHash}]], (obj: any) => new this._swapDeserializer(this, obj) ); for(let value of swaps) { if(value._state===ToBTCSwapState.CLAIMED || value._state===ToBTCSwapState.SOFT_CLAIMED) throw new UserError("Lightning invoice was already paid!"); } } /** * Calculates maximum lightning network routing fee based on amount * * @param amount BTC amount of the swap in satoshis * @param overrideBaseFee Override wrapper's default base fee * @param overrideFeePPM Override wrapper's default PPM * * @returns Maximum lightning routing fee in sats * * @private */ private calculateFeeForAmount(amount: bigint, overrideBaseFee?: bigint, overrideFeePPM?: bigint) : bigint { return BigInt(overrideBaseFee ?? this._options.lightningBaseFee) + (amount * BigInt(overrideFeePPM ?? this._options.lightningFeePPM) / 1000000n); } /** * Verifies returned LP data * * @param signer * @param resp Response as returned by the LP * @param parsedPr Parsed bolt11 lightning invoice * @param token Smart chain token to be used in the swap * @param lp * @param calculatedOptions Swap options computed from the swap create options * @param data Parsed swap data returned by the LP * @param requiredTotal Required total to be paid on the input (for exactIn swaps) * * @throws {IntermediaryError} In case the response is not valid * * @private */ private async verifyReturnedData( signer: string, resp: ToBTCLNResponseType, parsedPr: PaymentRequestObject & {tagsObject: TagsObject}, token: string, lp: Intermediary, calculatedOptions: { maxFee: bigint | Promise<bigint>, expiryTimestamp: bigint }, data: T["Data"], requiredTotal?: bigint ): Promise<void> { if(resp.routingFeeSats > await calculatedOptions.maxFee) throw new IntermediaryError("Invalid max fee sats returned"); if(requiredTotal!=null && resp.total !== requiredTotal) throw new IntermediaryError("Invalid data returned - total amount"); if(parsedPr.tagsObject.payment_hash==null) throw new Error("Swap invoice doesn't contain payment hash field!"); const claimHash = this._contract(lp.getContractVersion(this.chainIdentifier)).getHashForHtlc(Buffer.from(parsedPr.tagsObject.payment_hash, "hex")); if( data.getAmount() !== resp.total || !Buffer.from(data.getClaimHash(), "hex").equals(claimHash) || data.getExpiry() !== calculatedOptions.expiryTimestamp || data.getType()!==ChainSwapType.HTLC || !data.isPayIn() || !data.isToken(token) || !data.isClaimer(lp.getAddress(this.chainIdentifier)) || !data.isOfferer(signer) || data.getTotalDeposit() !== 0n ) { throw new IntermediaryError("Invalid data returned"); } } /** * Returns the expiry seconds to use to calculate expiration timeout for HTLC, considers the `expirySeconds` and * `maxExpirySeconds` params. * * @param parsedPr * @param options * @private */ private getExpirySeconds( parsedPr: PaymentRequestObject & {tagsObject: TagsObject}, options?: ToBTCLNOptions ): { expirySeconds?: number, longExpiry: boolean } { let expirySeconds = options?.expirySeconds; let longExpiry = false; if( parsedPr.tagsObject.min_final_cltv_expiry!=null && parsedPr.tagsObject.min_final_cltv_expiry > 144 ) { expirySeconds ??= this._options.paymentTimeoutSeconds; let maxExpirySeconds = options?.maxExpirySeconds; if(maxExpirySeconds!=null) { if(maxExpirySeconds < expirySeconds) throw new Error(`maxExpirySeconds must be strictly larger than the regular expiration seconds: ${expirySeconds}`); } maxExpirySeconds ??= this._options.maxPaymentTimeoutSeconds; if(maxExpirySeconds > expirySeconds) { expirySeconds = Math.min(maxExpirySeconds, ((parsedPr.tagsObject.min_final_cltv_expiry - 144) * 600 * 2) + expirySeconds); longExpiry = true; } } return {expirySeconds, longExpiry}; } /** * Returns the quote/swap from a given intermediary * * @param signer Smartchain signer initiating the swap * @param amountData * @param lp Intermediary * @param pr bolt11 lightning network invoice * @param parsedPr Parsed bolt11 lightning network invoice * @param calculatedOptions Swap options computed from the swap create options * @param preFetches * @param abort Abort signal or controller, if AbortController is passed it is used as-is, when AbortSignal is passed * it is extended with extendAbortController and then used * @param additionalParams Additional params that should be sent to the LP * * @private */ private async getIntermediaryQuote( signer: string, amountData: Omit<AmountData, "amount">, lp: Intermediary, pr: string, parsedPr: PaymentRequestObject & {tagsObject: TagsObject}, calculatedOptions: { maxFee: bigint | Promise<bigint>, expiryTimestamp: bigint, longExpiry?: boolean }, preFetches: { feeRatePromise: {[contractVersion: string]: Promise<string | undefined>}, pricePreFetchPromise: Promise<bigint | undefined>, usdPricePrefetchPromise: Promise<number | undefined>, signDataPrefetchPromise?: {[contractVersion: string]: Promise<T["PreFetchVerification"] | undefined> | undefined} }, abort: AbortSignal | AbortController, additionalParams?: Record<string, any>, ) { if(lp.services[SwapType.TO_BTCLN]==null) throw new Error("LP service for processing to btcln swaps not found!"); const version = lp.getContractVersion(this.chainIdentifier); const abortController = abort instanceof AbortController ? abort : extendAbortController(abort); const reputationPromise = this.preFetchIntermediaryReputation(amountData, lp, abortController, version); try { const {signDataPromise, resp} = await tryWithRetries(async(retryCount: number) => { const {signDataPrefetch, response} = this._lpApi.initToBTCLN(this.chainIdentifier, lp.url, { offerer: signer, pr, maxFee: await calculatedOptions.maxFee, expiryTimestamp: calculatedOptions.expiryTimestamp, token: amountData.token, feeRate: throwIfUndefined(preFetches.feeRatePromise[version], "Network fee rate pre-fetch failed!"), additionalParams }, this._options.postRequestTimeout, abortController.signal, retryCount>0 ? false : undefined); let signDataPromise = preFetches.signDataPrefetchPromise?.[version]; if(signDataPromise==null) { signDataPromise = this.preFetchSignData(signDataPrefetch, version); } else signDataPrefetch.catch(() => {}); return { signDataPromise, resp: await response }; }, undefined, e => e instanceof RequestError, abortController.signal); if(parsedPr.millisatoshis==null) throw new Error("Swap invoice doesn't have msat amount field!"); const amountOut: bigint = (BigInt(parsedPr.millisatoshis) + 999n) / 1000n; const totalFee: bigint = resp.swapFee + resp.maxFee; const data: T["Data"] = new (this._swapDataDeserializer(version))(resp.data); data.setOfferer(signer); await this.verifyReturnedData(signer, resp, parsedPr, amountData.token, lp, calculatedOptions, data); const swapFeeBtc = resp.swapFee * amountOut / (data.getAmount() - totalFee); const [pricingInfo, signatureExpiry, reputation] = await Promise.all([ this.verifyReturnedPrice( lp.services[SwapType.TO_BTCLN], true, amountOut, data.getAmount(), amountData.token, {networkFee: resp.maxFee, swapFeeBtc}, preFetches.pricePreFetchPromise, preFetches.usdPricePrefetchPromise, abortController.signal ), this.verifyReturnedSignature( signer, data, resp, preFetches.feeRatePromise[version], signDataPromise, version, abortController.signal ), reputationPromise ]); abortController.signal.throwIfAborted(); if(reputation!=null) lp.reputation[amountData.token.toString()] = reputation; const quote = new ToBTCLNSwap<T>(this, { pricingInfo, url: lp.url, expiry: signatureExpiry, swapFee: resp.swapFee, swapFeeBtc, feeRate: (await preFetches.feeRatePromise[version])!, signatureData: resp, data, networkFee: resp.maxFee, networkFeeBtc: resp.routingFeeSats, confidence: resp.confidence, pr, exactIn: false, contractVersion: version, longExpiry: calculatedOptions.longExpiry } as IToBTCSwapInit<T["Data"]>); return quote; } catch (e) { abortController.abort(e); throw e; } } /** * Returns a newly created Smart chain -> Lightning swap using the HTLC based escrow swap protocol, * the amount is parsed from the provided lightning network payment request (bolt11 invoice) * * @param signer Source chain signer address initiating the swap * @param recipient BOLT11 payment request (bitcoin lightning invoice) you wish to pay * @param amountData Token to swap * @param lps An array of intermediaries (LPs) to get the quotes from * @param options Optional additional quote options * @param additionalParams Optional additional parameters sent to the LP when creating the swap * @param abortSignal Abort signal * @param preFetches Optional existing pre-fetch promises for the swap (only used internally for LNURL swaps) */ async create( signer: string, recipient: string, amountData: Omit<AmountData, "amount"> & {exactIn: false}, lps: Intermediary[], options?: ToBTCLNOptions, additionalParams?: Record<string, any>, abortSignal?: AbortSignal, preFetches?: { feeRatePromise: {[contractVersion: string]: Promise<string | undefined>}, pricePreFetchPromise: Promise<bigint | undefined>, usdPricePrefetchPromise: Promise<number | undefined>, signDataPrefetchPromise?: {[contractVersion: string]: Promise<T["PreFetchVerification"] | undefined> | undefined} } ): Promise<{ quote: Promise<ToBTCLNSwap<T>>, intermediary: Intermediary }[]> { const parsedPr = bolt11Decode(recipient); if(parsedPr.millisatoshis==null) throw new UserError("Must be an invoice with amount"); const amountOut: bigint = (BigInt(parsedPr.millisatoshis) + 999n) / 1000n; const lpVersions = Intermediary.getContractVersionsForLps(this.chainIdentifier, lps); const {expirySeconds, longExpiry} = this.getExpirySeconds(parsedPr, options); const _options = { ...this.toRequiredSwapOptions({...amountData, amount: amountOut}, {...options, expirySeconds}), longExpiry }; if(parsedPr.tagsObject.payment_hash==null) throw new Error("Provided lightning invoice doesn't contain payment hash field!"); await this.checkPaymentHashWasPaid(parsedPr.tagsObject.payment_hash); const _hash = mapArrayToObject(lpVersions, (contractVersion: string) => { return this._contract(contractVersion).getHashForHtlc(Buffer.from(parsedPr.tagsObject.payment_hash!, "hex")).toString("hex"); }); const _abortController = extendAbortController(abortSignal); const _preFetches = preFetches ?? { pricePreFetchPromise: this.preFetchPrice(amountData, _abortController.signal), feeRatePromise: this.preFetchFeeRate(signer, amountData, _hash, _abortController, lpVersions), usdPricePrefetchPromise: this.preFetchUsdPrice(_abortController.signal), signDataPrefetchPromise: mapArrayToObject(lpVersions, (contractVersion: string) => { return this._contract(contractVersion).preFetchBlockDataForSignatures==null ? this.preFetchSignData(Promise.resolve(true), contractVersion) : undefined; }) }; return lps.map(lp => { return { intermediary: lp, quote: this.getIntermediaryQuote(signer, amountData, lp, recipient, parsedPr, _options, _preFetches, _abortController.signal, additionalParams) } }); } /** * Parses and fetches lnurl pay params from the specified lnurl * * @param lnurl LNURL to be parsed and fetched * @param abortSignal Abort signal * @throws {UserError} if the LNURL is invalid or if it's not a LNURL-pay * * @private */ private async getLNURLPay(lnurl: string | LNURLPayParamsWithUrl, abortSignal: AbortSignal): Promise<LNURLPayParamsWithUrl> { if(typeof(lnurl)!=="string") return lnurl; const res = await LNURL.getLNURL(lnurl, true, this._options.getRequestTimeout, abortSignal); if(res==null) throw new UserError("Invalid LNURL"); if(res.tag!=="payRequest") throw new UserError("Not a LNURL-pay"); return res; } /** * Returns the quote/swap from the given LP * * @param signer Source chain signer address initiating the swap * @param amountData Token to swap * @param invoiceCreateService Service for creating fixed amount invoices * @param lp Intermediary (LPs) to get the quote from * @param dummyPr Dummy minimum value bolt11 lightning invoice returned from the LNURL-pay, used to estimate * network fees for an actual invoice * @param calculatedOptions Swap options computed from the swap create options * @param preFetches Optional existing pre-fetch promises for the swap (only used internally for LNURL swaps) * @param abortSignal Abort signal * @param additionalParams Additional params to be sent to the intermediary * * @private */ private async getIntermediaryQuoteExactIn( signer: string, amountData: AmountData, invoiceCreateService: LightningInvoiceCreateService, lp: Intermediary, dummyPr: string, calculatedOptions: { maxFee: bigint | Promise<bigint>, expiryTimestamp: bigint, longExpiry?: boolean }, preFetches: { feeRatePromise: {[contractVersion: string]: Promise<string | undefined>}, pricePreFetchPromise: Promise<bigint | undefined>, usdPricePrefetchPromise: Promise<number | undefined> }, abortSignal: AbortSignal, additionalParams?: Record<string, any>, ) { if(lp.services[SwapType.TO_BTCLN]==null) throw new Error("LP service for processing to btcln swaps not found!"); const version = lp.getContractVersion(this.chainIdentifier); const abortController = extendAbortController(abortSignal); const reputationPromise = this.preFetchIntermediaryReputation(amountData, lp, abortController, version); try { const {signDataPromise, prepareResp} = await tryWithRetries(async(retryCount: number) => { const {signDataPrefetch, response} = this._lpApi.prepareToBTCLNExactIn(this.chainIdentifier, lp.url, { token: amountData.token, offerer: signer, pr: dummyPr, amount: amountData.amount, maxFee: await calculatedOptions.maxFee, expiryTimestamp: calculatedOptions.expiryTimestamp, additionalParams }, this._options.postRequestTimeout, abortController.signal, retryCount>0 ? false : undefined); return { signDataPromise: this.preFetchSignData(signDataPrefetch, version), prepareResp: await response }; }, undefined, e => e instanceof RequestError, abortController.signal); if(prepareResp.amount <= 0n) throw new IntermediaryError("Invalid amount returned (zero or negative)"); if(invoiceCreateService.minMsats!=null) { if(prepareResp.amount < invoiceCreateService.minMsats / 1000n) throw new UserError("Amount less than minimum"); } if(invoiceCreateService.maxMSats!=null) { if(prepareResp.amount > invoiceCreateService.maxMSats / 1000n) throw new UserError("Amount more than maximum"); } const invoice = await invoiceCreateService.getInvoice(Number(prepareResp.amount), abortController.signal); const parsedInvoice = bolt11Decode(invoice); const resp = await tryWithRetries( (retryCount: number) => this._lpApi.initToBTCLNExactIn(lp.url, { pr: invoice, reqId: prepareResp.reqId, feeRate: throwIfUndefined(preFetches.feeRatePromise[version], "Network fee rate pre-fetch failed!"), additionalParams }, this._options.postRequestTimeout, abortController.signal, retryCount>0 ? false : undefined), undefined, RequestError, abortController.signal ); if(parsedInvoice.millisatoshis==null) throw new Error("Swap invoice doesn't have msat amount field!"); const amountOut: bigint = (BigInt(parsedInvoice.millisatoshis) + 999n) / 1000n; const totalFee: bigint = resp.swapFee + resp.maxFee; const data: T["Data"] = new (this._swapDataDeserializer(version))(resp.data); data.setOfferer(signer); await this.verifyReturnedData(signer, resp, parsedInvoice, amountData.token, lp, calculatedOptions, data, amountData.amount); const swapFeeBtc = resp.swapFee * amountOut / (data.getAmount() - totalFee); const [pricingInfo, signatureExpiry, reputation] = await Promise.all([ this.verifyReturnedPrice( lp.services[SwapType.TO_BTCLN], true, prepareResp.amount, data.getAmount(), amountData.token, {networkFee: resp.maxFee, swapFeeBtc}, preFetches.pricePreFetchPromise, preFetches.usdPricePrefetchPromise, abortSignal ), this.verifyReturnedSignature( signer, data, resp, preFetches.feeRatePromise[version], signDataPromise, version, abortController.signal ), reputationPromise ]); abortController.signal.throwIfAborted(); if(reputation!=null) lp.reputation[amountData.token.toString()] = reputation; const quote = new ToBTCLNSwap<T>(this, { pricingInfo, url: lp.url, expiry: signatureExpiry, swapFee: resp.swapFee, swapFeeBtc, feeRate: (await preFetches.feeRatePromise[version])!, signatureData: resp, data, networkFee: resp.maxFee, networkFeeBtc: resp.routingFeeSats, confidence: resp.confidence, pr: invoice, exactIn: true, contractVersion: version, longExpiry: calculatedOptions?.longExpiry } as IToBTCSwapInit<T["Data"]>); return quote; } catch (e) { abortController.abort(e); throw e; } } /** * Returns a newly created Smart chain -> Lightning swap using the HTLC based escrow swap protocol via * invoice creation service. This allows exactIn swaps by requesting the desired fixed amount lightning * network invoice from the service. * * @param signer Source chain signer address initiating the swap * @param invoiceCreateServicePromise Service to request destination lightning network invoices from * @param amountData Amount, token and exact input/output data for to swap * @param lps An array of intermediaries (LPs) to get the quotes from * @param options Optional additional quote options * @param additionalParams Optional additional parameters sent to the LP when creating the swap * @param abortSignal Abort signal */ async createViaInvoiceCreateService( signer: string, invoiceCreateServicePromise: Promise<LightningInvoiceCreateService>, amountData: AmountData, lps: Intermediary[], options?: ToBTCLNOptions, additionalParams?: Record<string, any>, abortSignal?: AbortSignal ): Promise<{ quote: Promise<ToBTCLNSwap<T>>, intermediary: Intermediary }[]> { if(!this.isInitialized) throw new Error("Not initialized, call init() first!"); const lpVersions = Intermediary.getContractVersionsForLps(this.chainIdentifier, lps); const _abortController = extendAbortController(abortSignal); const pricePreFetchPromise: Promise<bigint | undefined> = this.preFetchPrice(amountData, _abortController.signal); const usdPricePrefetchPromise: Promise<number | undefined> = this.preFetchUsdPrice(_abortController.signal); const feeRatePromise = this.preFetchFeeRate(signer, amountData, undefined, _abortController, lpVersions); const signDataPrefetchPromise = mapArrayToObject(lpVersions, (contractVersion: string) => { return this._contract(contractVersion).preFetchBlockDataForSignatures==null ? this.preFetchSignData(Promise.resolve(true), contractVersion) : undefined; }); try { const invoiceCreateService = await invoiceCreateServicePromise; if(amountData.exactIn) { const dummyInvoice = await invoiceCreateService.getInvoice( invoiceCreateService.minMsats==null ? 1 : Number(invoiceCreateService.minMsats/1000n), _abortController.signal ); const parsedDummyInvoice = bolt11Decode(dummyInvoice); const {expirySeconds, longExpiry} = this.getExpirySeconds(parsedDummyInvoice, options); const _options = { ...this.toRequiredSwapOptions(amountData, {...options, expirySeconds}, pricePreFetchPromise, _abortController.signal), longExpiry }; return lps.map(lp => { return { quote: this.getIntermediaryQuoteExactIn(signer, amountData, invoiceCreateService, lp, dummyInvoice, _options, { pricePreFetchPromise, usdPricePrefetchPromise, feeRatePromise }, _abortController.signal, additionalParams), intermediary: lp } }) } else { if(invoiceCreateService.minMsats!=null) { if(amountData.amount < invoiceCreateService.minMsats / 1000n) throw new UserError("Amount less than minimum"); } if(invoiceCreateService.maxMSats!=null) { if(amountData.amount > invoiceCreateService.maxMSats / 1000n) throw new UserError("Amount more than maximum"); } const invoice = await invoiceCreateService.getInvoice(Number(amountData.amount), _abortController.signal); return (await this.create(signer, invoice, {...amountData, exactIn: false}, lps, options, additionalParams, _abortController.signal, { feeRatePromise, pricePreFetchPromise, usdPricePrefetchPromise, signDataPrefetchPromise })); } } catch (e) { _abortController.abort(e); throw e; } } /** * Returns a newly created Smart chain -> Lightning swap using the HTLC based escrow swap protocol. Pays to * an LNURL-pay link. This allows exactIn swaps by requesting the desired fixed amount lightning * network invoice from the LNURL service. * * @param signer Source chain signer address initiating the swap * @param lnurl LNURL-pay link of the recipient * @param amountData Amount, token and exact input/output data for to swap * @param lps An array of intermediaries (LPs) to get the quotes from * @param options Optional additional quote options * @param additionalParams Optional additional parameters sent to the LP when creating the swap * @param abortSignal Abort signal */ async createViaLNURL( signer: string, lnurl: string | LNURLPayParamsWithUrl, amountData: AmountData, lps: Intermediary[], options?: ToBTCLNOptions & {comment?: string}, additionalParams?: Record<string, any>, abortSignal?: AbortSignal ): Promise<{ quote: Promise<ToBTCLNSwap<T>>, intermediary: Intermediary }[]> { let successActions: {[pr: string]: LNURLPaySuccessAction} = {}; const _abortController = extendAbortController(abortSignal); const invoiceCreateService = (async() => { let payRequest: LNURLPayParamsWithUrl = await this.getLNURLPay(lnurl, _abortController.signal); if( options?.comment!=null && (payRequest.commentAllowed==null || options.comment.length>payRequest.commentAllowed) ) throw new UserError("Comment not allowed or too long"); return { getInvoice: async (amountSats: number, abortSignal?: AbortSignal) => { const {invoice, successAction} = await LNURL.useLNURLPay( payRequest, BigInt(amountSats), options?.comment, this._options.getRequestTimeout, abortSignal ); if(successAction!=null) successActions[invoice] = successAction; return invoice; }, minMsats: BigInt(payRequest.minSendable), maxMsats: BigInt(payRequest.maxSendable), url: payRequest.url } })(); const quotes = await this.createViaInvoiceCreateService( signer, invoiceCreateService, amountData, lps, options, additionalParams, _abortController.signal ); _abortController.signal.throwIfAborted(); const resolved = await invoiceCreateService; _abortController.signal.throwIfAborted(); return quotes.map(value => ({ quote: value.quote.then(quote => { let _successAction: LNURLPaySuccessAction | undefined; const quoteAddress = quote.getOutputAddress(); if(quoteAddress!=null) { const successAction = successActions[quoteAddress]; if(successAction!=null) _successAction = successAction; } quote._setLNURLData(resolved.url, _successAction); return quote; }), intermediary: value.intermediary })); } /** * @inheritDoc */ async recoverFromSwapDataAndState( init: {data: T["Data"], getInitTxId: () => Promise<string>, getTxBlock: () => Promise<{blockTime: number, blockHeight: number}>}, state: SwapCommitState, contractVersion: string, lp?: Intermediary ): Promise<ToBTCLNSwap<T> | null> { const data = init.data; let paymentHash = data.getHTLCHashHint(); let secret: string | undefined; if(state.type===SwapCommitStateType.PAID) { secret = await state.getClaimResult(); paymentHash = Buffer.from(sha256(Buffer.from(secret, "hex"))).toString("hex"); } const swapInit: ToBTCLNSwapInit<T["Data"]> = { pricingInfo: { isValid: true, satsBaseFee: 0n, swapPriceUSatPerToken: 100_000_000_000_000n, realPriceUSatPerToken: 100_000_000_000_000n, differencePPM: 0n, feePPM: 0n, }, url: lp?.url, expiry: 0, swapFee: 0n, swapFeeBtc: 0n, feeRate: "", signatureData: undefined, data, networkFee: 0n, networkFeeBtc: 0n, confidence: 0, pr: paymentHash ?? undefined, exactIn: false, contractVersion }; const swap = new ToBTCLNSwap(this, swapInit); swap._commitTxId = await init.getInitTxId(); const blockData = await init.getTxBlock(); swap.createdAt = blockData.blockTime * 1000; swap._setInitiated(); swap._state = ToBTCSwapState.COMMITED; await swap._sync(false, false, state); await swap._save(); return swap; } }