diff --git a/api/_cache.ts b/api/_cache.ts index 8bf0d779e..f94c2cc82 100644 --- a/api/_cache.ts +++ b/api/_cache.ts @@ -89,6 +89,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/_dexes/utils.ts b/api/_dexes/utils.ts index 59afddfea..9b82dcb10 100644 --- a/api/_dexes/utils.ts +++ b/api/_dexes/utils.ts @@ -5,7 +5,6 @@ import { utils as ethersUtils, } 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"; @@ -42,6 +41,7 @@ import { getSwapProxyAddress, } from "../_spoke-pool-periphery"; import { getUniversalSwapAndBridgeAddress } from "../_swap-and-bridge"; +import { getFillDeadline } from "../_fill-deadline"; import { encodeActionCalls } from "../swap/_utils"; export type CrossSwapType = @@ -438,9 +438,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 ?? @@ -460,7 +458,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: @@ -517,17 +518,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/_fill-deadline.ts b/api/_fill-deadline.ts index 53b76b465..7c3716444 100644 --- a/api/_fill-deadline.ts +++ b/api/_fill-deadline.ts @@ -1,8 +1,4 @@ -import * as sdk from "@across-protocol/sdk"; - import { DEFAULT_FILL_DEADLINE_BUFFER_SECONDS } from "./_constants"; -import { getSpokePool } from "./_spoke-pool"; -import { getSVMRpc } from "./_providers"; function getFillDeadlineBuffer(chainId: number) { const bufferFromEnv = ( @@ -14,26 +10,13 @@ function getFillDeadlineBuffer(chainId: number) { return Number(bufferFromEnv ?? DEFAULT_FILL_DEADLINE_BUFFER_SECONDS); } -async function getCurrentTimeSvm(chainId: number): Promise { - const rpc = getSVMRpc(chainId); - const timestamp = await rpc - .getSlot({ - commitment: "confirmed", - }) - .send() - .then((slot) => rpc.getBlockTime(slot).send()); - return Number(timestamp); -} - -export async function getFillDeadline(chainId: number): Promise { +export function getFillDeadline( + chainId: number, + quoteTimestamp: number +): number { const fillDeadlineBuffer = getFillDeadlineBuffer(chainId); - let currentTime: number; - - if (sdk.utils.chainIsSvm(chainId)) { - currentTime = await getCurrentTimeSvm(chainId); - } else { - const spokePool = getSpokePool(chainId); - currentTime = (await spokePool.callStatic.getCurrentTime()).toNumber(); - } - return 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 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; } diff --git a/api/_providers.ts b/api/_providers.ts index 09964da7b..f0a19e3d3 100644 --- a/api/_providers.ts +++ b/api/_providers.ts @@ -2,6 +2,7 @@ import * as sdk from "@across-protocol/sdk"; import { PUBLIC_NETWORKS } from "@across-protocol/constants"; import { ethers, providers } from "ethers"; +import { providerRedisCache } from "./_cache"; import { getEnvs } from "./_env"; import { getLogger } from "./_logger"; @@ -11,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 = {}; @@ -72,6 +74,23 @@ export function 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 fixed Static RPC provider if an override url has been specified. * @returns A provider or undefined if an override was not specified. @@ -103,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({ @@ -113,25 +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", // cache namespace - 0 // disable RPC calls logging + quorum, + retries, + delay, + maxConcurrency, + cacheNamespace, + pctRpcCallsLogged, + providerCache, + providerCacheBlockDistance ); } + const maxConcurrencySpeed = 2; + const maxConcurrencyRateLimit = 5; + return new sdk.providers.SpeedProvider( - urls.map((url) => [{ url, headers, errorPassThrough: true }, chainId]), + providerConstructorParams, chainId, - 2, // max. concurrency used in `SpeedProvider` - 5, // max. concurrency used in `RateLimitedProvider` - "RPC_PROVIDER", // cache namespace - 1 // disable RPC calls logging + maxConcurrencySpeed, + maxConcurrencyRateLimit, + cacheNamespace, + pctRpcCallsLogged, + providerCache, + providerCacheBlockDistance ); } diff --git a/api/_utils.ts b/api/_utils.ts index 0b6221cf4..8fb0007d0 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -2685,3 +2685,27 @@ export function addTimeoutToPromise( }); return Promise.race([promise, timeout]); } + +export 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/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({ diff --git a/api/suggested-fees.ts b/api/suggested-fees.ts index 4d8867d50..9d9043281 100644 --- a/api/suggested-fees.ts +++ b/api/suggested-fees.ts @@ -27,6 +27,8 @@ import { parseL1TokenConfigSafe, getL1TokenConfigCache, ConvertDecimals, + computeUtilizationPostRelay, + PooledToken, } from "./_utils"; import { selectExclusiveRelayer } from "./_exclusivity"; import { @@ -231,11 +233,8 @@ const handler = async ( }, { contract: hubPool, - functionName: "liquidityUtilizationPostRelay", - args: [ - l1Token.address, - ConvertDecimals(inputToken.decimals, l1Token.decimals)(amount), - ], + functionName: "pooledTokens", + args: [l1Token.address], }, { contract: hubPool, @@ -249,10 +248,9 @@ const handler = async ( ]; const [ - [currentUt, nextUt, _quoteTimestamp, rawL1TokenConfig], + [currentUt, pooledToken, _quoteTimestamp, rawL1TokenConfig], tokenPriceUsd, limits, - fillDeadline, ] = await Promise.all([ callViaMulticall3(provider, multiCalls, { blockTag: quoteBlockNumber }), getCachedTokenPrice({ @@ -275,12 +273,20 @@ const handler = async ( depositWithMessage ? message : undefined, allowUnmatchedDecimals ), - getFillDeadline(destinationChainId), ]); + + const nextUt = computeUtilizationPostRelay( + pooledToken as unknown as PooledToken, // Cast is required because ethers response type is generic. + 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));