Skip to content

Commit

Permalink
Add new cron
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholaspai committed Jan 14, 2025
1 parent 970eb3c commit e1ecc76
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 79 deletions.
96 changes: 17 additions & 79 deletions api/cron-cache-gas-costs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { TypedVercelRequest } from "./_types";
import {
HUB_POOL_CHAIN_ID,
getCachedNativeGasCost,
getCachedOpStackL1DataFee,
getLogger,
handleErrorCondition,
resolveVercelEndpoint,
Expand All @@ -25,16 +24,10 @@ type Route = {
destinationTokenSymbol: string;
};

// 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: 20,
const updateIntervalsSecPerChain = {
default: 30,
};

const maxDurationSec = 60;
Expand All @@ -56,7 +49,7 @@ const handler = async (
) => {
const logger = getLogger();
logger.debug({
at: "CronCacheGasPrices",
at: "CronCacheGasCosts",
message: "Starting cron job...",
});
try {
Expand All @@ -71,7 +64,7 @@ const handler = async (
// Skip cron job on testnet
if (HUB_POOL_CHAIN_ID !== 1) {
logger.info({
at: "CronCacheGasPrices",
at: "CronCacheGasCosts",
message: "Skipping cron job on testnet",
});
return;
Expand All @@ -84,46 +77,6 @@ const handler = async (
// This marks the timestamp when the function started
const functionStart = Date.now();

/**
* @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);

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();
} catch (err) {
logger.warn({
at: "CronCacheGasPrices#updateL1DataFeePromise",
message: `Failed to set l1 data fee cache for chain ${chainId}`,
depositArgs,
gasCost,
error: err,
});
}
}
await utils.delay(secondsPerUpdate);
}
};

/**
* @notice Updates the native gas cost cache every `updateNativeGasCostIntervalsSecPerChain` seconds
* up to `maxDurationSec` seconds.
Expand All @@ -135,7 +88,7 @@ const handler = async (
chainId: number,
outputTokenAddress: string
): Promise<void> => {
const secondsPerUpdate = updateNativeGasCostIntervalsSecPerChain.default;
const secondsPerUpdate = updateIntervalsSecPerChain.default;
const depositArgs = getDepositArgsForChainId(chainId, outputTokenAddress);
const cache = getCachedNativeGasCost(depositArgs);

Expand All @@ -149,7 +102,7 @@ const handler = async (
await cache.set();
} catch (err) {
logger.warn({
at: "CronCacheGasPrices#updateNativeGasCostPromise",
at: "CronCacheGasCosts#updateNativeGasCostPromise",
message: `Failed to set native gas cost cache for chain ${chainId}`,
depositArgs,
error: err,
Expand All @@ -164,43 +117,28 @@ const handler = async (
.filter(({ destinationChainId }) => destinationChainId === chainId)
.map(({ destinationToken }) => destinationToken);

// 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
// `secondsPerUpdateForChain` seconds and stop after `maxDurationSec` seconds (1 minute).
const cacheUpdatePromise = Promise.all([
Promise.all(
mainnetChains.map(async (chain) => {
await Promise.all(
getOutputTokensToChain(chain.chainId).map((outputToken) =>
updateNativeGasCostPromise(chain.chainId, outputToken)
)
);
})
),
Promise.all(
mainnetChains.map(async (chain) => {
await Promise.all(
getOutputTokensToChain(chain.chainId).map((outputToken) =>
updateL1DataFeePromise(chain.chainId, outputToken)
)
);
})
),
]);
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: "CronCacheGasPrices",
at: "CronCacheGasCosts",
message: "Finished",
});
response.status(200);
response.send("OK");
} catch (error: unknown) {
return handleErrorCondition(
"cron-cache-gas-prices",
"cron-cache-gas-costs",
response,
logger,
error
Expand Down
155 changes: 155 additions & 0 deletions api/cron-cache-l1-data-fee.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { VercelResponse } from "@vercel/node";
import { TypedVercelRequest } from "./_types";
import {
HUB_POOL_CHAIN_ID,
getCachedNativeGasCost,
getCachedOpStackL1DataFee,
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";

type Route = {
originChainId: number;
originToken: string;
destinationChainId: number;
destinationToken: string;
originTokenSymbol: string;
destinationTokenSymbol: string;
};

// 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 updateIntervalsSecPerChain = {
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<Record<string, never>>,
response: VercelResponse
) => {
const logger = getLogger();
logger.debug({
at: "CronCacheL1DataFee",
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: "CronCacheL1DataFee",
message: "Skipping cron job on testnet",
});
return;
}

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

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

/**
* @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 = updateIntervalsSecPerChain.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();
if (utils.chainIsOPStack(chainId)) {
const cache = getCachedOpStackL1DataFee(depositArgs, gasCost);
try {
await cache.set();
} catch (err) {
logger.warn({
at: "CronCacheL1DataFee#updateL1DataFeePromise",
message: `Failed to set l1 data fee cache for chain ${chainId}`,
depositArgs,
gasCost,
error: err,
});
}
}
await utils.delay(secondsPerUpdate);
}
};

const getOutputTokensToChain = (chainId: number) =>
availableRoutes
.filter(({ destinationChainId }) => destinationChainId === chainId)
.map(({ destinationToken }) => destinationToken);

const cacheUpdatePromise = Promise.all(
mainnetChains.map(async (chain) => {
await Promise.all(
getOutputTokensToChain(chain.chainId).map((outputToken) =>
updateL1DataFeePromise(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: "CronCacheL1DataFee",
message: "Finished",
});
response.status(200);
response.send("OK");
} catch (error: unknown) {
return handleErrorCondition(
"cron-cache-l1-data-fee",
response,
logger,
error
);
}
};

export default handler;
7 changes: 7 additions & 0 deletions vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
"path": "/api/cron-cache-gas-costs",
"schedule": "* * * * *"
},
{
"path": "/api/cron-cache-l1-data-fee",
"schedule": "* * * * *"
},
{
"path": "/api/cron-ping-endpoints",
"schedule": "* * * * *"
Expand All @@ -25,6 +29,9 @@
"api/cron-cache-gas-costs.ts": {
"maxDuration": 90
},
"api/cron-cache-l1-data-fee.ts": {
"maxDuration": 90
},
"api/cron-ping-endpoints.ts": {
"maxDuration": 90
}
Expand Down

0 comments on commit e1ecc76

Please sign in to comment.