@0xsend/cws-cli
v0.4.0
Published
End-user Canton wallet CLI (TUI + scripted)
Readme
@0xsend/cws-cli
End-user Canton wallet CLI (cws) with both an interactive TUI and a
scripted subcommand mode. Ships an encrypted keystore, profile-based
multi-identity support, and mainnet typed-token confirmation for every
mutation.
Install
npm install -g @0xsend/cws-cli
# or: pnpm add -g, yarn global add, bun install -g
cws --helpRequires Node.js 20 or newer. The published bundle is fully self-contained — no extra @0xsend/* packages need to be installed alongside.
First run
Create a profile. The wizard walks you through the choices:
cws profile create workThe prompts in order:
- Network —
mainnet|testnet|localnet. Selects the Canton deployment and (for mainnet/testnet) the default cws-api base URL and Keycloak realm. Localnet requires you to supplybaseUrlandkeycloak.urlexplicitly. - API key — paste the cws-api key for the target realm. Stored inside the encrypted keystore, never on disk in plaintext.
- Signing key source — one of:
generate— new P-256 keypair (default; secp256k1 also supported), private key held only in the keystore.import— read a PEM/DER file from disk.paste— paste hex/base64 material at the prompt.
- Password — encrypts the keystore with Argon2id + AES-256-GCM. You will be asked for it every time a command needs to sign (no session cache in v1).
Profiles live under $WALLET_CONFIG (default ~/.config/wallet-cli).
Commands
cws
├── health check cws-api connectivity
├── profile
│ ├── create <name>
│ ├── list
│ ├── import <name> --from <path> --network <mainnet|testnet|localnet>
│ └── remove <name>
├── multisig
│ ├── list list multisigs visible to the active profile
│ ├── show <id> show multisig details
│ ├── pending list pending items across all visible multisigs
│ ├── create create a new multisig
│ ├── queue propose new multisig transactions (prepare + queue + self-sign)
│ │ ├── send <multisigPartyId> <to> <amount> <instrument> [--description <text>]
│ │ ├── accept <transferInstructionId> --multisig <partyId>
│ │ ├── preapproval grant <multisigPartyId> <providerPartyId> [--expires-at <ms>]
│ │ ├── preapproval revoke <multisigPartyId> <preapprovalContractId>
│ │ ├── automerge enable <multisigPartyId> --provider <partyId> --instrument <id>
│ │ ├── automerge disable <multisigPartyId> <delegationContractId>
│ │ └── automerge revoke <multisigPartyId> <delegationContractId>
│ ├── transaction operate on existing queued multisig transactions
│ │ ├── list --multisig <partyId> --queue [--status <status>] [--limit <n>]
│ │ ├── list --multisig <partyId> --offers [--direction <incoming|outgoing|all>]
│ │ ├── show <transactionId>
│ │ ├── history --multisig <partyId> [--status <status>] [--limit <n>]
│ │ ├── approve <transactionId>
│ │ ├── reject <transactionId>
│ │ ├── cancel <transactionId>
│ │ └── submit <transactionId> re-submit a fully-signed queued transaction
│ └── topology
│ ├── list --multisig <id>
│ ├── show <id> --multisig <multisigId>
│ ├── propose add-member <multisigPartyId> <party>
│ ├── propose remove-member <multisigPartyId> <party>
│ ├── propose set-threshold <multisigPartyId> <from> <to>
│ ├── approve <id> --multisig <multisigId>
│ └── reject <id> --multisig <multisigId>
├── transaction single-signer (1/1) operations — partyId derived from keystore
│ ├── list --offers [--direction <incoming|outgoing|all>]
│ ├── send <to> <amount> <instrument> [--description <text>]
│ ├── accept <transferInstructionId>
│ ├── preapproval grant <providerPartyId> [--expires-at <ms>]
│ ├── preapproval revoke <preapprovalContractId>
│ ├── automerge enable --provider <partyId> --instrument <id>
│ ├── automerge disable <delegationContractId>
│ └── automerge revoke <delegationContractId>
├── instruments
│ └── list [--json] list available instruments
└── holdings
└── list <partyId> [--instrument <id>] [--json] list holdings for a partyAll commands accept --json for machine-readable output and --profile
<name> to override WALLET_PROFILE.
Multisig vs single-signer
multisig queue * commands take an explicit <multisigPartyId> and route
the prepared transaction through the queue (threshold signers approve, then
multisig transaction submit or auto-submit when threshold is met).
transaction * commands are for 1/1 wallets. The party id is derived
automatically from the active profile's keystore — no party argument is
needed. The cws-api submits the transaction directly upon signing.
Health
cws healthProfile
cws profile list
cws profile create <name>
cws profile import <name> --from <path> --network <mainnet|testnet|localnet>
cws profile remove <name>Multisig — party-level
cws multisig list
cws multisig show <id>
cws multisig pending
cws multisig createMultisig — propose new queued tx (queue)
cws multisig queue send <multisigPartyId> <to> <amount> <instrument> [--description <text>]
cws multisig queue accept <transferInstructionId> --multisig <partyId>
cws multisig queue preapproval grant <multisigPartyId> <providerPartyId> [--expires-at <ms>]
cws multisig queue preapproval revoke <multisigPartyId> <preapprovalContractId>
cws multisig queue automerge enable <multisigPartyId> --provider <partyId> --instrument <id>
cws multisig queue automerge disable <multisigPartyId> <delegationContractId>
cws multisig queue automerge revoke <multisigPartyId> <delegationContractId>Multisig — act on queued tx (transaction)
list requires exactly one of --queue (queued multisig transactions) or
--offers (incoming TransferInstructions). Pass --direction to filter
offers by incoming (default), outgoing, or all.
cws multisig transaction list --multisig <partyId> --queue [--status <status>] [--limit <n>]
cws multisig transaction list --multisig <partyId> --offers [--direction <incoming|outgoing|all>]
cws multisig transaction show <transactionId>
cws multisig transaction history --multisig <partyId> [--status <status>] [--limit <n>]
cws multisig transaction approve <transactionId>
cws multisig transaction reject <transactionId>
cws multisig transaction cancel <transactionId>
cws multisig transaction submit <transactionId> # re-submit a fully-signed queued transactionMultisig — topology
cws multisig topology list --multisig <id>
cws multisig topology show <id> --multisig <multisigId>
cws multisig topology propose add-member <multisigPartyId> <party>
cws multisig topology propose remove-member <multisigPartyId> <party>
cws multisig topology propose set-threshold <multisigPartyId> <from> <to>
cws multisig topology approve <id> --multisig <multisigId>
cws multisig topology reject <id> --multisig <multisigId>Single-signer transaction (1/1 wallets)
list --offers discovers TransferInstruction contract ids you can act on
with accept / reject / withdraw. Default direction is incoming.
cws transaction list --offers [--direction <incoming|outgoing|all>]
cws transaction send <to> <amount> <instrument> [--description <text>]
cws transaction accept <transferInstructionId>
cws transaction preapproval grant <providerPartyId> [--expires-at <ms>]
cws transaction preapproval revoke <preapprovalContractId>
cws transaction automerge enable --provider <partyId> --instrument <id>
cws transaction automerge disable <delegationContractId>
cws transaction automerge revoke <delegationContractId>Instruments
cws instruments list [--json] # list available instruments across the active networkHoldings
cws holdings list <partyId> [--instrument <id>] [--json] # list holdings for a partyEnvironment variables
WALLET_CONFIG— root directory for profiles (default~/.config/wallet-cli).WALLET_PROFILE— profile name used when--profileis not passed.WALLET_PASSWORD— keystore password. Security warning: any process on the host can read the environment of another process owned by the same user; prefer the interactive prompt unless you are in a controlled non-interactive setting (CI runners with secret-backed env, etc.).
cws profile onboard <name> additionally reads the following when set,
skipping the matching inquirer prompt:
WALLET_NETWORK—localnet|testnet|mainnet.WALLET_USERNAME— Keycloak username the profile will authenticate as.WALLET_ONBOARDING_SECRET— one-shot onboarding secret minted by an admin viaPOST /onboarding-secrets.--yesadditionally skips the prepared-submission confirm so the flow runs end-to-end without a TTY. The same security caveat asWALLET_PASSWORDapplies; intended for test scripts and CI.
--password <value> is accepted on the argv but prints a warning on every
invocation because ps(1) leaks argv to other users on multi-user hosts.
Security model
- Single encrypted keystore per profile. The keystore envelope contains both the cws-api API key and the signing-key material (P-256 by default; secp256k1 supported).
- KDF: Argon2id with per-profile salt. Cipher: AES-256-GCM (authenticated encryption; tampering fails decryption instead of yielding silent garbage).
- Interactive mainnet mutations require typed-token confirmation: the
operator types the first 6 characters of the multisig party id.
--yesshort-circuits typed-token confirmation on every network, including mainnet — intended for CI and headless automation. The same security caveat asWALLET_PASSWORDapplies (env/argv visibility). --passwordon argv emits a warning. Argv is visible tops.- No session cache in v1. Every command that signs re-prompts for the password.
- Password zeroing is best-effort: JavaScript strings are immutable, so only
Buffer copies of the password inside
keystore.tsare actively zeroed.
Exit codes
Categories map to exit codes via EXIT_CODES in src/core/output.ts:
| Category | Code | |-------------|------| | unknown | 1 | | auth | 2 | | cancelled | 0 | | config | 3 | | io | 4 | | network | 5 | | safety | 6 | | signing | 7 | | validation | 8 |
cancelled deliberately exits 0 so shell pipelines treat user-aborted
confirmations as a successful no-op; the reason still surfaces in stderr.
Troubleshooting
- Unlock fails with decryption error — wrong password. There is no recovery path; if the password is lost, remove the profile and re-import the signing key.
- Keycloak 401 / 403 — API key expired or bound to the wrong realm.
Re-provision from the cws-api admin and
cws profile importa refreshed config, or edit the profile's encrypted keystore via theprofile createflow. - Localnet cannot reach cws-api — localnet profiles require both
baseUrlandkeycloak.urlto be set explicitly; the defaults only cover mainnet and testnet. Check the values withcws profile list --json.
TUI tour
cws (no args) or cws ui launches the terminal UI:
profile-picker → unlock → primary dashboard ⇄ account picker ⇄ multisig dashboardAfter unlock, the TUI lands on the active profile's primary wallet account and
attempts to show holdings inline. Multisigs are available from the account
picker and are treated as switchable first-party accounts rather than a
separate mode. If cws-api cannot load holdings because its Ledger API
dependency is unavailable, the dashboard stays usable and explains the
limitation. Press H for the full holdings view.
Primary-account dashboard hotkeys:
s— send transferA— accept transferP— manage transfer preapprovalsM— manage automerge delegationsi— browse instrumentsH— open full holdings viewx/Esc— switch accountsq— quit (suppressed while a text field has focus;Ctrl-Calways quits)
Multisig dashboard hotkeys:
s— send transfer proposalp— propose topology changea— approve pending itemr— reject pending itemi— browse instrumentsH— open full holdings viewx/Esc— switch accountsq— quit (suppressed while a text field has focus;Ctrl-Calways quits)
Mainnet safety: in interactive use, before any mutation submits, the TUI
asks you to type the first 6 characters of the multisig party id. --yes
short-circuits this prompt on every network — automation paths are
responsible for supplying it deliberately.
Links
- Source: https://github.com/0xsend/canton-monorepo/tree/dev/packages/cws-cli
- Website: https://send.it
License
MIT
