UNPKG

@atomiqlabs/sdk-lib

Version:

Basic SDK functionality library for atomiq

585 lines (505 loc) 21.2 kB
import {SwapType} from "./SwapType"; import {EventEmitter} from "events"; import {Buffer} from "buffer"; import {ISwapWrapper} from "./ISwapWrapper"; import {ChainType, SignatureData, SignatureVerificationError, SwapCommitStatus, SwapData} from "@atomiqlabs/base"; import {isPriceInfoType, PriceInfoType} from "../prices/abstract/ISwapPrice"; import {LoggerType, randomBytes, timeoutPromise, tryWithRetries} from "../utils/Utils"; import {SCToken, Token, TokenAmount, toTokenAmount} from "./Tokens"; import {SwapDirection} from "./SwapDirection"; export type ISwapInit<T extends SwapData> = { pricingInfo: PriceInfoType, url: string, expiry: number, swapFee: bigint, swapFeeBtc?: bigint, feeRate: any, signatureData?: SignatureData, data?: T, exactIn: boolean }; export function isISwapInit<T extends SwapData>(obj: any): obj is ISwapInit<T> { return typeof obj === 'object' && obj != null && isPriceInfoType(obj.pricingInfo) && typeof obj.url === 'string' && typeof obj.expiry === 'number' && typeof(obj.swapFee) === "bigint" && (obj.swapFeeBtc == null || typeof(obj.swapFeeBtc) === "bigint") && obj.feeRate != null && (obj.signatureData == null || ( typeof(obj.signatureData) === 'object' && typeof(obj.signatureData.prefix)==="string" && typeof(obj.signatureData.timeout)==="string" && typeof(obj.signatureData.signature)==="string" )) && (obj.data == null || typeof obj.data === 'object') && (typeof obj.exactIn === 'boolean'); } export type Fee< ChainIdentifier extends string = string, TSrc extends Token<ChainIdentifier> = Token<ChainIdentifier>, TDst extends Token<ChainIdentifier> = Token<ChainIdentifier> > = { amountInSrcToken: TokenAmount<ChainIdentifier, TSrc>; amountInDstToken: TokenAmount<ChainIdentifier, TDst>; usdValue: (abortSignal?: AbortSignal, preFetchedUsdPrice?: number) => Promise<number>; } export abstract class ISwap< T extends ChainType = ChainType, S extends number = number > { readonly chainIdentifier: string; readonly exactIn: boolean; readonly createdAt: number; protected readonly currentVersion: number = 1; protected version: number; protected initiated: boolean = false; protected logger: LoggerType; protected readonly abstract TYPE: SwapType; protected readonly wrapper: ISwapWrapper<T, ISwap<T, S>>; expiry?: number; readonly url: string; state: S; pricingInfo: PriceInfoType; data: T["Data"]; signatureData?: SignatureData; feeRate?: any; protected swapFee: bigint; protected swapFeeBtc?: bigint; /** * Transaction IDs for the swap on the smart chain side */ commitTxId: string; refundTxId?: string; claimTxId?: string; /** * Random nonce to differentiate the swap from others with the same identifier hash (i.e. when quoting the same swap * from multiple LPs) */ randomNonce: string; /** * Event emitter emitting "swapState" event when swap's state changes */ events: EventEmitter = new EventEmitter(); protected constructor(wrapper: ISwapWrapper<T, ISwap<T, S>>, obj: any); protected constructor(wrapper: ISwapWrapper<T, ISwap<T, S>>, swapInit: ISwapInit<T["Data"]>); protected constructor( wrapper: ISwapWrapper<T, ISwap<T, S>>, swapInitOrObj: ISwapInit<T["Data"]> | any, ) { this.chainIdentifier = wrapper.chainIdentifier; this.wrapper = wrapper; if(isISwapInit(swapInitOrObj)) { Object.assign(this, swapInitOrObj); this.version = this.currentVersion; this.createdAt = Date.now(); this.randomNonce = randomBytes(16).toString("hex"); } else { this.expiry = swapInitOrObj.expiry; this.url = swapInitOrObj.url; this.state = swapInitOrObj.state; this.pricingInfo = { isValid: swapInitOrObj._isValid, differencePPM: swapInitOrObj._differencePPM==null ? null : BigInt(swapInitOrObj._differencePPM), satsBaseFee: swapInitOrObj._satsBaseFee==null ? null : BigInt(swapInitOrObj._satsBaseFee), feePPM: swapInitOrObj._feePPM==null ? null : BigInt(swapInitOrObj._feePPM), realPriceUSatPerToken: swapInitOrObj._realPriceUSatPerToken==null ? null : BigInt(swapInitOrObj._realPriceUSatPerToken), swapPriceUSatPerToken: swapInitOrObj._swapPriceUSatPerToken==null ? null : BigInt(swapInitOrObj._swapPriceUSatPerToken), }; this.data = swapInitOrObj.data!=null ? new wrapper.swapDataDeserializer(swapInitOrObj.data) : null; this.swapFee = swapInitOrObj.swapFee==null ? null : BigInt(swapInitOrObj.swapFee); this.swapFeeBtc = swapInitOrObj.swapFeeBtc==null ? null : BigInt(swapInitOrObj.swapFeeBtc); this.signatureData = swapInitOrObj.signature==null ? null : { prefix: swapInitOrObj.prefix, timeout: swapInitOrObj.timeout, signature: swapInitOrObj.signature }; this.feeRate = swapInitOrObj.feeRate; this.commitTxId = swapInitOrObj.commitTxId; this.claimTxId = swapInitOrObj.claimTxId; this.refundTxId = swapInitOrObj.refundTxId; this.version = swapInitOrObj.version; this.initiated = swapInitOrObj.initiated; this.exactIn = swapInitOrObj.exactIn; this.createdAt = swapInitOrObj.createdAt ?? swapInitOrObj.expiry; this.randomNonce = swapInitOrObj.randomNonce; } if(this.version!==this.currentVersion) { this.upgradeVersion(); } if(this.initiated==null) this.initiated = true; } protected abstract upgradeVersion(): void; /** * Periodically checks for init signature's expiry * * @param abortSignal * @param interval How often to check (in seconds), default to 5s * @protected */ protected async watchdogWaitTillSignatureExpiry(abortSignal?: AbortSignal, interval: number = 5): Promise<void> { let expired = false while(!expired) { await timeoutPromise(interval*1000, abortSignal); try { expired = await this.wrapper.contract.isInitAuthorizationExpired(this.data, this.signatureData); } catch (e) { this.logger.warn("watchdogWaitTillSignatureExpiry(): Error when checking signature expiry: ", e); } } if(abortSignal!=null) abortSignal.throwIfAborted(); } /** * Periodically checks the chain to see whether the swap is committed * * @param abortSignal * @param interval How often to check (in seconds), default to 5s * @protected */ protected async watchdogWaitTillCommited(abortSignal?: AbortSignal, interval: number = 5): Promise<boolean> { let status: SwapCommitStatus = SwapCommitStatus.NOT_COMMITED; while(status===SwapCommitStatus.NOT_COMMITED) { await timeoutPromise(interval*1000, abortSignal); try { status = await this.wrapper.contract.getCommitStatus(this.getInitiator(), this.data); if( status===SwapCommitStatus.NOT_COMMITED && await this.wrapper.contract.isInitAuthorizationExpired(this.data, this.signatureData) ) return false; } catch (e) { this.logger.warn("watchdogWaitTillCommited(): Error when fetching commit status or signature expiry: ", e); } } if(abortSignal!=null) abortSignal.throwIfAborted(); return true; } /** * Periodically checks the chain to see whether the swap was finished (claimed or refunded) * * @param abortSignal * @param interval How often to check (in seconds), default to 5s * @protected */ protected async watchdogWaitTillResult(abortSignal?: AbortSignal, interval: number = 5): Promise< SwapCommitStatus.PAID | SwapCommitStatus.EXPIRED | SwapCommitStatus.NOT_COMMITED > { let status: SwapCommitStatus = SwapCommitStatus.COMMITED; while(status===SwapCommitStatus.COMMITED || status===SwapCommitStatus.REFUNDABLE) { await timeoutPromise(interval*1000, abortSignal); try { status = await this.wrapper.contract.getCommitStatus(this.getInitiator(), this.data); } catch (e) { this.logger.warn("watchdogWaitTillResult(): Error when fetching commit status: ", e); } } if(abortSignal!=null) abortSignal.throwIfAborted(); return status; } /** * Waits till the swap reaches a specific state * * @param targetState The state to wait for * @param type Whether to wait for the state exactly or also to a state with a higher number * @param abortSignal * @protected */ protected waitTillState(targetState: S, type: "eq" | "gte" | "neq" = "eq", abortSignal?: AbortSignal): Promise<void> { return new Promise((resolve, reject) => { let listener; listener = (swap) => { if(type==="eq" ? swap.state===targetState : type==="gte" ? swap.state>=targetState : swap.state!=targetState) { resolve(); this.events.removeListener("swapState", listener); } }; this.events.on("swapState", listener); if(abortSignal!=null) abortSignal.addEventListener("abort", () => { this.events.removeListener("swapState", listener); reject(abortSignal.reason); }); }); } ////////////////////////////// //// Pricing /** * Checks if the pricing for the swap is valid, according to max allowed price difference set in the ISwapPrice */ hasValidPrice(): boolean { return this.pricingInfo==null ? null : this.pricingInfo.isValid; } /** * Returns the price difference between offered price and current market price in PPM (parts per million) */ getPriceDifferencePPM(): bigint { return this.pricingInfo==null ? null :this.pricingInfo.differencePPM; } /** * Returns the price difference between offered price and current market price as a decimal number */ getPriceDifferencePct(): number { return this.pricingInfo==null ? null : this.pricingInfo.differencePPM==null ? null : Number(this.pricingInfo.differencePPM)/1000000; } /** * Re-fetches & revalidates the price data */ abstract refreshPriceData(): Promise<PriceInfoType>; /** * Returns the offered swap quote price */ abstract getSwapPrice(): number; /** * Returns the real current market price fetched from reputable exchanges */ abstract getMarketPrice(): number; /** * Returns the real swap fee percentage as PPM (parts per million) */ abstract getRealSwapFeePercentagePPM(): bigint; ////////////////////////////// //// Getters & utils abstract getInputTxId(): string | null; abstract getOutputTxId(): string | null; abstract getInputAddress(): string | null; abstract getOutputAddress(): string | null; /** * Returns the escrow hash - i.e. hash of the escrow data */ getEscrowHash(): string | null { return this.data?.getEscrowHash(); } /** * Returns the claim data hash - i.e. hash passed to the claim handler */ getClaimHash(): string { return this.data?.getClaimHash(); } /** * Returns the identification hash of the swap, usually claim data hash, but can be overriden, e.g. for * lightning swaps the identifier hash is used instead of claim data hash */ getIdentifierHash(): Buffer { const claimHashBuffer = Buffer.from(this.getClaimHash(), "hex"); if(this.randomNonce==null) return claimHashBuffer; return Buffer.concat([claimHashBuffer, Buffer.from(this.randomNonce, "hex")]); } /** * Returns the identification hash of the swap, usually claim data hash, but can be overriden, e.g. for * lightning swaps the identifier hash is used instead of claim data hash */ getIdentifierHashString(): string { const paymentHash = this.getIdentifierHash(); if(paymentHash==null) return null; return paymentHash.toString("hex"); } /** * Returns the ID of the swap, as used in the storage and getSwapById function */ getId(): string { return this.getIdentifierHashString(); } /** * Returns quote expiry in UNIX millis */ getExpiry(): number { return this.expiry; } /** * Returns the type of the swap */ getType(): SwapType { return this.TYPE; } /** * Returns the direction of the swap */ getDirection(): SwapDirection { return this.TYPE===SwapType.FROM_BTCLN || this.TYPE===SwapType.FROM_BTC ? SwapDirection.FROM_BTC : SwapDirection.TO_BTC; } /** * Returns the current state of the swap */ getState(): S { return this.state; } /** * Returns whether the swap is finished and in its terminal state (this can mean successful, refunded or failed) */ abstract isFinished(): boolean; /** * Checks whether the swap's quote has definitely expired and cannot be committed anymore, we can remove such swap */ abstract isQuoteExpired(): boolean; /** * Checks whether the swap's quote is soft expired (this means there is not enough time buffer for it to commit, * but it still can happen) */ abstract isQuoteSoftExpired(): boolean; /** * Returns whether the swap finished successful */ abstract isSuccessful(): boolean; /** * Returns whether the swap failed (e.g. was refunded) */ abstract isFailed(): boolean; /** * Returns the intiator address of the swap - address that created this swap */ abstract getInitiator(): string; /** * @param signer Signer to check with this swap's initiator * @throws {Error} When signer's address doesn't match with the swap's initiator one */ checkSigner(signer: T["Signer"] | string): void { if((typeof(signer)==="string" ? signer : signer.getAddress())!==this.getInitiator()) throw new Error("Invalid signer provided!"); } /** * Checks if the swap's quote is still valid */ async isQuoteValid(): Promise<boolean> { try { await tryWithRetries( () => this.wrapper.contract.isValidInitAuthorization( this.data, this.signatureData, this.feeRate ), null, SignatureVerificationError ); return true; } catch (e) { if(e instanceof SignatureVerificationError) { return false; } } } isInitiated(): boolean { return this.initiated; } /** * Checks whether there is some action required from the user for this swap - can mean either refundable or claimable */ abstract isActionable(): boolean; ////////////////////////////// //// Amounts & fees /** * Get the estimated smart chain fee of the commit transaction */ getCommitFee(): Promise<bigint> { return this.wrapper.contract.getCommitFee(this.data, this.feeRate); } /** * Returns output amount of the swap, user receives this much */ abstract getOutput(): TokenAmount; /** * Returns input amount of the swap, user needs to pay this much */ abstract getInput(): TokenAmount; /** * Returns input amount if the swap without the fees (swap fee, network fee) */ abstract getInputWithoutFee(): TokenAmount; /** * Returns total fee for the swap, the fee is represented in source currency & destination currency, but is * paid only once */ getFee(): Fee { return this.getSwapFee(); } /** * Returns swap fee for the swap, the fee is represented in source currency & destination currency, but is * paid only once */ abstract getSwapFee(): Fee; /** * Returns the transaction fee paid on the smart chain */ async getSmartChainNetworkFee(): Promise<TokenAmount<T["ChainId"], SCToken<T["ChainId"]>>> { const swapContract: T["Contract"] & {getRawCommitFee?: (data: T["Data"], feeRate?: string) => Promise<bigint>} = this.wrapper.contract; return toTokenAmount( await ( swapContract.getRawCommitFee!=null ? swapContract.getRawCommitFee(this.data, this.feeRate) : swapContract.getCommitFee(this.data, this.feeRate) ), this.wrapper.getNativeToken(), this.wrapper.prices ); } /** * Checks if the initiator/sender has enough balance to cover the transaction fee for processing the swap */ abstract hasEnoughForTxFees(): Promise<{enoughBalance: boolean, balance: TokenAmount, required: TokenAmount}>; ////////////////////////////// //// Storage serialize(): any { if(this.pricingInfo==null) return {}; return { id: this.getIdentifierHashString(), type: this.getType(), escrowHash: this.getEscrowHash(), initiator: this.getInitiator(), _isValid: this.pricingInfo.isValid, _differencePPM: this.pricingInfo.differencePPM==null ? null :this.pricingInfo.differencePPM.toString(10), _satsBaseFee: this.pricingInfo.satsBaseFee==null ? null :this.pricingInfo.satsBaseFee.toString(10), _feePPM: this.pricingInfo.feePPM==null ? null :this.pricingInfo.feePPM.toString(10), _realPriceUSatPerToken: this.pricingInfo.realPriceUSatPerToken==null ? null :this.pricingInfo.realPriceUSatPerToken.toString(10), _swapPriceUSatPerToken: this.pricingInfo.swapPriceUSatPerToken==null ? null :this.pricingInfo.swapPriceUSatPerToken.toString(10), state: this.state, url: this.url, data: this.data!=null ? this.data.serialize() : null, swapFee: this.swapFee==null ? null : this.swapFee.toString(10), swapFeeBtc: this.swapFeeBtc==null ? null : this.swapFeeBtc.toString(10), prefix: this.signatureData?.prefix, timeout: this.signatureData?.timeout, signature: this.signatureData?.signature, feeRate: this.feeRate==null ? null : this.feeRate.toString(), commitTxId: this.commitTxId, claimTxId: this.claimTxId, refundTxId: this.refundTxId, expiry: this.expiry, version: this.version, initiated: this.initiated, exactIn: this.exactIn, createdAt: this.createdAt, randomNonce: this.randomNonce } } _save(): Promise<void> { if(this.isQuoteExpired()) { return this.wrapper.removeSwapData(this); } else { return this.wrapper.saveSwapData(this); } } async _saveAndEmit(state?: S): Promise<void> { if(state!=null) this.state = state; await this._save(); this._emitEvent(); } ////////////////////////////// //// Events _emitEvent() { this.wrapper.events.emit("swapState", this); this.events.emit("swapState", this); } ////////////////////////////// //// Swap ticks & sync /** * Synchronizes swap state from chain and/or LP node, usually ran on startup * * @param save whether to save the new swap state or not * * @returns {boolean} true if the swap changed, false if the swap hasn't changed */ abstract _sync(save?: boolean): Promise<boolean>; /** * Runs quick checks on the swap, such as checking the expiry, usually ran periodically every few seconds * * @param save whether to save the new swap state or not * * @returns {boolean} true if the swap changed, false if the swap hasn't changed */ abstract _tick(save?: boolean): Promise<boolean>; }