From 5c79477fbb100c36ba7f2a8268e541e422cd2487 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Wed, 30 Jul 2025 20:51:53 -0400 Subject: [PATCH 01/11] feat: add RPC caching to API Signed-off-by: Matt Rice --- api/_utils.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/api/_utils.ts b/api/_utils.ts index 271299f61..88d54f8c2 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -74,6 +74,7 @@ import { buildInternalCacheKey, getCachedValue, makeCacheGetterAndSetter, + redisCache, } from "./_cache"; import { MissingParamError, @@ -1290,7 +1291,8 @@ function getProviderFromConfigJson( 0.5, // delay 5, // max. concurrency "RPC_PROVIDER", // cache namespace - 0 // disable RPC calls logging + 0, // disable RPC calls logging + redisCache ); } @@ -1300,7 +1302,8 @@ function getProviderFromConfigJson( 3, // max. concurrency used in `SpeedProvider` 5, // max. concurrency used in `RateLimitedProvider` "RPC_PROVIDER", // cache namespace - 1 // disable RPC calls logging + 0, // disable RPC calls logging + redisCache ); } From cc8714f0161c289237686ea2eed82cdd5666aad9 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Wed, 30 Jul 2025 22:01:55 -0400 Subject: [PATCH 02/11] feat: reduce RPC usage in API Signed-off-by: Matt Rice --- api/_fill-deadline.ts | 12 ++++++++---- api/_utils.ts | 24 ++++++++++++++++++++++++ api/suggested-fees.ts | 21 +++++++++++++-------- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/api/_fill-deadline.ts b/api/_fill-deadline.ts index 2016e28b4..9d05bb425 100644 --- a/api/_fill-deadline.ts +++ b/api/_fill-deadline.ts @@ -11,9 +11,13 @@ function getFillDeadlineBuffer(chainId: number) { return Number(bufferFromEnv ?? DEFAULT_FILL_DEADLINE_BUFFER_SECONDS); } -export async function getFillDeadline(chainId: number): Promise { +export async function getFillDeadline( + chainId: number, + quoteTimestamp: number +): Promise { const fillDeadlineBuffer = getFillDeadlineBuffer(chainId); - const spokePool = getSpokePool(chainId); - const currentTime = await spokePool.callStatic.getCurrentTime(); - return Number(currentTime) + fillDeadlineBuffer; + // Quote timestamp cannot be in the future or more than an hour old, so this is safe (i.e. cannot + // cause the contract to revert) as long as the fill deadline buffer is at least 1 hour smaller + // than the maximum buffer length allowed by the contract. + return Number(quoteTimestamp) + fillDeadlineBuffer; } diff --git a/api/_utils.ts b/api/_utils.ts index 88d54f8c2..9976cbaf0 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -2876,3 +2876,27 @@ export function getRejectedReasons( return []; } } + +type PooledToken = { + lpToken: string; + isEnabled: boolean; + lastLpFeeUpdate: BigNumber; + utilizedReserves: BigNumber; + liquidReserves: BigNumber; + undistributedLpFees: BigNumber; +}; + +// This logic is directly ported from the HubPool smart contract function by the same name. +export function computeUtilizationPostRelay( + pooledToken: PooledToken, + amount: BigNumber +) { + const flooredUtilizedReserves = pooledToken.utilizedReserves.gt(0) + ? pooledToken.utilizedReserves + : BigNumber.from(0); + const numerator = amount.add(flooredUtilizedReserves); + const denominator = pooledToken.liquidReserves.add(flooredUtilizedReserves); + + if (denominator.isZero()) return sdk.utils.fixedPointAdjustment; + return numerator.mul(sdk.utils.fixedPointAdjustment).div(denominator); +} diff --git a/api/suggested-fees.ts b/api/suggested-fees.ts index fefb377b7..bf7519613 100644 --- a/api/suggested-fees.ts +++ b/api/suggested-fees.ts @@ -31,6 +31,7 @@ import { parseL1TokenConfigSafe, getL1TokenConfigCache, ConvertDecimals, + computeUtilizationPostRelay, } from "./_utils"; import { selectExclusiveRelayer } from "./_exclusivity"; import { @@ -208,11 +209,8 @@ const handler = async ( }, { contract: hubPool, - functionName: "liquidityUtilizationPostRelay", - args: [ - l1Token.address, - ConvertDecimals(inputToken.decimals, l1Token.decimals)(amount), - ], + functionName: "pooledTokens", + args: [l1Token.address], }, { contract: hubPool, @@ -226,10 +224,9 @@ const handler = async ( ]; const [ - [currentUt, nextUt, _quoteTimestamp, rawL1TokenConfig], + [currentUt, pooledTokens, _quoteTimestamp, rawL1TokenConfig], tokenPriceUsd, limits, - fillDeadline, ] = await Promise.all([ callViaMulticall3(provider, multiCalls, { blockTag: quoteBlockNumber }), getCachedTokenPrice(l1Token.address, "usd"), @@ -247,12 +244,20 @@ const handler = async ( depositWithMessage ? message : undefined, allowUnmatchedDecimals ), - getFillDeadline(destinationChainId), ]); + + const nextUt = computeUtilizationPostRelay( + pooledTokens[l1Token.address], + ConvertDecimals(inputToken.decimals, l1Token.decimals)(amount) + ); + const { maxDeposit, maxDepositInstant, minDeposit, relayerFeeDetails } = limits; + const quoteTimestamp = parseInt(_quoteTimestamp.toString()); + const fillDeadline = getFillDeadline(destinationChainId, quoteTimestamp); + const amountInUsd = amount .mul(parseUnits(tokenPriceUsd.toString(), 18)) .div(parseUnits("1", inputToken.decimals)); From db203c9c7f56fa72270dce2b540929b27b7115f1 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Wed, 30 Jul 2025 22:06:34 -0400 Subject: [PATCH 03/11] fix Signed-off-by: Matt Rice --- api/_fill-deadline.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/_fill-deadline.ts b/api/_fill-deadline.ts index 9d05bb425..35423fa14 100644 --- a/api/_fill-deadline.ts +++ b/api/_fill-deadline.ts @@ -11,10 +11,10 @@ function getFillDeadlineBuffer(chainId: number) { return Number(bufferFromEnv ?? DEFAULT_FILL_DEADLINE_BUFFER_SECONDS); } -export async function getFillDeadline( +export function getFillDeadline( chainId: number, quoteTimestamp: number -): Promise { +): number { const fillDeadlineBuffer = getFillDeadlineBuffer(chainId); // Quote timestamp cannot be in the future or more than an hour old, so this is safe (i.e. cannot // cause the contract to revert) as long as the fill deadline buffer is at least 1 hour smaller From 112d3da9e64036df6aea5dd54dac8198131d280a Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Wed, 30 Jul 2025 22:26:09 -0400 Subject: [PATCH 04/11] fix Signed-off-by: Matt Rice --- api/_dexes/utils.ts | 17 +++++------------ api/build-deposit-tx.ts | 3 ++- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/api/_dexes/utils.ts b/api/_dexes/utils.ts index f59d2c9de..d61ca9e4c 100644 --- a/api/_dexes/utils.ts +++ b/api/_dexes/utils.ts @@ -38,6 +38,7 @@ import { getSwapProxyAddress, } from "../_spoke-pool-periphery"; import { getUniversalSwapAndBridgeAddress } from "../_swap-and-bridge"; +import { getFillDeadline } from "../_fill-deadline"; import axios, { AxiosRequestHeaders } from "axios"; export type CrossSwapType = @@ -355,7 +356,10 @@ export async function extractDepositDataStruct( exclusiveRelayer: crossSwapQuotes.bridgeQuote.suggestedFees.exclusiveRelayer, quoteTimestamp: crossSwapQuotes.bridgeQuote.suggestedFees.timestamp, - fillDeadline: await getFillDeadline(spokePool), + fillDeadline: getFillDeadline( + destinationChainId, + crossSwapQuotes.bridgeQuote.suggestedFees.timestamp + ), exclusivityDeadline: crossSwapQuotes.bridgeQuote.suggestedFees.exclusivityDeadline, exclusivityParameter: @@ -412,17 +416,6 @@ export async function extractSwapAndDepositDataStruct( }; } -async function getFillDeadline(spokePool: SpokePool): Promise { - const calls = [ - spokePool.interface.encodeFunctionData("getCurrentTime"), - spokePool.interface.encodeFunctionData("fillDeadlineBuffer"), - ]; - - const [currentTime, fillDeadlineBuffer] = - await spokePool.callStatic.multicall(calls); - return Number(currentTime) + Number(fillDeadlineBuffer); -} - export function getQuoteFetchStrategies( chainId: number, tokenInSymbol: string, diff --git a/api/build-deposit-tx.ts b/api/build-deposit-tx.ts index 86fca9ecc..781f8da63 100644 --- a/api/build-deposit-tx.ts +++ b/api/build-deposit-tx.ts @@ -81,7 +81,8 @@ const handler = async ( const originChainId = parseInt(originChainIdInput); const isNative = isNativeBoolStr === "true"; const fillDeadline = - _fillDeadline ?? (await getFillDeadline(destinationChainId)); + _fillDeadline ?? + getFillDeadline(destinationChainId, Number(quoteTimestamp)); if (originChainId === destinationChainId) { throw new InvalidParamError({ From 3c567fc8d21ddac5e6382bcc58af98fc7b8eae5b Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Wed, 30 Jul 2025 22:32:51 -0400 Subject: [PATCH 05/11] WIP Signed-off-by: Matt Rice --- api/_fill-deadline.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/_fill-deadline.ts b/api/_fill-deadline.ts index 35423fa14..fbfa6daf0 100644 --- a/api/_fill-deadline.ts +++ b/api/_fill-deadline.ts @@ -17,7 +17,7 @@ export function getFillDeadline( ): number { const fillDeadlineBuffer = getFillDeadlineBuffer(chainId); // Quote timestamp cannot be in the future or more than an hour old, so this is safe (i.e. cannot - // cause the contract to revert) as long as the fill deadline buffer is at least 1 hour smaller - // than the maximum buffer length allowed by the contract. + // cause the contract to revert or an unfillable deposit) as long as the fill deadline buffer is + // greater than 1 hour (+ some cushion for fill time). return Number(quoteTimestamp) + fillDeadlineBuffer; } From 7d61d96b0822b21fbc239c48bb5f4bbb3809c707 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Wed, 30 Jul 2025 22:59:19 -0400 Subject: [PATCH 06/11] WIP Signed-off-by: Matt Rice --- api/_utils.ts | 2 +- api/suggested-fees.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/api/_utils.ts b/api/_utils.ts index 9976cbaf0..13643ef67 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -2877,7 +2877,7 @@ export function getRejectedReasons( } } -type PooledToken = { +export type PooledToken = { lpToken: string; isEnabled: boolean; lastLpFeeUpdate: BigNumber; diff --git a/api/suggested-fees.ts b/api/suggested-fees.ts index bf7519613..fc71c9ced 100644 --- a/api/suggested-fees.ts +++ b/api/suggested-fees.ts @@ -32,6 +32,7 @@ import { getL1TokenConfigCache, ConvertDecimals, computeUtilizationPostRelay, + PooledToken, } from "./_utils"; import { selectExclusiveRelayer } from "./_exclusivity"; import { @@ -224,7 +225,7 @@ const handler = async ( ]; const [ - [currentUt, pooledTokens, _quoteTimestamp, rawL1TokenConfig], + [currentUt, pooledToken, _quoteTimestamp, rawL1TokenConfig], tokenPriceUsd, limits, ] = await Promise.all([ @@ -247,7 +248,7 @@ const handler = async ( ]); const nextUt = computeUtilizationPostRelay( - pooledTokens[l1Token.address], + pooledToken as unknown as PooledToken, // Cast is required because ethers response type is generic. ConvertDecimals(inputToken.decimals, l1Token.decimals)(amount) ); From 31669cb8ef62908e5f0e86294dd266e0cb554f56 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Wed, 30 Jul 2025 23:48:57 -0400 Subject: [PATCH 07/11] WIP Signed-off-by: Matt Rice --- api/_dexes/utils.ts | 3 --- api/_fill-deadline.ts | 1 - 2 files changed, 4 deletions(-) diff --git a/api/_dexes/utils.ts b/api/_dexes/utils.ts index d61ca9e4c..1d194a3ff 100644 --- a/api/_dexes/utils.ts +++ b/api/_dexes/utils.ts @@ -1,6 +1,5 @@ import { BigNumber, BigNumberish, constants } from "ethers"; import { utils } from "@across-protocol/sdk"; -import { SpokePool } from "@across-protocol/contracts/dist/typechain"; import { CHAIN_IDs } from "@across-protocol/constants"; import { getSwapRouter02Strategy } from "./uniswap/swap-router-02"; @@ -334,9 +333,7 @@ export async function extractDepositDataStruct( recipient: string; } ) { - const originChainId = crossSwapQuotes.crossSwap.inputToken.chainId; const destinationChainId = crossSwapQuotes.crossSwap.outputToken.chainId; - const spokePool = getSpokePool(originChainId); const message = crossSwapQuotes.bridgeQuote.message || "0x"; const refundAddress = crossSwapQuotes.crossSwap.refundAddress ?? diff --git a/api/_fill-deadline.ts b/api/_fill-deadline.ts index fbfa6daf0..7c3716444 100644 --- a/api/_fill-deadline.ts +++ b/api/_fill-deadline.ts @@ -1,5 +1,4 @@ import { DEFAULT_FILL_DEADLINE_BUFFER_SECONDS } from "./_constants"; -import { getSpokePool } from "./_utils"; function getFillDeadlineBuffer(chainId: number) { const bufferFromEnv = ( From 7bf236da4c5dd7713a68733ad89b9e04c4fc815f Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Tue, 5 Aug 2025 23:47:06 -0400 Subject: [PATCH 08/11] WIP Signed-off-by: Matt Rice --- api/_cache.ts | 14 ++++++++++++++ api/_utils.ts | 29 ++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/api/_cache.ts b/api/_cache.ts index 259a1a830..1a994eaa4 100644 --- a/api/_cache.ts +++ b/api/_cache.ts @@ -88,6 +88,20 @@ export class RedisCache implements interfaces.CachingMechanismInterface { export const redisCache = new RedisCache(); +// For some reason, the upstash client seems to strip the quote characters from the value. +// This is a workaround to add the quote characters back to the value so the provider responses won't fail to parse. +export class ProviderRedisCache extends RedisCache { + async get(key: string): Promise { + const value = await super.get(key); + if (typeof value === "string") { + return ('"' + value + '"') as T; + } + return value; + } +} + +export const providerRedisCache = new ProviderRedisCache(); + export function buildCacheKey( prefix: string, ...args: (string | number)[] diff --git a/api/_utils.ts b/api/_utils.ts index 13643ef67..bd996690b 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -74,7 +74,7 @@ import { buildInternalCacheKey, getCachedValue, makeCacheGetterAndSetter, - redisCache, + providerRedisCache, } from "./_cache"; import { MissingParamError, @@ -1261,6 +1261,23 @@ export const getProvider = ( return providerCache[cacheKey]; }; +// For most chains, we can cache immediately. +const DEFAULT_CACHE_BLOCK_DISTANCE = 0; + +// For chains that can reorg (mainnet and polygon), establish a buffer beyond which reorgs are rare. +const CUSTOM_CACHE_BLOCK_DISTANCE: Record = { + 1: 2, + 137: 10, +}; + +function getCacheBlockDistance(chainId: number) { + const cacheBlockDistance = CUSTOM_CACHE_BLOCK_DISTANCE[chainId]; + if (!cacheBlockDistance) { + return DEFAULT_CACHE_BLOCK_DISTANCE; + } + return cacheBlockDistance; +} + /** * Resolves a provider from the `rpc-providers.json` configuration file. */ @@ -1290,9 +1307,10 @@ function getProviderFromConfigJson( 3, // retries 0.5, // delay 5, // max. concurrency - "RPC_PROVIDER", // cache namespace + `RPC_PROVIDER_${process.env.RPC_CACHE_NAMESPACE}`, // cache namespace 0, // disable RPC calls logging - redisCache + providerRedisCache, + getCacheBlockDistance(chainId) ); } @@ -1301,9 +1319,10 @@ function getProviderFromConfigJson( chainId, 3, // max. concurrency used in `SpeedProvider` 5, // max. concurrency used in `RateLimitedProvider` - "RPC_PROVIDER", // cache namespace + `RPC_PROVIDER_${process.env.RPC_CACHE_NAMESPACE}`, // cache namespace 0, // disable RPC calls logging - redisCache + providerRedisCache, + getCacheBlockDistance(chainId) ); } From cf54c999a0ad85f59d046ba80dbddcbb52347ffd Mon Sep 17 00:00:00 2001 From: Melisa Guevara Date: Wed, 3 Sep 2025 16:00:33 -0300 Subject: [PATCH 09/11] remove sdk import --- api/_fill-deadline.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/_fill-deadline.ts b/api/_fill-deadline.ts index a427b6bf7..7c3716444 100644 --- a/api/_fill-deadline.ts +++ b/api/_fill-deadline.ts @@ -1,5 +1,3 @@ -import * as sdk from "@across-protocol/sdk"; - import { DEFAULT_FILL_DEADLINE_BUFFER_SECONDS } from "./_constants"; function getFillDeadlineBuffer(chainId: number) { From 5e165e6ea85dd2aad654da733f92c66e2c30d539 Mon Sep 17 00:00:00 2001 From: Melisa Guevara Date: Wed, 3 Sep 2025 16:01:58 -0300 Subject: [PATCH 10/11] remove import --- api/_utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/api/_utils.ts b/api/_utils.ts index 5b147cc37..db45c913a 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -55,7 +55,6 @@ import { buildInternalCacheKey, getCachedValue, makeCacheGetterAndSetter, - providerRedisCache, } from "./_cache"; import { MissingParamError, From 09e134af82a480ef8e708412ea80ef996e0a41ae Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Fri, 5 Sep 2025 17:51:52 +0200 Subject: [PATCH 11/11] feat: env var for disabling provider caching (#1808) --- api/_providers.ts | 57 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/api/_providers.ts b/api/_providers.ts index 1bb7e9920..ec7ccdda0 100644 --- a/api/_providers.ts +++ b/api/_providers.ts @@ -12,7 +12,8 @@ import { createSolanaRpc, MainnetUrl } from "@solana/kit"; export type RpcProviderName = keyof typeof rpcProvidersJson.providers.urls; -const { RPC_HEADERS } = getEnvs(); +const { RPC_HEADERS, RPC_CACHE_NAMESPACE, DISABLE_PROVIDER_CACHING } = + getEnvs(); export const providerCache: Record = {}; @@ -121,6 +122,20 @@ function getProviderFromConfigJson( const chainId = Number(_chainId); const urls = getRpcUrlsFromConfigJson(chainId); const headers = getProviderHeaders(chainId); + const providerConstructorParams: ConstructorParameters< + typeof sdk.providers.RetryProvider + >[0] = urls.map((url) => [{ url, headers, errorPassThrough: true }, chainId]); + + const pctRpcCallsLogged = 0; // disable RPC calls logging + + const cacheNamespace = RPC_CACHE_NAMESPACE + ? `RPC_PROVIDER_${RPC_CACHE_NAMESPACE}` + : "RPC_PROVIDER"; + const disableProviderCaching = DISABLE_PROVIDER_CACHING === "true"; + const providerCache = disableProviderCaching ? undefined : providerRedisCache; + const providerCacheBlockDistance = disableProviderCaching + ? undefined + : getCacheBlockDistance(chainId); if (urls.length === 0) { getLogger().warn({ @@ -131,29 +146,37 @@ function getProviderFromConfigJson( } if (!opts.useSpeedProvider) { + const quorum = 1; // quorum can be 1 in the context of the API + const retries = 3; + const delay = 0.5; + const maxConcurrency = 5; + return new sdk.providers.RetryProvider( - urls.map((url) => [{ url, headers, errorPassThrough: true }, chainId]), + providerConstructorParams, chainId, - 1, // quorum can be 1 in the context of the API - 3, // retries - 0.5, // delay - 5, // max. concurrency - `RPC_PROVIDER_${process.env.RPC_CACHE_NAMESPACE}`, // cache namespace - 0, // disable RPC calls logging - providerRedisCache, - getCacheBlockDistance(chainId) + quorum, + retries, + delay, + maxConcurrency, + cacheNamespace, + pctRpcCallsLogged, + providerCache, + providerCacheBlockDistance ); } + const maxConcurrencySpeed = 3; + const maxConcurrencyRateLimit = 5; + return new sdk.providers.SpeedProvider( - urls.map((url) => [{ url, headers, errorPassThrough: true }, chainId]), + providerConstructorParams, chainId, - 3, // max. concurrency used in `SpeedProvider` - 5, // max. concurrency used in `RateLimitedProvider` - `RPC_PROVIDER_${process.env.RPC_CACHE_NAMESPACE}`, // cache namespace - 0, // disable RPC calls logging - providerRedisCache, - getCacheBlockDistance(chainId) + maxConcurrencySpeed, + maxConcurrencyRateLimit, + cacheNamespace, + pctRpcCallsLogged, + providerCache, + providerCacheBlockDistance ); }