fix(card): bind session-key enable approval to the verified live currentNonce#2313
fix(card): bind session-key enable approval to the verified live currentNonce#2313jjramirezn wants to merge 1 commit into
Conversation
…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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough
ChangesNonce-bound session key grant
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
Code-analysis diffPainscore total: 5859.06 → 5859.96 (+0.9) 🆕 New findings (6)
✅ Resolved (5)
📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
There was a problem hiding this comment.
Actionable comments posted: 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
📒 Files selected for processing (2)
src/hooks/wallet/__tests__/useGrantSessionKey.test.tsxsrc/hooks/wallet/useGrantSessionKey.ts
| 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) |
There was a problem hiding this comment.
📐 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" srcRepository: 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/utilsRepository: 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/contextRepository: 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
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.serializePermissionAccountreads that nonce via the SDK'sgetKernelV3Nonce, which silently returns1on any read failure. A flaky read at grant time therefore mints an approval frozen to nonce 1. That mismatches only accounts whose livecurrentNonce ≠ 1(migrated / sudo-validator-changed accounts): their permission never installs, and every server-side auto-balance userOp revertsAA23 InvalidNonce. The grant still "succeeds", so it's invisible. Confirmed N=1 in prod today (Rain user 67ce33df,currentNonce=2; all 53 working cards arenonce=1).Fix: read
currentNonceexplicitly (throw on failure — never guess), build the enable typed data bound to that verified nonce, sign with the same sudo validator, and inject it intoserializePermissionAccount.Risk
Low / cohort-safe. The exported
getPluginsEnableTypedDatahonors the explicitvalidatorNonceand uses the same action/hook/validator + sudo-signing path, sononce=1accounts get a byte-identical approval. The only behavior change: a failedcurrentNonceread now aborts the grant loudly instead of silently producing a broken approval.QA
useGrantSessionKey.test.tsx: (1) grant binds the enable approval to the livecurrentNonce(assertsvalidatorNonce+ injected signature), (2) a failedcurrentNonceread aborts — no approval produced.currentNonce>1account 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).