UNPKG

@atomiqlabs/sdk

Version:

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

753 lines (682 loc) 36.2 kB
import {decode as bolt11Decode, PaymentRequestObject, TagsObject} from "@atomiqlabs/bolt11"; import { ChainSwapType, ChainType, ClaimEvent, InitializeEvent, LightningNetworkApi, LNNodeLiquidity, Messenger, RefundEvent, SwapCommitState, SwapCommitStateType } from "@atomiqlabs/base"; import {Intermediary} from "../../../../intermediaries/Intermediary"; import {Buffer} from "buffer"; import {UserError} from "../../../../errors/UserError"; import {IntermediaryError} from "../../../../errors/IntermediaryError"; import {SwapType} from "../../../../enums/SwapType"; import { extendAbortController, mapArrayToObject, parseHashValueExact32Bytes, randomBytes, throwIfUndefined } from "../../../../utils/Utils"; import { FromBTCLNAutoResponseType, IntermediaryAPI } from "../../../../intermediaries/apis/IntermediaryAPI"; import {RequestError} from "../../../../errors/RequestError"; import {ISwapPrice} from "../../../../prices/abstract/ISwapPrice"; import {EventEmitter} from "events"; import {ISwapWrapperOptions, WrapperCtorTokens} from "../../../ISwapWrapper"; import {UnifiedSwapEventListener} from "../../../../events/UnifiedSwapEventListener"; import {UnifiedSwapStorage} from "../../../../storage/UnifiedSwapStorage"; import {ISwap} from "../../../ISwap"; import {FromBTCLNAutoSwap, FromBTCLNAutoSwapInit, FromBTCLNAutoSwapState} from "./FromBTCLNAutoSwap"; import {IFromBTCLNDefinition, IFromBTCLNWrapper} from "../IFromBTCLNWrapper"; import {IClaimableSwapWrapper} from "../../../IClaimableSwapWrapper"; import {AmountData} from "../../../../types/AmountData"; import {LNURLWithdrawParamsWithUrl} from "../../../../types/lnurl/LNURLWithdraw"; import {tryWithRetries} from "../../../../utils/RetryUtils"; import {AllOptional} from "../../../../utils/TypeUtils"; import {sha256} from "@noble/hashes/sha2"; import {fromHumanReadableString} from "../../../../utils/TokenUtils"; export type FromBTCLNAutoOptions = { /** * Instead of letting the SDK generate the preimage/paymentHash pair internally you can pass your computed * paymentHash here, this will create the swap with the provided payment hash. Note that swaps created this way * won't settle automatically (as the SDK is missing the preimage). Once the HTLC towards the user is created in * the {@link FromBTCLNAutoSwapState.CLAIM_COMMITED} state, you should pass the secret preimage manually in the * {@link FromBTCLNAutoSwap.waitTillClaimed}, {@link FromBTCLNAutoSwap.claim} or {@link FromBTCLNAutoSwap.txsClaim} * functions. * * Accepts both, a {@link Buffer} and a hexadecimal `string` */ paymentHash?: Buffer | string, /** * Optional description to use for the swap lightning network invoice, keep the invoice length below 500 characters */ description?: string, /** * Optional description hash to use for the lightning network invoice, useful when returning the invoice as part of * an LNURL-pay service endpoint. * * Accepts both, a {@link Buffer} and a hexadecimal `string` */ descriptionHash?: Buffer | string, /** * Optional additional native token to receive as an output of the swap (e.g. STRK on Starknet or cBTC on Citrea). * When passed as a `bigint` it is specified in base units of the token and in `string` it is the human readable * decimal format. */ gasAmount?: bigint | string, /** * A flag to skip checking whether the lightning network node of the LP has enough channel liquidity to facilitate * the swap. */ unsafeSkipLnNodeCheck?: boolean, /** * A flag to attach 0 watchtower fee to the swap, this would make the settlement unattractive for the watchtowers * and therefore automatic settlement for such swaps will not be possible, you will have to settle manually * with {@link FromBTCLNSwap.claim} or {@link FromBTCLNSwap.txsClaim} functions. */ unsafeZeroWatchtowerFee?: boolean, /** * A safety factor to use when estimating the watchtower fee to attach to the swap (this has to cover the gas fee * of watchtowers settling the swap). A higher multiple here would mean that a swap is more attractive for * watchtowers to settle automatically. * * Uses a `1.25` multiple by default (i.e. the current network fee is multiplied by 1.25 and then used to estimate * the settlement gas fee cost) */ feeSafetyFactor?: number }; export type FromBTCLNAutoWrapperOptions = ISwapWrapperOptions & { safetyFactor: number, bitcoinBlocktime: number, unsafeSkipLnNodeCheck: boolean }; export type FromBTCLNAutoDefinition<T extends ChainType> = IFromBTCLNDefinition<T, FromBTCLNAutoWrapper<T>, FromBTCLNAutoSwap<T>>; /** * New escrow based (HTLC) swaps for Bitcoin Lightning -> Smart chain swaps not requiring manual settlement on * the destination by the user, and instead letting the LP initiate the escrow. Permissionless watchtower network * handles the claiming of HTLC, with the swap secret broadcasted over Nostr. Also adds a possibility for the user * to receive a native token on the destination chain as part of the swap (a "gas drop" feature). * * @category Swaps/Lightning → Smart chain */ export class FromBTCLNAutoWrapper< T extends ChainType > extends IFromBTCLNWrapper<T, FromBTCLNAutoDefinition<T>, FromBTCLNAutoWrapperOptions> implements IClaimableSwapWrapper<FromBTCLNAutoSwap<T>> { public readonly TYPE: SwapType.FROM_BTCLN_AUTO = SwapType.FROM_BTCLN_AUTO; /** * @internal */ protected readonly tickSwapState = [ FromBTCLNAutoSwapState.PR_CREATED, FromBTCLNAutoSwapState.QUOTE_SOFT_EXPIRED, FromBTCLNAutoSwapState.PR_PAID, FromBTCLNAutoSwapState.CLAIM_COMMITED ]; /** * @internal */ readonly _pendingSwapStates = [ FromBTCLNAutoSwapState.PR_CREATED, FromBTCLNAutoSwapState.QUOTE_SOFT_EXPIRED, FromBTCLNAutoSwapState.PR_PAID, FromBTCLNAutoSwapState.CLAIM_COMMITED, FromBTCLNAutoSwapState.EXPIRED ]; /** * @internal */ readonly _claimableSwapStates = [FromBTCLNAutoSwapState.CLAIM_COMMITED]; /** * @internal */ readonly _swapDeserializer = FromBTCLNAutoSwap; /** * @internal */ readonly _messenger: Messenger; /** * @param chainIdentifier * @param unifiedStorage Storage interface for the current environment * @param unifiedChainEvents On-chain event listener * @param chain * @param prices Swap pricing handler * @param tokens * @param versionedContracts * @param lnApi * @param messenger * @param lpApi * @param options * @param events Instance to use for emitting events */ 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"] } }, lnApi: LightningNetworkApi, messenger: Messenger, lpApi: IntermediaryAPI, options?: AllOptional<FromBTCLNAutoWrapperOptions>, events?: EventEmitter<{swapState: [ISwap]}> ) { super( chainIdentifier, unifiedStorage, unifiedChainEvents, chain, prices, tokens, versionedContracts, lnApi, lpApi, { ...options, safetyFactor: options?.safetyFactor ?? 2, bitcoinBlocktime: options?.bitcoinBlocktime ?? 10*60, unsafeSkipLnNodeCheck: options?.unsafeSkipLnNodeCheck ?? false }, events ); this._messenger = messenger; } /** * @inheritDoc * @internal */ protected async processEventInitialize(swap: FromBTCLNAutoSwap<T>, event: InitializeEvent<T["Data"]>): Promise<boolean> { if(swap._state===FromBTCLNAutoSwapState.PR_PAID || swap._state===FromBTCLNAutoSwapState.PR_CREATED || swap._state===FromBTCLNAutoSwapState.QUOTE_SOFT_EXPIRED) { if(swap._data==null) { //Obtain data from the initialize event const eventData = await event.swapData(); if(eventData==null) { this.logger.error("processEventInitialize("+swap.getId()+"): Error when fetching swap data for swap, null returned!"); return false; } try { await swap._saveRealSwapData(eventData, false); this.logger.info("processEventInitialize("+swap.getId()+"): Successfully taken swap data from on-chain event!"); } catch (e) { this.logger.error("processEventInitialize("+swap.getId()+"): Error when saving swap data for swap: ", e); return false; } } if(swap._getEscrowHash()!==event.escrowHash) { this.logger.error("processEventInitialize("+swap.getId()+"): Error when processing event, escrow hashes don't match!"); return false; } swap._commitedAt ??= Date.now(); swap._state = FromBTCLNAutoSwapState.CLAIM_COMMITED; if(swap.hasSecretPreimage()) swap._broadcastSecret().catch(e => { this.logger.error("processEventInitialize("+swap.getId()+"): Error when broadcasting swap secret: ", e); }); return true; } return false; } /** * @inheritDoc * @internal */ protected processEventClaim(swap: FromBTCLNAutoSwap<T>, event: ClaimEvent<T["Data"]>): Promise<boolean> { if(swap._state!==FromBTCLNAutoSwapState.FAILED && swap._state!==FromBTCLNAutoSwapState.CLAIM_CLAIMED) { swap._state = FromBTCLNAutoSwapState.CLAIM_CLAIMED; swap._setSwapSecret(event.result); return Promise.resolve(true); } return Promise.resolve(false); } /** * @inheritDoc * @internal */ protected processEventRefund(swap: FromBTCLNAutoSwap<T>, event: RefundEvent<T["Data"]>): Promise<boolean> { if(swap._state!==FromBTCLNAutoSwapState.CLAIM_CLAIMED && swap._state!==FromBTCLNAutoSwapState.FAILED) { swap._state = FromBTCLNAutoSwapState.FAILED; return Promise.resolve(true); } return Promise.resolve(false); } /** * Pre-fetches claimer (watchtower) bounty data for the swap. Doesn't throw, instead returns null and aborts the * provided abortController * * @param signer Smartchain signer address initiating the swap * @param amountData * @param options Options as passed to the swap creation function * @param abortController * * @param contractVersions * @private */ private preFetchClaimerBounty( signer: string, amountData: AmountData, options: {feeSafetyFactor: number, unsafeZeroWatchtowerFee: boolean}, abortController: AbortController, contractVersions: string[] ): {[chainVersion: string]: Promise<bigint | undefined>} { return mapArrayToObject(contractVersions, async (contractVersion) => { if(options.unsafeZeroWatchtowerFee) return 0n; const dummyAmount = BigInt(Math.floor(Math.random()* 0x1000000)); const dummySwapData = await this._contract(contractVersion).createSwapData( ChainSwapType.HTLC, this._chain.randomAddress(), signer, amountData.token, dummyAmount, this._contract(contractVersion).getHashForHtlc(randomBytes(32)).toString("hex"), this.getRandomSequence(), BigInt(Math.floor(Date.now()/1000)), false, true, BigInt(Math.floor(Math.random() * 0x10000)), BigInt(Math.floor(Math.random() * 0x10000)) ); try { const result = await this._contract(contractVersion).getClaimFee(this._chain.randomAddress(), dummySwapData); return result * BigInt(Math.floor(options.feeSafetyFactor*1000000)) / 1_000_000n } catch (e) { abortController.abort(e); } }); } /** * Verifies response returned from intermediary * * @param resp Response as returned by the intermediary * @param amountData * @param lp Intermediary * @param options Options as passed to the swap creation function * @param decodedPr Decoded bolt11 lightning network invoice * @param paymentHash Expected payment hash of the bolt11 lightning network invoice * @param claimerBounty Claimer bounty as request by the user * * @throws {IntermediaryError} in case the response is invalid * * @private */ private verifyReturnedData( resp: FromBTCLNAutoResponseType, amountData: AmountData, lp: Intermediary, options: {gasAmount: bigint, description?: string, descriptionHash?: Buffer}, decodedPr: PaymentRequestObject & {tagsObject: TagsObject}, paymentHash: Buffer, claimerBounty: bigint ): void { if(lp.getAddress(this.chainIdentifier)!==resp.intermediaryKey) throw new IntermediaryError("Invalid intermediary address/pubkey"); if(options.descriptionHash!=null && decodedPr.tagsObject.purpose_commit_hash!==options.descriptionHash.toString("hex")) throw new IntermediaryError("Invalid pr returned - description hash"); if(options.description!=null && decodedPr.tagsObject.description!==options.description) throw new IntermediaryError("Invalid pr returned - description"); if( decodedPr.tagsObject.payment_hash==null || !Buffer.from(decodedPr.tagsObject.payment_hash, "hex").equals(paymentHash) ) throw new IntermediaryError("Invalid pr returned - payment hash"); if(decodedPr.millisatoshis==null) throw new IntermediaryError("Invalid pr returned - msat field"); const amountIn = (BigInt(decodedPr.millisatoshis) + 999n) / 1000n; if(resp.btcAmountGas + resp.btcAmountSwap !== amountIn) throw new IntermediaryError("Invalid total btc returned"); if(resp.gasSwapFeeBtc + resp.swapFeeBtc !== resp.totalFeeBtc) throw new IntermediaryError("Invalid total btc fee returned"); if(resp.claimerBounty !== claimerBounty) throw new IntermediaryError("Invalid claimer bounty"); if(resp.totalGas !== options.gasAmount) throw new IntermediaryError("Invalid total gas amount"); if(!amountData.exactIn) { if(resp.total != amountData.amount) throw new IntermediaryError("Invalid amount returned"); } else { if(amountIn !== amountData.amount) throw new IntermediaryError("Invalid payment request returned, amount mismatch"); } } /** * Returns a newly created Lightning -> Smart chain swap using the HTLC based escrow swap protocol, * where watchtowers handle the automatic settlement of the swap on the destination chain. Also allows * specifying additional "gas drop" native token that the receipient receives on the destination chain * in the `options` argument. The user has to pay a bolt11 invoice on the input lightning network side. * * @param recipient Recipient's address on the destination chain * @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 * @param preFetches Optional pre-fetches for speeding up the quoting process (mainly used internally) */ create( recipient: string, amountData: AmountData, lps: Intermediary[], options?: FromBTCLNAutoOptions, additionalParams?: Record<string, any>, abortSignal?: AbortSignal, preFetches?: { pricePrefetchPromise: Promise<bigint | undefined>, usdPricePrefetchPromise: Promise<number | undefined>, claimerBountyPrefetch: {[contractVersion: string]: Promise<bigint | undefined>}, gasTokenPricePrefetchPromise?: Promise<bigint | undefined>, } ): { quote: Promise<FromBTCLNAutoSwap<T>>, intermediary: Intermediary }[] { if(!this.isInitialized) throw new Error("Not initialized, call init() first!"); const _options = { paymentHash: parseHashValueExact32Bytes(options?.paymentHash, "payment hash"), unsafeSkipLnNodeCheck: options?.unsafeSkipLnNodeCheck ?? this._options.unsafeSkipLnNodeCheck, gasAmount: this.parseGasAmount(options?.gasAmount), feeSafetyFactor: options?.feeSafetyFactor ?? 1.25, //No need to add much of a margin, since the claim should happen rather soon unsafeZeroWatchtowerFee: options?.unsafeZeroWatchtowerFee ?? false, description: options?.description, descriptionHash: parseHashValueExact32Bytes(options?.descriptionHash, "description hash") }; if( _options.gasAmount!==0n && ( this._chain.shouldGetNativeTokenDrop!=null ? !this._chain.shouldGetNativeTokenDrop(amountData.token) : amountData.token===this._chain.getNativeCurrencyAddress() ) ) throw new UserError("Cannot specify `gasAmount` for swaps to a native token!"); if(_options.description!=null && Buffer.byteLength(_options.description, "utf8") > 500) throw new UserError("Invalid description length"); const lpVersions = Intermediary.getContractVersionsForLps(this.chainIdentifier, lps); let secret: Buffer | undefined; let paymentHash: Buffer; if(_options?.paymentHash!=null) { paymentHash = _options.paymentHash; } else { ({secret, paymentHash} = this.getSecretAndHash()); } const _hash = mapArrayToObject(lpVersions, (contractVersion: string) => { return this._contract(contractVersion).getHashForHtlc(paymentHash).toString("hex"); }); const nativeTokenAddress = this._chain.getNativeCurrencyAddress(); const _abortController = extendAbortController(abortSignal); const _preFetches = preFetches ?? { pricePrefetchPromise: this.preFetchPrice(amountData, _abortController.signal), usdPricePrefetchPromise: this.preFetchUsdPrice(_abortController.signal), claimerBountyPrefetch: this.preFetchClaimerBounty(recipient, amountData, _options, _abortController, lpVersions), gasTokenPricePrefetchPromise: _options.gasAmount!==0n || !_options.unsafeZeroWatchtowerFee ? this.preFetchPrice({token: nativeTokenAddress}, _abortController.signal) : undefined }; return lps.map(lp => { return { intermediary: lp, quote: (async () => { if(lp.services[SwapType.FROM_BTCLN_AUTO]==null) throw new Error("LP service for processing from btcln auto swaps not found!"); const version = lp.getContractVersion(this.chainIdentifier); const abortController = extendAbortController(_abortController.signal); const liquidityPromise: Promise<bigint | undefined> = this.preFetchIntermediaryLiquidity(amountData, lp, abortController, version); const {lnCapacityPromise, resp} = await tryWithRetries(async(retryCount: number) => { const {lnPublicKey, response} = this._lpApi.initFromBTCLNAuto( this.chainIdentifier, lp.url, { paymentHash, amount: amountData.amount, claimer: recipient, token: amountData.token.toString(), description: _options.description, descriptionHash: _options.descriptionHash, exactOut: !amountData.exactIn, additionalParams, gasToken: this._chain.getNativeCurrencyAddress(), gasAmount: _options.gasAmount, claimerBounty: throwIfUndefined(_preFetches.claimerBountyPrefetch[version], "Watchtower fee pre-fetch failed!") }, this._options.postRequestTimeout, abortController.signal, retryCount>0 ? false : undefined ); let lnCapacityPromise: Promise<LNNodeLiquidity | null> | undefined; if(!_options.unsafeSkipLnNodeCheck) { lnCapacityPromise = this.preFetchLnCapacity(lnPublicKey); } else lnPublicKey.catch(() => {}); return { lnCapacityPromise, resp: await response }; }, undefined, RequestError, abortController.signal); const decodedPr = bolt11Decode(resp.pr); if(decodedPr.millisatoshis==null) throw new IntermediaryError("Invalid returned swap invoice, no msat amount field"); if(decodedPr.timeExpireDate==null) throw new IntermediaryError("Invalid returned swap invoice, no expiry date field"); const amountIn = (BigInt(decodedPr.millisatoshis) + 999n) / 1000n; const claimerBounty = (await _preFetches.claimerBountyPrefetch[version])!; try { this.verifyReturnedData(resp, amountData, lp, _options, decodedPr, paymentHash, claimerBounty); const [pricingInfo, gasPricingInfo] = await Promise.all([ this.verifyReturnedPrice( lp.services[SwapType.FROM_BTCLN_AUTO], false, resp.btcAmountSwap, resp.total, amountData.token, {swapFeeBtc: resp.swapFeeBtc}, _preFetches.pricePrefetchPromise, _preFetches.usdPricePrefetchPromise, abortController.signal ), _options.gasAmount===0n ? Promise.resolve(undefined) : this.verifyReturnedPrice( {...lp.services[SwapType.FROM_BTCLN_AUTO], swapBaseFee: 0}, //Base fee should be charged only on the amount, not on gas false, resp.btcAmountGas, resp.totalGas + resp.claimerBounty, nativeTokenAddress, {swapFeeBtc: resp.gasSwapFeeBtc}, _preFetches.gasTokenPricePrefetchPromise, _preFetches.usdPricePrefetchPromise, abortController.signal ), this.verifyIntermediaryLiquidity(resp.total, throwIfUndefined(liquidityPromise, "LP liquidity pre-fetch failed!")), _options.unsafeSkipLnNodeCheck ? Promise.resolve() : this.verifyLnNodeCapacity(lp, decodedPr, lnCapacityPromise, abortController.signal) ]); const swapInit: FromBTCLNAutoSwapInit<T["Data"]> = { pricingInfo, url: lp.url, expiry: decodedPr.timeExpireDate*1000, swapFee: resp.swapFee, gasSwapFee: resp.gasSwapFee, swapFeeBtc: resp.swapFeeBtc, gasSwapFeeBtc: resp.gasSwapFeeBtc, btcAmountGas: resp.btcAmountGas, btcAmountSwap: resp.btcAmountSwap, gasPricingInfo, initialSwapData: await this._contract(version).createSwapData( ChainSwapType.HTLC, lp.getAddress(this.chainIdentifier), recipient, amountData.token, resp.total, _hash[version], this.getRandomSequence(), BigInt(Math.floor(Date.now()/1000)), false, true, _options.gasAmount + resp.claimerBounty, resp.claimerBounty, nativeTokenAddress ), pr: resp.pr, secret: secret?.toString("hex"), exactIn: amountData.exactIn ?? true, contractVersion: version }; const quote = new FromBTCLNAutoSwap<T>(this, swapInit); return quote; } catch (e) { abortController.abort(e); throw e; } })() } }); } /** * Returns a newly created Lightning -> Smart chain swap using the HTLC based escrow swap protocol, * where watchtowers handle the automatic settlement of the swap on the destination chain. Also allows * specifying additional "gas drop" native token that the receipient receives on the destination chain * in the `options` argument. The swap is created with an LNURL-withdraw link which will be used to pay * the generated bolt11 invoice automatically when {@link FromBTCLNSwap.waitForPayment} is called on the * swap. * * @param recipient Recipient's address on the destination chain * @param lnurl LNURL-withdraw link to pull the funds 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 createViaLNURL( recipient: string, lnurl: string | LNURLWithdrawParamsWithUrl, amountData: AmountData, lps: Intermediary[], options?: FromBTCLNAutoOptions, additionalParams?: Record<string, any>, abortSignal?: AbortSignal ): Promise<{ quote: Promise<FromBTCLNAutoSwap<T>>, intermediary: Intermediary }[]> { if(!this.isInitialized) throw new Error("Not initialized, call init() first!"); const _options = { paymentHash: parseHashValueExact32Bytes(options?.paymentHash, "payment hash"), unsafeSkipLnNodeCheck: options?.unsafeSkipLnNodeCheck ?? this._options.unsafeSkipLnNodeCheck, gasAmount: this.parseGasAmount(options?.gasAmount), feeSafetyFactor: options?.feeSafetyFactor ?? 1.25, //No need to add much of a margin, since the claim should happen rather soon unsafeZeroWatchtowerFee: options?.unsafeZeroWatchtowerFee ?? false, description: options?.description, descriptionHash: parseHashValueExact32Bytes(options?.descriptionHash, "description hash") }; const lpVersions = Intermediary.getContractVersionsForLps(this.chainIdentifier, lps); const abortController = extendAbortController(abortSignal); const preFetches = { pricePrefetchPromise: this.preFetchPrice(amountData, abortController.signal), usdPricePrefetchPromise: this.preFetchUsdPrice(abortController.signal), gasTokenPricePrefetchPromise: _options.gasAmount!==0n || !_options.unsafeZeroWatchtowerFee ? this.preFetchPrice({token: this._chain.getNativeCurrencyAddress()}, abortController.signal) : undefined, claimerBountyPrefetch: this.preFetchClaimerBounty(recipient, amountData, _options, abortController, lpVersions) }; try { const exactOutAmountPromise: Promise<bigint | undefined> | undefined = !amountData.exactIn ? preFetches.pricePrefetchPromise.then(price => this._prices.getToBtcSwapAmount(this.chainIdentifier, amountData.amount, amountData.token, abortController.signal, price) ).catch(e => { abortController.abort(e); return undefined; }) : undefined; const withdrawRequest = await this.getLNURLWithdraw(lnurl, abortController.signal); const min = BigInt(withdrawRequest.minWithdrawable) / 1000n; const max = BigInt(withdrawRequest.maxWithdrawable) / 1000n; if(amountData.exactIn) { if(amountData.amount < min) throw new UserError("Amount less than LNURL-withdraw minimum"); if(amountData.amount > max) throw new UserError("Amount more than LNURL-withdraw maximum"); } else { const amount = (await exactOutAmountPromise)!; abortController.signal.throwIfAborted(); if((amount * 95n / 100n) < min) throw new UserError("Amount less than LNURL-withdraw minimum"); if((amount * 105n / 100n) > max) throw new UserError("Amount more than LNURL-withdraw maximum"); } return this.create(recipient, amountData, lps, _options, additionalParams, abortSignal, preFetches).map(data => { return { quote: data.quote.then(quote => { quote._setLNURLData( withdrawRequest.url, withdrawRequest.k1, withdrawRequest.callback ); const amountIn = quote.getInput().rawAmount!; if(amountIn < min) throw new UserError("Amount less than LNURL-withdraw minimum"); if(amountIn > max) throw new UserError("Amount more than LNURL-withdraw maximum"); return quote; }), intermediary: data.intermediary } }); } catch (e) { abortController.abort(e); throw e; } } /** * @inheritDoc * @internal */ protected async _checkPastSwaps(pastSwaps: FromBTCLNAutoSwap<T>[]): Promise<{ changedSwaps: FromBTCLNAutoSwap<T>[]; removeSwaps: FromBTCLNAutoSwap<T>[] }> { const changedSwapSet: Set<FromBTCLNAutoSwap<T>> = new Set(); const swapExpiredStatus: {[id: string]: boolean} = {}; const checkStatusSwaps: {[contractVersion: string]: (FromBTCLNAutoSwap<T> & {_data: T["Data"]})[]} = {}; await Promise.all(pastSwaps.map(async (pastSwap) => { if(pastSwap._shouldCheckIntermediary()) { try { const result = await pastSwap._checkIntermediaryPaymentReceived(false); if(result!=null) { changedSwapSet.add(pastSwap); } } catch (e) { this.logger.error(`_checkPastSwaps(): Failed to contact LP regarding swap ${pastSwap.getId()}, error: `, e); } } if(pastSwap._shouldFetchExpiryStatus()) { //Check expiry swapExpiredStatus[pastSwap.getId()] = await pastSwap._verifyQuoteDefinitelyExpired(); } if(pastSwap._shouldFetchOnchainState()) { //Add to swaps for which status should be checked if(pastSwap._data!=null) (checkStatusSwaps[pastSwap._contractVersion ?? "v1"] ??= []).push(pastSwap as FromBTCLNAutoSwap<T> & {_data: T["Data"]}); } })); for(let version in checkStatusSwaps) { if (this._versionedContracts[version] == null) { this.logger.warn(`_checkPastSwaps(): No contract was found for ${this.chainIdentifier} version ${version}! Skipping these swaps!`); continue; } const _checkStatusSwap = checkStatusSwaps[version]; const swapStatuses = await this._contract(version).getCommitStatuses(_checkStatusSwap.map(val => ({signer: val._getInitiator(), swapData: val._data}))); for(let pastSwap of _checkStatusSwap) { const shouldSave = await pastSwap._sync( false, swapExpiredStatus[pastSwap.getId()], swapStatuses[pastSwap.getEscrowHash()!], true ); if(shouldSave) { changedSwapSet.add(pastSwap); } } } const changedSwaps: FromBTCLNAutoSwap<T>[] = []; const removeSwaps: FromBTCLNAutoSwap<T>[] = []; changedSwapSet.forEach(val => { if(val.isQuoteExpired()) { removeSwaps.push(val); } else { changedSwaps.push(val); } }); return { changedSwaps, removeSwaps }; } /** * @inheritDoc */ async recoverFromSwapDataAndState( init: {data: T["Data"], getInitTxId: () => Promise<string>, getTxBlock: () => Promise<{blockTime: number, blockHeight: number}>}, state: SwapCommitState, contractVersion: string, lp?: Intermediary ): Promise<FromBTCLNAutoSwap<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: FromBTCLNAutoSwapInit<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, gasSwapFee: 0n, gasSwapFeeBtc: 0n, initialSwapData: data, data, pr: paymentHash ?? undefined, secret, exactIn: false, contractVersion } const swap = new FromBTCLNAutoSwap(this, swapInit); swap._commitTxId = await init.getInitTxId(); const blockData = await init.getTxBlock(); swap.createdAt = blockData.blockTime * 1000; swap._commitedAt = blockData.blockTime * 1000; swap._setInitiated(); swap._state = FromBTCLNAutoSwapState.CLAIM_COMMITED; await swap._sync(false, false, state); await swap._save(); return swap; } }