Skip to content

fix(card): bind session-key enable approval to the verified live currentNonce#2313

Draft
jjramirezn wants to merge 1 commit into
mainfrom
hotfix/card-grant-enable-nonce-binding
Draft

fix(card): bind session-key enable approval to the verified live currentNonce#2313
jjramirezn wants to merge 1 commit into
mainfrom
hotfix/card-grant-enable-nonce-binding

Conversation

@jjramirezn

Copy link
Copy Markdown
Contributor

Summary

A Rain card can silently fail forever: the card always declines, the auto-balance never funds collateral, yet the user looks set up. Root cause is in the session-key grant, not the rebalancer.

The session-key permission installs on-chain via an enable approval the passkey signs at grant time, bound to the account's Kernel currentNonce. serializePermissionAccount reads that nonce via the SDK's getKernelV3Nonce, which silently returns 1 on any read failure. A flaky read at grant time therefore mints an approval frozen to nonce 1. That mismatches only accounts whose live currentNonce ≠ 1 (migrated / sudo-validator-changed accounts): their permission never installs, and every server-side auto-balance userOp reverts AA23 InvalidNonce. The grant still "succeeds", so it's invisible. Confirmed N=1 in prod today (Rain user 67ce33df, currentNonce=2; all 53 working cards are nonce=1).

Fix: read currentNonce explicitly (throw on failure — never guess), build the enable typed data bound to that verified nonce, sign with the same sudo validator, and inject it into serializePermissionAccount.

Risk

Low / cohort-safe. The exported getPluginsEnableTypedData honors the explicit validatorNonce and uses the same action/hook/validator + sudo-signing path, so nonce=1 accounts get a byte-identical approval. The only behavior change: a failed currentNonce read now aborts the grant loudly instead of silently producing a broken approval.

QA

  • New useGrantSessionKey.test.tsx: (1) grant binds the enable approval to the live currentNonce (asserts validatorNonce + injected signature), (2) a failed currentNonce read aborts — no approval produced.
  • Typecheck + prettier clean; full unit suite green (one unrelated pre-existing cross-suite flake in add-money-states, passes in isolation).
  • Pre-merge: verify in Nutcracker sandbox with a currentNonce>1 account that the grant→enable→install→auto-balance path now completes.

Fixes the stuck user

Deploy → she re-grants once → enable binds to her live nonce (2) → permission installs → her existing balance auto-balances → card works.

Note

Replaces/retires peanut-api-ts #1065 (built on an earlier wrong mechanism — would loop on this InvalidNonce case).

…entNonce

The Rain card session-key permission installs on-chain via an 'enable' approval
the passkey signs at grant time, bound to the account's currentNonce. The SDK's
internal getKernelV3Nonce silently returns 1 on a read failure, so a flaky read
mints an approval frozen to nonce 1. That only mismatches accounts whose live
nonce != 1 (migrated / sudo-validator-changed) — their permission never installs
and every auto-balance reverts AA23 InvalidNonce, so the card always declines.
The grant still 'succeeds', so it is invisible. Confirmed N=1 in prod.

Read currentNonce explicitly (throw on failure, never guess), build the enable
typed data bound to it, sign with the same sudo validator, and inject it into
serializePermissionAccount. nonce=1 accounts get a byte-identical approval; the
only behavior change is a failed read now aborts instead of minting a bad one.
@vercel

vercel Bot commented Jun 29, 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 Jun 29, 2026 9:31pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

useGrantSessionKey is updated to read the kernel's live currentNonce via peanutPublicClient.readContract and pass it into getPluginsEnableTypedData, signing the result with sudoValidator before calling serializePermissionAccount with the explicit enableSignature. A regression test suite is added covering nonce-bound approval and RPC failure abort.

Changes

Nonce-bound session key grant

Layer / File(s) Summary
currentNonce read and explicit enable-signature wiring
src/hooks/wallet/useGrantSessionKey.ts
Adds CURRENT_NONCE_ABI, imports getPluginsEnableTypedData, extracts sudoValidator, reads live currentNonce from the session kernel account, generates enable typed data with that nonce, signs it via sudoValidator.signTypedData, and passes the resulting enableSignature into serializePermissionAccount.
Regression tests for nonce binding and RPC failure
src/hooks/wallet/__tests__/useGrantSessionKey.test.tsx
Mocks all external dependencies (analytics, ZeroDev SDK, kernel client, contract reads, rain API) and asserts that the fetched currentNonce is injected into the enable typed-data/signing/serialization flow, and that a readContract rejection aborts the grant without calling typed-data or serialization functions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • peanutprotocol/peanut-ui#2037: Directly modifies the same useGrantSessionKey.ts serialization path, changing which kernel address the enable approval is bound to.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: binding the session-key enable approval to the live currentNonce.
Description check ✅ Passed The description accurately explains the nonce-binding fix, the failure behavior, and the added tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@github-actions

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 5859.06 → 5859.96 (+0.9)
Findings: +1 net (+6 new, -5 resolved)

🆕 New findings (6)

  • medium high-mdd — src/hooks/wallet/useGrantSessionKey.ts:86 — useGrantSessionKey: MDD 35.0 (uses across many lines from declarations)
  • medium high-dlt — src/hooks/wallet/useGrantSessionKey.ts:86 — useGrantSessionKey: DLT 35 (calls 35 distinct functions — high context load)
  • medium complexity — src/hooks/wallet/useGrantSessionKey.ts — CC 21, MI 50.75, SLOC 215
  • low high-mdd — src/hooks/wallet/useGrantSessionKey.ts:98 — : MDD 18.9 (uses across many lines from declarations)
  • low high-dlt — src/hooks/wallet/useGrantSessionKey.ts:98 — : DLT 19 (calls 19 distinct functions — high context load)
  • low any-usage — src/hooks/wallet/useGrantSessionKey.ts — 7 any annotation(s)

✅ Resolved (5)

  • src/hooks/wallet/useGrantSessionKey.ts:79 — useGrantSessionKey: MDD 33.1 (uses across many lines from declarations)
  • src/hooks/wallet/useGrantSessionKey.ts:79 — useGrantSessionKey: DLT 30 (calls 30 distinct functions — high context load)
  • src/hooks/wallet/useGrantSessionKey.ts — CC 21, MI 52.29, SLOC 188
  • src/hooks/wallet/useGrantSessionKey.ts:91 — : MDD 14.7 (uses across many lines from declarations)
  • src/hooks/wallet/useGrantSessionKey.ts — 5 any annotation(s)

📈 Painscore deltas (top movers)

File Before After Δ
src/hooks/wallet/useGrantSessionKey.ts 10.2 11.1 +0.9

@github-actions

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1631 ran, 0 failed, 0 skipped, 23.5s

📊 Coverage (unit)

metric %
statements 55.0%
branches 37.4%
functions 42.6%
lines 54.9%
⏱ 10 slowest test cases
time test
3.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
0.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in updateUserById
0.2s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in updateUserById body
0.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Perk claimed shows shake class + go home button
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid 9-digit US account
0.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Perk claim in progress shows disabled button + progress
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle maximum length (17 digits) US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid US account with spaces
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle minimum length (6 digits) US account
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/hooks/wallet/useGrantSessionKey.ts`:
- Around line 169-206: The use of `any` around `kernelPluginManager` is masking
the exact account shape and triggering lint issues in the session-key grant
flow. Replace the `as any` casts with a small स्थानीय structural type for the
`kernelPluginManager` object used by `useGrantSessionKey`, covering the
referenced `sudoValidator`, `getAction`, and `hook` members so
`createKernelAccount` and `getPluginsEnableTypedData` can use it safely without
suppressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 21e8e53c-a5a7-4d0a-9cda-3c7169cdfdd5

📥 Commits

Reviewing files that changed from the base of the PR and between 6a5307a and 3cbc1e0.

📒 Files selected for processing (2)
  • src/hooks/wallet/__tests__/useGrantSessionKey.test.tsx
  • src/hooks/wallet/useGrantSessionKey.ts

Comment on lines +169 to +206
const sudoValidator = (kernelClient.account as any).kernelPluginManager.sudoValidator
const sessionKernelAccount = await createKernelAccount(peanutPublicClient, {
address: kernelClient.account!.address,
entryPoint: getEntryPoint('0.7'),
kernelVersion: KERNEL_V3_1,
plugins: {
sudo: (kernelClient.account as any).kernelPluginManager.sudoValidator,
sudo: sudoValidator,
regular: permissionPlugin,
},
})

const serialized = await serializePermissionAccount(sessionKernelAccount)
// The session-key permission installs on-chain via an "enable" approval
// the passkey signs here, bound to the account's `currentNonce`. We read
// that nonce EXPLICITLY and let the read throw on failure: the SDK's
// internal `getKernelV3Nonce` silently falls back to `1` on a read error,
// which produces an approval frozen to nonce 1. That only mismatches
// accounts whose live nonce ≠ 1 (e.g. any account migrated / with a
// sudo-validator change), and the grant still "succeeds" — so the card
// then declines forever because the enable reverts `AA23 InvalidNonce`
// on every auto-balance. Binding the enable to the verified live nonce
// (and failing loudly if we can't read it) is the fix.
const validatorNonce = Number(
await peanutPublicClient.readContract({
address: sessionKernelAccount.address,
abi: CURRENT_NONCE_ABI,
functionName: 'currentNonce',
})
)
const pm = (sessionKernelAccount as any).kernelPluginManager
const enableTypedData = await getPluginsEnableTypedData({
accountAddress: sessionKernelAccount.address,
chainId: PEANUT_WALLET_CHAIN.id,
kernelVersion: KERNEL_V3_1,
action: pm.getAction(),
hook: pm.hook,
validator: permissionPlugin,
validatorNonce,
} as any)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target file and related type declarations
git ls-files | rg 'src/hooks/wallet/useGrantSessionKey\.ts|createKernelAccount|getPluginsEnableTypedData|kernelPluginManager|CURRENT_NONCE_ABI'

# Show the relevant section with line numbers
sed -n '1,260p' src/hooks/wallet/useGrantSessionKey.ts

# Search for type/signature definitions
rg -n "createKernelAccount|getPluginsEnableTypedData|kernelPluginManager|CURRENT_NONCE_ABI|currentNonce" src

Repository: peanutprotocol/peanut-ui

Length of output: 18297


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect kernel client/account typing around the relevant access paths
sed -n '1,260p' src/context/kernelClient.context.tsx

# Look for any local type declarations or wrappers around kernel account/client
rg -n "type .*Kernel|interface .*Kernel|useKernelClient|kernelAccount|GenericSmartAccountClient|kernelPluginManager" src/context src/hooks src/utils

Repository: peanutprotocol/peanut-ui

Length of output: 17066


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the neighboring wallet hooks for existing account typing patterns
sed -n '1,320p' src/hooks/wallet/useSignSpendBundle.ts
sed -n '1,260p' src/hooks/wallet/useSpendBundle.ts
sed -n '1,220p' src/hooks/wallet/useSignUserOp.ts

# Find any local helpers/types that already model the account/plugin-manager shape
rg -n "signTypedData\\(|kernelPluginManager|sudoValidator|getAction\\(|hook\\b" src/hooks/wallet src/context

Repository: peanutprotocol/peanut-ui

Length of output: 35670


Model the minimal kernelPluginManager shape locally. The any casts here trip lint and hide the account surface this flow relies on; a small structural type for sudoValidator, getAction, and hook keeps the nonce path intact without suppressions.

🧰 Tools
🪛 ESLint

[error] 169-169: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 197-197: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 206-206: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/wallet/useGrantSessionKey.ts` around lines 169 - 206, The use of
`any` around `kernelPluginManager` is masking the exact account shape and
triggering lint issues in the session-key grant flow. Replace the `as any` casts
with a small स्थानीय structural type for the `kernelPluginManager` object used
by `useGrantSessionKey`, covering the referenced `sudoValidator`, `getAction`,
and `hook` members so `createKernelAccount` and `getPluginsEnableTypedData` can
use it safely without suppressions.

Source: Linters/SAST tools

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.

1 participant