From 69b09623c37cf9f59b4a1aeb1a1083513304868b Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 10 Jan 2025 15:34:01 -0700 Subject: [PATCH 01/28] improve(API): Reduce stale-while-revalidate and gas price cache times ## `stale-while-revalidate` We can reduce this cache time to 1s so that after the cached value is >1s old we can immediately start recomputing the limits value. This means in the best case we'll have as fresh gas cost data as possible. ## Gas price caching: We should ideally use less stale gas price data. However, we don't want to increase the /limits response time. We currently use the gas price to compute the gas cost so it makes sense to make the gas price cache time slightly longer or equal to the gas cost. This way if the gas cost cache is set, then we'll use the cached gas cost value. If its stale, then we'll fetch the gas price and hopefully hit the cache sometimes. This is why it doesn't make sense to set the gas price cache less than the gas cost cache time otherwise we'll very rarely hit the gas price cache. --- api/_utils.ts | 8 +++----- api/limits.ts | 6 +++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/api/_utils.ts b/api/_utils.ts index 20e004d7a..8b6c45c7b 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -1979,7 +1979,6 @@ export function getCachedFillGasUsage( ) { const ttlPerChain = { default: 10, - [CHAIN_IDs.ARBITRUM]: 10, }; const cacheKey = buildInternalCacheKey( @@ -1987,7 +1986,7 @@ export function getCachedFillGasUsage( deposit.destinationChainId, deposit.outputToken ); - const ttl = ttlPerChain[deposit.destinationChainId] || ttlPerChain.default; + const ttl = ttlPerChain.default; const fetchFn = async () => { const relayerFeeCalculatorQueries = getRelayerFeeCalculatorQueries( deposit.destinationChainId, @@ -2032,13 +2031,12 @@ export function getCachedFillGasUsage( export function latestGasPriceCache(chainId: number) { const ttlPerChain = { - default: 30, - [CHAIN_IDs.ARBITRUM]: 15, + default: 10, }; return makeCacheGetterAndSetter( buildInternalCacheKey("latestGasPriceCache", chainId), - ttlPerChain[chainId] || ttlPerChain.default, + ttlPerChain.default, async () => (await getMaxFeePerGas(chainId)).maxFeePerGas, (bnFromCache) => BigNumber.from(bnFromCache) ); diff --git a/api/limits.ts b/api/limits.ts index 06a363b14..dfab93548 100644 --- a/api/limits.ts +++ b/api/limits.ts @@ -401,9 +401,9 @@ const handler = async ( message: "Response data", responseJson, }); - // Respond with a 200 status code and 10 seconds of cache with - // 45 seconds of stale-while-revalidate. - sendResponse(response, responseJson, 200, 10, 45); + // Respond with a 200 status code and 1 second of cache time with + // 59s to keep serving the stale data while recomputing the cached value. + sendResponse(response, responseJson, 200, 1, 59); } catch (error: unknown) { return handleErrorCondition("limits", response, logger, error); } From 0e947911e27843746b521a8fe433ac2747535c5e Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 10 Jan 2025 20:08:21 -0700 Subject: [PATCH 02/28] Separate native gas cost and op stack l1 gas cost calculation from tokenGasCost calculation Willl allow us to use more customized caching times for different gas cost components that are expected to change on different time periods --- api/_utils.ts | 93 ++++++++++++++++++++++++++++++++++----------------- api/limits.ts | 62 +++++++++++++++++++++------------- 2 files changed, 101 insertions(+), 54 deletions(-) diff --git a/api/_utils.ts b/api/_utils.ts index 8b6c45c7b..44c89a6f5 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -1970,68 +1970,99 @@ export function isContractCache(chainId: number, address: string) { ); } -export function getCachedFillGasUsage( +export function getCachedNativeGasCost( deposit: Parameters[0], - gasPrice?: BigNumber, overrides?: Partial<{ relayerAddress: string; }> ) { + // We can use a long TTL since we are fetching only the native gas cost which should rarely change. const ttlPerChain = { - default: 10, + default: 60, }; const cacheKey = buildInternalCacheKey( - "fillGasUsage", + "nativeGasCost", deposit.destinationChainId, deposit.outputToken ); const ttl = ttlPerChain.default; const fetchFn = async () => { + const relayerAddress = + overrides?.relayerAddress ?? + sdk.constants.DEFAULT_SIMULATED_RELAYER_ADDRESS; const relayerFeeCalculatorQueries = getRelayerFeeCalculatorQueries( deposit.destinationChainId, overrides ); + const unsignedFillTxn = await sdk.utils.populateV3Relay( + relayerFeeCalculatorQueries.spokePool, + buildDepositForSimulation(deposit), + relayerAddress + ); + const voidSigner = new ethers.VoidSigner( + relayerAddress, + relayerFeeCalculatorQueries.provider + ); + return voidSigner.estimateGas(unsignedFillTxn); + }; + + return getCachedValue(cacheKey, ttl, fetchFn, (nativeGasCostFromCache) => { + return BigNumber.from(nativeGasCostFromCache); + }); +} + +export function getCachedOpStackL1DataFee( + deposit: Parameters[0], + nativeGasCost: BigNumber, + overrides?: Partial<{ + relayerAddress: string; + }> +) { + // This should roughly be the length of 1 block on Ethereum mainnet which is how often the L1 data fee should + // change since its based on the L1 base fee. However, this L1 data fee is mostly affected by the L1 base fee which + // should only change by 12.5% at most per block. + const ttlPerChain = { + default: 12, + }; + + const cacheKey = buildInternalCacheKey( + "opStackL1DataFee", + deposit.destinationChainId, + 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. - const markups = getGasMarkup(deposit.destinationChainId); - const gasCosts = await relayerFeeCalculatorQueries.getGasCosts( + const { opStackL1DataFeeMarkup } = getGasMarkup(deposit.destinationChainId); + const relayerFeeCalculatorQueries = getRelayerFeeCalculatorQueries( + deposit.destinationChainId, + overrides + ); + const { opStackL1GasCost } = await relayerFeeCalculatorQueries.getGasCosts( buildDepositForSimulation(deposit), overrides?.relayerAddress, { - gasPrice, - // We want the fee multipliers if the gasPrice is undefined: - baseFeeMultiplier: markups.baseFeeMarkup, - priorityFeeMultiplier: markups.priorityFeeMarkup, - opStackL1GasCostMultiplier: sdk.utils.chainIsOPStack( - deposit.destinationChainId - ) - ? getGasMarkup(deposit.destinationChainId).opStackL1DataFeeMarkup - : undefined, + gasUnits: nativeGasCost, // Passed in here to avoid gas cost recomputation by the SDK + gasPrice: 1, // We pass in a gas price here so that the SDK doesn't recompute the gas price but we don't + // use any results from the SDK dependent on this value so it doesn't matter what it is. The OP + // Stack L1 data fee is unaffected by this gas price. + opStackL1GasCostMultiplier: opStackL1DataFeeMarkup, } ); - return { - nativeGasCost: gasCosts.nativeGasCost, - tokenGasCost: gasCosts.tokenGasCost, - }; + return opStackL1GasCost; }; - return getCachedValue( - cacheKey, - ttl, - fetchFn, - (gasCosts: { nativeGasCost: BigNumber; tokenGasCost: BigNumber }) => { - return { - nativeGasCost: BigNumber.from(gasCosts.nativeGasCost), - tokenGasCost: BigNumber.from(gasCosts.tokenGasCost), - }; - } - ); + return getCachedValue(cacheKey, ttl, fetchFn, (l1DataFeeFromCache) => { + return BigNumber.from(l1DataFeeFromCache); + }); } export function latestGasPriceCache(chainId: number) { const ttlPerChain = { - default: 10, + default: 5, }; return makeCacheGetterAndSetter( diff --git a/api/limits.ts b/api/limits.ts index dfab93548..30220398b 100644 --- a/api/limits.ts +++ b/api/limits.ts @@ -34,8 +34,11 @@ import { validateDepositMessage, getCachedFillGasUsage, latestGasPriceCache, + getCachedNativeGasCost, + getCachedOpStackL1DataFee, } from "./_utils"; import { MissingParamError } from "./_errors"; +import { bnZero } from "utils"; const LimitsQueryParamsSchema = object({ token: optional(validAddress()), @@ -164,34 +167,43 @@ const handler = async ( message, }; - const [tokenPriceNative, _tokenPriceUsd, latestBlock, gasPrice] = - await Promise.all([ - getCachedTokenPrice( - l1Token.address, - sdk.utils.getNativeTokenSymbol(destinationChainId).toLowerCase() - ), - getCachedTokenPrice(l1Token.address, "usd"), - getCachedLatestBlock(HUB_POOL_CHAIN_ID), - // If Linea, then we will defer gas price estimation to the SDK in getCachedFillGasUsage because - // the priority fee depends upon the fill transaction calldata. - destinationChainId === CHAIN_IDs.LINEA - ? undefined - : latestGasPriceCache(destinationChainId).get(), - ]); + const [ + tokenPriceNative, + _tokenPriceUsd, + latestBlock, + gasPrice, + nativeGasCost, + ] = await Promise.all([ + getCachedTokenPrice( + l1Token.address, + sdk.utils.getNativeTokenSymbol(destinationChainId).toLowerCase() + ), + getCachedTokenPrice(l1Token.address, "usd"), + getCachedLatestBlock(HUB_POOL_CHAIN_ID), + // If Linea, then we will defer gas price estimation to the SDK in getCachedFillGasUsage because + // the priority fee depends upon the fill transaction calldata. + destinationChainId === CHAIN_IDs.LINEA + ? undefined + : latestGasPriceCache(destinationChainId).get(), + isMessageDefined + ? undefined // Only use cached gas units if message is not defined, i.e. standard for standard bridges + : getCachedNativeGasCost(depositArgs, { relayerAddress: relayer }), + ]); const tokenPriceUsd = ethers.utils.parseUnits(_tokenPriceUsd.toString()); const [ - gasCosts, + opStackL1GasCost, multicallOutput, fullRelayerBalances, transferRestrictedBalances, fullRelayerMainnetBalances, ] = await Promise.all([ - isMessageDefined - ? undefined // Only use cached gas units if message is not defined, i.e. standard for standard bridges - : getCachedFillGasUsage(depositArgs, gasPrice, { + nativeGasCost && sdk.utils.chainIsOPStack(destinationChainId) + ? // Only use cached gas units if message is not defined, i.e. standard for standard bridges + getCachedOpStackL1DataFee(depositArgs, nativeGasCost, { relayerAddress: relayer, - }), + }) + : undefined, callViaMulticall3(provider, multiCalls, { blockTag: latestBlock.number, }), @@ -221,15 +233,19 @@ const handler = async ( ) ), ]); - // This call should not make any additional RPC queries if gasCosts is defined--for any deposit - // with an empty message. + // This call should not make any additional RPC queries since we are passing in gasPrice, nativeGasCost + // and tokenGasCost. + const tokenGasCost = + nativeGasCost && gasPrice + ? nativeGasCost.mul(gasPrice).add(opStackL1GasCost ?? bnZero) + : undefined; const relayerFeeDetails = await getRelayerFeeDetails( depositArgs, tokenPriceNative, relayer, gasPrice, - gasCosts?.nativeGasCost, - gasCosts?.tokenGasCost + nativeGasCost, + tokenGasCost ); logger.debug({ at: "Limits", From ccb8fa8dbba674abb81cfae74d05abc128c6abc9 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 10 Jan 2025 20:23:06 -0700 Subject: [PATCH 03/28] Use gas price cache for Linea as well --- api/_utils.ts | 69 ++++++++++++++++++++++++++++++++++++--------------- api/limits.ts | 6 +---- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/api/_utils.ts b/api/_utils.ts index 44c89a6f5..4102dfbee 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -1970,6 +1970,25 @@ export function isContractCache(chainId: number, address: string) { ); } +function getUnsignedFillTxnFromDeposit( + deposit: Parameters[0], + _relayerAddress?: string +): Promise { + const relayerAddress = + _relayerAddress ?? sdk.constants.DEFAULT_SIMULATED_RELAYER_ADDRESS; + const relayerFeeCalculatorQueries = getRelayerFeeCalculatorQueries( + deposit.destinationChainId, + { + relayerAddress, + } + ); + return sdk.utils.populateV3Relay( + relayerFeeCalculatorQueries.spokePool, + buildDepositForSimulation(deposit), + relayerAddress + ); +} + export function getCachedNativeGasCost( deposit: Parameters[0], overrides?: Partial<{ @@ -1995,9 +2014,8 @@ export function getCachedNativeGasCost( deposit.destinationChainId, overrides ); - const unsignedFillTxn = await sdk.utils.populateV3Relay( - relayerFeeCalculatorQueries.spokePool, - buildDepositForSimulation(deposit), + const unsignedFillTxn = await getUnsignedFillTxnFromDeposit( + deposit, relayerAddress ); const voidSigner = new ethers.VoidSigner( @@ -2060,36 +2078,47 @@ export function getCachedOpStackL1DataFee( }); } -export function latestGasPriceCache(chainId: number) { +export function latestGasPriceCache( + deposit: Parameters[0], + overrides?: Partial<{ + relayerAddress: string; + }> +) { const ttlPerChain = { default: 5, }; - return makeCacheGetterAndSetter( - buildInternalCacheKey("latestGasPriceCache", chainId), + buildInternalCacheKey("latestGasPriceCache", deposit.destinationChainId), ttlPerChain.default, - async () => (await getMaxFeePerGas(chainId)).maxFeePerGas, + async () => (await getMaxFeePerGas(deposit, overrides)).maxFeePerGas, (bnFromCache) => BigNumber.from(bnFromCache) ); } -/** - * Resolve the current gas price for a given chain - * @param chainId The chain ID to resolve the gas price for - * @returns The gas price in the native currency of the chain - */ -export function getMaxFeePerGas( - chainId: number +export async function getMaxFeePerGas( + deposit: Parameters[0], + overrides?: Partial<{ + relayerAddress: string; + }> ): Promise { + const { destinationChainId } = deposit; const { baseFeeMarkup: baseFeeMultiplier, priorityFeeMarkup: priorityFeeMultiplier, - } = getGasMarkup(chainId); - return sdk.gasPriceOracle.getGasPriceEstimate(getProvider(chainId), { - chainId, - baseFeeMultiplier, - priorityFeeMultiplier, - }); + } = getGasMarkup(destinationChainId); + const unsignedFillTxn = await getUnsignedFillTxnFromDeposit( + deposit, + overrides?.relayerAddress + ); + return sdk.gasPriceOracle.getGasPriceEstimate( + getProvider(destinationChainId), + { + chainId: destinationChainId, + unsignedTx: unsignedFillTxn, + baseFeeMultiplier, + priorityFeeMultiplier, + } + ); } /** diff --git a/api/limits.ts b/api/limits.ts index 30220398b..cade54d9c 100644 --- a/api/limits.ts +++ b/api/limits.ts @@ -180,11 +180,7 @@ const handler = async ( ), getCachedTokenPrice(l1Token.address, "usd"), getCachedLatestBlock(HUB_POOL_CHAIN_ID), - // If Linea, then we will defer gas price estimation to the SDK in getCachedFillGasUsage because - // the priority fee depends upon the fill transaction calldata. - destinationChainId === CHAIN_IDs.LINEA - ? undefined - : latestGasPriceCache(destinationChainId).get(), + latestGasPriceCache(depositArgs, { relayerAddress: relayer }).get(), isMessageDefined ? undefined // Only use cached gas units if message is not defined, i.e. standard for standard bridges : getCachedNativeGasCost(depositArgs, { relayerAddress: relayer }), From ea1585ea28fa3ba9c427a2dce60a16ca4b8b8bbf Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 10 Jan 2025 20:36:31 -0700 Subject: [PATCH 04/28] fix: only cron cache gas prices for non Linea chains --- api/_utils.ts | 41 +++++++++++++++++++----------------- api/cron-cache-gas-prices.ts | 36 ++++++++++++++++++------------- api/gas-prices.ts | 3 +-- api/limits.ts | 5 +++-- 4 files changed, 47 insertions(+), 38 deletions(-) diff --git a/api/_utils.ts b/api/_utils.ts index 4102dfbee..ace7403f9 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -2079,7 +2079,8 @@ export function getCachedOpStackL1DataFee( } export function latestGasPriceCache( - deposit: Parameters[0], + chainId: number, + deposit?: Parameters[0], overrides?: Partial<{ relayerAddress: string; }> @@ -2088,37 +2089,39 @@ export function latestGasPriceCache( default: 5, }; return makeCacheGetterAndSetter( - buildInternalCacheKey("latestGasPriceCache", deposit.destinationChainId), + buildInternalCacheKey("latestGasPriceCache", chainId), ttlPerChain.default, - async () => (await getMaxFeePerGas(deposit, overrides)).maxFeePerGas, + async () => + (await getMaxFeePerGas(chainId, deposit, overrides)).maxFeePerGas, (bnFromCache) => BigNumber.from(bnFromCache) ); } export async function getMaxFeePerGas( - deposit: Parameters[0], + chainId: number, + deposit?: Parameters[0], overrides?: Partial<{ relayerAddress: string; }> ): Promise { - const { destinationChainId } = deposit; + if (deposit && deposit.destinationChainId !== chainId) { + throw new Error( + "Chain ID must match the destination chain ID of the deposit" + ); + } const { baseFeeMarkup: baseFeeMultiplier, priorityFeeMarkup: priorityFeeMultiplier, - } = getGasMarkup(destinationChainId); - const unsignedFillTxn = await getUnsignedFillTxnFromDeposit( - deposit, - overrides?.relayerAddress - ); - return sdk.gasPriceOracle.getGasPriceEstimate( - getProvider(destinationChainId), - { - chainId: destinationChainId, - unsignedTx: unsignedFillTxn, - baseFeeMultiplier, - priorityFeeMultiplier, - } - ); + } = getGasMarkup(chainId); + const unsignedFillTxn = deposit + ? await getUnsignedFillTxnFromDeposit(deposit, overrides?.relayerAddress) + : undefined; + return sdk.gasPriceOracle.getGasPriceEstimate(getProvider(chainId), { + chainId, + unsignedTx: unsignedFillTxn, + baseFeeMultiplier, + priorityFeeMultiplier, + }); } /** diff --git a/api/cron-cache-gas-prices.ts b/api/cron-cache-gas-prices.ts index ea494f9c6..5b97a3b14 100644 --- a/api/cron-cache-gas-prices.ts +++ b/api/cron-cache-gas-prices.ts @@ -10,6 +10,8 @@ import { UnauthorizedError } from "./_errors"; import mainnetChains from "../src/data/chains_1.json"; import { utils } from "@across-protocol/sdk"; +import { CHAIN_IDs } from "./_constants"; +import { BigNumber } from "ethers"; const updateIntervalsSecPerChain = { default: 10, @@ -52,23 +54,27 @@ const handler = async ( // But we want to update gas prices 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.map(async (chain) => { - const secondsPerUpdateForChain = - updateIntervalsSecPerChain[ - chain.chainId as keyof typeof updateIntervalsSecPerChain - ] || updateIntervalsSecPerChain.default; - const cache = latestGasPriceCache(chain.chainId); + const gasPricePromises = mainnetChains + .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; + while (true) { + const diff = Date.now() - functionStart; + // Stop after `maxDurationSec` seconds + if (diff >= maxDurationSec * 1000) { + break; + } + await cache.set(); + await utils.delay(secondsPerUpdateForChain); } - await cache.set(); - await utils.delay(secondsPerUpdateForChain); - } - }); + }); await Promise.all(gasPricePromises); logger.debug({ diff --git a/api/gas-prices.ts b/api/gas-prices.ts index 427da081e..5e17149ea 100644 --- a/api/gas-prices.ts +++ b/api/gas-prices.ts @@ -9,9 +9,8 @@ import { sendResponse, } from "./_utils"; import { TypedVercelRequest } from "./_types"; -import { ethers, providers, VoidSigner } from "ethers"; +import { ethers } from "ethers"; import * as sdk from "@across-protocol/sdk"; -import { L2Provider } from "@eth-optimism/sdk/dist/interfaces/l2-provider"; import mainnetChains from "../src/data/chains_1.json"; import { diff --git a/api/limits.ts b/api/limits.ts index cade54d9c..0c6863c32 100644 --- a/api/limits.ts +++ b/api/limits.ts @@ -32,7 +32,6 @@ import { getCachedLatestBlock, parsableBigNumberString, validateDepositMessage, - getCachedFillGasUsage, latestGasPriceCache, getCachedNativeGasCost, getCachedOpStackL1DataFee, @@ -180,7 +179,9 @@ const handler = async ( ), getCachedTokenPrice(l1Token.address, "usd"), getCachedLatestBlock(HUB_POOL_CHAIN_ID), - latestGasPriceCache(depositArgs, { relayerAddress: relayer }).get(), + latestGasPriceCache(destinationChainId, depositArgs, { + relayerAddress: relayer, + }).get(), isMessageDefined ? undefined // Only use cached gas units if message is not defined, i.e. standard for standard bridges : getCachedNativeGasCost(depositArgs, { relayerAddress: relayer }), From 5e4fd391feafc76bf8993d7a61fa6f19fac8d9a7 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 10 Jan 2025 20:45:05 -0700 Subject: [PATCH 05/28] Update limits.ts --- api/limits.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/limits.ts b/api/limits.ts index 0c6863c32..52431d44a 100644 --- a/api/limits.ts +++ b/api/limits.ts @@ -37,7 +37,6 @@ import { getCachedOpStackL1DataFee, } from "./_utils"; import { MissingParamError } from "./_errors"; -import { bnZero } from "utils"; const LimitsQueryParamsSchema = object({ token: optional(validAddress()), @@ -234,7 +233,9 @@ const handler = async ( // and tokenGasCost. const tokenGasCost = nativeGasCost && gasPrice - ? nativeGasCost.mul(gasPrice).add(opStackL1GasCost ?? bnZero) + ? nativeGasCost + .mul(gasPrice) + .add(opStackL1GasCost ?? ethers.BigNumber.from("0")) : undefined; const relayerFeeDetails = await getRelayerFeeDetails( depositArgs, From 296056fa29bfcc7389d1a81125156de790b3de17 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 10 Jan 2025 20:47:59 -0700 Subject: [PATCH 06/28] Only pass in depositArgs for Linea --- api/limits.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/api/limits.ts b/api/limits.ts index 52431d44a..4e576bf40 100644 --- a/api/limits.ts +++ b/api/limits.ts @@ -178,9 +178,15 @@ const handler = async ( ), getCachedTokenPrice(l1Token.address, "usd"), getCachedLatestBlock(HUB_POOL_CHAIN_ID), - latestGasPriceCache(destinationChainId, depositArgs, { - relayerAddress: relayer, - }).get(), + // We only want to derive an unsigned fill txn from the deposit args if the destination chain is Linea + // because only Linea's priority fee depends on the destination chain call data. + latestGasPriceCache( + destinationChainId, + CHAIN_IDs.LINEA === destinationChainId ? depositArgs : undefined, + { + relayerAddress: relayer, + } + ).get(), isMessageDefined ? undefined // Only use cached gas units if message is not defined, i.e. standard for standard bridges : getCachedNativeGasCost(depositArgs, { relayerAddress: relayer }), From c1f6e035d825a929be9c5769fe15d8dea8141112 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 10 Jan 2025 20:53:55 -0700 Subject: [PATCH 07/28] add extra part to cache key --- api/_utils.ts | 9 ++++++++- api/cron-cache-gas-prices.ts | 1 - 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/api/_utils.ts b/api/_utils.ts index ace7403f9..e45ef7dfd 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -2089,7 +2089,14 @@ export function latestGasPriceCache( default: 5, }; return makeCacheGetterAndSetter( - buildInternalCacheKey("latestGasPriceCache", chainId), + // If deposit is defined, then the gas price will be dependent on the fill transaction derived from the deposit. + // Therefore, we technically should cache a different gas price per different types of deposit so we add + // an additional outputToken to the cache key to distinguish between gas prices dependent on deposit args + // for different output tokens, which should be the main factor affecting the fill gas cost. + buildInternalCacheKey( + `latestGasPriceCache${deposit ? `-${deposit.outputToken}` : ""}`, + chainId + ), ttlPerChain.default, async () => (await getMaxFeePerGas(chainId, deposit, overrides)).maxFeePerGas, diff --git a/api/cron-cache-gas-prices.ts b/api/cron-cache-gas-prices.ts index 5b97a3b14..830267857 100644 --- a/api/cron-cache-gas-prices.ts +++ b/api/cron-cache-gas-prices.ts @@ -11,7 +11,6 @@ import { UnauthorizedError } from "./_errors"; import mainnetChains from "../src/data/chains_1.json"; import { utils } from "@across-protocol/sdk"; import { CHAIN_IDs } from "./_constants"; -import { BigNumber } from "ethers"; const updateIntervalsSecPerChain = { default: 10, From 5f7ab075d9f8a833154713876abf6dbe4bb4bf40 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 10 Jan 2025 22:09:27 -0700 Subject: [PATCH 08/28] Use sdk for helper methods --- api/_utils.ts | 62 ++++++++++++++++++++++----------------------------- package.json | 2 +- yarn.lock | 47 ++++++++++++++++++++++++++++++++------ 3 files changed, 68 insertions(+), 43 deletions(-) diff --git a/api/_utils.ts b/api/_utils.ts index e45ef7dfd..a7f194f67 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -1970,25 +1970,6 @@ export function isContractCache(chainId: number, address: string) { ); } -function getUnsignedFillTxnFromDeposit( - deposit: Parameters[0], - _relayerAddress?: string -): Promise { - const relayerAddress = - _relayerAddress ?? sdk.constants.DEFAULT_SIMULATED_RELAYER_ADDRESS; - const relayerFeeCalculatorQueries = getRelayerFeeCalculatorQueries( - deposit.destinationChainId, - { - relayerAddress, - } - ); - return sdk.utils.populateV3Relay( - relayerFeeCalculatorQueries.spokePool, - buildDepositForSimulation(deposit), - relayerAddress - ); -} - export function getCachedNativeGasCost( deposit: Parameters[0], overrides?: Partial<{ @@ -2014,10 +1995,11 @@ export function getCachedNativeGasCost( deposit.destinationChainId, overrides ); - const unsignedFillTxn = await getUnsignedFillTxnFromDeposit( - deposit, - relayerAddress - ); + const unsignedFillTxn = + await relayerFeeCalculatorQueries.getUnsignedTxFromDeposit( + buildDepositForSimulation(deposit), + relayerAddress + ); const voidSigner = new ethers.VoidSigner( relayerAddress, relayerFeeCalculatorQueries.provider @@ -2059,17 +2041,20 @@ export function getCachedOpStackL1DataFee( deposit.destinationChainId, overrides ); - const { opStackL1GasCost } = await relayerFeeCalculatorQueries.getGasCosts( - buildDepositForSimulation(deposit), - overrides?.relayerAddress, - { - gasUnits: nativeGasCost, // Passed in here to avoid gas cost recomputation by the SDK - gasPrice: 1, // We pass in a gas price here so that the SDK doesn't recompute the gas price but we don't - // use any results from the SDK dependent on this value so it doesn't matter what it is. The OP - // Stack L1 data fee is unaffected by this gas price. - opStackL1GasCostMultiplier: opStackL1DataFeeMarkup, - } - ); + const unsignedTx = + await relayerFeeCalculatorQueries.getUnsignedTxFromDeposit( + buildDepositForSimulation(deposit), + overrides?.relayerAddress + ); + const opStackL1GasCost = + await relayerFeeCalculatorQueries.getOpStackL1DataFee( + unsignedTx, + overrides?.relayerAddress, + { + opStackL2GasUnits: nativeGasCost, // Passed in here to avoid gas cost recomputation by the SDK + opStackL1DataFeeMultiplier: opStackL1DataFeeMarkup, + } + ); return opStackL1GasCost; }; @@ -2120,8 +2105,15 @@ export async function getMaxFeePerGas( baseFeeMarkup: baseFeeMultiplier, priorityFeeMarkup: priorityFeeMultiplier, } = getGasMarkup(chainId); + const relayerFeeCalculatorQueries = getRelayerFeeCalculatorQueries( + chainId, + overrides + ); const unsignedFillTxn = deposit - ? await getUnsignedFillTxnFromDeposit(deposit, overrides?.relayerAddress) + ? await relayerFeeCalculatorQueries.getUnsignedTxFromDeposit( + buildDepositForSimulation(deposit), + overrides?.relayerAddress + ) : undefined; return sdk.gasPriceOracle.getGasPriceEstimate(getProvider(chainId), { chainId, diff --git a/package.json b/package.json index 4ff85caf5..10de6425c 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@across-protocol/constants": "^3.1.24", "@across-protocol/contracts": "^3.0.19", "@across-protocol/contracts-v3.0.6": "npm:@across-protocol/contracts@3.0.6", - "@across-protocol/sdk": "^3.4.8", + "@across-protocol/sdk": "^3.4.10-beta.1", "@amplitude/analytics-browser": "^2.3.5", "@balancer-labs/sdk": "1.1.6-beta.16", "@emotion/react": "^11.13.0", diff --git a/yarn.lock b/yarn.lock index fd3648c77..deb950404 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,11 +16,16 @@ "@uma/common" "^2.17.0" hardhat "^2.9.3" -"@across-protocol/constants@^3.1.24", "@across-protocol/constants@^3.1.25": +"@across-protocol/constants@^3.1.24": version "3.1.25" resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.25.tgz#60d6d9814582ff91faf2b6d9f51d6dccb447b4ce" integrity sha512-GpZoYn7hETYL2BPMM2GqXAer6+l/xuhder+pvpb00HJcb/sqCjF7vaaeKxjKJ3jKtyeulYmdu0NDkeNm5KbNWA== +"@across-protocol/constants@^3.1.27": + version "3.1.28" + resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.28.tgz#0540f5a44b085b0951a853898afe174ea113db3a" + integrity sha512-rnI1pQgkJ6+hPIQNomsi8eQreVfWKfFn9i9Z39U0fAnoXodZklW0eqj5N0cXlEfahp5j2u1RCs7s6fQ9megCdw== + "@across-protocol/constants@^3.1.9": version "3.1.13" resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.13.tgz#b4caf494e9d9fe50290cca91b7883ea408fdb90a" @@ -83,14 +88,42 @@ yargs "^17.7.2" zksync-web3 "^0.14.3" -"@across-protocol/sdk@^3.4.8": - version "3.4.8" - resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.4.8.tgz#070abb97b687cfe22d89349d776f68008af5f6a7" - integrity sha512-m4JnT3Sh+zmTZ/Oi7QrTv3IuNB+myBcbnPnEEssZroyBost/yEPyxXks+EHeU67KrT84t/otGyNb5YpTOvOK0A== +"@across-protocol/contracts@^3.0.20": + version "3.0.20" + resolved "https://registry.yarnpkg.com/@across-protocol/contracts/-/contracts-3.0.20.tgz#5a70782093d21a96b2e955b7ed725bea7af6e804" + integrity sha512-ufyO+MrbY7+0TDm/1cDl9iAeR4P8jt0AM1F9wiCBHVIYtj1wMD4eNm7G5Am3u8p1ruMjRhi6dJEVQcRF2O+LUg== + dependencies: + "@across-protocol/constants" "^3.1.27" + "@coral-xyz/anchor" "^0.30.1" + "@defi-wonderland/smock" "^2.3.4" + "@eth-optimism/contracts" "^0.5.40" + "@ethersproject/abstract-provider" "5.7.0" + "@ethersproject/abstract-signer" "5.7.0" + "@ethersproject/bignumber" "5.7.0" + "@openzeppelin/contracts" "4.9.6" + "@openzeppelin/contracts-upgradeable" "4.9.6" + "@scroll-tech/contracts" "^0.1.0" + "@solana-developers/helpers" "^2.4.0" + "@solana/spl-token" "^0.4.6" + "@solana/web3.js" "^1.31.0" + "@types/yargs" "^17.0.33" + "@uma/common" "^2.37.3" + "@uma/contracts-node" "^0.4.17" + "@uma/core" "^2.61.0" + axios "^1.7.4" + bs58 "^6.0.0" + prettier-plugin-rust "^0.1.9" + yargs "^17.7.2" + zksync-web3 "^0.14.3" + +"@across-protocol/sdk@^3.4.10-beta.1": + version "3.4.10-beta.1" + resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.4.10-beta.1.tgz#81d56458d97dec424e2fe51730f8ae3953c84e6c" + integrity sha512-c4tD6RQBKiok4fAK99Kh69B5+PIcAz0DLaAhfWxInRxW4kBeAKSrOtjETxuYN1wRZ89qRsGx+N+7c/wDpeQDxg== dependencies: "@across-protocol/across-token" "^1.0.0" - "@across-protocol/constants" "^3.1.25" - "@across-protocol/contracts" "^3.0.19" + "@across-protocol/constants" "^3.1.27" + "@across-protocol/contracts" "^3.0.20" "@eth-optimism/sdk" "^3.3.1" "@ethersproject/bignumber" "^5.7.0" "@pinata/sdk" "^2.1.0" From d6dac139f7fd16dd183b7101158c83dcc508a0f0 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 10 Jan 2025 22:14:44 -0700 Subject: [PATCH 09/28] Update limits.ts --- api/limits.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/limits.ts b/api/limits.ts index 4e576bf40..fadcb6b5d 100644 --- a/api/limits.ts +++ b/api/limits.ts @@ -235,14 +235,14 @@ const handler = async ( ) ), ]); - // This call should not make any additional RPC queries since we are passing in gasPrice, nativeGasCost - // and tokenGasCost. const tokenGasCost = nativeGasCost && gasPrice ? nativeGasCost .mul(gasPrice) .add(opStackL1GasCost ?? ethers.BigNumber.from("0")) : undefined; + // This call should not make any additional RPC queries since we are passing in gasPrice, nativeGasCost + // and tokenGasCost. const relayerFeeDetails = await getRelayerFeeDetails( depositArgs, tokenPriceNative, From b06f9ccf640417ef15a60a9d782b5f058f015706 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 10 Jan 2025 22:46:34 -0700 Subject: [PATCH 10/28] Fix gas-prices --- api/_utils.ts | 10 +++-- api/gas-prices.ts | 102 +++++++++++++++++++--------------------------- api/limits.ts | 2 +- 3 files changed, 51 insertions(+), 63 deletions(-) diff --git a/api/_utils.ts b/api/_utils.ts index a7f194f67..ed04a80ca 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -2083,9 +2083,13 @@ export function latestGasPriceCache( chainId ), ttlPerChain.default, - async () => - (await getMaxFeePerGas(chainId, deposit, overrides)).maxFeePerGas, - (bnFromCache) => BigNumber.from(bnFromCache) + async () => await getMaxFeePerGas(chainId, deposit, overrides), + (gasPrice: sdk.gasPriceOracle.GasPriceEstimate) => { + return { + maxFeePerGas: BigNumber.from(gasPrice.maxFeePerGas), + maxPriorityFeePerGas: BigNumber.from(gasPrice.maxPriorityFeePerGas), + }; + } ); } diff --git a/api/gas-prices.ts b/api/gas-prices.ts index 5e17149ea..5cfe7f345 100644 --- a/api/gas-prices.ts +++ b/api/gas-prices.ts @@ -1,11 +1,11 @@ import { VercelResponse } from "@vercel/node"; import { - buildDepositForSimulation, + getCachedNativeGasCost, + getCachedOpStackL1DataFee, getGasMarkup, getLogger, - getMaxFeePerGas, - getRelayerFeeCalculatorQueries, handleErrorCondition, + latestGasPriceCache, sendResponse, } from "./_utils"; import { TypedVercelRequest } from "./_types"; @@ -27,6 +27,16 @@ const QueryParamsSchema = object({ }); type QueryParams = Infer; +const getDepositArgsForChainId = (chainId: number, tokenAddress: string) => { + return { + amount: ethers.BigNumber.from(100), + inputToken: sdk.constants.ZERO_ADDRESS, + outputToken: tokenAddress, + recipientAddress: DEFAULT_SIMULATED_RECIPIENT_ADDRESS, + originChainId: 0, // Shouldn't matter for simulation + destinationChainId: Number(chainId), + }; +}; const handler = async ( { query }: TypedVercelRequest, response: VercelResponse @@ -46,56 +56,41 @@ const handler = async ( }) .filter(([, tokenAddress]) => tokenAddress !== undefined) ); - // getMaxFeePerGas will return the gas price after including the baseFeeMultiplier. - const gasPrices = await Promise.all( - Object.keys(chainIdsWithToken).map((chainId) => { - return getMaxFeePerGas(Number(chainId)); + const gasData = await Promise.all( + Object.entries(chainIdsWithToken).map(([chainId, tokenAddress]) => { + const depositArgs = getDepositArgsForChainId( + Number(chainId), + tokenAddress + ); + return Promise.all([ + getCachedNativeGasCost(depositArgs), + latestGasPriceCache( + Number(chainId), + CHAIN_IDs.LINEA === Number(chainId) ? depositArgs : undefined + ).get(), + ]); }) ); + // We query the following gas costs after gas prices because token gas costs and op stack l1 gas costs + // depend on the gas price and native gas unit results. const gasCosts = await Promise.all( Object.entries(chainIdsWithToken).map( async ([chainId, tokenAddress], i) => { - // This is a dummy deposit used to pass into buildDepositForSimulation() to build a fill transaction - // that we can simulate without reversion. The only parameter that matters is that the destinationChainId - // is set to the spoke pool's chain ID we'll be simulating the fill call on. - const depositArgs = { - amount: ethers.BigNumber.from(100), - inputToken: sdk.constants.ZERO_ADDRESS, - outputToken: tokenAddress, - recipientAddress: DEFAULT_SIMULATED_RECIPIENT_ADDRESS, - originChainId: 0, // Shouldn't matter for simulation - destinationChainId: Number(chainId), - }; - const deposit = buildDepositForSimulation(depositArgs); - const relayerFeeCalculatorQueries = getRelayerFeeCalculatorQueries( - Number(chainId) + const depositArgs = getDepositArgsForChainId( + Number(chainId), + tokenAddress ); - const { baseFeeMarkup, priorityFeeMarkup, opStackL1DataFeeMarkup } = - getGasMarkup(Number(chainId)); - const { nativeGasCost, tokenGasCost, opStackL1GasCost, gasPrice } = - await relayerFeeCalculatorQueries.getGasCosts( - deposit, - relayerFeeCalculatorQueries.simulatedRelayerAddress, - { - // Pass in the already-computed gasPrice into this query so that the tokenGasCost includes - // the scaled gas price, - // e.g. tokenGasCost = nativeGasCost * (baseFee * baseFeeMultiplier + priorityFee). - // Except for Linea, where the gas price is dependent on the unsignedTx produced from the deposit, - // so let the SDK compute its gas price here. - gasPrice: - Number(chainId) === CHAIN_IDs.LINEA - ? undefined - : gasPrices[i].maxFeePerGas, - opStackL1GasCostMultiplier: opStackL1DataFeeMarkup, - baseFeeMultiplier: baseFeeMarkup, - priorityFeeMultiplier: priorityFeeMarkup, - } - ); + const [nativeGasCost, gasPrice] = gasData[i]; + const opStackL1GasCost = sdk.utils.chainIsOPStack(Number(chainId)) + ? await getCachedOpStackL1DataFee(depositArgs, nativeGasCost) + : undefined; + const tokenGasCost = nativeGasCost + .mul(gasPrice.maxFeePerGas) + .add(opStackL1GasCost ?? ethers.BigNumber.from("0")); return { nativeGasCost, tokenGasCost, opStackL1GasCost, - gasPrice, }; } ) @@ -106,23 +101,12 @@ const handler = async ( Object.keys(chainIdsWithToken).map((chainId, i) => [ chainId, { - gasPrice: - Number(chainId) === CHAIN_IDs.LINEA - ? gasCosts[i].gasPrice.toString() - : gasPrices[i].maxFeePerGas.toString(), + gasPrice: gasData[i][1].maxFeePerGas.toString(), gasPriceComponents: { - // Linea hardcodes base fee at 7 wei so we can always back it out fromthe gasPrice returned by the - // getGasCosts method. - maxFeePerGas: - Number(chainId) === CHAIN_IDs.LINEA - ? gasCosts[i].gasPrice.sub(7).toString() - : gasPrices[i].maxFeePerGas - .sub(gasPrices[i].maxPriorityFeePerGas) - .toString(), - priorityFeePerGas: - Number(chainId) === CHAIN_IDs.LINEA - ? "7" - : gasPrices[i].maxPriorityFeePerGas.toString(), + maxFeePerGas: gasData[i][1].maxFeePerGas + .sub(gasData[i][1].maxPriorityFeePerGas) + .toString(), + priorityFeePerGas: gasData[i][1].maxPriorityFeePerGas.toString(), baseFeeMultiplier: ethers.utils.formatEther( getGasMarkup(chainId).baseFeeMarkup ), diff --git a/api/limits.ts b/api/limits.ts index fadcb6b5d..d4dbcc764 100644 --- a/api/limits.ts +++ b/api/limits.ts @@ -169,7 +169,7 @@ const handler = async ( tokenPriceNative, _tokenPriceUsd, latestBlock, - gasPrice, + { maxFeePerGas: gasPrice }, nativeGasCost, ] = await Promise.all([ getCachedTokenPrice( From 9e18813baa5797418b98860e3884542a58a10d82 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 10 Jan 2025 23:00:03 -0700 Subject: [PATCH 11/28] Use utils in gas-prices.ts to read data from cache --- api/cron-cache-gas-prices.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/cron-cache-gas-prices.ts b/api/cron-cache-gas-prices.ts index 830267857..759cf3321 100644 --- a/api/cron-cache-gas-prices.ts +++ b/api/cron-cache-gas-prices.ts @@ -54,6 +54,8 @@ const handler = async ( // 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 = From bc5aec18cbe14bae969e2c295d5b2ce0c164222f Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sat, 11 Jan 2025 07:40:04 -0700 Subject: [PATCH 12/28] add gas costs to cron job --- api/_utils.ts | 28 ++++--- api/cron-cache-gas-prices.ts | 155 +++++++++++++++++++++++++++++------ api/gas-prices.ts | 4 +- api/limits.ts | 6 +- 4 files changed, 152 insertions(+), 41 deletions(-) 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, From f42156f452db6b98ca668c584d7f634371d56ff4 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sat, 11 Jan 2025 08:08:57 -0700 Subject: [PATCH 13/28] cache gas prices before cost --- api/_utils.ts | 8 ++++++-- api/cron-cache-gas-prices.ts | 16 ++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/api/_utils.ts b/api/_utils.ts index 9584bcdce..1961f38e6 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -2022,11 +2022,13 @@ export function getCachedOpStackL1DataFee( relayerAddress: string; }> ) { - // This should roughly be the length of 1 block on Ethereum mainnet which is how often the L1 data fee should + // This should roughly be longer than the length of 1 block on Ethereum mainnet which is how often the L1 data fee should // change since its based on the L1 base fee. However, this L1 data fee is mostly affected by the L1 base fee which // should only change by 12.5% at most per block. + // We set this higher than the secondsPerUpdate value in the cron cache gas prices job which will update this + // more frequently. const ttlPerChain = { - default: 12, + default: 24, }; const cacheKey = buildInternalCacheKey( @@ -2077,6 +2079,8 @@ export function latestGasPriceCache( relayerAddress: string; }> ) { + // We set this higher than the secondsPerUpdate value in the cron cache gas prices job which will update this + // more frequently. const ttlPerChain = { default: 10, [CHAIN_IDs.MAINNET]: 24, diff --git a/api/cron-cache-gas-prices.ts b/api/cron-cache-gas-prices.ts index 54d5b6185..06d863486 100644 --- a/api/cron-cache-gas-prices.ts +++ b/api/cron-cache-gas-prices.ts @@ -26,15 +26,15 @@ type Route = { destinationTokenSymbol: string; }; -// Set slightly lower than TTL in latestGasPriceCache +// Set lower than TTL in latestGasPriceCache const updateIntervalsSecPerChain = { default: 5, 1: 12, }; -// Set slightly lower than TTL in getCachedOpStackL1DataFee. +// Set lower than TTL in getCachedOpStackL1DataFee and getCachedNativeGasCost const updateL1DataFeeIntervalsSecPerChain = { - default: 10, + default: 12, }; const maxDurationSec = 60; @@ -128,8 +128,7 @@ const handler = async ( chainId: number, outputTokenAddress: string ) => { - const secondsPerUpdateForChain = - updateL1DataFeeIntervalsSecPerChain.default; + const secondsPerUpdate = updateL1DataFeeIntervalsSecPerChain.default; const depositArgs = getDepositArgsForChainId(chainId, outputTokenAddress); const gasCostCache = getCachedNativeGasCost(depositArgs); @@ -142,7 +141,7 @@ const handler = async ( const gasCost = await gasCostCache.get(); const cache = getCachedOpStackL1DataFee(depositArgs, gasCost); await cache.set(); - await utils.delay(secondsPerUpdateForChain); + await utils.delay(secondsPerUpdate); } }; @@ -153,6 +152,9 @@ const handler = async ( const gasPricePromises = mainnetChains.map(async (chain) => { // For each chain: // - update the destination gas price for the chain + if (chain.chainId !== CHAIN_IDs.LINEA) { + updateGasPricePromise(chain.chainId); + } // For each output token on that chain: // - update the simulated gas costs for the token const routesToChain = availableRoutes.filter( @@ -173,8 +175,6 @@ const handler = async ( updateGasPricePromise(chain.chainId, outputToken) ) ); - } else { - updateGasPricePromise(chain.chainId); } }); await Promise.all(gasPricePromises); From be317b70232670f15e4ab9bbda13f99918d8d358 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sat, 11 Jan 2025 08:12:27 -0700 Subject: [PATCH 14/28] remove promise.all --- api/cron-cache-gas-prices.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/api/cron-cache-gas-prices.ts b/api/cron-cache-gas-prices.ts index 06d863486..f3ca705bc 100644 --- a/api/cron-cache-gas-prices.ts +++ b/api/cron-cache-gas-prices.ts @@ -163,17 +163,13 @@ const handler = async ( const outputTokensForChain = routesToChain.map( ({ destinationToken }) => destinationToken ); - Promise.all( - outputTokensForChain.map((outputToken) => - updateL1DataFeePromise(chain.chainId, outputToken) - ) + 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) - ) + outputTokensForChain.map((outputToken) => + updateGasPricePromise(chain.chainId, outputToken) ); } }); From bbfe3f7418094ed1adf35cdcc4306dcb7317756b Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sat, 11 Jan 2025 08:15:17 -0700 Subject: [PATCH 15/28] Update _utils.ts --- api/_utils.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/api/_utils.ts b/api/_utils.ts index 1961f38e6..f403e508a 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -1977,8 +1977,9 @@ export function getCachedNativeGasCost( }> ) { // We can use a long TTL since we are fetching only the native gas cost which should rarely change. + // Set this longer than the secondsPerUpdate value in the cron cache gas prices job. const ttlPerChain = { - default: 60, + default: 120, }; const cacheKey = buildInternalCacheKey( "nativeGasCost", @@ -2022,13 +2023,13 @@ export function getCachedOpStackL1DataFee( relayerAddress: string; }> ) { - // This should roughly be longer than the length of 1 block on Ethereum mainnet which is how often the L1 data fee should + // This should be longer than the length of 1 block on Ethereum mainnet which is how often the L1 data fee should // change since its based on the L1 base fee. However, this L1 data fee is mostly affected by the L1 base fee which // should only change by 12.5% at most per block. // We set this higher than the secondsPerUpdate value in the cron cache gas prices job which will update this // more frequently. const ttlPerChain = { - default: 24, + default: 30, }; const cacheKey = buildInternalCacheKey( @@ -2082,8 +2083,7 @@ export function latestGasPriceCache( // We set this higher than the secondsPerUpdate value in the cron cache gas prices job which will update this // more frequently. const ttlPerChain = { - default: 10, - [CHAIN_IDs.MAINNET]: 24, + default: 30, }; return makeCacheGetterAndSetter( // If deposit is defined, then the gas price will be dependent on the fill transaction derived from the deposit. From ea1d1a7cbc6d06c1a0f372af21ffbf4f502bd484 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sat, 11 Jan 2025 08:22:41 -0700 Subject: [PATCH 16/28] cache op stack l1 costs for op chains only --- api/cron-cache-gas-prices.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/cron-cache-gas-prices.ts b/api/cron-cache-gas-prices.ts index f3ca705bc..5003e67c9 100644 --- a/api/cron-cache-gas-prices.ts +++ b/api/cron-cache-gas-prices.ts @@ -139,8 +139,10 @@ const handler = async ( break; } const gasCost = await gasCostCache.get(); - const cache = getCachedOpStackL1DataFee(depositArgs, gasCost); - await cache.set(); + if (utils.chainIsOPStack(chainId)) { + const cache = getCachedOpStackL1DataFee(depositArgs, gasCost); + await cache.set(); + } await utils.delay(secondsPerUpdate); } }; From 129e1b8c82a5db54cf7cba6076d29fc28debf2d1 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sat, 11 Jan 2025 08:26:20 -0700 Subject: [PATCH 17/28] Test only cache gas prices --- api/cron-cache-gas-prices.ts | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/api/cron-cache-gas-prices.ts b/api/cron-cache-gas-prices.ts index 5003e67c9..b4965fd09 100644 --- a/api/cron-cache-gas-prices.ts +++ b/api/cron-cache-gas-prices.ts @@ -157,23 +157,23 @@ const handler = async ( if (chain.chainId !== CHAIN_IDs.LINEA) { updateGasPricePromise(chain.chainId); } - // 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 - ); - 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) { - outputTokensForChain.map((outputToken) => - updateGasPricePromise(chain.chainId, outputToken) - ); - } + // // 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 + // ); + // 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) { + // outputTokensForChain.map((outputToken) => + // updateGasPricePromise(chain.chainId, outputToken) + // ); + // } }); await Promise.all(gasPricePromises); From 01981bdfa9182c6bf70e1c92cad1a7b4e14cf865 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sat, 11 Jan 2025 08:41:19 -0700 Subject: [PATCH 18/28] debug --- api/cron-cache-gas-prices.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/cron-cache-gas-prices.ts b/api/cron-cache-gas-prices.ts index b4965fd09..dfa79e6db 100644 --- a/api/cron-cache-gas-prices.ts +++ b/api/cron-cache-gas-prices.ts @@ -93,7 +93,7 @@ const handler = async ( const updateGasPricePromise = async ( chainId: number, outputTokenAddress?: string - ) => { + ): Promise => { const secondsPerUpdateForChain = updateIntervalsSecPerChain[ chainId as keyof typeof updateIntervalsSecPerChain @@ -143,6 +143,7 @@ const handler = async ( const cache = getCachedOpStackL1DataFee(depositArgs, gasCost); await cache.set(); } + console.log(`updateL1DataFeePromise: updated gas price for ${chainId}`); await utils.delay(secondsPerUpdate); } }; From 39027b35a8cc667d495b7d269053b104aa0fe85c Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sat, 11 Jan 2025 10:36:41 -0700 Subject: [PATCH 19/28] Fix cron job --- api/cron-cache-gas-prices.ts | 51 ++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/api/cron-cache-gas-prices.ts b/api/cron-cache-gas-prices.ts index dfa79e6db..fcc10124d 100644 --- a/api/cron-cache-gas-prices.ts +++ b/api/cron-cache-gas-prices.ts @@ -143,7 +143,6 @@ const handler = async ( const cache = getCachedOpStackL1DataFee(depositArgs, gasCost); await cache.set(); } - console.log(`updateL1DataFeePromise: updated gas price for ${chainId}`); await utils.delay(secondsPerUpdate); } }; @@ -152,31 +151,31 @@ 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). - const gasPricePromises = mainnetChains.map(async (chain) => { - // For each chain: - // - update the destination gas price for the chain - if (chain.chainId !== CHAIN_IDs.LINEA) { - updateGasPricePromise(chain.chainId); - } - // // 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 - // ); - // 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) { - // outputTokensForChain.map((outputToken) => - // updateGasPricePromise(chain.chainId, outputToken) - // ); - // } - }); - await Promise.all(gasPricePromises); + await 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 + mainnetChains + .filter((chain) => chain.chainId !== CHAIN_IDs.LINEA) + .map((chain) => updateGasPricePromise(chain.chainId)), + availableRoutes + .filter( + ({ destinationChainId }) => destinationChainId === CHAIN_IDs.LINEA + ) + .map(({ destinationToken }) => { + updateGasPricePromise(CHAIN_IDs.LINEA, destinationToken); + }), + mainnetChains.map((chain) => { + const routesToChain = availableRoutes.filter( + ({ destinationChainId }) => destinationChainId === chain.chainId + ); + const outputTokensForChain = routesToChain.map( + ({ destinationToken }) => destinationToken + ); + outputTokensForChain.map((outputToken) => + updateL1DataFeePromise(chain.chainId, outputToken) + ); + }), + ]); logger.debug({ at: "CronCacheGasPrices", From a63bf04fd2ca373d25f3dbc27f173768cbcf618c Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sat, 11 Jan 2025 10:38:47 -0700 Subject: [PATCH 20/28] Update cron-cache-gas-prices.ts --- api/cron-cache-gas-prices.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/api/cron-cache-gas-prices.ts b/api/cron-cache-gas-prices.ts index fcc10124d..517751ce6 100644 --- a/api/cron-cache-gas-prices.ts +++ b/api/cron-cache-gas-prices.ts @@ -29,7 +29,6 @@ type Route = { // Set lower than TTL in latestGasPriceCache const updateIntervalsSecPerChain = { default: 5, - 1: 12, }; // Set lower than TTL in getCachedOpStackL1DataFee and getCachedNativeGasCost From 21616901e81ce07393b65c3c8a3b57b62b83c994 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sat, 11 Jan 2025 13:13:45 -0700 Subject: [PATCH 21/28] fix promise nesting --- api/cron-cache-gas-prices.ts | 54 +++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/api/cron-cache-gas-prices.ts b/api/cron-cache-gas-prices.ts index 517751ce6..967afa91f 100644 --- a/api/cron-cache-gas-prices.ts +++ b/api/cron-cache-gas-prices.ts @@ -93,10 +93,7 @@ const handler = async ( chainId: number, outputTokenAddress?: string ): Promise => { - const secondsPerUpdateForChain = - updateIntervalsSecPerChain[ - chainId as keyof typeof updateIntervalsSecPerChain - ] || updateIntervalsSecPerChain.default; + const secondsPerUpdateForChain = updateIntervalsSecPerChain.default; const cache = latestGasPriceCache( chainId, outputTokenAddress @@ -126,7 +123,7 @@ const handler = async ( const updateL1DataFeePromise = async ( chainId: number, outputTokenAddress: string - ) => { + ): Promise => { const secondsPerUpdate = updateL1DataFeeIntervalsSecPerChain.default; const depositArgs = getDepositArgsForChainId(chainId, outputTokenAddress); const gasCostCache = getCachedNativeGasCost(depositArgs); @@ -146,6 +143,9 @@ const handler = async ( } }; + const lineaDestinationRoutes = availableRoutes.filter( + ({ destinationChainId }) => destinationChainId === CHAIN_IDs.LINEA + ); // 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 @@ -153,27 +153,31 @@ const handler = async ( await 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 - mainnetChains - .filter((chain) => chain.chainId !== CHAIN_IDs.LINEA) - .map((chain) => updateGasPricePromise(chain.chainId)), - availableRoutes - .filter( - ({ destinationChainId }) => destinationChainId === CHAIN_IDs.LINEA + Promise.all( + mainnetChains + .filter((chain) => chain.chainId !== CHAIN_IDs.LINEA) + .map((chain) => updateGasPricePromise(chain.chainId)) + ), + Promise.all( + lineaDestinationRoutes.map(({ destinationToken }) => + updateGasPricePromise(CHAIN_IDs.LINEA, destinationToken) ) - .map(({ destinationToken }) => { - updateGasPricePromise(CHAIN_IDs.LINEA, destinationToken); - }), - mainnetChains.map((chain) => { - const routesToChain = availableRoutes.filter( - ({ destinationChainId }) => destinationChainId === chain.chainId - ); - const outputTokensForChain = routesToChain.map( - ({ destinationToken }) => destinationToken - ); - outputTokensForChain.map((outputToken) => - updateL1DataFeePromise(chain.chainId, outputToken) - ); - }), + ), + Promise.all( + mainnetChains.map((chain) => { + const routesToChain = availableRoutes.filter( + ({ destinationChainId }) => destinationChainId === chain.chainId + ); + const outputTokensForChain = routesToChain.map( + ({ destinationToken }) => destinationToken + ); + return Promise.all( + outputTokensForChain.map((outputToken) => + updateL1DataFeePromise(chain.chainId, outputToken) + ) + ); + }) + ), ]); logger.debug({ From a84521705ddcc37d34ec024e0d99a74b1f85c9ca Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sat, 11 Jan 2025 13:28:06 -0700 Subject: [PATCH 22/28] Update cron-cache-gas-prices.ts --- api/cron-cache-gas-prices.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/cron-cache-gas-prices.ts b/api/cron-cache-gas-prices.ts index 967afa91f..d4facddbd 100644 --- a/api/cron-cache-gas-prices.ts +++ b/api/cron-cache-gas-prices.ts @@ -33,7 +33,7 @@ const updateIntervalsSecPerChain = { // Set lower than TTL in getCachedOpStackL1DataFee and getCachedNativeGasCost const updateL1DataFeeIntervalsSecPerChain = { - default: 12, + default: 10, }; const maxDurationSec = 60; From 5d00e70157ace077dd8b5a4070cca1834198aa32 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sat, 11 Jan 2025 13:48:28 -0700 Subject: [PATCH 23/28] update cache times --- api/_utils.ts | 7 +++---- api/cron-cache-gas-prices.ts | 3 ++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/_utils.ts b/api/_utils.ts index f403e508a..ab17bf097 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -2023,13 +2023,12 @@ export function getCachedOpStackL1DataFee( relayerAddress: string; }> ) { - // This should be longer than the length of 1 block on Ethereum mainnet which is how often the L1 data fee should - // change since its based on the L1 base fee. However, this L1 data fee is mostly affected by the L1 base fee which - // should only change by 12.5% at most per block. + // The L1 data fee should change after each Ethereum block since its based on the L1 base fee. + // However, the L1 base fee should only change by 12.5% at most per block. // We set this higher than the secondsPerUpdate value in the cron cache gas prices job which will update this // more frequently. const ttlPerChain = { - default: 30, + default: 60, }; const cacheKey = buildInternalCacheKey( diff --git a/api/cron-cache-gas-prices.ts b/api/cron-cache-gas-prices.ts index d4facddbd..14b33f00d 100644 --- a/api/cron-cache-gas-prices.ts +++ b/api/cron-cache-gas-prices.ts @@ -31,7 +31,8 @@ const updateIntervalsSecPerChain = { default: 5, }; -// Set lower than TTL in getCachedOpStackL1DataFee and getCachedNativeGasCost +// Set lower than TTL in getCachedOpStackL1DataFee and getCachedNativeGasCost. +// 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, }; From ce45f121caba66ca2d82ba4b8a2d2b2bd4aa33de Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sat, 11 Jan 2025 13:51:49 -0700 Subject: [PATCH 24/28] Update _utils.ts --- api/_utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/_utils.ts b/api/_utils.ts index ab17bf097..e32b46097 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -1979,7 +1979,7 @@ export function getCachedNativeGasCost( // We can use a long TTL since we are fetching only the native gas cost which should rarely change. // Set this longer than the secondsPerUpdate value in the cron cache gas prices job. const ttlPerChain = { - default: 120, + default: 60, }; const cacheKey = buildInternalCacheKey( "nativeGasCost", From 035ef1b9ff0b917542c6bd6c7e5cea11d695a4f2 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sat, 11 Jan 2025 13:59:42 -0700 Subject: [PATCH 25/28] Update cron-cache-gas-prices.ts --- api/cron-cache-gas-prices.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/cron-cache-gas-prices.ts b/api/cron-cache-gas-prices.ts index 14b33f00d..4c472961c 100644 --- a/api/cron-cache-gas-prices.ts +++ b/api/cron-cache-gas-prices.ts @@ -165,14 +165,14 @@ const handler = async ( ) ), Promise.all( - mainnetChains.map((chain) => { + mainnetChains.map(async (chain) => { const routesToChain = availableRoutes.filter( ({ destinationChainId }) => destinationChainId === chain.chainId ); const outputTokensForChain = routesToChain.map( ({ destinationToken }) => destinationToken ); - return Promise.all( + await Promise.all( outputTokensForChain.map((outputToken) => updateL1DataFeePromise(chain.chainId, outputToken) ) From 7449e8c3e33f6d997af229050a44bfd12e2b2ce6 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sat, 11 Jan 2025 14:18:50 -0700 Subject: [PATCH 26/28] Add native gas cost caching Keep cache warm --- api/cron-cache-gas-prices.ts | 56 ++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/api/cron-cache-gas-prices.ts b/api/cron-cache-gas-prices.ts index 4c472961c..6e5f5cb92 100644 --- a/api/cron-cache-gas-prices.ts +++ b/api/cron-cache-gas-prices.ts @@ -31,12 +31,18 @@ const updateIntervalsSecPerChain = { default: 5, }; -// Set lower than TTL in getCachedOpStackL1DataFee and getCachedNativeGasCost. +// 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, +}; + const maxDurationSec = 60; const getDepositArgsForChainId = (chainId: number, tokenAddress: string) => { @@ -114,10 +120,9 @@ const handler = async ( }; /** - * @notice Updates the L1 data fee and L2 gas cost caches every `updateL1DataFeeIntervalsSecPerChain` seconds + * @notice Updates the L1 data fee gas cost cache 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 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. */ @@ -144,6 +149,32 @@ const handler = async ( } }; + /** + * @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); + } + }; + const lineaDestinationRoutes = availableRoutes.filter( ({ destinationChainId }) => destinationChainId === CHAIN_IDs.LINEA ); @@ -172,11 +203,18 @@ const handler = async ( const outputTokensForChain = routesToChain.map( ({ destinationToken }) => destinationToken ); - await Promise.all( - outputTokensForChain.map((outputToken) => - updateL1DataFeePromise(chain.chainId, outputToken) - ) - ); + await Promise.all([ + Promise.all( + outputTokensForChain.map((outputToken) => + updateNativeGasCostPromise(chain.chainId, outputToken) + ) + ), + Promise.all( + outputTokensForChain.map((outputToken) => + updateL1DataFeePromise(chain.chainId, outputToken) + ) + ), + ]); }) ), ]); From 5441dc6e37999b35256b213d481c1f2ec3f1bde0 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sun, 12 Jan 2025 08:17:02 -0700 Subject: [PATCH 27/28] Increase ttl of native gas cost, add gasFeeDetails to response --- api/_utils.ts | 2 +- api/limits.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/api/_utils.ts b/api/_utils.ts index e32b46097..ab17bf097 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -1979,7 +1979,7 @@ export function getCachedNativeGasCost( // We can use a long TTL since we are fetching only the native gas cost which should rarely change. // Set this longer than the secondsPerUpdate value in the cron cache gas prices job. const ttlPerChain = { - default: 60, + default: 120, }; const cacheKey = buildInternalCacheKey( "nativeGasCost", diff --git a/api/limits.ts b/api/limits.ts index fc9b8e318..4781b939f 100644 --- a/api/limits.ts +++ b/api/limits.ts @@ -417,6 +417,14 @@ const handler = async ( capitalFeeTotal: relayerFeeDetails.capitalFeeTotal, capitalFeePercent: relayerFeeDetails.capitalFeePercent, }, + gasFeeDetails: tokenGasCost + ? { + nativeGasCost: nativeGasCost!.toString(), // Should exist if tokenGasCost exists + opStackL1GasCost: opStackL1GasCost?.toString(), + gasPrice: gasPrice.toString(), + tokenGasCost: tokenGasCost.toString(), + } + : undefined, }; logger.debug({ at: "Limits", From 05c27dfea941c8442ef9383c92ded5a2427b45fc Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Sun, 12 Jan 2025 12:57:35 -0700 Subject: [PATCH 28/28] sdk --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 10de6425c..526b36b83 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@across-protocol/constants": "^3.1.24", "@across-protocol/contracts": "^3.0.19", "@across-protocol/contracts-v3.0.6": "npm:@across-protocol/contracts@3.0.6", - "@across-protocol/sdk": "^3.4.10-beta.1", + "@across-protocol/sdk": "^3.4.10", "@amplitude/analytics-browser": "^2.3.5", "@balancer-labs/sdk": "1.1.6-beta.16", "@emotion/react": "^11.13.0", diff --git a/yarn.lock b/yarn.lock index deb950404..699f87e8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -116,10 +116,10 @@ yargs "^17.7.2" zksync-web3 "^0.14.3" -"@across-protocol/sdk@^3.4.10-beta.1": - version "3.4.10-beta.1" - resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.4.10-beta.1.tgz#81d56458d97dec424e2fe51730f8ae3953c84e6c" - integrity sha512-c4tD6RQBKiok4fAK99Kh69B5+PIcAz0DLaAhfWxInRxW4kBeAKSrOtjETxuYN1wRZ89qRsGx+N+7c/wDpeQDxg== +"@across-protocol/sdk@^3.4.10": + version "3.4.10" + resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.4.10.tgz#b74c551f1625afccc10f5b792f1f61395771cf40" + integrity sha512-kM+RyTNVXzS4dl5zwJZh6es5FTouN1nECd0cckE7Z/FzEFdMmQmCn4I1Ojgt4gmE5AuUBZef4/11ZvT8uRmutQ== dependencies: "@across-protocol/across-token" "^1.0.0" "@across-protocol/constants" "^3.1.27"