@atomiqlabs/sdk-lib
Version:
Basic SDK functionality library for atomiq
449 lines (381 loc) • 15.6 kB
text/typescript
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>;
}