Ability to add tokens in listing builder#1376
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
WalkthroughThis PR implements a complete Jupiter token integration system: a new ChangesJupiter Token Integration
Sequence Diagram(s)sequenceDiagram
participant User
participant TokenSelect
participant API as /api/tokens
participant JupAPI as Jupiter API
participant DB as Local DB
User->>TokenSelect: Type search term
TokenSelect->>TokenSelect: Filter local tokens
alt Local matches found
TokenSelect-->>User: Show local results
else Term >= 2 chars, no local match
TokenSelect->>TokenSelect: Debounce 350ms
TokenSelect->>API: GET /api/tokens?query=term
par Concurrent search
API->>DB: searchTokenList(term)
API->>JupAPI: GET /tokens/v2/search
end
DB-->>API: Matching local tokens
JupAPI-->>API: JupiterToken array
API->>API: normalizeTokenIcon per result
API-->>TokenSelect: Combined results
TokenSelect->>TokenSelect: Partition verified/unverified
TokenSelect-->>User: Show results with Add buttons
end
User->>TokenSelect: Click Add on Jupiter token
TokenSelect->>API: POST /api/tokens with mintAddress
API->>JupAPI: Search Jupiter for mint
JupAPI-->>API: Verified JupiterToken
API->>DB: addVerifiedJupiterToken
DB->>DB: Check conflicts and compute sortOrder
DB-->>API: Stored Token
API-->>TokenSelect: Success response
TokenSelect->>TokenSelect: addTokenToList(token)
TokenSelect->>TokenSelect: Update form and save draft
TokenSelect-->>User: Success toast
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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: 6
🧹 Nitpick comments (6)
src/server/tokenList.ts (1)
13-20: 💤 Low valueUse
Type | undefinedinstead of optional properties on the interface.export interface JupiterToken { id: string; name: string; symbol: string; - icon?: string | null; + icon: string | null | undefined; decimals: number; isVerified: boolean; }As per coding guidelines: "Use
property: Type | undefinedinstead ofproperty?: Typefor TypeScript type definitions to force explicit property passing and prevent bugs from accidentally omitting required properties".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/tokenList.ts` around lines 13 - 20, Update the JupiterToken interface so optional properties use explicit unions with undefined instead of the optional ?: syntax; specifically change the icon property on the JupiterToken interface from an optional declaration to an explicit type that includes undefined (e.g., icon: string | null | undefined) so callers must pass the property explicitly while preserving the null case.src/features/listing-builder/components/Form/Rewards/Tokens/TokenSelect.tsx (2)
38-45: 💤 Low valueAvoid duplicating the
JupiterTokenshape between client and server.This interface is byte-for-byte identical to the one exported from
src/server/tokenList.ts. Since the server module isn't safe to import into a client component, a small shared type module (e.g.src/types/jupiter.ts) imported asimport type { JupiterToken } from '@/types/jupiter'keeps the two in sync.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/listing-builder/components/Form/Rewards/Tokens/TokenSelect.tsx` around lines 38 - 45, Duplicate JupiterToken interface in TokenSelect.tsx; create a shared type file (e.g., src/types/jupiter.ts) that exports type JupiterToken, move the interface there, and then in TokenSelect.tsx replace the local interface with an import type statement (import type { JupiterToken } from '@/types/jupiter'); ensure the shared type exactly matches the server's exported shape and update any other client components to import the shared type instead of redefining it.
59-79: 💤 Low value
getTokenIconSrcoverlaps with the server-sidenormalizeTokenIcon.Every icon reaching this component comes from
/api/tokens(localgetTokenList, the Jupiter search merge, oraddVerifiedJupiterToken), all of which already run throughnormalizeTokenIconon the server. The/cdn/ipfs-io/branch here is therefore dead in the normal flow (server already rewrites it to/api/token-icon?url=...), and thenew URL(value)branch wraps an already-proxied URL.It's reasonable to keep a small client-side guard, but consider either:
- collapsing this to "return value if it starts with
/, otherwisedefaultTokenIcon", since the server is the single source of truth, or- extracting the normalization into a shared util and importing it on both sides.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/listing-builder/components/Form/Rewards/Tokens/TokenSelect.tsx` around lines 59 - 79, getTokenIconSrc duplicates server-side normalizeTokenIcon and thus contains dead branches (the /cdn/ipfs-io/ and URL-wrapping logic); simplify it by either 1) collapsing client-side logic to a minimal guard in getTokenIconSrc that returns the input if it startsWith('/') and otherwise returns defaultTokenIcon (since /api/tokens already normalizes icons), or 2) extract the normalization into a shared utility (e.g., normalizeTokenIcon) and import/use that from this component and the server code so both sides share the same logic; update TokenSelect.tsx to call the chosen single-source-of-truth function and remove the redundant branches.src/pages/api/tokens/index.ts (3)
41-56: ⚡ Quick win
Promise.allmakes the GET endpoint as fragile as Jupiter.If
searchJupiterTokensthrows (Jupiter outage, slow response, malformed JSON), the whole search response 500s even though the local DB results are available and useful. PreferPromise.allSettledso the user still gets local matches.♻️ Proposed change
- const [tokens, jupiterTokens] = await Promise.all([ - searchTokenList(query), - searchJupiterTokens(query), - ]); - - return res.status(200).json({ tokens, jupiterTokens }); + const [tokensResult, jupiterResult] = await Promise.allSettled([ + searchTokenList(query), + searchJupiterTokens(query), + ]); + + const tokens = tokensResult.status === 'fulfilled' ? tokensResult.value : []; + const jupiterTokens = + jupiterResult.status === 'fulfilled' ? jupiterResult.value : []; + if (jupiterResult.status === 'rejected') { + logger.warn(`Jupiter search failed: ${safeStringify(jupiterResult.reason)}`); + } + return res.status(200).json({ tokens, jupiterTokens });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/api/tokens/index.ts` around lines 41 - 56, Replace the fragile Promise.all call in the GET handler with Promise.allSettled so a failure in searchJupiterTokens doesn't 500 the whole response: call Promise.allSettled([searchTokenList(query), searchJupiterTokens(query)]), inspect the results for each promise (the fulfilled value from searchTokenList should be used as tokens; if searchJupiterTokens fulfilled include jupiterTokens, otherwise set jupiterTokens to [] or omit it), and still return res.status(200).json with available data; update references to searchTokenList and searchJupiterTokens and ensure errors from rejected results are logged but do not throw.
14-30: ⚡ Quick winAdd a timeout and validate the upstream response shape.
Two issues with
searchJupiterTokens:
- The
fetchhas noAbortController/deadline — a slow Jupiter response blocks the request thread (and, withPromise.all, blocks the GET handler from returning local results too).(await response.json()) as JupiterToken[]trusts the upstream payload; individual elements aren't validated, so downstream code (and the POST flow that depends onisVerified) operates on untyped data. A Zod schema withsafeParsewould catch malformed entries.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/api/tokens/index.ts` around lines 14 - 30, searchJupiterTokens currently calls fetch without a timeout and casts response.json() to JupiterToken[] blindly; add an AbortController with a short deadline (e.g., 3s) and pass its signal into fetch, aborting on timeout to ensure slow Jupiter responses don't block Promise.all. After getting response.json(), validate the payload using a Zod schema for the JupiterToken shape (include required fields such as id, symbol, name, icon, isVerified) and use safeParse to handle malformed data; on parse failure or non-array shapes log/return an empty array (or filter out invalid entries) and only map valid tokens through normalizeTokenIcon before returning. Ensure errors from fetch, timeout, or validation are caught and converted into a controlled response (not uncaught exceptions) so downstream code relying on isVerified receives typed/validated data.
59-66: ⚡ Quick winValidate the POST body with Zod per the API guideline.
The body is only checked with
typeof === 'string'. A Zod schema withsafeParseprovides proper error messages, length/format limits on the mint address, and a uniform validation surface.♻️ Sketch
const PostBodySchema = z.object({ mintAddress: z.string().trim().min(32).max(64), }); const parsed = PostBodySchema.safeParse(req.body); if (!parsed.success) return res.status(403).json({ error: parsed.error.message });As per coding guidelines: "Use Zod schemas for request validation with safeParse and return 403 status with validation errors on failure".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/api/tokens/index.ts` around lines 59 - 66, Replace the ad-hoc typeof check with a Zod schema: create a PostBodySchema (z.object({ mintAddress: z.string().trim().min(32).max(64) })) and call PostBodySchema.safeParse(req.body); if parsed.success is false return res.status(403).json({ error: parsed.error.message }); otherwise set mintAddress = parsed.data.mintAddress and remove the existing typeof/trim/empty check; use safeParse and the schema to validate req.body, mintAddress, and produce the 403 validation response.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/features/listing-builder/components/Form/Rewards/Tokens/TokenSelect.tsx`:
- Line 57: The defaultTokenIcon constant in TokenSelect.tsx is pointing to the
wrong path ('/tokens/dollar.svg') causing 404s; update the value of
defaultTokenIcon to the existing public asset path ('/assets/dollar.svg') and
ensure any fallback logic in the TokenSelect component uses this constant so
broken icon URLs fall back to the correct asset.
In `@src/pages/api/token-icon.ts`:
- Around line 86-87: Replace the direct console.error call in the token-icon
proxy error block with the shared logger: import the logger from '@/lib/logger'
and log the error with logger.error('Failed to proxy token icon',
safeStringify(error)) using the existing safeStringify utility (import if
missing), and return res.status(500).end() instead of 502; target the error
handling inside the token-icon API handler where 'Failed to proxy token icon' is
currently logged.
- Around line 48-88: Add an AbortController-based timeout around the upstream
fetch used in the try block (the call to fetch(iconUrl, { redirect: 'follow',
... })) so the request cannot hang indefinitely; create an AbortController, pass
controller.signal to fetch, start a setTimeout to call controller.abort() after
a short deadline (e.g. a few seconds), and clear the timeout after the fetch
completes; ensure the catch path treats an abort as a timeout (responding with
an appropriate 5xx/504) and still logs the error, leaving the rest of the
validation logic (content-type, content-length, MAX_ICON_SIZE_BYTES, and
isPrivateHost checks) unchanged.
- Around line 7-20: The current host blocklist (PRIVATE_HOST_PATTERNS and
isPrivateHost) misses IPv6 ULA and link-local ranges and IPv4-mapped IPv6 forms;
update isPrivateHost to parse the hostname as an IP using an IP parsing library
(e.g., ipaddr.js), detect IPv4, IPv6, and IPv4-mapped-IPv6 addresses, and check
membership against CIDRs fc00::/7, fe80::/10 and the IPv4 private ranges (plus
::1/128) before falling back to the existing string/regex checks; specifically,
replace the naive regex check in isPrivateHost with a parse-and-check flow that
uses ipaddr.js' parse/parseCIDR and range checking for PRIVATE_HOST_PATTERNS
equivalents while preserving the '.localhost' hostname rule.
In `@src/pages/api/tokens/index.ts`:
- Around line 32-86: The POST branch in the exported handler allows
unauthenticated writes via addVerifiedJupiterToken (upsert into tokenMetadata);
restrict this by removing POST from pages/api/tokens/index.ts and either (a)
create a new POST-only endpoint (e.g., pages/api/tokens/add.ts) whose handler
requires sponsor auth and wraps the logic in withSponsorAuth, or (b) gate the
POST branch inside the current handler by validating the session with
withSponsorAuth/withAuth before calling addVerifiedJupiterToken; ensure only
authenticated sponsor users can call the logic that upserts token data.
---
Nitpick comments:
In `@src/features/listing-builder/components/Form/Rewards/Tokens/TokenSelect.tsx`:
- Around line 38-45: Duplicate JupiterToken interface in TokenSelect.tsx; create
a shared type file (e.g., src/types/jupiter.ts) that exports type JupiterToken,
move the interface there, and then in TokenSelect.tsx replace the local
interface with an import type statement (import type { JupiterToken } from
'@/types/jupiter'); ensure the shared type exactly matches the server's exported
shape and update any other client components to import the shared type instead
of redefining it.
- Around line 59-79: getTokenIconSrc duplicates server-side normalizeTokenIcon
and thus contains dead branches (the /cdn/ipfs-io/ and URL-wrapping logic);
simplify it by either 1) collapsing client-side logic to a minimal guard in
getTokenIconSrc that returns the input if it startsWith('/') and otherwise
returns defaultTokenIcon (since /api/tokens already normalizes icons), or 2)
extract the normalization into a shared utility (e.g., normalizeTokenIcon) and
import/use that from this component and the server code so both sides share the
same logic; update TokenSelect.tsx to call the chosen single-source-of-truth
function and remove the redundant branches.
In `@src/pages/api/tokens/index.ts`:
- Around line 41-56: Replace the fragile Promise.all call in the GET handler
with Promise.allSettled so a failure in searchJupiterTokens doesn't 500 the
whole response: call Promise.allSettled([searchTokenList(query),
searchJupiterTokens(query)]), inspect the results for each promise (the
fulfilled value from searchTokenList should be used as tokens; if
searchJupiterTokens fulfilled include jupiterTokens, otherwise set jupiterTokens
to [] or omit it), and still return res.status(200).json with available data;
update references to searchTokenList and searchJupiterTokens and ensure errors
from rejected results are logged but do not throw.
- Around line 14-30: searchJupiterTokens currently calls fetch without a timeout
and casts response.json() to JupiterToken[] blindly; add an AbortController with
a short deadline (e.g., 3s) and pass its signal into fetch, aborting on timeout
to ensure slow Jupiter responses don't block Promise.all. After getting
response.json(), validate the payload using a Zod schema for the JupiterToken
shape (include required fields such as id, symbol, name, icon, isVerified) and
use safeParse to handle malformed data; on parse failure or non-array shapes
log/return an empty array (or filter out invalid entries) and only map valid
tokens through normalizeTokenIcon before returning. Ensure errors from fetch,
timeout, or validation are caught and converted into a controlled response (not
uncaught exceptions) so downstream code relying on isVerified receives
typed/validated data.
- Around line 59-66: Replace the ad-hoc typeof check with a Zod schema: create a
PostBodySchema (z.object({ mintAddress: z.string().trim().min(32).max(64) }))
and call PostBodySchema.safeParse(req.body); if parsed.success is false return
res.status(403).json({ error: parsed.error.message }); otherwise set mintAddress
= parsed.data.mintAddress and remove the existing typeof/trim/empty check; use
safeParse and the schema to validate req.body, mintAddress, and produce the 403
validation response.
In `@src/server/tokenList.ts`:
- Around line 13-20: Update the JupiterToken interface so optional properties
use explicit unions with undefined instead of the optional ?: syntax;
specifically change the icon property on the JupiterToken interface from an
optional declaration to an explicit type that includes undefined (e.g., icon:
string | null | undefined) so callers must pass the property explicitly while
preserving the null case.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c7564532-e280-4de0-8415-44b5c97072f6
📒 Files selected for processing (5)
src/constants/tokenList.tssrc/features/listing-builder/components/Form/Rewards/Tokens/TokenSelect.tsxsrc/pages/api/token-icon.tssrc/pages/api/tokens/index.tssrc/server/tokenList.ts
What does this PR do?
Where should the reviewer start?
How should this be manually tested?
Any background context you want to provide?
What are the relevant issues?
Screenshots (if appropriate)
Summary by CodeRabbit