UNPKG

@atomiqlabs/sdk-lib

Version:

Basic SDK functionality library for atomiq

237 lines (219 loc) 9.47 kB
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); } }