From f68dacc01a31e6fe8cb9dc2b4919911181064dd9 Mon Sep 17 00:00:00 2001 From: Alex Bakoushin Date: Wed, 17 Sep 2025 16:42:55 +0200 Subject: [PATCH 1/2] cache --- src/apps/beefy/api.ts | 175 +++++++++++++++++++++++++++++------------- 1 file changed, 120 insertions(+), 55 deletions(-) diff --git a/src/apps/beefy/api.ts b/src/apps/beefy/api.ts index 6fe05e0e..7c525e49 100644 --- a/src/apps/beefy/api.ts +++ b/src/apps/beefy/api.ts @@ -1,6 +1,7 @@ import got from '../../utils/got' import { Address } from 'viem' import { NetworkId } from '../../types/networkId' +import { LRUCache } from 'lru-cache' export type BeefyVault = { id: string @@ -40,6 +41,15 @@ export type GovVault = BeefyVault & { earnedTokenAddress: Address[] } +export type BeefyTvl = Record> + +export type BeefyApyBreakdown = Record< + string, + Record | undefined +> + +export type BeefyPrices = Record + export const NETWORK_ID_TO_BEEFY_BLOCKCHAIN_ID: Record< NetworkId, string | null @@ -75,21 +85,49 @@ const NETWORK_ID_TO_CHAIN_ID: { [NetworkId['base-sepolia']]: 84532, } +const cache = new LRUCache({ + max: 50, // Prevent excessive memory consumption + ttl: 10 * 60 * 1000, // 10 minutes +}) + +const CACHE_KEYS = { + VAULTS: (networkId: NetworkId) => `beefy:vaults:${networkId}`, + GOV_VAULTS: (networkId: NetworkId) => `beefy:gov-vaults:${networkId}`, + PRICES: (networkId: NetworkId) => `beefy:prices:${networkId}`, + APY_BREAKDOWN: () => 'beefy:apy-breakdown', + TVL: () => 'beefy:tvl', +} as const + export async function getBeefyVaults( networkId: NetworkId, ): Promise<{ vaults: BaseBeefyVault[]; govVaults: GovVault[] }> { - const [vaults, govVaults] = await Promise.all([ - got - .get( - `https://api.beefy.finance/harvestable-vaults/${NETWORK_ID_TO_BEEFY_BLOCKCHAIN_ID[networkId]}`, - ) - .json(), - got - .get( - `https://api.beefy.finance/gov-vaults/${NETWORK_ID_TO_BEEFY_BLOCKCHAIN_ID[networkId]}`, - ) - .json(), - ]) + const vaultsKey = CACHE_KEYS.VAULTS(networkId) + const govVaultsKey = CACHE_KEYS.GOV_VAULTS(networkId) + + let vaults = cache.get(vaultsKey) as BaseBeefyVault[] | undefined + let govVaults = cache.get(govVaultsKey) as GovVault[] | undefined + + if (!vaults || !govVaults) { + const [vaultsResponse, govVaultsResponse] = await Promise.all([ + got + .get( + `https://api.beefy.finance/harvestable-vaults/${NETWORK_ID_TO_BEEFY_BLOCKCHAIN_ID[networkId]}`, + ) + .json(), + got + .get( + `https://api.beefy.finance/gov-vaults/${NETWORK_ID_TO_BEEFY_BLOCKCHAIN_ID[networkId]}`, + ) + .json(), + ]) + + // Cache the responses + cache.set(vaultsKey, vaultsResponse) + cache.set(govVaultsKey, govVaultsResponse) + + vaults = vaultsResponse + govVaults = govVaultsResponse + } return { vaults, @@ -99,56 +137,83 @@ export async function getBeefyVaults( export async function getBeefyPrices( networkId: NetworkId, -): Promise> { - const [lpsPrices, tokenPrices, tokens] = await Promise.all([ - got - .get(`https://api.beefy.finance/lps`) - .json>(), - got - .get(`https://api.beefy.finance/prices`) - .json>(), - got - .get( - `https://api.beefy.finance/tokens/${NETWORK_ID_TO_BEEFY_BLOCKCHAIN_ID[networkId]}`, - ) - .json< - Record< - string, // oracleId - { - // These are the fields we need, but there are more - address: string - oracle: string // examples: 'lps', 'tokens' - oracleId: string - } - > - >(), - ]) - - // Combine lps prices with token prices - return { - ...lpsPrices, - ...Object.fromEntries( - Object.entries(tokens) - .filter(([, { oracle }]) => oracle === 'tokens') - .map(([, { address, oracleId }]) => [ - address.toLowerCase(), - tokenPrices[oracleId], - ]), - ), +): Promise { + const pricesKey = CACHE_KEYS.PRICES(networkId) + + let prices = cache.get(pricesKey) as BeefyPrices | undefined + + if (!prices) { + const [lpsPrices, tokenPrices, tokens] = await Promise.all([ + got.get(`https://api.beefy.finance/lps`).json(), + got.get(`https://api.beefy.finance/prices`).json(), + got + .get( + `https://api.beefy.finance/tokens/${NETWORK_ID_TO_BEEFY_BLOCKCHAIN_ID[networkId]}`, + ) + .json< + Record< + string, // oracleId + { + // These are the fields we need, but there are more + address: string + oracle: string // examples: 'lps', 'tokens' + oracleId: string + } + > + >(), + ]) + + // Combine lps prices with token prices + prices = { + ...lpsPrices, + ...Object.fromEntries( + Object.entries(tokens) + .filter(([, { oracle }]) => oracle === 'tokens') + .map(([, { address, oracleId }]) => [ + address.toLowerCase(), + tokenPrices[oracleId], + ]), + ), + } + + // Cache the response + cache.set(pricesKey, prices) } + + return prices } export async function getApyBreakdown() { - return got - .get(`https://api.beefy.finance/apy/breakdown/`) - .json | undefined>>() + const cacheKey = CACHE_KEYS.APY_BREAKDOWN() + + let apyBreakdown = cache.get(cacheKey) as BeefyApyBreakdown | undefined + + if (!apyBreakdown) { + // Fetch from API if not in cache + apyBreakdown = await got + .get(`https://api.beefy.finance/apy/breakdown/`) + .json() + + // Cache the response + cache.set(cacheKey, apyBreakdown) + } + + return apyBreakdown } export async function getTvls( networkId: NetworkId, ): Promise> { - const tvlResponse = await got - .get(`https://api.beefy.finance/tvl/`) - .json>>() - return tvlResponse[NETWORK_ID_TO_CHAIN_ID[networkId]] ?? {} + const cacheKey = CACHE_KEYS.TVL() + + let tvl = cache.get(cacheKey) as BeefyTvl | undefined + + if (!tvl) { + tvl = await got.get(`https://api.beefy.finance/tvl/`).json() + + // Cache the response + cache.set(cacheKey, tvl) + } + + return tvl[NETWORK_ID_TO_CHAIN_ID[networkId]] ?? {} } From aa2eb89821c52e81a313eced12b0aee8ee0d5f02 Mon Sep 17 00:00:00 2001 From: Alex Bakoushin Date: Fri, 19 Sep 2025 12:51:52 +0200 Subject: [PATCH 2/2] stale-while-revalidate --- src/apps/beefy/api.ts | 146 +++++++++++++++++++----------------------- 1 file changed, 66 insertions(+), 80 deletions(-) diff --git a/src/apps/beefy/api.ts b/src/apps/beefy/api.ts index 7c525e49..5926dabb 100644 --- a/src/apps/beefy/api.ts +++ b/src/apps/beefy/api.ts @@ -41,14 +41,13 @@ export type GovVault = BeefyVault & { earnedTokenAddress: Address[] } -export type BeefyTvl = Record> +type BeefyData = Record -export type BeefyApyBreakdown = Record< - string, - Record | undefined -> +type BeefyPrices = BeefyData -export type BeefyPrices = Record +type BeefyTvls = BeefyData + +type BeefyApyBreakdown = Record | undefined> export const NETWORK_ID_TO_BEEFY_BLOCKCHAIN_ID: Record< NetworkId, @@ -85,30 +84,27 @@ const NETWORK_ID_TO_CHAIN_ID: { [NetworkId['base-sepolia']]: 84532, } -const cache = new LRUCache({ - max: 50, // Prevent excessive memory consumption - ttl: 10 * 60 * 1000, // 10 minutes -}) - -const CACHE_KEYS = { - VAULTS: (networkId: NetworkId) => `beefy:vaults:${networkId}`, - GOV_VAULTS: (networkId: NetworkId) => `beefy:gov-vaults:${networkId}`, - PRICES: (networkId: NetworkId) => `beefy:prices:${networkId}`, - APY_BREAKDOWN: () => 'beefy:apy-breakdown', - TVL: () => 'beefy:tvl', +const CACHE_CONFIG = { + max: 20, + ttl: 5 * 1000, // 5 seconds + allowStale: true, // allow stale-while-revalidate behavior } as const -export async function getBeefyVaults( - networkId: NetworkId, -): Promise<{ vaults: BaseBeefyVault[]; govVaults: GovVault[] }> { - const vaultsKey = CACHE_KEYS.VAULTS(networkId) - const govVaultsKey = CACHE_KEYS.GOV_VAULTS(networkId) - - let vaults = cache.get(vaultsKey) as BaseBeefyVault[] | undefined - let govVaults = cache.get(govVaultsKey) as GovVault[] | undefined +// Cache used for non-parametrized endpoints +const urlCache = new LRUCache({ + ...CACHE_CONFIG, + fetchMethod: async (url: string) => { + return got.get(url).json() + }, +}) - if (!vaults || !govVaults) { - const [vaultsResponse, govVaultsResponse] = await Promise.all([ +const vaultsCache = new LRUCache< + NetworkId, + { vaults: BaseBeefyVault[]; govVaults: GovVault[] } +>({ + ...CACHE_CONFIG, + fetchMethod: async (networkId: NetworkId) => { + const [vaults, govVaults] = await Promise.all([ got .get( `https://api.beefy.finance/harvestable-vaults/${NETWORK_ID_TO_BEEFY_BLOCKCHAIN_ID[networkId]}`, @@ -121,31 +117,19 @@ export async function getBeefyVaults( .json(), ]) - // Cache the responses - cache.set(vaultsKey, vaultsResponse) - cache.set(govVaultsKey, govVaultsResponse) - - vaults = vaultsResponse - govVaults = govVaultsResponse - } - - return { - vaults, - govVaults, - } -} - -export async function getBeefyPrices( - networkId: NetworkId, -): Promise { - const pricesKey = CACHE_KEYS.PRICES(networkId) - - let prices = cache.get(pricesKey) as BeefyPrices | undefined + return { + vaults, + govVaults, + } + }, +}) - if (!prices) { +const pricesCache = new LRUCache({ + ...CACHE_CONFIG, + fetchMethod: async (networkId: NetworkId): Promise => { const [lpsPrices, tokenPrices, tokens] = await Promise.all([ - got.get(`https://api.beefy.finance/lps`).json(), - got.get(`https://api.beefy.finance/prices`).json(), + got.get(`https://api.beefy.finance/lps`).json(), + got.get(`https://api.beefy.finance/prices`).json(), got .get( `https://api.beefy.finance/tokens/${NETWORK_ID_TO_BEEFY_BLOCKCHAIN_ID[networkId]}`, @@ -164,7 +148,7 @@ export async function getBeefyPrices( ]) // Combine lps prices with token prices - prices = { + return { ...lpsPrices, ...Object.fromEntries( Object.entries(tokens) @@ -175,45 +159,47 @@ export async function getBeefyPrices( ]), ), } + }, +}) - // Cache the response - cache.set(pricesKey, prices) +export async function getApyBreakdown(): Promise { + const apyBreakdown = (await urlCache.fetch( + 'https://api.beefy.finance/apy/breakdown/', + )) as BeefyApyBreakdown | undefined + if (!apyBreakdown) { + throw new Error('Failed to fetch APY breakdown data') } - - return prices + return apyBreakdown } -export async function getApyBreakdown() { - const cacheKey = CACHE_KEYS.APY_BREAKDOWN() - - let apyBreakdown = cache.get(cacheKey) as BeefyApyBreakdown | undefined - - if (!apyBreakdown) { - // Fetch from API if not in cache - apyBreakdown = await got - .get(`https://api.beefy.finance/apy/breakdown/`) - .json() - - // Cache the response - cache.set(cacheKey, apyBreakdown) +export async function getTvls(networkId: NetworkId): Promise { + const tvlResponse = (await urlCache.fetch( + 'https://api.beefy.finance/tvl/', + )) as Record | undefined + if (!tvlResponse) { + throw new Error('Failed to fetch TVL data') } - - return apyBreakdown + return tvlResponse[NETWORK_ID_TO_CHAIN_ID[networkId]] ?? {} } -export async function getTvls( +export async function getBeefyVaults( networkId: NetworkId, -): Promise> { - const cacheKey = CACHE_KEYS.TVL() +): Promise<{ vaults: BaseBeefyVault[]; govVaults: GovVault[] }> { + const result = await vaultsCache.fetch(networkId) - let tvl = cache.get(cacheKey) as BeefyTvl | undefined + if (!result) { + throw new Error('Failed to fetch vaults data') + } - if (!tvl) { - tvl = await got.get(`https://api.beefy.finance/tvl/`).json() + return result +} - // Cache the response - cache.set(cacheKey, tvl) +export async function getBeefyPrices( + networkId: NetworkId, +): Promise { + const prices = await pricesCache.fetch(networkId) + if (!prices) { + throw new Error('Failed to fetch prices data') } - - return tvl[NETWORK_ID_TO_CHAIN_ID[networkId]] ?? {} + return prices }