Skip to content

feat(card): repair enable-bricked kernels inline in the grant preflight#2414

Merged
Hugo0 merged 2 commits into
devfrom
feat/grant-preflight-nonce-repair
Jul 14, 2026
Merged

feat(card): repair enable-bricked kernels inline in the grant preflight#2414
Hugo0 merged 2 commits into
devfrom
feat/grant-preflight-nonce-repair

Conversation

@jjramirezn

@jjramirezn jjramirezn commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to peanut-ui#2412 / peanut-api-ts#1179. An on-chain sweep of the 2025-09-18 root-validator migration wave found 91 kernel accounts with validNonceFrom > currentNonce — a revocation floor above the counter that makes every enable-mode validation revert InvalidNonce forever (Kernel.sol's floor check; root-passkey ops are exempt, so the wallets look healthy). 3 of them already surfaced as card auto-balance victims; the rest are latent and would hit this the day they activate a card. A smaller cohort of undeployed pre-cutoff accounts mints approvals whose baked v0.0.3 initCode CREATE2-derives the wrong address (AA14 initCode must return sender).

This folds the repair into useGrantSessionKey so affected users never see a failure:

  • The grant preflight now reads validNonceFrom alongside the existing getCode/currentNonce/accountMetadata reads (same parallel batch — no extra latency for healthy accounts).
  • Floored account → root userOp invalidateNonce(floor + 1) (kernel syncs the counter up), then the grant binds the fresh nonce. Undeployed migration-wrapper account → the migration no-op deploys it (the SDK wrapper swaps the root validator in the same op), then the grant proceeds. Either way it's one extra passkey tap during card setup instead of a support ticket + /fix-card-signature round-trip.
  • Repair confirmation is by re-read chain state, never the bundle receipt (a reverted userOp still yields a successful bundle) — same rationale as ensureRootValidatorMigrated. A retry after a confirm timeout re-diagnoses live state first, so it can never re-send an already-consumed invalidation.
  • A floor beyond Kernel v3.1's invalidation cap (> currentNonce + 10; no valid target exists) fails loudly with contact-support copy instead of sending a deterministically-reverting op. No known account is in that state.
  • New analytics: card_session_key_preflight_repair (mode: invalidate | deploy) to watch the latent cohort drain.

Privacy note: detection is purely on-chain state of the connected wallet — no account list, no addresses, nothing user-identifying ships in this open-source repo.

Risks / breaking changes

  • Healthy accounts: one additional parallel eth_call in the grant preflight; behavior otherwise unchanged (all existing nonce-binding tests pass unmodified).
  • Affected accounts get a second passkey prompt inside grant() — consumers (EnableAutoBalanceBanner, ReEnableModal, spendPreflight) already treat grant() as an opaque async step with a spinner, so no consumer changes needed.
  • If the floor read fails on a deployed account the grant now fails loudly (previously it would sign anyway) — deliberate: skipping the check can mint a dead-on-arrival approval.

QA

  • kernelNonceRepair.utils unit tests: repair-call selection per mode, retry-skips-consumed-invalidation, cap guard, confirm-by-state incl. never-confirms (7 tests).
  • useGrantSessionKey suites: all existing nonce-binding/validator-binding tests pass unchanged; 2 new tests cover floored → repair → bind-fresh-nonce and undeployed-wrapper → deploy-first (15 total across the three suites).
  • Typecheck + full jest green (1 pre-existing add-money failure, exists on dev/main).

Design notes / accepted trade-offs (from review passes)

  • Deploy path reuses ensureRootValidatorMigrated (review finding): a hand-rolled "is it deployed" confirm is satisfiable by a reverted migration inside a successful bundle (account lands on v0.0.2, approval silently dead) — the gate verifies the root swap against on-chain ground truth and rebuilds the client.
  • Flaky validNonceFrom read does not block healthy grants: falls back to pre-floor-check behavior + card_session_key_preflight_repair{mode:floor-read-failed} analytics. A floored account slipping through is still caught by the sweep's permanent-failure path and /fix-card-signature.
  • Accepted: repair userOps route through handleSendUserOpEncoded, which force-logs-out on the stale-webAuthnKey failure. Identical exposure already exists in prod for the migration no-op in runCollateralSpendPreflight; the logout is deliberate account-safety behavior, not new risk surface.
  • Accepted: the extra passkey tap has no bespoke framing overlay (spendPreflight wraps its migration tap in setSecurityOverlay). Wiring the overlay through grant consumers is scope creep for a one-time tap affecting a bounded legacy cohort — follow-up if support signals confusion.
  • Accepted: undeployed pre-cutoff grants gain a bundler round-trip + confirm window. The old "instant" path minted AA14-dead approvals for exactly these accounts — instant-but-broken isn't a baseline worth preserving.

A subset of accounts from the 2025-09-18 root-validator migration wave carry
validNonceFrom > currentNonce, so every card session-key enable they ever
grant reverts InvalidNonce on backend replay — dead on arrival, invisible to
the user until the card silently never funds. A separate cohort of undeployed
pre-cutoff accounts mints approvals whose initCode derives the wrong address
(AA14). Both states are detectable from pure on-chain reads of the connected
wallet (nothing account-specific ships in this repo).

The grant now reads validNonceFrom alongside currentNonce and, when repair is
needed, sends the fixing root userOp first (invalidateNonce(floor+1), or the
migration no-op deploy) — one extra passkey tap during card setup instead of
a support ticket. Confirmation is by re-read chain state, never the bundle
receipt, mirroring ensureRootValidatorMigrated.
@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Jul 14, 2026 2:09pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8510f07e-ec69-4b2c-a45b-4e8de0c2b054

📥 Commits

Reviewing files that changed from the base of the PR and between a91e1a7 and 3f5b96b.

📒 Files selected for processing (6)
  • src/constants/analytics.consts.ts
  • src/hooks/wallet/__tests__/useGrantSessionKey.test.ts
  • src/hooks/wallet/__tests__/useGrantSessionKey.test.tsx
  • src/hooks/wallet/useGrantSessionKey.ts
  • src/utils/__tests__/kernelNonceRepair.utils.test.ts
  • src/utils/kernelNonceRepair.utils.ts
 ____________________________________________________________________________________________________________________________________________
< Organize teams around functionality. Don't separate designers from coders, testers from data modelers. Build teams the way you build code. >
 --------------------------------------------------------------------------------------------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/grant-preflight-nonce-repair

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

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6179.6 → 6186.26 (+6.66)
Findings: +1 net (+8 new, -7 resolved)

🆕 New findings (8)

  • high complexity — src/hooks/wallet/useGrantSessionKey.ts — CC 35, MI 52.89, SLOC 293
  • high hotspot — src/constants/analytics.consts.ts — 32 commits, +308/-7 lines since 6 months ago
  • high complexity — src/constants/analytics.consts.ts — CC 1, MI 33.38, SLOC 167
  • medium high-mdd — src/hooks/wallet/useGrantSessionKey.ts:100 — useGrantSessionKey: MDD 49.4 (uses across many lines from declarations)
  • medium high-dlt — src/hooks/wallet/useGrantSessionKey.ts:100 — useGrantSessionKey: DLT 48 (calls 48 distinct functions — high context load)
  • medium high-mdd — src/hooks/wallet/useGrantSessionKey.ts:113 — : MDD 40.9 (uses across many lines from declarations)
  • medium high-dlt — src/hooks/wallet/useGrantSessionKey.ts:113 — : DLT 30 (calls 30 distinct functions — high context load)
  • medium complexity — src/utils/kernelNonceRepair.utils.ts — CC 17, MI 58.72, SLOC 79

✅ Resolved (7)

  • src/constants/analytics.consts.ts — 30 commits, +302/-6 lines since 6 months ago
  • src/constants/analytics.consts.ts — CC 1, MI 33.45, SLOC 166
  • src/hooks/wallet/useGrantSessionKey.ts:97 — useGrantSessionKey: DLT 41 (calls 41 distinct functions — high context load)
  • src/hooks/wallet/useGrantSessionKey.ts:97 — useGrantSessionKey: MDD 38.6 (uses across many lines from declarations)
  • src/hooks/wallet/useGrantSessionKey.ts — CC 26, MI 51.71, SLOC 234
  • src/hooks/wallet/useGrantSessionKey.ts:109 — : MDD 21.7 (uses across many lines from declarations)
  • src/hooks/wallet/useGrantSessionKey.ts:109 — : DLT 24 (calls 24 distinct functions — high context load)

📈 Painscore deltas (top movers)

File Before After Δ
src/utils/kernelNonceRepair.utils.ts 0.0 6.4 +6.4

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1933 ran, 0 failed, 0 skipped, 26.9s

📊 Coverage (unit)

metric %
statements 58.9%
branches 42.6%
functions 47.5%
lines 59.1%
⏱ 10 slowest test cases
time test
2.6s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.0s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.2s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.2s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.2s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › keeps stickers off the username pill (final pass respects the keep-out)
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid 9-digit US account
0.1s src/utils/__tests__/demo-balance.test.ts › resetDemoBalance refills and restarts the TTL window
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ENS name
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ETH address with surrounding spaces
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@jjramirezn jjramirezn marked this pull request as ready for review July 14, 2026 13:46
- deploy path now goes through ensureRootValidatorMigrated: the hand-rolled
  confirm only checked 'deployed', which a reverted migration inside a
  successful bundle satisfies while leaving the account on v0.0.2 — the
  approval signed next would be silently dead (the exact class this PR
  repairs); the gate verifies the swap against on-chain ground truth and
  rebuilds the client
- a flaky validNonceFrom read no longer aborts healthy grants — proceed on
  pre-floor-check behavior and flag it (floored stragglers are still caught
  by the sweep + /fix-card-signature)
- repairEnableNonce: confirm-poll survives flaky reads (tap and gas are
  already spent); a lagging 'undeployed' fresh read is retryable, never
  'contact support'; cap check runs against fresh state only
- preflight-repair analytics captured after success, not before the tap
@jjramirezn

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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

@Hugo0 Hugo0 merged commit a9cb2d9 into dev Jul 14, 2026
18 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants