@xspswap/smart-order-router
Version:
XSwap Protocol V3 Smart Order Router
161 lines • 15.1 kB
JavaScript
import { BigNumber } from '@ethersproject/bignumber';
import { WETH9 } from '@x-swap-protocol/sdk-core';
import _ from 'lodash';
import { log } from '../../../../util';
import { CurrencyAmount } from '../../../../util/amounts';
import { IV2GasModelFactory, usdGasTokensByChain, } from '../gas-model';
// Constant cost for doing any swap regardless of pools.
export const BASE_SWAP_COST = BigNumber.from(100000); // Reduced for low gas networks
// Constant per extra hop in the route.
export const COST_PER_EXTRA_HOP = BigNumber.from(35000); // Reduced for low gas networks
/**
* 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
*/
export class V2HeuristicGasModelFactory extends IV2GasModelFactory {
constructor() {
super();
}
async buildGasModel({ chainId, gasPriceWei, poolProvider, token, }) {
if (token.equals(WETH9[chainId])) {
const usdPool = await this.getHighestLiquidityUSDPool(chainId, poolProvider);
return {
estimateGasCost: (routeWithValidQuote) => {
const { gasCostInEth, gasUse } = this.estimateGas(routeWithValidQuote, gasPriceWei, chainId);
const ethToken0 = usdPool.token0.address == WETH9[chainId].address;
const ethTokenPrice = ethToken0
? usdPool.token0Price
: usdPool.token1Price;
const gasCostInTermsOfUSD = ethTokenPrice.quote(gasCostInEth);
return {
gasEstimate: gasUse,
gasCostInToken: gasCostInEth,
gasCostInUSD: gasCostInTermsOfUSD,
};
},
};
}
// 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.
const ethPool = await this.getEthPool(chainId, token, poolProvider);
if (!ethPool) {
log.info('Unable to find ETH pool with the quote token to produce gas adjusted costs. Route will not account for gas.');
}
const usdPool = await this.getHighestLiquidityUSDPool(chainId, poolProvider);
return {
estimateGasCost: (routeWithValidQuote) => {
const usdToken = usdPool.token0.address == WETH9[chainId].address
? usdPool.token1
: usdPool.token0;
const { gasCostInEth, gasUse } = this.estimateGas(routeWithValidQuote, gasPriceWei, chainId);
if (!ethPool) {
return {
gasEstimate: gasUse,
gasCostInToken: CurrencyAmount.fromRawAmount(token, 0),
gasCostInUSD: CurrencyAmount.fromRawAmount(usdToken, 0),
};
}
const ethToken0 = ethPool.token0.address == WETH9[chainId].address;
const ethTokenPrice = ethToken0
? ethPool.token0Price
: ethPool.token1Price;
let gasCostInTermsOfQuoteToken;
try {
gasCostInTermsOfQuoteToken = ethTokenPrice.quote(gasCostInEth);
}
catch (err) {
log.error({
ethTokenPriceBase: ethTokenPrice.baseCurrency,
ethTokenPriceQuote: ethTokenPrice.quoteCurrency,
gasCostInEth: gasCostInEth.currency,
}, 'Debug eth price token issue');
throw err;
}
const ethToken0USDPool = usdPool.token0.address == WETH9[chainId].address;
const ethTokenPriceUSDPool = ethToken0USDPool
? usdPool.token0Price
: usdPool.token1Price;
let gasCostInTermsOfUSD;
try {
gasCostInTermsOfUSD = ethTokenPriceUSDPool.quote(gasCostInEth);
}
catch (err) {
log.error({
usdT1: usdPool.token0.symbol,
usdT2: usdPool.token1.symbol,
gasCostInEthToken: gasCostInEth.currency.symbol,
}, 'Failed to compute USD gas price');
throw err;
}
return {
gasEstimate: gasUse,
gasCostInToken: gasCostInTermsOfQuoteToken,
gasCostInUSD: gasCostInTermsOfUSD,
};
},
};
}
estimateGas(routeWithValidQuote, gasPriceWei, chainId) {
const hops = routeWithValidQuote.route.pairs.length;
const gasUse = BASE_SWAP_COST.add(COST_PER_EXTRA_HOP.mul(hops - 1));
const totalGasCostWei = gasPriceWei.mul(gasUse);
const weth = WETH9[chainId];
const gasCostInEth = CurrencyAmount.fromRawAmount(weth, totalGasCostWei.toString());
return { gasCostInEth, gasUse };
}
async getEthPool(chainId, token, poolProvider) {
const weth = WETH9[chainId];
const poolAccessor = await poolProvider.getPools([[weth, token]]);
const pool = poolAccessor.getPool(weth, token);
if (!pool || pool.reserve0.equalTo(0) || pool.reserve1.equalTo(0)) {
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) {
const usdTokens = usdGasTokensByChain[chainId];
if (!usdTokens) {
throw new Error(`Could not find a USD token for computing gas costs on ${chainId}`);
}
const usdPools = _.map(usdTokens, (usdToken) => [
usdToken,
WETH9[chainId],
]);
const poolAccessor = await poolProvider.getPools(usdPools);
const poolsRaw = poolAccessor.getAllPools();
const pools = _.filter(poolsRaw, (pool) => pool.reserve0.greaterThan(0) && pool.reserve1.greaterThan(0));
if (pools.length == 0) {
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 = _.maxBy(pools, (pool) => {
if (pool.token0.equals(WETH9[chainId])) {
return parseFloat(pool.reserve0.toSignificant(2));
}
else {
return parseFloat(pool.reserve1.toSignificant(2));
}
});
return maxPool;
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidjItaGV1cmlzdGljLWdhcy1tb2RlbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uL3NyYy9yb3V0ZXJzL2FscGhhLXJvdXRlci9nYXMtbW9kZWxzL3YyL3YyLWhldXJpc3RpYy1nYXMtbW9kZWwudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLDBCQUEwQixDQUFDO0FBQ3JELE9BQU8sRUFBa0IsS0FBSyxFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFFbEUsT0FBTyxDQUFDLE1BQU0sUUFBUSxDQUFDO0FBR3ZCLE9BQU8sRUFBRSxHQUFHLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUN2QyxPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFFMUQsT0FBTyxFQUdMLGtCQUFrQixFQUNsQixtQkFBbUIsR0FDcEIsTUFBTSxjQUFjLENBQUM7QUFFdEIsd0RBQXdEO0FBQ3hELE1BQU0sQ0FBQyxNQUFNLGNBQWMsR0FBRyxTQUFTLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsK0JBQStCO0FBRXJGLHVDQUF1QztBQUN2QyxNQUFNLENBQUMsTUFBTSxrQkFBa0IsR0FBRyxTQUFTLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsK0JBQStCO0FBRXhGOzs7Ozs7Ozs7Ozs7Ozs7O0dBZ0JHO0FBQ0gsTUFBTSxPQUFPLDBCQUEyQixTQUFRLGtCQUFrQjtJQUNoRTtRQUNFLEtBQUssRUFBRSxDQUFDO0lBQ1YsQ0FBQztJQUVNLEtBQUssQ0FBQyxhQUFhLENBQUMsRUFDekIsT0FBTyxFQUNQLFdBQVcsRUFDWCxZQUFZLEVBQ1osS0FBSyxHQUNzQjtRQUMzQixJQUFJLEtBQUssQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBRSxDQUFDLEVBQUU7WUFDakMsTUFBTSxPQUFPLEdBQVMsTUFBTSxJQUFJLENBQUMsMEJBQTBC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