import Web3 from 'web3'; import BigNumber from 'bignumber.js'; import { formatFixed } from '@ethersproject/bignumber'; import { tokenAddrContains, validateAndParseAddress } from './utils' import { Pair } from './pair' import { DODO_SDK_DATA } from './data/constants'; import { GetQuoteTokenList } from './tokenType'; import * as DODO_PROXY_ABI from './abi/dodoProxy.json'; import * as DODO_SWAP_CALC_HELPER from './abi/dodoSwapCalcHelper.json'; export class DODORoute { public ETH_ADDRESS: string = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' public WETH_TOKEN: { symbol: string; address: string; decimals: number, name: string, source: string } public NETWORK_ID: number public WEB3_URL: string public DODO_PROXY: string public DODO_APPROVE: string public DODO_SWAP_CALC_HELPER: string public constructor( _NETWORK_ID: number, _WEB3_URL?: string, _DODO_PROXY?: string, _DODO_APPROVE?: string ) { this.NETWORK_ID = _NETWORK_ID switch (_NETWORK_ID) { case 1: this.WEB3_URL = _WEB3_URL || DODO_SDK_DATA.WEB3_URL this.DODO_SWAP_CALC_HELPER = DODO_SDK_DATA.DODO_SWAP_CALC_HELPER this.DODO_APPROVE = _DODO_APPROVE || DODO_SDK_DATA.DODO_APPROVE this.DODO_PROXY = _DODO_PROXY || DODO_SDK_DATA.DODO_PROXY this.WETH_TOKEN = DODO_SDK_DATA.WETH break; case 42: this.WEB3_URL = _WEB3_URL || DODO_SDK_DATA.WEB3_URL_KOVAN this.DODO_SWAP_CALC_HELPER = DODO_SDK_DATA.DODO_SWAP_CALC_HELPER_KOVAN this.DODO_APPROVE = _DODO_APPROVE || DODO_SDK_DATA.DODO_APPROVE_KOVAN this.DODO_PROXY = _DODO_PROXY || DODO_SDK_DATA.DODO_PROXY_KOVAN this.WETH_TOKEN = DODO_SDK_DATA.WETH_KOVAN break; case 56: this.WEB3_URL = _WEB3_URL || DODO_SDK_DATA.WEB3_URL_BSC this.DODO_SWAP_CALC_HELPER = DODO_SDK_DATA.DODO_SWAP_CALC_HELPER_BSC this.DODO_APPROVE = _DODO_APPROVE || DODO_SDK_DATA.DODO_APPROVE_BSC this.DODO_PROXY = _DODO_PROXY || DODO_SDK_DATA.DODO_PROXY_BSC this.WETH_TOKEN = DODO_SDK_DATA.WETH_BSC break; } } //TODO: 询价与构造交易合并 public async GetDODORoute( fromToken: { decimals: number; symbol: string; address: string; source: string; name?: string; }, toToken: { symbol: string; address: string; name: string; decimals: number; source: string }, fromAmount: string, routeDetail: { resAmount: number, resPricePerToToken: number, resPricePerFromToken: number, priceImpact: number, useSource: string, targetDecimals: number, targetApproveAddr: string } ) { if (fromToken.address != this.ETH_ADDRESS) { routeDetail.targetApproveAddr = this.DODO_APPROVE; } if (fromToken.source != 'dodo' || toToken.source != 'dodo') { return routeDetail } if (fromToken.address == this.ETH_ADDRESS) fromToken = this.WETH_TOKEN if (toToken.address == this.ETH_ADDRESS) toToken = this.WETH_TOKEN let _priceImpact: number = 0; let _resAmount: string; [_resAmount, , , _priceImpact, routeDetail.resPricePerToToken, routeDetail.resPricePerFromToken,] = await this.GetDODOSwapCallParams(fromToken, toToken, fromAmount, true) routeDetail.priceImpact = _priceImpact < 0 ? 0 : _priceImpact routeDetail.resAmount = parseFloat(formatFixed(_resAmount, toToken.decimals)) return routeDetail; } public async MakeDODORoute( fromToken: { decimals: number; symbol: string; address: string; source: string; name?: string; }, toToken: { symbol: string; address: string; name: string; decimals: number; source: string }, fromAmount: string, minAmount: string, slippage: number = 0, deadline: number = 0, web3Provider: any = null ): Promise<[string, number]> { const web3 = new Web3(web3Provider || new Web3.providers.HttpProvider(this.WEB3_URL)); const dodoSwapInstance = new web3.eth.Contract(DODO_PROXY_ABI['default'], this.DODO_PROXY) let isFromETH = false; let isToETH = false; if (fromToken.address == this.ETH_ADDRESS) { isFromETH = true fromToken = this.WETH_TOKEN } if (toToken.address == this.ETH_ADDRESS) { isToETH = true; toToken = this.WETH_TOKEN } const [returnAmount, dodoPairs, directions, ,] = await this.GetDODOSwapCallParams(fromToken, toToken, fromAmount, false) if (returnAmount == '0' || dodoPairs.length == 0) return ['', 0]; if (new BigNumber(returnAmount).lt(new BigNumber(minAmount))) { return ["", 0] } let minReturnAmount = minAmount let originFromTokenAddr = isFromETH ? this.ETH_ADDRESS : fromToken.address let originToTokenAddr = isToETH ? this.ETH_ADDRESS : toToken.address let strDirections: string = '' for (let i = directions.length - 1; i >= 0; i--) { strDirections += directions[i].toString() } let data = await dodoSwapInstance.methods.dodoSwapV1( originFromTokenAddr, originToTokenAddr, fromAmount, minReturnAmount, dodoPairs, parseInt(strDirections, 2), deadline ).encodeABI() return [data, 0]; } public async GetDODOSwapCallParams( fromToken: { decimals: number; symbol: string; address: string; source: string; name?: string; }, toToken: { symbol: string; address: string; name: string; decimals: number; source: string }, fromAmount: string, ifQuote: boolean, web3Provider: any = null ): Promise<[string, string[], number[], number, number, number, number, { decimals: number; symbol: string; address: string; source: string; name?: string; }?, { decimals: number; symbol: string; address: string; source: string; name?: string; }?]> { const web3 = new Web3(web3Provider || new Web3.providers.HttpProvider(this.WEB3_URL)); const [pairs, selectedRoute] = await this.CalcDODORoute(fromToken, toToken, fromAmount) let directions: number[] = [] let dodoPairs: string[] = [] let dodoSwapCalcHelperInstance = new web3.eth.Contract(DODO_SWAP_CALC_HELPER['default'], this.DODO_SWAP_CALC_HELPER) let originPrice: number = 1 let priceImpact: number = 0 let resPricePerToToken: number = 0 for (let i = 0; i < pairs.length; i++) { let curPair = pairs[i] dodoPairs.push(curPair.pair) if (validateAndParseAddress(curPair.base) === validateAndParseAddress(selectedRoute[i].address)) { directions[i] = 0; } else { directions[i] = 1; } } try { var [returmAmount, midPrices, feeRates] = await dodoSwapCalcHelperInstance.methods.calcReturnAmountV1( fromAmount, dodoPairs, directions ).call(); if (ifQuote) { for (let i = 0; i < pairs.length; i++) { let curDecimals: number = 18 if (directions[i] == 0) { if (selectedRoute[i + 1].decimals != selectedRoute[i].decimals) curDecimals = selectedRoute[i + 1].decimals + (18 - selectedRoute[i].decimals) originPrice *= parseFloat(formatFixed(midPrices[i], curDecimals)) } else { if (selectedRoute[i + 1].decimals != selectedRoute[i].decimals) curDecimals = selectedRoute[i].decimals + (18 - selectedRoute[i + 1].decimals) originPrice *= (1 / parseFloat(formatFixed(midPrices[i], curDecimals))) } originPrice *= (1 - parseFloat(formatFixed(feeRates[i], "18"))) } let resAmount = parseFloat(formatFixed(returmAmount, toToken.decimals)) resPricePerToToken = parseFloat(formatFixed(fromAmount, fromToken.decimals)) / resAmount; priceImpact = 1 - 1 / new BigNumber(originPrice).multipliedBy(resPricePerToToken).toNumber() } } catch (error) { console.log("dodo route calc error:", error) return ["0", [], [], 0, 0, 0, 0, fromToken, toToken] } return [returmAmount, dodoPairs, directions, priceImpact, resPricePerToToken, 1 / resPricePerToToken, originPrice, fromToken, toToken] } //TODO: 当前返回格式不支持单跳拆单 private async CalcDODORoute( fromToken: { symbol: string; address: string; decimals: number }, toToken: { symbol: string; address: string; name: string; decimals: number; source: string; }, fromAmount: string, web3Provider: any = null ): Promise<[{ pair: string, base: string }[], { symbol: string, address: string, name: string, decimals: number, source: string }[]]> { const web3 = new Web3(web3Provider || new Web3.providers.HttpProvider(this.WEB3_URL)); /* @Notice V1.5 * 1. base <=> quote * 2. base <=> quote1 <=> quote2 * 3. base1 <=> quote <=> base2 * 4. base1 <=> quote1 <=> quote2 <=> base2 */ let quoteTokenList = GetQuoteTokenList(this.NETWORK_ID) let dodoRoute = [] if (tokenAddrContains(quoteTokenList, toToken)) { dodoRoute.push([fromToken, toToken]) quoteTokenList.map(cur => { if (cur.address != toToken.address) dodoRoute.push([fromToken, cur, toToken]) }) } else if (tokenAddrContains(quoteTokenList, fromToken)) { dodoRoute.push([fromToken, toToken]) quoteTokenList.map(cur => { if (cur.address != fromToken.address) dodoRoute.push([fromToken, cur, toToken]) }) } else { quoteTokenList.map(cur => { dodoRoute.push([fromToken, cur, toToken]) }) for (let i = 0; i < quoteTokenList.length; i++) { for (let j = 0; j < quoteTokenList.length; j++) { if (i != j) dodoRoute.push([fromToken, quoteTokenList[i], quoteTokenList[j], toToken]) } } } let pairs: { pair: string, base: string }[] = [] let selectedRoute: { symbol: string; address: string; name: string; decimals: number; source: string; }[] = [] let pairObj: Pair = new Pair(this.NETWORK_ID, this.WEB3_URL) for (let i = 0; i < dodoRoute.length; i++) { let curRoute: { address: string }[] = dodoRoute[i] for (let j = 0; j < curRoute.length - 1; j++) { let pairDetail: { pair: string, base: string } = await pairObj.getAddress(validateAndParseAddress(curRoute[j].address), validateAndParseAddress(curRoute[j + 1].address), web3.currentProvider) if (pairDetail.pair === '' || pairDetail.pair === '0x1A7fE5D6f0BB2D071E16BDD52C863233BBFd38e9') { pairs = [] break } pairs.push(pairDetail) } if (pairs.length > 0) { selectedRoute = dodoRoute[i] break } } return [pairs, selectedRoute] } }