feat: add token metadata proxy endpoint#1265
Conversation
📝 WalkthroughWalkthroughAdds a new GET /api/v1/tokens/metadata endpoint with IP rate limiting, chain-specific routing for EVM and Solana (via Alchemy RPC), query validation, metadata retrieval, and comprehensive error mapping and responses. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Handler as TokenMetadata Handler
participant RateLimit as Rate Limiter
participant Router as Chain Router
participant EVM as Alchemy EVM RPC
participant Solana as Alchemy Solana RPC
Client->>Handler: GET /api/v1/tokens/metadata?chainId=X&tokenAddress=Y
Handler->>Handler: Validate query params
alt Missing params
Handler->>Client: 400 Bad Request
else Valid params
Handler->>RateLimit: Check IP rate limit
alt Rate limit exceeded
RateLimit->>Handler: Limit exceeded
Handler->>Client: 429 Too Many Requests
else Within limit
RateLimit->>Handler: Allowed
Handler->>Router: Route by chainId
alt EVM chain (eip155:*)
Router->>Handler: EVM routing
Handler->>Handler: Validate address
Handler->>EVM: alchemy_getTokenMetadata RPC
EVM->>Handler: Metadata response
else Solana chain (solana:*)
Router->>Handler: Solana routing
Handler->>Handler: Validate mint address
Handler->>Solana: getAsset RPC call
Solana->>Handler: Asset metadata response
else Unsupported chain
Handler->>Client: 422 Unprocessable Entity
end
alt Metadata found
Handler->>Client: 200 OK + Token metadata
else Not found
Handler->>Client: 404 Not Found
end
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@node/proxy/api/src/tokenMetadata.ts`:
- Around line 82-101: The in-memory requestsByIp map in isRateLimited can grow
unbounded because expired buckets aren't removed; update isRateLimited (or add a
helper it calls) to evict stale entries by iterating requestsByIp and deleting
any entry whose resetAt <= now before inserting or counting a request (use the
same now timestamp and retain existing behavior for count/reset logic),
referencing requestsByIp, isRateLimited, windowMs, and maxRequestsPerWindow so
stale IP buckets are removed and memory stays bounded.
- Line 84: The rate-limiting using req.ip in tokenMetadata.ts will see the
proxy's IP unless Express is configured with trust proxy; update the app
initialization to call app.set('trust proxy', <appropriate value>) (for example
'loopback', 1, or a custom function) in the module that creates the Express app
(where the Express instance is named app) so req.ip returns the client IP behind
your ingress/proxy and per-IP rate limits work correctly.
- Line 23: Replace the current regex-only validator in isValidSolanaAddress with
a lightweight Base58 decoder that decodes the address and verifies the resulting
byte array is exactly 32 bytes (the size of a Solana public key); keep the
function name isValidSolanaAddress and ensure it returns false on decode errors
or wrong length, avoiding adding `@solana/web3.js` as a dependency. This change
will reject malformed Base58 strings that happen to match the regex and yield a
proper 422 validation failure before calling the RPC.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
node/proxy/api/src/app.tsnode/proxy/api/src/tokenMetadata.ts
|
Addressed the CodeRabbit feedback in this branch:
Validation:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
node/proxy/api/src/tokenMetadata.ts (1)
123-154: Rate limiter is process-local; consider shared enforcement for multi-replica deploys.This implementation works per instance, but limits can be bypassed (or uneven) behind load balancing. Consider Redis or ingress-level rate limiting for consistent global behavior.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@node/proxy/api/src/tokenMetadata.ts` around lines 123 - 154, The current isRateLimited method (and its in-memory requestsByIp map, nextCleanupAt, cleanupIntervalMs, windowMs, maxRequestsPerWindow) enforces limits only per process which fails for multi-replica deployments; replace the in-process logic with a shared store or external rate-limiter: migrate the counter/reset logic to a Redis-backed implementation (e.g., use INCR with EXPIRE or a Lua script for atomic sliding-window semantics) or delegate to ingress/edge rate limiting, updating isRateLimited to call the shared Redis helper (or external API) instead of reading/writing requestsByIp and remove the cleanup loop. Ensure keys are derived from req.ip (or X-Forwarded-For) to maintain the same behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@node/proxy/api/src/tokenMetadata.ts`:
- Around line 181-191: The code currently accepts any chainId starting with
"solana:" but always fetches mainnet metadata; update the chainId validation in
the block that handles chainId.startsWith('solana:') to explicitly allow only
supported Solana identifiers (e.g. 'solana:mainnet' or whatever supported list
you maintain) before calling isValidSolanaAddress or getSolanaTokenMetadata, and
if the chainId is unsupported call sendValidationError to return a 422-style
validation error; modify the logic around isValidSolanaAddress,
getSolanaTokenMetadata, and sendValidationError to first check the exact chainId
membership and only proceed to address validation and getSolanaTokenMetadata for
supported chain IDs.
---
Nitpick comments:
In `@node/proxy/api/src/tokenMetadata.ts`:
- Around line 123-154: The current isRateLimited method (and its in-memory
requestsByIp map, nextCleanupAt, cleanupIntervalMs, windowMs,
maxRequestsPerWindow) enforces limits only per process which fails for
multi-replica deployments; replace the in-process logic with a shared store or
external rate-limiter: migrate the counter/reset logic to a Redis-backed
implementation (e.g., use INCR with EXPIRE or a Lua script for atomic
sliding-window semantics) or delegate to ingress/edge rate limiting, updating
isRateLimited to call the shared Redis helper (or external API) instead of
reading/writing requestsByIp and remove the cleanup loop. Ensure keys are
derived from req.ip (or X-Forwarded-For) to maintain the same behavior.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
node/proxy/api/src/app.tsnode/proxy/api/src/tokenMetadata.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- node/proxy/api/src/app.ts
node/proxy/api/src/tokenMetadata.ts
Outdated
| if (chainId.startsWith('solana:')) { | ||
| if (!isValidSolanaAddress(tokenAddress)) { | ||
| sendValidationError(res, { | ||
| tokenAddress: 'Invalid Solana mint address', | ||
| }) | ||
| return null | ||
| } | ||
|
|
||
| const metadata = await this.getSolanaTokenMetadata(chainId, tokenAddress) | ||
| return metadata | ||
| } |
There was a problem hiding this comment.
Reject unsupported Solana chain IDs explicitly.
Any solana:* value is currently accepted, but metadata is always fetched from Solana mainnet. This can return incorrect data for unsupported Solana references instead of a 422 validation error.
🔧 Suggested fix
+const SUPPORTED_SOLANA_CHAIN_IDS = new Set<string>([
+ // Add the Solana CAIP-2 chain IDs this endpoint actually supports.
+ // e.g. 'solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ'
+])
+
if (chainId.startsWith('solana:')) {
+ if (!SUPPORTED_SOLANA_CHAIN_IDS.has(chainId)) {
+ sendValidationError(res, {
+ chainId: `Unsupported chainId: ${chainId}`,
+ })
+ return null
+ }
+
if (!isValidSolanaAddress(tokenAddress)) {
sendValidationError(res, {
tokenAddress: 'Invalid Solana mint address',
})
return null🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@node/proxy/api/src/tokenMetadata.ts` around lines 181 - 191, The code
currently accepts any chainId starting with "solana:" but always fetches mainnet
metadata; update the chainId validation in the block that handles
chainId.startsWith('solana:') to explicitly allow only supported Solana
identifiers (e.g. 'solana:mainnet' or whatever supported list you maintain)
before calling isValidSolanaAddress or getSolanaTokenMetadata, and if the
chainId is unsupported call sendValidationError to return a 422-style validation
error; modify the logic around isValidSolanaAddress, getSolanaTokenMetadata, and
sendValidationError to first check the exact chainId membership and only proceed
to address validation and getSolanaTokenMetadata for supported chain IDs.
Remove hand-rolled Base58 decoder, custom IP rate limiter, viem address
validation, error sanitization helpers, and response envelope. The proxy
now receives chainId + tokenAddress, injects the Alchemy API key,
forwards to the correct endpoint, normalizes the Solana response, and
returns a flat { chainId, tokenAddress, name, symbol, decimals, logo }.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Warn instead of crashing when ALCHEMY_API_KEY is missing, returning 503 from the handler. Add Cache-Control: public, max-age=86400 to successful responses since token metadata is essentially immutable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Solana URL follows the same ALCHEMY_API_KEY pattern as EVM chains. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
/api/v1/tokens/metadatapass-through proxy endpoint for token metadata lookups via Alchemyalchemy_getTokenMetadataand Solana mainnet viagetAssetchainId+tokenAddressquery params, injects the Alchemy API key, and returns a flat normalized response:{ chainId, tokenAddress, name, symbol, decimals, logo }Cache-Control: public, max-age=86400headerALCHEMY_API_KEYis not configuredALCHEMY_API_KEYtosample.envTesting
yarn eslint node/proxy/api/src/tokenMetadata.ts node/proxy/api/src/app.ts— passes/health,/api/v1/tokens/metadata)