Skip to content

feat(cli): multisig support — N-of-M wallets + token operations#85

Open
Zhaiyuqing2003 wants to merge 43 commits into
developfrom
feat/multisig-cli
Open

feat(cli): multisig support — N-of-M wallets + token operations#85
Zhaiyuqing2003 wants to merge 43 commits into
developfrom
feat/multisig-cli

Conversation

@Zhaiyuqing2003

@Zhaiyuqing2003 Zhaiyuqing2003 commented May 6, 2026

Copy link
Copy Markdown
Contributor

Replaces the standalone Rust fastset-multisig-cli by adding multisig support to the TypeScript fast-cli, backed by a new MultiSigSigner abstraction in @fastxyz/sdk. Polymorphic operations (send, token create/mint/burn/manage) work for both single-signer and multisig accounts; multisig coordination commands (init, import, export, pending, vote) live under a dedicated namespace.

Two phases, both included here. Specs and plans are in docs/superpowers/.

Phase 1 — Multisig backbone

SDK (@fastxyz/sdk minor):

  • New MultiSigSigner class symmetric to SignersignEnvelopeFor(versionedTx) for the cosigner-vote path, signTransaction(opts) for the initiate path.
  • Helpers: deriveMultiSigAddress, deriveMultiSigAddressBytes, assertAuthorizedSigner.
  • Tagged errors: MultiSigConfigInvalidError, NotAuthorizedSignerError.

CLI (@fastxyz/cli minor):

  • accounts table migrated to a tagged union (single | multisig) with a CHECK constraint enforcing the discriminator. Single-row backfill set to kind='single'.
  • signer-resolver service maps any account row → Signer | MultiSigSigner based on kind, with --as <member> disambiguation for multisig.
  • New commands: fast multisig init / import / export / pending / vote.
  • fast send Fast→Fast route is now polymorphic on account kind.
  • fast account list adds a KIND column (e.g. multisig 2-of-3).
  • Six new tagged errors (NotAMember, AmbiguousMember, AddressDerivationMismatch, AlreadyVoted, WalletKindMismatch, MultiSigConfigInvalid).

Phase 2 — Token operations

  • Extracted submitOperation pipeline helper consolidating build/sign/submit/branch logic so the four token commands stay focused.
  • fast token create / mint / burn / manage, polymorphic over single + multisig.
  • --memo on create and manage (BCS layouts have user_data); not on mint / burn.
  • token manage fetches the current updateId from getTokenInfo and submits it verbatim (validator increments after settlement — confirmed against the Rust reference).
  • All four ops record to the local history table on success.

Review follow-up — 2026-05-21

  • Keep Fast-side balance rendering for multisig wallets while skipping EVM balance lookups.
  • Wrap multisig export --out filesystem failures as FileIOError.
  • Remove double 0x prefixes from multisig pending/vote hashes and related error output.
  • Return NotAMemberError when --as names a non-member.
  • Reject multisig accounts used as signer inputs during multisig init.
  • Treat IncompleteVerifierSigs as an unfinished transaction result in submitOperation.
  • Normalize malformed Fast-address failures in token mint/manage to InvalidAddressError.
  • Resolve passwords after the concrete single-signer/member account is known, so unencrypted keys work in JSON/non-interactive flows.
  • Replace ESM-incompatible __dirname usage in Vitest files with import.meta.url path resolution.
  • Bring design docs and changeset in line with the implemented token command surface.

Test plan

  • pnpm -C app/cli exec tsc --noEmit
  • pnpm -C app/cli exec vitest run (14 files, 80 tests)
  • pnpm -C packages/fast-sdk exec vitest run tests/unit/multisig-signer.test.ts (14 tests)
  • pnpm build (9/9 turbo tasks)
  • GitHub Actions CI on latest commit: test (20) and test (22) passed
  • Earlier full monorepo pnpm test passed before the review follow-up patch.
  • fast multisig --help, fast multisig {init,import,export,pending,vote} --help render.
  • fast token --help, fast token {create,mint,burn,manage} --help render.
  • Smoke test against testnet: 2-of-3 multisig init → send (incomplete-multisig) → cosigner vote → success.
  • Smoke test against testnet: token create from single-signer → admin assigned correctly.
  • Smoke test against testnet: token manage rejected with wrong updateId, accepted with current.

Remaining tasks before merge

  • Run the three testnet smoke tests above with funded testnet accounts and the required RPC/network configuration.
  • Get final reviewer approval on the latest green commit.

Breaking changes / migration

  • accounts table gains kind, multisig_config columns; evm_address, encrypted_key, encrypted become nullable. Existing rows backfill kind='single' automatically on first launch (drizzle migration 0001_bumpy_komodo.sql).
  • AccountInfo is now a tagged union — code that touched AccountInfo directly needs to discriminate on kind. All in-tree consumers updated.

Out of scope (deferred)

  • Rename SDK MultiSigConfigInvalidError to disambiguate from the CLI class with the same _tag (technical debt; runtime is fine because the CLI re-maps SDK errors).
  • Type FastRpc.submitTransaction / getPendingMultisigTransactions returns to drop as never casts.
  • Promise-cache MultiSigSigner.ensureValid for concurrency.
  • Validate multisigConfig on read in rowToInfo.
  • Extract token-args helpers (parseAmount, resolveTokenIdAndDecimals, resolvePassword) — duplication is contained for now.
  • Add command-handler integration tests (current tests exercise submitOperation directly).
  • Print the new token id in token create output.
  • Aliases for external (non-keystore) signer addresses.

Copilot AI review requested due to automatic review settings May 6, 2026 00:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds multisig wallet support to the TypeScript SDK/CLI and extends the CLI’s transaction flow so token operations can run through either single-signer or multisig accounts. This fits the repo’s goal of making fast-cli the primary TypeScript interface for account, transfer, and token workflows.

Changes:

  • Adds MultiSigSigner plus multisig address/signing helpers to @fastxyz/sdk.
  • Migrates the CLI account model to single | multisig, adds multisig resolver/pipeline services, and introduces fast multisig + fast token command groups.
  • Adds migrations, tests, docs, and release metadata for the new multisig/token surface.

Reviewed changes

Copilot reviewed 48 out of 49 changed files in this pull request and generated 23 comments.

Show a summary per file
File Description
packages/fast-sdk/tests/unit/multisig-signer.test.ts New SDK unit tests for multisig derivation and signing.
packages/fast-sdk/tests/unit/fixtures/multisig-addresses.json Rust-derived multisig address fixtures.
packages/fast-sdk/src/interface/multisig-signer.ts New multisig signer implementation and helpers.
packages/fast-sdk/src/index.ts Re-exports multisig SDK APIs.
docs/superpowers/specs/2026-05-04-multisig-in-fast-cli-design.md Design/spec document for multisig CLI/SDK work.
app/cli/vitest.config.ts Adds CLI Vitest configuration.
app/cli/tests/unit/tx-pipeline.test.ts Unit tests for shared submit pipeline.
app/cli/tests/unit/signer-resolver.test.ts Unit tests for single vs multisig signer resolution.
app/cli/tests/unit/multisig-wallet-schema.test.ts Unit tests for multisig wallet config parsing.
app/cli/tests/unit/account-store-multisig.test.ts Unit test for multisig account persistence.
app/cli/tests/integration/token-operations.test.ts Service-level token flow tests.
app/cli/tests/integration/multisig-happy-path.test.ts Service-level multisig flow tests.
app/cli/src/services/tx-pipeline.ts Shared build/sign/submit helper for token ops.
app/cli/src/services/storage/history.ts Broadens history entry type handling.
app/cli/src/services/storage/account.ts Converts account storage to tagged union and adds multisig persistence/export rules.
app/cli/src/services/signer-resolver.ts Resolves account rows into Signer or MultiSigSigner.
app/cli/src/services/api/fast.ts Adds pending-multisig RPC wrapper.
app/cli/src/schemas/multisig-wallet.ts Defines multisig wallet JSON schema/helpers.
app/cli/src/schemas/history.ts Expands history types for token operations.
app/cli/src/main.ts Adds command validation/help coverage for multisig and token commands.
app/cli/src/errors/index.ts Wires new multisig/account errors into CLI error union.
app/cli/src/errors/account.ts Adds multisig/account-specific tagged errors.
app/cli/src/db/schema.ts Updates account table shape for multisig storage.
app/cli/src/commands/token/mint.ts New token mint command.
app/cli/src/commands/token/manage.ts New token manage command.
app/cli/src/commands/token/index.ts Exports token command set.
app/cli/src/commands/token/create.ts New token create command.
app/cli/src/commands/token/burn.ts New token burn command.
app/cli/src/commands/send.ts Makes Fast→Fast send path multisig-aware.
app/cli/src/commands/pay.ts Restricts pay to single-signer accounts.
app/cli/src/commands/multisig/vote.ts New multisig vote command.
app/cli/src/commands/multisig/pending.ts New pending multisig listing command.
app/cli/src/commands/multisig/init.ts New multisig wallet init command.
app/cli/src/commands/multisig/index.ts Exports multisig command set.
app/cli/src/commands/multisig/import.ts New multisig import command.
app/cli/src/commands/multisig/export.ts New multisig export command.
app/cli/src/commands/info/balance.ts Adjusts balance output for multisig accounts.
app/cli/src/commands/index.ts Registers new command handlers.
app/cli/src/commands/fund/crypto.ts Restricts crypto funding to single-signer accounts.
app/cli/src/commands/account/list.ts Adds account kind output.
app/cli/src/commands/account/export.ts Blocks exporting multisig rows as private keys.
app/cli/src/cli.ts Adds parsers for multisig/token CLI commands and send --as.
app/cli/package.json Adds CLI test script.
app/cli/drizzle/meta/0001_snapshot.json Drizzle snapshot for account schema update.
app/cli/drizzle/meta/_journal.json Registers new migration in drizzle journal.
app/cli/drizzle/0001_bumpy_komodo.sql Migrates accounts table to tagged union shape.
.changeset/multisig-signer.md Release note for SDK/CLI multisig changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/cli/src/commands/info/balance.ts Outdated
Comment thread app/cli/src/commands/multisig/export.ts
Comment thread app/cli/src/commands/multisig/pending.ts Outdated
Comment thread app/cli/src/commands/multisig/vote.ts Outdated
Comment thread app/cli/src/services/signer-resolver.ts Outdated
Comment thread app/cli/tests/integration/token-operations.test.ts Outdated
Comment thread app/cli/tests/integration/multisig-happy-path.test.ts Outdated
Comment thread docs/superpowers/specs/2026-05-04-multisig-in-fast-cli-design.md Outdated
Comment thread docs/superpowers/specs/2026-05-04-multisig-in-fast-cli-design.md Outdated
Comment thread .changeset/multisig-signer.md Outdated
@kaichii kaichii changed the base branch from main to feat/cli-explicit-default-token May 21, 2026 02:04
@kaichii kaichii changed the base branch from feat/cli-explicit-default-token to develop May 21, 2026 02:04
Yuqing Zhai and others added 23 commits May 21, 2026 14:29
Replaces the standalone Rust fastset-multisig-cli by adding multisig
support to fast-cli, backed by a new MultiSigSigner abstraction in
fast-sdk. Polymorphic operations (send, token create/mint/burn/manage)
work for both single-signer and multisig accounts; multisig
coordination commands (init, import, export, pending, vote) live under
a dedicated namespace. Two-phase implementation: backbone first,
token operations second.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
21 tasks covering: SDK MultiSigSigner + helpers, tagged-union accounts
schema migration, signer-resolver service, multisig
init/import/export/pending/vote commands, polymorphic send, and a
2-of-3 happy-path integration test. Phase 2 (token operations) gets a
separate plan once Phase 1 lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous Task 4 implementation embedded the snake_case BCS-input
config into the camelCase + branded SignatureOrMultiSig.Type slot.
Worked at runtime but failed tsc.

- Brand pubkey, signature, authorized_signers, quorum, and nonce via
  Schema.decodeUnknown of *FromInput schemas.
- Construct the camelCase config (authorizedSigners) at the envelope
  boundary; the BCS-input MultiSigConfig type is unchanged.
- Drop redundant ensureValid() call; getSignerPublicKey already does it.
- Hoist Signer/TransactionBuilder imports in the test from dynamic
  imports to a static top-of-file block.
- Re-cast versioned tx after sender swap in test (spread loses brand).
- Add byte-equivalence assertion: a single-signer Signer.signTypedData
  over the same versioned tx must produce the same 64-byte signature
  as the multisig partial. Locks in the cross-signer aggregation
  invariant proxy aggregation depends on.
Add 6 typed multisig error classes (MultiSigConfigInvalidError,
NotAMemberError, AmbiguousMemberError, AddressDerivationMismatchError,
AlreadyVotedError, WalletKindMismatchError), extend the ClientError
union, push the multisig-rejection check into the storage-layer
exportAccount, and replace the Task 9 placeholder InvalidUsageError
multisig refusals in pay, send, fund/crypto, and account/export with
WalletKindMismatchError so error codes are stable and structured.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Fast→Fast route now dispatches on account kind via resolveSigner.
Single-signer flows continue to use TransactionBuilder unchanged;
multisig flows initiate via MultiSigSigner.signTransaction and surface
the IncompleteMultiSig submit response with a partial-signature notice
(no history record until quorum is reached).

The multisig refusal guard moves to the EVM-bridging branches only,
since those routes still need a single-signer seed for EVM signing.
A new --as flag lets multisig users pick a member when more than one
local single-signer matches the wallet config.
Yuqing Zhai and others added 16 commits May 21, 2026 14:33
multisigInitParser and multisigImportParser each declared a local
--network flag that collided with the same option in globalOptions,
which caused the parser merger to throw DuplicateOptionError on
every CLI invocation. Both handlers fall back to config.network
(the global flag's destination) instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final-review polish: the existing changeset only bumped @fastxyz/sdk
even though @fastxyz/cli also gains the multisig command group +
account schema migration. The export subcommand parser also lacked
description fields on its NAME argument and --out option, leaving
blank rows in --help output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six tasks: extract submitOperation pipeline helper from send.ts and
add fast token create/mint/burn/manage commands plus an integration
test. Each command is polymorphic (single + multisig) by routing
through the helper. Phase 1's signer-resolver, error taxonomy, and
SDK MultiSigSigner carry over unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Document that JoinCommittee/ChangeCommittee aren't reachable via the
single-signer TransactionBuilder dispatch (multisig path bypasses the
switch and hands operations directly to signTransaction).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `fast token create --name <s> --decimals <n> --initial-supply <amt>
[--minters <addr,...>] [--memo <s>] [--as <name>]`. Polymorphic on the
active account kind via submitOperation: single-signer accounts submit
on-chain, multisig accounts return an incomplete-multisig partial that
cosigners can pick up via `fast multisig pending` / `multisig vote`.
Active account becomes the token admin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The validator expects the operation to carry the token's CURRENT
updateId (it increments after settlement), not the next value. The
plan's original guess (current + 1n) was wrong — confirmed against
fastset-multisig-cli/src/main.rs and fastset validator_tests.rs
where sequential management ops use update_id 0, 1, 2, ... against
a token that starts at 0.

Without this fix, every fast token manage call against a real
network is rejected with an UpdateIdMismatch-style error. Plan
amended to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror the send.ts pattern across token create/mint/burn/manage so that
successful token operations show up in `fast info history`. Extends the
HistoryEntry.type union with token-create, token-mint, token-burn, and
token-manage variants. Recording happens only on the success branch
(incomplete-multisig has no certificate yet).

For `token create`, the deterministic token id is computed via
getTokenId(senderBytes, nonce, 0n) using the resolved signer's public
key (single) or derived address bytes (multisig); the resulting hex id
is also surfaced in the JSON output for convenience.
@kaichii kaichii force-pushed the feat/multisig-cli branch from d7e03a1 to 2b1268d Compare May 21, 2026 07:42
@kaichii kaichii force-pushed the feat/multisig-cli branch from 214d1d9 to 7905218 Compare May 21, 2026 13:02
@kaichii kaichii requested review from KcatMa and xc93 May 21, 2026 13:19
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.

3 participants