Skip to content

feat(wallets): a quorum acting signer returns the created transaction unsigned, as prepareOnly does (quorum foundation 5/5) - #2268

Open
daniil-dovgal wants to merge 6 commits into
daniil/ts-quorum-m0d-approvalsfrom
daniil/ts-quorum-m0f-prepare-only
Open

daniil-dovgal wants to merge 6 commits into
daniil/ts-quorum-m0d-approvalsfrom
daniil/ts-quorum-m0f-prepare-only

Conversation

@daniil-dovgal

Copy link
Copy Markdown
Contributor

Description

High level implementation overview

Last foundation PR of the TS SDK quorum stack, stacked on #2225. The earlier PRs:

Before: send / sendTransaction with signer: "quorum:<id>" created the transaction, then failed on the approval
path. A non-api-key active signer threw QuorumSignerNotSupportedError; an api-key one polled and threw
TransactionAwaitingApprovalError. Either way the created request was left behind.

After: per EDD 007 §7 "A quorum forces prepareOnly" (crossbit-main #33357), the call returns the created request
unsigned, without signing or waiting. Members approve it later with approve. No public type changes.

const { transactionId } = await wallet.send(to, "usdc", "1", { prepareOnly: true, signer: "quorum:…" });
// { hash: undefined, explorerLink: undefined, transactionId }

Details

  • wallets/wallet.ts isPrepareOnly(options, signerLocator): a truthy prepareOnly, or a quorum: locator
    (hasQuorumLocatorPrefix). One protected helper; E2 (member-only keys, signature calls) and E3 (signer-change
    approver) reuse it. Truthy, as sendTransaction and the signature calls already read it; only send checked
    === true, so an untyped send caller passing a non-boolean now gets the early return, which never signs.
  • The signer is resolved once, in the public method, and the same value is sent and checked. EVM / Solana / Stellar
    createTransaction now take { signer } instead of resolving it. This matters from E2 on, where resolution reads
    the wallet config.
  • Type gap (accepted): with prepareOnly: false, the type says hash: string while a quorum call returns
    undefined. TransactionInputOptions already requires prepareOnly, so callers pass true to get the matching
    type; the JSDoc on send / sendTransaction says so. EDD use case 5 ({ signer: treasury } alone) does not
    compile for that reason; the EDD snippet gets prepareOnly: true rather than the types changing.
  • approve is unchanged. At this point no client signs a quorum entry, so it never reaches "threshold not met". The
    EDD thread's proposal for that case (a quorum-only wait returns { transactionId }, hash undefined) lands with E1.
  • Unchanged on purpose: Stellar upgrade / migrate (the EDD lists neither), and a malformed quorum: locator,
    which still reaches the API and gets its 400 mapped as today. Solana runs the same code, but its API rejects a
    quorum signer at creation, so the changeset and JSDoc describe EVM and Stellar only.

Next Steps

Merge this into E1 (#2255), E2 (#2256) and E3 (#2261), one adaptation commit each: E1's quorum sends stop signing held
members, E2's inferred quorum and signature calls return early unsigned, E3's quorum-approved signer changes return early.

Test plan

  • Tests first. wallets/wallet.quorum-prepare-only.test.ts (new, 80 cases), for send (EVM and Stellar wallets)
    and EVM / Solana / Stellar sendTransaction:
    • quorum signer × api-key and external-wallet active signers × prepareOnly true / false / omitted: the exact
      result, the signer sent, no transaction read, no approval;
    • the active signer is one of the quorum's members: its onSign is never called;
    • a creation error with a quorum signer throws as with any other signer; a retry creates two requests;
    • non-quorum controls (active signer, explicit locator, a real server secret) still wait for the landed hash, and a
      truthy non-boolean prepareOnly returns early.
    • Red baseline (first 52 cases): 20 failed, 32 passed. api-key rows threw TransactionAwaitingApprovalError,
      external-wallet rows QuorumSignerNotSupportedError. Rows added later were checked by mutation: dropping the quorum
      check fails the member rows, and === true fails the truthy rows.
  • From packages/wallets: pnpm exec vitest run src/wallets src/quorum src/utils src/chains src/signers: 36 files,
    1068 tests passed.
  • tsc on src/ clean; the new spec type-checks under a test tsconfig; biome reports no new warnings.
  • pr-checker, 3 rounds, Ready. Round 1 added the member, creation-error and real-server-secret rows. Round 2
    scoped the changeset and JSDoc to what this PR does: no promise of approve before E1, no Solana claim.
  • Known gap, older than this PR: no wallet-level test that preAuthIfNeeded runs before creation for an email or
    phone signer. The order is unchanged.

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>
@changeset-bot

changeset-bot Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7ce2207

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 November 3, 2026 20:20
@greptile-apps

greptile-apps Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge.

Reviews (1) · Last reviewed commit: "docs(wallets): quorum prepareOnly notes ..." · Reviewed by Greptile

daniil-dovgal and others added 2 commits November 3, 2026 18:58
…nly as truthy

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>

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