@atomiqlabs/sdk
Version:
atomiq labs SDK for cross-chain swaps between smart chains and bitcoin
1,039 lines • 70.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FromBTCSwap = exports.isFromBTCSwapInit = exports.FromBTCSwapState = void 0;
const IFromBTCSelfInitSwap_1 = require("../IFromBTCSelfInitSwap");
const SwapType_1 = require("../../../../enums/SwapType");
const base_1 = require("@atomiqlabs/base");
const buffer_1 = require("buffer");
const Utils_1 = require("../../../../utils/Utils");
const BitcoinUtils_1 = require("../../../../utils/BitcoinUtils");
const IBitcoinWallet_1 = require("../../../../bitcoin/wallet/IBitcoinWallet");
const btc_signer_1 = require("@scure/btc-signer");
const SingleAddressBitcoinWallet_1 = require("../../../../bitcoin/wallet/SingleAddressBitcoinWallet");
const IEscrowSelfInitSwap_1 = require("../../IEscrowSelfInitSwap");
const TokenAmount_1 = require("../../../../types/TokenAmount");
const Token_1 = require("../../../../types/Token");
const Logger_1 = require("../../../../utils/Logger");
const BitcoinWalletUtils_1 = require("../../../../utils/BitcoinWalletUtils");
/**
* State enum for legacy escrow based Bitcoin -> Smart chain swaps.
*
* @category Swaps/Legacy/Bitcoin → Smart chain
*/
var FromBTCSwapState;
(function (FromBTCSwapState) {
/**
* Bitcoin swap address has expired and the intermediary (LP) has already refunded
* its funds. No BTC should be sent anymore!
*/
FromBTCSwapState[FromBTCSwapState["FAILED"] = -4] = "FAILED";
/**
* Bitcoin swap address has expired, user should not send any BTC anymore! Though
* the intermediary (LP) hasn't refunded yet. So if there is a transaction already
* in-flight the swap might still succeed.
*/
FromBTCSwapState[FromBTCSwapState["EXPIRED"] = -3] = "EXPIRED";
/**
* Swap has expired for good and there is no way how it can be executed anymore
*/
FromBTCSwapState[FromBTCSwapState["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
*/
FromBTCSwapState[FromBTCSwapState["QUOTE_SOFT_EXPIRED"] = -1] = "QUOTE_SOFT_EXPIRED";
/**
* Swap quote was created, use the {@link FromBTCSwap.commit} or {@link FromBTCSwap.txsCommit} functions
* to initiate it by creating the swap escrow on the destination smart chain
*/
FromBTCSwapState[FromBTCSwapState["PR_CREATED"] = 0] = "PR_CREATED";
/**
* Swap escrow was initiated (committed) on the destination chain, user can send the BTC to the
* swap address with the {@link FromBTCSwap.getFundedPsbt}, {@link FromBTCSwap.getAddress} or
* {@link FromBTCSwap.getHyperlink} functions.
*/
FromBTCSwapState[FromBTCSwapState["CLAIM_COMMITED"] = 1] = "CLAIM_COMMITED";
/**
* Input bitcoin transaction was confirmed, wait for automatic settlement by the watchtowers
* using the {@link FromBTCSwap.waitTillClaimed} function or settle manually using the {@link FromBTCSwap.claim}
* or {@link FromBTCSwap.txsClaim} function.
*/
FromBTCSwapState[FromBTCSwapState["BTC_TX_CONFIRMED"] = 2] = "BTC_TX_CONFIRMED";
/**
* Swap successfully settled and funds received on the destination chain
*/
FromBTCSwapState[FromBTCSwapState["CLAIM_CLAIMED"] = 3] = "CLAIM_CLAIMED";
})(FromBTCSwapState = exports.FromBTCSwapState || (exports.FromBTCSwapState = {}));
const FromBTCSwapStateDescription = {
[FromBTCSwapState.FAILED]: "Bitcoin swap address has expired and the intermediary (LP) has already refunded its funds. No BTC should be sent anymore!",
[FromBTCSwapState.EXPIRED]: "Bitcoin swap address has expired, user should not send any BTC anymore! Though the intermediary (LP) hasn't refunded yet. So if there is a transaction already in-flight the swap might still succeed.",
[FromBTCSwapState.QUOTE_EXPIRED]: "Swap has expired for good and there is no way how it can be executed anymore",
[FromBTCSwapState.QUOTE_SOFT_EXPIRED]: "The swap is expired, though there is still a chance that it will be processed",
[FromBTCSwapState.PR_CREATED]: "Swap quote was created, initiate it by creating the swap escrow on the destination smart chain",
[FromBTCSwapState.CLAIM_COMMITED]: "Swap escrow was initiated (committed) on the destination chain, user can send the BTC to the Bitcoin swap address.",
[FromBTCSwapState.BTC_TX_CONFIRMED]: "Input bitcoin transaction was confirmed, wait for automatic settlement by the watchtower or settle manually.",
[FromBTCSwapState.CLAIM_CLAIMED]: "Swap successfully settled and funds received on the destination chain"
};
function isFromBTCSwapInit(obj) {
return typeof (obj.data) === "object" &&
(obj.address == null || typeof (obj.address) === "string") &&
(obj.amount == null || typeof (obj.amount) === "bigint") &&
(obj.requiredConfirmations == null || typeof (obj.requiredConfirmations) === "number") &&
(0, IEscrowSelfInitSwap_1.isIEscrowSelfInitSwapInit)(obj);
}
exports.isFromBTCSwapInit = isFromBTCSwapInit;
/**
* Legacy escrow (PrTLC) based swap for Bitcoin -> Smart chains, requires manual initiation
* of the swap escrow on the destination chain.
*
* @category Swaps/Legacy/Bitcoin → Smart chain
*/
class FromBTCSwap extends IFromBTCSelfInitSwap_1.IFromBTCSelfInitSwap {
constructor(wrapper, initOrObject) {
if (isFromBTCSwapInit(initOrObject) && initOrObject.url != null)
initOrObject.url += "/frombtc";
super(wrapper, initOrObject);
this.TYPE = SwapType_1.SwapType.FROM_BTC;
/**
* @internal
*/
this.swapStateName = (state) => FromBTCSwapState[state];
/**
* @internal
*/
this.swapStateDescription = FromBTCSwapStateDescription;
/**
* @internal
*/
this.inputToken = Token_1.BitcoinTokens.BTC;
if (isFromBTCSwapInit(initOrObject)) {
this._state = FromBTCSwapState.PR_CREATED;
this._data = initOrObject.data;
this.feeRate = initOrObject.feeRate;
this.address = initOrObject.address;
this.amount = initOrObject.amount;
this.requiredConfirmations = initOrObject.requiredConfirmations;
}
else {
this.address = initOrObject.address;
this.amount = (0, Utils_1.toBigInt)(initOrObject.amount);
this.senderAddress = initOrObject.senderAddress;
this.txId = initOrObject.txId;
this.vout = initOrObject.vout;
this.requiredConfirmations = initOrObject.requiredConfirmations ?? this._data.getConfirmationsHint();
this.btcTxConfirmedAt = initOrObject.btcTxConfirmedAt;
}
this.tryRecomputeSwapPrice();
this.logger = (0, Logger_1.getLogger)("FromBTC(" + this.getIdentifierHashString() + "): ");
}
/**
* @inheritDoc
* @internal
*/
getSwapData() {
return this._data;
}
/**
* @inheritDoc
* @internal
*/
upgradeVersion() {
if (this.version == null) {
switch (this._state) {
case -2:
this._state = FromBTCSwapState.FAILED;
break;
case -1:
this._state = FromBTCSwapState.QUOTE_EXPIRED;
break;
case 0:
this._state = FromBTCSwapState.PR_CREATED;
break;
case 1:
this._state = FromBTCSwapState.CLAIM_COMMITED;
break;
case 2:
this._state = FromBTCSwapState.BTC_TX_CONFIRMED;
break;
case 3:
this._state = FromBTCSwapState.CLAIM_CLAIMED;
break;
}
this.version = 1;
}
}
//////////////////////////////
//// Getters & utils
/**
* Returns bitcoin address where the on-chain BTC should be sent to
*/
getAddress() {
if (this._state === FromBTCSwapState.PR_CREATED)
throw new Error("Cannot get bitcoin address of non-initiated swaps! Initiate swap first with commit() or txsCommit().");
return this.address ?? "";
}
/**
* Unsafe bitcoin hyperlink getter, returns the address even before the swap is committed!
*
* @private
*/
_getHyperlink() {
return this.address == null || this.amount == null ? "" : "bitcoin:" + this.address + "?amount=" + encodeURIComponent((Number(this.amount) / 100000000).toString(10));
}
/**
* @inheritDoc
*/
getHyperlink() {
if (this._state === FromBTCSwapState.PR_CREATED)
throw new Error("Cannot get bitcoin address of non-initiated swaps! Initiate swap first with commit() or txsCommit().");
return this._getHyperlink();
}
/**
* @inheritDoc
*/
getInputAddress() {
return this.senderAddress ?? null;
}
/**
* @inheritDoc
*/
getInputTxId() {
return this.txId ?? null;
}
async _setSubmittedBitcoinTx(txId, psbt) {
let changed = false;
if (this.txId !== txId) {
this.txId = txId;
changed = true;
}
const submittedVout = this.address == null || this.amount == null || psbt == null
? undefined
: (0, BitcoinUtils_1.getVoutIndex)(psbt, this.wrapper._options.bitcoinNetwork, this.address, this.amount);
if (submittedVout != null && this.vout !== submittedVout) {
this.vout = submittedVout;
changed = true;
}
const submittedSenderAddress = psbt == null
? undefined
: (0, BitcoinUtils_1.getSenderAddress)(psbt, this.wrapper._options.bitcoinNetwork);
if (submittedSenderAddress != null && this.senderAddress !== submittedSenderAddress) {
this.senderAddress = submittedSenderAddress;
changed = true;
}
if (changed)
await this._saveAndEmit();
}
/**
* Returns timeout time (in UNIX milliseconds) when the on-chain address will expire and no funds should be sent
* to that address anymore
*/
getTimeoutTime() {
return Number(this.wrapper._getOnchainSendTimeout(this._data, this.requiredConfirmations ?? 6)) * 1000;
}
/**
* @inheritDoc
*/
requiresAction() {
return this.isClaimable() || (this._state === FromBTCSwapState.CLAIM_COMMITED && this.getTimeoutTime() > Date.now() && this.txId == null);
}
/**
* @inheritDoc
*/
isFinished() {
return this._state === FromBTCSwapState.CLAIM_CLAIMED || this._state === FromBTCSwapState.QUOTE_EXPIRED || this._state === FromBTCSwapState.FAILED;
}
/**
* @inheritDoc
*/
isClaimable() {
return this._state === FromBTCSwapState.BTC_TX_CONFIRMED;
}
/**
* @inheritDoc
*/
isSuccessful() {
return this._state === FromBTCSwapState.CLAIM_CLAIMED;
}
/**
* @inheritDoc
*/
isFailed() {
return this._state === FromBTCSwapState.FAILED || this._state === FromBTCSwapState.EXPIRED;
}
/**
* @inheritDoc
*/
isInProgress() {
return this._state === FromBTCSwapState.CLAIM_COMMITED ||
this._state === FromBTCSwapState.BTC_TX_CONFIRMED;
}
/**
* @inheritDoc
*/
isQuoteExpired() {
return this._state === FromBTCSwapState.QUOTE_EXPIRED;
}
/**
* @inheritDoc
*/
isQuoteSoftExpired() {
return this._state === FromBTCSwapState.QUOTE_EXPIRED || this._state === FromBTCSwapState.QUOTE_SOFT_EXPIRED;
}
/**
* @inheritDoc
* @internal
*/
canCommit(skipQuoteExpiryChecks) {
if (this._state !== FromBTCSwapState.PR_CREATED && (!skipQuoteExpiryChecks || this._state !== FromBTCSwapState.QUOTE_SOFT_EXPIRED))
return false;
if (this.requiredConfirmations == null)
return false;
const expiry = this.wrapper._getOnchainSendTimeout(this._data, this.requiredConfirmations);
const currentTimestamp = BigInt(Math.floor(Date.now() / 1000));
return (expiry - currentTimestamp) >= this.wrapper._options.minSendWindow;
}
//////////////////////////////
//// Amounts & fees
/**
* @inheritDoc
*/
getInputToken() {
return Token_1.BitcoinTokens.BTC;
}
/**
* @inheritDoc
*/
getInput() {
return (0, TokenAmount_1.toTokenAmount)(this.amount ?? null, this.inputToken, this.wrapper._prices);
}
/**
* Returns claimer bounty, acting as a reward for watchtowers to claim the swap automatically,
* this amount is pre-funded by the user on the destination chain when the swap escrow
* is initiated. For total pre-funded deposit amount see {@link getTotalDeposit}.
*/
getClaimerBounty() {
return (0, TokenAmount_1.toTokenAmount)(this._data.getClaimerBounty(), this.wrapper._tokens[this._data.getDepositToken()], this.wrapper._prices);
}
//////////////////////////////
//// Bitcoin tx
/**
* If the required number of confirmations is not known, this function tries to infer it by looping through
* possible confirmation targets and comparing the claim hashes
*
* @param btcTx Bitcoin transaction
* @param vout Output index of the desired output in the bitcoin transaction
*
* @private
*/
inferRequiredConfirmationsCount(btcTx, vout) {
const txOut = btcTx.outs[vout];
for (let i = 1; i <= 20; i++) {
const computedClaimHash = this._contract.getHashForOnchain(buffer_1.Buffer.from(txOut.scriptPubKey.hex, "hex"), BigInt(txOut.value), i);
if (computedClaimHash.toString("hex") === this._data.getClaimHash()) {
return i;
}
}
}
/**
* @inheritDoc
*/
getRequiredConfirmationsCount() {
return this.requiredConfirmations ?? NaN;
}
/**
* Checks whether a bitcoin payment was already made, returns the payment or `null` when no payment has been made.
*
* @internal
*/
async getBitcoinPayment() {
const txoHashHint = this._data.getTxoHashHint();
if (txoHashHint == null)
throw new Error("Swap data doesn't include the txo hash hint! Cannot check bitcoin transaction!");
if (this.address == null)
throw new Error("Cannot check bitcoin payment, because the address is not known! This can happen after a swap is recovered.");
const result = await this.wrapper._btcRpc.checkAddressTxos(this.address, buffer_1.Buffer.from(txoHashHint, "hex"));
if (result == null)
return null;
if (this.requiredConfirmations == null) {
this.requiredConfirmations = this.inferRequiredConfirmationsCount(result.tx, result.vout);
}
return {
inputAddresses: result.tx.inputAddresses,
txId: result.tx.txid,
vout: result.vout,
confirmations: result.tx.confirmations ?? 0,
targetConfirmations: this.getRequiredConfirmationsCount()
};
}
/**
* Used to set the txId of the bitcoin payment from the on-chain events listener
*
* @param txId Transaction ID that settled the swap on the smart chain
*
* @internal
*/
async _setBitcoinTxId(txId) {
if (this.txId !== txId || this.address == null || this.vout == null || this.senderAddress == null || this.amount == null) {
const btcTx = await this.wrapper._btcRpc.getTransaction(txId);
if (btcTx == null)
return;
const txoHashHint = this._data.getTxoHashHint();
if (txoHashHint != null) {
const expectedTxoHash = buffer_1.Buffer.from(txoHashHint, "hex");
const vout = btcTx.outs.findIndex(out => (0, Utils_1.getTxoHash)(out.scriptPubKey.hex, out.value).equals(expectedTxoHash));
if (vout !== -1) {
this.vout = vout;
//If amount or address are not known, parse them from the bitcoin tx
// this can happen if the swap is recovered from on-chain data and
// hence doesn't contain the address and amount data
if (this.amount == null)
this.amount = BigInt(btcTx.outs[vout].value);
if (this.address == null)
try {
this.address = (0, BitcoinUtils_1.fromOutputScript)(this.wrapper._options.bitcoinNetwork, btcTx.outs[vout].scriptPubKey.hex);
}
catch (e) {
this.logger.warn("_setBitcoinTxId(): Failed to parse address from output script: ", e);
}
if (this.requiredConfirmations == null) {
this.requiredConfirmations = this.inferRequiredConfirmationsCount(btcTx, vout);
}
}
}
if (btcTx.inputAddresses != null) {
this.senderAddress = btcTx.inputAddresses[0];
}
}
this.txId = txId;
}
/**
* @inheritDoc
*
* @throws {Error} if in invalid state (must be {@link FromBTCSwapState.CLAIM_COMMITED})
*/
async waitForBitcoinTransaction(updateCallback, checkIntervalSeconds, abortSignal) {
if (this._state !== FromBTCSwapState.CLAIM_COMMITED && this._state !== FromBTCSwapState.EXPIRED)
throw new Error("Must be in COMMITED state!");
const txoHashHint = this._data.getTxoHashHint();
if (txoHashHint == null)
throw new Error("Swap data doesn't include the txo hash hint! Cannot check bitcoin transaction!");
if (this.address == null)
throw new Error("Cannot check bitcoin payment, because the address is not known! This can happen after a swap is recovered.");
let abortedDueToEnoughConfirmationsResult;
const abortController = (0, Utils_1.extendAbortController)(abortSignal);
const result = await this.wrapper._btcRpc.waitForAddressTxo(this.address, buffer_1.Buffer.from(txoHashHint, "hex"), this.requiredConfirmations ?? 6, //In case confirmation count is not known, we use a conservative estimate
(btcTx, vout, txEtaMs) => {
let requiredConfirmations = this.requiredConfirmations;
if (btcTx != null && vout != null && requiredConfirmations == null) {
requiredConfirmations = this.inferRequiredConfirmationsCount(btcTx, vout);
}
if (btcTx != null && (btcTx.txid !== this.txId ||
this.vout == null ||
this.senderAddress == null ||
(this.requiredConfirmations == null && requiredConfirmations != null))) {
this.txId = btcTx.txid;
this.vout = vout;
this.requiredConfirmations = requiredConfirmations;
if (btcTx.inputAddresses != null)
this.senderAddress = btcTx.inputAddresses[0];
this._saveAndEmit().catch(e => {
this.logger.error("waitForBitcoinTransaction(): Failed to save swap from within waitForAddressTxo callback:", e);
});
}
//Abort the loop as soon as the transaction gets enough confirmations, this is required in case
// we pass a default 6 confirmations to the fn, but then are able to infer the actual confirmation
// target from the prior block
if (btcTx?.confirmations != null && requiredConfirmations != null && requiredConfirmations <= btcTx.confirmations && vout != null) {
abortedDueToEnoughConfirmationsResult = {
tx: btcTx,
vout
};
abortController.abort();
return;
}
if (updateCallback != null)
updateCallback(btcTx?.txid, btcTx == null ? undefined : (btcTx?.confirmations ?? 0), requiredConfirmations ?? NaN, txEtaMs);
}, abortController.signal, checkIntervalSeconds).catch(e => {
//We catch the case when the loop was aborted due to the transaction getting enough confirmations
if (abortedDueToEnoughConfirmationsResult != null)
return abortedDueToEnoughConfirmationsResult;
throw e;
});
if (abortSignal != null)
abortSignal.throwIfAborted();
this.txId = result.tx.txid;
this.vout = result.vout;
if (result.tx.inputAddresses != null)
this.senderAddress = result.tx.inputAddresses[0];
if (this._state !== FromBTCSwapState.CLAIM_CLAIMED &&
this._state !== FromBTCSwapState.FAILED) {
this.btcTxConfirmedAt ??= Date.now();
this._state = FromBTCSwapState.BTC_TX_CONFIRMED;
}
await this._saveAndEmit();
return result.tx.txid;
}
/**
* Private getter of the funded PSBT that doesn't check current state
*
* @param _bitcoinWallet Bitcoin wallet to fund the PSBT with
* @param feeRate Optional bitcoin fee rate in sats/vB
* @param additionalOutputs Optional additional outputs that should also be included in the generated PSBT
*
* @private
*/
async _getFundedPsbt(_bitcoinWallet, feeRate, additionalOutputs) {
if (this.address == null)
throw new Error("Cannot create funded PSBT, because the address is not known! This can happen after a swap is recovered.");
let bitcoinWallet;
if ((0, IBitcoinWallet_1.isIBitcoinWallet)(_bitcoinWallet)) {
bitcoinWallet = _bitcoinWallet;
}
else {
bitcoinWallet = new SingleAddressBitcoinWallet_1.SingleAddressBitcoinWallet(this.wrapper._btcRpc, this.wrapper._options.bitcoinNetwork, _bitcoinWallet);
}
//TODO: Maybe re-introduce fee rate check here if passed from the user
if (feeRate == null) {
feeRate = await bitcoinWallet.getFeeRate();
}
const basePsbt = new btc_signer_1.Transaction({
allowUnknownOutputs: true,
allowLegacyWitnessUtxo: true
});
basePsbt.addOutput({
amount: this.amount,
script: (0, BitcoinUtils_1.toOutputScript)(this.wrapper._options.bitcoinNetwork, this.address)
});
if (additionalOutputs != null)
additionalOutputs.forEach(output => {
basePsbt.addOutput({
amount: output.amount,
script: output.outputScript ?? (0, BitcoinUtils_1.toOutputScript)(this.wrapper._options.bitcoinNetwork, output.address)
});
});
const psbt = await bitcoinWallet.fundPsbt(basePsbt, feeRate);
//Sign every input
const signInputs = [];
for (let i = 0; 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
*/
getFundedPsbt(_bitcoinWallet, feeRate, additionalOutputs) {
if (this._state !== FromBTCSwapState.CLAIM_COMMITED)
throw new Error("Swap not committed yet, please initiate the swap first with commit() call!");
if (this.txId != null)
throw new Error("Bitcoin transaction already submitted for this swap!");
return this._getFundedPsbt(_bitcoinWallet, feeRate, additionalOutputs);
}
/**
* @inheritDoc
*
* @throws {Error} if the swap is in invalid state (not in {@link FromBTCSwapState.CLAIM_COMMITED}), or if
* the swap bitcoin address already expired.
*/
async submitPsbt(_psbt) {
const psbt = (0, BitcoinUtils_1.parsePsbtTransaction)(_psbt);
if (this._state !== FromBTCSwapState.CLAIM_COMMITED)
throw new Error("Swap not committed yet, please initiate the swap first with commit() call!");
if (this.txId != null)
throw new Error("Bitcoin transaction already submitted for this swap!");
//Ensure not expired
if (this.getTimeoutTime() < Date.now()) {
throw new Error("Swap address expired!");
}
const output0 = psbt.getOutput(0);
if (this.amount != null && output0.amount !== this.amount)
throw new Error("PSBT output amount invalid, expected: " + this.amount + " got: " + output0.amount);
if (this.address != null) {
const expectedOutputScript = (0, BitcoinUtils_1.toOutputScript)(this.wrapper._options.bitcoinNetwork, this.address);
if (output0.script == null || !expectedOutputScript.equals(output0.script))
throw new Error("PSBT output script invalid!");
}
if (!psbt.isFinal)
psbt.finalize();
const txId = await this.wrapper._btcRpc.sendRawTransaction(buffer_1.Buffer.from(psbt.toBytes(true, true)).toString("hex"));
await this._setSubmittedBitcoinTx(txId, psbt);
return txId;
}
/**
* @inheritDoc
*/
async estimateBitcoinFee(_bitcoinWallet, feeRate) {
if (this.address == null || this.amount == null)
return null;
const bitcoinWallet = (0, BitcoinWalletUtils_1.toBitcoinWallet)(_bitcoinWallet, this.wrapper._btcRpc, this.wrapper._options.bitcoinNetwork);
const txFee = await bitcoinWallet.getTransactionFee(this.address, this.amount, feeRate);
if (txFee == null)
return null;
return (0, TokenAmount_1.toTokenAmount)(BigInt(txFee), Token_1.BitcoinTokens.BTC, this.wrapper._prices);
}
/**
* @inheritDoc
*/
async sendBitcoinTransaction(wallet, feeRate) {
if (this.address == null || this.amount == null)
throw new Error("Cannot send bitcoin transaction, because the address is not known! This can happen after a swap is recovered.");
if (this._state !== FromBTCSwapState.CLAIM_COMMITED)
throw new Error("Swap not committed yet, please initiate the swap first with commit() call!");
if (this.txId != null)
throw new Error("Bitcoin transaction already submitted for this swap!");
//Ensure not expired
if (this.getTimeoutTime() < Date.now()) {
throw new Error("Swap address expired!");
}
if ((0, IBitcoinWallet_1.isIBitcoinWallet)(wallet)) {
const txId = await wallet.sendTransaction(this.address, this.amount, feeRate);
await this._setSubmittedBitcoinTx(txId);
return txId;
}
else {
const { psbt, psbtHex, psbtBase64, signInputs } = await this.getFundedPsbt(wallet, feeRate);
const signedPsbt = await wallet.signPsbt({
psbt, psbtHex, psbtBase64
}, signInputs);
return await this.submitPsbt(signedPsbt);
}
}
//////////////////////////////
//// Execution
/**
* Executes the swap with the provided bitcoin wallet,
*
* @param dstSigner Signer on the destination network, needs to have the same address as the one specified when
* quote was created, this is required for legacy swaps because the destination wallet needs to actively open
* a bitcoin swap address to which the BTC is then sent, this means that the address also needs to have enough
* native tokens to pay for gas on the destination network
* @param wallet Bitcoin wallet to use to sign the bitcoin transaction, can also be null - then the execution waits
* till a transaction is received from an external wallet
* @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 `swap.claim()` to settle the swap on the destination manually
*/
async execute(dstSigner, wallet, callbacks, options) {
if (this._state === FromBTCSwapState.FAILED)
throw new Error("Swap failed!");
if (this._state === FromBTCSwapState.EXPIRED)
throw new Error("Swap address expired!");
if (this._state === FromBTCSwapState.QUOTE_EXPIRED || this._state === FromBTCSwapState.QUOTE_SOFT_EXPIRED)
throw new Error("Swap quote expired!");
if (this._state === FromBTCSwapState.CLAIM_CLAIMED)
throw new Error("Swap already settled!");
if (this._state === FromBTCSwapState.PR_CREATED) {
await this.commit(dstSigner, options?.abortSignal, undefined, callbacks?.onDestinationCommitSent);
}
if (this._state === FromBTCSwapState.CLAIM_COMMITED) {
if (wallet != null) {
const bitcoinPaymentSent = await this.getBitcoinPayment();
if (bitcoinPaymentSent == null && this.txId == null) {
//Send btc tx
const txId = await this.sendBitcoinTransaction(wallet, options?.feeRate);
if (callbacks?.onSourceTransactionSent != null)
callbacks.onSourceTransactionSent(txId);
}
}
const txId = await this.waitForBitcoinTransaction(callbacks?.onSourceTransactionConfirmationStatus, options?.btcTxCheckIntervalSeconds, options?.abortSignal);
if (callbacks?.onSourceTransactionConfirmed != null)
callbacks.onSourceTransactionConfirmed(txId);
}
// @ts-ignore
if (this._state === FromBTCSwapState.CLAIM_CLAIMED)
return true;
if (this._state === FromBTCSwapState.BTC_TX_CONFIRMED) {
const success = await this.waitTillClaimed(options?.maxWaitTillAutomaticSettlementSeconds ?? 60, options?.abortSignal);
if (success && callbacks?.onSwapSettled != null)
callbacks.onSwapSettled(this.getOutputTxId());
return success;
}
throw new Error("Invalid state reached!");
}
/**
* @internal
*/
async _getExecutionStatus(options) {
const state = this._state;
const now = Date.now();
const timeoutTime = this.getTimeoutTime();
let confirmations;
let bitcoinTxId;
let destinationSetupStatus = "awaiting";
let bitcoinPaymentStatus = "inactive";
let destinationSettlementStatus = "inactive";
let buildCurrentAction = async () => undefined;
switch (state) {
case FromBTCSwapState.PR_CREATED: {
const quoteValid = await this._verifyQuoteValid();
destinationSetupStatus = quoteValid && timeoutTime >= now ? "awaiting" : "soft_expired";
if (quoteValid && timeoutTime >= now) {
buildCurrentAction = this._buildInitSmartChainTxAction.bind(this);
}
break;
}
case FromBTCSwapState.QUOTE_SOFT_EXPIRED:
destinationSetupStatus = "soft_expired";
break;
case FromBTCSwapState.QUOTE_EXPIRED:
destinationSetupStatus = "expired";
break;
case FromBTCSwapState.CLAIM_COMMITED:
case FromBTCSwapState.EXPIRED:
case FromBTCSwapState.FAILED:
const bitcoinPayment = this.address == null ? null : await this.getBitcoinPayment();
bitcoinTxId = bitcoinPayment?.txId;
let bitcoinConfirmationDelay;
if (bitcoinPayment != null && bitcoinPayment.confirmations < bitcoinPayment.targetConfirmations) {
const tx = await this.wrapper._btcRpc.getTransaction(bitcoinPayment.txId);
const result = tx == null
? null
: await this.wrapper._btcRpc.getConfirmationDelay(tx, bitcoinPayment.targetConfirmations);
bitcoinConfirmationDelay = result ?? -1;
}
destinationSetupStatus = "completed";
if (bitcoinPayment == null) {
if (this.txId != null) {
bitcoinPaymentStatus = state === FromBTCSwapState.FAILED ? "expired" : "received";
if (state !== FromBTCSwapState.FAILED) {
buildCurrentAction = this._buildWaitBitcoinConfirmationsAction.bind(this, -1, "Wait for bitcoin transaction to be picked up by the RPC and confirmed.");
}
}
else {
bitcoinPaymentStatus = "awaiting";
if (state === FromBTCSwapState.EXPIRED)
bitcoinPaymentStatus = "soft_expired";
if (state === FromBTCSwapState.FAILED)
bitcoinPaymentStatus = "expired";
if (state === FromBTCSwapState.CLAIM_COMMITED && timeoutTime >= now &&
this.address != null && this.amount != null) {
buildCurrentAction = this._buildSendToAddressOrSignPsbtAction.bind(this);
}
}
}
else if (bitcoinPayment.confirmations >= bitcoinPayment.targetConfirmations) {
bitcoinPaymentStatus = "confirmed";
if (state !== FromBTCSwapState.FAILED) {
buildCurrentAction = this._buildWaitBitcoinConfirmationsAction.bind(this, bitcoinConfirmationDelay ?? -1, undefined);
}
}
else {
bitcoinPaymentStatus = "received";
confirmations = {
current: bitcoinPayment.confirmations,
target: bitcoinPayment.targetConfirmations,
etaSeconds: bitcoinConfirmationDelay ?? -1
};
if (state !== FromBTCSwapState.FAILED) {
buildCurrentAction = this._buildWaitBitcoinConfirmationsAction.bind(this, bitcoinConfirmationDelay ?? -1, undefined);
}
}
destinationSettlementStatus = state === FromBTCSwapState.FAILED ? "expired" : "inactive";
break;
case FromBTCSwapState.BTC_TX_CONFIRMED:
destinationSetupStatus = "completed";
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 FromBTCSwapState.CLAIM_CLAIMED:
destinationSetupStatus = "completed";
bitcoinPaymentStatus = "confirmed";
destinationSettlementStatus = "settled";
break;
}
if (bitcoinPaymentStatus === "confirmed") {
const requiredConfirmations = this.getRequiredConfirmationsCount();
if (!Number.isNaN(requiredConfirmations)) {
confirmations = {
current: requiredConfirmations,
target: requiredConfirmations,
etaSeconds: 0
};
}
}
return {
steps: [
{
type: "Setup",
side: "destination",
chain: this.chainIdentifier,
title: "Open Bitcoin swap address",
description: `Create the escrow on the ${this.chainIdentifier} side to open the Bitcoin swap address`,
status: destinationSetupStatus,
setupTxId: this._commitTxId
},
{
type: "Payment",
side: "source",
chain: "BITCOIN",
title: "Bitcoin payment",
description: "Send Bitcoin to the swap address and wait for the transaction to confirm",
status: bitcoinPaymentStatus,
confirmations,
initTxId: this.txId ?? bitcoinTxId,
settleTxId: this.txId
},
{
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._commitTxId,
settleTxId: 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!");
if (idempotent) {
// Handle idempotent calls
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.txId)
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._commitTxId === txId || 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 === FromBTCSwapState.CLAIM_COMMITED) {
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 === FromBTCSwapState.PR_CREATED || this._state === FromBTCSwapState.QUOTE_SOFT_EXPIRED) {
if (!await this._verifyQuoteValid())
throw new Error("Quote is already expired!");
if (this.getTimeoutTime() < Date.now())
throw new Error("Swap address already expired or close to expiry!");
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.waitTillCommited(abortSignal);
return txIds;
}
if (this._state === FromBTCSwapState.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 _buildSendToAddressOrSignPsbtAction(actionOptions) {
if (this.address == null)
throw new Error("Bitcoin swap address not known!");
if (this.amount == null)
throw new Error("Bitcoin swap amount not known!");
if (actionOptions?.bitcoinWallet == null) {
return {
type: "SendToAddress",
name: "Deposit on Bitcoin",
description: "Send funds to the bitcoin swap address",
chain: "BITCOIN",
txs: [{
type: "BITCOIN_ADDRESS",
address: this.address,
hyperlink: this._getHyperlink(),
amount: (0, TokenAmount_1.toTokenAmount)(this.amount, Token_1.BitcoinTokens.BTC, this.wrapper._prices)
}],
waitForTransactions: async (maxWaitTimeSeconds, pollIntervalSeconds, abortSignal) => {
let btcTxId;
const abortController = (0, Utils_1.extendAbortController)(abortSignal, maxWaitTimeSeconds, "Timed out waiting for bitcoin transaction");
try {
return await this.waitForBitcoinTransaction((txId) => {
btcTxId = txId;
abortController.abort();
}, pollIntervalSeconds, abortController.signal);
}
catch (e) {
if (btcTxId != null)
return btcTxId;
throw e;
}
}
};
}
return {
type: "SignPSBT",
name: "Deposit on Bitcoin",
description: "Send funds to the bitcoin swap address",
chain: "BITCOIN",
txs: [{
...await this.getFundedPsbt(actionOptions.bitcoinWallet, actionOptions?.bitcoinFeeRate),
type: "FUNDED_PSBT"
}],
submitPsbt: async (signedPsbt, idempotent) => {
return this._submitExecutionTransactions(Array.isArray(signedPsbt) ? signedPsbt : [signedPsbt], undefined, [FromBTCSwapState.CLAIM_COMMITED], idempotent);
}
};
}
/**
* @internal
*/
async _buildWaitBitcoinConfirmationsAction(confirmationDelay, description) {
return {
type: "Wait",
name: "Bitcoin confirmations",
description: description ?? "Wait for bitcoin transaction to confirm",
pollTimeSeconds: 10,
expectedTimeSeconds: confirmationDelay === -1 ? -1 : Math.floor(confirmationDelay / 1000),
wait: async (maxWaitTimeSeconds, pollIntervalSeconds, abortSignal, btcConfirmationsCallback) => {
const abortController = (0, Utils_1.extendAbortController)(abortSignal, maxWaitTimeSeconds, "Timed out waiting for bitcoin transaction to confirm");
await this.waitForBitcoinTransaction(btcConfirmationsCallback, pollIntervalSeconds, abortController.signal);
}
};
}
/**
* @internal
*/
async _buildWaitSettlementAction(maxWaitTillAutomaticSettlementSeconds) {
return {
type: "Wait",
name: "Automatic settlement",
description: "Wait for automatic settlement by the watchtower",
pollTimeSeconds: 5,
expectedTimeSeconds: 10,
wait: async (maxWaitTimeSeconds, pollIntervalSeconds, abortSignal) => {
await this.waitTillClaimed(maxWaitTimeSeconds ?? maxWaitTillAutomaticSettlementSeconds ?? 60, abortSignal, pollIntervalSeconds);
}
};
}
/**
* @internal
*/
async _buildInitSmartChainTxAction(actionOptions) {
return {
type: "SignSmartChainTransaction",
name: "Initiate swap",
description: `Opens up the bitcoin swap address on the ${this.chainIdentifier} side`,
chain: this.chainIdentifier,
txs: await this.prepareTransactions(this.txsCommit(actionOptions?.skipChecks)),
submitTransactions: async (txs, abortSignal, idempotent) => {
return this._submitExecutionTransactions(txs, abortSignal, [FromBTCSwapState.PR_CREATED, FromBTCSwapState.QUOTE_SOFT_EXPIRED], idempotent);
},
requiredSigner: this._getInitiator()
};
}
/**
* @inheritDoc
* @internal
*/
async _buildClaimSmartChainTxAction(actionOptions) {
const signerAddress = await this.wrapper._getSignerAddress(actionOptions?.manualSettlementSmartChainSigner);
return {
type: "SignSmartChainTransaction",
name: "Settle manually",
description: "Manually settle (claim) the swap on the destination smart chain",
chain: this.chainIdentifier,
txs: await this.prepareTransactions(this.txsClaim(actionOptions?.manualSettlementSmartChainSigner)),
submitTransactions: async (txs, abortSignal, idempotent) => {
return this._submitExecutionTransactions(txs, abortSignal, [FromBTCSwapState.BTC_TX_CONFIRMED], idempotent);
},
requiredSigner: signerAddress ?? this._getInitiator()
};
}
/**
* @inheritDoc
*
* @param options.bitcoinFeeRate Optional fee rate to use for the created Bitcoin transaction
* @param options.bitcoinWallet Bitcoin wallet to use, when provided the function returns a funded
* psbt (`"FUNDED_PSBT"`), if not passed just a bitcoin receive address is returned (`"ADDRESS"`)
* @param options.skipChecks Skip checks like making sure init signature is still valid and swap
* wasn't commited yet (this is handled on swap creation, if you commit right after quoting, you
* can use `skipChecks=true`)
* @param options.manualSettlementSmartChainSigner Optional smart chain signer to create a manual claim (settlement) transaction
* @param options.maxWaitTillAutomaticSettlementSeconds Maximum time to wait for an automatic settlement after
* the bitcoin transaction is confirmed (defaults to 60 seconds)
*/
async getExecutionAction(options) {
const executionStatus = await this._getExecutionStatus(options);
return executionStatus.buildCurrentAction(options);
}
/**
* @inheritDoc
*/
async getExecutionStatus(options) {
const executionStatus = await this._getExecutionStatus(options);
return {
steps: executionStatus.steps,
currentAction: options?.skipBuildingAction ? undefined : await executionStatus.buildCurrentAction(options),
stateInfo: this._getStateInfo(executionStatus.state)
};
}
/**
* @inherit