0xweb
Version:
Contract package manager and other web3 tools
229 lines (194 loc) • 7.31 kB
text/typescript
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);
}
.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<ISwapPoolInfo[]> {
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<ISwapPoolInfo[]> {
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;
}
.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,
};
}
}