UNPKG

@atomiqlabs/sdk

Version:

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

391 lines (354 loc) 16.1 kB
import {ChainIds, MultiChain} from "../../swapper/Swapper"; import {Token} from "../../types/Token"; import {PriceInfoType} from "../../types/PriceInfoType"; /** * Abstract base class for swap pricing implementations * * @category Pricing */ export abstract class ISwapPrice<T extends MultiChain = MultiChain> { maxAllowedFeeDifferencePPM: bigint; protected constructor(maxAllowedFeeDifferencePPM: bigint) { this.maxAllowedFeeDifferencePPM = maxAllowedFeeDifferencePPM; } /** * Gets the decimal places for a given token, returns `-1` if token should be ignored & `null` if token is not found * * @param chainIdentifier Chain identifier of the smart chain * @param tokenAddress Token address * @protected */ protected abstract getDecimals<C extends ChainIds<T>>(chainIdentifier: C, tokenAddress: string): number | null; /** * Returns the price of the token in BTC uSats (microSats) * * @param chainIdentifier Chain identifier of the smart chain * @param tokenAddress Token address * @param abortSignal * @protected */ protected abstract getPrice<C extends ChainIds<T>>(chainIdentifier: C, tokenAddress: string, abortSignal?: AbortSignal): Promise<bigint>; /** * Returns the price of bitcoin in USD (sats/USD) * * @param abortSignal * @protected */ protected abstract getUsdPrice(abortSignal?: AbortSignal): Promise<number>; /** * Gets the decimal places for a given token, returns `-1` if token should be ignored & throws if token is not found * * @param chainIdentifier Chain identifier of the smart chain * @param tokenAddress Token address * @throws {Error} When token is not known * @protected */ protected getDecimalsThrowing<C extends ChainIds<T>>(chainIdentifier: C, tokenAddress: string): number { const decimals = this.getDecimals(chainIdentifier, tokenAddress); if(decimals==null) throw new Error(`Cannot get decimal count for token ${chainIdentifier}:${tokenAddress}!`); return decimals; } /** * Recomputes pricing info without fetching the current price * * @param chainIdentifier Chain identifier of the smart chain * @param amountSats Amount of sats (BTC) to be received from the swap * @param satsBaseFee Base fee in sats (BTC) as reported by the intermediary * @param feePPM PPM fee rate as reported by the intermediary * @param paidToken Amount of token to be paid to the swap * @param tokenAddress Token address to be paid */ public recomputePriceInfoSend<C extends ChainIds<T>>( chainIdentifier: C, amountSats: bigint, satsBaseFee: bigint, feePPM: bigint, paidToken: bigint, tokenAddress: string ): PriceInfoType { const totalSats = (amountSats * (1000000n + feePPM) / 1000000n) + satsBaseFee; const totalUSats = totalSats * 1000000n; const swapPriceUSatPerToken = totalUSats * (10n ** BigInt(this.getDecimalsThrowing(chainIdentifier, tokenAddress))) / paidToken; return { isValid: true, differencePPM: 0n, satsBaseFee, feePPM, realPriceUSatPerToken: this.shouldIgnore(chainIdentifier, tokenAddress) ? undefined : swapPriceUSatPerToken, swapPriceUSatPerToken }; } /** * Checks whether the swap amounts are valid given the current market rate for a given pair * * @param chainIdentifier Chain identifier of the smart chain * @param amountSats Amount of sats (BTC) to be received from the swap * @param satsBaseFee Base fee in sats (BTC) as reported by the intermediary * @param feePPM PPM fee rate as reported by the intermediary * @param paidToken Amount of token to be paid to the swap * @param tokenAddress Token address to be paid * @param abortSignal * @param preFetchedPrice An optional price pre-fetched with {@link preFetchPrice} * @param realSwapFeeSats */ public async isValidAmountSend<C extends ChainIds<T>>( chainIdentifier: C, amountSats: bigint, satsBaseFee: bigint, feePPM: bigint, paidToken: bigint, tokenAddress: string, abortSignal?: AbortSignal, preFetchedPrice?: bigint | null, realSwapFeeSats?: bigint ): Promise<PriceInfoType> { if(realSwapFeeSats!=undefined && realSwapFeeSats<0) throw new Error("Invalid swap fee! Swap fee cannot be negative!"); const totalSats = realSwapFeeSats!=undefined ? amountSats + realSwapFeeSats : (amountSats * (1000000n + feePPM) / 1000000n) + satsBaseFee; const totalUSats = totalSats * 1000000n; const swapPriceUSatPerToken = totalUSats * (10n ** BigInt(this.getDecimalsThrowing(chainIdentifier, tokenAddress))) / paidToken; if(this.shouldIgnore(chainIdentifier, tokenAddress)) return { isValid: true, differencePPM: 0n, satsBaseFee, feePPM, realPriceUSatPerToken: undefined, swapPriceUSatPerToken }; const calculatedAmtInToken = await this.getFromBtcSwapAmount(chainIdentifier, totalSats, tokenAddress, abortSignal, preFetchedPrice); const realPriceUSatPerToken = totalUSats * (10n ** BigInt(this.getDecimalsThrowing(chainIdentifier, tokenAddress))) / calculatedAmtInToken; const difference = paidToken - calculatedAmtInToken; //Will be >0 if we need to pay more than we should've const differencePPM = difference * 1000000n / calculatedAmtInToken; return { isValid: differencePPM <= this.maxAllowedFeeDifferencePPM, differencePPM, satsBaseFee, feePPM, realPriceUSatPerToken, swapPriceUSatPerToken }; } /** * Recomputes pricing info without fetching the current price * * @param chainIdentifier Chain identifier of the smart chain * @param amountSats Amount of sats (BTC) to be paid to the swap * @param satsBaseFee Base fee in sats (BTC) as reported by the intermediary * @param feePPM PPM fee rate as reported by the intermediary * @param receiveToken Amount of token to be received from the swap * @param tokenAddress Token address to be received */ public recomputePriceInfoReceive<C extends ChainIds<T>>( chainIdentifier: C, amountSats: bigint, satsBaseFee: bigint, feePPM: bigint, receiveToken: bigint, tokenAddress: string, ): PriceInfoType { const totalSats = (amountSats * (1000000n - feePPM) / 1000000n) - satsBaseFee; const totalUSats = totalSats * 1000000n; const swapPriceUSatPerToken = totalUSats * (10n ** BigInt(this.getDecimalsThrowing(chainIdentifier, tokenAddress))) / receiveToken; return { isValid: true, differencePPM: 0n, satsBaseFee, feePPM, realPriceUSatPerToken: this.shouldIgnore(chainIdentifier, tokenAddress) ? undefined : swapPriceUSatPerToken, swapPriceUSatPerToken }; } /** * Checks whether the swap amounts are valid given the current market rate for a given pair * * @param chainIdentifier Chain identifier of the smart chain * @param amountSats Amount of sats (BTC) to be paid to the swap * @param satsBaseFee Base fee in sats (BTC) as reported by the intermediary * @param feePPM PPM fee rate as reported by the intermediary * @param receiveToken Amount of token to be received from the swap * @param tokenAddress Token address to be received * @param abortSignal * @param preFetchedPrice An optional price pre-fetched with {@link preFetchPrice} * @param realSwapFeeSats */ public async isValidAmountReceive<C extends ChainIds<T>>( chainIdentifier: C, amountSats: bigint, satsBaseFee: bigint, feePPM: bigint, receiveToken: bigint, tokenAddress: string, abortSignal?: AbortSignal, preFetchedPrice?: bigint | null, realSwapFeeSats?: bigint ): Promise<PriceInfoType> { if(realSwapFeeSats!=undefined) { if(realSwapFeeSats>=amountSats) throw new Error("Invalid swap fee! Larger than or equal to total output amount!"); if(realSwapFeeSats<0) throw new Error("Invalid swap fee! Must be non-negative!"); } const totalSats = realSwapFeeSats!=undefined ? amountSats - realSwapFeeSats : (amountSats * (1000000n - feePPM) / 1000000n) - satsBaseFee; const totalUSats = totalSats * 1000000n; const swapPriceUSatPerToken = totalUSats * (10n ** BigInt(this.getDecimalsThrowing(chainIdentifier, tokenAddress))) / receiveToken; if(this.shouldIgnore(chainIdentifier, tokenAddress)) return { isValid: true, differencePPM: 0n, satsBaseFee, feePPM, realPriceUSatPerToken: undefined, swapPriceUSatPerToken }; const calculatedAmtInToken = await this.getFromBtcSwapAmount(chainIdentifier, totalSats, tokenAddress, abortSignal, preFetchedPrice); const realPriceUSatPerToken = totalUSats * (10n ** BigInt(this.getDecimalsThrowing(chainIdentifier, tokenAddress))) / calculatedAmtInToken; const difference = calculatedAmtInToken - receiveToken; //Will be >0 if we receive less than we should've const differencePPM = difference * 100000n / calculatedAmtInToken; return { isValid: differencePPM <= this.maxAllowedFeeDifferencePPM, differencePPM, satsBaseFee, feePPM, realPriceUSatPerToken, swapPriceUSatPerToken }; } /** * Pre-fetches the pricing data for a given token, such that further calls to {@link isValidAmountReceive} or * {@link isValidAmountSend} are quicker and don't need to wait for the price fetch * * @param chainIdentifier Chain identifier of the smart chain * @param tokenAddress Token address * @param abortSignal */ public preFetchPrice<C extends ChainIds<T>>(chainIdentifier: C, tokenAddress: string, abortSignal?: AbortSignal): Promise<bigint> { return this.getPrice(chainIdentifier, tokenAddress, abortSignal); } /** * Pre-fetches the Bitcoin USD price data, such that further calls to {@link getBtcUsdValue}, * {@link getTokenUsdValue} or {@link getUsdValue} are quicker and don't need to wait for the price fetch * * @param abortSignal */ public preFetchUsdPrice(abortSignal?: AbortSignal): Promise<number> { return this.getUsdPrice(abortSignal); } /** * Returns amount of `toToken` that is equivalent to `fromAmount` satoshis * * @param chainIdentifier Chain identifier string for the smart chain * @param fromAmount Amount of satoshis * @param toToken Token address * @param abortSignal * @param preFetchedPrice An optional price pre-fetched with {@link preFetchPrice} * @throws {Error} when token is not found */ public async getFromBtcSwapAmount<C extends ChainIds<T>>( chainIdentifier: C, fromAmount: bigint, toToken: string, abortSignal?: AbortSignal, preFetchedPrice?: bigint | null ): Promise<bigint> { if(this.getDecimals(chainIdentifier, toToken.toString())==null) throw new Error("Token not found!"); const price = preFetchedPrice || await this.getPrice(chainIdentifier, toToken, abortSignal); return fromAmount * (10n ** BigInt(this.getDecimalsThrowing(chainIdentifier, toToken.toString()))) * (1000000n) //To usat / (price); } /** * Returns amount of satoshis that are equivalent to `fromAmount` of `fromToken` * * @param chainIdentifier Chain identifier string for the smart chain * @param fromAmount Amount of the token * @param fromToken Token address * @param abortSignal * @param preFetchedPrice An optional price pre-fetched with {@link preFetchPrice} * @throws {Error} when token is not found */ public async getToBtcSwapAmount<C extends ChainIds<T>>( chainIdentifier: C, fromAmount: bigint, fromToken: string, abortSignal?: AbortSignal, preFetchedPrice?: bigint ): Promise<bigint> { if(this.getDecimals(chainIdentifier, fromToken.toString())==null) throw new Error("Token not found"); const price = preFetchedPrice || await this.getPrice(chainIdentifier, fromToken, abortSignal); return fromAmount * price / 1000000n / (10n ** BigInt(this.getDecimalsThrowing(chainIdentifier, fromToken.toString()))); } /** * Returns whether the token should be ignored and pricing for it not calculated * * @param chainIdentifier Chain identifier string for the smart chain * @param tokenAddress Token address * @throws {Error} if token is not found */ public shouldIgnore<C extends ChainIds<T>>(chainIdentifier: C, tokenAddress: string): boolean { const coin = this.getDecimals(chainIdentifier, tokenAddress.toString()); if(coin==null) throw new Error("Token not found"); return coin===-1; } /** * Returns the USD value of the bitcoin amount * * @param btcSats Bitcoin amount in satoshis * @param abortSignal * @param preFetchedUsdPrice An optional price pre-fetched with {@link preFetchUsdPrice} */ public async getBtcUsdValue( btcSats: bigint, abortSignal?: AbortSignal, preFetchedUsdPrice?: number ): Promise<number> { return Number(btcSats)*(preFetchedUsdPrice || await this.getUsdPrice(abortSignal)); } /** * Returns the USD value of the smart chain token amount * * @param chainIdentifier Chain identifier string for the smart chain * @param tokenAmount Amount of the token in base units * @param tokenAddress Token address * @param abortSignal * @param preFetchedUsdPrice An optional price pre-fetched with {@link preFetchUsdPrice} */ public async getTokenUsdValue<C extends ChainIds<T>>( chainIdentifier: C, tokenAmount: bigint, tokenAddress: string, abortSignal?: AbortSignal, preFetchedUsdPrice?: number ): Promise<number> { const [btcAmount, usdPrice] = await Promise.all([ this.getToBtcSwapAmount(chainIdentifier, tokenAmount, tokenAddress, abortSignal), preFetchedUsdPrice==null ? this.preFetchUsdPrice(abortSignal) : Promise.resolve(preFetchedUsdPrice) ]); return Number(btcAmount)*usdPrice; } /** * Returns the USD value of the token amount * * @param amount Amount in base units of the token * @param token Token to fetch the usd price for * @param abortSignal * @param preFetchedUsdPrice An optional price pre-fetched with {@link preFetchUsdPrice} */ public getUsdValue<C extends ChainIds<T>>( amount: bigint, token: Token<C>, abortSignal?: AbortSignal, preFetchedUsdPrice?: number ): Promise<number> { if(token.chain==="BTC") { return this.getBtcUsdValue(amount, abortSignal, preFetchedUsdPrice); } else { return this.getTokenUsdValue(token.chainId, amount, token.address, abortSignal, preFetchedUsdPrice); } } }