Skip to content

feat: route custom token metadata imports through proxy#12040

Merged
kaladinlight merged 12 commits intodevelopfrom
feat/custom-token-metadata-proxy-v2
Mar 17, 2026
Merged

feat: route custom token metadata imports through proxy#12040
kaladinlight merged 12 commits intodevelopfrom
feat/custom-token-metadata-proxy-v2

Conversation

@0xApotheosis
Copy link
Member

@0xApotheosis 0xApotheosis commented Feb 26, 2026

⚠️ Requires unchained friend to be merged for the proxy to deploy: shapeshift/unchained#1265

Description

Route custom token metadata import lookups through api.proxy.shapeshift.com instead of direct browser Alchemy/Metaplex calls. The primary motivation is that our Alchemy key is being abused since it's checked into the repo. This PR puts it behind a proxy so the key isn't exposed client-side. The key will be rotated once this PR merges.

  • Remove dead client-side Alchemy SDK usage and centralize supported custom-token chain IDs
  • Remove committed VITE_ALCHEMY_POLYGON_URL and related config/type references
  • Update CSP for proxy-based metadata lookups

Issue (if applicable)

N/A

Risk

Low-medium. This changes how custom token metadata is fetched (proxy vs direct Alchemy calls), but does not affect any on-chain transactions, wallet interactions, or core state management. If the proxy is unavailable, custom token imports would fail to resolve metadata.

What protocols, transaction types, wallets or contract interactions might be affected by this PR?

No protocols, transaction types, wallets, or contract interactions are affected. This only impacts the custom token import metadata lookup flow.

Testing

Engineering

  • Verify custom token import flow works end-to-end on this branch

Operations

  • Import a custom token (e.g. a lesser-known ERC-20 on Ethereum) via the trade asset search and verify metadata (name, symbol, icon) resolves correctly
  • Verify custom token import on Polygon and other supported chains

Screenshots (if applicable)

N/A

Summary by CodeRabbit

  • New Features

    • Loading indicator shows while search results are fetched.
  • Improvements

    • Custom token import fetches metadata via a proxy API for more consistent results.
    • Searches by contract address return fuller results (market-cap filtering skipped).
    • Asset deduplication/grouping improved when searching by token address.
  • Chores

    • Removed Alchemy SDK and related integrations; added proxy API base URL configuration.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

Warning

Rate limit exceeded

@kaladinlight has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 8 minutes and 27 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 727a8d85-2ca9-414c-b4b1-f1ec40babd07

📥 Commits

Reviewing files that changed from the base of the PR and between b1a04e8 and 059f9b9.

📒 Files selected for processing (1)
  • src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx
📝 Walkthrough

Walkthrough

Migrates custom token metadata retrieval from the Alchemy SDK to a proxy API: removes Alchemy env vars, SDK, CSP entries, and related modules; adds VITE_PROXY_API_BASE_URL and CUSTOM_TOKEN_IMPORT_SUPPORTED_CHAIN_IDS; refactors token search hooks, deduplication, and UI to use HTTP proxy and store-based asset handling. (34 words)

Changes

Cohort / File(s) Summary
Environment & Types
.env, .env.development, src/vite-env.d.ts, src/config.ts
Removed Alchemy environment keys, added VITE_PROXY_API_BASE_URL, and updated ImportMetaEnv/type validators and config validators.
CSP & Security
headers/csps/alchemy.ts, headers/csps/chains/ethereum.ts, headers/csps/index.ts
Deleted Alchemy CSP file, removed Alchemy entry from exported CSPs, and removed Alchemy polygon URL from Ethereum connect-src.
Dependency / SDK removal
package.json, src/lib/alchemySdkInstance.ts
Removed alchemy-sdk dependency and deleted the Alchemy SDK instance factory and its supported-chain constants.
Proxy & Supported Chains
src/lib/customTokenImportSupportedChainIds.ts, src/config.ts
Added CUSTOM_TOKEN_IMPORT_SUPPORTED_CHAIN_IDS export and added proxy base URL validator/default.
Custom Token Query & API
src/components/TradeAssetSearch/hooks/useGetCustomTokensQuery.tsx
Replaced Solana/EVM on-chain logic with Axios HTTP proxy calls; changed returned item shape to MinimalAsset[]; simplified error handling and query branching.
Search UI & Modal
src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx, src/components/Layout/Header/GlobalSearch/AssetSearchResults.tsx, src/components/TradeAssetSearch/components/SearchTermAssetList.tsx
Switched to store-based asset lookup and CUSTOM_TOKEN_IMPORT_SUPPORTED_CHAIN_IDS; renamed isSearchingisLoading, added Spinner UI, simplified custom-asset upsert and mapping logic.
Asset dedupe & filtering
src/lib/assetSearch/deduplicateAssets.ts, src/state/slices/common-selectors.ts, src/lib/assetSearch/deduplicateAssets.test.ts
Added address-aware deduplication (use assetId familyKey for contract-address searches); bypassed market-cap filters when searching by contract address; updated/added tests with concrete addresses.
Monitoring docs
src/index.tsx, src/utils/sentry/httpclient.ts
Removed alchemy.com from Sentry denyUrls and trimmed example denyUrls comment.

Sequence Diagram

sequenceDiagram
    actor User
    participant SearchUI as GlobalSearch / UI
    participant Hook as useGetCustomTokensQuery
    participant Proxy as Proxy API
    participant Store as Asset Store

    User->>SearchUI: Enter token address + chain
    SearchUI->>Hook: Request token metadata (address, chainId)
    Hook->>Proxy: HTTP GET /custom-token?address=...&chainId=...
    Proxy-->>Hook: Return MinimalAsset[]
    Hook-->>SearchUI: Return MinimalAsset[]
    SearchUI->>Store: selectAssets() / assetsById
    Store-->>SearchUI: Existing assets map
    SearchUI->>Store: Upsert new assets (makeAsset / upsert)
    Store-->>SearchUI: Confirm upsert
    SearchUI-->>User: Render results (or Spinner / SearchEmpty)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped from Alchemy's vine,

to a lean proxy path, swift and fine.
No keys to hide, no SDK weight,
Tokens fetched quick — we celebrate!
Thump-thump, the search sings: new results await.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: route custom token metadata imports through proxy' clearly and concisely describes the main change in the PR: redirecting custom token metadata fetches through a proxy API instead of direct client-side calls.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/custom-token-metadata-proxy-v2
📝 Coding Plan
  • Generate coding plan for human review comments

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Collaborator

@NeOMakinG NeOMakinG left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — Clean refactoring

Changes:

  1. Proxy routing — Custom token metadata lookups now go through api.proxy.shapeshift.com instead of direct Alchemy calls
  2. Removed committed API keysVITE_ALCHEMY_API_KEY, VITE_ALCHEMY_POLYGON_URL, VITE_ALCHEMY_SOLANA_BASE_URL removed from .env
  3. Centralized chain IDs — New CUSTOM_TOKEN_IMPORT_SUPPORTED_CHAIN_IDS constant replaces scattered Alchemy SDK references
  4. CSP updates — Added proxy URL, removed direct Alchemy URLs

Benefits:

  • No more client-side API keys
  • Centralized monitoring and rate limiting via proxy
  • Cleaner code with single source of truth for supported chains

CI passes ✅


🤖 Reviewed by Claude Code

@NeOMakinG
Copy link
Collaborator

🤖 QABot Test Report

1/1 tests passed

Test Status
Code Review (proxy routing) ✅ Passed

Verified: Custom token metadata now routes through proxy API, committed API keys removed.

📊 Full Report: https://qabot-kappa.vercel.app/runs/27ac5bb7-4fe0-4104-96be-c8a4055b6c6d


🤖 Automated QA by Claude Code

@kaladinlight kaladinlight marked this pull request as ready for review March 16, 2026 21:31
@kaladinlight kaladinlight requested a review from a team as a code owner March 16, 2026 21:31
@kaladinlight
Copy link
Contributor

  • cleaned up the hook to return minimal assets to reduce the need for that duplicated logic in components
  • remove alchemy-sdk as we are no longer using it and cleaned up the csp headers
  • added loading spinner to AssetSearchResults to avoid displaying "No tokens found" while searching for custom tokens
  • allow searching for low market cap tokens when searching by contract address
  • fix dedupe logic to display all matching tokens when searching by contract address

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/components/TradeAssetSearch/hooks/useGetCustomTokensQuery.tsx (1)

86-91: Unconventional skipToken usage pattern.

Returning skipToken from within the query function is not the standard react-query pattern. The idiomatic approach is queryFn: condition ? skipToken : () => fetchData(). However, since the combine function (line 105) filters out skipToken values, this works as a sentinel value approach.

If this pattern causes any issues (e.g., react-query warnings or unexpected query states), consider refactoring to pass skipToken directly to queryFn when the address is invalid.

💡 Optional: Idiomatic skipToken pattern
+  const isValidAddress = isValidEvmAddress || isValidSolanaAddress
+
   const customTokenQueries = useQueries({
     queries: chainIds.map(chainId => ({
       queryKey: getCustomTokenQueryKey(contractAddress, chainId),
-      queryFn: getQueryFn(chainId),
+      queryFn: isValidAddress ? () => getCustomToken(chainId) : skipToken,
       enabled: customTokenImportEnabled,
       staleTime: Infinity,
     })),
     combine: queries => mergeQueryOutputs(queries, results => results.filter(isMinimalAsset)),
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TradeAssetSearch/hooks/useGetCustomTokensQuery.tsx` around
lines 86 - 91, The current getQueryFn returns the sentinel skipToken from inside
the returned function which is non-idiomatic and can trigger react-query
warnings; change getQueryFn so it returns skipToken directly as the queryFn when
the address is invalid instead of returning skipToken from the inner function —
i.e., for a given chainId, return skipToken if (!isValidEvmAddress &&
!isValidSolanaAddress) else return a function that calls
getCustomToken(chainId); keep references to getCustomToken, isValidEvmAddress,
isValidSolanaAddress and ensure the combine call (which filters skipToken)
continues to work with this refactor.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/Layout/Header/GlobalSearch/AssetSearchResults.tsx`:
- Around line 22-31: The no-results empty state (SearchEmpty) is currently gated
by !isSearching and can be skipped when the parent keeps isSearching true for
active queries; update AssetSearchResults so that when noResults && searchQuery
is true it returns <SearchEmpty searchQuery={searchQuery} /> regardless of
isSearching (e.g., check/render SearchEmpty before the spinner or change the
spinner condition), leaving the Spinner shown only for active searches that are
not already known to have no results.

---

Nitpick comments:
In `@src/components/TradeAssetSearch/hooks/useGetCustomTokensQuery.tsx`:
- Around line 86-91: The current getQueryFn returns the sentinel skipToken from
inside the returned function which is non-idiomatic and can trigger react-query
warnings; change getQueryFn so it returns skipToken directly as the queryFn when
the address is invalid instead of returning skipToken from the inner function —
i.e., for a given chainId, return skipToken if (!isValidEvmAddress &&
!isValidSolanaAddress) else return a function that calls
getCustomToken(chainId); keep references to getCustomToken, isValidEvmAddress,
isValidSolanaAddress and ensure the combine call (which filters skipToken)
continues to work with this refactor.
🪄 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: c9484e74-15f4-42be-9668-5b41dd8eafba

📥 Commits

Reviewing files that changed from the base of the PR and between d14b850 and 18fe7f6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (18)
  • .env
  • .env.development
  • headers/csps/alchemy.ts
  • headers/csps/chains/ethereum.ts
  • headers/csps/index.ts
  • package.json
  • src/components/Layout/Header/GlobalSearch/AssetSearchResults.tsx
  • src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx
  • src/components/TradeAssetSearch/components/SearchTermAssetList.tsx
  • src/components/TradeAssetSearch/hooks/useGetCustomTokensQuery.tsx
  • src/config.ts
  • src/index.tsx
  • src/lib/alchemySdkInstance.ts
  • src/lib/assetSearch/deduplicateAssets.ts
  • src/lib/customTokenImportSupportedChainIds.ts
  • src/state/slices/common-selectors.ts
  • src/utils/sentry/httpclient.ts
  • src/vite-env.d.ts
💤 Files with no reviewable changes (5)
  • headers/csps/chains/ethereum.ts
  • src/lib/alchemySdkInstance.ts
  • package.json
  • headers/csps/index.ts
  • headers/csps/alchemy.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx`:
- Around line 71-73: The forEach callback in GlobalSearchModal.tsx implicitly
returns the result of dispatch (Biome flags this); change the callback to a
block-bodied function so it returns void — e.g. replace the arrow shorthand with
a block that calls
dispatch(assetsSlice.actions.upsertAsset(makeAsset(assetsById, token))); (or use
a for...of loop over customTokens.filter(...)) so the callback does not return
the dispatch value. Ensure you keep the same identifiers: customTokens,
assetsById, dispatch, assetsSlice.actions.upsertAsset, and makeAsset.
- Around line 61-64: The destructure of useGetCustomTokensQuery in
GlobalSearchModal drops proxy failures and lets the UI fall through to
SearchEmpty; update GlobalSearchModal to read the hook's error and isError
(e.g., const { data: customTokens, isLoading: isLoadingCustomTokens, error,
isError } = useGetCustomTokensQuery(...)) and when isError or error indicates a
proxy/5xx failure call the useErrorToast hook (useErrorToast(...)) to surface a
translated error message and log details, then prevent showing SearchEmpty as a
normal “no match” result (e.g., show error state or early return) so proxy
lookup failures are routed through the normal toast/logging path rather than
silently appearing as no results.
- Around line 66-74: The render flashes SearchEmpty because customTokens are
upserted asynchronously and isLoadingCustomTokens can flip false before the
store-backed results contain the new assetIds; modify the logic that clears the
loading gate (or the component that chooses to render SearchEmpty) to wait until
the upserted assetIds are present in results or alternatively render
customTokens directly when results are missing. Concretely, in the effect around
customTokens (and the similar block at 184-188) ensure you compute the
missingIds by comparing customTokens' assetId to selectAssets(store.getState())
and either block clearing isLoadingCustomTokens until all missingIds are present
in results (or add a boolean derived from results.includes(assetId)), or change
the render branch that uses isLoadingCustomTokens/results to fall back to
rendering customTokens until the store results include those assetIds (use
symbols: customTokens, selectAssets, assetsSlice.actions.upsertAsset, makeAsset,
isLoadingCustomTokens, results, SearchEmpty).

In `@src/lib/assetSearch/deduplicateAssets.test.ts`:
- Around line 90-93: The tests use real-looking address assetIds (axlusdcFamily,
axlusdcPrimary, AXLUSDC_OPTIMISM) but only exercise symbol/name search paths;
add assertions that explicitly perform an address-based search (using the
address-like assetId string such as axlusdcFamily or axlusdcPrimary.assetId) and
assert deduplicateAssets (or the test helper invoking the address search branch)
returns the expected deduplicated result for the address path; update the
existing test blocks that reference the AXLUSDC fixtures (the sections around
the axlusdcFamily / axlusdcPrimary fixtures and the later AXLUSDC_OPTIMISM-based
assertions) to include these address-search assertions so the new
contract-address deduplication branch is covered.
🪄 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: 865e68ea-536c-4ff8-8467-43944c0e7665

📥 Commits

Reviewing files that changed from the base of the PR and between 18fe7f6 and fd9ef04.

📒 Files selected for processing (3)
  • src/components/Layout/Header/GlobalSearch/AssetSearchResults.tsx
  • src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx
  • src/lib/assetSearch/deduplicateAssets.test.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx`:
- Line 87: Remove the stray debugging console.log in the GlobalSearchModal
component: delete the console.log({ searchAssets, customAssets, results }) call
so the modal no longer logs on every keystroke; locate this call inside the
GlobalSearchModal (component/function) render/effect where searchAssets,
customAssets, and results are referenced and simply remove the line or replace
it with a non-logging alternative (e.g., a conditional debug-only logger behind
a dev flag) if needed for local debugging.

In `@src/components/TradeAssetSearch/components/SearchTermAssetList.tsx`:
- Around line 109-117: customAssets currently skips the same
filtering/eligibility path used by assetsForChain and only checks
!assetsById[token.assetId], which can surface assets the caller filtered out or
drop valid proxy hits; change the logic in SearchTermAssetList so you first
materialize proxy-backed assets from customTokens (using the same
makeAsset/proxy resolution path), run those proxy assets through the exact
eligibility filters used by assetsForChain (the same predicates that produce the
list passed to existingAssetIds), and only after that dedupe against
existingAssetIds/assetsById before returning customAssets so proxy results
respect the same chain/list filters and are correctly de-duplicated.
🪄 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: 593aff5f-23c4-43fe-9d61-e0ebcb405067

📥 Commits

Reviewing files that changed from the base of the PR and between fd9ef04 and b1a04e8.

📒 Files selected for processing (4)
  • src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx
  • src/components/TradeAssetSearch/components/SearchTermAssetList.tsx
  • src/lib/assetSearch/deduplicateAssets.test.ts
  • src/lib/assetSearch/deduplicateAssets.ts

@kaladinlight kaladinlight enabled auto-merge (squash) March 17, 2026 03:27
@kaladinlight kaladinlight merged commit 46bb320 into develop Mar 17, 2026
4 checks passed
@kaladinlight kaladinlight deleted the feat/custom-token-metadata-proxy-v2 branch March 17, 2026 03:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants