Skip to content

feat(spend): one-tap mixed spend for the sign-only engine, dark behind its own flag - #2969

Closed
innolope-dev wants to merge 3 commits into
feat/session-key-spend-build-gatefrom
feat/session-key-spend-sign-engine
Closed

feat(spend): one-tap mixed spend for the sign-only engine, dark behind its own flag#2969
innolope-dev wants to merge 3 commits into
feat/session-key-spend-build-gatefrom
feat/session-key-spend-sign-engine

Conversation

@innolope-dev

Copy link
Copy Markdown
Collaborator

Stacked on #2959 (feat/session-key-spend-build-gate) — it needs the PostHog-flag gate from there. Retarget to dev once #2959 merges. Stacked PRs get no automatic tests/format/chip run; tests.yml is triggered by hand on this branch.

Why

QR pay, Manteca withdraw and the card lock/cancel modals sign a mixed spend through useSignSpendBundle and hand the UserOp to the backend to broadcast. That engine still costs two passkey sheets (Rain admin EIP-712, then the UserOp). #2959 covers only the broadcasting engine (useSpendBundle). Item 3 of mono/engineering/passkey-prompt-pairs-2026-09-03.md.

What

  • signMixedEphemeralSpend (src/hooks/wallet/mixedEphemeralSign.ts): sign-only twin of tryMixedEphemeralSpend. Same single enable-signature tap inside createEphemeralSpendSession; the ephemeral key then signs the Rain admin EIP-712 and the UserOp silently, self-uninstall as the last call, and returns the unbroadcast op in the existing SignedUserOpData shape. signUserOperation from the ZeroDev SDK, exactly what useSignUserOp uses.
  • useSignSpendBundle mixed path tries it first when sessionKeySignEnabled(); on any failure it falls through to the two-tap path with the same prep (nothing was broadcast, nothing at stake) and reports session_key_spend_fallback with flow: 'sign-only'.
  • Own flag session_key_spend_sign (build gate shared with feat(spend): one-tap mixed spend for everyone (broadcasting engine) #2959). Why not reuse session_key_spend: here a permission that does not validate on-chain surfaces later, as the backend's broadcast reverting, with no client-side retry. So this flag stays off until feat(spend): one-tap mixed spend for everyone (broadcasting engine) #2959 has proven the ERC-1271 ordering on production contracts, then it is one click.
  • createEphemeralSpendSession takes an optional ttlSeconds; this path uses 10 min (default stays 180 s). The backend broadcasts after Manteca settles inside the same request, with a receipt re-poll on timeout, so 3 min was tight. The op stays single-use through its nonce and the batch still ends in the self-uninstall.
  • /dev/session-key-spend shows the second flag.
  • buildWithdrawCall exported from mixedEphemeralSpend.ts (shared with the twin).

Rollout

  1. feat(spend): one-tap mixed spend for everyone (broadcasting engine) #2959 merges, flag session_key_spend proves one real mixed send on prod (one sheet, no fallback).
  2. Then create session_key_spend_sign, internal cohort; one real QR pay on a mixed balance; expect one sheet and session_key_spend_attempted{flow:'sign-only'} with no fallback and a successful backend broadcast.
  3. Widen.

Verification

  • New tests: mixedEphemeralSign (artifact shape, TTL, silent admin sig, uninstall last, signed through the session client and never sent, dispose on every exit; preflight and signing failures as ok:false), sessionKeySignEnabled gate matrix, and three useSignSpendBundle cases (flag off unchanged, flag on returns the ephemeral artifact with no passkey admin signature, fallback reuses the same prep and reports the reason).
  • pnpm typecheck clean; ephemeralSpendKey, useGrantSessionKey, session-key-spend.consts, useSignSpendBundle suites green; prettier clean.

…d its own flag

QR pay, Manteca withdraw and the card lock/cancel modals sign a mixed spend
through useSignSpendBundle and hand the UserOp to the backend to broadcast.
That engine still costs two passkey sheets (Rain admin EIP-712 + UserOp);
ui#2959 only covers the broadcasting engine.

signMixedEphemeralSpend is the sign-only twin of tryMixedEphemeralSpend:
the same single enable-signature tap, then the ephemeral key signs the
admin EIP-712 and the UserOp silently, uninstall last, and the unbroadcast
artifact goes back in the same SignedSpendArtifact shape. Nothing is sent
from the client, so a signing failure falls back to the two-tap path with
nothing at stake.

Own flag (session_key_spend_sign) rather than the broadcasting engine's:
a permission that fails on-chain surfaces here as the backend's broadcast
reverting, with no client-side retry. Stays off until ui#2959 has proven
the ERC-1271 ordering on production contracts.

The permission lifetime is 10 min for this path (backend broadcasts after
Manteca settles, with a receipt re-poll on timeout); the op stays
single-use through its nonce.
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
peanut-wallet Ready Ready Preview Sep 3, 2026 8:29pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 1323a8f2-4cb4-4d0e-a10f-ae38ad404423

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

…or cannot be resolved

Same gap Chip found on the broadcasting engine (ui#2959): the validator
lookup sat outside the helper's catch, so a rejection escaped as a failed
spend after /prepare had run. It now yields ok:false and the two-tap path
signs against the same preparation.
@innolope-dev

Copy link
Copy Markdown
Collaborator Author

Folded into #2959: with no rollout gate the sign-only engine port no longer needs anything from a flag branch, so both engines ship in one PR. Commits 0bc310f and e0d0ca7 were cherry-picked there and the flag stripped.

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