import di from 'a-di'; import alot from 'alot'; import memd from 'memd'; import { env } from 'atma-io'; import { $bigint } from '@dequanto/utils/$bigint'; import { $date } from '@dequanto/utils/$date'; import { IBlockchainExplorer } from '@dequanto/explorer/IBlockchainExplorer'; import { Web3Client } from '@dequanto/clients/Web3Client'; import { $address } from '@dequanto/utils/$address'; import { TAddress } from '@dequanto/models/TAddress'; import { TPlatform } from '@dequanto/models/TPlatform'; import { TokensService } from '@dequanto/tokens/TokensService'; import { IToken, ITokenBase } from '@dequanto/models/IToken'; import { AmmV2ExchangeBase } from '../../AmmV2ExchangeBase'; import { SushiswapPolygonExchange } from '../../SushiswapPolygonExchange'; import { PancakeswapExchange } from '../../PancakeswapExchange'; import { UniswapV2Exchange } from '../../UniswapV2Exchange'; import { $cache } from '@dequanto/utils/$cache'; export interface ISwapPoolInfo { address: TAddress token0: TAddress token1: TAddress from?: IToken to?: IToken }; export interface ISwapPool extends ISwapPoolInfo { date: Date fromPrice?: number reserves?: { _reserve0: bigint _reserve1: bigint _blockTimestampLast: number } } export interface ISwapRouted { outToken: ITokenBase outAmount: bigint outUsd: number outUsdPrice: number inToken: ITokenBase inAmount: bigint inUsd: number inUsdPrice: number route: ISwapped[] } export 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 } } const CACHE_PATH = $cache.file(`dex-pools.json`); const CACHE_SECONDS = $date.parseTimespan('7d', { get: 's' }); export class AmmPairV2Service { private exchange: AmmV2ExchangeBase private tokensService: TokensService; private targetCoins: string[] constructor(public client: Web3Client, public explorer: IBlockchainExplorer) { switch (client.platform) { case 'bsc': this.exchange = di.resolve(PancakeswapExchange, this.client, this.explorer); this.targetCoins = ['BUSD', 'USDT']; break; case 'eth': this.exchange = di.resolve(UniswapV2Exchange, this.client, this.explorer); this.targetCoins = ['USDC', 'USDT', 'DAI']; break; case 'polygon': this.exchange = di.resolve(SushiswapPolygonExchange, this.client, this.explorer); this.targetCoins = ['USDC', 'USDT', 'DAI']; break; default: throw new Error(`Unsupported Platform for exchange yet: ${client.platform}`); } this.tokensService = di.resolve(TokensService, this.client.platform, this.explorer); } @memd.deco.memoize({ perInstance: true, trackRef: true, maxAge: CACHE_SECONDS, key: (ctx, platform, address) => { let self = ctx.this as AmmPairV2Service; let key = `bestRoute_${platform}_${address}`; return key; }, persistence: new memd.FsTransport({ path: CACHE_PATH }) }) async resolveBestStableRoute (platform: TPlatform, address: TAddress): Promise { let pool = await alot(this.targetCoins) .mapAsync(async symbol => { return this.getPoolInfo(address, symbol); }) .filterAsync(x => x != null) .sortByAsync(({reserveTo}) => reserveTo, 'desc') .firstAsync(); if (pool == null || pool.reserveTo < (50_000n * BigInt(pool.pair.to.decimals))) { // if NO or low-liquidity pool found, check the WETH pool const SYMBOL = { bsc: 'WBNB', polygon: 'MATIC' }[platform] ?? 'WETH'; const nativeTokenPool = await this.getPoolInfo(address, SYMBOL); if (nativeTokenPool == null || nativeTokenPool.reserveTo < 10) { return null; } let nativeRoute = await this.resolveBestStableRoute(platform, nativeTokenPool.pair.to.address); if (nativeRoute == null) { return null; } return [ nativeTokenPool.pair, ...nativeRoute ]; } return [ pool.pair ]; } async resolveRoute (address: TAddress, symbols: string[]): Promise { let tokens = await alot(symbols) .mapAsync(x => this.tokensService.getTokenOrDefault(x)) .toArrayAsync(); let prev = address; let out = [] as ISwapPoolInfo[]; for (let token of tokens) { let poolInfo = await this.getPoolInfo(prev, token.address); out.push(poolInfo.pair); prev = token.address; } return out; } @memd.deco.memoize({ perInstance: true, trackRef: true, key: (ctx, fromAddress, toSymbol) => { let self = ctx.this as AmmPairV2Service; let key = `pool_${self.client.platform}_${fromAddress}_${toSymbol}`; return key; }, persistence: new memd.FsTransport({ path: CACHE_PATH }) }) private async getPoolInfo (fromAddress: TAddress, symbol: string): Promise<{ pair: ISwapPoolInfo, reserveTo: number }> { let toToken = await this.tokensService.getTokenOrDefault(symbol); if (toToken == null) { return null; } let lpAddress = await this.exchange.factoryContract.getPair(fromAddress, toToken.address); if ($address.isEmpty(lpAddress)) { return null; } let poolPair = this.exchange.pairContract(lpAddress); let lpReserves = await poolPair.getReserves(); if (lpReserves == null || lpReserves._reserve0 < 1000n) { return null; } let [ fromI, toI ] = BigInt(fromAddress) < BigInt(toToken.address) ? [0, 1] : [1, 0]; let reserveFrom: bigint = lpReserves[`_reserve${fromI}`]; let reserveTo: bigint = lpReserves[`_reserve${toI}`]; if (toToken.decimals == null) { toToken.decimals = 18; } if (reserveTo < 10n ** BigInt(toToken.decimals ?? 18)) { return null; } let fromToken = await this.tokensService.getTokenOrDefault(fromAddress); return { pair: PairUtil.createPairInfo(fromToken, toToken, lpAddress), reserveTo: $bigint.toEther(reserveTo, toToken.decimals), }; } } namespace PairUtil { export function createPairInfo (fromToken: IToken, toToken: IToken, lpAddress: TAddress): ISwapPoolInfo { let token0 = fromToken.address; let token1 = toToken.address; if (BigInt(token1) < BigInt(token0)) { token0 = toToken.address; token1 = fromToken.address; } return { address: lpAddress, token0, token1, from: fromToken, to: toToken, }; } }