UNPKG

0xweb

Version:

Contract package manager and other web3 tools

467 lines (379 loc) 15.9 kB
import di from 'a-di'; import alot from 'alot'; import { IBlockchainExplorer } from '@dequanto/explorer/IBlockchainExplorer'; import { Web3Client } from '@dequanto/clients/Web3Client'; import { IToken } from '@dequanto/models/IToken'; import { AmmV2ExchangeBase } from './AmmV2ExchangeBase'; import { PancakeswapExchange } from './PancakeswapExchange'; import { UniswapV2Exchange } from './UniswapV2Exchange'; import { TAddress } from '@dequanto/models/TAddress'; import { TokenUtils } from '../utils/TokenUtils'; import { TokensService } from '../TokensService'; import { $address } from '@dequanto/utils/$address'; import { LoggerService } from '@dequanto/loggers/LoggerService'; import { TResult, TResultAsync } from '@dequanto/models/TResult'; import { TokenPriceStore } from '../TokenOracles/TokenPriceStore'; import { $bigint } from '@dequanto/utils/$bigint'; import { AmmPairV2Service, ISwapPool, ISwapPoolInfo } from './AmmBase/V2/AmmPairV2Service'; import { SushiswapPolygonExchange } from './SushiswapPolygonExchange'; import { IOracle, IOracleOptions, IOracleResult, ISwapOptions } from '../TokenOracles/IOracle'; import { ILogger } from '@dequanto/loggers/ILogger'; export class AmmV2PriceQuote implements IOracle { private exchange: AmmV2ExchangeBase private tokensService: TokensService private pairService: AmmPairV2Service private logger: ILogger constructor(public client: Web3Client, public explorer: IBlockchainExplorer) { switch (client.platform) { case 'bsc': this.exchange = di.resolve(PancakeswapExchange, this.client, this.explorer); break; case 'eth': this.exchange = di.resolve(UniswapV2Exchange, this.client, this.explorer); break; case 'polygon': this.exchange = di.resolve(SushiswapPolygonExchange, this.client, this.explorer); break; default: throw new Error(`Unsupported Platform for exchange yet: ${client.platform}`); } this.tokensService = di.resolve(TokensService, this.client.platform, this.explorer) this.pairService = di.resolve(AmmPairV2Service, this.client, this.explorer) this.logger = di.resolve(LoggerService, 'AmmPriceV2Oracle'); } async getPrice (token: IToken, opts?: IOracleOptions): TResultAsync<IOracleResult> { let { error, result } = await this.getRoute(token, opts); if (error != null) { return { error }; } let priceResult = { price: result.outUsd, date: new Date(), pools: result.route.map(route => { let sorted = BigInt(route.from.address) < BigInt(route.to.address); let t1 = { price: sorted ? route.fromPrice : route.toPrice, decimals: sorted ? route.from.decimals : route.to.decimals, total: sorted ? route.pool.reserve0 : route.pool.reserve1, }; let t2 = { price: sorted ? route.toPrice : route.fromPrice, decimals: sorted ? route.to.decimals : route.from.decimals, total: sorted ? route.pool.reserve1 : route.pool.reserve0, }; function getTotalToken(t: { price: number, total: bigint, decimals: number }): bigint { let amount = t.total / 10n** BigInt(t.decimals); return $bigint.multWithFloat(amount, t.price); } return getTotalToken(t1) + getTotalToken(t2); }) }; return { result: priceResult }; } async getRoute (token: IToken, opts?: ISwapOptions): TResultAsync<ISwapRouted> { let amount = opts?.amountWei ?? (BigInt(opts?.amount ?? 1) * 10n ** BigInt(token.decimals)); if (TokenUtils.isStable(token.symbol)) { // Assume swap 1:1 let usd = $bigint.divToFloat(amount, 10n**BigInt(token.decimals)); return { result: { outToken: token, outAmount: amount, outUsd: usd, outUsdPrice: 1, inToken: token, inAmount: amount, inUsd: usd, inUsdPrice: 1, route: [], } }; } let cashableDate = opts?.date ?? (opts?.block == null && new Date() || null); if (cashableDate != null) { let swap = await this.getSwapFromCache(token, amount, cashableDate); if (swap != null) { return swap; } } let pairs: ISwapPoolInfo[]; if (opts?.pairs) { pairs = opts.pairs.map(pair => { let tokens = [pair.from.address, pair.to.address ]; let sorted = BigInt(tokens[0]) < BigInt(tokens[1]); return <ISwapPoolInfo> { address: pair.address, token0: sorted ? tokens[0] : tokens[1], token1: sorted ? tokens[1] : tokens[0], from: pair.from, to: pair.to }; }) } let route: ISwapPoolInfo[] = pairs ?? (opts?.route != null ? await this.pairService.resolveRoute(token.address, opts.route) : await this.pairService.resolveBestStableRoute(this.client.platform, token.address) ); if (route == null || route.length === 0) { let error = new Error(`Route not found for Token ${token.address}`); return { error }; } let pools = await alot(route).mapAsync<TResult<ISwapPool>>(async lp => { if (cashableDate != null) { let price = await this.getPriceInUsdFromCache(lp.from.address, cashableDate); if (price != null) { return { result: <ISwapPool> { ...lp, date: cashableDate, priceFrom: price } }; } } let poolPair = this.exchange.pairContract(lp.address); let lpReserves = await poolPair .forBlock(opts?.block ?? opts?.date) .getReserves(); if (lpReserves == null || lpReserves._reserve0 < 1000n) { let error = new Error(`Small reserve in the routed pool ${lp.address}: ${lpReserves._reserve1} - ${lpReserves._reserve0}`); this.logger.log(error.message); return { error }; } return { result: <ISwapPool> { ...lp, date: cashableDate, reserves: lpReserves } }; }).toArrayAsync({ errors: 'reject' }); let error = alot(pools).first(x => x.error != null)?.error; if (error != null) { return { error }; } let swapped = await TokenPrice.swapRouted( token, amount, pools.map(x => x.result), this.tokensService ); return { result: swapped }; } private async getSwapFromCache (token: IToken, inAmount: bigint, date: Date): TResultAsync<ISwapRouted> { if (date == null) { return null; } let inPrice = await this.getPriceInUsdFromCache(token.address, date); if (inPrice == null) { return null; } let usdcToken = await this.tokensService.getToken('USDC'); let outAmount = inAmount * ($bigint.toBigInt(inPrice * 10**6) * $bigint.pow(10, usdcToken.decimals - 6)) / ($bigint.pow(10, token.decimals)); let outUsd = $bigint.divToFloat(outAmount, 10n**BigInt(token.decimals)); return { result: { outToken: usdcToken, outAmount: outAmount, outUsd: outUsd, outUsdPrice: 1, inToken: token, inAmount: inAmount, inUsd: outUsd, inUsdPrice: inPrice, route: [], } } } private async getPriceInUsdFromCache (token: TAddress, date: Date): Promise<number> { if (date == null) { return null; } return TokenPriceStore.forToken(token).getPrice(date.getTime()); } private async setPriceInUsdToCache (token: TAddress, date: Date, price: number): Promise<void> { if (date == null) { return null; } return TokenPriceStore.forToken(token).setPrice(price, date.getTime()); } } export class TokenRangePriceService { private cache = new Map<string, TResultAsync<ISwapRouted>>() private INTERVAL = 5 * 60 * 1000; constructor (private service: AmmV2PriceQuote) { } async getRoute (symbol: string, opts?: ISwapOptions): TResultAsync<ISwapRouted> async getRoute (address: TAddress, opts?: ISwapOptions): TResultAsync<ISwapRouted> async getRoute (token: IToken, opts?: ISwapOptions): TResultAsync<ISwapRouted> async getRoute (mix: IToken | TAddress | string, opts?: ISwapOptions): 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.getRoute(<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(`Invalid from token address ${fromTokenAddress} != ${fromToken.address}`); } let $fromPrice = lp.fromPrice; if ($fromPrice != null) { let $fromUsd = TokenUtils.calcTotal(fromToken, fromAmount, $fromPrice); return <ISwapped> { from: fromToken, fromAmount: fromAmount, fromUsd: $fromUsd, fromPrice: $fromPrice, // Optimistic assume same USD out. toUsd: $fromUsd }; } 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}`); if (lp.date) { // Cache prices let fromStore = TokenPriceStore.forToken(fromToken.address); fromStore.setPrice(fromPrice, lp.date.getTime()); let toStore = TokenPriceStore.forToken(toToken.address); toStore.setPrice(toPrice, lp.date.getTime()); } 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, } }; } } interface ISwapRouted { outToken: IToken outAmount: bigint outUsd: number outUsdPrice: number inToken: IToken inAmount: bigint inUsd: number inUsdPrice: number route: ISwapped[] } interface ISwapped { from: IToken fromAmount: bigint fromUsd?: number fromPrice?: number to: IToken toAmount: bigint toUsd?: number toPrice?: number usd: number date: Date pool: { address: TAddress reserve0: bigint reserve1: bigint } }