UNPKG

@atomiqlabs/sdk

Version:

atomiq labs SDK for cross-chain swaps between smart chains and bitcoin

374 lines (373 loc) 15.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ISwapWrapper = exports.DEFAULT_MAX_PARALLEL_SWAP_SYNCS = exports.DEFAULT_MAX_PARALLEL_SWAP_TICKS = void 0; const base_1 = require("@atomiqlabs/base"); const events_1 = require("events"); const IntermediaryError_1 = require("../errors/IntermediaryError"); const Logger_1 = require("../utils/Logger"); const TokenUtils_1 = require("../utils/TokenUtils"); const UserError_1 = require("../errors/UserError"); exports.DEFAULT_MAX_PARALLEL_SWAP_TICKS = 50; exports.DEFAULT_MAX_PARALLEL_SWAP_SYNCS = 50; /** * Base abstract class for swap handler implementations * * @category Swaps/Base */ class ISwapWrapper { constructor(chainIdentifier, unifiedStorage, unifiedChainEvents, chain, prices, tokens, lpApi, options, events) { /** * Logger instance * @internal */ this.logger = (0, Logger_1.getLogger)(this.constructor.name + ": "); /** * In-memory mapping of pending (not initiated) swaps, utilizing weak references to automatically * free memory when swaps are dereferenced in not initiated state * @internal */ this.pendingSwaps = new Map(); /** * Whether this wrapper is initialized (have to call {@link init} to initialize a wrapper) * @internal */ this.isInitialized = false; if (options?.maxParallelSwapTicks != null && options.maxParallelSwapTicks < 1) throw new Error("maxParallelSwapTicks must be at least 1!"); if (options?.maxParallelSwapSyncs != null && options.maxParallelSwapSyncs < 1) throw new Error("maxParallelSwapSyncs must be at least 1!"); this.unifiedStorage = unifiedStorage; this.unifiedChainEvents = unifiedChainEvents; this.chainIdentifier = chainIdentifier; this._chain = chain; this._prices = prices; this.events = events || new events_1.EventEmitter(); this._lpApi = lpApi; this._options = options; this._tokens = tokens; } /** * Parses the provided gas amount from its `string` or `bigint` representation to `bigint` base units. * * Defaults to `0n` if no gasAmount is provided * * @param gasAmount * @internal */ parseGasAmount(gasAmount) { let result; if (typeof (gasAmount) === "string") { result = (0, TokenUtils_1.fromHumanReadableString)(gasAmount, this._getNativeToken()); if (result == null) throw new UserError_1.UserError("Invalid `gasAmount` option provided, not a numerical string!"); } else { result = gasAmount; } return result ?? 0n; } /** * Pre-fetches swap price for a given swap * * @param amountData Amount data * @param abortSignal Abort signal * @returns Price of the token in uSats (micro sats) * @internal */ preFetchPrice(amountData, abortSignal) { return this._prices.preFetchPrice(this.chainIdentifier, amountData.token, abortSignal).catch(e => { this.logger.error("preFetchPrice.token(): Error: ", e); return undefined; }); } /** * Pre-fetches bitcoin's USD price * * @param abortSignal Abort signal * @internal */ preFetchUsdPrice(abortSignal) { return this._prices.preFetchUsdPrice(abortSignal).catch(e => { this.logger.error("preFetchPrice.usd(): Error: ", e); return undefined; }); } /** * Verifies returned price for swaps * * @param lpServiceData Service data for the service in question (TO_BTCLN, TO_BTC, etc.) of the given intermediary * @param send Whether this is a send (Smart chain -> Bitcoin) or receive (Bitcoin -> Smart chain) swap * @param amountSats Amount in BTC * @param amountToken Amount in token * @param token Token used in the swap * @param feeData Fee data as returned by the intermediary * @param pricePrefetchPromise Optional price pre-fetch promise * @param usdPricePrefetchPromise Optiona USD price pre-fetch promise * @param abortSignal Abort signal * @returns Price info object * @throws {IntermediaryError} if the calculated fee is too high * * @internal */ async verifyReturnedPrice(lpServiceData, send, amountSats, amountToken, token, feeData, pricePrefetchPromise = Promise.resolve(undefined), usdPricePrefetchPromise = Promise.resolve(undefined), abortSignal) { const swapBaseFee = BigInt(lpServiceData.swapBaseFee); const swapFeePPM = BigInt(lpServiceData.swapFeePPM); if (send && feeData.networkFee != null) amountToken = amountToken - feeData.networkFee; const [isValidAmount, usdPrice] = await Promise.all([ send ? this._prices.isValidAmountSend(this.chainIdentifier, amountSats, swapBaseFee, swapFeePPM, amountToken, token, abortSignal, await pricePrefetchPromise, feeData.swapFeeBtc) : this._prices.isValidAmountReceive(this.chainIdentifier, amountSats, swapBaseFee, swapFeePPM, amountToken, token, abortSignal, await pricePrefetchPromise, feeData.swapFeeBtc), usdPricePrefetchPromise.then(value => { if (value != null) return value; return this._prices.preFetchUsdPrice(abortSignal); }) ]); if (!isValidAmount.isValid) throw new IntermediaryError_1.IntermediaryError("Fee too high"); isValidAmount.realPriceUsdPerBitcoin = usdPrice; return isValidAmount; } /** * Starts the interval calling the {@link ISwap._tick} on all the known swaps in tick-enabled states * @internal */ startTickInterval() { if (this.tickSwapState == null || this.tickSwapState.length === 0) return; if (this.tickAbortController != null) this.tickAbortController.abort("New tick interval has been started!"); const abortController = this.tickAbortController = new AbortController(); let run; run = async () => { if (!this.isInitialized) return; await this.tick(undefined, abortController.signal).catch(e => { if (abortController.signal.aborted) return; this.logger.warn("startTickInterval(): Tick on swaps failed, error: ", e); }); if (abortController.signal.aborted) return; if (!this.isInitialized) return; this.tickInterval = setTimeout(run, 1000); }; run(); } /** * Runs checks on passed swaps, syncing their state from on-chain data * * @param pastSwaps Swaps to check * @internal */ async _checkPastSwaps(pastSwaps) { const changedSwaps = []; const removeSwaps = []; await Promise.all(pastSwaps.map((swap) => swap._sync(false).then(changed => { if (swap.isQuoteExpired()) { removeSwaps.push(swap); this.logger.debug("_checkPastSwaps(): Removing expired swap: " + swap.getId()); } else { if (changed) changedSwaps.push(swap); } }).catch(e => this.logger.error("_checkPastSwaps(): Error when checking swap " + swap.getId() + ": ", e)))); return { changedSwaps, removeSwaps }; } /** * Initializes the swap wrapper, needs to be called before any other action can be taken * * @param noTimers Whether to skip scheduling a tick timer for the swaps, if the tick timer is not initiated * the swap states depending on e.g. expiry can be out of sync with the actual expiration of the swap * @param noCheckPastSwaps Whether to skip checking past swaps on initialization (by default all pending swaps * are re-checked on init, and their state is synchronized from the on-chain data) */ async init(noTimers = false, noCheckPastSwaps = false) { if (this.isInitialized) return; if (!noCheckPastSwaps) { //Save events received in the meantime into the event queue and process them only after we've checked and // processed all the past swaps let eventQueue = []; const initListener = (event, swap) => { eventQueue.push({ event, swap }); return Promise.resolve(); }; if (this.processEvent != null) this.unifiedChainEvents.registerListener(this.TYPE, initListener, this._swapDeserializer.bind(null, this)); await this.checkPastSwaps(); if (this.processEvent != null) { //Process accumulated event queue for (let event of eventQueue) { await this.processEvent(event.event, event.swap); } //Unregister the temporary event handler this.unifiedChainEvents.unregisterListener(this.TYPE); } } if (this.processEvent != null) this.unifiedChainEvents.registerListener(this.TYPE, this.processEvent.bind(this), this._swapDeserializer.bind(null, this)); this.isInitialized = true; if (!noTimers) this.startTickInterval(); // this.logger.info("init(): Swap wrapper initialized"); } /** * Un-subscribes from event listeners on the smart chain, terminates the tick interval and stops this wrapper */ async stop() { this.isInitialized = false; this.unifiedChainEvents.unregisterListener(this.TYPE); this.logger.info("stop(): Swap wrapper stopped"); if (this.tickInterval != null) { clearTimeout(this.tickInterval); delete this.tickInterval; } if (this.tickAbortController != null) { this.tickAbortController.abort("Wrapper instance stopped!"); delete this.tickAbortController; } } /** * Runs checks on all the known pending swaps, syncing their state from on-chain data * * @remarks Doesn't work properly if you pass non-persisted swaps * * @param pastSwaps Optional array of past swaps to check, otherwise all relevant swaps will be fetched * from the persistent storage * @param noSave Whether to skip saving the swap changes in the persistent storage */ async checkPastSwaps(pastSwaps, noSave) { if (pastSwaps == null) pastSwaps = await this.unifiedStorage.query([[{ key: "type", value: this.TYPE }, { key: "state", value: this._pendingSwapStates }]], (val) => new this._swapDeserializer(this, val)); const maxParallelSyncs = this._options.maxParallelSwapSyncs ?? exports.DEFAULT_MAX_PARALLEL_SWAP_SYNCS; const totalRemoveSwaps = []; const totalChangedSwaps = []; for (let i = 0; i < pastSwaps.length; i += maxParallelSyncs) { const { removeSwaps, changedSwaps } = await this._checkPastSwaps(pastSwaps.slice(i, i + maxParallelSyncs)); if (!noSave) { await this.unifiedStorage.removeAll(removeSwaps, true); await this.unifiedStorage.saveAll(changedSwaps, true); changedSwaps.forEach(swap => swap._emitEvent()); removeSwaps.forEach(swap => swap._emitEvent()); } totalRemoveSwaps.push(...removeSwaps); totalChangedSwaps.push(...changedSwaps); } return { removeSwaps: totalRemoveSwaps, changedSwaps: totalChangedSwaps }; } /** * Invokes {@link ISwap._tick} on all the known swaps * * @param swaps Optional array of swaps to invoke `_tick()` on, otherwise all relevant swaps will be fetched * from the persistent storage * @param abortSignal Abort signal */ async tick(swaps, abortSignal) { if (swaps == null) swaps = await this.unifiedStorage.query([[{ key: "type", value: this.TYPE }, { key: "state", value: this.tickSwapState }]], (val) => new this._swapDeserializer(this, val)); abortSignal?.throwIfAborted(); const parallelTicks = this._options.maxParallelSwapTicks ?? exports.DEFAULT_MAX_PARALLEL_SWAP_TICKS; let promises = []; for (let pendingSwap of this.pendingSwaps.values()) { const value = pendingSwap.deref(); if (value != null) promises.push(value._tick(true).catch(e => { this.logger.warn(`tick(): Error ticking swap ${value.getId()}: `, e); })); if (promises.length >= parallelTicks) { await Promise.all(promises); abortSignal?.throwIfAborted(); promises = []; } } for (let value of swaps) { promises.push(value._tick(true).catch(e => { this.logger.warn(`tick(): Error ticking swap ${value.getId()}: `, e); })); if (promises.length >= parallelTicks) { await Promise.all(promises); abortSignal?.throwIfAborted(); promises = []; } } if (promises.length > 0) await Promise.all(promises); abortSignal?.throwIfAborted(); } /** * Returns the smart chain's native token used to pay for fees * @internal */ _getNativeToken() { return this._tokens[this._chain.getNativeCurrencyAddress()]; } /** * Saves the swap, if it is not initiated it is only saved to pending swaps * * @param swap Swap to save * * @internal */ _saveSwapData(swap) { if (!this._options.saveUninitializedSwaps) { if (!swap.isInitiated()) { this.logger.debug("saveSwapData(): Swap " + swap.getId() + " not initiated, saving to pending swaps"); this.pendingSwaps.set(swap.getId(), new WeakRef(swap)); return Promise.resolve(); } else { this.pendingSwaps.delete(swap.getId()); } } return this.unifiedStorage.save(swap); } /** * Removes the swap from the persistent storage and pending swaps * * @param swap Swap to remove * * @internal */ _removeSwapData(swap) { this.pendingSwaps.delete(swap.getId()); if (!swap._persisted) return Promise.resolve(); return this.unifiedStorage.remove(swap); } /** * Retrieves a swap by its ID from the pending swap mapping * * @param id * * @internal */ _getPendingSwap(id) { return this.pendingSwaps.get(id)?.deref() ?? null; } /** * @internal */ async _getSignerAddress(signer) { let address = undefined; if (signer != null) { if (typeof (signer) === "string") { address = signer; } else if ((0, base_1.isAbstractSigner)(signer)) { address = signer.getAddress(); } else { address = (await this._chain.wrapSigner(signer)).getAddress(); } } return address; } } exports.ISwapWrapper = ISwapWrapper;