Skip to content

feat(wallets): a member-only key acts as its quorum (EVM + Stellar quorum E2) - #2256

Open
daniil-dovgal wants to merge 14 commits into
daniil/ts-quorum-e1-approvefrom
daniil/ts-quorum-e2-act-as-quorum
Open

daniil-dovgal wants to merge 14 commits into
daniil/ts-quorum-e1-approvefrom
daniil/ts-quorum-e2-act-as-quorum

Conversation

@daniil-dovgal

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

Copy link
Copy Markdown
Contributor

Description

High level implementation overview

Second milestone of the TS SDK quorum stack, stacked on #2255 (E1).

Before this PR, creating calls always sent the active key as the request's signer. A member-only key therefore got the API's SIGNER_IS_QUORUM_MEMBER, and signMessage / signTypedData had no way to name a quorum.

This PR implements EDD 007 §7 "Role" and "A quorum forces prepareOnly" for creating calls:

  • A key that is only a member acts as its quorum; a key in several quorums throws QuorumSelectionAmbiguousError.
  • A request made as a quorum, named or inferred, is created unsigned and returned as with prepareOnly: true. Members sign it later with approve.
  • signMessage / signTypedData take a signer option, like send.

Other keys behave as before, and make no extra request.

await wallet.useSigner(alice); // Alice is only a member of `treasury`
const { transactionId } = await wallet.send(to, "usdc", "1000", { prepareOnly: true }); // created as `treasury`, unsigned
await wallet.approve({ transactionId }); // Alice signs her member entry (E1)
await evmWallet.signMessage({ message, options: { prepareOnly: true, signer: treasury.locator } }); // ERC-1271 as the quorum

Details

  • wallets/wallet.ts resolveSignerParams (the feat(wallets): acting-signer seam, held signers and quorum API errors at call sites (quorum foundation 3/4) #2233 seam, now async): an explicit signer is sent as given. Otherwise the active key is sent, unless useSigner selected it as a member-only key (matched by locator). For such a key it reads the wallet's config with one getWallet call and runs feat(wallets): quorum rules and quorum API errors (quorum foundation 2/3) #2215's resolveActingRole (recovery and delegated quorums): the key's quorum, the ambiguous error before any request, or the key itself when it has since become a registered signer (on the request's chain, which signTypedData may set) or recovery method, or left its quorum. A failed read, or one with no usable config, throws WalletNotAvailableError. Only member-only keys pay for the read.
  • Each public creating method resolves the signer once and passes it to feat(wallets): a quorum acting signer returns the created transaction unsigned, as prepareOnly does (quorum foundation 5/5) #2268's isPrepareOnly. So send, sendTransaction (every chain), signMessage and signTypedData return early for an inferred quorum too, and the role is read once per call.
  • useSigner remembers that locator only for a member key; any other selection clears it. Keys swapped in by device recovery or withRecoverySigner don't match it. Chain wallets (EVMWallet.from, …) carry the locator through the constructor.
  • wallets/evm.ts / wallets/types.ts: SignatureInputOptions gains optional signer?: SignerOption. A quorum locator requests an ERC-1271 signature as that quorum and returns { signatureId } with signature undefined.
  • Stellar upgrade / migrate are unchanged: without an explicit signer they send none, and the server uses the admin signer.

Accepted:

  • For an email or phone member key in several quorums, the ambiguity error comes after preAuthIfNeeded, so an OTP can run first. preAuth can swap the active key, so the role is resolved for the key that actually signs.
  • The static result type follows prepareOnly, not the signer (EDD "Type honesty"): pass prepareOnly: true when a quorum may act.
  • On a multichain EVM wallet, a delegated quorum installed only on another chain still counts toward ambiguity: the config doesn't say where a quorum is installed.

Next Steps

E3 (#2261): signer changes approved by a quorum. The four signer-change calls take signer (sent as approver) and return early when it is a quorum, and useRecoveryMethod accepts a member of a recovery quorum.

Test plan

  • Tests first. Red baselines: 18 failed, 84 passed (first commit); 3 failed, 30 passed (signature calls as a quorum). The transaction rows were green after merging feat(wallets): approve as a quorum member (EVM + Stellar quorum E1) #2255: the inferred quorum reaches isPrepareOnly.
  • Suites:
    • wallets/wallet.quorum-acting-role.test.ts (41 tests): send, EVM and Stellar sendTransaction, signMessage and signTypedData act as a delegated or recovery quorum and return unsigned, with and without prepareOnly; one wallet read per call; registration on signTypedData's chain; the member key signs later through approve; ambiguity, explicit signer, membership changed after useSigner, address case, failed or config-less reads, an uninstalled quorum; no read for other keys on every chain; Stellar upgrade unchanged.
    • wallets/wallet.signer-params.test.ts: the EVM signature calls join feat(wallets): acting-signer seam, held signers and quorum API errors at call sites (quorum foundation 3/4) #2233's explicit-signer rows.
    • wallets/wallet.quorum-member-signer.test.ts, utils/recovery.test.ts.
  • Mutation checks (marker clear, signers() read, cached recovery, exact-case match, first delegated quorum only, a second role read before the early return): each fails its rows.
  • Run: cd packages/wallets && pnpm exec vitest run src/wallets src/quorum src/utils src/chains src/signers → 1196 passed. tsc on src/ is clean, the changed test files type-check with a test tsconfig, and biome reports no errors.
  • Fresh-context review: three rounds for the first part, three for this one; every finding fixed (one read per call, Stellar coverage, the role on signTypedData's chain).

Package updates

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

daniil-dovgal and others added 6 commits October 28, 2026 23:51
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…; signer option on EVM signatures

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t the marker clear, non-member reads on every chain, a failed lookup

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h config; test address case, two delegated quorums, later recovery changes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… helper returns no recovery methods when the API lists none

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: 2840a07

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 added this pull request to stack #2257 October 29, 2026 05:38
@daniil-dovgal
daniil-dovgal marked this pull request as ready for review October 29, 2026 06:29
@greptile-apps

greptile-apps Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The change affects request creation for member-only keys across wallet chains and EVM signatures.

Fix the cross-chain registration check before merging so member-only keys reliably act as their quorum.

Reviews (1) · Last reviewed commit: "Merge remote-tracking branch 'origin/dan..." · Reviewed by Greptile

Comment thread packages/wallets/src/wallets/wallet.ts Resolved
… acts as its quorum

The role read counted any delegated signer with the key's locator as registered. EVM signers list the chains
they are registered on, so a key registered on Polygon only sent itself on Base instead of its quorum.
isSignerOnChain now holds that rule for both the role read and mapApiSignerToSigner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
daniil-dovgal and others added 2 commits November 4, 2026 21:22
Conflicts: m0f resolves the signer once in each public method and passes it to
createTransaction; E2 made resolveSignerParams async. Each public method now awaits
it once, so isPrepareOnly sees the inferred quorum.

Fallout in wallet.quorum-acting-role.test.ts:
- a member-only key's send returns unsigned as its quorum (m0f), nothing signed;
- a quorum-only signature wait returns { signatureId, signature: undefined } (E1);
- the shared settle helper replaces the local copy.

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

EDD 007 §7 "A quorum forces prepareOnly": signMessage and signTypedData resolve
the signer once and return { signatureId, signature: undefined } when it is a
quorum, named in `signer` or inferred from a key that is only a member. Members
sign later with approve().

The acting-role suite now asserts the early return without prepareOnly for the
inferred quorum on send, EVM and Stellar sendTransaction, and both signature calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
daniil-dovgal and others added 4 commits November 4, 2026 21:39
…ter with approve

- Each creating call (send, EVM sendTransaction, signMessage, signTypedData) with a
  member-only key reads the wallet once: a second read that finds the key
  registered must not turn the quorum request into a flat one. Mutation-checked.
- A member-only key signs its member entry of a send and of a signature made as its
  quorum through a later approve() (EDD 007 §7 use case 2).
- JSDoc on send / sendTransaction names the inferred quorum too.
- createAwaitingRequest joins the shared test helpers; a duplicate signature row goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The one-read table gains Stellar sendTransaction (mutation-checked) and types
  settle<unknown>, so the file type-checks with the test tsconfig.
- The one-read and approve() rows move into the creating-call describe as "when"
  contexts and reuse its setup; UNSIGNED_TRANSACTION / UNSIGNED_SIGNATURE replace
  the repeated literals.
- signMessage JSDoc and the changeset say to pass prepareOnly: true when a quorum
  may act, so the return type matches.

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

- UNSIGNED_TRANSACTION / UNSIGNED_SIGNATURE join the test helpers; E1's suite keeps
  its AWAITING_* names as aliases.
- The acting-role suite uses one STELLAR_CONTRACT_CALL input.
- signTypedData JSDoc says to pass prepareOnly: true when a quorum may act.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…est's chain

signTypedData creates its request on params.chain, which can differ from the
wallet object's chain. The role read now checks registration on that chain, so a
key registered there acts as itself (EDD 007 §7 Role row 1), and a key registered
only on the wallet's chain acts as its quorum. Other calls keep the wallet's chain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@daniil-dovgal daniil-dovgal changed the title feat(wallets): a member-only key acts as its quorum (EVM quorum E2) feat(wallets): a member-only key acts as its quorum (EVM + Stellar quorum E2) 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