Skip to content

fix(receipt): copy/display crypto withdrawal destinations verbatim (Tron/Solana) - #2507

Merged
abalinda merged 1 commit into
mainfrom
hotfix/receipt-wallet-address-copy
Jul 24, 2026
Merged

fix(receipt): copy/display crypto withdrawal destinations verbatim (Tron/Solana)#2507
abalinda merged 1 commit into
mainfrom
hotfix/receipt-wallet-address-copy

Conversation

@abalinda

@abalinda abalinda commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

QA of the Tron/Solana withdraw hotfix (#2490) found the withdrawal receipt corrupts the destination address. The account-details row runs the identifier through formatIban, which treats any string whose first two chars are letters as an IBAN and uppercases + space-chunks it. Every Tron address starts with T… and base58 is case-sensitive, so:

  • Copy: the clipboard got an uppercased, space-chunked (invalid) address for every Tron/Solana withdrawal — pasting it into a wallet fails base58/checksum validation.
  • Display: maskAccountIdentifier's plain fallback runs the same formatIban on any identifier matching /^[a-zA-Z]{2}\d/, so Tron addresses whose 3rd char is a digit (~13%, e.g. TN9R…) were also mangled on screen. (The commit that shipped fix(withdraw): show non-EVM (Tron/Solana) token selection in the selector button #2490 claimed "display was already safe" — it wasn't.)

Fix

Keyed to the actual wire vocabulary the BE serializes for a CRYPTO_WITHDRAW: recipientAccount.type is 'address' (history mapGenericIntent, sender viewer), 'evm-address', or 'peanut-wallet'never the Prisma WALLET_EXTERNAL enum, which accountTypeToApi maps to 'evm-address' before it reaches the FE.

A closed 3-value isCryptoAddressType allowlist (every other type stays a bank rail → keeps formatIban) now gates both the copy helper (getAccountCopyValue) and the display mask (maskAccountIdentifier short-circuit → verbatim). The row labels crypto destinations "Address" instead of "Account Number".

Risks / QA

  • Display-and-copy only. Bank rails (iban/us/clabe/cbu/cvu/pix/BANK_*/raw payment_rail) are outside the closed allowlist, so their formatIban copy + per-rail masking are untouched.
  • Tests feed the real wire values ('address'/'evm-address') and cover both copy and display — each fails on the pre-fix code (verified: formatIban corrupts both sample Tron addresses; the digit-3rd-char one also on display). Full jest suite green.
  • Cross-repo: pairs with peanut-api-ts hotfix/non-evm-withdraw-false-failed (the BE was flipping these same withdrawals to Failed). Independent deploys.

Adversarial multi-agent review caught that the first pass keyed off 'WALLET_*' — a type the wire never carries — making the fix a no-op whose tests were green on a fabricated value. This is the corrected version.

Summary by CodeRabbit

  • Bug Fixes
    • Crypto wallet addresses are now displayed and copied without unwanted formatting or masking.
    • Bank account identifiers continue to use appropriate IBAN formatting and masking.
    • Transaction details now label crypto destinations as “Address” for clearer identification.
    • Supports address handling across Tron, Solana, EVM, and other supported crypto address types.

…corrupted them

The receipt's account-details row ran the destination through formatIban,
which treats any string whose first two chars are letters as an IBAN and
uppercases + space-chunks it. Every Tron address starts with 'T…' and
base58 is case-sensitive, so:
- COPY: the clipboard got an uppercased, space-chunked (invalid) address
  for EVERY Tron/Solana destination.
- DISPLAY: maskAccountIdentifier's 'plain' fallback runs the same formatIban
  on any identifier matching /^[a-zA-Z]{2}\d/, so Tron addresses whose 3rd
  char is a digit (~13%, e.g. 'TN9R…') were also mangled on screen.

Keyed to the ACTUAL wire vocabulary the BE serializes for a CRYPTO_WITHDRAW:
recipientAccount.type is 'address' (history mapGenericIntent, sender viewer),
'evm-address', or 'peanut-wallet' — never the Prisma 'WALLET_EXTERNAL' enum,
which accountTypeToApi maps to 'evm-address' before it ever reaches the FE.
A closed 3-value isCryptoAddressType allowlist (every other type stays a bank
rail → keeps formatIban) now gates both the copy helper and the display mask;
the row labels crypto destinations 'Address' instead of 'Account Number'.

Tests feed the real wire values ('address'/'evm-address') and cover both copy
and display — each fails on the pre-fix code. (Caught in adversarial review:
the first pass keyed off 'WALLET_*', a type the wire never carries, so the fix
was a no-op and its tests were green on a fabricated value.)
@vercel

vercel Bot commented Jul 24, 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, Comment Jul 24, 2026 1:44pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Crypto wallet identifiers are now recognized by their wire types, kept unmasked and copied verbatim. Bank-account identifiers retain rail-specific labels, IBAN formatting, and masking. The transaction receipt uses the new shared copy-value helper.

Changes

Crypto account handling

Layer / File(s) Summary
Crypto type recognition and masking
src/utils/account-mask.utils.ts
Adds case-insensitive recognition for crypto address types and bypasses bank-account masking for those identifiers.
Transaction account labels and copy values
src/components/TransactionDetails/transaction-details.utils.ts, src/components/TransactionDetails/__tests__/transaction-details.utils.test.ts
Labels crypto destinations as Address, preserves crypto identifiers for copying, retains bank-account formatting, and tests crypto and bank-rail behavior.
Receipt clipboard integration
src/components/TransactionDetails/TransactionDetailsReceipt.tsx
Uses getAccountCopyValue when copying bank account identifiers from the transaction receipt.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested labels: enhancement

Suggested reviewers: hugo0

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main fix: preserving crypto withdrawal destinations during copy/display, and the Tron/Solana examples match the changed behavior.
✨ 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 hotfix/receipt-wallet-address-copy

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

@abalinda
abalinda marked this pull request as draft July 24, 2026 13:40
@github-actions

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6263.81 → 6263.22 (-0.59)
Findings: +1 net (+9 new, -8 resolved)

🆕 New findings (9)

  • critical complexity — src/components/TransactionDetails/TransactionDetailsReceipt.tsx — CC 161, MI 52.86, SLOC 352
  • high hotspot — src/components/TransactionDetails/TransactionDetailsReceipt.tsx — 41 commits, +481/-1076 lines since 6 months ago
  • high complexity — src/utils/account-mask.utils.ts — CC 13, MI 49.58, SLOC 74
  • medium high-mdd — src/components/TransactionDetails/TransactionDetailsReceipt.tsx:75 — TransactionDetailsReceipt: MDD 178.4 (uses across many lines from declarations)
  • medium complexity — src/components/TransactionDetails/transaction-details.utils.ts — CC 23, MI 64.11, SLOC 65
  • low high-mdd — src/utils/account-mask.utils.ts:85 — maskAccountIdentifier: MDD 14.8 (uses across many lines from declarations)
  • low structural-dup — components/Send/link/views/Success.link.send.view.tsx:102 — 14 duplicate lines / 73 tokens with components/TransactionDetails/TransactionDetailsReceipt.tsx:840
  • low missing-return-type — src/components/TransactionDetails/transaction-details.utils.ts:58 — getBankAccountLabel: exported fn missing return type annotation
  • low missing-return-type — src/components/TransactionDetails/transaction-details.utils.ts:75 — getAccountCopyValue: exported fn missing return type annotation

✅ Resolved (8)

  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx — CC 161, MI 52.87, SLOC 352
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx — 40 commits, +475/-1073 lines since 6 months ago
  • src/utils/account-mask.utils.ts — CC 11, MI 46.53, SLOC 65
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx:75 — TransactionDetailsReceipt: MDD 177.7 (uses across many lines from declarations)
  • src/components/TransactionDetails/transaction-details.utils.ts — CC 20, MI 64.2, SLOC 57
  • components/Send/link/views/Success.link.send.view.tsx:102 — 14 duplicate lines / 73 tokens with components/TransactionDetails/TransactionDetailsReceipt.tsx:837
  • src/utils/account-mask.utils.ts:67 — maskAccountIdentifier: MDD 13.7 (uses across many lines from declarations)
  • src/components/TransactionDetails/transaction-details.utils.ts:55 — getBankAccountLabel: exported fn missing return type annotation

📈 Painscore deltas (top movers)

File Before After Δ
src/utils/account-mask.utils.ts 9.6 8.6 -1.0

@github-actions

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2151 ran, 0 failed, 0 skipped, 37.8s

📊 Coverage (unit)

metric %
statements 60.9%
branches 44.4%
functions 50.0%
lines 61.3%
⏱ 10 slowest test cases
time test
3.8s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.5s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
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/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › keeps stickers off the username pill (final pass respects the keep-out)
0.2s src/utils/__tests__/demo-balance.test.ts › debits and floors at zero
0.2s src/utils/__tests__/demo-balance.test.ts › keeps a spent-down balance across a cold start within the TTL
0.2s src/utils/__tests__/demo-balance.test.ts › starts at the full balance on a fresh install and stamps a timestamp
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
src/components/TransactionDetails/__tests__/transaction-details.utils.test.ts (1)

34-80: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover peanut-wallet in the downstream helper tests.

The predicate test includes peanut-wallet, but getAccountCopyValue, maskAccountIdentifier, and getBankAccountLabel only exercise address and evm-address. Add peanut-wallet cases so every allowlisted wire type is protected across copy, display, and labeling paths.

🤖 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/components/TransactionDetails/__tests__/transaction-details.utils.test.ts`
around lines 34 - 80, Extend the tests for getAccountCopyValue,
maskAccountIdentifier, and getBankAccountLabel to include the allowlisted
peanut-wallet wire type, covering copy, display, and labeling behavior alongside
address and evm-address. Reuse the existing crypto destination expectations and
ensure peanut-wallet is labeled “Address.”
🤖 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.

Nitpick comments:
In
`@src/components/TransactionDetails/__tests__/transaction-details.utils.test.ts`:
- Around line 34-80: Extend the tests for getAccountCopyValue,
maskAccountIdentifier, and getBankAccountLabel to include the allowlisted
peanut-wallet wire type, covering copy, display, and labeling behavior alongside
address and evm-address. Reuse the existing crypto destination expectations and
ensure peanut-wallet is labeled “Address.”

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b46a622e-3993-4b78-ba7c-7d0714a5b180

📥 Commits

Reviewing files that changed from the base of the PR and between 760ede2 and 5706f86.

📒 Files selected for processing (4)
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx
  • src/components/TransactionDetails/__tests__/transaction-details.utils.test.ts
  • src/components/TransactionDetails/transaction-details.utils.ts
  • src/utils/account-mask.utils.ts

@abalinda
abalinda marked this pull request as ready for review July 24, 2026 14:25

@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 for code quality, DRY, and bug-risk. Approving — ship as-is (LOW risk).

Traced both the copy path (getAccountCopyValue) and the display path (maskAccountIdentifier) end-to-end against formatIban.

Quality/DRY — clean. isCryptoAddressType is defined once in account-mask.utils and reused by both the copy helper and the mask short-circuit — no duplication. This is a root-cause fix in the right place: it gates both corruption paths, not just the copy symptom the ticket named. Tests feed the real wire values ('address'/'evm-address') and each fails on pre-fix code.

Two small notes (non-blocking):

  1. Closed allowlist is coupled to the BE vocabulary. CRYPTO_ADDRESS_TYPES = {address, evm-address, peanut-wallet}. Any future crypto wire type the BE introduces (e.g. 'tron-address') falls through to formatIban → this exact corruption class silently returns. The comment frames this as the deliberate safe-default (new rails keep IBAN formatting), which is the right call — just flagging the coupling.
  2. peanut-wallet now renders verbatim + labels as "Address" where it previously hit the mask/label default. Behavior change for the user's-own-wallet row; low risk since it's an EVM address and verbatim is correct.

@abalinda
abalinda merged commit d685b3a into main Jul 24, 2026
24 of 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