UNPKG

@atomiqlabs/sdk

Version:

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

1,044 lines 80.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SpvFromBTCSwap = exports.isSpvFromBTCSwapInit = exports.SpvFromBTCSwapState = void 0; const ISwap_1 = require("../ISwap"); const base_1 = require("@atomiqlabs/base"); const SwapType_1 = require("../../enums/SwapType"); const Utils_1 = require("../../utils/Utils"); const BitcoinUtils_1 = require("../../utils/BitcoinUtils"); const btc_signer_1 = require("@scure/btc-signer"); const buffer_1 = require("buffer"); const IBitcoinWallet_1 = require("../../bitcoin/wallet/IBitcoinWallet"); const FeeType_1 = require("../../enums/FeeType"); const PercentagePPM_1 = require("../../types/fees/PercentagePPM"); const TokenAmount_1 = require("../../types/TokenAmount"); const Token_1 = require("../../types/Token"); const Logger_1 = require("../../utils/Logger"); const TimeoutUtils_1 = require("../../utils/TimeoutUtils"); const PriceInfoType_1 = require("../../types/PriceInfoType"); const BitcoinWalletUtils_1 = require("../../utils/BitcoinWalletUtils"); /** * State enum for SPV vault (UTXO-controlled vault) based swaps * @category Swaps/Bitcoin → Smart chain */ var SpvFromBTCSwapState; (function (SpvFromBTCSwapState) { /** * Catastrophic failure has occurred when processing the swap on the smart chain side, * this implies a bug in the smart contract code or the user and intermediary deliberately * creating a bitcoin transaction with invalid format unparsable by the smart contract. */ SpvFromBTCSwapState[SpvFromBTCSwapState["CLOSED"] = -5] = "CLOSED"; /** * Some of the bitcoin swap transaction inputs were double-spent, this means the swap * has failed and no BTC was sent */ SpvFromBTCSwapState[SpvFromBTCSwapState["FAILED"] = -4] = "FAILED"; /** * The intermediary (LP) declined to co-sign the submitted PSBT, hence the swap failed */ SpvFromBTCSwapState[SpvFromBTCSwapState["DECLINED"] = -3] = "DECLINED"; /** * Swap has expired for good and there is no way how it can be executed anymore */ SpvFromBTCSwapState[SpvFromBTCSwapState["QUOTE_EXPIRED"] = -2] = "QUOTE_EXPIRED"; /** * A swap is almost expired, and it should be presented to the user as expired, though * there is still a chance that it will be processed */ SpvFromBTCSwapState[SpvFromBTCSwapState["QUOTE_SOFT_EXPIRED"] = -1] = "QUOTE_SOFT_EXPIRED"; /** * Swap was created, use the {@link SpvFromBTCSwap.getFundedPsbt} or {@link SpvFromBTCSwap.getPsbt} functions * to get the bitcoin swap PSBT that should be signed by the user's wallet and then submitted via the * {@link SpvFromBTCSwap.submitPsbt} function. */ SpvFromBTCSwapState[SpvFromBTCSwapState["CREATED"] = 0] = "CREATED"; /** * Swap bitcoin PSBT was submitted by the client to the SDK */ SpvFromBTCSwapState[SpvFromBTCSwapState["SIGNED"] = 1] = "SIGNED"; /** * Swap bitcoin PSBT sent to the intermediary (LP), waiting for the intermediary co-sign * it and broadcast. You can use the {@link SpvFromBTCSwap.waitTillClaimedOrFronted} * function to wait till the intermediary broadcasts the transaction and the transaction * confirms. */ SpvFromBTCSwapState[SpvFromBTCSwapState["POSTED"] = 2] = "POSTED"; /** * Intermediary (LP) has co-signed and broadcasted the bitcoin transaction. You can use the * {@link SpvFromBTCSwap.waitTillClaimedOrFronted} function to wait till the transaction * confirms. */ SpvFromBTCSwapState[SpvFromBTCSwapState["BROADCASTED"] = 3] = "BROADCASTED"; /** * Settlement on the destination smart chain was fronted and funds were already received * by the user, even before the final settlement. */ SpvFromBTCSwapState[SpvFromBTCSwapState["FRONTED"] = 4] = "FRONTED"; /** * Bitcoin transaction confirmed with necessary amount of confirmations, wait for automatic * settlement by the watchtower with the {@link waitTillClaimedOrFronted} function, or settle manually * using the {@link FromBTCSwap.claim} or {@link FromBTCSwap.txsClaim} function. */ SpvFromBTCSwapState[SpvFromBTCSwapState["BTC_TX_CONFIRMED"] = 5] = "BTC_TX_CONFIRMED"; /** * Swap settled on the smart chain and funds received */ SpvFromBTCSwapState[SpvFromBTCSwapState["CLAIMED"] = 6] = "CLAIMED"; })(SpvFromBTCSwapState = exports.SpvFromBTCSwapState || (exports.SpvFromBTCSwapState = {})); const SpvFromBTCSwapStateDescription = { [SpvFromBTCSwapState.CLOSED]: "Catastrophic failure has occurred when processing the swap on the smart chain side, this implies a bug in the smart contract code or the user and intermediary deliberately creating a bitcoin transaction with invalid format unparsable by the smart contract.", [SpvFromBTCSwapState.FAILED]: "Some of the bitcoin swap transaction inputs were double-spent, this means the swap has failed and no BTC was sent", [SpvFromBTCSwapState.DECLINED]: "The intermediary (LP) declined to co-sign the submitted PSBT, hence the swap failed", [SpvFromBTCSwapState.QUOTE_EXPIRED]: "Swap has expired for good and there is no way how it can be executed anymore", [SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED]: "A swap is almost expired, and it should be presented to the user as expired, though there is still a chance that it will be processed", [SpvFromBTCSwapState.CREATED]: "Swap was created, get the bitcoin swap PSBT that should be signed by the user's wallet and then submit it back to the SDK.", [SpvFromBTCSwapState.SIGNED]: "Swap bitcoin PSBT was submitted by the client to the SDK", [SpvFromBTCSwapState.POSTED]: "Swap bitcoin PSBT sent to the intermediary (LP), waiting for the intermediary co-sign it and broadcast.", [SpvFromBTCSwapState.BROADCASTED]: "Intermediary (LP) has co-signed and broadcasted the bitcoin transaction.", [SpvFromBTCSwapState.FRONTED]: "Settlement on the destination smart chain was fronted and funds were already received by the user, even before the final settlement.", [SpvFromBTCSwapState.BTC_TX_CONFIRMED]: "Bitcoin transaction confirmed with necessary amount of confirmations, wait for automatic settlement by the watchtower or settle manually.", [SpvFromBTCSwapState.CLAIMED]: "Swap settled on the smart chain and funds received" }; function isSpvFromBTCSwapInit(obj) { return typeof obj === "object" && typeof (obj.quoteId) === "string" && typeof (obj.recipient) === "string" && typeof (obj.vaultOwner) === "string" && typeof (obj.vaultId) === "bigint" && typeof (obj.vaultRequiredConfirmations) === "number" && Array.isArray(obj.vaultTokenMultipliers) && obj.vaultTokenMultipliers.reduce((prev, curr) => prev && typeof (curr) === "bigint", true) && typeof (obj.vaultBtcAddress) === "string" && typeof (obj.vaultUtxo) === "string" && typeof (obj.vaultUtxoValue) === "bigint" && typeof (obj.btcDestinationAddress) === "string" && typeof (obj.btcAmount) === "bigint" && typeof (obj.btcAmountSwap) === "bigint" && typeof (obj.btcAmountGas) === "bigint" && typeof (obj.minimumBtcFeeRate) === "number" && typeof (obj.outputTotalSwap) === "bigint" && typeof (obj.outputSwapToken) === "string" && typeof (obj.outputTotalGas) === "bigint" && typeof (obj.outputGasToken) === "string" && typeof (obj.gasSwapFeeBtc) === "bigint" && typeof (obj.gasSwapFee) === "bigint" && typeof (obj.callerFeeShare) === "bigint" && typeof (obj.frontingFeeShare) === "bigint" && typeof (obj.executionFeeShare) === "bigint" && typeof (obj.genesisSmartChainBlockHeight) === "number" && (obj.gasPricingInfo == null || (0, PriceInfoType_1.isPriceInfoType)(obj.gasPricingInfo)) && (0, ISwap_1.isISwapInit)(obj); } exports.isSpvFromBTCSwapInit = isSpvFromBTCSwapInit; /** * New spv vault (UTXO-controlled vault) based swaps for Bitcoin -> Smart chain swaps not requiring * any initiation on the destination chain, and with the added possibility for the user to receive * a native token on the destination chain as part of the swap (a "gas drop" feature). * * @category Swaps/Bitcoin → Smart chain */ class SpvFromBTCSwap extends ISwap_1.ISwap { constructor(wrapper, initOrObject) { if (isSpvFromBTCSwapInit(initOrObject) && initOrObject.url != null) initOrObject.url += "/frombtc_spv"; super(wrapper, initOrObject); this.currentVersion = 2; this.TYPE = SwapType_1.SwapType.SPV_VAULT_FROM_BTC; /** * @internal */ this.swapStateDescription = SpvFromBTCSwapStateDescription; /** * @internal */ this.swapStateName = (state) => SpvFromBTCSwapState[state]; if (isSpvFromBTCSwapInit(initOrObject)) { this._state = SpvFromBTCSwapState.CREATED; this.quoteId = initOrObject.quoteId; this.recipient = initOrObject.recipient; this.vaultOwner = initOrObject.vaultOwner; this.vaultId = initOrObject.vaultId; this.vaultRequiredConfirmations = initOrObject.vaultRequiredConfirmations; this.vaultTokenMultipliers = initOrObject.vaultTokenMultipliers; this.vaultBtcAddress = initOrObject.vaultBtcAddress; this.vaultUtxo = initOrObject.vaultUtxo; this.vaultUtxoValue = initOrObject.vaultUtxoValue; this.btcDestinationAddress = initOrObject.btcDestinationAddress; this.btcAmount = initOrObject.btcAmount; this.btcAmountSwap = initOrObject.btcAmountSwap; this.btcAmountGas = initOrObject.btcAmountGas; this.minimumBtcFeeRate = initOrObject.minimumBtcFeeRate; this.outputTotalSwap = initOrObject.outputTotalSwap; this.outputSwapToken = initOrObject.outputSwapToken; this.outputTotalGas = initOrObject.outputTotalGas; this.outputGasToken = initOrObject.outputGasToken; this.gasSwapFeeBtc = initOrObject.gasSwapFeeBtc; this.gasSwapFee = initOrObject.gasSwapFee; this.callerFeeShare = initOrObject.callerFeeShare; this.frontingFeeShare = initOrObject.frontingFeeShare; this.executionFeeShare = initOrObject.executionFeeShare; this._genesisSmartChainBlockHeight = initOrObject.genesisSmartChainBlockHeight; this.gasPricingInfo = initOrObject.gasPricingInfo; const vaultAddressType = (0, BitcoinUtils_1.toCoinselectAddressType)((0, BitcoinUtils_1.toOutputScript)(this.wrapper._options.bitcoinNetwork, this.vaultBtcAddress)); if (vaultAddressType !== "p2tr" && vaultAddressType !== "p2wpkh" && vaultAddressType !== "p2wsh") throw new Error("Vault address type must be of witness type: p2tr, p2wpkh, p2wsh"); } else { this.quoteId = initOrObject.quoteId; this.recipient = initOrObject.recipient; this.vaultOwner = initOrObject.vaultOwner; this.vaultId = BigInt(initOrObject.vaultId); this.vaultRequiredConfirmations = initOrObject.vaultRequiredConfirmations; this.vaultTokenMultipliers = initOrObject.vaultTokenMultipliers.map((val) => BigInt(val)); this.vaultBtcAddress = initOrObject.vaultBtcAddress; this.vaultUtxo = initOrObject.vaultUtxo; this.vaultUtxoValue = BigInt(initOrObject.vaultUtxoValue); this.btcDestinationAddress = initOrObject.btcDestinationAddress; this.btcAmount = BigInt(initOrObject.btcAmount); this.btcAmountSwap = BigInt(initOrObject.btcAmountSwap); this.btcAmountGas = BigInt(initOrObject.btcAmountGas); this.minimumBtcFeeRate = initOrObject.minimumBtcFeeRate; this.outputTotalSwap = BigInt(initOrObject.outputTotalSwap); this.outputSwapToken = initOrObject.outputSwapToken; this.outputTotalGas = BigInt(initOrObject.outputTotalGas); this.outputGasToken = initOrObject.outputGasToken; this.gasSwapFeeBtc = BigInt(initOrObject.gasSwapFeeBtc); this.gasSwapFee = BigInt(initOrObject.gasSwapFee); this.callerFeeShare = BigInt(initOrObject.callerFeeShare); this.frontingFeeShare = BigInt(initOrObject.frontingFeeShare); this.executionFeeShare = BigInt(initOrObject.executionFeeShare); this._genesisSmartChainBlockHeight = initOrObject.genesisSmartChainBlockHeight; this._senderAddress = initOrObject.senderAddress; this._claimTxId = initOrObject.claimTxId; this._frontTxId = initOrObject.frontTxId; this.gasPricingInfo = (0, PriceInfoType_1.deserializePriceInfoType)(initOrObject.gasPricingInfo); this.btcTxConfirmedAt = initOrObject.btcTxConfirmedAt; this.posted = initOrObject.posted; if (initOrObject.data != null) this._data = new (this.wrapper._spvWithdrawalDataDeserializer(this._contractVersion))(initOrObject.data); } this.tryCalculateSwapFee(); this.logger = (0, Logger_1.getLogger)("SPVFromBTC(" + this.getId() + "): "); this._contract = wrapper._contract(this._contractVersion); } /** * @inheritDoc * @internal */ upgradeVersion() { if (this.version === 1) { this.posted = this.initiated && this._data != null; this.version = 2; } } /** * @inheritDoc * @internal */ tryCalculateSwapFee() { if (this.swapFeeBtc == null && this.swapFee != null) { this.swapFeeBtc = this.swapFee * this.btcAmountSwap / this.getOutputWithoutFee().rawAmount; } if (this.pricingInfo != null && this.pricingInfo.swapPriceUSatPerToken == null) { const priceUsdPerBtc = this.pricingInfo.realPriceUsdPerBitcoin; this.pricingInfo = this.wrapper._prices.recomputePriceInfoReceive(this.chainIdentifier, this.btcAmountSwap, this.pricingInfo.satsBaseFee, this.pricingInfo.feePPM, this.getOutputWithoutFee().rawAmount, this.outputSwapToken); this.pricingInfo.realPriceUsdPerBitcoin = priceUsdPerBtc; } } ////////////////////////////// //// Pricing /** * @inheritDoc */ async refreshPriceData() { if (this.pricingInfo == null) return; const usdPricePerBtc = this.pricingInfo.realPriceUsdPerBitcoin; this.pricingInfo = await this.wrapper._prices.isValidAmountReceive(this.chainIdentifier, this.btcAmountSwap, this.pricingInfo.satsBaseFee, this.pricingInfo.feePPM, this.getOutputWithoutFee().rawAmount, this.outputSwapToken, undefined, undefined, this.swapFeeBtc); this.pricingInfo.realPriceUsdPerBitcoin = usdPricePerBtc; } ////////////////////////////// //// Getters & utils /** * @inheritDoc * @internal */ _getInitiator() { return this.recipient; } /** * @inheritDoc * @internal */ _getEscrowHash() { return this._data?.btcTx?.txid ?? null; } /** * @inheritDoc */ getId() { return this.quoteId + this._randomNonce; } /** * @inheritDoc */ getQuoteExpiry() { return this.expiry - 20 * 1000; } /** * @inheritDoc * @internal */ _verifyQuoteDefinitelyExpired() { return Promise.resolve(this.expiry < Date.now()); } /** * @inheritDoc * @internal */ _verifyQuoteValid() { return Promise.resolve(this.expiry > Date.now() && (this._state === SpvFromBTCSwapState.CREATED || this._state === SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED)); } /** * @inheritDoc */ getOutputAddress() { return this.recipient; } /** * @inheritDoc */ getOutputTxId() { return this._frontTxId ?? this._claimTxId ?? null; } /** * @inheritDoc */ getInputAddress() { return this._senderAddress ?? null; } /** * @inheritDoc */ getInputTxId() { return this._data?.btcTx?.txid ?? null; } /** * @inheritDoc */ requiresAction() { return this._state === SpvFromBTCSwapState.BTC_TX_CONFIRMED; } /** * @inheritDoc */ isFinished() { return this.isSuccessful() || this.isFailed() || this.isQuoteExpired(); } /** * @inheritDoc */ isClaimable() { return this._state === SpvFromBTCSwapState.BTC_TX_CONFIRMED; } /** * @inheritDoc */ isSuccessful() { return this._state === SpvFromBTCSwapState.FRONTED || this._state === SpvFromBTCSwapState.CLAIMED; } /** * @inheritDoc */ isFailed() { return this._state === SpvFromBTCSwapState.FAILED || this._state === SpvFromBTCSwapState.DECLINED || this._state === SpvFromBTCSwapState.CLOSED; } /** * @inheritDoc */ isInProgress() { return this._state === SpvFromBTCSwapState.POSTED || this._state === SpvFromBTCSwapState.BROADCASTED || this._state === SpvFromBTCSwapState.BTC_TX_CONFIRMED; } /** * @inheritDoc */ isQuoteExpired() { return this._state === SpvFromBTCSwapState.QUOTE_EXPIRED; } /** * @inheritDoc */ isQuoteSoftExpired() { return this._state === SpvFromBTCSwapState.QUOTE_EXPIRED || this._state === SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED; } /** * Returns the data about used spv vault (UTXO-controlled vault) to perform the swap */ getSpvVaultData() { return { owner: this.vaultOwner, vaultId: this.vaultId, utxo: this.vaultUtxo }; } ////////////////////////////// //// Amounts & fees /** * Returns the input BTC amount in sats without any fees * * @internal */ getInputSwapAmountWithoutFee() { return (this.btcAmountSwap - this.swapFeeBtc) * 100000n / (100000n + this.callerFeeShare + this.frontingFeeShare + this.executionFeeShare); } /** * Returns the input gas BTC amount in sats without any fees * * @internal */ getInputGasAmountWithoutFee() { return (this.btcAmountGas - this.gasSwapFeeBtc) * 100000n / (100000n + this.callerFeeShare + this.frontingFeeShare); } /** * Returns to total input BTC amount in sats without any fees (this is BTC amount for the swap + BTC amount * for the gas drop). * * @internal */ getInputAmountWithoutFee() { return this.getInputSwapAmountWithoutFee() + this.getInputGasAmountWithoutFee(); } /** * Returns the swap output amount without any fees, this value is therefore always higher than * the actual received output. * * @internal */ getOutputWithoutFee() { return (0, TokenAmount_1.toTokenAmount)((this.outputTotalSwap * (100000n + this.callerFeeShare + this.frontingFeeShare + this.executionFeeShare) / 100000n) + (this.swapFee ?? 0n), this.wrapper._tokens[this.outputSwapToken], this.wrapper._prices, this.pricingInfo); } /** * Returns the swap fee charged by the intermediary (LP) on this swap * * @internal */ getSwapFee() { if (this.pricingInfo == null) throw new Error("No pricing info known, cannot estimate fee!"); const outputToken = this.wrapper._tokens[this.outputSwapToken]; const gasSwapFeeInOutputToken = this.gasSwapFeeBtc * (10n ** BigInt(outputToken.decimals)) * 1000000n / this.pricingInfo.swapPriceUSatPerToken; const feeWithoutBaseFee = this.swapFeeBtc - this.pricingInfo.satsBaseFee; const swapFeePPM = feeWithoutBaseFee * 1000000n / (this.btcAmount - this.swapFeeBtc - this.gasSwapFeeBtc); const amountInSrcToken = (0, TokenAmount_1.toTokenAmount)(this.swapFeeBtc + this.gasSwapFeeBtc, Token_1.BitcoinTokens.BTC, this.wrapper._prices, this.pricingInfo); return { amountInSrcToken, amountInDstToken: (0, TokenAmount_1.toTokenAmount)(this.swapFee + gasSwapFeeInOutputToken, outputToken, this.wrapper._prices, this.pricingInfo), currentUsdValue: amountInSrcToken.currentUsdValue, usdValue: amountInSrcToken.usdValue, pastUsdValue: amountInSrcToken.pastUsdValue, composition: { base: (0, TokenAmount_1.toTokenAmount)(this.pricingInfo.satsBaseFee, Token_1.BitcoinTokens.BTC, this.wrapper._prices, this.pricingInfo), percentage: (0, PercentagePPM_1.ppmToPercentage)(swapFeePPM) } }; } /** * Returns the fee to be paid to watchtowers on the destination chain to automatically * process and settle this swap without requiring any user interaction * * @internal */ getWatchtowerFee() { if (this.pricingInfo == null) throw new Error("No pricing info known, cannot estimate fee!"); const totalFeeShare = this.callerFeeShare + this.frontingFeeShare; const outputToken = this.wrapper._tokens[this.outputSwapToken]; const watchtowerFeeInOutputToken = this.getInputGasAmountWithoutFee() * totalFeeShare * (10n ** BigInt(outputToken.decimals)) * 1000000n / this.pricingInfo.swapPriceUSatPerToken / 100000n; const feeBtc = this.getInputAmountWithoutFee() * (totalFeeShare + this.executionFeeShare) / 100000n; const amountInSrcToken = (0, TokenAmount_1.toTokenAmount)(feeBtc, Token_1.BitcoinTokens.BTC, this.wrapper._prices, this.pricingInfo); return { amountInSrcToken, amountInDstToken: (0, TokenAmount_1.toTokenAmount)((this.outputTotalSwap * (totalFeeShare + this.executionFeeShare) / 100000n) + watchtowerFeeInOutputToken, outputToken, this.wrapper._prices, this.pricingInfo), currentUsdValue: amountInSrcToken.currentUsdValue, usdValue: amountInSrcToken.usdValue, pastUsdValue: amountInSrcToken.pastUsdValue }; } /** * @inheritDoc */ getFee() { const swapFee = this.getSwapFee(); const watchtowerFee = this.getWatchtowerFee(); const amountInSrcToken = (0, TokenAmount_1.toTokenAmount)(swapFee.amountInSrcToken.rawAmount + watchtowerFee.amountInSrcToken.rawAmount, Token_1.BitcoinTokens.BTC, this.wrapper._prices, this.pricingInfo); return { amountInSrcToken, amountInDstToken: (0, TokenAmount_1.toTokenAmount)(swapFee.amountInDstToken.rawAmount + watchtowerFee.amountInDstToken.rawAmount, this.wrapper._tokens[this.outputSwapToken], this.wrapper._prices, this.pricingInfo), currentUsdValue: amountInSrcToken.currentUsdValue, usdValue: amountInSrcToken.usdValue, pastUsdValue: amountInSrcToken.pastUsdValue }; } /** * @inheritDoc */ getFeeBreakdown() { return [ { type: FeeType_1.FeeType.SWAP, fee: this.getSwapFee() }, { type: FeeType_1.FeeType.NETWORK_OUTPUT, fee: this.getWatchtowerFee() } ]; } /** * @inheritDoc */ getOutputToken() { return this.wrapper._tokens[this.outputSwapToken]; } /** * @inheritDoc */ getOutput() { return (0, TokenAmount_1.toTokenAmount)(this.outputTotalSwap, this.wrapper._tokens[this.outputSwapToken], this.wrapper._prices, this.pricingInfo); } /** * @inheritDoc */ getGasDropOutput() { return (0, TokenAmount_1.toTokenAmount)(this.outputTotalGas, this.wrapper._tokens[this.outputGasToken], this.wrapper._prices, this.gasPricingInfo); } /** * @inheritDoc */ getInputWithoutFee() { return (0, TokenAmount_1.toTokenAmount)(this.getInputAmountWithoutFee(), Token_1.BitcoinTokens.BTC, this.wrapper._prices, this.pricingInfo); } /** * @inheritDoc */ getInputToken() { return Token_1.BitcoinTokens.BTC; } /** * @inheritDoc */ getInput() { return (0, TokenAmount_1.toTokenAmount)(this.btcAmount, Token_1.BitcoinTokens.BTC, this.wrapper._prices, this.pricingInfo); } ////////////////////////////// //// Bitcoin tx /** * @inheritDoc */ getRequiredConfirmationsCount() { return this.vaultRequiredConfirmations; } /** * Returns raw transaction details that can be used to manually create a swap PSBT. It is better to use * the {@link getPsbt} or {@link getFundedPsbt} function retrieve an already prepared PSBT. */ async getTransactionDetails() { const [txId, voutStr] = this.vaultUtxo.split(":"); const vaultScript = (0, BitcoinUtils_1.toOutputScript)(this.wrapper._options.bitcoinNetwork, this.vaultBtcAddress); const out2script = (0, BitcoinUtils_1.toOutputScript)(this.wrapper._options.bitcoinNetwork, this.btcDestinationAddress); const opReturnData = this._contract.toOpReturnData(this.recipient, [ this.outputTotalSwap / this.vaultTokenMultipliers[0], this.outputTotalGas / this.vaultTokenMultipliers[1] ]); const out1script = buffer_1.Buffer.concat([ opReturnData.length > 75 ? buffer_1.Buffer.from([0x6a, 0x4c, opReturnData.length]) : buffer_1.Buffer.from([0x6a, opReturnData.length]), opReturnData ]); if (this.callerFeeShare < 0n || this.callerFeeShare > 0xfffffn) throw new Error("Caller fee out of bounds!"); if (this.frontingFeeShare < 0n || this.frontingFeeShare > 0xfffffn) throw new Error("Fronting fee out of bounds!"); if (this.executionFeeShare < 0n || this.executionFeeShare > 0xfffffn) throw new Error("Execution fee out of bounds!"); const nSequence0 = 0x80000000n | (this.callerFeeShare & 0xfffffn) | (this.frontingFeeShare & 1047552n) << 10n; const nSequence1 = 0x80000000n | (this.executionFeeShare & 0xfffffn) | (this.frontingFeeShare & 1023n) << 20n; return { in0txid: txId, in0vout: parseInt(voutStr), in0sequence: Number(nSequence0), vaultAmount: this.vaultUtxoValue, vaultScript, in1sequence: Number(nSequence1), out1script, out2amount: this.btcAmount, out2script, locktime: 500000000 + Math.floor(Math.random() * 1000000000) //Use this as a random salt to make the btc txId unique! }; } /** * Returns the raw PSBT (not funded), the wallet should fund the PSBT (add its inputs) and importantly **set the nSequence field of the * 2nd input** (input 1 - indexing from 0) to the value returned in `in1sequence`, sign the PSBT and then pass * it back to the swap with {@link submitPsbt} function. The transaction should use at least the returned `feeRate` * sats/vB as the transaction fee. */ async getPsbt() { const res = await this.getTransactionDetails(); const psbt = new btc_signer_1.Transaction({ allowUnknownOutputs: true, allowLegacyWitnessUtxo: true, lockTime: res.locktime }); psbt.addInput({ txid: res.in0txid, index: res.in0vout, witnessUtxo: { amount: res.vaultAmount, script: res.vaultScript }, sequence: res.in0sequence }); psbt.addOutput({ amount: res.vaultAmount, script: res.vaultScript }); psbt.addOutput({ amount: 0n, script: res.out1script }); psbt.addOutput({ amount: res.out2amount, script: res.out2script }); const serializedPsbt = buffer_1.Buffer.from(psbt.toPSBT()); return { psbt, psbtHex: serializedPsbt.toString("hex"), psbtBase64: serializedPsbt.toString("base64"), in1sequence: res.in1sequence, feeRate: this.minimumBtcFeeRate }; } /** * Returns the PSBT that is already funded with wallet's UTXOs (runs a coin-selection algorithm to choose UTXOs to use), * also returns inputs indices that need to be signed by the wallet before submitting the PSBT back to the SDK with * {@link submitPsbt} * * @remarks * Note that when passing the `feeRate` argument, the fee must be at least {@link minimumBtcFeeRate} sats/vB. * * @param _bitcoinWallet Sender's bitcoin wallet * @param feeRate Optional fee rate in sats/vB for the transaction * @param additionalOutputs additional outputs to add to the PSBT - can be used to collect fees from users * @param utxos Pre-fetched list of UTXOs to spend from * @param spendFully Instructs the wallet to spend all the passed UTXOs in the transaction without creating any * change output, if the `feeRate` is passed, it will also enforce that the feeRate in sats/vB for the resulting * transaction is not more than 50% and 10 sats/vB larger (considering also the CPFP adjustments) */ async getFundedPsbt(_bitcoinWallet, feeRate, additionalOutputs, utxos, spendFully) { const bitcoinWallet = (0, BitcoinWalletUtils_1.toBitcoinWallet)(_bitcoinWallet, this.wrapper._btcRpc, this.wrapper._options.bitcoinNetwork); if (feeRate != null) { if (feeRate < this.minimumBtcFeeRate) throw new Error("Bitcoin tx fee needs to be at least " + this.minimumBtcFeeRate + " sats/vB"); } else { feeRate = Math.max(this.minimumBtcFeeRate, await bitcoinWallet.getFeeRate()); } let { psbt, in1sequence } = await this.getPsbt(); if (additionalOutputs != null) additionalOutputs.forEach(output => { psbt.addOutput({ amount: output.amount, script: output.outputScript ?? (0, BitcoinUtils_1.toOutputScript)(this.wrapper._options.bitcoinNetwork, output.address) }); }); psbt = await bitcoinWallet.fundPsbt(psbt, feeRate, utxos, spendFully); psbt.updateInput(1, { sequence: in1sequence }); //Sign every input except the first one const signInputs = []; for (let i = 1; i < psbt.inputsLength; i++) { signInputs.push(i); } const serializedPsbt = buffer_1.Buffer.from(psbt.toPSBT()); return { psbt, psbtHex: serializedPsbt.toString("hex"), psbtBase64: serializedPsbt.toString("base64"), signInputs, feeRate }; } /** * @inheritDoc */ async submitPsbt(_psbt) { const psbt = (0, BitcoinUtils_1.parsePsbtTransaction)(_psbt); //Ensure not expired if (this.expiry < Date.now()) { throw new Error("Quote expired!"); } //Ensure valid state if (this._state !== SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED && this._state !== SpvFromBTCSwapState.CREATED) { throw new Error("Invalid swap state!"); } if (this.url == null) throw new Error("LP URL not known, cannot submit PSBT!"); //Ensure all inputs except the 1st are finalized for (let i = 1; i < psbt.inputsLength; i++) { if ((0, btc_signer_1.getInputType)(psbt.getInput(i)).txType === "legacy") throw new Error("Legacy (non-segwit) inputs are not allowed in the transaction!"); psbt.finalizeIdx(i); } const btcTx = await this.wrapper._btcRpc.parseTransaction(buffer_1.Buffer.from(psbt.toBytes(true)).toString("hex")); const data = await this._contract.getWithdrawalData(btcTx); this.logger.debug("submitPsbt(): parsed withdrawal data: ", data); //Verify correct withdrawal data if (!data.isRecipient(this.recipient) || data.rawAmounts[0] * this.vaultTokenMultipliers[0] !== this.outputTotalSwap || (data.rawAmounts[1] ?? 0n) * this.vaultTokenMultipliers[1] !== this.outputTotalGas || data.callerFeeRate !== this.callerFeeShare || data.frontingFeeRate !== this.frontingFeeShare || data.executionFeeRate !== this.executionFeeShare || data.getSpentVaultUtxo() !== this.vaultUtxo || BigInt(data.getNewVaultBtcAmount()) !== this.vaultUtxoValue || !data.getNewVaultScript().equals((0, BitcoinUtils_1.toOutputScript)(this.wrapper._options.bitcoinNetwork, this.vaultBtcAddress)) || data.getExecutionData() != null) { throw new Error("Invalid withdrawal tx data submitted!"); } //Verify correct LP output const lpOutput = psbt.getOutput(2); if (lpOutput.script == null || lpOutput.amount !== this.btcAmount || !(0, BitcoinUtils_1.toOutputScript)(this.wrapper._options.bitcoinNetwork, this.btcDestinationAddress).equals(buffer_1.Buffer.from(lpOutput.script))) { throw new Error("Invalid LP bitcoin output in transaction!"); } //Verify vault utxo not spent yet if (await this.wrapper._btcRpc.isSpent(this.vaultUtxo)) { throw new Error("Vault UTXO already spent, please create new swap!"); } //Verify tx is parsable by the contract try { await this._contract.checkWithdrawalTx(data); } catch (e) { throw new Error("Transaction not parsable by the contract: " + (e.message ?? e.toString())); } //Ensure still not expired if (this.expiry < Date.now()) { throw new Error("Quote expired!"); } this._data = data; this.initiated = true; this.posted = true; await this._saveAndEmit(SpvFromBTCSwapState.SIGNED); try { await this.wrapper._lpApi.initSpvFromBTC(this.chainIdentifier, this.url, { quoteId: this.quoteId, psbtHex: buffer_1.Buffer.from(psbt.toPSBT(0)).toString("hex") }); await this._saveAndEmit(SpvFromBTCSwapState.POSTED); } catch (e) { await this._saveAndEmit(SpvFromBTCSwapState.DECLINED); throw e; } return this._data.getTxId(); } /** * @inheritDoc */ async estimateBitcoinFee(_bitcoinWallet, feeRate) { const bitcoinWallet = (0, BitcoinWalletUtils_1.toBitcoinWallet)(_bitcoinWallet, this.wrapper._btcRpc, this.wrapper._options.bitcoinNetwork); const txFee = await bitcoinWallet.getFundedPsbtFee((await this.getPsbt()).psbt, feeRate); if (txFee == null) return null; return (0, TokenAmount_1.toTokenAmount)(BigInt(txFee), Token_1.BitcoinTokens.BTC, this.wrapper._prices, this.pricingInfo); } /** * @inheritDoc */ async sendBitcoinTransaction(wallet, feeRate, utxos, spendFully) { const { psbt, psbtBase64, psbtHex, signInputs } = await this.getFundedPsbt(wallet, feeRate, undefined, utxos, spendFully); let signedPsbt; if ((0, IBitcoinWallet_1.isIBitcoinWallet)(wallet)) { signedPsbt = await wallet.signPsbt(psbt, signInputs); } else { signedPsbt = await wallet.signPsbt({ psbt, psbtHex, psbtBase64 }, signInputs); } return await this.submitPsbt(signedPsbt); } /** * Executes the swap with the provided bitcoin wallet * * @param wallet Bitcoin wallet to use to sign the bitcoin transaction * @param callbacks Callbacks to track the progress of the swap * @param options Optional options for the swap like feeRate, AbortSignal, and timeouts/intervals * * @returns {boolean} Whether a swap was settled automatically by swap watchtowers or requires manual claim by the * user, in case `false` is returned the user should call the {@link claim} function to settle the swap on the * destination manually */ async execute(wallet, callbacks, options) { if (this._state === SpvFromBTCSwapState.CLOSED) throw new Error("Swap encountered a catastrophic failure!"); if (this._state === SpvFromBTCSwapState.FAILED) throw new Error("Swap failed!"); if (this._state === SpvFromBTCSwapState.DECLINED) throw new Error("Swap execution already declined by the LP!"); if (this._state === SpvFromBTCSwapState.QUOTE_EXPIRED) throw new Error("Swap quote expired!"); if (this._state === SpvFromBTCSwapState.CLAIMED || this._state === SpvFromBTCSwapState.FRONTED) throw new Error("Swap already settled or fronted!"); if (this._state === SpvFromBTCSwapState.CREATED) { const txId = await this.sendBitcoinTransaction(wallet, options?.feeRate, options?.utxos, options?.spendFully); if (callbacks?.onSourceTransactionSent != null) callbacks.onSourceTransactionSent(txId); } if (this._state === SpvFromBTCSwapState.POSTED || this._state === SpvFromBTCSwapState.BROADCASTED) { const txId = await this.waitForBitcoinTransaction(callbacks?.onSourceTransactionConfirmationStatus, options?.btcTxCheckIntervalSeconds, options?.abortSignal); if (callbacks?.onSourceTransactionConfirmed != null) callbacks.onSourceTransactionConfirmed(txId); } // @ts-ignore if (this._state === SpvFromBTCSwapState.CLAIMED || this._state === SpvFromBTCSwapState.FRONTED) return true; if (this._state === SpvFromBTCSwapState.BTC_TX_CONFIRMED) { const success = await this.waitTillClaimedOrFronted(options?.maxWaitTillAutomaticSettlementSeconds ?? 60, options?.abortSignal); if (success && callbacks?.onSwapSettled != null) callbacks.onSwapSettled(this.getOutputTxId()); return success; } throw new Error("Unexpected state reached!"); } /** * @internal */ async _getExecutionStatus(options) { const state = this._state; const now = Date.now(); let confirmations; let bitcoinPaymentStatus = "inactive"; let destinationSettlementStatus = "inactive"; let buildCurrentAction = async () => undefined; switch (state) { case SpvFromBTCSwapState.QUOTE_EXPIRED: case SpvFromBTCSwapState.DECLINED: case SpvFromBTCSwapState.FAILED: bitcoinPaymentStatus = "expired"; break; case SpvFromBTCSwapState.CREATED: { const quoteValid = await this._verifyQuoteValid(); bitcoinPaymentStatus = quoteValid ? "awaiting" : "soft_expired"; if (quoteValid) { buildCurrentAction = this._buildDepositPsbtAction.bind(this); } break; } case SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED: case SpvFromBTCSwapState.SIGNED: case SpvFromBTCSwapState.POSTED: case SpvFromBTCSwapState.BROADCASTED: case SpvFromBTCSwapState.FRONTED: { const bitcoinPayment = await this.getBitcoinPayment(); let bitcoinConfirmationDelay = -1; let knownBitcoinPaymentStatus; if (bitcoinPayment != null) { if (bitcoinPayment.confirmations >= bitcoinPayment.targetConfirmations) { knownBitcoinPaymentStatus = "confirmed"; } else { const result = await this.wrapper._btcRpc.getConfirmationDelay(bitcoinPayment.btcTx, bitcoinPayment.targetConfirmations); confirmations = { current: bitcoinPayment.confirmations, target: bitcoinPayment.targetConfirmations, etaSeconds: result ?? -1 }; knownBitcoinPaymentStatus = "received"; bitcoinConfirmationDelay = result ?? -1; } } if (state === SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED) bitcoinPaymentStatus = knownBitcoinPaymentStatus ?? "soft_expired"; if (state === SpvFromBTCSwapState.POSTED || state === SpvFromBTCSwapState.SIGNED) bitcoinPaymentStatus = knownBitcoinPaymentStatus ?? "awaiting"; if (state === SpvFromBTCSwapState.BROADCASTED) bitcoinPaymentStatus = knownBitcoinPaymentStatus ?? "received"; destinationSettlementStatus = "inactive"; if (state === SpvFromBTCSwapState.FRONTED) { bitcoinPaymentStatus = knownBitcoinPaymentStatus ?? "received"; destinationSettlementStatus = "settled"; } if (state === SpvFromBTCSwapState.SIGNED || state === SpvFromBTCSwapState.POSTED || state === SpvFromBTCSwapState.BROADCASTED) { buildCurrentAction = this._buildWaitBitcoinConfirmationsAction.bind(this, bitcoinConfirmationDelay); } break; } case SpvFromBTCSwapState.BTC_TX_CONFIRMED: bitcoinPaymentStatus = "confirmed"; if (this.btcTxConfirmedAt == null || options?.maxWaitTillAutomaticSettlementSeconds === 0 || (now - this.btcTxConfirmedAt) > (options?.maxWaitTillAutomaticSettlementSeconds ?? 60) * 1000) { destinationSettlementStatus = "awaiting_manual"; buildCurrentAction = this._buildClaimSmartChainTxAction.bind(this); } else { destinationSettlementStatus = "awaiting_automatic"; buildCurrentAction = this._buildWaitSettlementAction.bind(this, options?.maxWaitTillAutomaticSettlementSeconds); } break; case SpvFromBTCSwapState.CLAIMED: bitcoinPaymentStatus = "confirmed"; destinationSettlementStatus = "settled"; break; case SpvFromBTCSwapState.CLOSED: bitcoinPaymentStatus = "confirmed"; destinationSettlementStatus = "expired"; break; } if (bitcoinPaymentStatus === "confirmed") { confirmations = { current: this.getRequiredConfirmationsCount(), target: this.getRequiredConfirmationsCount(), etaSeconds: 0 }; } return { steps: [ { type: "Payment", side: "source", chain: "BITCOIN", title: "Bitcoin payment", description: "Sign and submit the Bitcoin swap PSBT, then wait for the bitcoin transaction to confirm", status: bitcoinPaymentStatus, confirmations, initTxId: this._data?.btcTx?.txid, settleTxId: bitcoinPaymentStatus === "confirmed" ? this._data?.btcTx?.txid : undefined }, { type: "Settlement", side: "destination", chain: this.chainIdentifier, title: "Destination settlement", description: `Wait for automatic settlement on the ${this.chainIdentifier} side, or settle manually if it takes too long`, status: destinationSettlementStatus, initTxId: this._frontTxId ?? this._claimTxId, settleTxId: this._frontTxId ?? this._claimTxId } ], buildCurrentAction, state }; } /** * @inheritDoc * @internal */ async _submitExecutionTransactions(txs, abortSignal, requiredStates, idempotent) { if (txs.length === 0) throw new Error("Need to submit at least 1 transaction in the array, submitted empty array of transactions!"); // Handle idempotent calls if (idempotent) { let idempotencyTriggered = false; const txIds = []; for (let tx of txs) { let parsedTx; if (typeof (tx) === "string") { try { parsedTx = await this.wrapper._chain.deserializeSignedTx(tx); } catch (e) { } try { parsedTx = (0, BitcoinUtils_1.parsePsbtTransaction)(tx); } catch (e) { } } else { parsedTx = tx; } if (parsedTx == null) { this.logger.debug("_submitExecutionTransactions(): Failed to parse provided execution transaction: ", tx); continue; } if (parsedTx instanceof btc_signer_1.Transaction) { // Bitcoin tx const btcTx = await this.wrapper._btcRpc.parseTransaction(buffer_1.Buffer.from(parsedTx.toBytes(true)).toString("hex")); if (btcTx.txid === this._data?.getTxId()) { if (this._state !== SpvFromBTCSwapState.SIGNED && this._state !== SpvFromBTCSwapState.DECLINED) idempotencyTriggered = true; } txIds.push(btcTx.txid); } else { // SC tx if (this.wrapper._chain.getTxId != null) { const txId = await this.wrapper._chain.getTxId(parsedTx); if (this._claimTxId === txId) idempotencyTriggered = true; txIds.push(txId); } } } if (idempotencyTriggered) return txIds; } if (requiredStates != null && !requiredStates.includes(this._state)) throw new Error("Swap state has changed before transactions were submitted!"); if (this._state === SpvFromBTCSwapState.CREATED || this._state === SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED) { let psbt; if (txs.length !== 1) throw new Error("Need to submit exactly 1 signed PSBT!"); if (typeof (txs[0]) !== "string" && !(txs[0] instanceof btc_signer_1.Transaction)) throw new Error("Must submit a valid PSBT as hex/base64 string or `@scure/btc-signer` Transaction object!"); psbt = txs[0]; return [await this.submitPsbt(psbt)]; } if (this._state === SpvFromBTCSwapState.BTC_TX_CONFIRMED) { const parsedTxs = []; for (let tx of txs) { parsedTxs.push(typeof (tx) === "string" ? await this.wrapper._chain.deserializeSignedTx(tx) : tx); } const txIds = await this.wrapper._chain.sendSignedAndConfirm(parsedTxs, true, abortSignal, false); await this.waitTillClaimed(undefined, abortSignal); return txIds; } throw new Error("Invalid swap state for transaction submission!"); } /** * @internal */ async _buildDepositPsbtAction(actionOptions) { return { type: "SignPSBT", name: "Deposit on Bitcoin", description: "Send funds to the bitcoin swap address", chain: "BITCOIN", txs: [ actionOptions?.bitcoinWallet == null ? { ...await this.getPsbt(), type: "RAW_PSBT" } : { ...await this.getFundedPsbt(actionOptions.bitcoinWallet, actionOptions?.bitcoinFeeRate), type: "FUNDED_PSBT" } ], submitPsbt: async (signedPsbt, idempotent) => { return this._submitExecutionTransactions(Array.isArray(signedPsbt) ? signedPsbt : [signedPsbt], undefined, [SpvFromBTCSwapState.CREATED, SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED], idempotent); } }; } /** * @internal */ async _buildWaitBitcoinConfirmationsAction(confirmationDelay) { return { type: "W