diff --git a/api/cron-cache-gas-costs.ts b/api/cron-cache-gas-costs.ts new file mode 100644 index 000000000..54491ebf1 --- /dev/null +++ b/api/cron-cache-gas-costs.ts @@ -0,0 +1,209 @@ +import { VercelResponse } from "@vercel/node"; +import { TypedVercelRequest } from "./_types"; +import { + HUB_POOL_CHAIN_ID, + getCachedNativeGasCost, + getCachedOpStackL1DataFee, + getLogger, + handleErrorCondition, + resolveVercelEndpoint, +} from "./_utils"; +import { UnauthorizedError } from "./_errors"; + +import mainnetChains from "../src/data/chains_1.json"; +import { utils, constants } from "@across-protocol/sdk"; +import { 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 lower than TTL in getCachedOpStackL1DataFee +// Set lower than the L1 block time so we can try to get as up to date L1 data fees based on L1 base fees as possible. +const updateL1DataFeeIntervalsSecPerChain = { + default: 10, +}; + +// Set lower than TTL in getCachedNativeGasCost. This should rarely change so we should just make sure +// we keep this cache warm. +const updateNativeGasCostIntervalsSecPerChain = { + default: 30, +}; + +// Force the cache update promises to stop 1s before the Vercel serverless function times out. +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 +) => { + const logger = getLogger(); + logger.debug({ + at: "CronCacheGasPrices", + message: "Starting cron job...", + }); + try { + const authHeader = request.headers?.["authorization"]; + if ( + !process.env.CRON_SECRET || + authHeader !== `Bearer ${process.env.CRON_SECRET}` + ) { + throw new UnauthorizedError(); + } + + // Skip cron job on testnet + if (HUB_POOL_CHAIN_ID !== 1) { + logger.info({ + at: "CronCacheGasPrices", + message: "Skipping cron job on testnet", + }); + 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 L1 data fee gas cost cache every `updateL1DataFeeIntervalsSecPerChain` seconds + * up to `maxDurationSec` seconds. + * @param chainId Chain to estimate l1 data fee 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 + ): Promise => { + const secondsPerUpdate = 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(); + if (utils.chainIsOPStack(chainId)) { + const cache = getCachedOpStackL1DataFee(depositArgs, gasCost); + try { + await cache.set(); + } catch (err) { + logger.warn({ + at: "CronCacheGasPrices#updateL1DataFeePromise", + message: `Failed to set l1 data fee cache for chain ${chainId}`, + depositArgs, + gasCost, + error: err, + }); + } + } + await utils.delay(secondsPerUpdate); + } + }; + + /** + * @notice Updates the native gas cost cache every `updateNativeGasCostIntervalsSecPerChain` seconds + * up to `maxDurationSec` seconds. + * @param chainId Chain to estimate gas cost for + * @param outputTokenAddress This output token will be used to construct a fill transaction to simulate + * gas costs for. + */ + const updateNativeGasCostPromise = async ( + chainId: number, + outputTokenAddress: string + ): Promise => { + const secondsPerUpdate = updateNativeGasCostIntervalsSecPerChain.default; + const depositArgs = getDepositArgsForChainId(chainId, outputTokenAddress); + const cache = getCachedNativeGasCost(depositArgs); + + while (true) { + const diff = Date.now() - functionStart; + // Stop after `maxDurationSec` seconds + if (diff >= maxDurationSec * 1000) { + break; + } + try { + await cache.set(); + } catch (err) { + logger.warn({ + at: "CronCacheGasPrices#updateNativeGasCostPromise", + message: `Failed to set native gas cost cache for chain ${chainId}`, + depositArgs, + error: err, + }); + } + await utils.delay(secondsPerUpdate); + } + }; + + // The minimum interval for Vercel Serverless Functions cron jobs is 1 minute. + // 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 cacheUpdatePromises = Promise.all([ + Promise.all( + mainnetChains.map(async (chain) => { + const routesToChain = availableRoutes.filter( + ({ destinationChainId }) => destinationChainId === chain.chainId + ); + const outputTokensForChain = routesToChain.map( + ({ destinationToken }) => destinationToken + ); + await Promise.all([ + Promise.all( + outputTokensForChain.map((outputToken) => + updateNativeGasCostPromise(chain.chainId, outputToken) + ) + ), + Promise.all( + outputTokensForChain.map((outputToken) => + updateL1DataFeePromise(chain.chainId, outputToken) + ) + ), + ]); + }) + ), + ]); + await Promise.race([cacheUpdatePromises, utils.delay(maxDurationSec)]); + + logger.debug({ + at: "CronCacheGasPrices", + message: "Finished", + }); + response.status(200); + response.send("OK"); + } catch (error: unknown) { + return handleErrorCondition( + "cron-cache-gas-prices", + response, + logger, + error + ); + } +}; + +export default handler; diff --git a/api/cron-cache-gas-prices.ts b/api/cron-cache-gas-prices.ts index 6e5f5cb92..b74a475ce 100644 --- a/api/cron-cache-gas-prices.ts +++ b/api/cron-cache-gas-prices.ts @@ -2,8 +2,6 @@ import { VercelResponse } from "@vercel/node"; import { TypedVercelRequest } from "./_types"; import { HUB_POOL_CHAIN_ID, - getCachedNativeGasCost, - getCachedOpStackL1DataFee, getLogger, handleErrorCondition, latestGasPriceCache, @@ -31,18 +29,7 @@ const updateIntervalsSecPerChain = { default: 5, }; -// Set lower than TTL in getCachedOpStackL1DataFee -// Set lower than the L1 block time so we can try to get as up to date L1 data fees based on L1 base fees as possible. -const updateL1DataFeeIntervalsSecPerChain = { - default: 10, -}; - -// Set lower than TTL in getCachedNativeGasCost. This should rarely change so we should just make sure -// we keep this cache warm. -const updateNativeGasCostIntervalsSecPerChain = { - default: 30, -}; - +// Force the cache update promises to stop 1s before the Vercel serverless function times out. const maxDurationSec = 60; const getDepositArgsForChainId = (chainId: number, tokenAddress: string) => { @@ -101,38 +88,10 @@ const handler = async ( outputTokenAddress?: string ): Promise => { const secondsPerUpdateForChain = 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 gas cost cache every `updateL1DataFeeIntervalsSecPerChain` seconds - * up to `maxDurationSec` seconds. - * @param chainId Chain to estimate l1 data fee 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 - ): Promise => { - const secondsPerUpdate = updateL1DataFeeIntervalsSecPerChain.default; - const depositArgs = getDepositArgsForChainId(chainId, outputTokenAddress); - const gasCostCache = getCachedNativeGasCost(depositArgs); + const depositArgs = outputTokenAddress + ? getDepositArgsForChainId(chainId, outputTokenAddress) + : undefined; + const cache = latestGasPriceCache(chainId, depositArgs); while (true) { const diff = Date.now() - functionStart; @@ -140,38 +99,17 @@ const handler = async ( if (diff >= maxDurationSec * 1000) { break; } - const gasCost = await gasCostCache.get(); - if (utils.chainIsOPStack(chainId)) { - const cache = getCachedOpStackL1DataFee(depositArgs, gasCost); + try { await cache.set(); + } catch (err) { + logger.warn({ + at: "CronCacheGasPrices#updateGasPricePromise", + message: `Failed to set gas price cache for chain ${chainId}`, + depositArgs, + error: err, + }); } - await utils.delay(secondsPerUpdate); - } - }; - - /** - * @notice Updates the native gas cost cache every `updateNativeGasCostIntervalsSecPerChain` seconds - * up to `maxDurationSec` seconds. - * @param chainId Chain to estimate gas cost for - * @param outputTokenAddress This output token will be used to construct a fill transaction to simulate - * gas costs for. - */ - const updateNativeGasCostPromise = async ( - chainId: number, - outputTokenAddress: string - ): Promise => { - const secondsPerUpdate = updateNativeGasCostIntervalsSecPerChain.default; - const depositArgs = getDepositArgsForChainId(chainId, outputTokenAddress); - const cache = getCachedNativeGasCost(depositArgs); - - while (true) { - const diff = Date.now() - functionStart; - // Stop after `maxDurationSec` seconds - if (diff >= maxDurationSec * 1000) { - break; - } - await cache.set(); - await utils.delay(secondsPerUpdate); + await utils.delay(secondsPerUpdateForChain); } }; @@ -182,7 +120,7 @@ const handler = async ( // 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). - await Promise.all([ + const cacheUpdatePromises = Promise.all([ // @dev Linea gas prices are dependent on the L2 calldata to be submitted so compute one gas price for each output token, // so we compute one gas price per output token for Linea Promise.all( @@ -195,29 +133,8 @@ const handler = async ( updateGasPricePromise(CHAIN_IDs.LINEA, destinationToken) ) ), - Promise.all( - mainnetChains.map(async (chain) => { - const routesToChain = availableRoutes.filter( - ({ destinationChainId }) => destinationChainId === chain.chainId - ); - const outputTokensForChain = routesToChain.map( - ({ destinationToken }) => destinationToken - ); - await Promise.all([ - Promise.all( - outputTokensForChain.map((outputToken) => - updateNativeGasCostPromise(chain.chainId, outputToken) - ) - ), - Promise.all( - outputTokensForChain.map((outputToken) => - updateL1DataFeePromise(chain.chainId, outputToken) - ) - ), - ]); - }) - ), ]); + await Promise.race([cacheUpdatePromises, utils.delay(maxDurationSec)]); logger.debug({ at: "CronCacheGasPrices", diff --git a/vercel.json b/vercel.json index fa7fa812a..8f37ce6a3 100644 --- a/vercel.json +++ b/vercel.json @@ -9,6 +9,10 @@ "path": "/api/cron-cache-gas-prices", "schedule": "* * * * *" }, + { + "path": "/api/cron-cache-gas-costs", + "schedule": "* * * * *" + }, { "path": "/api/cron-ping-endpoints", "schedule": "* * * * *" @@ -18,6 +22,9 @@ "api/cron-cache-gas-prices.ts": { "maxDuration": 90 }, + "api/cron-cache-gas-costs.ts": { + "maxDuration": 90 + }, "api/cron-ping-endpoints.ts": { "maxDuration": 90 }