diff --git a/api/_utils.ts b/api/_utils.ts index ed04a80ca..9584bcdce 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -1980,13 +1980,11 @@ export function getCachedNativeGasCost( const ttlPerChain = { default: 60, }; - const cacheKey = buildInternalCacheKey( "nativeGasCost", deposit.destinationChainId, deposit.outputToken ); - const ttl = ttlPerChain.default; const fetchFn = async () => { const relayerAddress = overrides?.relayerAddress ?? @@ -2007,9 +2005,14 @@ export function getCachedNativeGasCost( return voidSigner.estimateGas(unsignedFillTxn); }; - return getCachedValue(cacheKey, ttl, fetchFn, (nativeGasCostFromCache) => { - return BigNumber.from(nativeGasCostFromCache); - }); + return makeCacheGetterAndSetter( + cacheKey, + ttlPerChain.default, + fetchFn, + (nativeGasCostFromCache) => { + return BigNumber.from(nativeGasCostFromCache); + } + ); } export function getCachedOpStackL1DataFee( @@ -2032,7 +2035,6 @@ export function getCachedOpStackL1DataFee( deposit.outputToken // This should technically differ based on the output token since the L2 calldata // size affects the L1 data fee and this calldata can differ based on the output token. ); - const ttl = ttlPerChain.default; const fetchFn = async () => { // We don't care about the gas token price or the token gas price, only the raw gas units. In the API // we'll compute the gas price separately. @@ -2058,9 +2060,14 @@ export function getCachedOpStackL1DataFee( return opStackL1GasCost; }; - return getCachedValue(cacheKey, ttl, fetchFn, (l1DataFeeFromCache) => { - return BigNumber.from(l1DataFeeFromCache); - }); + return makeCacheGetterAndSetter( + cacheKey, + ttlPerChain.default, + fetchFn, + (l1DataFeeFromCache) => { + return BigNumber.from(l1DataFeeFromCache); + } + ); } export function latestGasPriceCache( @@ -2071,7 +2078,8 @@ export function latestGasPriceCache( }> ) { const ttlPerChain = { - default: 5, + default: 10, + [CHAIN_IDs.MAINNET]: 24, }; return makeCacheGetterAndSetter( // If deposit is defined, then the gas price will be dependent on the fill transaction derived from the deposit. diff --git a/api/cron-cache-gas-prices.ts b/api/cron-cache-gas-prices.ts index 759cf3321..54d5b6185 100644 --- a/api/cron-cache-gas-prices.ts +++ b/api/cron-cache-gas-prices.ts @@ -2,23 +2,54 @@ import { VercelResponse } from "@vercel/node"; import { TypedVercelRequest } from "./_types"; import { HUB_POOL_CHAIN_ID, + getCachedNativeGasCost, + getCachedOpStackL1DataFee, getLogger, handleErrorCondition, latestGasPriceCache, + resolveVercelEndpoint, } from "./_utils"; import { UnauthorizedError } from "./_errors"; import mainnetChains from "../src/data/chains_1.json"; -import { utils } from "@across-protocol/sdk"; -import { CHAIN_IDs } from "./_constants"; +import { utils, constants } from "@across-protocol/sdk"; +import { CHAIN_IDs, DEFAULT_SIMULATED_RECIPIENT_ADDRESS } from "./_constants"; +import axios from "axios"; +import { ethers } from "ethers"; +type Route = { + originChainId: number; + originToken: string; + destinationChainId: number; + destinationToken: string; + originTokenSymbol: string; + destinationTokenSymbol: string; +}; + +// Set slightly lower than TTL in latestGasPriceCache const updateIntervalsSecPerChain = { - default: 10, + default: 5, 1: 12, }; +// Set slightly lower than TTL in getCachedOpStackL1DataFee. +const updateL1DataFeeIntervalsSecPerChain = { + default: 10, +}; + const maxDurationSec = 60; +const getDepositArgsForChainId = (chainId: number, tokenAddress: string) => { + return { + amount: ethers.BigNumber.from(100), + inputToken: constants.ZERO_ADDRESS, + outputToken: tokenAddress, + recipientAddress: DEFAULT_SIMULATED_RECIPIENT_ADDRESS, + originChainId: 0, // Shouldn't matter for simulation + destinationChainId: Number(chainId), + }; +}; + const handler = async ( request: TypedVercelRequest>, response: VercelResponse @@ -46,36 +77,106 @@ const handler = async ( return; } + const availableRoutes = ( + await axios(`${resolveVercelEndpoint()}/api/available-routes`) + ).data as Array; + // This marks the timestamp when the function started const functionStart = Date.now(); + /** + * @notice Updates the gas price cache every `updateIntervalsSecPerChain` seconds up to `maxDurationSec` seconds. + * @param chainId Chain to estimate gas price for + * @param outputTokenAddress Optional param to set if the gas price is dependent on the calldata of the transaction + * to be submitted on the chainId. This output token will be used to construct a fill transaction to simulate. + */ + const updateGasPricePromise = async ( + chainId: number, + outputTokenAddress?: string + ) => { + const secondsPerUpdateForChain = + updateIntervalsSecPerChain[ + chainId as keyof typeof updateIntervalsSecPerChain + ] || updateIntervalsSecPerChain.default; + const cache = latestGasPriceCache( + chainId, + outputTokenAddress + ? getDepositArgsForChainId(chainId, outputTokenAddress) + : undefined + ); + + while (true) { + const diff = Date.now() - functionStart; + // Stop after `maxDurationSec` seconds + if (diff >= maxDurationSec * 1000) { + break; + } + await cache.set(); + await utils.delay(secondsPerUpdateForChain); + } + }; + + /** + * @notice Updates the L1 data fee and L2 gas cost caches every `updateL1DataFeeIntervalsSecPerChain` seconds + * up to `maxDurationSec` seconds. + * @dev This function will also update the L2 gas costs because this value is required to get the L1 data fee. + * @param chainId Chain to estimate gas price for + * @param outputTokenAddress This output token will be used to construct a fill transaction to simulate + * gas costs for. + */ + const updateL1DataFeePromise = async ( + chainId: number, + outputTokenAddress: string + ) => { + const secondsPerUpdateForChain = + updateL1DataFeeIntervalsSecPerChain.default; + const depositArgs = getDepositArgsForChainId(chainId, outputTokenAddress); + const gasCostCache = getCachedNativeGasCost(depositArgs); + + while (true) { + const diff = Date.now() - functionStart; + // Stop after `maxDurationSec` seconds + if (diff >= maxDurationSec * 1000) { + break; + } + const gasCost = await gasCostCache.get(); + const cache = getCachedOpStackL1DataFee(depositArgs, gasCost); + await cache.set(); + await utils.delay(secondsPerUpdateForChain); + } + }; + // The minimum interval for Vercel Serverless Functions cron jobs is 1 minute. - // But we want to update gas prices more frequently than that. + // But we want to update gas data more frequently than that. // To circumvent this, we run the function in a loop and update gas prices every // `secondsPerUpdateForChain` seconds and stop after `maxDurationSec` seconds (1 minute). - const gasPricePromises = mainnetChains - // @dev Remove Linea from this cron cache job because Linea's gas price is dependent on the - // calldata of the transaction to be submitted on Linea. - .filter((chain) => CHAIN_IDs.LINEA !== chain.chainId) - .map(async (chain) => { - const secondsPerUpdateForChain = - updateIntervalsSecPerChain[ - chain.chainId as keyof typeof updateIntervalsSecPerChain - ] || updateIntervalsSecPerChain.default; - // The deposit args don't matter for any chain besides Linea, which is why we filter it out - // above, because gas price on Linea is dependent on the fill transaction args. - const cache = latestGasPriceCache(chain.chainId); - - while (true) { - const diff = Date.now() - functionStart; - // Stop after `maxDurationSec` seconds - if (diff >= maxDurationSec * 1000) { - break; - } - await cache.set(); - await utils.delay(secondsPerUpdateForChain); - } - }); + const gasPricePromises = mainnetChains.map(async (chain) => { + // For each chain: + // - update the destination gas price for the chain + // For each output token on that chain: + // - update the simulated gas costs for the token + const routesToChain = availableRoutes.filter( + ({ destinationChainId }) => destinationChainId === chain.chainId + ); + const outputTokensForChain = routesToChain.map( + ({ destinationToken }) => destinationToken + ); + Promise.all( + outputTokensForChain.map((outputToken) => + updateL1DataFeePromise(chain.chainId, outputToken) + ) + ); + // @dev Linea gas prices are dependent on the L2 calldata to be submitted so compute one gas price for each output token + if (chain.chainId === CHAIN_IDs.LINEA) { + Promise.all( + outputTokensForChain.map((outputToken) => + updateGasPricePromise(chain.chainId, outputToken) + ) + ); + } else { + updateGasPricePromise(chain.chainId); + } + }); await Promise.all(gasPricePromises); logger.debug({ diff --git a/api/gas-prices.ts b/api/gas-prices.ts index 5cfe7f345..83ac3dc5c 100644 --- a/api/gas-prices.ts +++ b/api/gas-prices.ts @@ -63,7 +63,7 @@ const handler = async ( tokenAddress ); return Promise.all([ - getCachedNativeGasCost(depositArgs), + getCachedNativeGasCost(depositArgs).get(), latestGasPriceCache( Number(chainId), CHAIN_IDs.LINEA === Number(chainId) ? depositArgs : undefined @@ -82,7 +82,7 @@ const handler = async ( ); const [nativeGasCost, gasPrice] = gasData[i]; const opStackL1GasCost = sdk.utils.chainIsOPStack(Number(chainId)) - ? await getCachedOpStackL1DataFee(depositArgs, nativeGasCost) + ? await getCachedOpStackL1DataFee(depositArgs, nativeGasCost).get() : undefined; const tokenGasCost = nativeGasCost .mul(gasPrice.maxFeePerGas) diff --git a/api/limits.ts b/api/limits.ts index d4dbcc764..fc9b8e318 100644 --- a/api/limits.ts +++ b/api/limits.ts @@ -189,7 +189,9 @@ const handler = async ( ).get(), isMessageDefined ? undefined // Only use cached gas units if message is not defined, i.e. standard for standard bridges - : getCachedNativeGasCost(depositArgs, { relayerAddress: relayer }), + : getCachedNativeGasCost(depositArgs, { + relayerAddress: relayer, + }).get(), ]); const tokenPriceUsd = ethers.utils.parseUnits(_tokenPriceUsd.toString()); @@ -204,7 +206,7 @@ const handler = async ( ? // Only use cached gas units if message is not defined, i.e. standard for standard bridges getCachedOpStackL1DataFee(depositArgs, nativeGasCost, { relayerAddress: relayer, - }) + }).get() : undefined, callViaMulticall3(provider, multiCalls, { blockTag: latestBlock.number,