UNPKG

@atomiqlabs/sdk

Version:

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

559 lines (481 loc) 31 kB
import {MultiChain, Swapper} from "../swapper/Swapper"; import {ApiEndpoint, createApiEndpoint, toApiAmount, toApiLNURL, toApiToken} from "./ApiTypes"; import {ISwap} from "../swaps/ISwap"; import {serializeAction} from "./SerializedAction"; import {FeeType} from "../enums/FeeType"; import {SwapSide} from "../enums/SwapSide"; import {SwapType} from "../enums/SwapType"; import {MinimalBitcoinWalletInterface} from "../types/wallets/MinimalBitcoinWalletInterface"; import {FromBTCLNSwap, FromBTCLNSwapState} from "../swaps/escrow_swaps/frombtc/ln/FromBTCLNSwap"; import {FromBTCLNAutoSwap, FromBTCLNAutoSwapState} from "../swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoSwap"; import { CreateSwapInput, CreateSwapOutput, GetSpendableBalanceInput, GetSpendableBalanceOutput, GetSupportedTokensInput, GetSupportedTokensOutput, GetSwapCounterTokensInput, GetSwapCounterTokensOutput, GetSwapLimitsInput, GetSwapLimitsOutput, GetSwapStatusInput, GetSwapStatusOutput, ListPendingSwapsInput, ListPendingSwapsOutput, ListSwapOutput, ListSwapsInput, ListSwapsOutput, ParseAddressInput, ParseAddressOutput, SettleWithLnurlInput, SettleWithLnurlOutput, SubmitTransactionInput, SubmitTransactionOutput, SwapOutputBase } from "./ApiEndpoints"; import {SwapExecutionStep} from "../types/SwapExecutionStep"; import {SwapStateInfo} from "../types/SwapStateInfo"; import {IEscrowSwap} from "../swaps/escrow_swaps/IEscrowSwap"; import {ToBTCLNSwap} from "../swaps/escrow_swaps/tobtc/ln/ToBTCLNSwap"; import {isSwapType} from "../utils/SwapUtils"; function requiresSecretRevealForApi(swap: ISwap, state: number): boolean | undefined { if(swap instanceof FromBTCLNSwap) { if(swap.hasSecretPreimage()) return false; return state===FromBTCLNSwapState.PR_PAID || state===FromBTCLNSwapState.CLAIM_COMMITED; } if(swap instanceof FromBTCLNAutoSwap) { if(swap.hasSecretPreimage()) return false; return state===FromBTCLNAutoSwapState.CLAIM_COMMITED; } } function createSwapOutputBase( swap: ISwap, steps: SwapExecutionStep[], stateInfo: SwapStateInfo<number> ): SwapOutputBase { const input = swap.getInput(); const output = swap.getOutput(); const feeBreakdown = swap.getFeeBreakdown(); // Build fees from breakdown const swapFeeEntry = feeBreakdown.find(f => f.type === FeeType.SWAP); const networkFeeEntry = feeBreakdown.find(f => f.type === FeeType.NETWORK_OUTPUT); const flags: any = {}; if(isSwapType(swap, SwapType.TO_BTCLN)) { flags.lightningRecipientIsNonCustodialWallet = swap.isPayingToNonCustodialWallet(); flags.lightningPaymentWillLikelyFail = swap.willLikelyFail(); flags.lightningPaymentWithLongHTLCExpiration = swap.hasLongExpiration(); } return { swapId: swap.getId(), swapType: SwapType[swap.getType()], state: { number: stateInfo.state, name: stateInfo.name, description: stateInfo.description }, quote: { inputAmount: toApiAmount(input), outputAmount: toApiAmount(output), fees: { swap: swapFeeEntry ? toApiAmount(swapFeeEntry.fee.amountInSrcToken) : { amount: "0", rawAmount: "0", decimals: 0, symbol: "", chain: "" }, ...(networkFeeEntry ? { networkOutput: toApiAmount(networkFeeEntry.fee.amountInSrcToken) } : {}) }, expiry: swap.getQuoteExpiry(), outputAddress: swap.getOutputAddress()! }, flags, createdAt: swap.createdAt, steps, ...(swap instanceof ToBTCLNSwap && swap.isLNURL() ? { lnurl: { pay: swap.getLNURL()!, successAction: swap.getSuccessAction() ?? undefined } } : (swap instanceof FromBTCLNSwap || swap instanceof FromBTCLNAutoSwap) && swap.isLNURL() ? { lnurl: { withdraw: swap.getLNURL()!, } } : {}) }; } function createListSwapOutput( swap: ISwap, steps: SwapExecutionStep[], stateInfo: SwapStateInfo<number> ): ListSwapOutput { return { ...createSwapOutputBase(swap, steps, stateInfo), isFinished: swap.isFinished(), isSuccess: swap.isSuccessful(), isFailed: swap.isFailed(), isExpired: swap.isQuoteExpired() }; } function parseSwapSide(side: "INPUT" | "OUTPUT"): SwapSide { return side === "INPUT" ? SwapSide.INPUT : SwapSide.OUTPUT; } export type SwapperApiConfig = { syncOnGetStatus?: boolean, idempotentTxSubmission?: boolean }; export class SwapperApi<T extends MultiChain> { readonly endpoints: { createSwap: ApiEndpoint<CreateSwapInput, CreateSwapOutput, "POST">; listSwaps: ApiEndpoint<ListSwapsInput, ListSwapsOutput, "GET">; listPendingSwaps: ApiEndpoint<ListPendingSwapsInput, ListPendingSwapsOutput, "GET">; getSupportedTokens: ApiEndpoint<GetSupportedTokensInput, GetSupportedTokensOutput, "GET">; getSwapCounterTokens: ApiEndpoint<GetSwapCounterTokensInput, GetSwapCounterTokensOutput, "GET">; getSwapLimits: ApiEndpoint<GetSwapLimitsInput, GetSwapLimitsOutput, "GET">; parseAddress: ApiEndpoint<ParseAddressInput, ParseAddressOutput, "GET">; getSpendableBalance: ApiEndpoint<GetSpendableBalanceInput, GetSpendableBalanceOutput, "GET">; getSwapStatus: ApiEndpoint<GetSwapStatusInput, GetSwapStatusOutput, "GET">; submitTransaction: ApiEndpoint<SubmitTransactionInput, SubmitTransactionOutput, "POST">; settleWithLnurl: ApiEndpoint<SettleWithLnurlInput, SettleWithLnurlOutput, "POST">; }; constructor(private swapper: Swapper<T>, private readonly config?: SwapperApiConfig) { this.config ??= {}; this.config.syncOnGetStatus ??= true; this.endpoints = { createSwap: createApiEndpoint<CreateSwapInput, CreateSwapOutput, "POST">("POST", "Create a new cross-chain atomic swap. Returns a swap object with swapId, swapType, state, createdAt, quote (with inputAmount and outputAmount as ApiAmount objects each having amount/rawAmount/decimals/symbol/chain, a fees breakdown {swap, networkOutput?} of ApiAmount values, expiry, and outputAddress), a steps array, and optional lnurl. After creation, poll getSwapStatus periodically to get the next required action.", this.createSwap.bind(this), { srcToken: { type: "string", required: true, description: "Source token ticker (e.g. 'BITCOIN-BTC', 'LIGHTNING-BTC', 'STARKNET-STRK', 'SOLANA-SOL')" }, dstToken: { type: "string", required: true, description: "Destination token ticker" }, amount: { type: "bigint", required: true, description: "Amount in base units as an integer" }, amountType: { type: "string", required: true, description: "EXACT_IN or EXACT_OUT", allowedValues: ["EXACT_IN", "EXACT_OUT"] }, srcAddress: { type: "string", required: false, description: "Source address (only required for Smart chain -> BTC/Lightning swaps)" }, dstAddress: { type: "string", required: true, description: "Destination address" }, gasAmount: { type: "bigint", required: false, description: "Gas token amount to receive on destination chain, in base units" }, paymentHash: { type: "string", required: false, description: "Custom payment hash for Lightning swaps" }, lightningInvoiceDescription: { type: "string", required: false, description: "Description for Lightning invoice" }, lightningInvoiceDescriptionHash: { type: "string", required: false, description: "Description hash for Lightning invoice (hex)" }, lightningPaymentHTLCTimeout: { type: "number", required: false, description: "Custom expiry time in seconds" }, lightningPaymentMaxHTLCTimeout: { type: "number", required: false, description: "Custom maximum expiry time in seconds" } }), listSwaps: createApiEndpoint<ListSwapsInput, ListSwapsOutput, "GET">("GET", "List all swaps for a given signer address. Returns an array of swap objects, each with swapId, swapType, state, quote, steps, and terminal state flags (isFinished, isSuccess, isFailed, isExpired). Optionally filter by smart chain.", this.listSwaps.bind(this), { signer: { type: "string", required: true, description: "Smart chain signer address to filter swaps for" }, chainId: { type: "string", required: false, description: "Optional smart chain identifier to filter swaps" } }), listPendingSwaps: createApiEndpoint<ListPendingSwapsInput, ListPendingSwapsOutput, "GET">("GET", "List swaps that require user action for a given signer address. Returns an array of swap objects with the same structure as listSwaps.", this.listPendingSwaps.bind(this), { signer: { type: "string", required: true, description: "Smart chain signer address to filter pending swaps for" }, chainId: { type: "string", required: false, description: "Optional smart chain identifier to filter pending swaps" } }), getSupportedTokens: createApiEndpoint<GetSupportedTokensInput, GetSupportedTokensOutput, "GET">("GET", "List all tokens available as swap input or output. Returns an array of ApiToken objects, each with id (e.g. BITCOIN-BTC, LIGHTNING-BTC, STARKNET-STRK), chainId, ticker, name, decimals, and address.", this.getSupportedTokens.bind(this), { side: { type: "string", required: true, description: "Whether to list valid source tokens (INPUT) or destination tokens (OUTPUT)", allowedValues: ["INPUT", "OUTPUT"] } }), getSwapCounterTokens: createApiEndpoint<GetSwapCounterTokensInput, GetSwapCounterTokensOutput, "GET">("GET", "Get tokens that can be swapped against a given token. Returns an array of ApiToken objects (id, chainId, ticker, name, decimals, address). Use to discover valid trading pairs.", this.getSwapCounterTokens.bind(this), { token: { type: "string", required: true, description: "Token identifier accepted by the API, e.g. BITCOIN-BTC, LIGHTNING-BTC, STARKNET-STRK, or a token address" }, side: { type: "string", required: true, description: "Treat the provided token as a source token (INPUT) or destination token (OUTPUT)", allowedValues: ["INPUT", "OUTPUT"] } }), getSwapLimits: createApiEndpoint<GetSwapLimitsInput, GetSwapLimitsOutput, "GET">("GET", "Get minimum and maximum swap amounts for a source/destination token pair. Returns {input: {min, max?}, output: {min, max?}} where each value is an ApiAmount object with amount (decimal string), rawAmount (base units string), decimals, symbol, and chain.", this.getSwapLimits.bind(this), { srcToken: { type: "string", required: true, description: "Source token identifier accepted by the API, e.g. BITCOIN-BTC, LIGHTNING-BTC, STARKNET-STRK" }, dstToken: { type: "string", required: true, description: "Destination token identifier accepted by the API, e.g. BITCOIN-BTC, LIGHTNING-BTC, STARKNET-STRK" } }), parseAddress: createApiEndpoint<ParseAddressInput, ParseAddressOutput, "GET">("GET", "Parse and validate an address, Lightning invoice, LNURL, or Bitcoin URI. Returns {address, type} and optionally lnurl (ApiLNURL with pay/withdraw details), min/max/amount (as ApiAmount objects).", this.parseAddress.bind(this), { address: { type: "string", required: true, description: "Address, invoice, LNURL, or URI string to parse" } }), getSpendableBalance: createApiEndpoint<GetSpendableBalanceInput, GetSpendableBalanceOutput, "GET">("GET", "Get the spendable balance for a wallet address and token, accounting for chain fees. Returns {balance: ApiAmount, feeRate?} where ApiAmount has amount (decimal string), rawAmount (base units string), decimals, symbol, and chain.", this.getSpendableBalance.bind(this), { wallet: { type: "string", required: true, description: "Wallet address to query" }, token: { type: "string", required: true, description: "Token identifier accepted by the API, e.g. BITCOIN-BTC, STARKNET-STRK, or a token address" }, targetChain: { type: "string", required: false, description: "Destination smart chain for Bitcoin SPV-vault fee estimation" }, gasDrop: { type: "boolean", required: false, description: "Whether to include gas-drop footprint when estimating Bitcoin SPV-vault spendable balance" }, feeRate: { type: "string", required: false, description: "Manual fee rate override" }, minBitcoinFeeRate: { type: "number", required: false, description: "Minimum Bitcoin fee rate to enforce" }, feeMultiplier: { type: "number", required: false, description: "Multiplier applied to smart-chain native token commit fee estimate" } }), getSwapStatus: createApiEndpoint<GetSwapStatusInput, GetSwapStatusOutput, "GET">("GET", "Get the current status and next required action for a swap. Returns swap state, terminal flags (isFinished, isSuccess, isFailed, isExpired), and currentAction (an action object, or null when no action is currently required). For Lightning-to-smart-chain swaps it may also return requiresSecretReveal: true — when set, pass the secret parameter in subsequent getSwapStatus calls so the HTLC can be claimed. Handle each action type: SignPSBT — ask user to sign the Bitcoin PSBT with their wallet, then submit via submitTransaction. SignSmartChainTransaction — ask user to sign with their Solana/Starknet/EVM wallet, then submit via submitTransaction. SendToAddress — show the address and amount to the user, they pay externally, keep polling. Wait — poll again after pollTimeSeconds. Poll repeatedly until isFinished is true.", this.getSwapStatus.bind(this), { swapId: { type: "string", required: true, description: "The swap identifier" }, secret: { type: "string", required: false, description: "Revealed swap secret pre-image (in hexadecimal format) for lightning network swaps" }, bitcoinAddress: { type: "string", required: false, description: "Bitcoin wallet address to obtain funded PSBT" }, bitcoinPublicKey: { type: "string", required: false, description: "Bitcoin wallet public key (in hexadecimal format) to obtain funded PSBT" }, bitcoinFeeRate: { type: "number", required: false, description: "Fee rate to use when creating a funded PSBT" }, signer: { type: "string", required: false, description: "Alternative different smart chain signer to use for refunds and manual settlement" } }), submitTransaction: createApiEndpoint<SubmitTransactionInput, SubmitTransactionOutput, "POST">("POST", "Submit signed transaction(s) for a swap. Call this after the user has signed the transaction returned by getSwapStatus. Returns {txHashes: string[]} with the submitted transaction hashes. After submission, continue polling getSwapStatus.", this.submitTransaction.bind(this), { swapId: { type: "string", required: true, description: "The swap identifier" }, signedTxs: { type: "array", required: true, description: "Array of signed transaction data", items: {type: "string", required: true, description: "Single string-serialized & signed transaction"} } }), settleWithLnurl: createApiEndpoint<SettleWithLnurlInput, SettleWithLnurlOutput, "POST">("POST", "Settle a Lightning Network swap using an LNURL-withdraw link. Returns {paymentHash: string} on success.", this.settleWithLnurl.bind(this), { swapId: { type: "string", required: true, description: "The swap identifier" }, lnurlWithdraw: { type: "string", required: false, description: "LNURL-withdraw link to use to settle the Lightning network swap, if the swap was already created with the LNURL-withdraw link, this is optional" } }) }; } private txSerializer(chainId: string, tx: any): Promise<string> { const chain = (this.swapper._chains as any)[chainId]; if (chain == null) throw new Error("Unknown chain: " + chainId); return chain.chainInterface.serializeTx(tx); } async init(): Promise<void> { await this.swapper.init(); } /** * Should be ran periodically, this synchronizes the swap's state with the on-chain data and also purges * expired swaps from the persistent storage */ async sync(): Promise<void> { await this.swapper._syncSwaps(); } /** * Optionally good to run this periodically, such that any LPs that are dropped off because they are unresponsive * can be found again. */ async reloadLps(): Promise<void> { await this.swapper.intermediaryDiscovery.reloadIntermediaries(); } private async createSwap(input: CreateSwapInput): Promise<CreateSwapOutput> { const exactIn = input.amountType === "EXACT_IN"; // Build options from input const options: any = {}; if (input.gasAmount != null) options.gasAmount = input.gasAmount; if (input.paymentHash != null) options.paymentHash = Buffer.from(input.paymentHash, "hex"); if (input.lightningInvoiceDescription != null) options.description = input.lightningInvoiceDescription; if (input.lightningInvoiceDescriptionHash != null) options.descriptionHash = Buffer.from(input.lightningInvoiceDescriptionHash, "hex"); if (input.lightningPaymentHTLCTimeout != null) options.expirySeconds = input.lightningPaymentHTLCTimeout; if (input.lightningPaymentMaxHTLCTimeout != null) options.maxExpirySeconds = input.lightningPaymentMaxHTLCTimeout; // swapper.swap() handles routing based on token types const swap = await this.swapper.swap( input.srcToken, input.dstToken, input.amount, exactIn, input.srcAddress, input.dstAddress, Object.keys(options).length > 0 ? options : undefined ); const {steps, stateInfo} = await swap.getExecutionStatus({skipBuildingAction: true}); return createSwapOutputBase(swap, steps, stateInfo); } private validateSwapListInput(input: ListSwapsInput): void { if (input.chainId != null && !this.swapper.getSmartChains().includes(input.chainId as any)) { throw new Error("Unknown chainId: " + input.chainId); } if (!this.swapper.Utils.isValidSmartChainAddress(input.signer, input.chainId as any)) { throw new Error( input.chainId != null ? `Invalid ${input.chainId} signer address: ` + input.signer : `Invalid smart chain signer address: ` + input.signer ); } } private async createListedSwapOutputs(swaps: ISwap[]): Promise<ListSwapsOutput> { return Promise.all( swaps .filter(swap => swap.getType() !== SwapType.TRUSTED_FROM_BTC) .map(async swap => { const {steps, stateInfo} = await swap.getExecutionStatus({skipBuildingAction: true}); return createListSwapOutput(swap, steps, stateInfo); }) ); } private async listSwaps(input: ListSwapsInput): Promise<ListSwapsOutput> { this.validateSwapListInput(input); const swaps = await this.swapper.getAllSwaps(input.chainId as any, input.signer); return this.createListedSwapOutputs(swaps); } private async listPendingSwaps(input: ListPendingSwapsInput): Promise<ListPendingSwapsOutput> { this.validateSwapListInput(input); const swaps = await this.swapper.getPendingSwaps(input.chainId as any, input.signer); return this.createListedSwapOutputs(swaps); } private async getSupportedTokens(input: GetSupportedTokensInput): Promise<GetSupportedTokensOutput> { return this.swapper.getSupportedTokens(parseSwapSide(input.side)).map(toApiToken); } private async getSwapCounterTokens(input: GetSwapCounterTokensInput): Promise<GetSwapCounterTokensOutput> { const token = this.swapper.getToken(input.token); return this.swapper.getSwapCounterTokens(token, parseSwapSide(input.side)).map(toApiToken); } private async getSwapLimits(input: GetSwapLimitsInput): Promise<GetSwapLimitsOutput> { const srcToken = this.swapper.getToken(input.srcToken); const dstToken = this.swapper.getToken(input.dstToken); let limits = this.swapper.getSwapLimits(srcToken, dstToken); if(dstToken.chainId!=="LIGHTNING") { if(limits.input.min.rawAmount===1n || limits.output.min.rawAmount===1n) { // Execute a dummy swap to get the proper limits try { await this.swapper.swap( srcToken, dstToken, 1n, limits.input.min.rawAmount===1n, srcToken.chainId==="LIGHTNING" ? undefined : this.swapper.Utils.randomAddress(srcToken.chainId), this.swapper.Utils.randomAddress(dstToken.chainId) ); } catch (e) {} limits = this.swapper.getSwapLimits(srcToken, dstToken); } } return { input: { min: toApiAmount(limits.input.min), ...(limits.input.max != null ? {max: toApiAmount(limits.input.max)} : {}) }, output: { min: toApiAmount(limits.output.min), ...(limits.output.max != null ? {max: toApiAmount(limits.output.max)} : {}) } }; } private async parseAddress(input: ParseAddressInput): Promise<ParseAddressOutput> { const result = await this.swapper.Utils.parseAddress(input.address); if(result == null) throw new Error("Invalid address"); return { address: result.address, type: result.type, ...(result.lnurl != null ? {lnurl: toApiLNURL(result.lnurl, this.swapper)} : {}), ...(result.min != null ? {min: toApiAmount(result.min)} : {}), ...(result.max != null ? {max: toApiAmount(result.max)} : {}), ...(result.amount != null ? {amount: toApiAmount(result.amount)} : {}) }; } private async getSpendableBalance(input: GetSpendableBalanceInput): Promise<GetSpendableBalanceOutput> { const token = this.swapper.getToken(input.token); if(token.chainId === "LIGHTNING") throw new Error("Lightning wallet spendable balance is not supported by this endpoint."); if(input.feeRate != null && input.feeMultiplier != null) throw new Error("`feeMultiplier` cannot be specified alongside the `feeRate` parameter."); if(token.chainId === "BITCOIN") { if(input.targetChain != null && !this.swapper.getSmartChains().includes(input.targetChain as any)) { throw new Error("Unknown targetChain: " + input.targetChain); } if (!this.swapper.Utils.isValidBitcoinAddress(input.wallet)) throw new Error(`Invalid BITCOIN wallet address: ` + input.wallet); let btcFeeRate: number; if(input.feeRate != null) { btcFeeRate = parseFloat(input.feeRate); if(isNaN(btcFeeRate) || btcFeeRate <= 0) throw new Error("Bitcoin `feeRate` must be a valid positive number!"); } else btcFeeRate = await this.swapper._bitcoinRpc.getFeeRate() if(input.feeMultiplier != null) btcFeeRate *= input.feeMultiplier; const {balance, feeRate} = await this.swapper.Utils.getBitcoinSpendableBalance(input.wallet, input.targetChain as any, { gasDrop: input.gasDrop, feeRate: btcFeeRate, minFeeRate: input.minBitcoinFeeRate }); return { balance: toApiAmount(balance), feeRate }; } if(input.gasDrop === true) throw new Error("`gasDrop` is only supported for Bitcoin balances."); if(input.minBitcoinFeeRate != null) throw new Error("`minBitcoinFeeRate` is only supported for Bitcoin balances."); if (!this.swapper.Utils.isValidSmartChainAddress(input.wallet, token.chainId)) throw new Error(`Invalid ${token.chainId} wallet address: ` + input.wallet); const balance = await this.swapper.Utils.getSpendableBalance(input.wallet, token as any, { feeMultiplier: input.feeMultiplier, feeRate: input.feeRate }); return { balance: toApiAmount(balance) }; } private async getSwapStatus(input: GetSwapStatusInput): Promise<GetSwapStatusOutput> { const swap = await this.swapper.getSwapById(input.swapId); if (swap == null) { throw new Error("Swap not found: " + input.swapId); } if (input.signer != null && !this.swapper.Utils.isValidSmartChainAddress(input.signer, swap.chainIdentifier)) { throw new Error(`Invalid ${swap.chainIdentifier} signer address: ` + input.signer); } if (input.secret != null) { try { Buffer.from(input.secret, "hex"); } catch (e) { throw new Error(`Invalid secret passed, has to be a hexadecimal string!`); } } let bitcoinWallet: MinimalBitcoinWalletInterface | undefined; if (input.bitcoinAddress != null && input.bitcoinPublicKey != null) { bitcoinWallet = { publicKey: input.bitcoinPublicKey, address: input.bitcoinAddress }; } else if(input.bitcoinAddress != null || input.bitcoinPublicKey != null) { throw new Error("When specifying bitcoin wallet you have to pass both `bitcoinAddress` and `bitcoinPublicKey` params!"); } if (input.bitcoinFeeRate != null) { if(isNaN(input.bitcoinFeeRate)) throw new Error("Bitcoin fee rate passed cannot be NaN!"); if(input.bitcoinFeeRate <= 0) throw new Error("Bitcoin fee rate passed cannot be negative or 0!"); } if(this.config?.syncOnGetStatus) await swap._sync(true); const {steps, stateInfo, currentAction} = await swap.getExecutionStatus({ secret: input.secret, bitcoinWallet, bitcoinFeeRate: input.bitcoinFeeRate, manualSettlementSmartChainSigner: input.signer, refundSmartChainSigner: input.signer }); return { ...createListSwapOutput(swap, steps, stateInfo), currentAction: currentAction ? await serializeAction(currentAction, this.txSerializer.bind(this)) : null, requiresSecretReveal: requiresSecretRevealForApi(swap, stateInfo.state), escrow: swap instanceof IEscrowSwap && swap._data!=null ? { data: swap._data.getEscrowStruct(), initTxId: swap._commitTxId } : undefined }; } private async submitTransaction(input: SubmitTransactionInput, abortSignal?: AbortSignal): Promise<SubmitTransactionOutput> { const swap = await this.swapper.getSwapById(input.swapId); if (swap == null) { throw new Error("Swap not found: " + input.swapId); } return { txHashes: await swap._submitExecutionTransactions(input.signedTxs, abortSignal, undefined, this.config?.idempotentTxSubmission) } } private async settleWithLnurl(input: SettleWithLnurlInput, abortSignal?: AbortSignal): Promise<SettleWithLnurlOutput> { const swap = await this.swapper.getSwapById(input.swapId); if (swap == null) throw new Error("Swap not found: " + input.swapId); if (swap instanceof FromBTCLNAutoSwap) { if(swap._state!==FromBTCLNAutoSwapState.PR_CREATED) throw new Error("Invalid swap state, must be in PR_CREATED state!"); } else if (swap instanceof FromBTCLNSwap) { if(swap._state!==FromBTCLNSwapState.PR_CREATED) throw new Error("Invalid swap state, must be in PR_CREATED state!"); } else { throw new Error("Endpoint only supports swaps from Lightning"); } if (!swap.isLNURL()) { if (input.lnurlWithdraw==null) throw new Error("The swap is not configured to use LNURL, please pass the `lnurlWithdraw` parameter!"); if (!this.swapper.Utils.isValidLNURL(input.lnurlWithdraw)) throw new Error("Invalid LNURL-withdraw link provided: " + input.lnurlWithdraw); await swap.settleWithLNURLWithdraw(input.lnurlWithdraw); } else { if (input.lnurlWithdraw!=null) throw new Error("The swap is already configured with an LNURL link, don't pass the `lnurlWithdraw` parameter!"); } let success: boolean; if (swap instanceof FromBTCLNAutoSwap) { // For non-legacy swap, we don't need to wait till the swap advances all the way to committed state success = await swap._waitForLpPaymentReceived(2, abortSignal); } else { // For legacy swap waitForPayment waits just for the swap to transition into PR_PAID success = await swap.waitForPayment(undefined, 2, abortSignal); } if(!success) throw new Error("Failed to settle the swap with the LNURL-withdraw link!"); return { paymentHash: swap.getInputTxId()! }; } }