post-cutover: TRANSACTION_INTENT completeness + masking + run-mode banner + cheat polish#1912
post-cutover: TRANSACTION_INTENT completeness + masking + run-mode banner + cheat polish#1912
Conversation
…rmer
Decomplexify added a TRANSACTION_INTENT branch to the transformer with no
tests. A 40-line cyclomatic switch covering 8 kinds × 4 userRoles × isUser
combinations had zero coverage — that's how Hugo's playtest screenshots
snuck through (UUID-as-recipient, EU flag instead of ES, missing avatars).
33 fixture cases covering:
- Every legacy entry type (DIRECT_SEND, SEND_LINK, REQUEST, BRIDGE_OFFRAMP,
MANTECA_OFFRAMP, BRIDGE_ONRAMP, MANTECA_ONRAMP, DEPOSIT, MANTECA_QR_PAYMENT,
PERK_REWARD, BANK_SEND_LINK_CLAIM)
- Every TRANSACTION_INTENT kind × userRole that exists in the new switch
- The default-arm guard so a future BE-added kind doesn't silently fall
through to direction='send'
- Reaper-failed rows: failReason='\${kind}_timeout' renders user-friendly
copy instead of the kind's default name
Asserts direction, transactionCardType, userName, isLinkTransaction,
bankAccountDetailsDefined, and isPeerActuallyUser (proxied via the
isVerified output gate). 33/33 green.
…asking
Decomplexify added a TRANSACTION_INTENT switch to transactionTransformer
that was shaped much shallower than the legacy switches it replaced. Hugo's
playtest screenshots (a-g) were all instances of the same class — missing
userRole=RECIPIENT branches, missing kind cases, missing fullName plumbing,
missing visibility-gate updates downstream.
Transformer changes:
- CRYPTO_DEPOSIT: new case. Mirrors legacy DEPOSIT but lifts the
isPeerActuallyUser=false hardcode so a deposit from a Peanut user shows
a clickable avatar (Hugo's screenshot f).
- LINK_CREATE: full SENDER/RECIPIENT/BOTH branch logic mirroring legacy
SEND_LINK. Resolves claimer username when the claimer is a Peanut user.
- FIAT_OFFRAMP: userRole=RECIPIENT branch — multi-user fulfillment edge
case where viewer received the offramp's USDC.
- REQUEST_PAY: bridge-fulfillment subcase (direction='bank_request_fulfillment'
when extraData.fulfillmentType==='bridge' and userRole=SENDER).
- default arm: Sentry breadcrumb on unhandled kinds. A future BE-added
kind no longer silently maps to 'send' — the warning fires within
minutes of the kind appearing in production.
- Reaper-failed branch: failReason='\${kind}_timeout' overrides userName
to user-friendly copy ("Send didn't complete", etc.) so zombie rows
don't pretend the action succeeded.
bankAccountDetails plumbing (the EU-vs-ES flag bug, screenshot d):
- shouldPlumbBankAccountDetails() now extends the gate to TRANSACTION_INTENT
kind=FIAT_OFFRAMP/CRYPTO_WITHDRAW. Previously the gate only fired for
legacy BRIDGE_OFFRAMP/BANK_SEND_LINK_CLAIM, so intent-routed withdrawals
silently dropped the IBAN row and getBankAccountCountryCode fell back
to the EUR currency flag.
- Also adds MANTECA_OFFRAMP — independent legacy bug, was never plumbed
even pre-decomplexify.
Visibility gates in useReceiptViewModel:
- depositInstructions: extended to TRANSACTION_INTENT/kind=ONRAMP.
- mantecaDepositInfo: same extension.
New per-rail account masking (utils/account-mask.utils.ts):
- IBAN, CLABE, CBU, CVU: '**** **** **** 0217' (last 4)
- US ACH, GB: 'last-4-account-only' (routing stays plain — bank-public)
- PIX, MANTECA_ALIAS: NOT masked. PIX keys are emails/phones/CPFs/UUIDs;
masking would mangle meaning. Truncate at 32 chars if needed.
Wired into the bankAccountDetails row in TransactionDetailsReceipt — copy
icon still yields the FULL identifier (mask is for visual privacy, not
interaction).
Invite-friends desktop fix:
- navigator.share is mobile-only; desktop fell through to clipboard.writeText
with no toast (screenshot b — silent click). Added toast.info('Invite
link copied!') matching the existing ShareButton pattern.
…eal-money Without this, "wait, am I pointing at staging right now?" was answered by reading 4 env vars across 2 repos. Now the dev banner has a high-contrast pill (yellow=sandbox, red=real-money), and a styled banner logs to console on every page load — visually impossible to miss. utils/mode.ts: classifies the env constellation into 3 coherent presets: sandbox = local API + arb-sepolia + harness-ecdsa staging-mirror = staging API + arb-sepolia prod-real = prod API + arb-mainnet (DANGER) custom = anything else (tagged with components) logRunMode() uses %c console styling — 22px bold yellow background for sandbox, 22px bold red background for real-money. Same call backs the Banner mount log + the new debug.mode() cheat. Production builds skip both the pill and the log (gated on IS_PRODUCTION).
…, mode)
Three pain-point fixes from today's playtest:
(1) impersonate('hugostagqa') is now one-shot. Previously needed { pk: '0x...' }
or the kernel didn't init. Default harness PK hardcoded (sandbox key,
matches the local-clone wallet override). Resolution order changed:
impersonate ALWAYS resets to opts.pk ?? DEFAULT — doesn't honor stale
localStorage values that broke earlier sessions.
(2) Errors are friendly now:
Old: "Error: username 'hugostagqasfsaasfafsa' not found"
New: "Error: username 'hugostagqasfsaasfafsa' not found in app.users
Did you mean: 'hugostagqa', 'hugodev', 'hugolocal', ...?
(875 users in this DB)
Tip: list available users with debug.listUsers()."
BE 404s now return suggestions[] + totalUsers; mint-jwt 404/500s return
a hint field. Network errors throw with "is the API running on $URL?"
(3) New commands:
debug.listUsers(prefix?, limit=20) — browse usernames
debug.harnessSigner(pk?) — re-arm signer w/o changing JWT
(PK format validated)
debug.mode() — log api/chain/signing in big
yellow text (red if real money)
All gated by HARNESS_ENABLED + requireTestMode; refuse to run against prod.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughChanges introduce run-mode detection utilities, bank account masking for receipt display, comprehensive transaction data mapping tests, and enhanced debug API commands. Modifications extend transaction onramp flow handling, refine transaction intent display logic, mask sensitive banking information, and add developer debugging capabilities including user impersonation, signer configuration, and run-mode introspection. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Review rate limit: 3/5 reviews remaining, refill in 23 minutes and 5 seconds. Comment |
Code-analysis diffPainscore total: 5542.68 → 5670.99 (+128.31) 🆕 New findings (41)
…and 21 more. ✅ Resolved (34)
…and 14 more. 📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/utils/account-mask.utils.ts (1)
50-87: Minor naming inconsistency intruncate-32mode.The mode is named
truncate-32but the implementation truncates to 30 characters (29 + ellipsis). This is functionally fine — it's a display limit, not a hard constraint. Consider renaming totruncate-30or adjusting toidentifier.slice(0, 31) + '…'for 32-char output if the naming is meant to be precise.If you want the name to match behavior:
type MaskMode = /** "**** **** **** 0217" — keep last 4, format in groups of 4. IBAN/CLABE/CBU/CVU. */ | 'last-4' /** Last 4 of account number; routing number stays plain. US ACH / GB sort code. */ | 'last-4-account-only' - /** Truncate at 32 chars with ellipsis. PIX (email/phone/CPF/UUID — masking corrupts). */ - | 'truncate-32' + /** Truncate at 30 chars with ellipsis. PIX (email/phone/CPF/UUID — masking corrupts). */ + | 'truncate-30' /** Show as-is. Manteca aliases — short user-chosen strings. */ | 'plain'And update
MASK_RULESaccordingly.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/account-mask.utils.ts` around lines 50 - 87, The truncate-32 mode in maskAccountIdentifier is misaligned with its name: it currently returns 29 chars + '…' (30 chars total). To make the behavior match 'truncate-32', change the truncation in the 'truncate-32' case to take the first 31 characters and append '…' (i.e., use identifier.slice(0, 31) + '…'), and ensure any related MASK_RULES entries using 'truncate-32' remain correct; alternatively, if you prefer the current 30-char behavior, rename the mode in MASK_RULES and usages to 'truncate-30' to keep names consistent.
🤖 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/utils/mode.ts`:
- Around line 95-105: The console banner in logRunMode() uses the boolean
realMoney to set tag (⚠ REAL MONEY MODE vs 🟢 SANDBOX MODE) which can disagree
with m.preset (e.g., staging-mirror or custom) and confuse readers; update the
logic so the tag is derived from m.preset (or a small helper that maps presets
to modes) instead of only realMoney, and then use that computed tag when logging
(ensure symbols like logRunMode, realMoney, tag, and m.preset are the points of
change) so the printed headline always aligns with the computed preset.
- Around line 65-67: The preset logic omits checking signing for the staging and
prod branches, so combinations like staging+arb-sepolia+harness-ecdsa are
incorrectly labeled; update the conditional expressions that set preset (the
lines using api, chain and assigning to preset) to also include signing ===
'harness-ecdsa' (i.e., require signing in the second and third branches just
like the first) so staging + arb-sepolia + harness-ecdsa yields 'staging-mirror'
and prod + arb-mainnet + harness-ecdsa yields 'prod-real'.
---
Nitpick comments:
In `@src/utils/account-mask.utils.ts`:
- Around line 50-87: The truncate-32 mode in maskAccountIdentifier is misaligned
with its name: it currently returns 29 chars + '…' (30 chars total). To make the
behavior match 'truncate-32', change the truncation in the 'truncate-32' case to
take the first 31 characters and append '…' (i.e., use identifier.slice(0, 31) +
'…'), and ensure any related MASK_RULES entries using 'truncate-32' remain
correct; alternatively, if you prefer the current 30-char behavior, rename the
mode in MASK_RULES and usages to 'truncate-30' to keep names consistent.
🪄 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: d9fb9246-7f43-4ecd-ae8e-140d30bd2c72
📒 Files selected for processing (8)
src/components/Global/Banner/index.tsxsrc/components/TransactionDetails/TransactionDetailsReceipt.tsxsrc/components/TransactionDetails/__tests__/transactionTransformer.test.tssrc/components/TransactionDetails/transactionTransformer.tssrc/components/TransactionDetails/useReceiptViewModel.tssrc/context/PeanutDebug.tsxsrc/utils/account-mask.utils.tssrc/utils/mode.ts
| if (api === 'local' && chain === 'arb-sepolia' && signing === 'harness-ecdsa') preset = 'sandbox' | ||
| else if (api === 'staging' && chain === 'arb-sepolia') preset = 'staging-mirror' | ||
| else if (api === 'prod' && chain === 'arb-mainnet') preset = 'prod-real' |
There was a problem hiding this comment.
Include signing in the named preset checks.
Right now staging + arb-sepolia + harness-ecdsa is still labeled staging-mirror, and prod + arb-mainnet + harness-ecdsa is still labeled prod-real. That hides the passkey bypass state in the exact summary this helper is supposed to provide.
♻️ Proposed fix
- if (api === 'local' && chain === 'arb-sepolia' && signing === 'harness-ecdsa') preset = 'sandbox'
- else if (api === 'staging' && chain === 'arb-sepolia') preset = 'staging-mirror'
- else if (api === 'prod' && chain === 'arb-mainnet') preset = 'prod-real'
+ if (api === 'local' && chain === 'arb-sepolia' && signing === 'harness-ecdsa') preset = 'sandbox'
+ else if (api === 'staging' && chain === 'arb-sepolia' && signing === 'passkey') preset = 'staging-mirror'
+ else if (api === 'prod' && chain === 'arb-mainnet' && signing === 'passkey') preset = 'prod-real'
else preset = `custom (${api} · ${chain} · ${signing})`🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/mode.ts` around lines 65 - 67, The preset logic omits checking
signing for the staging and prod branches, so combinations like
staging+arb-sepolia+harness-ecdsa are incorrectly labeled; update the
conditional expressions that set preset (the lines using api, chain and
assigning to preset) to also include signing === 'harness-ecdsa' (i.e., require
signing in the second and third branches just like the first) so staging +
arb-sepolia + harness-ecdsa yields 'staging-mirror' and prod + arb-mainnet +
harness-ecdsa yields 'prod-real'.
| const headlineStyle = realMoney | ||
| ? 'background: #dc2626; color: #fff; font-size: 22px; font-weight: 900; padding: 10px 16px; border-radius: 4px; letter-spacing: 0.05em;' | ||
| : 'background: #facc15; color: #000; font-size: 22px; font-weight: 900; padding: 10px 16px; border-radius: 4px; letter-spacing: 0.05em;' | ||
|
|
||
| const detailStyle = 'font-size: 13px; font-weight: 600; line-height: 1.6em;' | ||
| const tag = realMoney ? '⚠ REAL MONEY MODE' : '🟢 SANDBOX MODE' | ||
|
|
||
| // eslint-disable-next-line no-console | ||
| console.log( | ||
| `${prefix ? prefix + ' ' : ''}%c${tag} · ${m.preset}%c\n` + | ||
| ` api = ${m.api} (${m.apiUrl})\n` + |
There was a problem hiding this comment.
Keep the console headline aligned with the computed preset.
logRunMode() prints 🟢 SANDBOX MODE for staging-mirror and custom too, so the first line can contradict m.preset. That makes the warning banner easier to misread.
♻️ Proposed fix
- const tag = realMoney ? '⚠ REAL MONEY MODE' : '🟢 SANDBOX MODE'
+ const tag =
+ m.preset === 'sandbox'
+ ? '🟢 SANDBOX MODE'
+ : m.preset === 'staging-mirror'
+ ? '🟡 STAGING MIRROR'
+ : realMoney
+ ? '⚠ REAL MONEY MODE'
+ : '⚙ CUSTOM MODE'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/mode.ts` around lines 95 - 105, The console banner in logRunMode()
uses the boolean realMoney to set tag (⚠ REAL MONEY MODE vs 🟢 SANDBOX MODE)
which can disagree with m.preset (e.g., staging-mirror or custom) and confuse
readers; update the logic so the tag is derived from m.preset (or a small helper
that maps presets to modes) instead of only realMoney, and then use that
computed tag when logging (ensure symbols like logRunMode, realMoney, tag, and
m.preset are the points of change) so the printed headline always aligns with
the computed preset.
Summary
FE counterpart to peanut-api-ts#676. Four-PR-bundle of post-decomplexify fixes from the 2026-04-29 playtest.
82faa1311transactionTransformer(33 cases)b16c0e712bankAccountDetailsplumbing + per-rail masking + invite-friends desktop toast246c6847c%c-styled console log (yellow=sandbox, red=real-money)b4c974c47listUsers,harnessSigner,modecommandsWhy this exists
Hugo's playtest screenshots all traced to the same root: the new
case EHistoryEntryType.TRANSACTION_INTENTswitch intransactionTransformer.tswas shaped much shallower than the legacy switches it replaced — missinguserRole=RECIPIENTbranches, missing kind cases (no CRYPTO_DEPOSIT, no LINK_CREATE-claimer-resolution), and downstream visibility gates inuseReceiptViewModel.ts+ thebankAccountDetailsprojection still gated on legacy entry types only.Manifested as:
bankAccountDetailsdropped for intent-routed offramps$2 PENDINGzombie rows from Feb 18 with no Cancel — addressed via reaper in BE PR [TASK-8600] feat: validate usernames #676 + muted-FAIL copy hereWhat changed
transactionTransformer.ts— fill the missing casesCRYPTO_DEPOSITkind case. SetsisPeerActuallyUser=truewhensenderAccount.isUser(improvement over legacy DEPOSIT which always forcedfalse).LINK_CREATE × RECIPIENTbranch — claimer resolution mirroring legacy SEND_LINK.FIAT_OFFRAMP × RECIPIENTbranch.REQUEST_PAYbridge-fulfillment subcase.defaultarm: Sentry breadcrumb on unhandled kinds (defensive — a future BE-added kind no longer silently maps to 'send').failReason='\${kind}_timeout'overrides userName to user-friendly copy ("Send didn't complete", "Bank transfer didn't complete", etc.) so zombie rows don't pretend the action succeeded.bankAccountDetailsplumbing (the EU/ES flag bug)New
shouldPlumbBankAccountDetails(entry)extends the gate to TRANSACTION_INTENT/kind=FIAT_OFFRAMP/CRYPTO_WITHDRAW + (legacy) MANTECA_OFFRAMP. Without this, intent-routed withdrawals lost the IBAN row andgetBankAccountCountryCodefell back to currency-flag.Visibility gates in
useReceiptViewModel.tsdepositInstructions: extended to TRANSACTION_INTENT/kind=ONRAMPmantecaDepositInfo: same extensionPer-rail account masking (
utils/account-mask.utils.ts— new)**** **** **** 0217Run-mode banner (
utils/mode.ts— new)Classifies env into
sandbox / staging-mirror / prod-real / custom. Banner pill in dev: yellow for sandbox, red for real-money.logRunMode()styles a 22px bold console banner on every page load via%c. Same helper backs the newdebug.mode()cheat.Debug cheats
debug.impersonate('username')is one-shot now — default harness PK hardcoded; resolution always overwrites stale localStorage values.debug.listUsers(prefix?, limit?),debug.harnessSigner(pk?),debug.mode().Invite-friends desktop fix
navigator.shareis mobile-only; desktop falls through toclipboard.writeText— addedtoast.info('Invite link copied!')matching existing ShareButton pattern.Test plan
npm test— 962 passingnpm run typecheck— clean/historyrow-clicks for each kind render correctly (verified during playtest)debug.impersonate('hugostagqa')is one-shot (no{pk}needed) on local cloneOut of scope (filed in api repo todo.md)