diff --git a/api/_polyfills.ts b/api/_polyfills.ts new file mode 100644 index 000000000..3ca5155b4 --- /dev/null +++ b/api/_polyfills.ts @@ -0,0 +1,8 @@ +import * as fetch from "node-fetch"; + +// Required for @vercel/kv +if (!globalThis.fetch) { + (globalThis as any).fetch = fetch; +} + +export {}; diff --git a/api/_utils.ts b/api/_utils.ts index 586076971..e6c44d15b 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -1,3 +1,7 @@ +// _POLYFILLS MUST BE IMPORTED FIRST +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import * as _pFill from "./_polyfills"; + import { AcceleratingDistributor__factory } from "@across-protocol/across-token/dist/typechain"; import { ERC20__factory, @@ -11,7 +15,6 @@ import { Log, Logging } from "@google-cloud/logging"; import axios from "axios"; import { BigNumber, ethers, providers, utils } from "ethers"; import { StructError, define } from "superstruct"; - import enabledMainnetRoutesAsJson from "../src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json"; import enabledGoerliRoutesAsJson from "../src/data/routes_5_0x0e2817C49698cc0874204AeDf7c72Be2Bb7fCD5d.json"; @@ -36,6 +39,7 @@ import { defaultRelayerAddressOverride, } from "./_constants"; import { PoolStateResult } from "./_types"; +import { kv } from "@vercel/kv"; type LoggingUtility = sdk.relayFeeCalculator.Logger; @@ -242,6 +246,7 @@ export const getTokenDetails = async ( }; export class InputError extends Error {} +export class ServerError extends Error {} /** * Resolves an Infura provider given the name of the ETH network @@ -620,45 +625,35 @@ export const isRouteEnabled = ( * @param token The valid ERC20 token address on the given `chainId`. * @returns A promise that resolves to the BigNumber of the balance */ -export const getBalance = ( +export const getCachedBalance = async ( chainId: string | number, account: string, token: string ): Promise => { - return sdk.utils.getTokenBalance( - account, - token, - getProvider(Number(chainId)), - BLOCK_TAG_LAG - ); -}; - -/** - * 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 account A valid Web3 wallet address - * @param token The valid ERC20 token address on the given `chainId`. - * @returns A promise that resolves to the BigNumber of the balance - */ -export const getCachedTokenBalance = async ( - chainId: string | number, - account: string, - token: string -): Promise => { - // Make the request to the vercel API. - const response = await axios.get<{ balance: string }>( - `${resolveVercelEndpoint()}/api/account-balance`, - { - params: { - chainId, - account, - token, - }, + const key = `balance_${chainId}-${account}-${token}`.toLowerCase(); + const data = await kv.get(key); + if (sdk.utils.isDefined(data)) { + return BigNumber.from(data); + } else { + const balance = await sdk.utils.getTokenBalance( + account, + token, + getProvider(Number(chainId)), + BLOCK_TAG_LAG // We should do this for consistency + ); + try { + await kv.set(key, balance.toString(), { + ex: 60 * 5, // 5 minutes + }); + getLogger().debug({ + at: "_utils#getBalance", + message: `Cached balance for ${key}: ${balance}`, + }); + } catch (_err) { + throw new ServerError(); } - ); - // Return the balance - return BigNumber.from(response.data.balance); + return balance; + } }; /** diff --git a/api/account-balance.ts b/api/account-balance.ts deleted file mode 100644 index 56be95685..000000000 --- a/api/account-balance.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { VercelResponse } from "@vercel/node"; -import { assert, Infer, type, string } from "superstruct"; -import { TypedVercelRequest } from "./_types"; -import { - getBalance, - getLogger, - handleErrorCondition, - validAddress, -} from "./_utils"; - -const AccountBalanceQueryParamsSchema = type({ - token: validAddress(), - account: validAddress(), - chainId: string(), -}); - -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 } = query; - // Rely on the utils to query the balance of the account for the token - const balance = await getBalance(chainId, account, token); - // Package the response - const result = { - balance: balance.toString(), - account: account, - token: token, - }; - // Log the response - logger.debug({ - at: "AccountBalance", - message: "Response data", - responseJson: result, - }); - // Set the caching headers that will be used by the CDN. - response.setHeader( - "Cache-Control", - "s-maxage=150, stale-while-revalidate=150" - ); - // 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 c1e6a2152..cb818b4cf 100644 --- a/api/limits.ts +++ b/api/limits.ts @@ -19,7 +19,6 @@ import { getRelayerFeeDetails, getCachedTokenPrice, getTokenDetails, - getCachedTokenBalance, maxBN, minBN, isRouteEnabled, @@ -30,6 +29,7 @@ import { getProvider, HUB_POOL_CHAIN_ID, ENABLED_ROUTES, + getCachedBalance, getDefaultRelayerAddress, sendResponse, } from "./_utils"; @@ -165,19 +165,19 @@ const handler = async ( hubPool.callStatic.multicall(multicallInput, { blockTag: BLOCK_TAG_LAG }), Promise.all( fullRelayers.map((relayer) => - getCachedTokenBalance(destinationChainId!, relayer, destinationToken) + getCachedBalance(destinationChainId!, destinationToken, relayer) ) ), Promise.all( transferRestrictedRelayers.map((relayer) => - getCachedTokenBalance(destinationChainId!, relayer, destinationToken) + getCachedBalance(destinationChainId!, destinationToken, relayer) ) ), Promise.all( fullRelayers.map((relayer) => destinationChainId === "1" ? ethers.BigNumber.from("0") - : getCachedTokenBalance("1", relayer, l1Token) + : getCachedBalance("1", l1Token, relayer) ) ), ]); diff --git a/api/suggested-fees.ts b/api/suggested-fees.ts index 622fa00ef..dbfe10d90 100644 --- a/api/suggested-fees.ts +++ b/api/suggested-fees.ts @@ -29,8 +29,8 @@ import { HUB_POOL_CHAIN_ID, ENABLED_ROUTES, getSpokePoolAddress, - getCachedTokenBalance, getDefaultRelayerAddress, + getCachedBalance, } from "./_utils"; const SuggestedFeesQueryParamsSchema = type({ @@ -135,7 +135,7 @@ const handler = async ( `Could not resolve token address on ${destinationChainId} for ${l1Token}` ); } - const balanceOfToken = await getCachedTokenBalance( + const balanceOfToken = await getCachedBalance( destinationChainId, relayer, destinationToken diff --git a/package.json b/package.json index dd58b1a38..f76a2f181 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "jose": "^4.9.3", "lodash-es": "^4.17.21", "luxon": "^3.3.0", + "node-fetch": "^3.3.2", "numeral": "^2.0.6", "react": "^17.0.2", "react-device-detect": "^2.2.2", diff --git a/yarn.lock b/yarn.lock index 4694b56ea..c62daf97d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13719,6 +13719,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== + data-urls@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143" @@ -16240,6 +16245,14 @@ fecha@^4.2.0: resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + fetch-ponyfill@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/fetch-ponyfill/-/fetch-ponyfill-4.1.0.tgz#ae3ce5f732c645eab87e4ae8793414709b239893" @@ -16537,6 +16550,13 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -21695,6 +21715,11 @@ node-dir@^0.1.17: dependencies: minimatch "^3.0.2" +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + node-emoji@^1.10.0: version "1.11.0" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" @@ -21751,6 +21776,15 @@ node-fetch@^2.6.1, node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" +node-fetch@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" + integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + node-forge@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" @@ -27190,6 +27224,11 @@ web-encoding@^1.0.2, web-encoding@^1.0.6: optionalDependencies: "@zxing/text-encoding" "0.9.0" +web-streams-polyfill@^3.0.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" + integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== + web3-bzz@1.7.4: version "1.7.4" resolved "https://registry.yarnpkg.com/web3-bzz/-/web3-bzz-1.7.4.tgz#9419e606e38a9777443d4ce40506ebd796e06075"