Skip to content

fix(qr): recognize scanned Solana and Tron addresses - #2675

Merged
kushagrasarathe merged 3 commits into
mainfrom
fix/qr-scanner-base58-case
Aug 12, 2026
Merged

fix(qr): recognize scanned Solana and Tron addresses#2675
kushagrasarathe merged 3 commits into
mainfrom
fix/qr-scanner-base58-case

Conversation

@abalinda

@abalinda abalinda commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

The QR scanner rejected valid Solana deposit addresses as "Unrecognized QR code".

processQRCode lowercased the scanned payload and passed that copy to recognizeQr. Base58 carries meaning in its case, so the lowercase pass destroyed the address:

  • Solana — an uppercase L is a valid base58 character, a lowercase l is not. Any address holding an L stopped matching. That is ~53% of addresses (measured over random 44-char base58 strings).
  • Tron — every Tron address starts with an uppercase T, and the pattern anchors on ^T. Lowercasing broke 100% of them, always.

Hex addresses were unaffected, which is why this stayed invisible: EVM survives .toLowerCase() because hex is case-insensitive.

The fix

Recognize the raw scan first, and retry lowercased only for an all-uppercase payload:

const recognized = recognizeQr(data) ?? (data === data.toUpperCase() ? recognizeQr(normalized) : null)

The two halves encode when case is information and when it is an artifact:

Payload Case is… Behavior
Solana / Tron base58 dataLl, Tt raw pass matches (the fix)
All-uppercase bech32 / BOLT-11 / hex artifact — QR alphanumeric mode encodes uppercase only, and is denser for it retried lowercased
Mixed-case hex the user's — EIP-55 checksum honoured; viem rejects a bad one

That last row is a deliberate tightening. The old blanket lowercase laundered a mixed-case address with a broken EIP-55 checksum into a valid-looking payment target — which is the exact corruption EIP-55 exists to catch. It now reaches the unrecognized modal instead of the send flow.

normalized still drives the routing below, where hex and ENS are case-insensitive anyway, and recognizeQr lowercases the branches that need it (Peanut URL, PIX) itself.

Task

TASK-21111 — reported twice via support; reporter received the bug bounty.

Evidence

recognizeQr was never the problem. Its 635-line suite passes raw mixed-case data and even asserts case-sensitivity on purpose ('LNBC…' must not match BITCOIN_INVOICE). The single call site lowercased one line earlier and had no test at all — so the new test covers the component, not the parser.

PostHog, last 60 days of qr_scanned:

qr_type scans users
null (unrecognized) 457 215
SOLANA_ADDRESS 6 4
TRON_ADDRESS 0 0

Tron has never once been recognized in production. One unrecognized scan in that window is a confirmed base58 Solana address holding an L.

Production sanity checks behind the design:

  • All 28 distinct mixed-case addresses scanned in 90 days carry a valid EIP-55 checksum — the tightening costs nothing real.
  • All-uppercase payloads are genuinely common (93 in 180 days: EMVCo, FIDO, PIX), which is why the uppercase retry stays.

Risks

Low. Frontend only, one expression, no backend or contract surface.

The one behavior change beyond recognition is the bad-checksum tightening described above — measured at zero production instances.

Recognized Solana/Tron scans now reach the existing "not supported yet / get notified" modal instead of the unrecognized one. See Design notes.

Design notes / accepted trade-offs

Routing is unchanged, deliberately. After this fix a scanned Solana address lands on the existing QR_NOT_SUPPORTED modal. Solana and Tron are live withdraw destinations (chainRegistry.consts.ts, Rhino, behind chain-rollout-solana / chain-rollout-tron), so that copy is arguably stale — but wiring scan-to-send would need a withdraw deep-link with chain preselection plus flag gating. That is a feature, not this bug, and the reported user was scanning Peanut's own deposit address, for which "send to it" is not a meaningful destination anyway. Flagging it rather than scope-creeping.

Revealed, not introduced — uppercase URL schemes. The PEANUT_URL branch strips the protocol with a case-sensitive regex (index.tsx:286), so HTTPS://PEANUT.ME/satoshi routes to //PEANUT.ME/satoshi and 404s. Same QR-uppercasing reason as above, so it is reachable. Pre-existing and untouched by this diff — filed for a follow-up rather than bundled here.

Solana/Bitcoin label ambiguity. A Solana address starting with 1 or 3 (~3.4%) matches the Bitcoin pattern first and is labelled "Bitcoin". Cosmetic, on a modal that says "not supported" either way, and unfixable without real checksum validation. Pre-existing.

QA

npm test — 228 suites, 2925 tests, green.

New pin: src/components/Global/QRScannerOverlay/__tests__/base58-case.test.tsx. It discriminates all three candidate designs — only the shipped one passes all 8:

variant failures
original (lowercase everything) 3 — both base58 cases + checksum laundering
raw-only (no uppercase retry) 3 — all uppercase cases
shipped 0

Manual: open the scanner, scan any Solana address containing an uppercase L (e.g. the QR on Add money → Crypto → Solana). Before: "Unrecognized QR code". After: "Solana not supported yet."

Screenshots

N/A — no visible change. The modal that renders was already built; this PR only changes which one is reached.

Summary by CodeRabbit

  • Bug Fixes
    • Improved QR code scanning for case-sensitive addresses and payment payloads.
    • Preserved valid uppercase and mixed-case formats, including checksummed EVM addresses.
    • Added fallback handling for fully uppercase QR contents.
    • Improved routing for Peanut URLs.
    • Improved validation and handling of invalid address checksums.
    • Enhanced support for Solana, Tron, bech32, and BOLT11 QR payloads.

The scanner lowercased the scanned payload before handing it to
recognizeQr. Base58 carries meaning in its case, so that destroyed the
address: an uppercase L is a valid Solana character while a lowercase l
is not, and every Tron address starts with an uppercase T. About half of
all Solana addresses and every Tron address fell through to the
"Unrecognized QR code" modal.

recognizeQr was always correct and its own suite even asserts
case-sensitivity on purpose. Only the call site was wrong, so the new
test covers the component, not the parser.

Task: TASK-21111
@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview Aug 12, 2026 12:20pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f8475ec9-8d9e-4563-bea2-2f72d4bb0171

📥 Commits

Reviewing files that changed from the base of the PR and between f29b325 and 15dd9a8.

📒 Files selected for processing (2)
  • src/components/Global/QRScannerOverlay/__tests__/base58-case.test.tsx
  • src/components/Global/QRScannerOverlay/index.tsx

📝 Walkthrough

Walkthrough

QRScannerOverlay now preserves scanned-string casing during QR recognition. Regression tests cover case-sensitive addresses, uppercase payload fallback, checksum validation, and Peanut URL routing.

Changes

QR scanner case preservation

Layer / File(s) Summary
Preserve and validate scan case
src/components/Global/QRScannerOverlay/index.tsx, src/components/Global/QRScannerOverlay/__tests__/base58-case.test.tsx
processQRCode passes the original scan string to recognizeQr and falls back to lowercase only for all-uppercase payloads. Tests cover Solana, Tron, EVM, bech32, Lightning, checksum validation, and Peanut URL inputs.

Estimated code review effort: 3 (Moderate) | ~15–30 minutes

Suggested reviewers: innolope-dev

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: fixing QR recognition for Solana and Tron addresses.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/qr-scanner-base58-case

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

@abalinda

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7119.83 → 7120.29 (+0.46)
Findings: 0 net (+6 new, -6 resolved)

🆕 New findings (6)

  • critical complexity — src/components/Global/QRScannerOverlay/index.tsx — CC 81, MI 54.87, SLOC 346
  • high method-complexity — src/components/Global/QRScannerOverlay/index.tsx:229 — CC 35 SLOC 157
  • medium high-mdd — src/components/Global/QRScannerOverlay/index.tsx:208 — QRScannerOverlay: MDD 68.8 (uses across many lines from declarations)
  • medium high-dlt — src/components/Global/QRScannerOverlay/index.tsx:208 — QRScannerOverlay: DLT 44 (calls 44 distinct functions — high context load)
  • medium high-mdd — src/components/Global/QRScannerOverlay/index.tsx:229 — processQRCode: MDD 43.1 (uses across many lines from declarations)
  • medium high-dlt — src/components/Global/QRScannerOverlay/index.tsx:229 — processQRCode: DLT 32 (calls 32 distinct functions — high context load)

✅ Resolved (6)

  • src/components/Global/QRScannerOverlay/index.tsx — CC 80, MI 54.95, SLOC 344
  • src/components/Global/QRScannerOverlay/index.tsx:229 — CC 34 SLOC 155
  • src/components/Global/QRScannerOverlay/index.tsx:208 — QRScannerOverlay: MDD 65.7 (uses across many lines from declarations)
  • src/components/Global/QRScannerOverlay/index.tsx:208 — QRScannerOverlay: DLT 43 (calls 43 distinct functions — high context load)
  • src/components/Global/QRScannerOverlay/index.tsx:229 — processQRCode: MDD 41.5 (uses across many lines from declarations)
  • src/components/Global/QRScannerOverlay/index.tsx:229 — processQRCode: DLT 31 (calls 31 distinct functions — high context load)

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2928 ran, 0 failed, 0 skipped, 38.8s

📊 Coverage (unit)

metric %
statements 66.1%
branches 51.2%
functions 56.3%
lines 66.9%
⏱ 10 slowest test cases
time test
2.8s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
0.8s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.3s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.3s src/hooks/__tests__/useCrispTokenId.test.ts › retries then stays undefined when the endpoint keeps failing (no fallback token)
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/utils/__tests__/sentry.utils.test.ts › still lets a per-call timeoutMs win over the default
0.2s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.2s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.2s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.2s src/utils/__tests__/auth-token.test.ts › ignores the guarded marker and falls back to the plain token
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

Recognizing the raw scan alone was too strict in the other direction.
QR alphanumeric mode encodes uppercase only and is much denser, so
encoders routinely uppercase bech32 addresses, BOLT-11 invoices and hex
addresses. Those payloads lost their case to the encoder, so lowercasing
them back is safe.

A payload holding any lowercase letter kept its original case, so a
mixed-case EIP-55 checksum is the user's and viem must stay free to
reject a bad one. The old blanket lowercase laundered a corrupted
address into a valid-looking payment target; that stops here.

Found by /code-review.
@abalinda

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@abalinda
abalinda marked this pull request as ready for review August 12, 2026 12:10
The explanation ran longer than the code it explained.
@abalinda

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 47 minutes.

@abalinda

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@kushagrasarathe kushagrasarathe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed via pr-review rulebook (T2, full-diff read). Fix verified: all recognizeQr branches checked case-safe on raw input (ENS, PIX email/UUID, EMV regexes all case-tolerant); bad-checksum tightening consistent through the clipboard-extract recursion path. Tests pin the component wiring, not the parser. One accepted edge: lowercase-0x + all-caps-hex EVM address now rejected — measured zero in prod. Reminder: base is main, back-merge to dev after landing.

@kushagrasarathe
kushagrasarathe merged commit 8c50e1d into main Aug 12, 2026
26 checks passed
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.

2 participants