UNPKG

@sky-mavis/smart-order-router

Version:
155 lines 16.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.V2HeuristicGasModelFactory = exports.COST_PER_EXTRA_HOP = exports.BASE_SWAP_COST = void 0; const bignumber_1 = require("@ethersproject/bignumber"); const lodash_1 = __importDefault(require("lodash")); const util_1 = require("../../../../util"); const amounts_1 = require("../../../../util/amounts"); const gas_factory_helpers_1 = require("../../../../util/gas-factory-helpers"); const gas_model_1 = require("../gas-model"); // Constant cost for doing any swap regardless of pools. exports.BASE_SWAP_COST = bignumber_1.BigNumber.from(135000); // 115000, bumped up by 20_000 @eric 7/8/2022 // Constant per extra hop in the route. exports.COST_PER_EXTRA_HOP = bignumber_1.BigNumber.from(50000); // 20000, bumped up by 30_000 @eric 7/8/2022 /** * Computes a gas estimate for a V2 swap using heuristics. * Considers number of hops in the route and the typical base cost for a swap. * * We compute gas estimates off-chain because * 1/ Calling eth_estimateGas for a swaps requires the caller to have * the full balance token being swapped, and approvals. * 2/ Tracking gas used using a wrapper contract is not accurate with Multicall * due to EIP-2929. We would have to make a request for every swap we wanted to estimate. * 3/ For V2 we simulate all our swaps off-chain so have no way to track gas used. * * Note, certain tokens e.g. rebasing/fee-on-transfer, may incur higher gas costs than * what we estimate here. This is because they run extra logic on token transfer. * * @export * @class V2HeuristicGasModelFactory */ class V2HeuristicGasModelFactory extends gas_model_1.IV2GasModelFactory { async buildGasModel({ chainId, gasPriceWei, poolProvider, token, providerConfig, }) { const usdPoolPromise = this.getHighestLiquidityUSDPool(chainId, poolProvider, providerConfig); // Only fetch the native gasToken pool if specified by the config AND the gas token is not the native currency. const nativeAndSpecifiedGasTokenPoolPromise = (providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.gasToken) && !(providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.gasToken.equals(util_1.WRAPPED_NATIVE_CURRENCY[chainId])) ? this.getEthPool(chainId, providerConfig.gasToken, poolProvider, providerConfig) : Promise.resolve(null); const [usdPool, nativeAndSpecifiedGasTokenPool] = await Promise.all([ usdPoolPromise, nativeAndSpecifiedGasTokenPoolPromise, ]); let ethPool = null; if (!token.equals(util_1.WRAPPED_NATIVE_CURRENCY[chainId])) { ethPool = await this.getEthPool(chainId, token, poolProvider, providerConfig); } const usdToken = usdPool.token0.address == util_1.WRAPPED_NATIVE_CURRENCY[chainId].address ? usdPool.token1 : usdPool.token0; const calculateL1GasFees = async () => { const nativePool = !token.equals(util_1.WRAPPED_NATIVE_CURRENCY[chainId]) ? await (0, gas_factory_helpers_1.getV2NativePool)(token, poolProvider, providerConfig) : null; return await (0, gas_factory_helpers_1.calculateL1GasFeesHelper)(chainId, usdPool, token, nativePool); }; return { estimateGasCost: (routeWithValidQuote) => { var _a; const { gasCostInEth, gasUse } = this.estimateGas(routeWithValidQuote, gasPriceWei, chainId, providerConfig); /** ------ MARK: USD logic -------- */ const gasCostInTermsOfUSD = (0, gas_model_1.getQuoteThroughNativePool)(chainId, gasCostInEth, usdPool); /** ------ MARK: Conditional logic run if gasToken is specified -------- */ let gasCostInTermsOfGasToken = undefined; if (nativeAndSpecifiedGasTokenPool) { gasCostInTermsOfGasToken = (0, gas_model_1.getQuoteThroughNativePool)(chainId, gasCostInEth, nativeAndSpecifiedGasTokenPool); } // if the gasToken is the native currency, we can just use the gasCostInEth else if ((_a = providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.gasToken) === null || _a === void 0 ? void 0 : _a.equals(util_1.WRAPPED_NATIVE_CURRENCY[chainId])) { gasCostInTermsOfGasToken = gasCostInEth; } /** ------ MARK: return early if quoteToken is wrapped native currency ------- */ if (token.equals(util_1.WRAPPED_NATIVE_CURRENCY[chainId])) { return { gasEstimate: gasUse, gasCostInToken: gasCostInEth, gasCostInUSD: gasCostInTermsOfUSD, gasCostInGasToken: gasCostInTermsOfGasToken, }; } // If the quote token is not WETH, we convert the gas cost to be in terms of the quote token. // We do this by getting the highest liquidity <token>/ETH pool. if (!ethPool) { util_1.log.info('Unable to find ETH pool with the quote token to produce gas adjusted costs. Route will not account for gas.'); return { gasEstimate: gasUse, gasCostInToken: amounts_1.CurrencyAmount.fromRawAmount(token, 0), gasCostInUSD: amounts_1.CurrencyAmount.fromRawAmount(usdToken, 0), }; } const gasCostInTermsOfQuoteToken = (0, gas_model_1.getQuoteThroughNativePool)(chainId, gasCostInEth, ethPool); return { gasEstimate: gasUse, gasCostInToken: gasCostInTermsOfQuoteToken, gasCostInUSD: gasCostInTermsOfUSD, gasCostInGasToken: gasCostInTermsOfGasToken, }; }, calculateL1GasFees, }; } estimateGas(routeWithValidQuote, gasPriceWei, chainId, providerConfig) { const hops = routeWithValidQuote.route.pairs.length; let gasUse = exports.BASE_SWAP_COST.add(exports.COST_PER_EXTRA_HOP.mul(hops - 1)); if (providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.additionalGasOverhead) { gasUse = gasUse.add(providerConfig.additionalGasOverhead); } const totalGasCostWei = gasPriceWei.mul(gasUse); const weth = util_1.WRAPPED_NATIVE_CURRENCY[chainId]; const gasCostInEth = amounts_1.CurrencyAmount.fromRawAmount(weth, totalGasCostWei.toString()); return { gasCostInEth, gasUse }; } async getEthPool(chainId, token, poolProvider, providerConfig) { const weth = util_1.WRAPPED_NATIVE_CURRENCY[chainId]; const poolAccessor = await poolProvider.getPools([[weth, token]], providerConfig); const pool = poolAccessor.getPool(weth, token); if (!pool || pool.reserve0.equalTo(0) || pool.reserve1.equalTo(0)) { util_1.log.error({ weth, token, reserve0: pool === null || pool === void 0 ? void 0 : pool.reserve0.toExact(), reserve1: pool === null || pool === void 0 ? void 0 : pool.reserve1.toExact(), }, `Could not find a valid WETH pool with ${token.symbol} for computing gas costs.`); return null; } return pool; } async getHighestLiquidityUSDPool(chainId, poolProvider, providerConfig) { const usdTokens = gas_model_1.USD_GAS_TOKEN_BY_CHAIN[chainId]; if (!usdTokens) { throw new Error(`Could not find a USD token for computing gas costs on ${chainId}`); } const usdPools = lodash_1.default.map(usdTokens, usdToken => [usdToken, util_1.WRAPPED_NATIVE_CURRENCY[chainId]]); const poolAccessor = await poolProvider.getPools(usdPools, providerConfig); const poolsRaw = poolAccessor.getAllPools(); const pools = lodash_1.default.filter(poolsRaw, pool => pool.reserve0.greaterThan(0) && pool.reserve1.greaterThan(0) && // this case should never happen in production, but when we mock the pool provider it may return non native pairs (pool.token0.equals(util_1.WRAPPED_NATIVE_CURRENCY[chainId]) || pool.token1.equals(util_1.WRAPPED_NATIVE_CURRENCY[chainId]))); if (pools.length == 0) { util_1.log.error({ pools }, `Could not find a USD/WETH pool for computing gas costs.`); throw new Error(`Can't find USD/WETH pool for computing gas costs.`); } const maxPool = lodash_1.default.maxBy(pools, pool => { if (pool.token0.equals(util_1.WRAPPED_NATIVE_CURRENCY[chainId])) { return parseFloat(pool.reserve0.toSignificant(2)); } else { return parseFloat(pool.reserve1.toSignificant(2)); } }); return maxPool; } } exports.V2HeuristicGasModelFactory = V2HeuristicGasModelFactory; //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidjItaGV1cmlzdGljLWdhcy1tb2RlbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uL3NyYy9yb3V0ZXJzL2FscGhhLXJvdXRlci9nYXMtbW9kZWxzL3YyL3YyLWhldXJpc3RpYy1nYXMtbW9kZWwudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBQ0Esd0RBQXFEO0FBR3JELG9EQUF1QjtBQUl2QiwyQ0FBZ0U7QUFDaEUsc0RBQTBEO0FBQzFELDhFQUFpRztBQUVqRyw0Q0FPc0I7QUFFdEIsd0RBQXdEO0FBQzNDLFFBQUEsY0FBYyxHQUFHLHFCQUFTLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsNkNBQTZDO0FBRW5HLHVDQUF1QztBQUMxQixRQUFBLGtCQUFrQixHQUFHLHFCQUFTLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsNENBQTRDO0FBRXJHOzs7Ozs7Ozs7Ozs7Ozs7O0dBZ0JHO0FBQ0gsTUFBYSwwQkFBMkIsU0FBUSw4QkFBa0I7SUFDekQsS0FBSyxDQUFDLGFBQWEsQ0FBQyxFQUN6QixPQUFPLEVBQ1AsV0FBVyxFQUNYLFlBQVksRUFDWixLQUFLLEVBQ0wsY0FBYyxHQUNhO1FBQzNCLE1BQU0sY0FBYyxHQUFrQixJQUFJLENBQUMsMEJBQTBCLENBQUMsT0FBTyxFQUFFLFlBQVksRUFBRSxjQUFjLENBQUMsQ0FBQztRQUU3RywrR0FBK0c7UUFDL0csTUFBTSxxQ0FBcUMsR0FDekMsQ0FBQSxjQUFjLGFBQWQsY0FBYyx1QkFBZCxjQUFjLENBQUUsUUFBUSxLQUFJLENBQUMsQ0FBQSxjQUFjLGFBQWQsY0FBYyx1QkFBZCxjQUFjLENBQUUsUUFBUSxDQUFDLE1BQU0sQ0FBQyw4QkFBdUIsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFBO1lBQzVGLENBQUMsQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLE9BQU8sRUFBRSxjQUFjLENBQUMsUUFBUSxFQUFFLFlBQVksRUFB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