import Web3 from 'web3'; import { BigNumber } from '@ethersproject/bignumber'; import { ChainId, Token, Trade, TokenAmount, Pair } from '@uniswap/sdk' import { getAddress } from '@ethersproject/address'; import { Contract } from '@ethersproject/contracts'; import { DODO_SDK_DATA } from './data/constants'; import UNI_PAIR_ABI from './abi/external/uniswapPairV2.json'; import * as MULTI_ABI from './abi/external/mutlicall.json'; export class UniRoute { public WEB3_URL: string constructor(_WEB3_URL?: string) { this.WEB3_URL = _WEB3_URL || DODO_SDK_DATA.WEB3_URL } public async getSwapCallDetails( fromToken: { decimals: number; address: string }, toToken: { decimals: number; address: string }, maxHops: number, swapAmountIn: string ): Promise<{ "path": string[], "outAmount": string, "priceImpact": number, "fromToken": { decimals: number; address: string }, "toToken": { decimals: number; address: string } }> { const _fromToken = new Token(ChainId.MAINNET, getAddress(fromToken.address), fromToken.decimals) const _toToken = new Token(ChainId.MAINNET, getAddress(toToken.address), toToken.decimals) let pairs: Pair[] = await this.getAllPairs(_fromToken, _toToken); let trade: Trade if (pairs.length > 0) { trade = Trade.bestTradeExactIn(pairs, new TokenAmount(_fromToken, swapAmountIn), _toToken, { maxHops: maxHops, maxNumResults: 1 })[0] ?? null } if (trade == null) return null let path = trade.route.path.map(a => a.address); // const slippageTolerance = new Percent('10000', '10000') // let data = Router.swapCallParameters(trade, { allowedSlippage: slippageTolerance, deadline: deadline, recipient: to }) // let contract = new Contract(DODO_SDK_DATA.UNI_ROUTER_V2, UNI_ROUTER_ABI, new JsonRpcProvider(DODO_SDK_DATA.WEB3_URL)) // let dataAbi = contract.interface.encodeFunctionData(data.methodName, data.args) return { "path": path, "outAmount": trade.outputAmount.raw.toString(), "priceImpact": parseFloat(trade.priceImpact.toFixed()), "fromToken": fromToken, "toToken": toToken } } private async getAllPairs(tokenA?: Token, tokenB?: Token): Promise { const bases: Token[] = BASES_TO_CHECK_TRADES_AGAINST const basePairs: [Token, Token][] = [] for (let i = 0; i < bases.length; i++) { for (let j = 0; j < bases.length; j++) { if (bases[i].address !== bases[j].address) basePairs.push([bases[i], bases[j]]) } } const allPairCombinations: [Token, Token][] = tokenA && tokenB ? [ [tokenA, tokenB], ...bases.map((base): [Token, Token] => [tokenA, base]), ...bases.map((base): [Token, Token] => [tokenB, base]), ...basePairs ] .filter((tokens): tokens is [Token, Token] => Boolean(tokens[0] && tokens[1])) .filter(([t0, t1]) => t0.address !== t1.address) .filter(([tokenA, tokenB]) => { const customBases = CUSTOM_BASES if (!customBases) return true const customBasesA: Token[] | undefined = customBases[tokenA.address] const customBasesB: Token[] | undefined = customBases[tokenB.address] if (!customBasesA && !customBasesB) return true if (customBasesA && !customBasesA.find(base => tokenB.equals(base))) return false if (customBasesB && !customBasesB.find(base => tokenA.equals(base))) return false return true }) : [] const allPairs = await usePairs(allPairCombinations) return Object.values( allPairs .filter((result): result is [PairState.EXISTS, Pair] => Boolean(result[0] === PairState.EXISTS && result[1])) .reduce<{ [pairAddress: string]: Pair }>((memo, [, curr]) => { memo[curr.liquidityToken.address] = memo[curr.liquidityToken.address] ?? curr return memo }, {}) ) } } //Uni Helper export const AMPL = new Token(ChainId.MAINNET, '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 9, 'AMPL', 'Ampleforth') export const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin') export const WETH = new Token(ChainId.MAINNET, '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 18, 'WETH', 'Wrapper Ether') export const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C') export const USDT = new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD') export const COMP = new Token(ChainId.MAINNET, '0xc00e94Cb662C3520282E6f5717214004A7f26888', 18, 'COMP', 'Compound') export const MKR = new Token(ChainId.MAINNET, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 18, 'MKR', 'Maker') /** * Some tokens can only be swapped via certain pairs, so we override the list of bases that are considered for these * tokens. */ export const CUSTOM_BASES = { [AMPL.address]: [DAI, WETH] } export const BASES_TO_CHECK_TRADES_AGAINST = [WETH, DAI, USDC, USDT, COMP, MKR] export enum PairState { LOADING, NOT_EXISTS, EXISTS, INVALID } export async function usePairs(tokens: [Token | undefined, Token | undefined][]): Promise<[PairState, Pair | null][]> { let calls = [] const web3 = new Web3(new Web3.providers.HttpProvider(DODO_SDK_DATA.WEB3_URL)); const multiInstance = new web3.eth.Contract(MULTI_ABI['default'], DODO_SDK_DATA.MULTI_CALL); tokens.map(([tokenA, tokenB]) => { let addr = tokenA && tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined if (addr != undefined) { calls.push({ "target": addr, "callData": "0x0902f1ac" //getReserves() }) } }) const res = await multiInstance.methods.aggregate(calls).call(); const results = res[1] return results.map((result, i) => { const tokenA = tokens[i][0] const tokenB = tokens[i][1] if (!tokenA || !tokenB || tokenA.equals(tokenB)) return [PairState.INVALID, null] if (result == '0x') return [PairState.NOT_EXISTS, null] const reserve0 = BigNumber.from(results[i].substring(0, 66)).toString() const reserve1 = BigNumber.from('0x' + results[i].substring(66, 130)).toString() const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA] return [ PairState.EXISTS, new Pair(new TokenAmount(token0, reserve0), new TokenAmount(token1, reserve1)) ] }) } export async function fetchPairReserves( address: string, provider ): Promise { try { const [reserve0, reserve1] = await new Contract(address, UNI_PAIR_ABI, provider).getReserves() return { result: { reserve0, reserve1 }, loading: false } } catch (error) { return { result: null, loading: false } } }