From 5797e0a6e5d7cec599d601c8219604370f5f6138 Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Wed, 25 Oct 2023 17:26:21 -0400 Subject: [PATCH] feat: add redis caching support for getBalance --- api/_imports.ts | 5 ++++ api/_utils.ts | 59 ++++++++++++++++------------------------- api/account-balance.ts | 60 ------------------------------------------ api/limits.ts | 8 +++--- api/suggested-fees.ts | 17 ++++++------ package.json | 2 ++ yarn.lock | 34 ++++++++++++++++++++++++ 7 files changed, 77 insertions(+), 108 deletions(-) create mode 100644 api/_imports.ts delete mode 100644 api/account-balance.ts diff --git a/api/_imports.ts b/api/_imports.ts new file mode 100644 index 000000000..a1b1bcd66 --- /dev/null +++ b/api/_imports.ts @@ -0,0 +1,5 @@ +import * as fetch from "node-fetch"; + +if (!globalThis.fetch) { + (globalThis as any).fetch = fetch; +} diff --git a/api/_utils.ts b/api/_utils.ts index 2d0d6948b..b4d413cea 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -1,3 +1,4 @@ +import "./_imports"; import { AcceleratingDistributor__factory } from "@across-protocol/across-token/dist/typechain"; import { ERC20__factory, @@ -11,7 +12,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"; @@ -35,6 +35,7 @@ import { BLOCK_TAG_LAG, } from "./_constants"; import { PoolStateResult } from "./_types"; +import { kv } from "@vercel/kv"; type LoggingUtility = sdk.relayFeeCalculator.Logger; @@ -620,45 +621,31 @@ 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 = ( - 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 ( +export const getBalance = 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, - }, - } - ); - // Return the balance - return BigNumber.from(response.data.balance); + 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 + ); + await kv.set(key, balance.toString(), { + ex: 60 * 5, // 5 minutes + }); + getLogger().debug({ + at: "_utils#getBalance", + message: `Cached balance for ${key}: ${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 863338519..e33aba839 100644 --- a/api/limits.ts +++ b/api/limits.ts @@ -18,7 +18,6 @@ import { getRelayerFeeDetails, getCachedTokenPrice, getTokenDetails, - getCachedTokenBalance, maxBN, minBN, isRouteEnabled, @@ -29,6 +28,7 @@ import { getProvider, HUB_POOL_CHAIN_ID, ENABLED_ROUTES, + getBalance, } from "./_utils"; import { constants } from "@across-protocol/sdk-v2"; @@ -156,19 +156,19 @@ const handler = async ( hubPool.callStatic.multicall(multicallInput, { blockTag: BLOCK_TAG_LAG }), Promise.all( fullRelayers.map((relayer) => - getCachedTokenBalance(destinationChainId!, destinationToken, relayer) + getBalance(destinationChainId!, destinationToken, relayer) ) ), Promise.all( transferRestrictedRelayers.map((relayer) => - getCachedTokenBalance(destinationChainId!, destinationToken, relayer) + getBalance(destinationChainId!, destinationToken, relayer) ) ), Promise.all( fullRelayers.map((relayer) => destinationChainId === "1" ? ethers.BigNumber.from("0") - : getCachedTokenBalance("1", l1Token, relayer) + : getBalance("1", l1Token, relayer) ) ), ]); diff --git a/api/suggested-fees.ts b/api/suggested-fees.ts index 0e7d47db6..fd2cb5a93 100644 --- a/api/suggested-fees.ts +++ b/api/suggested-fees.ts @@ -24,7 +24,7 @@ import { HUB_POOL_CHAIN_ID, ENABLED_ROUTES, getSpokePoolAddress, - getCachedTokenBalance, + getBalance, } from "./_utils"; const SuggestedFeesQueryParamsSchema = type({ @@ -94,10 +94,11 @@ const handler = async ( // Our message encoding is a hex string, so we need to check that the length is even. throw new InputError("Message must be an even hex string"); } - const isRecipientAContract = await sdk.utils.isContractDeployedToAddress( - recipientAddress, - provider - ); + const isRecipientAContract = + (await sdk.utils.isContractDeployedToAddress( + recipientAddress, + provider + )) || true; if (!isRecipientAContract) { throw new InputError( "Recipient must be a contract when a message is provided" @@ -116,10 +117,10 @@ const handler = async ( `Could not resolve token address on ${destinationChainId} for ${l1Token}` ); } - const balanceOfToken = await getCachedTokenBalance( + const balanceOfToken = await getBalance( destinationChainId, - destinationToken, - relayerAddress + relayerAddress, + destinationToken ); if (balanceOfToken.lt(amountInput)) { throw new InputError( diff --git a/package.json b/package.json index 04e4557a1..8002563ee 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@safe-global/safe-apps-sdk": "^8.1.0", "@sentry/react": "^7.37.2", "@uma/sdk": "^0.22.2", + "@vercel/kv": "^0.2.3", "@web3-onboard/coinbase": "^2.2.5", "@web3-onboard/core": "^2.21.2", "@web3-onboard/gnosis": "^2.2.0", @@ -35,6 +36,7 @@ "jose": "^4.9.3", "lodash-es": "^4.17.21", "luxon": "^3.3.0", + "node-fetch": "2", "numeral": "^2.0.6", "react": "^17.0.2", "react-device-detect": "^2.2.2", diff --git a/yarn.lock b/yarn.lock index efa79fb05..0389a0362 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9123,11 +9123,25 @@ "@uniswap/v3-core" "1.0.0" base64-sol "1.0.1" +"@upstash/redis@1.22.0": + version "1.22.0" + resolved "https://registry.yarnpkg.com/@upstash/redis/-/redis-1.22.0.tgz#3ae3ef056608c8598f46e8bc3b3f405a94781142" + integrity sha512-sXoJDoEqqik0HbrNE7yRWckOySEFsoBxfRdCgOqkc0w6py19ZZG50SpGkDDEUXSnBqP8VgGYXhWAiBpqxrt5oA== + dependencies: + isomorphic-fetch "^3.0.0" + "@vercel/build-utils@5.4.1": version "5.4.1" resolved "https://registry.yarnpkg.com/@vercel/build-utils/-/build-utils-5.4.1.tgz#09a347f189fb13a3184a46c89d04e0f3da9793df" integrity sha512-5+xIl65g8TpgLPQtlq7WYaS31/UZf+GcAO1O1IifUsLGM8+fhSgmmUVz7yoQ53YNWdnSA3WiKZfCgKopQPjlYQ== +"@vercel/kv@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@vercel/kv/-/kv-0.2.3.tgz#182419a8e44fcc7ff1638522f8d79a1846d99795" + integrity sha512-Wq1+EsRBQmvLlcqCZeYVg1MAARWrnETgLe3Sy3UCqG+zg7LThpkt0YHZe1NN3Aj4IRmCKQamotWrLDdEx+ZB3w== + dependencies: + "@upstash/redis" "1.22.0" + "@vercel/node-bridge@3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@vercel/node-bridge/-/node-bridge-3.0.0.tgz#443655b74713ec65531726fb5a30c5c528c804bf" @@ -19543,6 +19557,14 @@ isomorphic-fetch@^2.2.1: node-fetch "^1.0.1" whatwg-fetch ">=0.10.0" +isomorphic-fetch@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4" + integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA== + dependencies: + node-fetch "^2.6.1" + whatwg-fetch "^3.4.1" + isomorphic-unfetch@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f" @@ -22512,6 +22534,13 @@ node-environment-flags@1.0.6: object.getownpropertydescriptors "^2.0.3" semver "^5.7.0" +node-fetch@2: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-fetch@2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -29553,6 +29582,11 @@ whatwg-fetch@^2.0.4: resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== +whatwg-fetch@^3.4.1: + version "3.6.19" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.19.tgz#caefd92ae630b91c07345537e67f8354db470973" + integrity sha512-d67JP4dHSbm2TrpFj8AbO8DnL1JXL5J9u0Kq2xW6d0TFDbCA3Muhdt8orXC22utleTVj7Prqt82baN6RBvnEgw== + whatwg-mimetype@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7"