Skip to content

Commit

Permalink
feat(API): Split gas component cron jobs up into different files (#1372)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholaspai authored Jan 14, 2025
1 parent 49f146c commit b42d17f
Show file tree
Hide file tree
Showing 5 changed files with 363 additions and 114 deletions.
9 changes: 9 additions & 0 deletions api/_types/utility.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,12 @@ export type TokenInfo = {
name: string;
addresses: Record<number, string>;
};

export type DepositRoute = {
originChainId: number;
originToken: string;
destinationChainId: number;
destinationToken: string;
originTokenSymbol: string;
destinationTokenSymbol: string;
};
152 changes: 152 additions & 0 deletions api/cron-cache-gas-costs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { VercelResponse } from "@vercel/node";
import { DepositRoute, TypedVercelRequest } from "./_types";
import {
HUB_POOL_CHAIN_ID,
getCachedNativeGasCost,
getLogger,
handleErrorCondition,
resolveVercelEndpoint,
} from "./_utils";
import { UnauthorizedError } from "./_errors";

import mainnetChains from "../src/data/chains_1.json";
import { utils, constants } from "@across-protocol/sdk";
import { DEFAULT_SIMULATED_RECIPIENT_ADDRESS } from "./_constants";
import axios from "axios";
import { ethers } from "ethers";

// Set lower than TTL in getCachedNativeGasCost. This should rarely change so we should just make sure
// we keep this cache warm.
const updateIntervalsSecPerChain = {
default: 30,
};

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<Record<string, never>>,
response: VercelResponse
) => {
const logger = getLogger();
logger.debug({
at: "CronCacheGasCosts",
message: "Starting cron job...",
});
try {
const authHeader = request.headers?.["authorization"];
if (
!process.env.CRON_SECRET ||
authHeader !== `Bearer ${process.env.CRON_SECRET}`
) {
throw new UnauthorizedError();
}

// Skip cron job on testnet
if (HUB_POOL_CHAIN_ID !== 1) {
logger.info({
at: "CronCacheGasCosts",
message: "Skipping cron job on testnet",
});
return;
}

const availableRoutes = (
await axios(`${resolveVercelEndpoint()}/api/available-routes`)
).data as Array<DepositRoute>;

// This marks the timestamp when the function started
const functionStart = Date.now();

const updateCounts: Record<number, Record<string, number>> = {};

/**
* @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<void> => {
updateCounts[chainId] ??= {};
updateCounts[chainId][outputTokenAddress] ??= 0;
const secondsPerUpdate = updateIntervalsSecPerChain.default;
const depositArgs = getDepositArgsForChainId(chainId, outputTokenAddress);
const cache = getCachedNativeGasCost(depositArgs);

while (true) {
const diff = Date.now() - functionStart;
// Stop after `maxDurationSec` seconds
if (diff >= maxDurationSec * 1000) {
break;
}
try {
await cache.set();
updateCounts[chainId][outputTokenAddress]++;
} catch (err) {
logger.warn({
at: "CronCacheGasCosts#updateNativeGasCostPromise",
message: `Failed to set native gas cost cache for chain ${chainId}`,
depositArgs,
error: err,
});
}
await utils.delay(secondsPerUpdate);
}
};

const getOutputTokensToChain = (chainId: number) => {
const destinationTokens = new Set<string>();
availableRoutes
.filter(({ destinationChainId }) => destinationChainId === chainId)
.forEach(({ destinationToken }) => {
if (!destinationTokens.has(destinationToken)) {
destinationTokens.add(destinationToken);
}
});
return Array.from(destinationTokens);
};

const cacheUpdatePromise = Promise.all(
mainnetChains.map(async (chain) => {
await Promise.all(
getOutputTokensToChain(chain.chainId).map((outputToken) =>
updateNativeGasCostPromise(chain.chainId, outputToken)
)
);
})
);
// There are many routes and therefore many promises to wait to resolve so we force the
// function to stop after `maxDurationSec` seconds.
await Promise.race([cacheUpdatePromise, utils.delay(maxDurationSec)]);

logger.debug({
at: "CronCacheGasCosts",
message: "Finished",
updateCounts,
});
response.status(200).json({ updateCounts });
} catch (error: unknown) {
return handleErrorCondition(
"cron-cache-gas-costs",
response,
logger,
error
);
}
};

export default handler;
144 changes: 30 additions & 114 deletions api/cron-cache-gas-prices.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { VercelResponse } from "@vercel/node";
import { TypedVercelRequest } from "./_types";
import { DepositRoute, TypedVercelRequest } from "./_types";
import {
HUB_POOL_CHAIN_ID,
getCachedNativeGasCost,
getCachedOpStackL1DataFee,
getLogger,
handleErrorCondition,
latestGasPriceCache,
Expand All @@ -17,32 +15,11 @@ 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 lower than TTL in latestGasPriceCache
const updateIntervalsSecPerChain = {
default: 5,
};

// Set lower than TTL in getCachedOpStackL1DataFee
// Set lower than the L1 block time so we can try to get as up to date L1 data fees based on L1 base fees as possible.
const updateL1DataFeeIntervalsSecPerChain = {
default: 10,
};

// Set lower than TTL in getCachedNativeGasCost. This should rarely change so we should just make sure
// we keep this cache warm.
const updateNativeGasCostIntervalsSecPerChain = {
default: 30,
};

const maxDurationSec = 60;

const getDepositArgsForChainId = (chainId: number, tokenAddress: string) => {
Expand Down Expand Up @@ -85,11 +62,13 @@ const handler = async (

const availableRoutes = (
await axios(`${resolveVercelEndpoint()}/api/available-routes`)
).data as Array<Route>;
).data as Array<DepositRoute>;

// This marks the timestamp when the function started
const functionStart = Date.now();

const updateCounts: Record<number, number> = {};

/**
* @notice Updates the gas price cache every `updateIntervalsSecPerChain` seconds up to `maxDurationSec` seconds.
* @param chainId Chain to estimate gas price for
Expand All @@ -100,84 +79,43 @@ const handler = async (
chainId: number,
outputTokenAddress?: string
): Promise<void> => {
updateCounts[chainId] ??= 0;
const secondsPerUpdateForChain = updateIntervalsSecPerChain.default;
const cache = latestGasPriceCache(
chainId,
outputTokenAddress
? getDepositArgsForChainId(chainId, outputTokenAddress)
: undefined
);

while (true) {
const diff = Date.now() - functionStart;
// Stop after `maxDurationSec` seconds
if (diff >= maxDurationSec * 1000) {
break;
}
await cache.set();
await utils.delay(secondsPerUpdateForChain);
}
};

/**
* @notice Updates the L1 data fee gas cost cache every `updateL1DataFeeIntervalsSecPerChain` seconds
* up to `maxDurationSec` seconds.
* @param chainId Chain to estimate l1 data fee for
* @param outputTokenAddress This output token will be used to construct a fill transaction to simulate
* gas costs for.
*/
const updateL1DataFeePromise = async (
chainId: number,
outputTokenAddress: string
): Promise<void> => {
const secondsPerUpdate = updateL1DataFeeIntervalsSecPerChain.default;
const depositArgs = getDepositArgsForChainId(chainId, outputTokenAddress);
const gasCostCache = getCachedNativeGasCost(depositArgs);
const depositArgs = outputTokenAddress
? getDepositArgsForChainId(chainId, outputTokenAddress)
: undefined;
const cache = latestGasPriceCache(chainId, depositArgs);

while (true) {
const diff = Date.now() - functionStart;
// Stop after `maxDurationSec` seconds
if (diff >= maxDurationSec * 1000) {
break;
}
const gasCost = await gasCostCache.get();
if (utils.chainIsOPStack(chainId)) {
const cache = getCachedOpStackL1DataFee(depositArgs, gasCost);
try {
await cache.set();
updateCounts[chainId]++;
} catch (err) {
logger.warn({
at: "CronCacheGasPrices#updateGasPricePromise",
message: `Failed to set gas price cache for chain ${chainId}`,
depositArgs,
error: err,
});
}
await utils.delay(secondsPerUpdate);
await utils.delay(secondsPerUpdateForChain);
}
};

/**
* @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<void> => {
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 = () => {
const routes = new Set<string>();
availableRoutes
.filter(
({ destinationChainId }) => destinationChainId === CHAIN_IDs.LINEA
)
.forEach(({ destinationToken }) => routes.add(destinationToken));
return Array.from(routes);
};

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
Expand All @@ -191,40 +129,18 @@ const handler = async (
.map((chain) => updateGasPricePromise(chain.chainId))
),
Promise.all(
lineaDestinationRoutes.map(({ destinationToken }) =>
lineaDestinationRoutes().map((destinationToken) =>
updateGasPricePromise(CHAIN_IDs.LINEA, destinationToken)
)
),
Promise.all(
mainnetChains.map(async (chain) => {
const routesToChain = availableRoutes.filter(
({ destinationChainId }) => destinationChainId === chain.chainId
);
const outputTokensForChain = routesToChain.map(
({ destinationToken }) => destinationToken
);
await Promise.all([
Promise.all(
outputTokensForChain.map((outputToken) =>
updateNativeGasCostPromise(chain.chainId, outputToken)
)
),
Promise.all(
outputTokensForChain.map((outputToken) =>
updateL1DataFeePromise(chain.chainId, outputToken)
)
),
]);
})
),
]);

logger.debug({
at: "CronCacheGasPrices",
message: "Finished",
updateCounts,
});
response.status(200);
response.send("OK");
response.status(200).json({ updateCounts });
} catch (error: unknown) {
return handleErrorCondition(
"cron-cache-gas-prices",
Expand Down
Loading

0 comments on commit b42d17f

Please sign in to comment.