UNPKG

@atomiqlabs/sdk-lib

Version:

Basic SDK functionality library for atomiq

449 lines (381 loc) 15.6 kB
import {SwapType} from "./enums/SwapType"; import {EventEmitter} from "events"; import {ISwapWrapper} from "./ISwapWrapper"; import {ChainType} from "@atomiqlabs/base"; import {isPriceInfoType, PriceInfoType} from "../prices/abstract/ISwapPrice"; import {LoggerType, randomBytes} from "../utils/Utils"; import {SCToken, TokenAmount} from "../Tokens"; import {SwapDirection} from "./enums/SwapDirection"; import {Fee, FeeBreakdown} from "./fee/Fee"; export type ISwapInit = { pricingInfo: PriceInfoType, url?: string, expiry: number, swapFee: bigint, swapFeeBtc?: bigint, exactIn: boolean }; export function isISwapInit(obj: any): obj is ISwapInit { return typeof obj === 'object' && obj != null && isPriceInfoType(obj.pricingInfo) && (obj.url==null || typeof obj.url === 'string') && typeof obj.expiry === 'number' && typeof(obj.swapFee) === "bigint" && (obj.swapFeeBtc == null || typeof(obj.swapFeeBtc) === "bigint") && (typeof obj.exactIn === 'boolean'); } export type PercentagePPM = { ppm: bigint, decimal: number, percentage: number, toString: (decimal?: number) => string }; export function ppmToPercentage(ppm: bigint): PercentagePPM { if(ppm==null) return null; const percentage = Number(ppm)/10_000; return { ppm, decimal: Number(ppm)/1_000_000, percentage: percentage, toString: (decimals?: number) => (decimals!=null ? percentage.toFixed(decimals) : percentage)+"%" } } export abstract class ISwap< T extends ChainType = ChainType, S extends number = number > { protected readonly abstract TYPE: SwapType; protected readonly currentVersion: number = 1; protected readonly wrapper: ISwapWrapper<T, ISwap<T, S>>; readonly url: string; readonly chainIdentifier: T["ChainId"]; readonly exactIn: boolean; createdAt: number; protected version: number; protected initiated: boolean = false; protected logger: LoggerType; expiry?: number; state: S; pricingInfo: PriceInfoType; protected swapFee: bigint; protected swapFeeBtc?: bigint; /** * 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<{swapState: [ISwap]}> = new EventEmitter(); protected constructor(wrapper: ISwapWrapper<T, ISwap<T, S>>, obj: any); protected constructor(wrapper: ISwapWrapper<T, ISwap<T, S>>, swapInit: ISwapInit); protected constructor( wrapper: ISwapWrapper<T, ISwap<T, S>>, swapInitOrObj: ISwapInit | 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.swapFee = swapInitOrObj.swapFee==null ? null : BigInt(swapInitOrObj.swapFee); this.swapFeeBtc = swapInitOrObj.swapFeeBtc==null ? null : BigInt(swapInitOrObj.swapFeeBtc); 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; /** * 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 protected tryRecomputeSwapPrice(): void { if(this.pricingInfo.swapPriceUSatPerToken==null) { if(this.getDirection()===SwapDirection.TO_BTC) { const input = this.getInput() as TokenAmount<T["ChainId"], SCToken<T["ChainId"]>>; this.pricingInfo = this.wrapper.prices.recomputePriceInfoSend( this.chainIdentifier, this.getOutput().rawAmount, this.pricingInfo.satsBaseFee, this.pricingInfo.feePPM, input.rawAmount, input.token.address ); } else { const output = this.getOutput() as TokenAmount<T["ChainId"], SCToken<T["ChainId"]>>; this.pricingInfo = this.wrapper.prices.recomputePriceInfoReceive( this.chainIdentifier, this.getInput().rawAmount, this.pricingInfo.satsBaseFee, this.pricingInfo.feePPM, output.rawAmount, output.token.address ); } } } /** * Re-fetches & revalidates the price data */ async refreshPriceData(): Promise<void> { if(this.pricingInfo==null) return null; if(this.getDirection()===SwapDirection.TO_BTC) { const input = this.getInput() as TokenAmount<T["ChainId"], SCToken<T["ChainId"]>>; this.pricingInfo = await this.wrapper.prices.isValidAmountSend( this.chainIdentifier, this.getOutput().rawAmount, this.pricingInfo.satsBaseFee, this.pricingInfo.feePPM, input.rawAmount, input.token.address ); } else { const output = this.getOutput() as TokenAmount<T["ChainId"], SCToken<T["ChainId"]>>; this.pricingInfo = await this.wrapper.prices.isValidAmountReceive( this.chainIdentifier, this.getInput().rawAmount, this.pricingInfo.satsBaseFee, this.pricingInfo.feePPM, output.rawAmount, output.token.address ); } } /** * 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 pricing info about the swap */ getPriceInfo(): { marketPrice: number, swapPrice: number, difference: PercentagePPM } { const swapPrice = this.getDirection()===SwapDirection.TO_BTC ? 100_000_000_000_000/Number(this.pricingInfo.swapPriceUSatPerToken) : Number(this.pricingInfo.swapPriceUSatPerToken)/100_000_000_000_000; const marketPrice = this.getDirection()===SwapDirection.TO_BTC ? 100_000_000_000_000/Number(this.pricingInfo.realPriceUSatPerToken) : Number(this.pricingInfo.realPriceUSatPerToken)/100_000_000_000_000; return { marketPrice, swapPrice, difference: ppmToPercentage(this.pricingInfo.differencePPM) } } ////////////////////////////// //// Getters & utils abstract _getEscrowHash(): 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 */ protected 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 */ abstract verifyQuoteValid(): Promise<boolean>; abstract getOutputAddress(): string | null; abstract getInputTxId(): string | null; abstract getOutputTxId(): string | null; /** * Returns the ID of the swap, as used in the storage and getSwapById function */ abstract getId(): string; /** * Checks whether there is some action required from the user for this swap - can mean either refundable or claimable */ abstract requiresAction(): boolean; /** * 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; isInitiated(): boolean { return this.initiated; } _setInitiated(): void { this.initiated = true; } /** * Returns quote expiry in UNIX millis */ getQuoteExpiry(): 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.TO_BTC || this.TYPE===SwapType.TO_BTCLN ? SwapDirection.TO_BTC : SwapDirection.FROM_BTC; } /** * Returns the current state of the swap */ getState(): S { return this.state; } ////////////////////////////// //// Amounts & fees /** * 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 */ abstract getFee(): Fee; /** * Returns the breakdown of all the fees paid */ abstract getFeeBreakdown(): FeeBreakdown<T["ChainId"]>; ////////////////////////////// //// Storage serialize(): any { if(this.pricingInfo==null) return {}; return { id: this.getId(), 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, swapFee: this.swapFee==null ? null : this.swapFee.toString(10), swapFeeBtc: this.swapFeeBtc==null ? null : this.swapFeeBtc.toString(10), 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 protected _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>; }