@atomiqlabs/sdk-lib
Version:
Basic SDK functionality library for atomiq
604 lines (603 loc) • 34.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SpvFromBTCWrapper = void 0;
const ISwapWrapper_1 = require("../ISwapWrapper");
const base_1 = require("@atomiqlabs/base");
const SpvFromBTCSwap_1 = require("./SpvFromBTCSwap");
const utils_1 = require("@scure/btc-signer/utils");
const SwapType_1 = require("../enums/SwapType");
const Utils_1 = require("../../utils/Utils");
const BitcoinUtils_1 = require("../../utils/BitcoinUtils");
const IntermediaryAPI_1 = require("../../intermediaries/IntermediaryAPI");
const RequestError_1 = require("../../errors/RequestError");
const IntermediaryError_1 = require("../../errors/IntermediaryError");
const btc_signer_1 = require("@scure/btc-signer");
class SpvFromBTCWrapper extends ISwapWrapper_1.ISwapWrapper {
/**
* @param chainIdentifier
* @param unifiedStorage Storage interface for the current environment
* @param unifiedChainEvents On-chain event listener
* @param chain
* @param contract Underlying contract handling the swaps
* @param prices Pricing to use
* @param tokens
* @param spvWithdrawalDataDeserializer Deserializer for SpvVaultWithdrawalData
* @param btcRelay
* @param synchronizer Btc relay synchronizer
* @param btcRpc Bitcoin RPC which also supports getting transactions by txoHash
* @param options
* @param events Instance to use for emitting events
*/
constructor(chainIdentifier, unifiedStorage, unifiedChainEvents, chain, contract, prices, tokens, spvWithdrawalDataDeserializer, btcRelay, synchronizer, btcRpc, options, events) {
if (options == null)
options = {};
options.bitcoinNetwork ??= utils_1.TEST_NETWORK;
options.maxConfirmations ??= 6;
options.bitcoinBlocktime ??= 10 * 60;
options.maxTransactionsDelta ??= 3;
options.maxRawAmountAdjustmentDifferencePPM ??= 100;
options.maxBtcFeeOffset ??= 5;
options.maxBtcFeeMultiplier ??= 1.5;
super(chainIdentifier, unifiedStorage, unifiedChainEvents, chain, prices, tokens, options, events);
this.claimableSwapStates = [SpvFromBTCSwap_1.SpvFromBTCSwapState.BTC_TX_CONFIRMED];
this.TYPE = SwapType_1.SwapType.SPV_VAULT_FROM_BTC;
this.swapDeserializer = SpvFromBTCSwap_1.SpvFromBTCSwap;
this.pendingSwapStates = [
SpvFromBTCSwap_1.SpvFromBTCSwapState.CREATED,
SpvFromBTCSwap_1.SpvFromBTCSwapState.SIGNED,
SpvFromBTCSwap_1.SpvFromBTCSwapState.POSTED,
SpvFromBTCSwap_1.SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED,
SpvFromBTCSwap_1.SpvFromBTCSwapState.BROADCASTED,
SpvFromBTCSwap_1.SpvFromBTCSwapState.DECLINED,
SpvFromBTCSwap_1.SpvFromBTCSwapState.BTC_TX_CONFIRMED
];
this.tickSwapState = [
SpvFromBTCSwap_1.SpvFromBTCSwapState.CREATED,
SpvFromBTCSwap_1.SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED,
SpvFromBTCSwap_1.SpvFromBTCSwapState.SIGNED,
SpvFromBTCSwap_1.SpvFromBTCSwapState.POSTED,
SpvFromBTCSwap_1.SpvFromBTCSwapState.BROADCASTED
];
this.spvWithdrawalDataDeserializer = spvWithdrawalDataDeserializer;
this.contract = contract;
this.btcRelay = btcRelay;
this.synchronizer = synchronizer;
this.btcRpc = btcRpc;
}
processEventFront(event, swap) {
if (swap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.SIGNED || swap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.POSTED ||
swap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.BROADCASTED || swap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.DECLINED ||
swap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED || swap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.BTC_TX_CONFIRMED) {
swap.state = SpvFromBTCSwap_1.SpvFromBTCSwapState.FRONTED;
return true;
}
return false;
}
processEventClaim(event, swap) {
if (swap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.SIGNED || swap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.POSTED ||
swap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.BROADCASTED || swap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.DECLINED ||
swap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED || swap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.BTC_TX_CONFIRMED) {
swap.state = SpvFromBTCSwap_1.SpvFromBTCSwapState.CLAIMED;
return true;
}
return false;
}
processEventClose(event, swap) {
if (swap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.SIGNED || swap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.POSTED ||
swap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.BROADCASTED || swap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.DECLINED ||
swap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED || swap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.BTC_TX_CONFIRMED) {
swap.state = SpvFromBTCSwap_1.SpvFromBTCSwapState.CLOSED;
return true;
}
return false;
}
async processEvent(event, swap) {
if (swap == null)
return;
let swapChanged = false;
if (event instanceof base_1.SpvVaultFrontEvent) {
swapChanged = this.processEventFront(event, swap);
if (event.meta?.txId != null && swap.frontTxId !== event.meta.txId) {
swap.frontTxId = event.meta.txId;
swapChanged ||= true;
}
}
if (event instanceof base_1.SpvVaultClaimEvent) {
swapChanged = this.processEventClaim(event, swap);
if (event.meta?.txId != null && swap.claimTxId !== event.meta.txId) {
swap.claimTxId = event.meta.txId;
swapChanged ||= true;
}
}
if (event instanceof base_1.SpvVaultCloseEvent) {
swapChanged = this.processEventClose(event, swap);
}
this.logger.info("processEvents(): " + event.constructor.name + " processed for " + swap.getId() + " swap: ", swap);
if (swapChanged) {
await swap._saveAndEmit();
}
return true;
}
/**
* Pre-fetches latest finalized block height of the smart chain
*
* @param abortController
* @private
*/
async preFetchFinalizedBlockHeight(abortController) {
try {
const block = await (0, Utils_1.tryWithRetries)(() => this.chain.getFinalizedBlock(), null, null, abortController.signal);
return block.height;
}
catch (e) {
abortController.abort(e);
return null;
}
}
/**
* Pre-fetches caller (watchtower) bounty data for the swap. Doesn't throw, instead returns null and aborts the
* provided abortController
*
* @param amountData
* @param options Options as passed to the swap creation function
* @param pricePrefetch
* @param nativeTokenPricePrefetch
* @param abortController
* @private
*/
async preFetchCallerFeeShare(amountData, options, pricePrefetch, nativeTokenPricePrefetch, abortController) {
if (options.unsafeZeroWatchtowerFee)
return 0n;
if (amountData.amount === 0n)
return 0n;
try {
const [feePerBlock, btcRelayData, currentBtcBlock, claimFeeRate, nativeTokenPrice] = await Promise.all([
(0, Utils_1.tryWithRetries)(() => this.btcRelay.getFeePerBlock(), null, null, abortController.signal),
(0, Utils_1.tryWithRetries)(() => this.btcRelay.getTipData(), null, null, abortController.signal),
this.btcRpc.getTipHeight(),
(0, Utils_1.tryWithRetries)(() => this.contract.getClaimFee(this.chain.randomAddress(), null, null), null, null, abortController.signal),
nativeTokenPricePrefetch ?? (amountData.token === this.chain.getNativeCurrencyAddress() ?
pricePrefetch :
this.prices.preFetchPrice(this.chainIdentifier, this.chain.getNativeCurrencyAddress(), abortController.signal))
]);
const currentBtcRelayBlock = btcRelayData.blockheight;
const blockDelta = Math.max(currentBtcBlock - currentBtcRelayBlock + this.options.maxConfirmations, 0);
const totalFeeInNativeToken = ((BigInt(blockDelta) * feePerBlock) +
(claimFeeRate * BigInt(this.options.maxTransactionsDelta))) * BigInt(Math.floor(options.feeSafetyFactor * 1000000)) / 1000000n;
let payoutAmount;
if (amountData.exactIn) {
//Convert input amount in BTC to
const amountInNativeToken = await this.prices.getFromBtcSwapAmount(this.chainIdentifier, amountData.amount, this.chain.getNativeCurrencyAddress(), abortController.signal, nativeTokenPrice);
payoutAmount = amountInNativeToken - totalFeeInNativeToken;
}
else {
if (amountData.token === this.chain.getNativeCurrencyAddress()) {
//Both amounts in same currency
payoutAmount = amountData.amount;
}
else {
//Need to convert both to native currency
const btcAmount = await this.prices.getToBtcSwapAmount(this.chainIdentifier, amountData.amount, amountData.token, abortController.signal, await pricePrefetch);
payoutAmount = await this.prices.getFromBtcSwapAmount(this.chainIdentifier, btcAmount, this.chain.getNativeCurrencyAddress(), abortController.signal, nativeTokenPrice);
}
}
this.logger.debug("preFetchCallerFeeShare(): Caller fee in native token: " + totalFeeInNativeToken.toString(10) + " total payout in native token: " + payoutAmount.toString(10));
const callerFeeShare = ((totalFeeInNativeToken * 100000n) + payoutAmount - 1n) / payoutAmount; //Make sure to round up here
if (callerFeeShare < 0n)
return 0n;
if (callerFeeShare >= 2n ** 20n)
return 2n ** 20n - 1n;
return callerFeeShare;
}
catch (e) {
abortController.abort(e);
return null;
}
}
/**
* Verifies response returned from intermediary
*
* @param resp Response as returned by the intermediary
* @param amountData
* @param lp Intermediary
* @param options Options as passed to the swap creation function
* @param callerFeeShare
* @param bitcoinFeeRatePromise Maximum accepted fee rate from the LPs
* @param abortSignal
* @private
* @throws {IntermediaryError} in case the response is invalid
*/
async verifyReturnedData(resp, amountData, lp, options, callerFeeShare, bitcoinFeeRatePromise, abortSignal) {
const btcFeeRate = await bitcoinFeeRatePromise;
abortSignal.throwIfAborted();
if (btcFeeRate != null && resp.btcFeeRate > btcFeeRate)
throw new IntermediaryError_1.IntermediaryError("Bitcoin fee rate returned too high!");
//Vault related
let vaultScript;
let vaultAddressType;
let btcAddressScript;
//Ensure valid btc addresses returned
try {
vaultScript = (0, BitcoinUtils_1.toOutputScript)(this.options.bitcoinNetwork, resp.vaultBtcAddress);
vaultAddressType = (0, BitcoinUtils_1.toCoinselectAddressType)(vaultScript);
btcAddressScript = (0, BitcoinUtils_1.toOutputScript)(this.options.bitcoinNetwork, resp.btcAddress);
}
catch (e) {
throw new IntermediaryError_1.IntermediaryError("Invalid btc address data returned", e);
}
const decodedUtxo = resp.btcUtxo.split(":");
if (resp.address !== lp.getAddress(this.chainIdentifier) || //Ensure the LP is indeed the vault owner
resp.vaultId < 0n || //Ensure vaultId is not negative
vaultScript == null || //Make sure vault script is parsable and of known type
btcAddressScript == null || //Make sure btc address script is parsable and of known type
vaultAddressType === "p2pkh" || vaultAddressType === "p2sh-p2wpkh" || //Constrain the vault script type to witness types
decodedUtxo.length !== 2 || decodedUtxo[0].length !== 64 || isNaN(parseInt(decodedUtxo[1])) || //Check valid UTXO
resp.btcFeeRate < 1 || resp.btcFeeRate > 10000 //Sanity check on the returned BTC fee rate
)
throw new IntermediaryError_1.IntermediaryError("Invalid vault data returned!");
//Amounts sanity
if (resp.btcAmountSwap + resp.btcAmountGas !== resp.btcAmount)
throw new Error("Btc amount mismatch");
if (resp.swapFeeBtc + resp.gasSwapFeeBtc !== resp.totalFeeBtc)
throw new Error("Btc fee mismatch");
//TODO: For now ensure fees are at 0
if (resp.callerFeeShare !== callerFeeShare ||
resp.frontingFeeShare !== 0n ||
resp.executionFeeShare !== 0n)
throw new IntermediaryError_1.IntermediaryError("Invalid caller/fronting/execution fee returned");
//Check expiry
const timeNowSeconds = Math.floor(Date.now() / 1000);
if (resp.expiry < timeNowSeconds)
throw new IntermediaryError_1.IntermediaryError(`Quote already expired, expiry: ${resp.expiry}, systemTime: ${timeNowSeconds}, clockAdjusted: ${Date._now != null}`);
let utxo = resp.btcUtxo.toLowerCase();
const [txId, voutStr] = utxo.split(":");
const abortController = (0, Utils_1.extendAbortController)(abortSignal);
let [vault, { vaultUtxoValue, btcTx }] = await Promise.all([
(async () => {
//Fetch vault data
let vault;
try {
vault = await this.contract.getVaultData(resp.address, resp.vaultId);
}
catch (e) {
this.logger.error("Error getting spv vault (owner: " + resp.address + " vaultId: " + resp.vaultId.toString(10) + "): ", e);
throw new IntermediaryError_1.IntermediaryError("Spv swap vault not found", e);
}
abortController.signal.throwIfAborted();
//Make sure vault is opened
if (!vault.isOpened())
throw new IntermediaryError_1.IntermediaryError("Returned spv swap vault is not opened!");
//Make sure the vault doesn't require insane amount of confirmations
if (vault.getConfirmations() > this.options.maxConfirmations)
throw new IntermediaryError_1.IntermediaryError("SPV swap vault needs too many confirmations: " + vault.getConfirmations());
const tokenData = vault.getTokenData();
//Amounts - make sure the amounts match
if (amountData.exactIn) {
if (resp.btcAmount !== amountData.amount)
throw new IntermediaryError_1.IntermediaryError("Invalid amount returned");
}
else {
//Check the difference between amount adjusted due to scaling to raw amount
const adjustedAmount = amountData.amount / tokenData[0].multiplier * tokenData[0].multiplier;
const adjustmentPPM = (amountData.amount - adjustedAmount) * 1000000n / amountData.amount;
if (adjustmentPPM > this.options.maxRawAmountAdjustmentDifferencePPM)
throw new IntermediaryError_1.IntermediaryError("Invalid amount0 multiplier used, rawAmount diff too high");
if (resp.total !== adjustedAmount)
throw new IntermediaryError_1.IntermediaryError("Invalid total returned");
}
if (options.gasAmount == null || options.gasAmount === 0n) {
if (resp.totalGas !== 0n)
throw new IntermediaryError_1.IntermediaryError("Invalid gas total returned");
}
else {
//Check the difference between amount adjusted due to scaling to raw amount
const adjustedGasAmount = options.gasAmount / tokenData[0].multiplier * tokenData[0].multiplier;
const adjustmentPPM = (options.gasAmount - adjustedGasAmount) * 1000000n / options.gasAmount;
if (adjustmentPPM > this.options.maxRawAmountAdjustmentDifferencePPM)
throw new IntermediaryError_1.IntermediaryError("Invalid amount1 multiplier used, rawAmount diff too high");
if (resp.totalGas !== adjustedGasAmount)
throw new IntermediaryError_1.IntermediaryError("Invalid gas total returned");
}
return vault;
})(),
(async () => {
//Require the vault UTXO to have at least 1 confirmation
let btcTx = await this.btcRpc.getTransaction(txId);
abortController.signal.throwIfAborted();
if (btcTx.confirmations == null || btcTx.confirmations < 1)
throw new IntermediaryError_1.IntermediaryError("SPV vault UTXO not confirmed");
const vout = parseInt(voutStr);
if (btcTx.outs[vout] == null)
throw new IntermediaryError_1.IntermediaryError("Invalid UTXO, doesn't exist");
const vaultUtxoValue = btcTx.outs[vout].value;
return { btcTx, vaultUtxoValue };
})(),
(async () => {
//Require vault UTXO is unspent
if (await this.btcRpc.isSpent(utxo))
throw new IntermediaryError_1.IntermediaryError("Returned spv vault UTXO is already spent", null, true);
abortController.signal.throwIfAborted();
})()
]).catch(e => {
abortController.abort(e);
throw e;
});
this.logger.debug("verifyReturnedData(): Vault UTXO: " + vault.getUtxo() + " current utxo: " + utxo);
//Trace returned utxo back to what's saved on-chain
let pendingWithdrawals = [];
while (vault.getUtxo() !== utxo) {
const [txId, voutStr] = utxo.split(":");
//Such that 1st tx isn't fetched twice
if (btcTx.txid !== txId)
btcTx = await this.btcRpc.getTransaction(txId);
const withdrawalData = await this.contract.getWithdrawalData(btcTx);
abortSignal.throwIfAborted();
pendingWithdrawals.unshift(withdrawalData);
utxo = pendingWithdrawals[0].getSpentVaultUtxo();
this.logger.debug("verifyReturnedData(): Vault UTXO: " + vault.getUtxo() + " current utxo: " + utxo);
if (pendingWithdrawals.length >= this.options.maxTransactionsDelta)
throw new IntermediaryError_1.IntermediaryError("BTC <> SC state difference too deep, maximum: " + this.options.maxTransactionsDelta);
}
//Verify that the vault has enough balance after processing all pending withdrawals
let vaultBalances;
try {
vaultBalances = vault.calculateStateAfter(pendingWithdrawals);
}
catch (e) {
this.logger.error("Error calculating spv vault balance (owner: " + resp.address + " vaultId: " + resp.vaultId.toString(10) + "): ", e);
throw new IntermediaryError_1.IntermediaryError("Spv swap vault balance prediction failed", e);
}
if (vaultBalances.balances[0].scaledAmount < resp.total)
throw new IntermediaryError_1.IntermediaryError("SPV swap vault, insufficient balance, required: " + resp.total.toString(10) +
" has: " + vaultBalances.balances[0].scaledAmount.toString(10));
if (vaultBalances.balances[1].scaledAmount < resp.totalGas)
throw new IntermediaryError_1.IntermediaryError("SPV swap vault, insufficient balance, required: " + resp.totalGas.toString(10) +
" has: " + vaultBalances.balances[1].scaledAmount.toString(10));
//Also verify that all the withdrawal txns are valid, this is an extra sanity check
try {
for (let withdrawal of pendingWithdrawals) {
await this.contract.checkWithdrawalTx(withdrawal);
}
}
catch (e) {
this.logger.error("Error calculating spv vault balance (owner: " + resp.address + " vaultId: " + resp.vaultId.toString(10) + "): ", e);
throw new IntermediaryError_1.IntermediaryError("Spv swap vault balance prediction failed", e);
}
abortSignal.throwIfAborted();
return {
vault,
vaultUtxoValue
};
}
/**
* Returns a newly created swap, receiving 'amount' on chain
*
* @param signer Smartchain signer's address intiating the swap
* @param amountData Amount of token & amount to swap
* @param lps LPs (liquidity providers) to get the quotes from
* @param options Quote options
* @param additionalParams Additional parameters sent to the LP when creating the swap
* @param abortSignal Abort signal for aborting the process
*/
create(signer, amountData, lps, options, additionalParams, abortSignal) {
options ??= {};
options.gasAmount ??= 0n;
options.feeSafetyFactor ??= 1.25;
const _abortController = (0, Utils_1.extendAbortController)(abortSignal);
const pricePrefetchPromise = this.preFetchPrice(amountData, _abortController.signal);
const finalizedBlockHeightPrefetchPromise = this.preFetchFinalizedBlockHeight(_abortController);
const nativeTokenAddress = this.chain.getNativeCurrencyAddress();
const gasTokenPricePrefetchPromise = options.gasAmount === 0n ?
null :
this.preFetchPrice({ token: nativeTokenAddress }, _abortController.signal);
const callerFeePrefetchPromise = this.preFetchCallerFeeShare(amountData, options, pricePrefetchPromise, gasTokenPricePrefetchPromise, _abortController);
const bitcoinFeeRatePromise = options.maxAllowedNetworkFeeRate != null ?
Promise.resolve(options.maxAllowedNetworkFeeRate) :
this.btcRpc.getFeeRate().then(x => this.options.maxBtcFeeOffset + (x * this.options.maxBtcFeeMultiplier)).catch(e => {
_abortController.abort(e);
return null;
});
return lps.map(lp => {
return {
intermediary: lp,
quote: (0, Utils_1.tryWithRetries)(async () => {
const abortController = (0, Utils_1.extendAbortController)(_abortController.signal);
try {
const resp = await (0, Utils_1.tryWithRetries)(async (retryCount) => {
return await IntermediaryAPI_1.IntermediaryAPI.prepareSpvFromBTC(this.chainIdentifier, lp.url, {
address: signer,
amount: amountData.amount,
token: amountData.token.toString(),
exactOut: !amountData.exactIn,
gasToken: nativeTokenAddress,
gasAmount: options.gasAmount,
callerFeeRate: callerFeePrefetchPromise,
frontingFeeRate: 0n,
additionalParams
}, this.options.postRequestTimeout, abortController.signal, retryCount > 0 ? false : null);
}, null, e => e instanceof RequestError_1.RequestError, abortController.signal);
this.logger.debug("create(" + lp.url + "): LP response: ", resp);
const callerFeeShare = await callerFeePrefetchPromise;
const [pricingInfo, gasPricingInfo, { vault, vaultUtxoValue }] = await Promise.all([
this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.SPV_VAULT_FROM_BTC], false, resp.btcAmountSwap, resp.total * (100000n + callerFeeShare) / 100000n, amountData.token, {}, pricePrefetchPromise, abortController.signal),
options.gasAmount === 0n ? Promise.resolve() : this.verifyReturnedPrice({ ...lp.services[SwapType_1.SwapType.SPV_VAULT_FROM_BTC], swapBaseFee: 0 }, //Base fee should be charged only on the amount, not on gas
false, resp.btcAmountGas, resp.totalGas * (100000n + callerFeeShare) / 100000n, nativeTokenAddress, {}, gasTokenPricePrefetchPromise, abortController.signal),
this.verifyReturnedData(resp, amountData, lp, options, callerFeeShare, bitcoinFeeRatePromise, abortController.signal)
]);
const swapInit = {
pricingInfo,
url: lp.url,
expiry: resp.expiry * 1000,
swapFee: resp.swapFee,
swapFeeBtc: resp.swapFeeBtc,
exactIn: amountData.exactIn ?? true,
quoteId: resp.quoteId,
recipient: signer,
vaultOwner: resp.address,
vaultId: resp.vaultId,
vaultRequiredConfirmations: vault.getConfirmations(),
vaultTokenMultipliers: vault.getTokenData().map(val => val.multiplier),
vaultBtcAddress: resp.vaultBtcAddress,
vaultUtxo: resp.btcUtxo,
vaultUtxoValue: BigInt(vaultUtxoValue),
btcDestinationAddress: resp.btcAddress,
btcAmount: resp.btcAmount,
btcAmountSwap: resp.btcAmountSwap,
btcAmountGas: resp.btcAmountGas,
minimumBtcFeeRate: resp.btcFeeRate,
outputTotalSwap: resp.total,
outputSwapToken: amountData.token,
outputTotalGas: resp.totalGas,
outputGasToken: nativeTokenAddress,
gasSwapFeeBtc: resp.gasSwapFeeBtc,
gasSwapFee: resp.gasSwapFee,
callerFeeShare: resp.callerFeeShare,
frontingFeeShare: resp.frontingFeeShare,
executionFeeShare: resp.executionFeeShare,
genesisSmartChainBlockHeight: await finalizedBlockHeightPrefetchPromise
};
const quote = new SpvFromBTCSwap_1.SpvFromBTCSwap(this, swapInit);
await quote._save();
return quote;
}
catch (e) {
abortController.abort(e);
throw e;
}
}, null, err => !(err instanceof IntermediaryError_1.IntermediaryError && err.recoverable), _abortController.signal)
};
});
}
/**
* Returns a random dummy PSBT that can be used for fee estimation, the last output (the LP output) is omitted
* to allow for coinselection algorithm to determine maximum sendable amount there
*
* @param includeGasToken Whether to return the PSBT also with the gas token amount (increases the vSize by 8)
*/
getDummySwapPsbt(includeGasToken = false) {
//Construct dummy swap psbt
const psbt = new btc_signer_1.Transaction({
allowUnknownInputs: true,
allowLegacyWitnessUtxo: true,
allowUnknownOutputs: true
});
const randomVaultOutScript = btc_signer_1.OutScript.encode({ type: "tr", pubkey: Buffer.from("0101010101010101010101010101010101010101010101010101010101010101", "hex") });
psbt.addInput({
txid: (0, Utils_1.randomBytes)(32),
index: 0,
witnessUtxo: {
script: randomVaultOutScript,
amount: 600n
}
});
psbt.addOutput({
script: randomVaultOutScript,
amount: 600n
});
const opReturnData = this.contract.toOpReturnData(this.chain.randomAddress(), includeGasToken ? [0xffffffffffffffffn, 0xffffffffffffffffn] : [0xffffffffffffffffn]);
psbt.addOutput({
script: Buffer.concat([
opReturnData.length <= 75 ? Buffer.from([0x6a, opReturnData.length]) : Buffer.from([0x6a, 0x4c, opReturnData.length]),
opReturnData
]),
amount: 0n
});
return psbt;
}
async _checkPastSwaps(pastSwaps) {
const changedSwaps = new Set();
const removeSwaps = [];
const broadcastedOrConfirmedSwaps = [];
for (let pastSwap of pastSwaps) {
let changed = false;
if (pastSwap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.SIGNED ||
pastSwap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.POSTED ||
pastSwap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.BROADCASTED ||
pastSwap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED ||
pastSwap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.DECLINED ||
pastSwap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.BTC_TX_CONFIRMED) {
//Check BTC transaction
if (await pastSwap._syncStateFromBitcoin(false))
changed ||= true;
}
if (pastSwap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.CREATED ||
pastSwap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.SIGNED ||
pastSwap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.POSTED) {
if (pastSwap.expiry < Date.now()) {
if (pastSwap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.CREATED) {
pastSwap.state = SpvFromBTCSwap_1.SpvFromBTCSwapState.QUOTE_EXPIRED;
}
else {
pastSwap.state = SpvFromBTCSwap_1.SpvFromBTCSwapState.QUOTE_SOFT_EXPIRED;
}
changed ||= true;
}
}
if (pastSwap.isQuoteExpired()) {
removeSwaps.push(pastSwap);
continue;
}
if (changed)
changedSwaps.add(pastSwap);
if (pastSwap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.BROADCASTED || pastSwap.state === SpvFromBTCSwap_1.SpvFromBTCSwapState.BTC_TX_CONFIRMED) {
broadcastedOrConfirmedSwaps.push(pastSwap);
}
}
const checkWithdrawalStateSwaps = [];
const _fronts = await this.contract.getFronterAddresses(broadcastedOrConfirmedSwaps.map(val => ({
owner: val.vaultOwner,
vaultId: val.vaultId,
withdrawal: val.data
})));
const _vaultUtxos = await this.contract.getVaultLatestUtxos(broadcastedOrConfirmedSwaps.map(val => ({
owner: val.vaultOwner,
vaultId: val.vaultId
})));
for (const pastSwap of broadcastedOrConfirmedSwaps) {
const fronterAddress = _fronts[pastSwap.data.getTxId()];
const latestVaultUtxo = _vaultUtxos[pastSwap.vaultOwner]?.[pastSwap.vaultId.toString(10)];
if (fronterAddress === undefined)
this.logger.warn(`_checkPastSwaps(): No fronter address returned for ${pastSwap.data.getTxId()}`);
if (latestVaultUtxo === undefined)
this.logger.warn(`_checkPastSwaps(): No last vault utxo returned for ${pastSwap.data.getTxId()}`);
if (await pastSwap._shouldCheckWithdrawalState(fronterAddress, latestVaultUtxo))
checkWithdrawalStateSwaps.push(pastSwap);
}
const withdrawalStates = await this.contract.getWithdrawalStates(checkWithdrawalStateSwaps.map(val => ({
withdrawal: val.data,
scStartBlockheight: val.genesisSmartChainBlockHeight
})));
for (const pastSwap of checkWithdrawalStateSwaps) {
const status = withdrawalStates[pastSwap.data.getTxId()];
if (status == null) {
this.logger.warn(`_checkPastSwaps(): No withdrawal state returned for ${pastSwap.data.getTxId()}`);
continue;
}
this.logger.debug("syncStateFromChain(): status of " + pastSwap.data.btcTx.txid, status?.type);
let changed = false;
switch (status.type) {
case base_1.SpvWithdrawalStateType.FRONTED:
pastSwap.frontTxId = status.txId;
pastSwap.state = SpvFromBTCSwap_1.SpvFromBTCSwapState.FRONTED;
changed ||= true;
break;
case base_1.SpvWithdrawalStateType.CLAIMED:
pastSwap.claimTxId = status.txId;
pastSwap.state = SpvFromBTCSwap_1.SpvFromBTCSwapState.CLAIMED;
changed ||= true;
break;
case base_1.SpvWithdrawalStateType.CLOSED:
pastSwap.state = SpvFromBTCSwap_1.SpvFromBTCSwapState.CLOSED;
changed ||= true;
break;
}
if (changed)
changedSwaps.add(pastSwap);
}
return {
changedSwaps: Array.from(changedSwaps),
removeSwaps
};
}
}
exports.SpvFromBTCWrapper = SpvFromBTCWrapper;