From 703312bb7db53f7766a41cc3ac2db9e4feb781ca Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Mon, 23 Oct 2023 14:00:04 -0400 Subject: [PATCH] improve: add token caching --- api/_utils.ts | 52 +++++++++++++++++++++++++++---- api/account-balance.ts | 70 ++++++++++++++++++++++++++++++++++++++++++ api/limits.ts | 8 ++--- api/suggested-fees.ts | 7 +++-- 4 files changed, 124 insertions(+), 13 deletions(-) create mode 100644 api/account-balance.ts diff --git a/api/_utils.ts b/api/_utils.ts index c657ed9e7..5a491d8e6 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -611,23 +611,63 @@ export const isRouteEnabled = ( }; /** - * Resolves the balance of a given ERC20 token at a provided address + * Resolves the balance of a given ERC20 token at a provided address. If no token is provided, the balance of the + * native currency will be returned. * @param chainId The blockchain Id to query against - * @param token The valid ERC20 token address on the given `chainId` + * @param token The valid ERC20 token address on the given `chainId`. If undefined, the native currency will be used * @param account A valid Web3 wallet address - * @param blockTag A blockTag to specify a historical balance date + * @param blockTag A blockTag to specify a historical balance date or the latest balance * @returns A promise that resolves to the BigNumber of the balance */ export const getBalance = ( chainId: string | number, - token: string, account: string, + token?: string, blockTag: number | "latest" = "latest" ): Promise => { - return ERC20__factory.connect(token, getProvider(Number(chainId))).balanceOf( + const provider = getProvider(Number(chainId)); + if (sdk.utils.isDefined(token)) { + return sdk.utils.getTokenBalance(account, token, provider, blockTag); + } else { + return provider.getBalance(account, blockTag); + } +}; + +/** + * Resolves the cached balance of a given ERC20 token at a provided address. If no token is provided, the balance of the + * native currency will be returned. + * @param chainId The blockchain Id to query against + * @param token The valid ERC20 token address on the given `chainId`. If undefined, the native currency will be used + * @param account A valid Web3 wallet address + * @param blockTag A blockTag to specify a historical balance date or the latest balance + * @returns A promise that resolves to the BigNumber of the balance + */ +export const getCachedTokenBalance = async ( + chainId: string | number, + account: string, + token?: string, + blockTag: number | "latest" = "latest" +): Promise => { + // Define the initial params + const params: Record = { + chainId, account, - { blockTag } + }; + if (token) { + params["token"] = token; + } + if (blockTag !== "latest") { + params["blockTag"] = blockTag; + } + // Make the request + const response = await axios.get<{ balance: string }>( + `${resolveVercelEndpoint()}/api/account-balance`, + { + params, + } ); + // Return the balance + return BigNumber.from(response.data.balance); }; /** diff --git a/api/account-balance.ts b/api/account-balance.ts new file mode 100644 index 000000000..952889d12 --- /dev/null +++ b/api/account-balance.ts @@ -0,0 +1,70 @@ +import { VercelResponse } from "@vercel/node"; +import { assert, Infer, optional, type, min, integer } from "superstruct"; +import { TypedVercelRequest } from "./_types"; +import { + getBalance, + getLogger, + handleErrorCondition, + validAddress, +} from "./_utils"; + +const AccountBalanceQueryParamsSchema = type({ + token: optional(validAddress()), + account: validAddress(), + chainId: min(integer(), 1), + blockTag: optional(min(integer(), 0)), +}); + +type AccountBalanceQueryParams = Infer; + +const handler = async ( + { query }: TypedVercelRequest, + response: VercelResponse +) => { + const logger = getLogger(); + logger.debug({ + at: "AccountBalance", + message: "Query data", + query, + }); + try { + // Validate the query parameters + assert(query, AccountBalanceQueryParamsSchema); + // Deconstruct the query parameters + let { token, account, chainId, blockTag } = query; + // Rely on the utils to query the balance of either the native + // token or an ERC20 token. + const balance = await getBalance(chainId, account, token, blockTag); + // Package the response + const result = { + balance: balance.toString(), + account: account, + token: token, + isNative: token === undefined, + tag: blockTag, + }; + // Determine the age of the caching of the response + // 1. If the blockTag is specified, then we can cache for 5 minutes (300 seconds) + // 2. If the blockTag is not specified, the "latest" block is used, and we should + // only cache for 10 seconds. + const cachingTime = blockTag ? 300 : 10; + // Log the response + logger.debug({ + at: "AccountBalance", + message: "Response data", + responseJson: result, + secondsCached: cachingTime, + }); + // Set the caching headers that will be used by the CDN. + response.setHeader( + "Cache-Control", + `s-maxage=${cachingTime}, stale-while-revalidate=${cachingTime}}` + ); + // Return the response + response.status(200).json(result); + } catch (error: unknown) { + return handleErrorCondition("account-balance", response, logger, error); + } +}; + +export default handler; diff --git a/api/limits.ts b/api/limits.ts index 2ab48bb7d..3fcced6c7 100644 --- a/api/limits.ts +++ b/api/limits.ts @@ -18,7 +18,7 @@ import { getRelayerFeeDetails, getCachedTokenPrice, getTokenDetails, - getBalance, + getCachedTokenBalance, maxBN, minBN, isRouteEnabled, @@ -156,7 +156,7 @@ const handler = async ( hubPool.callStatic.multicall(multicallInput, { blockTag: BLOCK_TAG_LAG }), Promise.all( fullRelayers.map((relayer) => - getBalance( + getCachedTokenBalance( destinationChainId!, destinationToken, relayer, @@ -166,7 +166,7 @@ const handler = async ( ), Promise.all( transferRestrictedRelayers.map((relayer) => - getBalance( + getCachedTokenBalance( destinationChainId!, destinationToken, relayer, @@ -178,7 +178,7 @@ const handler = async ( fullRelayers.map((relayer) => destinationChainId === "1" ? ethers.BigNumber.from("0") - : getBalance("1", l1Token, relayer, BLOCK_TAG_LAG) + : getCachedTokenBalance("1", l1Token, relayer, BLOCK_TAG_LAG) ) ), ]); diff --git a/api/suggested-fees.ts b/api/suggested-fees.ts index ac02c6e43..945a363f2 100644 --- a/api/suggested-fees.ts +++ b/api/suggested-fees.ts @@ -24,6 +24,7 @@ import { HUB_POOL_CHAIN_ID, ENABLED_ROUTES, getSpokePoolAddress, + getCachedTokenBalance, } from "./_utils"; const SuggestedFeesQueryParamsSchema = type({ @@ -115,10 +116,10 @@ const handler = async ( `Could not resolve token address on ${destinationChainId} for ${l1Token}` ); } - const balanceOfToken = await sdk.utils.getTokenBalance( - relayerAddress, + const balanceOfToken = await getCachedTokenBalance( + destinationChainId, destinationToken, - provider + relayerAddress ); if (balanceOfToken.lt(amountInput)) { throw new InputError(