@atomiqlabs/sdk
Version:
atomiq labs SDK for cross-chain swaps between smart chains and bitcoin
1,044 lines • 80.8 kB
JavaScript
"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