UNPKG

0xweb

Version:

Contract package manager and other web3 tools

266 lines (205 loc) 9.04 kB
import di from 'a-di'; import { IBlockchainExplorer } from '@dequanto/explorer/IBlockchainExplorer'; import { Web3Client } from '@dequanto/clients/Web3Client'; import { IToken } from '@dequanto/models/IToken'; import { TAddress } from '@dequanto/models/TAddress'; import { TokenUtils } from './utils/TokenUtils'; import { TokensService } from './TokensService'; import { $address } from '@dequanto/utils/$address'; import { TResultAsync } from '@dequanto/models/TResult'; import { ISwapped, ISwapPool, ISwapRouted } from './TokenExchanges/AmmBase/V2/AmmPairV2Service'; import { $logger } from '@dequanto/utils/$logger'; import { $require } from '@dequanto/utils/$require'; import { IOracle } from './TokenOracles/IOracle'; import { ChainlinkOracle } from './TokenOracles/chainlink/ChainlinkOracle'; import { SpotPriceAggregator } from './TokenOracles/SpotPriceAggregator/SpotPriceAggregator'; interface ITokenPriceOptions { /** default: 1 */ amount?: number amountWei?: bigint date?: Date block?: number route?: string[] pairs?: any[] } interface ITokenPrice { error?: Error price?: number pools?: any[] provider?: string } export class TokenPriceService { private tokens: TokensService private oracles: { provider: string, oracle: IOracle }[]; constructor(private client: Web3Client, private explorer: IBlockchainExplorer) { this.tokens = di.resolve(TokensService, this.client.network, this.explorer) this.oracles = [ { provider: `chainlink`, oracle: new ChainlinkOracle([ client ]) }, { provider: `1inchAggregator-${client.network}`, oracle: new SpotPriceAggregator() }, // AMMs are covered already by 1inchAggregator, so no need to add them here // { provider: `ammV2`, oracle: new AmmV2PriceQuote(this.client, this.explorer) }, ]; } async getPrice (symbol: string, opts?: ITokenPriceOptions): Promise<ITokenPrice> async getPrice (address: TAddress, opts?: ITokenPriceOptions): Promise<ITokenPrice> async getPrice (token: IToken, opts?: ITokenPriceOptions): Promise<ITokenPrice> async getPrice (mix: IToken | TAddress | string, opts?: ITokenPriceOptions): Promise<ITokenPrice> { let token: IToken; try { token = typeof mix === 'string' ? await this.tokens.getKnownToken(mix) : mix; } catch (error) {} $require.notNull(token, `Token ${mix} not found`); $require.Number(token.decimals, `Token has no decimals ${token.symbol}`); let errors = []; for (let oracleData of this.oracles) { let oracle = oracleData.oracle; let { error, result } = await oracle.getPrice(token, opts); if (error) { errors.push(error); continue; } if (result.price == null || result.price === 0) { errors.push(new Error(`No price`)); continue; } return { provider: oracleData.provider, ...result }; } let message = errors.map(x => x.message).join('; '); throw new Error(message); } } export class TokenPriceServiceCacheable { private cache = new Map<string, ReturnType<TokenPriceService['getPrice']>>() private INTERVAL = 5 * 60 * 1000; constructor (private service: TokenPriceService) { } async getPrice (symbol: string, opts?: ITokenPriceOptions): TResultAsync<ISwapRouted> async getPrice (address: TAddress, opts?: ITokenPriceOptions): TResultAsync<ISwapRouted> async getPrice (token: IToken, opts?: ITokenPriceOptions): TResultAsync<ISwapRouted> async getPrice (mix: IToken | TAddress | string, opts?: ITokenPriceOptions): TResultAsync<ISwapRouted> { let key: string = typeof mix === 'string' ? mix : mix.address; let byBlock: number = null; let byDate: Date = null; if (opts?.block != null) { byBlock = opts.block; key += '_' + (byBlock - byBlock % 20) + ''; } else { let d = opts.date ?? new Date; byDate = new Date(d); byDate.setMilliseconds(0); byDate.setSeconds(0); let minutes = byDate.getMinutes(); minutes -= minutes % 5; byDate.setMinutes(minutes); key += '_' + byDate.toISOString(); } if (this.cache.has(key)) { return this.cache.get(key); } let promise = this.service.getPrice(<any> mix, { ...(opts ?? {}), date: byDate, block: byBlock }); this.cache.set(key, promise); return promise; } } namespace TokenPrice { export async function swapRouted (fromToken: IToken, fromAmount: bigint, route: ISwapPool[], tokenService: TokensService): Promise<ISwapRouted> { let $step: ISwapped; let $fromToken = fromToken; let $fromAmount = fromAmount; let $route = [] as ISwapped[]; for (let i = 0; i < route.length; i++) { $step = await calcSwap($fromToken, $fromAmount, route[i], tokenService); $fromAmount = $step.toAmount; $fromToken = $step.to; $route.push($step); } calcUsdFromRoute($route); let $stepFirst = $route[0]; //console.log('LAST STEP for ', fromToken.symbol, $step); return { outToken: $step.to, outAmount: $step.toAmount, outUsd: $step.toUsd, outUsdPrice: $step.toPrice, inToken: $stepFirst.from, inAmount: $stepFirst.fromAmount, inUsd: $stepFirst.fromUsd, inUsdPrice: $stepFirst.fromPrice, route: $route, }; } function calcUsdFromRoute (route: ISwapped[]) { let knownUsd = route.find(x => x.fromUsd != null || x.toUsd != null); if (knownUsd == null) { return; } let knownUsdI = route.indexOf(knownUsd); for (let i = knownUsdI - 1; i > -1; i--) { let knownPrice = route[i + 1]; let prev = route[i]; prev.toUsd = knownPrice.fromUsd; prev.toPrice = TokenUtils.calcPrice(prev.toAmount, prev.to, prev.toUsd); prev.fromUsd = prev.toUsd; prev.fromPrice = TokenUtils.calcPrice(prev.fromAmount, prev.from, prev.fromUsd); } for (let i = knownUsdI + 1; i < route.length; i++) { let knownPrice = route[i - 1]; let next = route[i]; next.fromUsd = knownPrice.toUsd; next.fromPrice = TokenUtils.calcPrice(next.fromAmount, next.from, next.fromUsd); next.toUsd = next.fromUsd; next.toPrice = TokenUtils.calcPrice(next.toAmount, next.to, next.toUsd); } } export async function calcPrices (swapped: ISwapped) { } export async function calcSwap (fromToken: IToken, fromAmount: bigint, lp: ISwapPool, tokenService: TokensService) { let fromTokenAddress: TAddress = lp.from.address; let toTokenAddress: TAddress = lp.to.address; if ($address.eq(fromTokenAddress, fromToken.address) === false) { throw new Error(`TP: Invalid from token addres ${fromTokenAddress} != ${fromToken.address}`); } let [ fromI, toI ] = BigInt(fromToken.address) < BigInt(toTokenAddress) ? [0, 1] : [1, 0]; let toToken = lp.to; let reserveFrom: bigint = lp.reserves[`_reserve${fromI}`]; let reserveTo: bigint = lp.reserves[`_reserve${toI}`]; let k = reserveFrom * reserveTo; let reserveFromAfter = reserveFrom + fromAmount; let reserveToAfter = k / reserveFromAfter; let amountActual = reserveTo - reserveToAfter; let fromUsd = TokenUtils.calcUsdIfStable(fromAmount, fromToken); let toUsd = TokenUtils.calcUsdIfStable(amountActual, toToken); let fromPrice = TokenUtils.calcPrice(fromAmount, fromToken, fromUsd ?? toUsd); let toPrice = TokenUtils.calcPrice(amountActual, toToken, toUsd ?? fromUsd); //console.log('FromPice', fromPrice, fromAmount, fromToken, fromUsd, toUsd); $logger.log(`Swap: ${fromToken.symbol}(${fromAmount})[${fromUsd}$] > ${toToken.symbol} (${amountActual})[${toUsd}$]; Price ${fromToken.symbol}: ${fromPrice}`); return <ISwapped> { from: lp.from, fromAmount: fromAmount, fromUsd: fromUsd ?? toUsd, fromPrice, to: lp.to, toAmount: amountActual, toUsd: toUsd ?? fromUsd, toPrice, usd: 0, date: new Date(Number(lp.reserves._blockTimestampLast * 1000)), pool: { address: lp.address, reserve0: lp.reserves._reserve0, reserve1: lp.reserves._reserve1, } }; } }