Skip to content

feat(wallets): teach the approval loop the nested quorumApprovals shape - #1993

Open
panosinthezone wants to merge 3 commits into
panos/wal-11290-m4-2-sdk-widen-create-side-recovery-to-accept-a-quorumfrom
panos/wal-11291-m4-3-sdk-teach-the-approval-loop-the-nested-quorumapprovals
Open

feat(wallets): teach the approval loop the nested quorumApprovals shape#1993
panosinthezone wants to merge 3 commits into
panos/wal-11290-m4-2-sdk-widen-create-side-recovery-to-accept-a-quorumfrom
panos/wal-11291-m4-3-sdk-teach-the-approval-loop-the-nested-quorumapprovals

Conversation

@panosinthezone

@panosinthezone panosinthezone commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Linear: WAL-11291 (M4-3)
Stacked on #1989 (M4-2).

What

Per EDD §6.4, #collectApprovals in wallets/wallet.ts now understands quorum entries in approvals.pending instead of throwing QuorumSignerNotSupportedError (the #requireNonQuorumApprovals guard is gone):

  • Flat entry → unchanged path, byte-for-byte: resolve the signer (with the existing local-device fallback), sign message, throw InvalidSignerError if the signer isn't held.
  • Quorum entry → descend into quorumApprovals.pending and match each member's locator against the signers this client holds (active signer, additionalSigners, local-device fallback). A match signs that member's own message and submits { signer: <memberLocator>, signature }. No match → skip with an info log, don't error — holders sign independently, and a member that already submitted is no longer in pending, so re-approving is an idempotent no-op.
  • No client-side threshold counting — the API and the on-chain primitive decide when the quorum is satisfied (non-custodial guarantee). If this client can sign nothing, the POST is skipped entirely and the current transaction/signature state is returned.
  • The Solana ed25519 special case (sign onChain.transaction instead of the keccak hash in message, device signers excepted) applies per member.
  • When the same signer covers several pending messages in one call (e.g. a flat entry plus a quorum membership), each approval carries its message per the EDD §6.4 submission contract; otherwise message is omitted and the wire format is unchanged. Note: the backend does not read message yet (SubmitApprovalV2025Schema is a non-strict Zod object, so the extra key is silently stripped, never a 400), and the scenario is unreachable at launch — this is forward-compatibility only. Approval gains the optional message field (additive, minor-safe).

Locator-parsing audit

Per the ticket (backend hit this bug class in Paella-Labs/crossbit-main#27842): the only SDK code parsing approvals[].signer locators is this loop — quorum entries are now handled before flat resolution, so nothing ever tries to resolve a quorum:<id> locator as a concrete signer.

Out of scope (M4-4)

Selecting a quorum member as the wallet's active signer (useSigner, withRecoverySigner, device-recovery resume) still throws the honest "not yet supported" errors. The loop is exercised today via additionalSigners and the device fallback.

Testing

New Wallet - quorum approval loop suite (9 tests total in the file): member signs only its own message, signature + transaction paths, Solana ed25519 per-member case, non-member/already-submitted skips without erroring, flat entry still signed alongside a skipped quorum entry, and message disambiguation. Full packages/wallets suite passes (687 tests), tsc clean.

🤖 Generated with Claude Code

Quorum entries in approvals.pending no longer throw: the loop signs the
members this client holds (each member's own message, submitted under the
member locator) and skips the rest without error, leaving threshold
accounting to the API. The Solana ed25519 special case applies per member,
and approvals carry their message only when one signer submits several
signatures in a single call. Flat approval entries are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 571aab2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@crossmint/wallets-sdk Minor
@crossmint/wallets-quickstart-devkit Patch
@crossmint/wallets-playground-react Patch
@crossmint/client-sdk-react-base Patch
@crossmint/client-sdk-react-native-ui Patch
@crossmint/client-sdk-react-ui Patch
@crossmint/wallets-playground-expo Patch
@crossmint/auth-ssr-nextjs-demo Patch
@crossmint/client-sdk-nextjs-starter Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@greptile-apps

greptile-apps Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
### Issue 1
packages/wallets/src/wallets/wallet.ts:1200-1222
**Quorum path nesting at 5+ levels — extract a helper**

The quorum branch inside `#collectApprovals` reaches six levels of indentation (class → method → for → if → inner-for → inner-if → `continue`). Per the team rule, deeply nested code (5+ levels) should be refactored into a helper method with an early-exit pattern to improve readability. Extracting this block into a private `#collectQuorumSignables` (or similar) that returns the matched `SignerAdapter`/`PendingMemberApproval` pairs would flatten `#collectApprovals` and make the quorum-vs-flat branching easier to follow at a glance.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "feat(wallets): teach the approval loop t..." | Re-trigger Greptile

Comment thread packages/wallets/src/wallets/wallet.ts
Flattens #collectApprovals per the nesting rule: the quorum branch moves to
#collectQuorumSignables with an early-continue in the caller.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Reviews (2): Last reviewed commit: "refactor(wallets): extract quorum member..." | Re-trigger Greptile

…accept-a-quorum' into panos/wal-11291-m4-3-sdk-teach-the-approval-loop-the-nested-quorumapprovals
@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Reviews (3): Last reviewed commit: "Merge branch 'panos/wal-11290-m4-2-sdk-w..." | Re-trigger Greptile

Comment on lines +1342 to +1344
// For Solana device signers (secp256r1), the SWIG precompile expects a signature
// over the keccak256 hash, which is provided in pendingApproval.message.
// For other Solana signers (ed25519), the full serialized transaction is signed.

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.

Let's not mention implementation details!

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