Skip to content

fix: show Solana balance and SPL tokens in side panel#42

Merged
BitHighlander merged 3 commits intodevelopfrom
fix/solana-balance-via-portfolio-endpoint
Apr 21, 2026
Merged

fix: show Solana balance and SPL tokens in side panel#42
BitHighlander merged 3 commits intodevelopfrom
fix/solana-balance-via-portfolio-endpoint

Conversation

@BitHighlander
Copy link
Copy Markdown
Collaborator

Summary

Three layered bugs were each sufficient to zero out Solana balance display; all three are fixed together because fixing any one alone leaves the chain still broken.

  • Wrong CAIPshortListSymbolToCaip['SOL'] pointed at wrapped-SOL SPL token (…/solana:so111…, all lowercase). Now points at native SOL solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501, matching pioneer-caip's ChainToCaip and vault-v11's config.
  • Wrong Pioneer endpoint/charts/portfolio returns empty for any Solana pubkey (verified with direct curl against the live API). Vault-v11 uses /portfolio via pioneer.GetPortfolioBalances, which returns SOL + SPL tokens in one flat array. Solana pubkeys are now routed to a third batch hitting /portfolio (authenticated with key:public-*); EVM/UTXO still use /charts/portfolio for its Zapper/Unchained token data.
  • Response case mismatch — Pioneer echoes CAIP/networkId back lowercase regardless of request casing, but the side-panel asset list uses canonical mixed-case network IDs from ChainToNetworkId. Strict b.networkId === asset.networkId matches in Balances.tsx silently dropped every Solana entry. Solana response entries are now rewritten to canonical casing before they enter the merged balances array.
  • First-run racefetchBalancesFromPioneer() fired before prefetchSolanaPubkey() persisted the Solana pubkey, so run 1 never included Solana at all. A forced refetch is now chained on prefetch resolution.

Ground-truth verification

For the exact address derived from the device at m/44'/501'/0'/0' (CD9R61PMZFafFQ9QsPZATm74hFyEvYaNtEtwGvvHmRYH), Pioneer's /portfolio returns:

native   SOL      bal=0.23898842  usd=20.40
token    USDC     bal=8.56099     usd=8.56
token    CLUBMOON bal=6.6
token    36HAbP   bal=199980

Test plan

  • Reload the extension with a KeepKey holding SOL — Solana card shows non-zero native balance and USD value in the side panel
  • SPL tokens appear under Solana's Tokens tab with correct balances
  • Cold start (fresh install / extension reset) shows Solana balance within a second or two of the initial load
  • Other chains (ETH, BTC, BCH, etc.) continue to show balances and ERC-20 tokens as before — no regression from the endpoint split
  • Background service worker console shows [fetchBalances] solana batch: N natives, M tokens after Solana pubkey is registered

🤖 Generated with Claude Code

BitHighlander and others added 3 commits April 20, 2026 21:56
…rows

Replace the plain centered spinner + "Loading balances…" text with a
multi-layer animated hero spinner (triple counter-rotating rings, pulsing
glow, breathing center dot) above skeleton cards that mirror the real asset
row layout. Adds a subtle kk-logo watermark behind everything and a teal
shimmer sweep that travels across each skeleton card staggered by 150ms.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Three layered bugs were each sufficient to zero out Solana balance display;
all three are fixed together because fixing any one alone leaves the chain
still broken:

1. Wrong CAIP in shortListSymbolToCaip['SOL'] / shortListNameToCaip.solana.
   Previously pointed at wrapped-SOL SPL token
   (solana:.../solana:so111…, all lowercase). Now points at native SOL
   (solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501), matching
   pioneer-caip's ChainToCaip and vault-v11's config.

2. Wrong Pioneer endpoint for Solana. /charts/portfolio returns empty
   {balances:[], tokens:[]} for any Solana pubkey (verified via direct
   curl). Vault-v11 uses /portfolio via pioneer.GetPortfolioBalances,
   which returns natives + SPL tokens in one flat array. Route Solana
   pubkeys to a third batch hitting /portfolio with the required
   key:public-* Authorization header; EVM/UTXO still go through
   /charts/portfolio for its richer Zapper/Unchained token data.

3. Response case mismatch. Pioneer echoes CAIP/networkId back in
   lowercase regardless of request casing. The side-panel asset list
   uses canonical mixed-case network IDs from ChainToNetworkId, so
   strict b.networkId === asset.networkId comparisons in Balances.tsx
   silently dropped every Solana entry. Rewrite Solana entries to
   canonical casing before they enter the merged balances array.

Also eliminates a first-run race: the initial fetchBalancesFromPioneer()
fired before prefetchSolanaPubkey() persisted the Solana pubkey, so run 1
never included Solana at all. Chain a forced refetch on prefetch resolution
so the Solana entry lands in cachedBalances before the UI mounts.

Verified against the live Pioneer API: for the exact address the client
derives from the device at m/44'/501'/0'/0', /portfolio returns
{native SOL + 3 SPL tokens}.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…es to UI

Addresses two PR review findings:

1. HIGH: stale earlier fetch could clobber a later fetch's result. The
   first cold-start fetchBalancesFromPioneer() starts before the Solana
   pubkey is persisted, and the chained forceRefresh fetch runs in
   parallel. If the forced fetch finishes first (correctly populating
   cachedBalances with SOL + SPL tokens) and the original slower fetch
   finishes later, the original unconditionally overwrote cachedBalances
   back to the pre-Solana snapshot. Fix: tag each fetch with a monotonic
   latestFetchId bumped only when real work starts (not for dedup-return
   paths), and only commit to cachedBalances if this fetch is still the
   most recent (myFetchId === latestFetchId). Superseded fetches log
   their discard and return their result to direct callers without
   touching the cache.

2. MEDIUM: UI never observed late cache updates. Balances.tsx and
   SidePanel.tsx each fetched GET_APP_BALANCES once and then stopped
   listening, so the cold-start forced refetch that lands Solana after
   the panel mounts was invisible to users. Fix: background now emits
   BALANCES_UPDATED via chrome.runtime.sendMessage every time
   cachedBalances is successfully written. Balances.tsx, SidePanel.tsx,
   and Tokens.tsx subscribe to that message and re-fetch so the UI
   reflects the latest cache without the user having to manually
   refresh.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@BitHighlander BitHighlander merged commit ef86de6 into develop Apr 21, 2026
3 of 4 checks passed
@BitHighlander BitHighlander deleted the fix/solana-balance-via-portfolio-endpoint branch April 21, 2026 03:12
BitHighlander added a commit that referenced this pull request Apr 21, 2026
…back

networkIdToIcon() was mapping Solana to the wrapped-SOL SPL CAIP
(solana:.../solana:So111…), which returns 403 on
keepkey.info/coins/<base64-caip>.png. The Avatar component fell back to
rendering the first letter of the network name — a green "S" badge where
the logo should be. Native SOL's slip44:501 CAIP has a real icon (200),
matching pioneer-caip's ChainToCaip convention used elsewhere in the
codebase after PR #42.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@BitHighlander BitHighlander mentioned this pull request Apr 21, 2026
4 tasks
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.

1 participant