decomplexify(frontend): unified intents + debug panel + Rhino SDA claim + Squid eviction#1904
decomplexify(frontend): unified intents + debug panel + Rhino SDA claim + Squid eviction#1904
Conversation
- Peanut wallet chain/token configurable via NEXT_PUBLIC_PEANUT_WALLET_* env vars - Primary chain in PUBLIC_CLIENTS_BY_CHAIN now follows PEANUT_WALLET_CHAIN - Ultra Relay gas=0 scoped to Arbitrum mainnet only (testnets use standard estimation) - @zerodev/permissions package for session key permission approvals - Kernel client context imports reworked for chain-agnostic setup
…orts De-Complexify M1 UI snapshots. Captures screenshots + a11y tree + console logs + metadata at each step of core flows. Flows (19 test cases × mobile 375x667 + desktop 1440x900 = 38 total): - home: balance, nav CTAs, history scroll - qr-pay: no-code error, Mercado Pago mock, PIX mock - send-link: landing, amount entry, URL state - withdraw: landing, crypto, manteca, AR/bank - add-money: landing, AR/bank (Manteca), US/bank (Bridge) - setup: setup, profile, points, history pages - claim: no pubKey, invalid pubKey Infrastructure: - playwright.config.ts: mobile + desktop projects, serial execution - e2e/global-setup.ts: authenticates via /dev/test-session API endpoint - e2e/utils/capture.ts: captureStep (screenshot + a11y + meta), console collector - .gitignore: e2e/__results__, __report__, __snapshots__, .auth/ Auth bypass: global-setup hits the API's /dev/test-session (built in peanut-api-ts-decomplexify), gets a JWT, stores as cookie, saves storageState for all tests. No passkey ceremony needed. Not yet run against live servers — structure validated via playwright test --list.
Home screen now shows smart account + Rain collateral as a single spendable balance. Outgoing flows auto-route across both buckets (collateral-only -> smart-only -> mixed) and produce a single history entry via the backend's TransactionIntent pipeline. - useSpendBundle: strategy routing + EIP-712 admin signing for Rain withdraw (collateral-only direct-transfer; mixed bundles withdrawAsset + usdc.transfer + subsequentCalls in one kernel UserOp) - useSendMoney, useWallet.sendTransactions accept `kind` for history categorization; 5 callers tagged (P2P send, QR pay, link create, crypto withdraw, fiat offramp) - One-time session-key grant (useGrantSessionKey) with a single CallPolicy covering both USDC.transfer(collateralProxy) and coordinator.withdrawAsset; inline grant on first collateral spend - useRainCardOverview hook with 30s poll + WS invalidation on rain_card_balance_changed; WebSocket singleton gated on auth ready - Stable-value spendable balance pattern avoids flicker during auto-balancer deposits (smart down + collateral up) - hasSufficientBalance -> hasSufficientSpendableBalance across 7 flows; crypto-withdraw skips recordPayment when strategy touched collateral (no more orphan WITHDRAW charges) - TRANSACTION_INTENT history entry type with kind-aware drawer mapping - Dev page /dev/card-session-approve for testing the grant flow
Playwright-based behavioral snapshots for De-Complexify M2 verification.
Mobile viewport (390x844 Chromium + Pixel 7 UA) is the default — Peanut
is a mobile-first PWA.
Flows organized by auth requirement:
- public.spec.ts: routes that work without auth (landing, support,
claim, invite, qr) — the stable regression foundation
- dev-showcase.spec.ts: /dev/components and /dev/ds — canonical design
system targets; critical for M2 Redux/MUI/flow-context refactors
- home.spec.ts, qr-pay.spec.ts, send-link.spec.ts, withdraw.spec.ts,
add-money.spec.ts, setup.spec.ts, claim.spec.ts: authenticated flows
(best-effort; the app's wallet init gates these in ways the harness
can't fully fake — we snapshot whatever state we land in)
Infrastructure:
playwright.config.ts: mobile + desktop projects, serial execution, 120s
timeout per test, retain-on-failure traces.
e2e/global-setup.ts: hits API /dev/test-session for a JWT, stores as
'jwt-token' cookie (matching src/utils/cookie-migration.utils.ts),
dismisses "Got it" + PWA install modals, saves storageState.
e2e/utils/capture.ts: captureStep writes PNG + meta.json + aria.txt
per step. collectConsoleLogs filters known noise, flushes on demand.
e2e/utils/dismiss-modals.ts: loop-based modal dismissal (mobile has
stacked PWA install + mobile-first modals).
package.json scripts:
test:e2e → mobile (default)
test:e2e:desktop → desktop only
test:e2e:all → both viewports
.gitignore: e2e/__results__, __report__, __snapshots__, .auth/
Verified on desktop: 7 public routes captured at correct URLs.
Mobile run in progress — some auth-dependent flows still need more work
(the app's client-side wallet init is hard to fake). Public flows work.
…UTING with mono - kernelClient + Create.request.link.view: localStorage bypass so mono/engineering/qa Playwright harness can drive authenticated screens with a seeded user (no real passkey). - CONTRIBUTING.md: delegate shared rules to mono root, keep UI-specific overrides only. - e2e/: in-progress flow specs + scaffolding (not yet wired to mono/qa).
…qa harness UI-side changes to make the app drivable from a headless Playwright runner: - kernelClient.context.tsx: when localStorage.__harness_skip_passkey=true and __harness_ecdsa_pk is set, bootstrap a kernel account via signerToEcdsaValidator instead of the passkey path. Behavior is identical post-init (same bundler, same paymaster, same kernel v3.1 / EP 0.7) — just a different validator so Playwright can actually sign userops. - zerodev.consts.ts: PEANUT_WALLET_CHAIN + _TOKEN now pick from env (NEXT_PUBLIC_PEANUT_WALLET_CHAIN_ID / _TOKEN) so sandbox can target arbitrumSepolia + testnet USDC. Prod build path unchanged. - app/actions/clients.ts: PUBLIC_CLIENTS_BY_CHAIN keyed off PEANUT_WALLET_CHAIN rather than hardcoded arbitrum. - context/ReproduceBootstrap.tsx: new client component. When URL includes ?__reproduce=<sessionId>, fetches the manifest from /dev/reproduce/<id>, seeds localStorage + jwt-token cookie, strips the query, hard-reloads. Makes the Nutcracker gallery "Open live" button work end-to-end. - app/ClientProviders.tsx: wire ReproduceBootstrap under a <Suspense> boundary (useSearchParams requires it in App Router). - package.json: add @zerodev/ecdsa-validator dependency.
Skip the auth-redirect-to-/setup when a reproduce session is pending — `?__reproduce=<id>` is present but ReproduceBootstrap hasn't finished setting cookies yet. Without this guard, the redirect races the bootstrap and bounces the user away before they land authed. Gated behind NEXT_PUBLIC_HARNESS_SKIP_PASSKEY_CHECK so prod is unaffected. Part of the Nutcracker "▶ play" flow (see engineering/qa/VERIFICATION-PLAN §13a, Slice J). Was previously uncommitted since the earlier session that introduced the reproduce flow; consolidated onto decomplexify per feedback_harness_branch_decomplexify.
…session Without this, ▶ play against a browser that has cookies / SW caches from a previous reproduce session would surface the wrong user in the header despite the cookie being overwritten. Service worker served stale /users/me responses. IndexedDB (TanStack Query persister) carried over too. Full wipe order before seeding the new scenario user: - expire jwt-token cookie - localStorage keys except reproduce session flag - caches matching USER_DATA_CACHE_PATTERNS - unregister every service worker on the origin - delete all IndexedDB databases Then seed new localStorage + new cookie + hard reload. Hard-refresh removed as a debug step — a fresh Chrome window now does the right thing out of the box. Also: use history.replaceState instead of router.replace to strip the ?__reproduce param before reload (router.replace was async, racing the reload, so the param survived into the post-reload URL).
Path B proof-of-concept. Companion to the QA harness runner (Node/ Playwright) that drives the same scenarios server-side. Both sides consume the JSON action descriptors defined at engineering/qa/lib/action-dsl.mjs. ReproduceBootstrap: after wiping + re-seeding persistent state, stash the manifest's accumulated stepActions into sessionStorage under __harness_replay_actions, then hard-reload as before. HarnessReplay: new component mounted alongside ReproduceBootstrap in ClientProviders. On mount, reads the stashed descriptors, clears the key (idempotent against strict-mode double-mounts), waits for providers to stabilise (1.5s), then runs the descriptors sequentially: goto - pushState + popstate dispatch wait - setTimeout click-role - scan button/link/textbox for accessible name match click-text - treewalk for visible element whose textContent matches fill-amount- native setter + input event for React controlled inputs fill - same for generic selectors wait-for-text / wait-for-selector - 100ms poll until visible / timeout No-op outside sandbox mode (same env guard as ReproduceBootstrap). Keeps WithdrawFlowContext / other useState-only contexts honest — sensitive mid-flow state isn't persisted anywhere; it's rebuilt by driving the UI.
Two follow-ups to the first-cut HarnessReplay interpreter after running it end-to-end through the test-path-b-replay validator: 1. Cross-page goto: `goto` action now uses `location.assign` (full navigation, Next.js fetches + mounts target route with effects firing naturally) and suspends the loop so the new page's HarnessReplay re-mount resumes from the next action via the HARNESS_REPLAY_INDEX_KEY cursor in sessionStorage. Previous pushState+popstate approach didn't trigger Next.js router, leaving subsequent actions to run against a stale route. 2. Strict-mode guard: module-level `replayInFlight` flag prevents React dev strict-mode double-mount from firing two parallel replay loops. Previously saw interleaved action logs. 3. findByRole: strip Unicode symbols from accessible name before regex-matching. Buttons like "↑ Withdraw" (arrow icon + text) weren't matching /^\s*withdraw\s*$/i because Playwright's accessible-name normalisation strips the icon children; our DOM walk kept them in textContent. Also falls back to anchor-less match when the ^...$ regex has icon prefix/suffix. POC state: server-side DSL fully works (8/8 scenario green, stepActions emitted in reproduce.json, gallery→/dev/reproduce→session plumbing green). Client-side interpreter has the architecture in place + above fixes, but some actions still time out on first-load race conditions (paint delays, provider warm-up) — the production polish (action retries, longer defaults, accessibility-tree find) is tracked as next-session work.
…play Path B client-side replay end-to-end for e2e-withdraw-bank-ach step 06 now reliably reaches the success-receipt state. Five fixes, each addressing a distinct failure mode from the POC pass: 1. goto no-op when already on target URL — avoids a needless full-page reload when the action DSL's goto matches current location. 2. Replace sessionStorage lease with a per-document in-flight flag. SessionStorage persisted across navigation and blocked the next page's HarnessReplay from firing; module-level resets per page load. 3. Bypass /crisp-proxy — Crisp chat widget iframe mounts ClientProviders which loads HarnessReplay again, running a parallel replay loop against empty DOM. Early-return when pathname starts with /crisp-proxy. 4. Accessible-name computation better matches Playwright's getByRole so click-role / click-text match the same elements the runner does. 5. Assorted logging + timeout bumps for slow Turbopack cold-compiles.
State machine on /card: - computeCardState: pioneer → add-card → pending/manual-review/rejected → active (ENABLED+noCard correctly routes to add-card, not pending) - findActiveCard helper centralizes card selection across subpages Screens: - AddCardEntryScreen: CardFace preview (sample PAN + Peanut Pioneer NUTTY YOU + 06/69) + feature list + CTA - CardTermsScreen: US 5-checkbox / INT 4-checkbox variants, E-Sign + Issuer Privacy Policy links - ApplicationStatusScreen: pending / manual-review / rejected - YourCardScreen: CardFace + reveal + Card management + Red zone, nuqs ?action=lock|unlock|cancel - CardPinScreen: masked view with eye-slash toggle, set/change via ?mode=set, auto-mask on blur/visibility/30s - CardLimitsScreen: single per-transaction row (Weekly/Yearly not supported by Rain) - LockCardModal: SlideToAction for lock, Button for unlock - CancelCardModal: 3-phase (confirm → feedback → thanks); defers queryClient invalidation to handleClose so modal doesn't unmount mid-flow - PhysicalCardScreen: Coming soon → Join → "You are #N on the list" - AddToWalletCarousel: 4-step iOS/Android via useWalletPlatform - CardPinSetupFlow + PinInput (4-dot custom) Apply flow end-to-end: - /card dispatcher: isIssuing > pendingTerms > state machine; handles incomplete (Sumsub) + terms-required + pending; "Setting up your card…" loading screen on issue - SumsubKycWrapper: onActionSubmitted listener + hasSubmittedRef skips "Stop verification?" after successful submit Supporting: - CardFace: pink card, hand SVG rotated -15deg, peanut icon + Visa brand mark, masked/revealed/preview modes - Home: 'card' activation step between deposit + outbound, dismissable via localStorage - Profile: My Card entry (gated by disableCardPioneers, now false) - 11 server actions → client-side fetches in services/rain.ts; cardApi.getInfo/purchase migrated too (kills POST /card noise) - Lock/Cancel/Limits modals get classWrap to fix mobile drawer issue
Inline the 6 pre-existing server actions in app/actions/rain.ts
(getRainCardOverview, getRainSessionKeyAddress, submitRainWithdraw-
SessionApproval, prepareRainWithdrawal, submitRainWithdrawal,
stampRainWithdrawal) into services/rain.ts using the existing
rainRequest helper + JWT cookie pattern. Drop app/actions/rain.ts.
All card calls now go through a single client-side rainApi object —
no `'use server'` indirection left for Rain. useGrantSessionKey
switches from the {data, error} return shape to try/catch.
CONTRIBUTING.md deleted; UI-specific rules (design system, URL-as-state, imports, layouts) folded into mono/AGENTS.md under its peanut-ui section. CLAUDE.md, AGENTS.md, .cursorrules, .windsurfrules all point to ../AGENTS.md so there's one source of truth across repos.
- AddToWalletCarousel: dropped the lavender "Step N preview" placeholder, renders the real Apple/Google step images (exported from Figma) via next/image - Added 8 step PNGs under src/assets/cards/wallet-steps/ and exposed APPLE_WALLET_STEPS / GOOGLE_WALLET_STEPS tuples in assets/cards/index.ts - YourCardScreen: new "Add to Apple Wallet" / "Add to Google Wallet" row in Card Management (hidden on desktop), links to existing /card/add-to-wallet; Physical card row re-positioned so the stack renders correctly - next.config.js: added allowedDevOrigins (default peanut.mucu.dev, extensible via NEXT_ALLOWED_DEV_ORIGINS) so Next.js 16 stops flagging dev chunks and HMR when developing against a tunneled origin
One-click helpers for hand-operated local testing: - Send me $10 / $100 USDC (harness EOA → your SA) - Approve KYC (Bridge · US/EU) — creates real Bridge sandbox customer - Approve KYC (Manteca · AR) — binds ACTIVE sandbox user - Create Sumsub applicant - Simulate Bridge deposit ($25) - Reset my provider state (keeps passkey + session) All actions call peanut-api-ts /dev/cheats/* which delegate to the Nutcracker harness library (engineering/qa/lib/*). Zero duplicated logic. Only mounts under /dev (DevLayout gates prod). Secret comes from NEXT_PUBLIC_TEST_HARNESS_SECRET with a safe local default. Also linked from /dev landing page.
…ight passkeyPreflight only recognized 'localhost' as a secure context, rejected '127.0.0.1' and '[::1]' even though all three are WebAuthn-compliant secure contexts per W3C. Result: Set-up-passkey button disabled when browsing http://127.0.0.1:3050 (the default servers.sh URL). Fix: prefer window.isSecureContext (the browser's own determination), fall back to hostname allowlist (localhost, 127.0.0.1, [::1], ::1) for environments where it's unreliable. usePasskeySupport.ts already did this correctly; only passkeyPreflight was wrong.
Continue from the terms screen now prompts the session-key grant in the same passkey tap that writes consent. Cancelled taps fail closed: the user stays on the terms screen with an error and no card gets issued. - useGrantSessionKey: split runSerialize() out of grant() and expose a new serializeGrant() that does only the passkey + returns the serialized string (no backend POST). grant() is unchanged for the existing lazy-grant consumers (useSpendBundle, /dev/card-session-approve). - services/rain.ts: applyForCard accepts optional serializedApproval, forwarded to POST /rain/cards. - /card/page.tsx: handleAcceptTerms checks for contractAddress + coordinatorAddress on overview (re-issue branch). If present, collect the tap first and pass the serialized string into applyForCard. First-time applicants skip the tap — the collateral proxy doesn't exist yet — and pick it up on the next re-issue pass.
kernelClient.context used to set maxFeePerGas=0 on any chain matching PEANUT_WALLET_CHAIN.id, relying on ZeroDev UltraRelay to override. Sandbox points PEANUT_WALLET_CHAIN at Arb Sepolia (421614), where the ZeroDev bundler is standard Pimlico — it rejects zero-fee userops with 'maxFeePerGas must be at least N — use pimlico_getUserOperationGasPrice'. Restrict the shortcut to mainnet Arb One (42161). Arb Sepolia, Ethereum Sepolia, Base Sepolia fall through to standard viem gas estimation.
…pto paths - .github/workflows/tests.yml: run `pnpm typecheck` before unit tests - Install missing @testing-library/dom peer dep (unblocks screen/fireEvent imports that were failing TS2305 across 6+ test files). - e2e/utils/*: switch to `import type` for Playwright types (verbatimModuleSyntax) - add-money-states.test.tsx: add required `supportedChains` to mocks - dev/cheats/page.tsx: coerce harnessSecret `| false` to string - withdraw/crypto/page.tsx, useBalance.ts: cast PEANUT_WALLET_TOKEN as Address - HarnessReplay.tsx: simplify findByRole signature away from the Action-derived conditional type that narrowed to `never` after an earlier refactor Typecheck now passes. Gates CI so future PRs can't regress.
Mounts <PeanutDebug /> client component that installs helpers on window
when hostname is localhost/127.0.0.1/[::1]:
peanutDebug.signOut() clear cookies+storage+IDB, reload
peanutDebug.whoami() current user KYC/wallet state
peanutDebug.fund("10") harness EOA → your SA ($10 USDC)
peanutDebug.approveKyc("bridge","US") bridge/manteca/sumsub
peanutDebug.simulateBridgeDeposit("25")
peanutDebug.resetMe() wipe your bridge/manteca/ledger
peanutDebug.ledgerHealth() ledger row counts + dual-write
peanutDebug.ledgerHistory() your ledger intents raw
peanutDebug.help() list all commands
All wrap the peanut-api-ts /dev/cheats/* endpoints (same as /dev/cheats UI
panel), zero duplicated logic. Uses NEXT_PUBLIC_TEST_HARNESS_SECRET with
a safe local default. No-op on non-localhost hostnames.
peanutDebug.faucetHelp() from the browser console: - prints the harness EOA (0x441D796c62548F74505DE578c458908d936A3B53) - explains the refill procedure + why we can't use a mock (Bridge/Manteca hardcode Circle's canonical contract) - opens https://faucet.circle.com/ in a new tab
Knip-flagged cleanup in peanut-ui: - Remove `jsqr`, `marked`, `@serwist/build` — no source references (knip), verified by grep. `@serwist/next` and `serwist` itself are still used. - PerkClaimModal exported both named and default for the same component; only the default is imported (HomeCarouselCTA). Drop the named export so the two forms can't drift.
Knip flagged all five as having no importers (verified by grep): - Marketing/index.ts barrel — no consumers - MarketingNav.tsx, Section.tsx, FAQSection.tsx — never imported - RelatedPages.tsx — shadowed by mdx/RelatedPages.tsx (the one MDX consumers actually bind to) Typecheck + 25 test suites (720 tests) pass.
- useWalletBalances (41) — TanStack Query helper, zero consumers - useWalletType (69) — detection helper, zero consumers - useCrispEmbedUrl (32) — Crisp helper, zero consumers (sibling useCrispProxyUrl / useCrispTokenId / useCrispUserData ARE used) - useAvatar (15) — dicebear identicon helper, zero consumers All verified by grep of the hook name across src/. Typecheck + 720 tests pass.
Both views are only referenced by the Setup/Views barrel, which itself is consumed by Setup.consts.tsx — that file only imports InstallPWA, SetupPasskey, SignupStep, LandingStep, SignTestTransaction. Welcome and JoinBeta never made it into the step flow. Trim the barrel to match.
All verified as having zero live imports. The two home-page ones were disabled via commented-out imports: - CardPurchaseScreen (216) — Card flow rewrite landed without it - Home/AddMoneyPromptModal (65) — zero consumers - Home/FloatingReferralButton (35) — import commented out in home page - Home/ReferralCampaignModal (84) — import commented out in home page - Global/Contributors/index.tsx (23) — sibling ContributorCard is live, this barrel had no consumers - Profile/components/CountryListSection (88) — zero consumers Typecheck + 720 tests pass.
There was a problem hiding this comment.
Actionable comments posted: 1
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/components/Kyc/SumsubKycWrapper.tsx (1)
109-116:⚠️ Potential issue | 🟠 MajorMulti-level flows are marked “submitted” too early, which bypasses close confirmation.
hasSubmittedRef.currentis set before the multi-level early return. That makes the close button skip confirmation even when Level 2 is still pending.🔧 Suggested fix
const handleSubmitted = () => { console.log('[sumsub] onApplicantSubmitted fired') - hasSubmittedRef.current = true // for multi-level workflows (LATAM), the SDK transitions to Level 2 // internally. don't close the modal on Level 1 submission. if (isMultiLevelRef.current) return + hasSubmittedRef.current = true stableOnComplete() } const handleActionSubmitted = () => { console.log('[sumsub] action submitted fired') - hasSubmittedRef.current = true if (isMultiLevelRef.current) return + hasSubmittedRef.current = true stableOnComplete() }Also applies to: 128-133, 251-261
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/Kyc/SumsubKycWrapper.tsx` around lines 109 - 116, The hasSubmittedRef.current is being set before the multi-level check in handleSubmitted, causing multi-level workflows to be treated as fully submitted and skipping the close confirmation; update handleSubmitted (and the other similar handlers at the other occurrences) so you only set hasSubmittedRef.current = true after confirming the workflow is not multi-level (i.e., after the if (isMultiLevelRef.current) return check) or only set it inside the branch that calls stableOnComplete, ensuring isMultiLevelRef, hasSubmittedRef, and stableOnComplete are used as before but the submitted flag is only flipped when actually completing the flow.src/components/Create/useCreateLink.tsx (1)
78-100:⚠️ Potential issue | 🔴 CriticalDon't guess
depositIdxfrom a preflight read when the receipt/logs are missing.
getNextDepositIndex()runs before the transaction, so the fallback can become stale as soon as any other deposit lands first. IfsendTransactionsreturns no receipt, this can generate a claim link for the wrong deposit; and if the receipt exists butDepositEventparsing fails, the current code crashes after funds may already be deposited. Only derivedepositIdxfrom the confirmed tx/event (or a tx-hash-based receipt lookup), and fail/poll instead of returning a guessed link.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/Create/useCreateLink.tsx` around lines 78 - 100, The code is incorrectly falling back to the preflight getNextDepositIndex() when sendTransactions() returns no receipt or when parseEventLogs() fails, which can produce a stale/wrong depositIdx; change the flow in useCreateLink so depositIdx is only derived from a confirmed transaction/event: stop using nextIndex as a fallback, call sendTransactions() first (or if keeping the Promise.all pattern, after sendTransactions resolve, if it returned a tx hash poll or fetch the receipt by transactionHash), then parse DepositEvent from the confirmed receipt via parseEventLogs and set depositIdx from topics[1]; if you cannot obtain a valid receipt or cannot parse the DepositEvent, surface an error (or start a polling retry) instead of returning a guessed index, and ensure txHash is used for receipt lookup and error handling (references: getNextDepositIndex, sendTransactions, parseEventLogs, DepositEvent, depositIdx).
♻️ Duplicate comments (2)
CLAUDE.md (1)
1-1:⚠️ Potential issue | 🟠 MajorVerify shared rules target resolves (
../AGENTS.md) across environments.Line 1 points outside this repo. If the parent file is missing in CI/dev containers, agent-rule loading will silently break again.
#!/bin/bash set -euo pipefail files=("CLAUDE.md" ".cursorrules" ".windsurfrules" "AGENTS.md") python - <<'PY' import os files = ["CLAUDE.md", ".cursorrules", ".windsurfrules", "AGENTS.md"] for f in files: with open(f, "r", encoding="utf-8") as fh: target = fh.readline().strip() resolved = os.path.normpath(os.path.join(os.path.dirname(f), target)) print(f"{f}: target='{target}' -> resolved='{resolved}' exists={os.path.exists(resolved)}") PYExpected result: all
exists=True.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CLAUDE.md` at line 1, CLAUDE.md's first-line target points outside the repo (../AGENTS.md) which can break agent-rule loading; change the target line in CLAUDE.md to a repo-local path (e.g., AGENTS.md or ./AGENTS.md) and add a small verification step (using the provided existence-check logic) to the CI or startup validation so files CLAUDE.md, .cursorrules, .windsurfrules, and AGENTS.md are checked and fail the build if any resolved path does not exist; reference the filenames CLAUDE.md, .cursorrules, .windsurfrules and AGENTS.md and ensure the validation runs in CI/dev containers.package.json (1)
158-164:⚠️ Potential issue | 🟠 MajorRemove the stale Peanut SDK Jest escape hatch.
The dependency is gone, but Jest still both special-cases
@squirrel-labsin transforms and aliases@squirrel-labs/peanut-sdkto a local mock. That keeps tests green even if a real import survives somewhere and would fail in the actual app/runtime.#!/bin/bash sed -n '156,166p' package.json rg -nP 'from\s+["'\'']@squirrel-labs/peanut-sdk["'\'']|require\(\s*["'\'']@squirrel-labs/peanut-sdk["'\'']\s*\)' .🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` around lines 158 - 164, Remove the stale Peanut SDK Jest escape hatch from package.json by deleting the "^@squirrel-labs/peanut-sdk$" entry in "moduleNameMapper" and removing the special-case "@squirrel-labs" token from the transform ignore pattern (the node_modules/(?!(`@squirrel-labs`|...)/) regex) so Jest no longer aliases or exempts `@squirrel-labs` imports; update the transform ignore regex and moduleNameMapper to reflect only currently needed packages (remove the `@squirrel-labs` references) and run the provided grep commands to verify no real imports to "@squirrel-labs/peanut-sdk" remain.
🟡 Minor comments (28)
src/components/Profile/index.tsx-50-53 (1)
50-53:⚠️ Potential issue | 🟡 MinorFix conditional menu positioning for the card-disabled state.
When
disableCardPioneersis true,Your Badgesbecomes the first item but still usesposition="middle", which can break group styling.Suggested fix
{!underMaintenanceConfig.disableCardPioneers && ( <ProfileMenuItem icon="credit-card" label="Your Card" href="/card" position="first" /> )} -<ProfileMenuItem icon="achievements" label="Your Badges" href="/badges" position="middle" /> +<ProfileMenuItem + icon="achievements" + label="Your Badges" + href="/badges" + position={underMaintenanceConfig.disableCardPioneers ? 'first' : 'middle'} +/>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/Profile/index.tsx` around lines 50 - 53, The ProfileMenuItem for "Your Badges" currently hardcodes position="middle" causing incorrect group styling when underMaintenanceConfig.disableCardPioneers is true; update the component so the position prop for the "Your Badges" ProfileMenuItem is conditional (e.g., set to "first" when underMaintenanceConfig.disableCardPioneers is true, otherwise "middle"), or compute a small variable above (e.g., badgesPosition) and pass that to the ProfileMenuItem to ensure proper positioning when the "Your Card" item is omitted.src/components/TransactionDetails/TransactionDetailsReceipt.tsx-410-412 (1)
410-412:⚠️ Potential issue | 🟡 MinorUse nullish checks for converted amount presence.
At Line 410,
!amountwill hide valid numeric0. Prefer explicit null/undefined (and optional empty-string) checks.Suggested fix
- if (!code || !amount) return null + if (!code || amount === null || amount === undefined || amount === '') return null if (code.toUpperCase() === 'USD') return null🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/TransactionDetails/TransactionDetailsReceipt.tsx` around lines 410 - 412, The current guard in TransactionDetailsReceipt that uses `if (!code || !amount) return null` hides legitimate zero amounts; change the second condition to an explicit null/undefined/empty-string check (e.g. `amount == null` or `amount === '' || amount == null`) so that numeric 0 is allowed, while still returning null when amount is null/undefined/empty; keep the existing `code` uppercasing logic (`code.toUpperCase() === 'USD'`) and the final `return `${code.toUpperCase()} ${formatCurrency(amount)}`` unchanged.e2e/utils/dismiss-modals.ts-1-1 (1)
1-1:⚠️ Potential issue | 🟡 MinorFix Prettier formatting.
The pipeline indicates a Prettier formatting issue. Run
pnpm prettier --write e2e/utils/dismiss-modals.tsto resolve.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/utils/dismiss-modals.ts` at line 1, Run Prettier to fix formatting in the file that imports Page (import type { Page } from '@playwright/test'); execute the project's Prettier formatter (e.g. pnpm prettier --write) targeting that file, ensure trailing newline and consistent spacing are applied, and commit the reformatted file.src/config/wagmi.config.tsx-49-49 (1)
49-49:⚠️ Potential issue | 🟡 MinorConsider the root cause of the type cast requirement.
The
wagmiConfig as Configcast is used only forcookieToInitialState(line 49) but not forWagmiProvider(line 52), despite both expecting the same config type. This inconsistency suggestscreateConfig's generic type inference may not be resolving correctly. While the cast works as a workaround, it masks a potential issue. Either investigate why the return type isn't being properly inferred fromcreateConfig(), or add a comment explaining why the cast is specifically required forcookieToInitialState.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/config/wagmi.config.tsx` at line 49, The cast "wagmiConfig as Config" is hiding a typing mismatch between createConfig()'s inferred return and cookieToInitialState; either fix the source of inference by explicitly typing the wagmiConfig variable to the expected Config type returned from createConfig (or supply the proper generic to createConfig) so both wagmiConfig and WagmiProvider use the same inferred type, or update the cookieToInitialState function signature to accept the actual type returned by createConfig (e.g., use ReturnType<typeof createConfig>), and if you must keep the cast, add a short comment on why the cast is required mentioning wagmiConfig, cookieToInitialState, WagmiProvider, and createConfig.e2e/utils/capture.ts-1-1 (1)
1-1:⚠️ Potential issue | 🟡 MinorRun formatter to resolve the CI Prettier warning.
This file is currently tripping the formatting check.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/utils/capture.ts` at line 1, This file is failing Prettier; run the project's formatter (e.g., prettier --write or npm run format) on e2e/utils/capture.ts (or the whole repo) to apply the required formatting changes, then stage and commit the formatted file so the CI Prettier check passes.e2e/utils/capture.ts-79-102 (1)
79-102:⚠️ Potential issue | 🟡 Minor
collectConsoleLogsshould expose listener cleanup to avoid duplicate subscriptions.When called multiple times on the same
page, listeners accumulate and can duplicate entries.Patch
export function collectConsoleLogs(page: Page) { const entries: Array<{ type: string; text: string }> = [] - page.on('console', (msg) => { + const listener = (msg: Parameters<Page['on']>[1] extends (arg: infer T) => any ? T : never) => { const text = msg.text() if (CONSOLE_NOISE.some((p) => p.test(text))) return entries.push({ type: msg.type(), text: text.slice(0, 500) }) - }) + } + page.on('console', listener) return { entries, + dispose: () => page.off('console', listener), flush: (testInfo: TestInfo, name: string) => { @@ } return entries }, } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/utils/capture.ts` around lines 79 - 102, The collectConsoleLogs function currently attaches a console listener via page.on('console', ...) but never removes it, causing duplicate subscriptions when called multiple times; modify collectConsoleLogs to store the listener callback in a const (e.g., const handler = (msg) => { ... }), attach it with page.on('console', handler), and expose a cleanup method (e.g., stop or dispose) in the returned object that calls page.off('console', handler) (or page.removeListener if off isn't available) to unregister the listener; keep entries and flush behavior the same but ensure callers can call the new cleanup method to prevent listener accumulation.docs/TESTING.md-8-12 (1)
8-12:⚠️ Potential issue | 🟡 MinorDrop the hard-coded test counts and timings.
These numbers will go stale quickly and create needless doc churn. Keep the commands stable, and point readers to CI for current counts/durations instead.
📝 Suggested doc tweak
- npm test # Jest unit + component (710+ tests, ~25s) - npx playwright test --project=mobile # E2E smoke (49 tests, ~8 min) - npx tsx e2e/scripts/generate-report.ts --save-baseline # Visual regression baseline - npx tsx e2e/scripts/generate-report.ts # Compare against baseline + npm test # Jest unit + component tests + npx playwright test --project=mobile # Mobile E2E smoke + npx tsx e2e/scripts/generate-report.ts --save-baseline # Save visual regression baseline + npx tsx e2e/scripts/generate-report.ts # Compare against baseline🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/TESTING.md` around lines 8 - 12, The doc contains hard-coded test counts and timings next to the commands (e.g., "npm test", "npx playwright test --project=mobile", "npx tsx e2e/scripts/generate-report.ts --save-baseline", "npx tsx e2e/scripts/generate-report.ts") which will go stale; remove the parenthetical counts/durations and instead keep only the commands and a short note directing readers to check the CI system (or test dashboard) for current test counts and runtimes.src/app/(mobile-ui)/card/add-to-wallet/page.tsx-13-15 (1)
13-15:⚠️ Potential issue | 🟡 MinorPrevent duplicate "viewed" analytics events for a single page visit.
This effect re-fires whenever
platformchanges, which can overcountCARD_ADD_TO_WALLET_VIEWEDfor one screen view.Suggested fix
-import { type FC, useEffect } from 'react' +import { type FC, useEffect, useRef } from 'react' @@ const AddToWalletPage: FC = () => { const router = useRouter() const platform = useWalletPlatform() + const hasTrackedViewedRef = useRef(false) + useEffect(() => { + if (hasTrackedViewedRef.current) return posthog.capture(ANALYTICS_EVENTS.CARD_ADD_TO_WALLET_VIEWED, { platform: platform ?? 'unknown' }) + hasTrackedViewedRef.current = true }, [platform])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`(mobile-ui)/card/add-to-wallet/page.tsx around lines 13 - 15, The effect currently calls posthog.capture(ANALYTICS_EVENTS.CARD_ADD_TO_WALLET_VIEWED) whenever platform changes, which can double-count; change the logic in the useEffect that calls posthog.capture (or wrap it) so it only runs once per mount—e.g., add a local ref like hasTrackedRef and in the useEffect (watching platform if you need its final value) check if hasTrackedRef.current is false, call posthog.capture(ANALYTICS_EVENTS.CARD_ADD_TO_WALLET_VIEWED, { platform: platform ?? 'unknown' }), set hasTrackedRef.current = true, and prevent further captures; alternatively, if platform value at mount is sufficient, run the useEffect with an empty dependency array and capture platform ?? 'unknown' once.e2e/utils/env.ts-14-23 (1)
14-23:⚠️ Potential issue | 🟡 MinorResolve Prettier drift in
getHarnessSecretblock.Pipeline already reports a formatting warning; Line 15 onward appears to use indentation that diverges from formatter output.
🎯 Minimal formatting fix
export function getHarnessSecret(): string { - const secret = process.env.TEST_HARNESS_SECRET - if (!secret) { - throw new Error( - 'TEST_HARNESS_SECRET is not set. The e2e harness must match the value on the API side — ' + - 'set it in your shell (.env.e2e, bin/qa env, or direct export) before running tests.', - ) - } - return secret + const secret = process.env.TEST_HARNESS_SECRET + if (!secret) { + throw new Error( + 'TEST_HARNESS_SECRET is not set. The e2e harness must match the value on the API side — ' + + 'set it in your shell (.env.e2e, bin/qa env, or direct export) before running tests.', + ) + } + return secret }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/utils/env.ts` around lines 14 - 23, The getHarnessSecret function has Prettier/indentation drift in its body (the const/if/throw/return lines and the multiline error string); reformat that block to match the project's Prettier style (fix indentation of the const secret, the if/throw block and the concatenated error message) or run the project's formatter, ensuring the throw message lines are indented consistently and the return is aligned with the function body in getHarnessSecret.e2e/flows/claim.spec.ts-19-40 (1)
19-40:⚠️ Potential issue | 🟡 MinorPrettier formatting needs fixing in this spec.
CI already reports a Prettier warning for this PR; this file uses inconsistent indentation and should be auto-formatted before merge.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/flows/claim.spec.ts` around lines 19 - 40, This spec has inconsistent Prettier formatting (misaligned indentation in the two tests "claim page without pubKey — error state" and "claim page with invalid pubKey") — run your project's Prettier/formatter on this file and reformat the block containing the tests that call collectConsoleLogs, captureStep and consoleLogs.flush so indentation and spacing are consistent with the repo style, then commit the formatted file.e2e/flows/dev-showcase.spec.ts-15-44 (1)
15-44:⚠️ Potential issue | 🟡 MinorRun Prettier on this spec.
CI is already reporting a formatting failure, and the tab-indented blocks in this new file look like the likely source.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/flows/dev-showcase.spec.ts` around lines 15 - 44, The spec file fails CI formatting; run Prettier on this test file and replace tab indentation with your project's configured spacing so the AST and lint formatting match CI expectations; specifically reformat the test.describe block and its nested tests (references: test.describe, test('/dev/components landing', test('/dev — root dev page', and test('/dev/ds — design system root')) as well as helper calls collectConsoleLogs, dismissModals, captureStep) so all lines use the project's Prettier settings and commit the rewritten file.e2e/flows/home.spec.ts-18-83 (1)
18-83:⚠️ Potential issue | 🟡 MinorRun Prettier on this spec.
CI already reports a formatting failure, and the tab-indented blocks in this file are likely contributing to it.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/flows/home.spec.ts` around lines 18 - 83, This spec is failing CI formatting due to tab-indented blocks; run Prettier (or your repo's formatter) on the file to replace tabs with the repository's configured spaces and normalize line breaks/indentation for the test.describe block and both test(...) cases (the "authenticated home renders with core elements" and "home with history persona — shows activity" tests), then save/commit the reformatted file so CI passes.src/components/Claim/__tests__/claim-states.test.tsx-336-344 (1)
336-344:⚠️ Potential issue | 🟡 MinorThis test never verifies the retry message it claims to cover.
The case only asserts that the loading spinner is still visible. It does not check any retry copy or retry affordance, so the behavior in the test name is currently untested. Either assert the retry UI explicitly or rename the test to match the current expectation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/Claim/__tests__/claim-states.test.tsx` around lines 336 - 344, The test "Loading with retries shows retry message" currently only asserts the loading spinner and never checks retry UI; update the test in claim-states.test.tsx so it either (A) asserts the retry copy/affordance after retries by advancing timers or awaiting the retry state and then checking for the expected text/button via screen.getByText/getByRole, referencing mockSendLinksApi.get and renderClaim to trigger the retry loop, or (B) if you prefer not to test the retry UI here, rename the test to reflect that it only confirms the loading spinner (e.g., "shows loading spinner when API fails") so the test name matches the assertion. Ensure you reference the existing mocks (mockSendLinksApi.get) and helper (renderClaim) when implementing the fix.src/app/(mobile-ui)/home/page.tsx-56-56 (1)
56-56:⚠️ Potential issue | 🟡 MinorKeep the warning modal on the same balance source as the card.
This page now renders
spendableBalance, but the warning effect still usesbalance/isFetchingBalance. With pending holds, users can see a low spendable balance and still get the “high balance” warning. Please migrate the warning check to the spendable fields as well, or make the modal copy explicit that it is based on total balance.Also applies to: 188-192
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`(mobile-ui)/home/page.tsx at line 56, The warning modal currently uses balance and isFetchingBalance while the UI shows spendableBalance; update the warning logic to base its threshold/checks and loading state on spendableBalance and isFetchingSpendableBalance (references: useWallet(), balance, isFetchingBalance, spendableBalance, isFetchingSpendableBalance) so the modal reflects the same balance source as the card, and also update any modal copy (or the check) around the high-balance message for the related lines (188-192) to explicitly reference "spendable balance" if you prefer to keep the check on total balance.e2e/flows/dev-showcase.spec.ts-36-37 (1)
36-37:⚠️ Potential issue | 🟡 MinorCheck response status when navigating to optional routes.
page.goto()returns a Response object for HTTP error pages like 404, so the current checkif (!res)won't skip them. The route will continue running against the app's 404 page instead. Useif (!res || !res.ok())to properly skip when the route doesn't exist.Suggested fix
- const res = await page.goto('/dev/ds', { waitUntil: 'domcontentloaded' }).catch(() => null) - if (!res) return // not all repos have this route + const res = await page.goto('/dev/ds', { waitUntil: 'domcontentloaded' }).catch(() => null) + if (!res || !res.ok()) return // not all repos have this route🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/flows/dev-showcase.spec.ts` around lines 36 - 37, The current navigation check using page.goto(...) only tests for a null response and misses HTTP errors; update the guard after the call to page.goto in dev-showcase.spec.ts to also check the response status by using the Response object's ok() method (i.e., change the conditional that tests res to use if (!res || !res.ok()) ) so optional routes that return 404/500 are skipped; locate the page.goto call and the following if (!res) return and modify the conditional accordingly.e2e/flows/send-link.spec.ts-45-47 (1)
45-47:⚠️ Potential issue | 🟡 MinorURL-state test does not verify the
stepstate.The assertion only checks the path, so it won’t fail if query-state parsing regresses.
Suggested change
- // Check that URL contains step parameter - expect(page.url()).toContain('/send') + // Check that URL contains step parameter + expect(page.url()).toContain('step=inputAmount')🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/flows/send-link.spec.ts` around lines 45 - 47, The test currently only asserts expect(page.url()).toContain('/send') and should also verify the query-state by parsing the URL returned from page.url() and asserting the presence and expected value of the "step" query parameter; update the assertion near expect(page.url()).toContain('/send') to parse the URL (using URL or URLSearchParams on page.url()) and add an assertion that searchParams.get('step') exists and equals the expected step string (or at minimum is truthy) so regressions in query-state parsing will fail the test.e2e/flows/send-flow.spec.ts-87-91 (1)
87-91:⚠️ Potential issue | 🟡 MinorBack-navigation behavior is observed but not validated.
The test captures a screenshot after
goBack()but never asserts the step actually changed.Suggested change
await page.goBack() await page.waitForTimeout(1000) await captureStep(page, testInfo, { name: '03-send-after-back' }) + expect(page.url()).toContain('step=inputAmount')🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/flows/send-flow.spec.ts` around lines 87 - 91, After calling page.goBack() you must assert the UI actually moved to the previous step before taking the screenshot; use the page.goBack() call followed by an assertion that the step indicator/active step changed (e.g., check the step label or the active CSS class of the step element) and only then call captureStep(page, testInfo, { name: '03-send-after-back' }). Locate the DOM element used for step state in this test (the same element captureStep visually documents) and use Playwright's expect on its textContent or classList to verify the expected previous step value.e2e/flows/icon-regression.spec.ts-60-67 (1)
60-67:⚠️ Potential issue | 🟡 MinorButton-specific regression check can pass vacuously.
If no Lucide icons are rendered inside buttons, this loop runs zero times and the test still passes, so the target regression is not actually guarded.
Suggested change
const inButtons = all.filter((i) => i.inAnyButton) + expect(inButtons.length, 'expected at least one lucide icon inside a button').toBeGreaterThan(0) for (const icon of inButtons) { expect(icon.inlineFill, `${icon.name} inside a button must have fill:none inline`).toMatch( /^(none|currentcolor)$/ ) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/flows/icon-regression.spec.ts` around lines 60 - 67, The test currently lets the in-button checks pass vacuously when no Lucide icons appear inside buttons; add an explicit assertion that the extracted inButtons array is non-empty before iterating so the regression is actually enforced. Locate the inButtons = all.filter((i) => i.inAnyButton) line and add an assertion like expect(inButtons.length, 'expected at least one Lucide icon inside a button for this regression test').toBeGreaterThan(0) (or throw/fail with a clear message) immediately before the for (const icon of inButtons) loop so the test fails when there are no in-button icons to validate.e2e/flows/withdraw.spec.ts-52-75 (1)
52-75:⚠️ Potential issue | 🟡 MinorClose manual browser contexts in
finally.If any step fails before the last line,
await context.close()is skipped and the worker keeps a leaked mobile context around. Wrap these two tests intry/finallyso cleanup still happens on early failure.Suggested fix pattern
test('withdraw/manteca — ARS/LATAM withdrawal (verified-ar)', async ({ browser }, testInfo) => { const context = await browser.newContext({ ...devices['Pixel 7'] }) - const persona = await usePersona(context, 'verified-ar') - - const page = await context.newPage() - const consoleLogs = collectConsoleLogs(page) - await installApiMocks(page) - - await page.goto('/withdraw/manteca') - await captureStep(page, testInfo, { name: '01-withdraw-manteca-initial' }) - - await page.waitForTimeout(2000) - await captureStep(page, testInfo, { name: '02-withdraw-manteca-loaded' }) - - if (persona) { - testInfo.annotations.push({ - type: 'persona', - description: `verified-ar (${persona.userId})`, - }) - } - - consoleLogs.flush(testInfo, 'withdraw-manteca') - await context.close() + try { + const persona = await usePersona(context, 'verified-ar') + const page = await context.newPage() + const consoleLogs = collectConsoleLogs(page) + await installApiMocks(page) + + await page.goto('/withdraw/manteca') + await captureStep(page, testInfo, { name: '01-withdraw-manteca-initial' }) + + await page.waitForTimeout(2000) + await captureStep(page, testInfo, { name: '02-withdraw-manteca-loaded' }) + + if (persona) { + testInfo.annotations.push({ + type: 'persona', + description: `verified-ar (${persona.userId})`, + }) + } + + consoleLogs.flush(testInfo, 'withdraw-manteca') + } finally { + await context.close() + } })Also applies to: 77-100
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/flows/withdraw.spec.ts` around lines 52 - 75, The tests "withdraw/manteca — ARS/LATAM withdrawal (verified-ar)" (test block starting with test(... 'withdraw/manteca — ARS/LATAM withdrawal (verified-ar)')) leak the browser context if an assertion or step throws before await context.close(); wrap the body that uses context (creation, usePersona, newPage, collectConsoleLogs, installApiMocks, navigation, captures, annotations, consoleLogs.flush) in a try/finally and move await context.close() into the finally so the Playwright context is always closed even on early failure; apply the same try/finally pattern to the sibling test around lines 77-100 as noted.src/components/Card/CardLimitsScreen.tsx-21-21 (1)
21-21:⚠️ Potential issue | 🟡 MinorFormat card limits with fixed cents precision.
For non-round-dollar amounts, this currently renders values like
$12.5. Limits are monetary UI, so they should stay at two fraction digits.Suggested fix
-const formatDollars = (cents: number) => `$${(cents / 100).toLocaleString('en-US', { minimumFractionDigits: 0 })}` +const formatDollars = (cents: number) => + `$${(cents / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/Card/CardLimitsScreen.tsx` at line 21, The formatDollars helper currently formats cents to dollars with variable fraction digits, causing outputs like "$12.5"; update the function formatDollars to always render two decimal places by dividing cents by 100 and using toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) (or equivalent) so values like 1250 -> "$12.50" and 125 -> "$1.25"; locate the formatDollars arrow function in CardLimitsScreen and change its options accordingly.e2e/flows/send-link-e2e.spec.ts-56-72 (1)
56-72:⚠️ Potential issue | 🟡 MinorAdd a minimal render assertion to the
/send?view=linksmoke test.Right now this passes on a blank/error shell as long as no wagmi-specific error is emitted. A tiny positive assertion, like the
/claimtest already has, would make the regression guard much harder to false-pass.Suggested fix
await page.goto('/send?view=link', { waitUntil: 'domcontentloaded' }) await page.waitForLoadState('networkidle', { timeout: 30_000 }).catch(() => {}) + + const body = await page.locator('body').innerText() + expect(body.length, 'send-link page should render some body content').toBeGreaterThan(0) const wagmiErrors = errors.filter((e) => /useSignTypedData must be used|WagmiContext|WagmiProvider/.test(e)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/flows/send-link-e2e.spec.ts` around lines 56 - 72, The test "send-link page renders without a wagmi provider error" currently only checks for absence of console errors; add a minimal positive render assertion (like the `/claim` test does) to ensure the page actually rendered content. Locate the test in send-link-e2e.spec.ts and after the page.waitForLoadState call, assert that a specific, stable UI element is present/visible (for example a heading or element unique to the send link view such as "Send link" text, a button, or a data-testid like send-link-form) so the test fails on blank/error shells as well as wagmi errors.src/components/Claim/Link/Initial.view.tsx-629-636 (1)
629-636:⚠️ Potential issue | 🟡 MinorAvoid stale route-cache writes in
fetchRoute.Because
fetchRouteis async,[...routes, route]uses the array snapshot from when the request started. If two previews resolve out of order, one cache entry can overwrite the other. Use a functional updater here so concurrent fetches merge safely.Suggested fix
- setRoutes([...routes, route]) + setRoutes((current) => { + const alreadyCached = current.some( + (existing) => + existing.chainId === route.chainId && + areEvmAddressesEqual(existing.tokenAddress, route.tokenAddress) + ) + return alreadyCached ? current : [...current, route] + })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/Claim/Link/Initial.view.tsx` around lines 629 - 636, The current fetchRoute async handler appends a new route using the stale snapshot [...routes, route], which can lead to lost entries when multiple previews resolve out of order; fix this by switching the state update to a functional updater so concurrent fetches merge safely (use setRoutes(prev => [...prev, route]) in place of setRoutes([...routes, route]) inside the fetchRoute resolution where route, routes and setRoutes are referenced).src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx-1093-1123 (1)
1093-1123:⚠️ Potential issue | 🟡 MinorRender the PIX case only once, after the PIX-specific mock is in place.
This test mounts
QRPayPagebefore overridingmockMantecaApi.initiateQrPayment, then mounts it again. The first instance can leave DOM behind that satisfies the assertion, so the test can pass for the wrong state.Suggested fix
test('PIX success shows PIX icon', async () => { - renderQrPay({ qrCode: 'pix://payment?id=123', type: 'PIX', t: '1' }) - mockMantecaApi.initiateQrPayment.mockResolvedValue({ code: 'LOCK_PIX', type: 'PIX', @@ }) - // Re-render with PIX type const { unmount } = renderQrPay({ qrCode: 'pix://payment?id=123', type: 'PIX', t: '1' })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx around lines 1093 - 1123, The test currently calls renderQrPay before setting mockMantecaApi.initiateQrPayment which can leave a stale DOM; fix it by removing the first renderQrPay call and only call renderQrPay({ qrCode: 'pix://payment?id=123', type: 'PIX', t: '1' }) after you set mockMantecaApi.initiateQrPayment.mockResolvedValue(...), then perform the same waitFor/assertions and finally call unmount(); this ensures the component (renderQrPay) is mounted only once with the PIX-specific mock in place.e2e/scripts/generate-report.ts-314-324 (1)
314-324:⚠️ Potential issue | 🟡 MinorKeep manual baseline mode sticky when using hold-
Bpeek.
keyupalways forces the toggle back off. If I already enabled “Baseline” manually and tapB, the report flips back to “Current” on release.Suggested fix
(function() { var toggle = document.getElementById('toggle-baseline') var lblCurrent = document.getElementById('lbl-current') var lblBaseline = document.getElementById('lbl-baseline') + var isPeeking = false @@ document.addEventListener('keydown', function(e) { if (e.key === 'b' && !e.repeat && !toggle.checked) { + isPeeking = true toggle.checked = true setView(true) } }) document.addEventListener('keyup', function(e) { - if (e.key === 'b' && toggle.checked) { + if (e.key === 'b' && isPeeking) { + isPeeking = false toggle.checked = false setView(false) } })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/scripts/generate-report.ts` around lines 314 - 324, The keyup handler unconditionally turns off the baseline toggle causing manual baseline mode to be lost; modify the keydown/keyup pair to track whether the hold-'B' action initiated the checked state (e.g., introduce a local flag like peekActivatedByHold or store previousChecked) so keyup only clears toggle.checked and calls setView(false) if the flag indicates the toggle was changed by the keydown handler; update the keydown handler to set that flag when it sets toggle.checked = true and reset/clear the flag in the keyup branch after reverting the view.e2e/scripts/bootstrap-auth.ts-63-70 (1)
63-70:⚠️ Potential issue | 🟡 MinorDon't reuse partial bootstrap artifacts.
This fast path only checks
bootstrap-storage.json, then immediately readsbootstrap-meta.json. If a previous run left storage behind without metadata, the script crashes instead of recreating the account. Guard both files together before reusing them, likeglobal-setup.tsalready does.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/scripts/bootstrap-auth.ts` around lines 63 - 70, The bootstrap() fast-path currently only checks BOOTSTRAP_STATE_PATH before reading BOOTSTRAP_META_PATH, which crashes if meta is missing; update the guard in bootstrap() to require both BOOTSTRAP_STATE_PATH and BOOTSTRAP_META_PATH exist (and still honor FORCE) before reusing artifacts, mirroring the pattern in global-setup.ts, and if either file is missing proceed with normal creation flow instead of reading bootstrap-meta.json.src/app/(mobile-ui)/dev/debug/page.tsx-154-162 (1)
154-162:⚠️ Potential issue | 🟡 Minor
kycAllnever writes a result for its own row.This preset stores outcomes under
kycAllBridge/kycAllManteca/kycAllSumsub, but the renderer only readsresults['kycAll']for this card. If one provider step fails, the card still looks untouched unless you inspect console output. Aggregate the preset result underkycAllor surface the child-step results here.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`(mobile-ui)/dev/debug/page.tsx around lines 154 - 162, The preset with key 'kycAll' runs three child calls ('kycAllBridge', 'kycAllManteca', 'kycAllSumsub') in its run handler but never writes a result for 'kycAll', so the UI (which reads results['kycAll']) appears unchanged; fix by capturing each call's response (await the three call(...) results inside the run function), aggregate their statuses/errors into a single summary object, and write that summary into the results store under the 'kycAll' key (so the renderer sees the outcome); keep the existing child keys if desired but ensure the run function for key 'kycAll' sets results['kycAll'] (or otherwise returns/dispatches an aggregated result) after the three calls and before refreshWhoami().e2e/global-setup.ts-71-74 (1)
71-74:⚠️ Potential issue | 🟡 MinorAlign both
createAllPersonascall sites to pass consistent persona identifier types.Bootstrap mode (line 72) passes
meta.username, but fallback mode (line 184) passesuser.emailas the third argument. While the codebase pattern elsewhere treats email as an acceptable fallback for username, inconsistency across initialization paths creates personas with different username representations. Both call sites should pass the same type of value (preferably the username when available, or explicitly fall back to email consistently).Also applies to: 183-185
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/global-setup.ts` around lines 71 - 74, The two call sites of createAllPersonas pass different identifier types (one passes meta.username, the other passes user.email); update both call sites to pass a consistent persona identifier by using the same expression (e.g., prefer username with an explicit fallback to email), for example change both third arguments to something like (meta.username || meta.email) or (user.username || user.email) so both bootstrap and fallback paths create personas with the same identifier type; ensure you update the calls where createAllPersonas is invoked (the bootstrap call using meta and the fallback call using user) to use the unified expression.e2e/utils/personas.ts-160-194 (1)
160-194:⚠️ Potential issue | 🟡 MinorDon't cast a partial persona set to a full
PersonaMap.This function explicitly skips failed persona creations, but its return type says every
PersonaIdis present. That removes the missing-persona case from the type system and makes direct indexing look safe when it isn't. Return a partial type that guarantees onlydefault.Possible fix
type PersonaMap = Record<PersonaId, Persona> +type AvailablePersonaMap = Partial<PersonaMap> & Pick<PersonaMap, 'default'> @@ -export async function createAllPersonas(defaultToken: string, defaultUserId: string, defaultUsername: string): Promise<PersonaMap> { - const personas: Partial<PersonaMap> = { +export async function createAllPersonas(defaultToken: string, defaultUserId: string, defaultUsername: string): Promise<AvailablePersonaMap> { + const personas: AvailablePersonaMap = { @@ - return personas as PersonaMap + return personas }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/utils/personas.ts` around lines 160 - 194, The function createAllPersonas incorrectly asserts a full PersonaMap even though some persona creations may fail; change its signature and return value to a partial type (e.g., Promise<Partial<PersonaMap>> or a custom type guaranteeing only default exists like Promise<{ default: Persona } & Partial<PersonaMap>>), remove the unsafe cast "as PersonaMap" at the end, and keep the internal personas variable typed as Partial<PersonaMap>; update any call sites that assume every PersonaId exists to handle missing entries or narrow to the guaranteed default entry.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c2b23611-b6cc-4721-b865-75875b09beb6
⛔ Files ignored due to path filters (18)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlpublic/peanut_guy.gifis excluded by!**/*.gifpublic/peanut_guy_black.gifis excluded by!**/*.gifsrc/assets/cards/peanut-card-hand.svgis excluded by!**/*.svgsrc/assets/cards/visa-brand-mark.pngis excluded by!**/*.pngsrc/assets/cards/wallet-steps/apple-step-1.pngis excluded by!**/*.pngsrc/assets/cards/wallet-steps/apple-step-2.pngis excluded by!**/*.pngsrc/assets/cards/wallet-steps/apple-step-3.pngis excluded by!**/*.pngsrc/assets/cards/wallet-steps/apple-step-4.pngis excluded by!**/*.pngsrc/assets/cards/wallet-steps/google-step-1.pngis excluded by!**/*.pngsrc/assets/cards/wallet-steps/google-step-2.pngis excluded by!**/*.pngsrc/assets/cards/wallet-steps/google-step-3.pngis excluded by!**/*.pngsrc/assets/cards/wallet-steps/google-step-4.pngis excluded by!**/*.pngsrc/assets/illustrations/peanut_guy.gifis excluded by!**/*.gifsrc/assets/illustrations/peanutguy.pngis excluded by!**/*.pngsrc/assets/iphone-ss/iphone-your-money-1.pngis excluded by!**/*.pngsrc/assets/iphone-ss/iphone-your-money-2.pngis excluded by!**/*.pngsrc/assets/iphone-ss/iphone-your-money-3.pngis excluded by!**/*.png
📒 Files selected for processing (282)
.cursorrules.env.example.github/workflows/indexnow.yml.github/workflows/preview.yaml.github/workflows/supply-chain-check.yml.github/workflows/tests.yml.gitignore.npmrc.windsurfrulesAGENTS.mdCLAUDE.mdREADME.mddocs/TESTING.mde2e/flows/add-money.spec.tse2e/flows/claim-flow.spec.tse2e/flows/claim.spec.tse2e/flows/dev-showcase.spec.tse2e/flows/home.spec.tse2e/flows/icon-regression.spec.tse2e/flows/kyc-gate.spec.tse2e/flows/profile-history.spec.tse2e/flows/public.spec.tse2e/flows/qr-pay.spec.tse2e/flows/request-flow.spec.tse2e/flows/send-flow.spec.tse2e/flows/send-link-e2e.spec.tse2e/flows/send-link.spec.tse2e/flows/setup.spec.tse2e/flows/withdraw-flow.spec.tse2e/flows/withdraw.spec.tse2e/global-setup.tse2e/scripts/bootstrap-auth.tse2e/scripts/generate-report.tse2e/utils/capture.tse2e/utils/dismiss-modals.tse2e/utils/env.tse2e/utils/mock-api.tse2e/utils/personas.tse2e/utils/seed.tsjest.setup.tsnext.config.jspackage.jsonplaywright.config.tsplaywright.regression.config.tsscripts/check-min-release-age.mjsscripts/copy-flags.mjsscripts/verify-content.tssrc/app/(mobile-ui)/add-money/__tests__/add-money-states.test.tsxsrc/app/(mobile-ui)/card/add-to-wallet/page.tsxsrc/app/(mobile-ui)/card/limit/page.tsxsrc/app/(mobile-ui)/card/page.tsxsrc/app/(mobile-ui)/card/physical/page.tsxsrc/app/(mobile-ui)/card/pin/page.tsxsrc/app/(mobile-ui)/dev/card-session-approve/page.tsxsrc/app/(mobile-ui)/dev/components/page.tsxsrc/app/(mobile-ui)/dev/debug/page.tsxsrc/app/(mobile-ui)/dev/page.tsxsrc/app/(mobile-ui)/home/page.tsxsrc/app/(mobile-ui)/layout.tsxsrc/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsxsrc/app/(mobile-ui)/qr-pay/page.tsxsrc/app/(mobile-ui)/refund/page.tsxsrc/app/(mobile-ui)/withdraw/[country]/bank/page.tsxsrc/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsxsrc/app/(mobile-ui)/withdraw/crypto/page.tsxsrc/app/(mobile-ui)/withdraw/manteca/page.tsxsrc/app/(mobile-ui)/withdraw/page.tsxsrc/app/ClientProviders.tsxsrc/app/[...recipient]/payment-layout-wrapper.tsxsrc/app/[locale]/(marketing)/stories/page.tsxsrc/app/actions/card.tssrc/app/actions/claimLinks.tssrc/app/actions/clients.tssrc/app/actions/onramp-quote.tssrc/app/actions/squid.tssrc/app/actions/supported-chains.tssrc/app/actions/users.tssrc/app/api/exchange-rate/route.tssrc/app/api/health/route.tssrc/app/api/health/squid/route.tssrc/app/api/peanut/user/get-user-from-cookie/__tests__/route.test.tssrc/app/api/peanut/user/get-user-from-cookie/route.tssrc/app/lp/card/CardLandingPage.tsxsrc/app/quests/constants.tssrc/app/quests/utils/styling.tssrc/assets/animationssrc/assets/cards/index.tssrc/components/0_Bruddle/CloudsBackground.tsxsrc/components/AddMoney/components/AddMoneyBankDetails.tsxsrc/components/AddMoney/components/DepositMethodList.tsxsrc/components/AddMoney/components/MantecaDepositShareDetails.tsxsrc/components/AddMoney/consts/index.tssrc/components/Badges/badge.utils.tssrc/components/Blog/index.tsxsrc/components/Card/AddCardEntryScreen.tsxsrc/components/Card/AddToWalletCarousel.tsxsrc/components/Card/ApplicationStatusScreen.tsxsrc/components/Card/CancelCardModal.tsxsrc/components/Card/CardFace.tsxsrc/components/Card/CardInfoScreen.tsxsrc/components/Card/CardLimitEditModal.tsxsrc/components/Card/CardLimitsScreen.tsxsrc/components/Card/CardPinScreen.tsxsrc/components/Card/CardPinSetupFlow.tsxsrc/components/Card/CardPioneerFlow.tsxsrc/components/Card/CardSuccessScreen.tsxsrc/components/Card/CardTermsScreen.tsxsrc/components/Card/LockCardModal.tsxsrc/components/Card/PhysicalCardScreen.tsxsrc/components/Card/PinInput.tsxsrc/components/Card/SlideToAction.tsxsrc/components/Card/YourCardScreen.tsxsrc/components/Card/__tests__/cardExpiry.utils.test.tssrc/components/Card/__tests__/cardState.utils.test.tssrc/components/Card/__tests__/pin.utils.test.tssrc/components/Card/cardExpiry.utils.tssrc/components/Card/cardState.utils.tssrc/components/Card/pin.utils.tssrc/components/Claim/Claim.consts.tssrc/components/Claim/Claim.interfaces.tssrc/components/Claim/Claim.tsxsrc/components/Claim/Claim.utils.tssrc/components/Claim/Generic/NotFound.view.tsxsrc/components/Claim/Generic/WrongPassword.view.tsxsrc/components/Claim/Generic/index.tssrc/components/Claim/Link/FlowManager.tsxsrc/components/Claim/Link/Initial.view.tsxsrc/components/Claim/Link/Onchain/Confirm.view.tsxsrc/components/Claim/Link/views/BankFlowManager.view.tsxsrc/components/Claim/__tests__/claim-states.test.tsxsrc/components/Claim/useClaimLink.tsxsrc/components/Common/CountryList.tsxsrc/components/Common/SavedAccountsView.tsxsrc/components/Create/Create.utils.tssrc/components/Create/useCreateLink.tsxsrc/components/CrispChat.tsxsrc/components/Global/Banner/index.tsxsrc/components/Global/Contributors/index.tsxsrc/components/Global/DirectSendQR/index.tsxsrc/components/Global/Drawer/index.tsxsrc/components/Global/ExchangeRateWidget/index.tsxsrc/components/Global/Footer/consts.tssrc/components/Global/GeneralRecipientInput/index.tsxsrc/components/Global/Icons/Icon.tsxsrc/components/Global/Icons/bulb.tsxsrc/components/Global/Icons/docs.tsxsrc/components/Global/Icons/double-check.tsxsrc/components/Global/Icons/invite-heart.tsxsrc/components/Global/Icons/peanut-support.tsxsrc/components/Global/Icons/txn-off.tsxsrc/components/Global/Icons/upload-cloud.tsxsrc/components/Global/Icons/wallet-cancel.tsxsrc/components/Global/IframeWrapper/index.tsxsrc/components/Global/Layout/index.tsxsrc/components/Global/PeanutActionDetailsCard/index.tsxsrc/components/Global/PeanutFactsLoading/index.tsxsrc/components/Global/QRCodeWrapper/index.tsxsrc/components/Global/ScreenOrientationLocker.tsxsrc/components/Global/Testimonials/index.tsxsrc/components/Global/TokenSelector/Components/TokenListItem.tsxsrc/components/Global/TokenSelector/TokenSelector.consts.tssrc/components/Global/TokenSelector/TokenSelector.tsxsrc/components/Global/USBankAccountInput/index.tsxsrc/components/Home/ActivationCTAs.tsxsrc/components/Home/AddMoneyPromptModal/index.tsxsrc/components/Home/FloatingReferralButton/index.tsxsrc/components/Home/HomeHistory.tsxsrc/components/Home/HomeLink.tsxsrc/components/Home/PerkClaimModal.tsxsrc/components/Home/ReferralCampaignModal/index.tsxsrc/components/Invites/JoinWaitlistPage.tsxsrc/components/Kyc/CountryFlagAndName.tsxsrc/components/Kyc/KycFlow.tsxsrc/components/Kyc/SumsubKycWrapper.tsxsrc/components/LandingPage/CurrencySelect.tsxsrc/components/Marketing/FAQSection.tsxsrc/components/Marketing/MarketingNav.tsxsrc/components/Marketing/RelatedPages.tsxsrc/components/Marketing/Section.tsxsrc/components/Marketing/index.tssrc/components/Notifications/NotificationNavigation.tsxsrc/components/Offramp/Offramp.consts.tssrc/components/Points/PerkClaimCard.tsxsrc/components/Profile/AvatarWithBadge.tsxsrc/components/Profile/components/CountryListSection.tsxsrc/components/Profile/index.tsxsrc/components/Refund/index.tsxsrc/components/Request/__tests__/request-states.test.tsxsrc/components/Request/link/views/Create.request.link.view.tsxsrc/components/Send/__tests__/send-states.test.tsxsrc/components/Send/link/LinkSendFlowManager.tsxsrc/components/Send/link/views/Initial.link.send.view.tsxsrc/components/Setup/Views/InstallPWA.tsxsrc/components/Setup/Views/JoinBeta.tsxsrc/components/Setup/Views/JoinWaitlist.tsxsrc/components/Setup/Views/Welcome.tsxsrc/components/Setup/Views/index.tssrc/components/Setup/context/SetupFlowContext.tsxsrc/components/TransactionDetails/TransactionAvatarBadge.tsxsrc/components/TransactionDetails/TransactionCard.tsxsrc/components/TransactionDetails/TransactionDetailsHeaderCard.tsxsrc/components/TransactionDetails/TransactionDetailsReceipt.tsxsrc/components/TransactionDetails/transactionTransformer.tssrc/components/User/UserCard.tsxsrc/components/Withdraw/views/Confirm.withdraw.view.tsxsrc/components/Withdraw/views/Initial.withdraw.view.tsxsrc/components/index.tssrc/components/utils/utils.tssrc/config/index.tssrc/config/peanut.config.tsxsrc/config/theme.config.tsxsrc/config/underMaintenance.config.tssrc/config/wagmi.config.tsxsrc/constants/actionlist.consts.tssrc/constants/analytics.consts.tssrc/constants/cache.consts.tssrc/constants/chain-details.jsonsrc/constants/countryCurrencyMapping.tssrc/constants/general.consts.tssrc/constants/harness.consts.tssrc/constants/points.consts.tssrc/constants/query.consts.tssrc/constants/rain.consts.tssrc/constants/rhino.consts.tssrc/constants/routes.tssrc/constants/token-details.jsonsrc/constants/tokens.consts.tssrc/constants/tooltips.tssrc/constants/zerodev.consts.tssrc/contentsrc/context/HarnessBootstrap.tsxsrc/context/HarnessReplay.tsxsrc/context/LinkSendFlowContext.tsxsrc/context/PeanutDebug.tsxsrc/context/ReproduceBootstrap.tsxsrc/context/RequestFulfillmentFlowContext.tsxsrc/context/WithdrawFlowContext.tsxsrc/context/authContext.tsxsrc/context/kernelClient.context.tsxsrc/context/tokenSelector.context.tsxsrc/data/seo/corridors.tssrc/data/seo/index.tssrc/data/team.tssrc/features/limits/consts.tssrc/features/limits/hooks/useLimitsValidation.tssrc/features/limits/utils.tssrc/features/payments/flows/contribute-pot/components/ContributorsDrawer.tsxsrc/features/payments/flows/contribute-pot/components/RequestPotActionList.tsxsrc/features/payments/flows/contribute-pot/useContributePotFlow.tssrc/features/payments/flows/direct-send/useDirectSendFlow.tssrc/features/payments/flows/semantic-request/SemanticRequestFlowContext.tsxsrc/features/payments/flows/semantic-request/useSemanticRequestFlow.tssrc/features/payments/flows/semantic-request/views/SemanticRequestConfirmView.tsxsrc/features/payments/flows/semantic-request/views/SemanticRequestInputView.tsxsrc/features/payments/shared/components/PaymentMethodActionList.tsxsrc/features/payments/shared/components/PaymentSuccessView.tsxsrc/features/payments/shared/hooks/useChargeManager.tssrc/features/payments/shared/hooks/useCrossChainTransfer.tssrc/features/payments/shared/hooks/usePaymentRecorder.tssrc/features/payments/shared/hooks/useRouteCalculation.tssrc/hooks/__tests__/useCardReveal.test.tssrc/hooks/query/user.tssrc/hooks/useAccountSetup.tssrc/hooks/useAccountSetupRedirect.tssrc/hooks/useActivationStatus.tssrc/hooks/useAvatar.tsxsrc/hooks/useCardReveal.tssrc/hooks/useCrispEmbedUrl.tssrc/hooks/useIdentityVerification.tsxsrc/hooks/useOnrampQuote.tssrc/hooks/useRainCardOverview.tssrc/hooks/useSupportedChainsAndTokens.tssrc/hooks/useTokenChainIcons.tssrc/hooks/useTokenPrice.tssrc/hooks/useWalletBalances.tssrc/hooks/useWalletPlatform.tssrc/hooks/useWalletType.tsxsrc/hooks/useWebSocket.tssrc/hooks/useZeroDev.tssrc/hooks/wallet/__tests__/useSendMoney.test.tsxsrc/hooks/wallet/__tests__/useSpendBundle.test.tssrc/hooks/wallet/useBalance.ts
💤 Files with no reviewable changes (45)
- src/components/Claim/Generic/NotFound.view.tsx
- src/components/Claim/Generic/WrongPassword.view.tsx
- src/config/index.ts
- src/app/(mobile-ui)/refund/page.tsx
- src/components/Setup/Views/index.ts
- src/components/Marketing/Section.tsx
- src/components/Notifications/NotificationNavigation.tsx
- src/components/Global/Footer/consts.ts
- src/components/Claim/Generic/index.ts
- src/components/Marketing/index.ts
- src/components/Home/AddMoneyPromptModal/index.tsx
- src/components/Global/Banner/index.tsx
- src/config/theme.config.tsx
- src/components/Claim/Claim.interfaces.ts
- src/components/Kyc/KycFlow.tsx
- src/components/Badges/badge.utils.ts
- src/components/Blog/index.tsx
- src/components/Marketing/MarketingNav.tsx
- src/components/Global/PeanutFactsLoading/index.tsx
- src/app/quests/constants.ts
- src/components/Global/Drawer/index.tsx
- src/components/Setup/Views/JoinBeta.tsx
- src/app/quests/utils/styling.ts
- src/components/Claim/Claim.utils.ts
- src/components/Home/HomeLink.tsx
- src/components/Setup/Views/Welcome.tsx
- src/components/Home/FloatingReferralButton/index.tsx
- src/components/Global/Icons/upload-cloud.tsx
- src/components/Global/Testimonials/index.tsx
- src/config/peanut.config.tsx
- src/components/CrispChat.tsx
- src/components/Profile/components/CountryListSection.tsx
- src/app/actions/squid.ts
- src/components/Global/Contributors/index.tsx
- src/components/Marketing/FAQSection.tsx
- src/components/Home/ReferralCampaignModal/index.tsx
- src/components/Marketing/RelatedPages.tsx
- src/components/index.ts
- src/app/api/health/squid/route.ts
- src/components/Refund/index.tsx
- src/components/Points/PerkClaimCard.tsx
- src/components/Global/USBankAccountInput/index.tsx
- src/components/Create/Create.utils.ts
- src/components/Setup/context/SetupFlowContext.tsx
- src/components/utils/utils.ts
Earlier fix only cleared the cookie on 401 (expired/invalid signature), but the same recovery story applies to 404 — typically when the local DB gets re-seeded out from under a stale cookie. Without clearing, the client redirect to /setup re-fires the same dead cookie on every navigation and never escapes.
The balance gate is `!!address && !!userAddress && address === userAddress`.
When any of these is false, useBalance is disabled and the FE silently
falls through to '$0.00' even when chain has real funds. Today's card
playtest spent an hour chasing this — the only symptom is "balance is 0",
no log, no error.
Add a dev-only console.warn that fires once when the gate blocks AND the
user has finished loading, naming the exact failing condition. One of:
- kernel address undefined (useZeroDev not ready / init failed)
- no PEANUT_WALLET account in user.accounts (BE response shape bug)
- address mismatch: kernel=0x… db=0x… (drift between webAuthnKey-derived
SA and the DB row, e.g. ZeroDev project id changed across sessions)
Diagnostic-only — no behavior change. Pairs with TODO #57b (wire-shape
contract) and the broader card-playtest hardening.
…d full-setup copy Pairs with the api-ts cheat additions (grant-card-access, fund-rain-collateral routes; full-setup chain expanded). New Card section in /dev/debug exposes the three card-test buttons and updates the full-setup description to mention the real-vs-synthetic on-chain steps so testers know what they're triggering.
1. qr-pay receipt undefined-vs-null. sendMoney's collateral-only path returns receipt: undefined (not null). Loose `receipt !== null` would fail to catch undefined and pass it into isTxReverted, which expects a TransactionReceipt. Widen the type to include undefined and use `!= null` (nullish-safe) so both forms short-circuit. 2. package.json engines.node: drop ^18.18.0 — Next.js 16 requires Node 20.9+, no point listing 18 as supported. 3. package.json jest config: drop the @squirrel-labs/peanut-sdk Jest alias + transformIgnorePatterns entry. The SDK was uninstalled on this branch; the alias was tracking dead code and would mask any accidental real-import regression. Mock file at src/utils/__mocks__/peanut-sdk.ts deleted.
55 files reformatted by `pnpm prettier --write`. CI's prettier check was failing on whitespace/quote-style drift across the e2e/ tree + recent decomplexify edits. No semantic changes. Closes the prettier-format gate on PR #1904.
tsconfig.json includes .next/types/**/*.ts. next-env.d.ts triple-slash references next/image-types/global (declares .svg/.gif/.webp module shapes) and imports .next/dev/types/routes.d.ts. Both come from a Next.js build/dev/typegen step. CI was failing typecheck on every static-asset import (~200 TS2307 errors) because no Next command had populated .next/types/. Adding `pnpm next typegen` is the cheap fix — generates types without a full build (~10s vs 90s). Verified locally: rm -rf .next, pnpm next typegen, pnpm typecheck → clean.
… 14-day floor Three deps were ≤3-8 days old, blocked by check-min-release-age: - lucide-react 1.9.0 (3d) → 1.8.0 (18d) - circle-flags 2.8.3 (8d) → 2.8.2 (99d) - posthog-node 5.30.1 (3d) → 5.29.2 (18d) All within the same minor series — no behavioural changes the codebase relies on. Bump back when each ages past the floor.
Users with an ACTIVE Rain card who never granted the one-tap session key were silently locked out of auto-balance: the rebalance loop filters out cards without serializedApproval, so every deposit was a no-op and the only path to grant was a card spend that touched collateral. A user who only deposits — or whose first spend doesn't need collateral — would never see the prompt. Adds EnableAutoBalanceBanner: a non-dismissible ActionModal on /home that shows up when the card is ACTIVE, has no withdraw approval, and Rain has provisioned the contracts (so grant() can actually succeed). preventClose + hideModalCloseButton guarantee the user has to act. On grant the overview refetch flips hasWithdrawApproval and the modal hides itself naturally. Independent of the activation funnel — an unactivated user can still hold an ACTIVE card and benefit from this surface.
Runs the painscore + complexity + dup + churn analyzer (lives in mono/engineering/code-analysis) on every PR. Posts an idempotent diff comment with new findings, resolved findings, and per-file painscore Δ. Auth: SUBMODULE_TOKEN (already used for content-sync) clones mono. Ref: vars.MONO_REF (default 'decomplexify' until the analyzer lands on mono main, then flip to 'main'). Regression gate is advisory (continue-on-error) for the first 2 weeks. Tighten by removing continue-on-error after the team is acclimatised.
Code-analysis diffPainscore total: 5668.14 → 5402.79 (-265.35) 🆕 New findings (663)
…and 643 more. ✅ Resolved (639)
…and 619 more. 📈 Painscore deltas (top movers)
|
- concurrency.group keyed on (workflow, PR number || ref) cancels stale in-flight runs when a newer commit lands. Big cost lever on rapid-iteration PRs (~80% of stale-run minutes recovered). - workflow_dispatch added to tests, code-analysis, supply-chain, preview — manual rerun from Actions tab or `gh workflow run` - paths-ignore on code-analysis only (not on tests — would risk blocking required status checks under branch protection) Tests (push: branches: ['**']) is the most-stale workflow today; this is where the savings will show up first.
TL;DR
The
decomplexifygiga-PR — frontend half. 108 commits, 357 files, +22,338 / −8,072 LoC vsdev. Backend half: peanutprotocol/peanut-api-ts#665. Mono docs+coordination: peanutprotocol/mono#12. Three should land together.Scope-wise: full eviction of
@squirrel-labs/peanut-sdk,@reown/appkit,@mui/*,@dicebear/*,@wagmi/core,jsqr,marked. MUI → Lucide icon migration. Squid cross-chain picker → Rhino SDA preview. Route rename/claim-v3→/claim(cross-repo with the api-ts PR). Card UI merge (viafeat/card-ui) + Native (Capacitor) merge (viaorigin/dev). Debug menu rehaul. Cross-cutting security/dep CVE cleanup (88 Dependabot warnings cleared).What's in here
1. Dependency eviction — net −8 packages
Removed from
package.json:@squirrel-labs/peanut-sdk— fully evicted (0c9c0f46adecomplexify(stage 1b)). Claim/deposit/link logic inlined as viem fragments insrc/utils/peanut-link.utils.ts+src/utils/peanut-claim.utils.ts+src/interfaces/peanut-sdk-types.ts.grep -r '@squirrel-labs' src/returns 0.@reown/appkit+@reown/appkit-adapter-wagmi— wallet-connect UI removed (6e83f4b85TASK-19015). E2e regression specs added to lock the absence (32cbf7dac).@mui/material+@mui/icons-material+@emotion/react+@emotion/styled— full MUI eviction in8158a69c2(TASK-19275). All icon imports rewritten to Lucide-react.@dicebear/core+@dicebear/collection— avatar generation deleted (no consumers after the eviction sweep).@safe-global/safe-apps-sdk— Safe-app embed flow removed; ~zero traffic.@wagmi/core— dropped (b88399e2f).jsqr,marked— small consolidations.Added:
lucide-react— icon library (~10KB tree-shakeable).circle-flags— country flags as SVG (67ff7348amigration from flagcdn.com CDN).@zerodev/permissions+@zerodev/webauthn-key+@zerodev/ecdsa-validator— kernel SA + WebAuthn signer infra (matches the api-ts admin kernel).playwright— e2e test infra.Supply chain: matches the api-ts PR's hardening —
minimum-release-age=20160(14 days) in.npmrc,scripts/check-min-release-age.mjsCI gate. 88 GitHub Dependabot warnings (4 critical, 30 high, 43 moderate, 11 low) addressed via lockfile bumps +pnpm.overrides(b09ccb6bdallowlist protobufjs 7.5.5,49ac7e925transitive crit/high CVEs,69d665fcaNext.js DoS via Server Components GHSA,b09ccb6bdprotobufjs RCE).2. Icon migration — MUI → Lucide
8158a69c2decomplexify (TASK-19275). ~200 component-level imports rewritten across the entire UI to uselucide-react.src/components/Global/Icons/Icon.tsxis the canonical entry point — every consumer reads from a singleIconNameunion type so renames at the SVG layer can't drift.Trickier corners stabilized in follow-up commits:
f0b751292,5da74ae1e,c6805e5ab)..btn svg { fill: inherit }Tailwind rule (pre-existing, written for MUI's filled icons) collapsed Lucide's stroke-based open-curve paths into solid blobs. Workaround: inlinestyle={{ fill: ... }}inLucideWrapper. Rule deletion + audit-of-non-<Icon>-SVGs-inside-.btn-callsites tracked as TODO added wagmi-->ethers signer conversion for sdk compatibility #1 inengineering/projects/decomplexify/TODO.md..btn svg { @apply icon-18 }global rule bypassed by default (dd419ccca); gated on:not(.custom-size)(a7e14b5b0) so Button can size its own icon wrapper (4ff3f8351,e843c5d8d).star,trophy,shield,check-circle,gift). Pulled the MUI path inline as custom SVG components.IconNamemembers tracked as TODO feat: bigger peanutman and goerli/optigoerli balance #5.3. Cross-chain claim — Squid → Rhino SDA
Squid's token/route picker fully removed (
b3f254752rip out,90435394cfull name purge,dce598718final squidQuoteId payload + sdkErrorHandler rename):src/app/actions/squid.tsdeleted.src/app/api/health/squid/deleted.useSquidChainsAndTokens,ISquidChain,ISquidToken,SQUID_API_BASE_URLpurged across contexts (LinkSendFlowContext,tokenSelector.context,WithdrawFlowContext,SemanticRequestFlowContext), components (WithdrawConfirm/Initial,LinkSendFlowManager, TokenSelector glue), hooks (useTokenPrice,useTokenChainIcons), validators, services.9d4941e16Rhino SDA frontend wiring): instead of a Squid quote, the user sees a Rhino SDA preview (deposit address, expected amount in/out, slippage). On commit, the relayer calls/claimwith the SDA address as recipient; Rhino does the bridge.squidQuoteIdfield stripped from the charge-create payload — backend already dropped it from the Charge model.4. Route rename —
/claim-v3→/claim9ca70965d—useClaimLink.tsx,services/sendLinks.ts,useCrossChainTransfer.tsPOST target swap. Same payload shape, same response. Lands together with the api-ts route rename in #665.5. Debug menu —
/dev/debug(rehauled from/dev/cheats)d29edca58— comprehensive grouped panel:reset-userwith confirm.Every action fires a pink-banner
console.login DevTools with latency.window.debug.*/window.cheats.*(alias) for ad-hoc commands. Replaces ~7 sequential cheat clicks with one. Localhost-gated via DevLayout;NEXT_PUBLIC_TEST_HARNESS_SECRETrequired.6. Card UI merge — Rain V2 card flow
d5dc8076bdecomplexify: merge feat/card-ui — Rain card UI on top of native+serverFetch. Card setup, balance display, transactions list, KYC integration. Nominally tested in sandbox perengineering/projects/card-merge-coordination.UI specifics:
cardApi.getInfo()with 30s staleTime.7. Native (Capacitor) merge
Pulled in via
fd92ad248merge oforigin/dev(native-app + card-pioneers + bridge fee UI). iOS + Android shells via Capacitor —capacitor.config.tsat root, native plugins for camera (QR scanning), keychain (passkey + JWT storage), and push notifications.8. Country flags — flagcdn.com CDN →
circle-flags67ff7348amigrated fromflagcdn.comto thecircle-flagsnpm package, plus uses country flag in bank-tx avatars (422a0f5b1fix bank icon black-on-black in cashout card-context).23e0c7c6etightened the migration code after review.Trade-off: shipped flags add ~80KB to the bundle but eliminate a 3rd-party CDN dependency (no DNS hop, no rate-limit risk, no CDN outage).
9. WS history-cache invalidation
72b6d324c— after the api-ts ledger collapse, every history-affecting event flows through the same WS-broadcast pipeline. We can flush the tanstackTRANSACTIONScache in one place instead of per-flow.src/components/Home/HomeHistory.tsx— in the WS history-event handler, callsqueryClient.invalidateQueries({ queryKey: [TRANSACTIONS] })on every broadcast (alongside the existingfetchBalancefor balance-affecting ones). Activity widget refetches immediately instead of waiting up to the 30s staleTime — visible across all intent kinds.src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx— same invalidation immediately after the Bridge confirm step succeeds, before flipping to the SUCCESS view, so the user sees the pending OFFRAMP entry without waiting for the next WS push or staleTime tick.Trade-off: more refetches during burst-y WS traffic. Acceptable —
TRANSACTIONSquery is small + indexed FE-side; tanstack dedupes in-flight calls so concurrent invalidations collapse.10. Dead-code purges
3664b38e2decomplexify: drop dead Account fields + Points-V1 ghost types:src/interfaces/account.ts. Compile fails if any consumer is still importing them — none did.TransactionPoints, the V1 transaction-point shape, and adjacent enums removed. The points UI uses the V2 shape exclusively now.Plus the Refund feature deleted entirely (peanut-ui
3c577555a, prior commit),switchNetworkhelper removed fromutils/general.utils.ts, 6 unused helpers dropped fromuseCreateLink.tsx.11. FE hardening for ledger-collapse playtest
240f8d8aedecomplexify: harden FE for ledger-collapse playtest.16437b365decomplexify: associateClaim PATCH sends body={} (B5 from playtest). Surgical fixes surfaced during the 2026-04-27 sandbox playtest while validating the api-ts ledger collapse.12. Security hardening
4b0aef2c4verify-content: write baseline through fd (CodeQL js/file-system-race).ee070838elog-injection: validate inputs / strip CRLF before logging.a0d8301e9stories: encode slug in href (CodeQL js/stored-xss).e53642efeci: harden workflows (CodeQL: pin tag + minimal permissions).01729edabfix: trapped 'Having trouble' modal in Sumsub + iframe KYC wrappers (UX-flavored security fix — was leaving users stuck).13. Testing infra
test:e2e— mobile (default; iPhone 14 viewport, 375x667). Per CLAUDE.md mobile-first directive.test:e2e:desktop— desktop opt-in.test:e2e:all— both.test:e2e:regression— runs against a live UI URL (used by Nutcracker dashboard).32cbf7dace2e: add regression specs for icon rendering + AppKit / wagmi removal (locks the absence of evicted deps).10b0e465ctest: ignore.claude/worktreesin jest scope.14. Misc fixes
e5642edabfix(withdraw): prefer real tx hash over userOp hash for confirmOfframp (CR-flagged from previous review pass).0905edc65analytics: instrument waitlist top-of-funnel.cf32aa2e8docs: point local-dev instructions atmono/scripts/dev.Risks
Cross-repo coordination
claim-v3→claimlands in peanutprotocol/peanut-api-ts#665 — both must merge together or claim 404s.Squid surface
Cache invalidation traffic
invalidateQuerieson every history-broadcast event. For active users during a burst, this can mean 5–10 invalidations/sec. Tanstack dedupes in-flight queries so the network shouldn't spike, but if we see issues we can debounce or scope by intent kind.Native (Capacitor) — UNTESTED BUILD
Card flow — partial sandbox coverage
Card setup + balance + spend nominally tested in sandbox per
engineering/projects/card-merge-coordination. Production data not exercised. Real Visa interchange + Rain V2 settlement + KYC geo edge cases need staging soak.Icon migration regressions
200+ icon swaps. Stroke-based Lucide icons can read poorly at small sizes vs MUI's filled originals. TODO #2 tracks the visual QA pass on critical flows (
/setup,/home,/send,/claim/*,/withdraw/*,/dev/ds/foundations/icons).Test plan
pnpm typecheck— clean (BLOCKING gate per CLAUDE.md)npm test(unit) — greenpnpm prettier --check .— cleanpnpm build— verify bundle size delta vsdevnpm run test:e2e— mobile Playwright suitenpm run test:e2e:desktop— desktop opt-in/dev/debug → Full setup):e2e-send-link-create-claimcovers this)qr3)pnpm cap build && pnpm cap run ios— Kushagra to verifyWhy one giga-PR
Same reasoning as the api-ts PR. Tried small/gradual in past sprints, kept yielding partial states where reviewers couldn't see the full picture and reverts left ambiguous half-states. One bundled cutover means one set of reviewers, one staging soak window, one rollback plan, no overlapping-feature-flag soup.
Trade-off: review burden is higher. Mitigated by:
git log --oneline | grep <theme>works)