cache wallet services (#793)

This commit is contained in:
Felicio
2025-09-10 16:17:47 +09:00
committed by GitHub
parent e227e0893a
commit d131e7bed8
7 changed files with 212 additions and 93 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'api': patch
---
cache wallet services
+86 -60
View File
@@ -9,81 +9,107 @@ import { type ApiRouter, apiRouter } from '@status-im/wallet/data'
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
import { headers as nextHeaders } from 'next/headers'
// import superjson from 'superjson'
import type { NextRequest } from 'next/server'
export type { ApiRouter }
// todo: use nodejs runtime
export const runtime = 'edge'
export const dynamic = 'force-dynamic'
async function handler(request: NextRequest) {
// let error: Error | undefined
// const response = await fetchRequestHandler({
return await fetchRequestHandler({
endpoint: '/api/trpc',
router: apiRouter,
req: request,
// allowBatching: true,
createContext: async () => {
const headers = new Headers(await nextHeaders())
try {
const response = await fetchRequestHandler({
// return await fetchRequestHandler({
endpoint: '/api/trpc',
router: apiRouter,
req: request,
// allowBatching: true,
createContext: async () => {
const headers = new Headers(await nextHeaders())
return { headers }
},
/**
* @see https://trpc.io/docs/v10/server/error-handling#handling-errors
*/
// onError: opts => {
// error = opts.error.cause
// },
responseMeta: opts => {
// note: opts.error does not have original cause (status code), contrary to onError
// note!: status code is inferred from TRPCError.code (TOO_MANY_REQUESTS, INTERNAL_SERVER_ERROR, etc.)
// const error = opts.errors?.[0]
return { headers }
},
/**
* @see https://trpc.io/docs/v10/server/error-handling#handling-errors
*/
// onError: opts => {
// error = opts.error.cause
// },
responseMeta: opts => {
// note: opts.error does not have original cause (status code), contrary to onError
// note!: status code is inferred from TRPCError.code (TOO_MANY_REQUESTS, INTERNAL_SERVER_ERROR, etc.)
// const error = opts.errors?.[0]
let cacheControl = 'public, max-age=3600'
// todo?: unset cache and revalidate and revalidate based on tag
// @see https://github.com/vercel/next.js/discussions/57792 for vercel caching error response
let cacheControl = 'public, max-age=3600'
if (
opts?.paths?.some(path =>
[
'nodes.broadcastTransaction',
'nodes.getNonce',
'nodes.getTransactionCount',
'nodes.getFeeRate',
'activities.page',
'activities.activities',
'assets.all',
'assets.nativeToken',
'assets.token',
'collectibles.page',
].includes(path)
) ||
opts?.type === 'mutation'
) {
cacheControl = 'private, no-store'
}
if (
opts?.paths?.some(path =>
[
'nodes.broadcastTransaction',
'nodes.getNonce',
'nodes.getTransactionCount',
'nodes.getFeeRate',
'activities.page',
'activities.activities',
'assets.all',
'assets.nativeToken',
'assets.token',
'collectibles.page',
].includes(path)
) ||
opts?.type === 'mutation'
) {
cacheControl = 'private, no-store'
}
return {
// status: 429,
headers: {
'cache-control': cacheControl,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
return {
// status: 429,
headers: {
'cache-control': cacheControl,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
}
},
// unstable_onChunk: undefined,
})
const status = response.status
const result = await response.json()
return Response.json(
result,
// { status: result.httpStatus }
// { status: 429 }
{ status: status }
)
} catch (error) {
console.error(error)
const status = 500
// @see https://github.com/trpc/trpc/discussions/3640#discussioncomment-5511435 for returning explicit trpc error in superjson construct as result and preventing possible timeouts due to additional parsing
const result = {
error: {
json: {
message: 'Internal server error',
code: -32603,
data: {
code: 'INTERNAL_SERVER_ERROR',
httpStatus: status,
// stack: undefined
},
},
}
},
// unstable_onChunk: undefined,
})
},
}
// const result = await response.json()
// return Response.json(
// result
// // { status: result.httpStatus }
// // { status: 429 }
// )
// @see https://vercel.com/docs/errors/FUNCTION_INVOCATION_TIMEOUT for ensuring response is always returned
return Response.json(result, { status: status })
}
}
export { handler as GET, handler as POST }
+6 -1
View File
@@ -2,5 +2,10 @@
"$schema": "https://openapi.vercel.sh/vercel.json",
"ignoreCommand": "git diff --quiet HEAD^ HEAD ../../{patches,package.json,turbo.json} ../../packages/wallet ./",
"installCommand": "pnpm install --dir ../../ --frozen-lockfile",
"buildCommand": "turbo run build --cwd ../../ --filter=./apps/api..."
"buildCommand": "turbo run build --cwd ../../ --filter=./apps/api...",
"functions": {
"app/api/**/*": {
"maxDuration": 800
}
}
}
+27 -9
View File
@@ -11,6 +11,7 @@ import {
getNativeTokenBalance,
} from '../../services/alchemy'
import {
CRYPTOCOMPARE_REVALIDATION_TIMES,
fetchTokenMetadata,
legacy_fetchTokenPriceHistory,
legacy_fetchTokensPrice,
@@ -366,7 +367,10 @@ async function all({
)
if (missingSymbols.length > 0) {
const prices = await legacy_fetchTokensPrice(missingSymbols)
const prices = await legacy_fetchTokensPrice(
missingSymbols,
CRYPTOCOMPARE_REVALIDATION_TIMES.CURRENT_PRICE,
)
for (const symbol of missingSymbols) {
const token = erc20TokenList.tokens.find(
@@ -453,14 +457,21 @@ async function nativeToken({
throw new Error('Balance not found')
}
const price = (await legacy_fetchTokensPrice([token.symbol]))[
token.symbol
]
const price = (
await legacy_fetchTokensPrice(
[token.symbol],
CRYPTOCOMPARE_REVALIDATION_TIMES.CURRENT_PRICE,
)
)[token.symbol]
const priceHistory = await legacy_fetchTokenPriceHistory(
token.symbol,
'all',
CRYPTOCOMPARE_REVALIDATION_TIMES.PRICE_HISTORY,
)
const tokenMetadata = await fetchTokenMetadata(
token.symbol,
CRYPTOCOMPARE_REVALIDATION_TIMES.TOKEN_METADATA,
)
const tokenMetadata = await fetchTokenMetadata(token.symbol)
const asset: Asset = map({
token,
@@ -548,14 +559,21 @@ async function token({
throw new Error(`Balance not found for token ${token.symbol}`)
}
const price = (await legacy_fetchTokensPrice([token.symbol]))[
token.symbol
]
const price = (
await legacy_fetchTokensPrice(
[token.symbol],
CRYPTOCOMPARE_REVALIDATION_TIMES.CURRENT_PRICE,
)
)[token.symbol]
const priceHistory = await legacy_fetchTokenPriceHistory(
token.symbol,
'all',
CRYPTOCOMPARE_REVALIDATION_TIMES.PRICE_HISTORY,
)
const tokenMetadata = await fetchTokenMetadata(
token.symbol,
CRYPTOCOMPARE_REVALIDATION_TIMES.TOKEN_METADATA,
)
const tokenMetadata = await fetchTokenMetadata(token.symbol)
const asset = map({
token,
@@ -7,7 +7,10 @@ import {
getNFTMetadata,
getNFTs,
} from '../../services/alchemy'
import { legacy_fetchTokensPrice } from '../../services/cryptocompare'
import {
CRYPTOCOMPARE_REVALIDATION_TIMES,
legacy_fetchTokensPrice,
} from '../../services/cryptocompare'
import { publicProcedure, router } from '../lib/trpc'
import type {
@@ -169,7 +172,12 @@ async function collectible(
let floorPrice: number | undefined
if (network === 'ethereum') {
const symbol = 'ETH'
price = (await legacy_fetchTokensPrice([symbol]))[symbol][currency].PRICE
price = (
await legacy_fetchTokensPrice(
[symbol],
CRYPTOCOMPARE_REVALIDATION_TIMES.CURRENT_PRICE,
)
)[symbol][currency].PRICE
floorPrice = nft.contract.openSeaMetadata.floorPrice ?? undefined
// note: looksRare data is significantly inconsistent with opensea data
// floorPrice = (await getNFTFloorPrice(contract, network)).openSea.floorPrice
@@ -16,7 +16,10 @@ import {
markApiKeyAsRateLimited,
markApiKeyAsSuccessful,
} from '../api-key-rotation'
import { getNativeTokenPrice } from '../coingecko'
import {
CRYPTOCOMPARE_REVALIDATION_TIMES,
legacy_fetchTokensPrice,
} from '../cryptocompare'
import { estimateConfirmationTime, processFeeHistory } from './utils'
import type { NetworkType } from '../../api/types'
@@ -923,7 +926,10 @@ export async function getFeeRate(
params: ['0x4', 'latest', [10, 50, 90]],
})
}),
getNativeTokenPrice('ethereum'),
legacy_fetchTokensPrice(
['ETH'],
CRYPTOCOMPARE_REVALIDATION_TIMES.TRADING_PRICE,
),
])
} catch (error) {
console.error('Failed to fetch fee rate:', error)
@@ -949,10 +955,7 @@ export async function getFeeRate(
reward: feeHistory?.result?.reward ?? null,
})
const ethPrice =
typeof ethPriceData?.usd === 'number'
? ethPriceData.usd
: parseFloat(ethPriceData?.usd) || 0
const ethPrice = ethPriceData?.['ETH']?.['USD']?.['PRICE'] || 0
const feeEth = parseFloat(formatEther(gasLimit * (baseFee + priorityFee)))
const feeEur = parseFloat((ethPrice > 0 ? feeEth * ethPrice : 0).toFixed(6))
@@ -1048,7 +1051,7 @@ async function _fetch<T extends ResponseBody>(
...(body && { body: JSON.stringify(body) }),
// why: https://nextjs.org/docs/app/building-your-application/data-fetching/fetching#reusing-data-across-multiple-functions
// why: https://github.com/vercel/next.js/issues/70946
cache: 'no-store', // no caching
cache: 'force-cache',
next: {
revalidate,
},
@@ -25,6 +25,18 @@ import type {
TokenMetadataResponseBody,
} from './types'
export const CRYPTOCOMPARE_REVALIDATION_TIMES = {
TRADING_PRICE: 15,
CURRENT_PRICE: 60,
PRICE_HISTORY: 3600,
PRICE_HISTORY_DAILY: 3600,
TOKEN_METADATA: 3600,
PRICE_FOR_DATE: 15,
} as const
type Revalidation =
(typeof CRYPTOCOMPARE_REVALIDATION_TIMES)[keyof typeof CRYPTOCOMPARE_REVALIDATION_TIMES]
/**
* @see https://min-api.cryptocompare.com/documentation?key=Historical&cat=dataHistoday
* @see https://min-api.cryptocompare.com/documentation?key=Historical&cat=dataHistohour
@@ -34,6 +46,7 @@ import type {
export async function legacy_fetchTokenPriceHistory(
symbol: string,
days: '1' | '7' | '30' | '90' | '365' | 'all' = '1',
revalidate: Revalidation = CRYPTOCOMPARE_REVALIDATION_TIMES.PRICE_HISTORY,
) {
if (days === 'all') {
const url = new URL('https://min-api.cryptocompare.com/data/v2/histoday')
@@ -46,7 +59,11 @@ export async function legacy_fetchTokenPriceHistory(
getRandomApiKey(serverEnv.CRYPTOCOMPARE_API_KEYS),
)
const body = await _fetch<legacy_TokenPriceHistoryResponseBody>(url, 3600)
const body = await _fetch<legacy_TokenPriceHistoryResponseBody>(
url,
revalidate,
'legacy_fetchTokenPriceHistory_today',
)
const data = body.Data.Data
return data
@@ -70,7 +87,11 @@ export async function legacy_fetchTokenPriceHistory(
getRandomApiKey(serverEnv.CRYPTOCOMPARE_API_KEYS),
)
const body = await _fetch<legacy_TokenPriceHistoryResponseBody>(url, 3600)
const body = await _fetch<legacy_TokenPriceHistoryResponseBody>(
url,
revalidate,
'legacy_fetchTokenPriceHistory_hour',
)
const data = body.Data.Data
_data = [...data, ..._data]
@@ -90,7 +111,10 @@ export async function legacy_fetchTokenPriceHistory(
/**
* @see https://developers.cryptocompare.com/documentation/data-api/asset_v1_metadata
*/
export async function fetchTokenMetadata(symbol: string) {
export async function fetchTokenMetadata(
symbol: string,
revalidate: Revalidation = CRYPTOCOMPARE_REVALIDATION_TIMES.TOKEN_METADATA,
) {
const url = new URL('https://data-api.cryptocompare.com/asset/v1/metadata')
url.searchParams.set('asset', symbol)
url.searchParams.set('asset_lookup_priority', 'SYMBOL')
@@ -100,7 +124,11 @@ export async function fetchTokenMetadata(symbol: string) {
getRandomApiKey(serverEnv.CRYPTOCOMPARE_API_KEYS),
)
const body = await _fetch<TokenMetadataResponseBody>(url, 3600)
const body = await _fetch<TokenMetadataResponseBody>(
url,
revalidate,
'fetchTokenMetadata',
)
const data = body.Data
return data
@@ -110,7 +138,10 @@ export async function fetchTokenMetadata(symbol: string) {
/**
* @see https://developers.cryptocompare.com/documentation/data-api/asset_v1_data_by_symbol
*/
export async function deprecated_fetchTokenMetadata(symbol: string) {
export async function deprecated_fetchTokenMetadata(
symbol: string,
revalidate: Revalidation = CRYPTOCOMPARE_REVALIDATION_TIMES.TOKEN_METADATA,
) {
const url = new URL(
'https://data-api.cryptocompare.com/asset/v1/data/by/symbol',
)
@@ -120,7 +151,11 @@ export async function deprecated_fetchTokenMetadata(symbol: string) {
getRandomApiKey(serverEnv.CRYPTOCOMPARE_API_KEYS),
)
const body = await _fetch<deprecated_TokensMetadataResponseBody>(url, 3600)
const body = await _fetch<deprecated_TokensMetadataResponseBody>(
url,
revalidate,
'deprecated_fetchTokenMetadata',
)
const data = body.Data[symbol]
return data
@@ -132,7 +167,10 @@ export async function deprecated_fetchTokenMetadata(symbol: string) {
*
* @see https://min-api.cryptocompare.com/documentation?key=Other&cat=allCoinsWithContentEndpoint
*/
export async function legacy_research_fetchTokenMetadata(symbol: string) {
export async function legacy_research_fetchTokenMetadata(
symbol: string,
revalidate: Revalidation = CRYPTOCOMPARE_REVALIDATION_TIMES.TOKEN_METADATA,
) {
const url = new URL('https://min-api.cryptocompare.com/data/all/coinlist')
url.searchParams.set('fsym', symbol)
url.searchParams.set(
@@ -142,7 +180,8 @@ export async function legacy_research_fetchTokenMetadata(symbol: string) {
const body = await _fetch<legacy_research_TokenMetadataResponseBody>(
url,
3600,
revalidate,
'legacy_research_fetchTokenMetadata',
)
const data = body.Data[symbol]
@@ -153,7 +192,10 @@ export async function legacy_research_fetchTokenMetadata(symbol: string) {
/**
* @see https://min-api.cryptocompare.com/documentation?key=Price&cat=multipleSymbolsFullPriceEndpoint
*/
export async function legacy_fetchTokensPrice(symbols: string[]) {
export async function legacy_fetchTokensPrice(
symbols: string[],
revalidate: Revalidation = CRYPTOCOMPARE_REVALIDATION_TIMES.CURRENT_PRICE,
) {
const url = new URL('https://min-api.cryptocompare.com/data/pricemultifull')
url.searchParams.set('fsyms', symbols.join(','))
url.searchParams.set('tsyms', 'USD')
@@ -164,7 +206,11 @@ export async function legacy_fetchTokensPrice(symbols: string[]) {
)
try {
const body = await _fetch<legacy_TokensPriceResponseBody>(url, 15)
const body = await _fetch<legacy_TokensPriceResponseBody>(
url,
revalidate,
'legacy_fetchTokensPrice',
)
const data = body.RAW
return data
@@ -181,7 +227,7 @@ export async function legacy_fetchTokensPrice(symbols: string[]) {
const filteredSymbols = symbols.filter(s => s !== failedSymbol)
if (filteredSymbols.length > 0) {
return await legacy_fetchTokensPrice(filteredSymbols)
return await legacy_fetchTokensPrice(filteredSymbols, revalidate)
}
}
}
@@ -198,6 +244,7 @@ export async function legacy_fetchTokensPrice(symbols: string[]) {
export async function fetchTokensPriceForDate(
symbols: string[],
timestamp: number,
revalidate: Revalidation = CRYPTOCOMPARE_REVALIDATION_TIMES.PRICE_FOR_DATE,
) {
const data: Record<string, { USD: { PRICE: number } }> = {}
@@ -214,7 +261,11 @@ export async function fetchTokensPriceForDate(
getRandomApiKey(serverEnv.CRYPTOCOMPARE_API_KEYS),
)
const body = await _fetch<legacy_TokenPriceHistoryResponseBody>(url, 15)
const body = await _fetch<legacy_TokenPriceHistoryResponseBody>(
url,
revalidate,
'fetchTokensPriceForDate',
)
const prices = body.Data.Data
if (prices.length > 0) {
@@ -239,13 +290,16 @@ async function _fetch<
| legacy_research_TokenMetadataResponseBody
| legacy_TokenPriceHistoryResponseBody
| TokenMetadataResponseBody,
>(url: URL, revalidate: number): Promise<T> {
>(url: URL, revalidate: number, tag: string): Promise<T> {
const response = await fetch(url, {
// why: https://nextjs.org/docs/app/building-your-application/data-fetching/fetching#reusing-data-across-multiple-functions
// why: https://github.com/vercel/next.js/issues/70946
cache: 'no-store', // no caching
cache: 'force-cache',
next: {
revalidate,
// todo?: revalidate on error
// @see https://github.com/vercel/next.js/discussions/57792 for vercel caching error response
tags: [tag],
},
})