Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(api): cache gas price #1222

Merged
merged 3 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions api/_errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ export class AcrossApiError extends Error {
}
}

export class UnauthorizedError extends AcrossApiError {
constructor(args?: { message: string }, opts?: ErrorOptions) {
super(
{
message: args?.message ?? "Unauthorized",
status: HttpErrorToStatusCode.UNAUTHORIZED,
},
opts
);
}
}

export class InputError extends AcrossApiError {
constructor(
args: {
Expand Down
26 changes: 24 additions & 2 deletions api/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@
makeCacheGetterAndSetter,
} from "./_cache";
import {
InputError,

Check warning on line 64 in api/_utils.ts

View workflow job for this annotation

GitHub Actions / format-and-lint

'InputError' is defined but never used
MissingParamError,
InvalidParamError,
handleErrorCondition,

Check warning on line 67 in api/_utils.ts

View workflow job for this annotation

GitHub Actions / format-and-lint

'handleErrorCondition' is defined but never used
RouteNotEnabledError,
} from "./_errors";

Expand Down Expand Up @@ -689,6 +689,7 @@
* @param message An optional message to include in the transfer
* @param relayerAddress An optional relayer address to use for the transfer
* @param gasUnits An optional gas unit to use for the transfer
* @param gasPrice An optional gas price to use for the transfer
* @returns The a promise to the relayer fee for the given `amount` of transferring `l1Token` to `destinationChainId`
*/
export const getRelayerFeeDetails = async (
Expand All @@ -703,7 +704,8 @@
},
tokenPrice?: number,
relayerAddress?: string,
gasUnits?: sdk.utils.BigNumberish
gasUnits?: sdk.utils.BigNumberish,
gasPrice?: sdk.utils.BigNumberish
): Promise<sdk.relayFeeCalculator.RelayerFeeDetails> => {
const {
inputToken,
Expand Down Expand Up @@ -731,7 +733,7 @@
sdk.utils.isMessageEmpty(message),
relayerAddress,
tokenPrice,
undefined,
gasPrice,
gasUnits
);
};
Expand Down Expand Up @@ -1955,6 +1957,26 @@
);
}

export function latestGasPriceCache(chainId: number) {
const ttlPerChain = {
default: 30,
[CHAIN_IDs.ARBITRUM]: 15,
};

return makeCacheGetterAndSetter(
buildInternalCacheKey("latestGasPriceCache", chainId),
ttlPerChain[chainId] || ttlPerChain.default,
async () => {
const gasPrice = await sdk.gasPriceOracle.getGasPriceEstimate(
getProvider(chainId),
chainId
);
return gasPrice.maxFeePerGas;
},
(bnFromCache) => BigNumber.from(bnFromCache)
);
}

/**
* Builds a URL search string from an object of query parameters.
*
Expand Down
3 changes: 2 additions & 1 deletion api/cron-cache-balances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
handleErrorCondition,
latestBalanceCache,
} from "./_utils";
import { UnauthorizedError } from "./_errors";

import mainnetChains from "../src/data/chains_1.json";

Expand All @@ -27,7 +28,7 @@ const handler = async (
!process.env.CRON_SECRET ||
authHeader !== `Bearer ${process.env.CRON_SECRET}`
) {
return response.status(401).json({ success: false });
throw new UnauthorizedError();
}

const {
Expand Down
100 changes: 100 additions & 0 deletions api/cron-cache-gas-prices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { VercelResponse } from "@vercel/node";
import { gasPriceOracle } from "@across-protocol/sdk";
import { TypedVercelRequest } from "./_types";
import {
HUB_POOL_CHAIN_ID,
getLogger,
handleErrorCondition,
latestGasPriceCache,
getProvider,
} from "./_utils";
import { UnauthorizedError } from "./_errors";

import mainnetChains from "../src/data/chains_1.json";

const updateIntervalsSecPerChain = {
default: 10,
1: 12,
};

const maxDurationSec = 60;

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

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

// The minimum interval for Vercel Serverless Functions cron jobs is 1 minute.
// But we want to update gas prices more frequently than that.
// To circumvent this, we run the function in a loop and update gas prices every
// `updateIntervalSec` seconds and stop after `maxDurationSec` seconds (1 minute).
const gasPricePromises = mainnetChains.map(async (chain) => {
while (true) {
const diff = Date.now() - functionStart;

// Stop after `maxDurationSec` seconds
if (diff >= maxDurationSec * 1000) {
break;
}

// Update gas price every `updateIntervalSec` seconds
const gasPrice = await gasPriceOracle.getGasPriceEstimate(
getProvider(chain.chainId),
chain.chainId
);
await latestGasPriceCache(chain.chainId).set(gasPrice.maxFeePerGas);

// Sleep for `updateIntervalSec` seconds
const updateIntervalSec =
updateIntervalsSecPerChain[
chain.chainId as keyof typeof updateIntervalsSecPerChain
] || updateIntervalsSecPerChain.default;
await new Promise((resolve) =>
setTimeout(resolve, updateIntervalSec * 1000)
);
}
});
await Promise.all(gasPricePromises);

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

export default handler;
12 changes: 10 additions & 2 deletions api/limits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
parsableBigNumberString,
validateDepositMessage,
getCachedFillGasUsage,
latestGasPriceCache,
} from "./_utils";
import { MissingParamError } from "./_errors";

Expand Down Expand Up @@ -163,7 +164,7 @@ const handler = async (
message,
};

const [tokenPriceNative, _tokenPriceUsd, latestBlock, gasUnits] =
const [tokenPriceNative, _tokenPriceUsd, latestBlock, gasUnits, gasPrice] =
await Promise.all([
getCachedTokenPrice(
l1Token.address,
Expand All @@ -177,6 +178,7 @@ const handler = async (
: getCachedFillGasUsage(depositArgs, {
relayerAddress: relayer,
}),
latestGasPriceCache(destinationChainId).get(),
]);
const tokenPriceUsd = ethers.utils.parseUnits(_tokenPriceUsd.toString());

Expand All @@ -187,7 +189,13 @@ const handler = async (
transferRestrictedBalances,
fullRelayerMainnetBalances,
] = await Promise.all([
getRelayerFeeDetails(depositArgs, tokenPriceNative, relayer, gasUnits),
getRelayerFeeDetails(
depositArgs,
tokenPriceNative,
relayer,
gasUnits,
gasPrice
),
callViaMulticall3(provider, multiCalls, {
blockTag: latestBlock.number,
}),
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"@across-protocol/constants": "^3.1.15",
"@across-protocol/contracts": "^3.0.10",
"@across-protocol/contracts-v3.0.6": "npm:@across-protocol/[email protected]",
"@across-protocol/sdk": "^3.1.32",
"@across-protocol/sdk": "^3.2.3",
"@amplitude/analytics-browser": "^2.3.5",
"@balancer-labs/sdk": "1.1.6-beta.16",
"@emotion/react": "^11.13.0",
Expand Down
5 changes: 5 additions & 0 deletions vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
"schedule": "* * * * *"
}
],
"functions": {
"api/cron-cache-gas-prices.ts": {
"maxDuration": 90
}
},
"rewrites": [
{
"source": "/api/deposit/status",
Expand Down
9 changes: 5 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,16 @@
axios "^1.6.2"
zksync-web3 "^0.14.3"

"@across-protocol/sdk@^3.1.32":
version "3.1.32"
resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.1.32.tgz#04248d6f722bf2706c9d2b9cd0bf228510a07ff6"
integrity sha512-BXv1dJPvZw3Ozsm1LPlFQD1OSdDYPZQH7czmSPCOEtrHoVl1eScKHxdu53hbD09//YhoVFF+i2W4auLXCJ8Stg==
"@across-protocol/sdk@^3.2.3":
version "3.2.3"
resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.2.3.tgz#9843a9a40b818af372c6022634046da8488d6997"
integrity sha512-IkHNxRWSI3G2VWMLX8+K1j7nSg44oeWaH19MzpSw8QR5AzgYv/hlzpGckSn2bdM7LFnWvOytoAVFmdBrcdFJ1Q==
dependencies:
"@across-protocol/across-token" "^1.0.0"
"@across-protocol/constants" "^3.1.14"
"@across-protocol/contracts" "^3.0.10"
"@eth-optimism/sdk" "^3.3.1"
"@ethersproject/bignumber" "^5.7.0"
"@pinata/sdk" "^2.1.0"
"@types/mocha" "^10.0.1"
"@uma/sdk" "^0.34.1"
Expand Down
Loading