@atomiqlabs/sdk-lib
Version:
Basic SDK functionality library for atomiq
237 lines (219 loc) • 9.47 kB
text/typescript
import {IPriceProvider} from "./abstract/IPriceProvider";
import {BinancePriceProvider} from "./providers/BinancePriceProvider";
import {OKXPriceProvider} from "./providers/OKXPriceProvider";
import {CoinGeckoPriceProvider} from "./providers/CoinGeckoPriceProvider";
import {CoinPaprikaPriceProvider} from "./providers/CoinPaprikaPriceProvider";
import {promiseAny, tryWithRetries, getLogger} from "../utils/Utils";
import {ICachedSwapPrice} from "./abstract/ICachedSwapPrice";
import {RequestError} from "../errors/RequestError";
import {ChainIds, MultiChain} from "../swaps/Swapper";
import {KrakenPriceProvider} from "./providers/KrakenPriceProvider";
export type RedundantSwapPriceAssets<T extends MultiChain> = {
binancePair?: string,
okxPair?: string,
coinGeckoCoinId?: string,
coinPaprikaCoinId?: string,
krakenPair?: string,
chains: {
[chainIdentifier in keyof T]?: {
address: string,
decimals: number
}
}
}[];
export type CtorCoinDecimals<T extends MultiChain> = {
chains: {
[chainIdentifier in keyof T]?: {
address: string,
decimals: number
}
}
}[];
type CoinDecimals<T extends MultiChain> = {
[chainIdentifier in keyof T]?: {
[tokenAddress: string]: number
}
};
const logger = getLogger("RedundantSwapPrice: ");
/**
* Swap price API using multiple price sources, handles errors on the APIs and automatically switches between them, such
* that there always is a functional API
*/
export class RedundantSwapPrice<T extends MultiChain> extends ICachedSwapPrice<T> {
static createFromTokenMap<T extends MultiChain>(maxAllowedFeeDiffPPM: bigint, assets: RedundantSwapPriceAssets<T>, cacheTimeout?: number): RedundantSwapPrice<T> {
const priceApis = [
new BinancePriceProvider(assets.map(coinData => {
return {
coinId: coinData.binancePair,
chains: coinData.chains
};
})),
new OKXPriceProvider(assets.map(coinData => {
return {
coinId: coinData.okxPair,
chains: coinData.chains
};
})),
new CoinGeckoPriceProvider(assets.map(coinData => {
return {
coinId: coinData.coinGeckoCoinId,
chains: coinData.chains
};
})),
new CoinPaprikaPriceProvider(assets.map(coinData => {
return {
coinId: coinData.coinPaprikaCoinId,
chains: coinData.chains
};
})),
new KrakenPriceProvider(assets.map(coinData => {
return {
coinId: coinData.krakenPair,
chains: coinData.chains
};
}))
];
return new RedundantSwapPrice(maxAllowedFeeDiffPPM, assets, priceApis, cacheTimeout);
}
coinsDecimals: CoinDecimals<T> = {};
priceApis: {
priceApi: IPriceProvider<T>,
operational: boolean
}[];
constructor(maxAllowedFeeDiffPPM: bigint, coinsDecimals: CtorCoinDecimals<T>, priceApis: IPriceProvider<T>[], cacheTimeout?: number) {
super(maxAllowedFeeDiffPPM, cacheTimeout);
for(let coinData of coinsDecimals) {
for(let chainId in coinData.chains) {
const {address, decimals} = coinData.chains[chainId];
this.coinsDecimals[chainId] ??= {};
(this.coinsDecimals[chainId] as any)[address.toString()] = decimals;
}
}
this.priceApis = priceApis.map(api => {
return {
priceApi: api,
operational: null
}
});
}
/**
* Returns price api that should be operational
*
* @private
*/
private getOperationalPriceApi(): {priceApi: IPriceProvider<T>, operational: boolean} {
return this.priceApis.find(e => e.operational===true);
}
/**
* Returns price apis that are maybe operational, in case none is considered operational returns all of the price
* apis such that they can be tested again whether they are operational
*
* @private
*/
private getMaybeOperationalPriceApis(): {priceApi: IPriceProvider<T>, operational: boolean}[] {
let operational = this.priceApis.filter(e => e.operational===true || e.operational===null);
if(operational.length===0) {
this.priceApis.forEach(e => e.operational=null);
operational = this.priceApis;
}
return operational;
}
/**
* Fetches price in parallel from multiple maybe operational price APIs
*
* @param chainIdentifier
* @param token
* @param abortSignal
* @private
*/
private async fetchPriceFromMaybeOperationalPriceApis<C extends ChainIds<T>>(chainIdentifier: C, token: string, abortSignal?: AbortSignal) {
try {
return await promiseAny<bigint>(this.getMaybeOperationalPriceApis().map(
obj => (async () => {
try {
const price = await obj.priceApi.getPrice(chainIdentifier, token, abortSignal);
logger.debug("fetchPrice(): Price from "+obj.priceApi.constructor.name+": ", price.toString(10));
obj.operational = true;
return price;
} catch (e) {
if(abortSignal!=null) abortSignal.throwIfAborted();
obj.operational = false;
throw e;
}
})()
))
} catch (e) {
if(abortSignal!=null) abortSignal.throwIfAborted();
throw e.find(err => !(err instanceof RequestError)) || e[0];
}
}
/**
* Fetches the prices, first tries to use the operational price API (if any) and if that fails it falls back
* to using maybe operational price APIs
*
* @param chainIdentifier
* @param token
* @param abortSignal
* @private
*/
protected fetchPrice<C extends ChainIds<T>>(chainIdentifier: C, token: string, abortSignal?: AbortSignal): Promise<bigint> {
return tryWithRetries(async () => {
const operationalPriceApi = this.getOperationalPriceApi();
if (operationalPriceApi != null) {
try {
return await operationalPriceApi.priceApi.getPrice(chainIdentifier, token, abortSignal);
} catch (err) {
if (abortSignal != null)
abortSignal.throwIfAborted();
operationalPriceApi.operational = false;
return await this.fetchPriceFromMaybeOperationalPriceApis(chainIdentifier, token, abortSignal);
}
}
return await this.fetchPriceFromMaybeOperationalPriceApis(chainIdentifier, token, abortSignal);
}, null, RequestError, abortSignal);
}
protected getDecimals<C extends ChainIds<T>>(chainIdentifier: C, token: string): number | null {
if(this.coinsDecimals[chainIdentifier]==null) return null;
return this.coinsDecimals[chainIdentifier][token.toString()];
}
/**
* Fetches BTC price in USD in parallel from multiple maybe operational price APIs
*
* @param abortSignal
* @private
*/
private async fetchUsdPriceFromMaybeOperationalPriceApis(abortSignal?: AbortSignal): Promise<number> {
try {
return await promiseAny<number>(this.getMaybeOperationalPriceApis().map(
obj => (async () => {
try {
const price = await obj.priceApi.getUsdPrice(abortSignal);
logger.debug("fetchPrice(): USD price from "+obj.priceApi.constructor.name+": ", price.toString(10));
obj.operational = true;
return price;
} catch (e) {
if(abortSignal!=null) abortSignal.throwIfAborted();
obj.operational = false;
throw e;
}
})()
))
} catch (e) {
if(abortSignal!=null) abortSignal.throwIfAborted();
throw e.find(err => !(err instanceof RequestError)) || e[0];
}
}
protected fetchUsdPrice(abortSignal?: AbortSignal): Promise<number> {
return tryWithRetries(() => {
const operationalPriceApi = this.getOperationalPriceApi();
if(operationalPriceApi!=null) {
return operationalPriceApi.priceApi.getUsdPrice(abortSignal).catch(err => {
if(abortSignal!=null) abortSignal.throwIfAborted();
operationalPriceApi.operational = false;
return this.fetchUsdPriceFromMaybeOperationalPriceApis(abortSignal);
});
}
return this.fetchUsdPriceFromMaybeOperationalPriceApis(abortSignal);
}, null, RequestError, abortSignal);
}
}