@atomiqlabs/sdk
Version:
atomiq labs SDK for cross-chain swaps between smart chains and bitcoin
391 lines (354 loc) • 16.1 kB
text/typescript
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);
}
}
}