Skip to content

feat(wallets): approve as a quorum member (EVM + Stellar quorum E1) - #2255

Open
daniil-dovgal wants to merge 21 commits into
daniil/ts-quorum-m0d-approvalsfrom
daniil/ts-quorum-e1-approve
Open

daniil-dovgal wants to merge 21 commits into
daniil/ts-quorum-m0d-approvalsfrom
daniil/ts-quorum-e1-approve

Conversation

@daniil-dovgal

@daniil-dovgal daniil-dovgal commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Description

High level implementation overview

First milestone of the TS SDK quorum stack, stacked on #2268. The foundation: #2214 reads quorum entries, #2215 adds the rules and errors, #2233 the signer seams, #2225 approval collection and the quorum-only poll outcome, and #2268 makes a quorum signer return { transactionId } unsigned.

This PR lets a client approve as a quorum member on EVM and Stellar, per EDD 007 §7 "Approval" and "Role" (crossbit-main #33357, merged):

  • approve signs every pending member this client holds, each over its own message: transactions on both chains, signatures (ERC-1271) on EVM.
  • While other members must still sign, approve returns the request without hash (or signature) instead of throwing.
  • useSigner accepts a key that is only a quorum member.

Before, any quorum entry threw QuorumSignerNotSupportedError, and useSigner refused member keys. Wallets without quorums behave as before. Solana keeps the guard: it has no quorums.

await wallet.useSigner(bob); // Bob is only a member of `treasury`
const tx = await wallet.approve({ transactionId });
if (tx.hash == null) {
    // other members must still sign
} else {
    showReceipt(tx.hash); // Bob's approval completed the quorum
}

Details

  • wallets/wallet.ts approval paths: when a quorum entry is pending, the held keys come from heldSigners() (feat(wallets): acting-signer seam, held signers and quorum API errors at call sites (quorum foundation 3/4) #2233), and every held pending member signs (§7 "never counts the threshold"; the API skips extras). If nothing held is pending, no request is sent. An api-key wallet leaves its flat entries to the API, as today.
  • chains/adapters/stellar.ts: supportsQuorumApprovals is on. The API sends one quorum entry per auth entry, so a member signs once per entry, all in one request; the API rejects only a member shared by two different quorums. A passkey member signs the hex of the 32-byte message, one prompt after another.
  • wallets/services/operation-poller.ts: a quorum-only wait returns { transactionId } with hash / explorerLink undefined, or { signatureId } with signature undefined. The types stay Transaction<false> / Signature<false> (EDD "Type honesty"), so callers check hash == null. A wait on any other signer still throws TransactionAwaitingApprovalError. No quorum progress is built any more, so an unreadable entry can't throw a plain Error (audit finding 11). SignatureAwaitingApprovalError, the error's quorums and QuorumApprovalProgress are gone; none was released.
  • Callers that act on the wait's result: addSigner resuming a registration that quorum members must still approve returns the signer with its transactionId / signatureId and its own status, as prepareOnly does. Stellar upgrade() returns phase 1 without a hash and creates no migrate transaction, since the wallet is not locked yet.
  • useSigner member keys (wallet.ts, signers/server/resolver.ts): a key resolves as a registered signer, then a recovery method, then a quorum member. A member takes its quorum's status, without the getSigner lookup that 404s for members. A passkey member needs its id.
  • api/client.ts approveTransaction: when two members race for the last slot, the API answers 422 "Already has the required number of approvals"; the client re-reads the transaction and goes on to its outcome. Every other 422 is returned unchanged.

Next Steps

E2 (#2256): a member-only active signer acts as its quorum and returns early, and EVM signMessage / signTypedData take signer. E3 (#2261): signer changes approved by a quorum.

Accepted:

  • removeSigner, the recovery-method calls and device recovery still treat any return of the wait as landed. They approve with a flat recovery signer here, so they can't reach a quorum-only wait; E3 and E5 keep that true.
  • Stellar upgrade / migrate with a quorum signer sign held members (feat(wallets): a quorum acting signer returns the created transaction unsigned, as prepareOnly does (quorum foundation 5/5) #2268 left them out, as the EDD does). Unreachable: quorum proofs need a v2 contract, and v2 has no upgrade target.
  • A Solana api-key wallet with a quorum-only wait returns without a hash, like the other chains; the API rejects Solana quorums.
  • A passkey member whose wallet has an id-less passkey recovery method resolves as that recovery method. A key in two delegated quorums takes the first one's status.

Test plan

  • Tests first. The Stellar and approve-rule commit's red baseline: 23 failed, 122 passed (the guard, the old awaiting errors, quorums on the outcome).
  • Suites:
    • wallet.quorum-approve.test.ts: the rules above on transactions and signatures, api-key wallets, addSigner resume, and a Stellar block (two auth entries, passkey and email members, others pending, expiry on submit and while waiting, the last-slot race, a flat entry not held).
    • wallet.quorum-member-signer.test.ts: every member type in recovery and delegated quorums.
    • operation-poller.test.ts (incl. finding 11), stellar.test.ts (upgrade / migrate), chain-adapter.test.ts, resolver.test.ts, and api/client.test.ts (422 rows; CI only, needs PREVIEW_CK_KEY).
  • Run: cd packages/wallets && pnpm exec vitest run src/wallets src/quorum src/utils src/chains src/signers → 1139 passed. tsc on src/ is clean; biome reports no errors.
  • Fresh-context review: three rounds for the EVM part, two for the Stellar part (Ready). The addSigner and Solana rows are mutation-checked.
  • Not here: a live Stellar testnet run (E6).

Package updates

  • @crossmint/wallets-sdk: minor (.changeset/quorum-approve-as-member.md).

daniil-dovgal and others added 7 commits October 28, 2026 21:33
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ingApprovalError and the signature outcome

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ips quorum entries; 422 on transaction approval re-reads

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…changeset

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…member; api-key wallets sign only quorum entries; re-read only an already-complete 422

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…API; a legacy-derived server member signs with its key; changeset wording

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ery key's flat entry next to a quorum entry; nest test context

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2511db4

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

This PR includes changesets to release 10 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/integration-tests 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

@daniil-dovgal
daniil-dovgal marked this pull request as ready for review October 29, 2026 04:54
@greptile-apps

greptile-apps Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Critical impact] The PR appears safe to merge.

Reviews (1) · Last reviewed commit: "fix(wallets): export TransactionAwaiting..." · Reviewed by Greptile

…: EVM turns on supportsQuorumApprovals; Solana and Stellar keep the guard

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
daniil-dovgal and others added 4 commits November 3, 2026 14:36
… unsigned, as prepareOnly does

send and EVM / Solana / Stellar sendTransaction resolve the signer once; when it is a quorum: locator the
call returns { transactionId } with hash and explorerLink undefined instead of signing and waiting (EDD 007
§7 "A quorum forces prepareOnly"). Types are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n over the early return, real server derivation

Checker round 1: a client holding one of the quorum's member keys signs nothing; a creation error with a quorum
signer throws as with any other signer; the server signer row derives its locator instead of stubbing it. The
changeset names both errors the old call threw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…action and the signature calls did

EVM / Solana / Stellar sendTransaction and signMessage / signTypedData treated any truthy prepareOnly as set;
only send required `=== true`. The shared helper now follows the majority, so an untyped caller passing a
non-boolean never gets a signing call it did not expect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… send covered on a Stellar wallet

Checker round 2: approve still throws for quorum members until E1, so the JSDoc and changeset no longer promise it;
Solana's API rejects a quorum signer at creation, so its JSDoc drops the note and the changeset scopes to EVM and
Stellar. The spec runs send on a Stellar wallet too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
daniil-dovgal and others added 9 commits November 3, 2026 18:58
…nly as truthy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings m0d's changeset wording with the exact error codes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-only

Brings the foundation audit fixes from m0a–m0d into m0f.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
m0f removed the isQuorumSigner import that E1's member lookup still uses; restored.
send() as a quorum now returns early unsigned (m0f), so E1's send-as-quorum test
signs the member through a later approve() instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ar members approve

EDD 007 §7 "Approval" (crossbit-main #33357): while only quorum members can still
approve, approve() returns the transaction without hash/explorerLink, or the
signature without signature, instead of throwing. A wait on a non-quorum signer
still throws TransactionAwaitingApprovalError.

- The awaiting outcome no longer builds quorum progress, so an unreadable quorum
  entry cannot throw a plain Error (audit finding 11). SignatureAwaitingApprovalError,
  the error's quorums, toQuorumProgress and QuorumApprovalProgress go (unreleased).
- Stellar turns on supportsQuorumApprovals: held members sign each auth entry's
  message (passkeys as hex of the 32 bytes) in one approvals request.
- Stellar upgrade() returns phase 1 without a hash when a quorum must still
  approve it, and creates no migrate transaction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bers still sign

With approve-and-wait returning instead of throwing on a quorum-only wait,
completeSignerRegistration reported a resumed registration as added. It now
returns the signer with its transactionId / signatureId and its own status, as
prepareOnly does. The operation-id shape is shared with the prepareOnly branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Solana wallet with an api-key signer leaves quorum entries to the API; when only
a quorum can still approve, approve() returns the transaction without a hash, as
on every other chain. Unreachable while the API rejects Solana quorums.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Stellar approve: when another member completes the quorum first, approve()
  returns the landed transaction the API client reads back after the 422.
- settle() moves to the test helpers; the approve, prepare-only and Stellar
  suites use it instead of their own copies.
- The two-quorum rejection test names what the API rejects: a member shared by
  two different quorums (two entries of one quorum are accepted).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n a helper

- completeSignerRegistration builds the operation id, the prepared shape and the
  added shape once each; prepareOnly and a quorum-only wait share the prepared
  return.
- #heldSigners replaces the ternary that mixed a call with an array literal.
- The addSigner resume rows pin the signer's own status ("awaiting-approval").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@daniil-dovgal daniil-dovgal changed the title feat(wallets): approve as a quorum member (EVM quorum E1) feat(wallets): approve as a quorum member (EVM + Stellar quorum E1) Nov 5, 2026

This branch has not been deployed

No deployments
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