UNPKG

@atomiqlabs/sdk-lib

Version:

Basic SDK functionality library for atomiq

201 lines (200 loc) 10.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ToBTCWrapper = void 0; const ToBTCSwap_1 = require("./ToBTCSwap"); const IToBTCWrapper_1 = require("../IToBTCWrapper"); const base_1 = require("@atomiqlabs/base"); const UserError_1 = require("../../../errors/UserError"); 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 ToBTCWrapper extends IToBTCWrapper_1.IToBTCWrapper { /** * @param chainIdentifier * @param unifiedStorage Storage interface for the current environment * @param unifiedChainEvents Smart chain on-chain event listener * @param contract Chain specific swap contract * @param prices Swap pricing handler * @param tokens * @param swapDataDeserializer Deserializer for chain specific SwapData * @param btcRpc Bitcoin RPC api * @param options * @param events Instance to use for emitting events */ constructor(chainIdentifier, unifiedStorage, unifiedChainEvents, contract, prices, tokens, swapDataDeserializer, btcRpc, options, events) { if (options == null) options = {}; options.bitcoinNetwork = options.bitcoinNetwork ?? utils_1.TEST_NETWORK; options.safetyFactor = options.safetyFactor || 2; options.maxConfirmations = options.maxConfirmations || 6; options.bitcoinBlocktime = options.bitcoinBlocktime || (60 * 10); options.maxExpectedOnchainSendSafetyFactor = options.maxExpectedOnchainSendSafetyFactor || 4; options.maxExpectedOnchainSendGracePeriodBlocks = options.maxExpectedOnchainSendGracePeriodBlocks || 12; super(chainIdentifier, unifiedStorage, unifiedChainEvents, contract, prices, tokens, swapDataDeserializer, options, events); this.TYPE = SwapType_1.SwapType.TO_BTC; this.swapDeserializer = ToBTCSwap_1.ToBTCSwap; this.btcRpc = btcRpc; } /** * Returns randomly generated random escrow nonce to be used for to BTC on-chain swaps * @private * @returns Escrow nonce */ getRandomNonce() { const firstPart = BigInt(Math.floor((Date.now() / 1000)) - 700000000); return (firstPart << 24n) | base_1.BigIntBufferUtils.fromBuffer((0, Utils_1.randomBytes)(3)); } /** * Converts bitcoin address to its corresponding output script * * @param addr Bitcoin address to get the output script for * @private * @returns Output script as Buffer * @throws {UserError} if invalid address is specified */ btcAddressToOutputScript(addr) { try { return (0, Utils_1.toOutputScript)(this.options.bitcoinNetwork, addr); } catch (e) { throw new UserError_1.UserError("Invalid address specified"); } } /** * Verifies returned LP data * * @param resp LP's response * @param amountData * @param lp * @param options Options as passed to the swap create function * @param data LP's returned parsed swap data * @param hash Payment hash of the swap * @private * @throws {IntermediaryError} if returned data are not correct */ verifyReturnedData(resp, amountData, lp, options, data, hash) { if (resp.totalFee !== (resp.swapFee + resp.networkFee)) throw new IntermediaryError_1.IntermediaryError("Invalid totalFee returned"); if (amountData.exactIn) { if (resp.total !== amountData.amount) throw new IntermediaryError_1.IntermediaryError("Invalid total returned"); } else { if (resp.amount !== amountData.amount) throw new IntermediaryError_1.IntermediaryError("Invalid amount returned"); } const maxAllowedBlockDelta = BigInt(options.confirmations + options.confirmationTarget + this.options.maxExpectedOnchainSendGracePeriodBlocks); const maxAllowedExpiryDelta = maxAllowedBlockDelta * BigInt(this.options.maxExpectedOnchainSendSafetyFactor) * BigInt(this.options.bitcoinBlocktime); const currentTimestamp = BigInt(Math.floor(Date.now() / 1000)); const maxAllowedExpiryTimestamp = currentTimestamp + maxAllowedExpiryDelta; if (data.getExpiry() > maxAllowedExpiryTimestamp) { throw new IntermediaryError_1.IntermediaryError("Expiry time returned too high!"); } if (data.getAmount() !== resp.total || data.getClaimHash() !== hash || data.getType() !== base_1.ChainSwapType.CHAIN_NONCED || !data.isPayIn() || !data.isToken(amountData.token) || data.getClaimer() !== lp.getAddress(this.chainIdentifier)) { throw new IntermediaryError_1.IntermediaryError("Invalid data returned"); } } /** * Returns quotes fetched from LPs, paying to an 'address' - a bitcoin address * * @param signer Smart-chain signer address initiating the swap * @param address Bitcoin on-chain address you wish to pay to * @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, address, amountData, lps, options, additionalParams, abortSignal) { if (!this.isInitialized) throw new Error("Not initialized, call init() first!"); options ??= {}; options.confirmationTarget ??= 3; options.confirmations ??= 2; const nonce = this.getRandomNonce(); const outputScript = this.btcAddressToOutputScript(address); const _hash = !amountData.exactIn ? this.contract.getHashForOnchain(outputScript, amountData.amount, options.confirmations, nonce).toString("hex") : null; const _abortController = (0, Utils_1.extendAbortController)(abortSignal); const pricePreFetchPromise = this.preFetchPrice(amountData, _abortController.signal); const feeRatePromise = this.preFetchFeeRate(signer, amountData, _hash, _abortController); return lps.map(lp => { return { intermediary: lp, quote: (async () => { const abortController = (0, Utils_1.extendAbortController)(_abortController.signal); const reputationPromise = this.preFetchIntermediaryReputation(amountData, lp, abortController); try { const { signDataPromise, resp } = await (0, Utils_1.tryWithRetries)(async (retryCount) => { const { signDataPrefetch, response } = IntermediaryAPI_1.IntermediaryAPI.initToBTC(this.chainIdentifier, lp.url, { btcAddress: address, amount: amountData.amount, confirmationTarget: options.confirmationTarget, confirmations: options.confirmations, nonce: nonce, token: amountData.token, offerer: signer, exactIn: amountData.exactIn, feeRate: feeRatePromise, additionalParams }, this.options.postRequestTimeout, abortController.signal, retryCount > 0 ? false : null); return { signDataPromise: this.preFetchSignData(signDataPrefetch), resp: await response }; }, null, RequestError_1.RequestError, abortController.signal); let hash = amountData.exactIn ? this.contract.getHashForOnchain(outputScript, resp.amount, options.confirmations, nonce).toString("hex") : _hash; const data = new this.swapDataDeserializer(resp.data); data.setOfferer(signer); this.verifyReturnedData(resp, amountData, lp, options, data, hash); const [pricingInfo, signatureExpiry, reputation] = await Promise.all([ this.verifyReturnedPrice(lp.services[SwapType_1.SwapType.TO_BTC], true, resp.amount, data.getAmount(), amountData.token, resp, pricePreFetchPromise, abortController.signal), this.verifyReturnedSignature(data, resp, feeRatePromise, signDataPromise, abortController.signal), reputationPromise ]); abortController.signal.throwIfAborted(); const quote = new ToBTCSwap_1.ToBTCSwap(this, { pricingInfo, url: lp.url, expiry: signatureExpiry, swapFee: resp.swapFee, feeRate: await feeRatePromise, signatureData: resp, data, networkFee: resp.networkFee, address, amount: resp.amount, confirmationTarget: options.confirmationTarget, satsPerVByte: Number(resp.satsPervByte), exactIn: amountData.exactIn ?? false, requiredConfirmations: options.confirmations, nonce }); await quote._save(); return quote; } catch (e) { abortController.abort(e); throw e; } })() }; }); } } exports.ToBTCWrapper = ToBTCWrapper;