Skip to content
14 changes: 14 additions & 0 deletions api/_cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(key: string): Promise<T | null> {
const value = await super.get<T>(key);
if (typeof value === "string") {
return ('"' + value + '"') as T;
}
return value;
}
}

export const providerRedisCache = new ProviderRedisCache();

export function buildCacheKey(
prefix: string,
...args: (string | number)[]
Expand Down
20 changes: 5 additions & 15 deletions api/_dexes/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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 ??
Expand All @@ -460,7 +458,10 @@ export async function extractDepositDataStruct(
exclusiveRelayer:
crossSwapQuotes.bridgeQuote.suggestedFees.exclusiveRelayer,
quoteTimestamp: crossSwapQuotes.bridgeQuote.suggestedFees.timestamp,
fillDeadline: await getFillDeadline(spokePool),
fillDeadline: getFillDeadline(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 💪

destinationChainId,
crossSwapQuotes.bridgeQuote.suggestedFees.timestamp
),
exclusivityDeadline:
crossSwapQuotes.bridgeQuote.suggestedFees.exclusivityDeadline,
exclusivityParameter:
Expand Down Expand Up @@ -517,17 +518,6 @@ export async function extractSwapAndDepositDataStruct(
};
}

async function getFillDeadline(spokePool: SpokePool): Promise<number> {
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,
Expand Down
33 changes: 8 additions & 25 deletions api/_fill-deadline.ts
Original file line number Diff line number Diff line change
@@ -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 = (
Expand All @@ -14,26 +10,13 @@ function getFillDeadlineBuffer(chainId: number) {
return Number(bufferFromEnv ?? DEFAULT_FILL_DEADLINE_BUFFER_SECONDS);
}

async function getCurrentTimeSvm(chainId: number): Promise<number> {
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<number> {
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;
}
71 changes: 58 additions & 13 deletions api/_providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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<string, providers.StaticJsonRpcProvider> =
{};
Expand Down Expand Up @@ -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<number, number> = {
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.
Expand Down Expand Up @@ -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({
Expand All @@ -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
);
}

Expand Down
24 changes: 24 additions & 0 deletions api/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2685,3 +2685,27 @@ export function addTimeoutToPromise<T>(
});
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);
}
3 changes: 2 additions & 1 deletion api/build-deposit-tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
22 changes: 14 additions & 8 deletions api/suggested-fees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
parseL1TokenConfigSafe,
getL1TokenConfigCache,
ConvertDecimals,
computeUtilizationPostRelay,
PooledToken,
} from "./_utils";
import { selectExclusiveRelayer } from "./_exclusivity";
import {
Expand Down Expand Up @@ -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,
Expand All @@ -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({
Expand All @@ -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));
Expand Down