@atomiqlabs/sdk-lib
Version:
Basic SDK functionality library for atomiq
274 lines (273 loc) • 15.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FromBTCWrapper = void 0;
const IFromBTCWrapper_1 = require("../IFromBTCWrapper");
const FromBTCSwap_1 = require("./FromBTCSwap");
const base_1 = require("@atomiqlabs/base");
const buffer_1 = require("buffer");
const IntermediaryError_1 = require("../../../errors/IntermediaryError");
const SwapType_1 = require("../../SwapType");
const Utils_1 = require("../../../utils/Utils");
const IntermediaryAPI_1 = require("../../../intermediaries/IntermediaryAPI");
const RequestError_1 = require("../../../errors/RequestError");
const utils_1 = require("@scure/btc-signer/utils");
class FromBTCWrapper extends IFromBTCWrapper_1.IFromBTCWrapper {
/**
* @param chainIdentifier
* @param unifiedStorage Storage interface for the current environment
* @param unifiedChainEvents On-chain event listener
* @param contract Underlying contract handling the swaps
* @param prices Pricing to use
* @param tokens
* @param swapDataDeserializer Deserializer for SwapData
* @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, contract, prices, tokens, swapDataDeserializer, btcRelay, synchronizer, btcRpc, options, events) {
if (options == null)
options = {};
options.bitcoinNetwork = options.bitcoinNetwork ?? utils_1.TEST_NETWORK;
options.safetyFactor = options.safetyFactor || 2;
options.blocksTillTxConfirms = options.blocksTillTxConfirms || 12;
options.maxConfirmations = options.maxConfirmations || 6;
options.minSendWindow = options.minSendWindow || 30 * 60; //Minimum time window for user to send in the on-chain funds for From BTC swap
options.bitcoinBlocktime = options.bitcoinBlocktime || 10 * 60;
super(chainIdentifier, unifiedStorage, unifiedChainEvents, contract, prices, tokens, swapDataDeserializer, options, events);
this.TYPE = SwapType_1.SwapType.FROM_BTC;
this.swapDeserializer = FromBTCSwap_1.FromBTCSwap;
this.pendingSwapStates = [
FromBTCSwap_1.FromBTCSwapState.PR_CREATED,
FromBTCSwap_1.FromBTCSwapState.QUOTE_SOFT_EXPIRED,
FromBTCSwap_1.FromBTCSwapState.CLAIM_COMMITED,
FromBTCSwap_1.FromBTCSwapState.BTC_TX_CONFIRMED,
FromBTCSwap_1.FromBTCSwapState.EXPIRED
];
this.tickSwapState = [FromBTCSwap_1.FromBTCSwapState.PR_CREATED, FromBTCSwap_1.FromBTCSwapState.CLAIM_COMMITED, FromBTCSwap_1.FromBTCSwapState.EXPIRED];
this.btcRelay = btcRelay;
this.synchronizer = synchronizer;
this.btcRpc = btcRpc;
}
processEventInitialize(swap, event) {
if (swap.state === FromBTCSwap_1.FromBTCSwapState.PR_CREATED || swap.state === FromBTCSwap_1.FromBTCSwapState.QUOTE_SOFT_EXPIRED) {
swap.state = FromBTCSwap_1.FromBTCSwapState.CLAIM_COMMITED;
return Promise.resolve(true);
}
return Promise.resolve(false);
}
processEventClaim(swap, event) {
if (swap.state !== FromBTCSwap_1.FromBTCSwapState.FAILED) {
swap.state = FromBTCSwap_1.FromBTCSwapState.CLAIM_CLAIMED;
return Promise.resolve(true);
}
return Promise.resolve(false);
}
processEventRefund(swap, event) {
if (swap.state !== FromBTCSwap_1.FromBTCSwapState.CLAIM_CLAIMED) {
swap.state = FromBTCSwap_1.FromBTCSwapState.FAILED;
return Promise.resolve(true);
}
return Promise.resolve(false);
}
/**
* Returns the swap expiry, leaving enough time for the user to send a transaction and for it to confirm
*
* @param data Parsed swap data
* @param requiredConfirmations Confirmations required to claim the tx
*/
getOnchainSendTimeout(data, requiredConfirmations) {
const tsDelta = (this.options.blocksTillTxConfirms + requiredConfirmations) * this.options.bitcoinBlocktime * this.options.safetyFactor;
return data.getExpiry() - BigInt(tsDelta);
}
/**
* Pre-fetches claimer (watchtower) bounty data for the swap. Doesn't throw, instead returns null and aborts the
* provided abortController
*
* @param signer Smartchain signer address initiating the swap
* @param amountData
* @param options Options as passed to the swap creation function
* @param abortController
* @private
*/
async preFetchClaimerBounty(signer, amountData, options, abortController) {
const startTimestamp = BigInt(Math.floor(Date.now() / 1000));
if (options.unsafeZeroWatchtowerFee) {
return {
feePerBlock: 0n,
safetyFactor: options.blockSafetyFactor,
startTimestamp: startTimestamp,
addBlock: 0,
addFee: 0n
};
}
const dummyAmount = BigInt(Math.floor(Math.random() * 0x1000000));
const dummySwapData = await this.contract.createSwapData(base_1.ChainSwapType.CHAIN, signer, signer, amountData.token, dummyAmount, this.contract.getHashForOnchain((0, Utils_1.randomBytes)(20), dummyAmount, 3).toString("hex"), this.getRandomSequence(), startTimestamp, false, true, BigInt(Math.floor(Math.random() * 0x10000)), BigInt(Math.floor(Math.random() * 0x10000)));
try {
const [feePerBlock, btcRelayData, currentBtcBlock, claimFeeRate] = 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(signer, dummySwapData), null, null, abortController.signal)
]);
const currentBtcRelayBlock = btcRelayData.blockheight;
const addBlock = Math.max(currentBtcBlock - currentBtcRelayBlock, 0);
return {
feePerBlock: feePerBlock * options.feeSafetyFactor,
safetyFactor: options.blockSafetyFactor,
startTimestamp: startTimestamp,
addBlock,
addFee: claimFeeRate * options.feeSafetyFactor
};
}
catch (e) {
abortController.abort(e);
return null;
}
}
/**
* Returns calculated claimer bounty calculated from the claimer bounty data as fetched from preFetchClaimerBounty()
*
* @param data Parsed swap data returned from the intermediary
* @param options Options as passed to the swap creation function
* @param claimerBounty Claimer bounty data as fetched from preFetchClaimerBounty() function
* @private
*/
getClaimerBounty(data, options, claimerBounty) {
const tsDelta = data.getExpiry() - claimerBounty.startTimestamp;
const blocksDelta = tsDelta / BigInt(this.options.bitcoinBlocktime) * BigInt(options.blockSafetyFactor);
const totalBlock = blocksDelta + BigInt(claimerBounty.addBlock);
return claimerBounty.addFee + (totalBlock * claimerBounty.feePerBlock);
}
/**
* 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 data Parsed swap data returned by the intermediary
* @param sequence Required swap sequence
* @param claimerBounty Claimer bount data as returned from the preFetchClaimerBounty() pre-fetch promise
* @param depositToken
* @private
* @throws {IntermediaryError} in case the response is invalid
*/
verifyReturnedData(resp, amountData, lp, options, data, sequence, claimerBounty, depositToken) {
if (amountData.exactIn) {
if (resp.amount !== amountData.amount)
throw new IntermediaryError_1.IntermediaryError("Invalid amount returned");
}
else {
if (resp.total !== amountData.amount)
throw new IntermediaryError_1.IntermediaryError("Invalid total returned");
}
const requiredConfirmations = resp.confirmations ?? lp.services[SwapType_1.SwapType.FROM_BTC].data.confirmations;
if (requiredConfirmations > this.options.maxConfirmations)
throw new IntermediaryError_1.IntermediaryError("Requires too many confirmations");
const totalClaimerBounty = this.getClaimerBounty(data, options, claimerBounty);
if (data.getClaimerBounty() !== totalClaimerBounty ||
data.getType() != base_1.ChainSwapType.CHAIN ||
data.getSequence() !== sequence ||
data.getAmount() !== resp.total ||
data.isPayIn() ||
!data.isToken(amountData.token) ||
data.getOfferer() !== lp.getAddress(this.chainIdentifier) ||
!data.isDepositToken(depositToken)) {
throw new IntermediaryError_1.IntermediaryError("Invalid data returned");
}
//Check that we have enough time to send the TX and for it to confirm
const expiry = this.getOnchainSendTimeout(data, requiredConfirmations);
const currentTimestamp = BigInt(Math.floor(Date.now() / 1000));
if ((expiry - currentTimestamp) < BigInt(this.options.minSendWindow)) {
throw new IntermediaryError_1.IntermediaryError("Send window too low");
}
const lockingScript = (0, Utils_1.toOutputScript)(this.options.bitcoinNetwork, resp.btcAddress);
const desiredExtraData = this.contract.getExtraData(lockingScript, resp.amount, requiredConfirmations);
const desiredClaimHash = this.contract.getHashForOnchain(lockingScript, resp.amount, requiredConfirmations);
if (!desiredClaimHash.equals(buffer_1.Buffer.from(data.getClaimHash(), "hex"))) {
throw new IntermediaryError_1.IntermediaryError("Invalid claim hash returned!");
}
if (!desiredExtraData.equals(buffer_1.Buffer.from(data.getExtraData(), "hex"))) {
throw new IntermediaryError_1.IntermediaryError("Invalid extra data returned!");
}
}
/**
* 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.blockSafetyFactor ??= 1;
options.feeSafetyFactor ??= 2n;
const sequence = this.getRandomSequence();
const _abortController = (0, Utils_1.extendAbortController)(abortSignal);
const pricePrefetchPromise = this.preFetchPrice(amountData, _abortController.signal);
const claimerBountyPrefetchPromise = this.preFetchClaimerBounty(signer, amountData, options, _abortController);
const nativeTokenAddress = this.contract.getNativeCurrencyAddress();
const feeRatePromise = this.preFetchFeeRate(signer, amountData, null, _abortController);
return lps.map(lp => {
return {
intermediary: lp,
quote: (async () => {
const abortController = (0, Utils_1.extendAbortController)(_abortController.signal);
const liquidityPromise = this.preFetchIntermediaryLiquidity(amountData, lp, abortController);
try {
const { signDataPromise, resp } = await (0, Utils_1.tryWithRetries)(async (retryCount) => {
const { signDataPrefetch, response } = IntermediaryAPI_1.IntermediaryAPI.initFromBTC(this.chainIdentifier, lp.url, nativeTokenAddress, {
claimer: signer,
amount: amountData.amount,
token: amountData.token.toString(),
exactOut: !amountData.exactIn,
sequence,
claimerBounty: claimerBountyPrefetchPromise,
feeRate: feeRatePromise,
additionalParams
}, this.options.postRequestTimeout, abortController.signal, retryCount > 0 ? false : null);
return {
signDataPromise: this.preFetchSignData(signDataPrefetch),
resp: await response
};
}, null, e => e instanceof RequestError_1.RequestError, abortController.signal);
const data = new this.swapDataDeserializer(resp.data);
data.setClaimer(signer);
this.verifyReturnedData(resp, amountData, lp, options, data, sequence, await claimerBountyPrefetchPromise, nativeTokenAddress);
const [pricingInfo, signatureExpiry] = await Promise.all([
//Get intermediary's liquidity
this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.FROM_BTC], false, resp.amount, resp.total, amountData.token, resp, pricePrefetchPromise, abortController.signal),
this.verifyReturnedSignature(data, resp, feeRatePromise, signDataPromise, abortController.signal),
this.verifyIntermediaryLiquidity(data.getAmount(), liquidityPromise),
]);
const quote = new FromBTCSwap_1.FromBTCSwap(this, {
pricingInfo,
url: lp.url,
expiry: signatureExpiry,
swapFee: resp.swapFee,
feeRate: await feeRatePromise,
signatureData: resp,
data,
address: resp.btcAddress,
amount: resp.amount,
exactIn: amountData.exactIn ?? true,
requiredConfirmations: resp.confirmations ?? lp.services[SwapType_1.SwapType.FROM_BTC].data.confirmations
});
await quote._save();
return quote;
}
catch (e) {
abortController.abort(e);
throw e;
}
})()
};
});
}
}
exports.FromBTCWrapper = FromBTCWrapper;