UNPKG

@xspswap/smart-order-router

Version:
223 lines 21.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.V3HeuristicGasModelFactory = void 0; const bignumber_1 = require("@ethersproject/bignumber"); const sdk_core_1 = require("@x-swap-protocol/sdk-core"); const lodash_1 = __importDefault(require("lodash")); const __1 = require("../../../.."); const amounts_1 = require("../../../../util/amounts"); const gas_factory_helpers_1 = require("../../../../util/gas-factory-helpers"); const log_1 = require("../../../../util/log"); const gas_model_1 = require("../gas-model"); const gas_costs_1 = require("./gas-costs"); /** * Computes a gas estimate for a V3 swap using heuristics. * Considers number of hops in the route, number of ticks crossed * and the typical base cost for a swap. * * We get the number of ticks crossed in a swap from the QuoterV2 * contract. * * 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. * * @export * @class V3HeuristicGasModelFactory */ class V3HeuristicGasModelFactory extends gas_model_1.IOnChainGasModelFactory { constructor() { super(); } async buildGasModel({ chainId, gasPriceWei, v3poolProvider: poolProvider, amountToken, quoteToken, }) { const usdPool = await (0, gas_factory_helpers_1.getHighestLiquidityV3USDPool)(chainId, poolProvider); const calculateL1GasFees = async () => { let l1Used = bignumber_1.BigNumber.from(0); let l1FeeInWei = bignumber_1.BigNumber.from(0); // wrap fee to native currency const nativeCurrency = __1.WRAPPED_NATIVE_CURRENCY[chainId]; const costNativeCurrency = amounts_1.CurrencyAmount.fromRawAmount(nativeCurrency, l1FeeInWei.toString()); // convert fee into usd const nativeTokenPrice = usdPool.token0.address == nativeCurrency.address ? usdPool.token0Price : usdPool.token1Price; const gasCostL1USD = nativeTokenPrice.quote(costNativeCurrency); let gasCostL1QuoteToken = costNativeCurrency; // if the inputted token is not in the native currency, quote a native/quote token pool to get the gas cost in terms of the quote token if (!quoteToken.equals(nativeCurrency)) { const nativePool = await (0, gas_factory_helpers_1.getHighestLiquidityV3NativePool)(quoteToken, poolProvider); if (!nativePool) { log_1.log.info('Could not find a pool to convert the cost into the quote token'); gasCostL1QuoteToken = amounts_1.CurrencyAmount.fromRawAmount(quoteToken, 0); } else { const nativeTokenPrice = nativePool.token0.address == nativeCurrency.address ? nativePool.token0Price : nativePool.token1Price; gasCostL1QuoteToken = nativeTokenPrice.quote(costNativeCurrency); } } // gasUsedL1 is the gas units used calculated from the bytes of the calldata // gasCostL1USD and gasCostL1QuoteToken is the cost of gas in each of those tokens return { gasUsedL1: l1Used, gasCostL1USD, gasCostL1QuoteToken, }; }; // If our quote token is WETH, we don't need to convert our gas use to be in terms // of the quote token in order to produce a gas adjusted amount. // We do return a gas use in USD however, so we still convert to usd. const nativeCurrency = __1.WRAPPED_NATIVE_CURRENCY[chainId]; if (quoteToken.equals(nativeCurrency)) { const estimateGasCost = (routeWithValidQuote) => { const { totalGasCostNativeCurrency, baseGasUse } = this.estimateGas(routeWithValidQuote, gasPriceWei, chainId); const token0 = usdPool.token0.address == nativeCurrency.address; const nativeTokenPrice = token0 ? usdPool.token0Price : usdPool.token1Price; const gasCostInTermsOfUSD = nativeTokenPrice.quote(totalGasCostNativeCurrency); return { gasEstimate: baseGasUse, gasCostInToken: totalGasCostNativeCurrency, gasCostInUSD: gasCostInTermsOfUSD, }; }; return { estimateGasCost, calculateL1GasFees, }; } // If the quote token is not in the native currency, we convert the gas cost to be in terms of the quote token. // We do this by getting the highest liquidity <quoteToken>/<nativeCurrency> pool. eg. <quoteToken>/ETH pool. const nativePool = await (0, gas_factory_helpers_1.getHighestLiquidityV3NativePool)(quoteToken, poolProvider); let nativeAmountPool = null; if (!amountToken.equals(nativeCurrency)) { nativeAmountPool = await (0, gas_factory_helpers_1.getHighestLiquidityV3NativePool)(amountToken, poolProvider); } const usdToken = usdPool.token0.address == nativeCurrency.address ? usdPool.token1 : usdPool.token0; const estimateGasCost = (routeWithValidQuote) => { const { totalGasCostNativeCurrency, baseGasUse } = this.estimateGas(routeWithValidQuote, gasPriceWei, chainId); let gasCostInTermsOfQuoteToken = null; if (nativePool) { const token0 = nativePool.token0.address == nativeCurrency.address; // returns mid price in terms of the native currency (the ratio of quoteToken/nativeToken) const nativeTokenPrice = token0 ? nativePool.token0Price : nativePool.token1Price; try { // native token is base currency gasCostInTermsOfQuoteToken = nativeTokenPrice.quote(totalGasCostNativeCurrency); } catch (err) { log_1.log.info({ nativeTokenPriceBase: nativeTokenPrice.baseCurrency, nativeTokenPriceQuote: nativeTokenPrice.quoteCurrency, gasCostInEth: totalGasCostNativeCurrency.currency, }, 'Debug eth price token issue'); throw err; } } // we have a nativeAmountPool, but not a nativePool else { log_1.log.info(`Unable to find ${nativeCurrency.symbol} pool with the quote token, ${quoteToken.symbol} to produce gas adjusted costs. Using amountToken to calculate gas costs.`); } // Highest liquidity pool for the non quote token / ETH // A pool with the non quote token / ETH should not be required and errors should be handled separately if (nativeAmountPool) { // get current execution price (amountToken / quoteToken) const executionPrice = new sdk_core_1.Price(routeWithValidQuote.amount.currency, routeWithValidQuote.quote.currency, routeWithValidQuote.amount.quotient, routeWithValidQuote.quote.quotient); const inputIsToken0 = nativeAmountPool.token0.address == nativeCurrency.address; // ratio of input / native const nativeAmountTokenPrice = inputIsToken0 ? nativeAmountPool.token0Price : nativeAmountPool.token1Price; const gasCostInTermsOfAmountToken = nativeAmountTokenPrice.quote(totalGasCostNativeCurrency); // Convert gasCostInTermsOfAmountToken to quote token using execution price const syntheticGasCostInTermsOfQuoteToken = executionPrice.quote(gasCostInTermsOfAmountToken); // Note that the syntheticGasCost being lessThan the original quoted value is not always strictly better // e.g. the scenario where the amountToken/ETH pool is very illiquid as well and returns an extremely small number // however, it is better to have the gasEstimation be almost 0 than almost infinity, as the user will still receive a quote if (gasCostInTermsOfQuoteToken === null || syntheticGasCostInTermsOfQuoteToken.lessThan(gasCostInTermsOfQuoteToken.asFraction)) { log_1.log.info({ nativeAmountTokenPrice: nativeAmountTokenPrice.toSignificant(6), gasCostInTermsOfQuoteToken: gasCostInTermsOfQuoteToken ? gasCostInTermsOfQuoteToken.toExact() : 0, gasCostInTermsOfAmountToken: gasCostInTermsOfAmountToken.toExact(), executionPrice: executionPrice.toSignificant(6), syntheticGasCostInTermsOfQuoteToken: syntheticGasCostInTermsOfQuoteToken.toSignificant(6), }, 'New gasCostInTermsOfQuoteToken calculated with synthetic quote token price is less than original'); gasCostInTermsOfQuoteToken = syntheticGasCostInTermsOfQuoteToken; } } // true if token0 is the native currency const token0USDPool = usdPool.token0.address == nativeCurrency.address; // gets the mid price of the pool in terms of the native token const nativeTokenPriceUSDPool = token0USDPool ? usdPool.token0Price : usdPool.token1Price; let gasCostInTermsOfUSD; try { gasCostInTermsOfUSD = nativeTokenPriceUSDPool.quote(totalGasCostNativeCurrency); } catch (err) { log_1.log.info({ usdT1: usdPool.token0.symbol, usdT2: usdPool.token1.symbol, gasCostInNativeToken: totalGasCostNativeCurrency.currency.symbol, }, 'Failed to compute USD gas price'); throw err; } // If gasCostInTermsOfQuoteToken is null, both attempts to calculate gasCostInTermsOfQuoteToken failed (nativePool and amountNativePool) if (gasCostInTermsOfQuoteToken === null) { log_1.log.info(`Unable to find ${nativeCurrency.symbol} pool with the quote token, ${quoteToken.symbol}, or amount Token, ${amountToken.symbol} to produce gas adjusted costs. Route will not account for gas.`); return { gasEstimate: baseGasUse, gasCostInToken: amounts_1.CurrencyAmount.fromRawAmount(quoteToken, 0), gasCostInUSD: amounts_1.CurrencyAmount.fromRawAmount(usdToken, 0), }; } return { gasEstimate: baseGasUse, gasCostInToken: gasCostInTermsOfQuoteToken, gasCostInUSD: gasCostInTermsOfUSD, }; }; return { estimateGasCost: estimateGasCost.bind(this), calculateL1GasFees, }; } estimateGas(routeWithValidQuote, gasPriceWei, chainId) { const totalInitializedTicksCrossed = bignumber_1.BigNumber.from(Math.max(1, lodash_1.default.sum(routeWithValidQuote.initializedTicksCrossedList))); const totalHops = bignumber_1.BigNumber.from(routeWithValidQuote.route.pools.length); const hopsGasUse = (0, gas_costs_1.COST_PER_HOP)(chainId).mul(totalHops); const tickGasUse = (0, gas_costs_1.COST_PER_INIT_TICK)(chainId).mul(totalInitializedTicksCrossed); const uninitializedTickGasUse = gas_costs_1.COST_PER_UNINIT_TICK.mul(0); // base estimate gas used based on chainId estimates for hops and ticks gas useage const baseGasUse = (0, gas_costs_1.BASE_SWAP_COST)(chainId) .add(hopsGasUse) .add(tickGasUse) .add(uninitializedTickGasUse); const baseGasCostWei = gasPriceWei.mul(baseGasUse); const wrappedCurrency = __1.WRAPPED_NATIVE_CURRENCY[chainId]; const totalGasCostNativeCurrency = amounts_1.CurrencyAmount.fromRawAmount(wrappedCurrency, baseGasCostWei.toString()); return { totalGasCostNativeCurrency, totalInitializedTicksCrossed, baseGasUse, }; } } exports.V3HeuristicGasModelFactory = V3HeuristicGasModelFactory; //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidjMtaGV1cmlzdGljLWdhcy1tb2RlbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uL3NyYy9yb3V0ZXJzL2FscGhhLXJvdXRlci9nYXMtbW9kZWxzL3YzL3YzLWhldXJpc3RpYy1nYXMtbW9kZWwudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBQUEsd0RBQXFEO0FBQ3JELHdEQUFrRDtBQUVsRCxvREFBdUI7QUFFdkIsbUNBQXNEO0FBRXRELHNEQUEwRDtBQUMxRCw4RUFHOEM7QUFDOUMsOENBQTJDO0FBRTNDLDRDQUlzQjtBQUV0QiwyQ0FLcUI7QUFFckI7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBaUJHO0FBQ0gsTUFBYSwwQkFBMkIsU0FBUSxtQ0FBdUI7SUFDckU7UUFDRSxLQUFLLEVBQUUsQ0FBQztJQUNWLENBQUM7SUFFTSxLQUFLLENBQUMsYUFBYSxDQUFDLEVBQ3pCLE9BQU8sRUFDUCxXQUFXLEVBQ1gsY0FBYyxFQUFFLFlBQVksRUFDNUIsV0FBVyxFQUNYLFVBQVUsR0FFbUI7UUFDN0IsTUFBTSxPQUFPLEdBQVMsTUFBTSxJQUFBLGtEQUE0QixFQUN0RCxPQUFPLEVBQ1AsWUFBWSxDQUNiLENBQUM7UUFFRixNQUFNLGtCQUFrQixHQUFHLEtBQUssSUFJN0IsRUFBRTtZQUNILElBQUksTUFBTSxHQUFHLHFCQUFTLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO1lBQy9CLElBQUksVUFBVSxHQUFHLHFCQUFTLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO1lBRW5DLDhCQUE4QjtZQUM5QixNQUFNLGNBQWMsR0FBRywyQkFBdUIsQ0FBQyxPQUFPLENBQUMsQ0FBQztZQUN4RCxNQUFNLGtCQUFrQixHQUFHLHdCQUFjLENBQUMsYUFBYSxDQUNyRCxjQUFjLEVBQ2QsVUFBVSxDQUFDLFFBQVEsRUFBRSxDQUN0QixDQUFDO1lBRUYsdUJBQXVCO1lBQ3ZCLE1BQU0sZ0JBQWdCLEdBQ3BCLE9BQU8s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