Wallet facade + CLI plan (v0.3)¶
Status: approved 2026-04-30. Implementation pending.
Goal: make pyrxd usable in 5 minutes from pip install to first Glyph mint without writing Python.
Approved decisions¶
These are settled. Rationale is in wallet-cli-decisions.md.
# |
Decision |
Choice |
|---|---|---|
1 |
CLI library |
click |
2 |
Binary layout |
one |
3 |
|
independent ( |
4 |
Glyph metadata |
file-only, with a |
5 |
Default network |
mainnet, with confirmation prompts on every destructive action |
6 |
Mnemonic display |
stdout with an Enter gate; user is responsible for terminal hygiene |
Scope: narrow and Glyph-focused¶
This plan is informed by radiant-core-wallet-research.md and scope-decision-2026-04-30.md. Summary:
Radiant Core ships a complete Bitcoin-Core-style wallet (
sendtoaddress, HD support, encryption, PSBT, multisig, etc.). Anyone running a node has those capabilities already.Radiant Core’s wallet does not understand Glyph tokens. Glyph script outputs are classified
nonstandard;listunspentreturns no token metadata;sendtoaddresswon’t auto-spend a Glyph UTXO.pyrxd’s value isn’t a better plain-RXD wallet — it’s the application-layer Glyph + Gravity tooling that doesn’t exist in the node.
In scope for v0.3:
HdWallet.send()/send_max()— close a real library API gap.A
pyrxdCLI focused on Glyph operations + minimal onboarding, not a full wallet replacement.An onboarding flow (
pyrxd setup) that helps users navigate the Radiant ecosystem without bundling its components.
Out of scope for v0.3 (deferred or skipped):
pyrxd send/send-max/build-tx/broadcast— duplicatesradiant-cli sendtoaddressfor node users; doesn’t move the needle for non-node users (they want Glyph features). May add later if onboarding friction is real.Bundling a Radiant Core node, replacing
radiant-cli, shipping a TUI/GUI — seescope-decision-2026-04-30.md.Hardware wallet integration, multi-account beyond
account=0, watch-only xpub mode, transaction history view,pyrxd gravity *subcommands. All v0.4+.
What gets built¶
Library layer¶
Net-new: nothing¶
RxdWallet already exists in src/pyrxd/wallet.py with send, send_max, get_balance, get_utxos. It works.
HdWallet exists in src/pyrxd/hd/wallet.py with derivation, gap-limit scanning, and encrypted save/load. It works.
GlyphBuilder and FtUtxoSet exist in src/pyrxd/glyph/. They work.
Single targeted edit: add HdWallet.send()¶
Add three methods to HdWallet:
def build_send_tx(self, to_address, photons, *, fee_rate=None, change_strategy="next_internal") -> Transaction:
"""Offline: build the unsigned tx. No network calls."""
def build_send_max_tx(self, to_address, *, fee_rate=None) -> Transaction:
"""Offline: build the sweep tx. No network calls."""
async def send(self, to_address, photons, client, *, fee_rate=None) -> str:
"""Build, sign, broadcast. Returns broadcast txid."""
async def send_max(self, to_address, client, *, fee_rate=None) -> str:
"""Sweep entire wallet balance. Returns broadcast txid."""
These mirror the RxdWallet pattern. Internally:
Collect spendable UTXOs across all known external + internal addresses (the existing
addressesdict fromrefresh()).Look up the corresponding signing key for each UTXO via
_xprv.derive(change/index).Build inputs with
unlocking_script_template = P2PKH().unlock(privkey).Pick change address: next unused internal index by default (the existing scan tracks
internal_tip).Compute fee with the existing two-pass pattern: trial-sign → measure bytes → rebuild with real change.
Reset all input
unlocking_scriptvalues between passes (the stale-signature pitfall documented intests/test_preimage.py).
This is ~150 lines following the exact pattern in RxdWallet. No novel cryptography — multi-key UTXO assembly only.
The send code is purely additive; it doesn’t touch any existing method. New code lives at the bottom of the class. Existing save/load/encryption/scan paths are not modified.
CLI layer (all new)¶
Files under src/pyrxd/cli/:
src/pyrxd/cli/
__init__.py # package marker
__main__.py # `python -m pyrxd` entry; defers to main:cli
main.py # @click.group, top-level entry, global options
context.py # CliContext dataclass — holds network, fee_rate, json/quiet flags, wallet path, electrumx URL
format.py # output helpers: human/JSON/quiet modes, photon formatting, color
errors.py # CLI-side exception types + exit-code mapping
prompts.py # confirmation prompts, mnemonic display with Enter gate
config.py # ~/.pyrxd/config.toml read/write
wallet_cmds.py # @cli.group: wallet new, wallet load, wallet info, wallet export-xpub
glyph_cmds.py # @cli.group: glyph mint-nft, deploy-ft, transfer-ft, list, init-metadata
query_cmds.py # bare commands: address, balance, utxos
setup_cmd.py # `pyrxd setup` — onboarding flow
Tests under tests/cli/:
tests/cli/
__init__.py
conftest.py # fixtures: tmp_path-based wallet dir, mocked ElectrumX, fake config
test_main.py # top-level: --help, --version, unknown subcommand, --json flag plumbing
test_format.py # snapshot tests for default/JSON/quiet output
test_prompts.py # mnemonic display gate, confirmation flow, --yes bypass
test_config.py # config.toml read/write, precedence (flags > env > config > defaults)
test_wallet_cmds.py
test_glyph_cmds.py
test_query_cmds.py
test_setup_cmd.py
Tooling wire-up (small touches)¶
pyproject.toml: add[project.scripts]entry forpyrxd = "pyrxd.cli.main:cli". One section, no risk to anything else.pyproject.toml: addclick = "^8.1"to[tool.poetry.dependencies].src/pyrxd/__init__.py: re-exportRxdWalletandHdWalletsofrom pyrxd import RxdWalletworks.README.md: add a “CLI quick start” section after the existing Python quick start. Do not remove the Python examples.docs/how-to/cli.md(new): full CLI reference.
Command surface¶
The pyrxd CLI is verb-first. Global flags apply to every subcommand.
Global flags¶
--network {mainnet,testnet,regtest} default: mainnet
--electrumx URL override; otherwise from config or built-in default
--wallet PATH path to encrypted HdWallet file (default: ~/.pyrxd/wallet.dat)
--json machine-readable output
--quiet suppress progress; print only the bare result
--no-color disable ANSI color
--config PATH alternate config file
--yes / -y skip confirmation prompts (required for `--json` on destructive ops)
Wallet management¶
pyrxd wallet new [--mnemonic-words 12|24] [--passphrase-prompt]
Generate a fresh BIP39 mnemonic + HdWallet. Prints mnemonic ONCE,
waits for Enter, then saves the encrypted wallet to --wallet path.
pyrxd wallet load
Validate that an existing wallet decrypts. Prompts for mnemonic
(hidden via getpass).
pyrxd wallet info
Print account index, address counts, last-used external/internal
indices. No network.
pyrxd wallet export-xpub
Print the account-level xpub (m/44'/236'/<account>') for watch-
only use. No private key material.
Address & balance (light wallet ops)¶
These exist not because pyrxd should replace radiant-cli, but because a user with no node still needs the basics to onboard. They’re the minimum surface to go from “fresh install” to “I have an address and know my balance.” Spending is intentionally not in this set — see scope notes above.
pyrxd address [--next | --index N] [--change]
--next (default) first external address with no history at its
P2PKH script hash or its token script hashes (#787);
runs the gap-limit scan first, not saved; exits 2 if the
scan cannot read an address
--index N deterministic index lookup, no network
--change internal chain instead of external
pyrxd balance [--refresh]
Print confirmed/unconfirmed photon balance across the wallet.
Always runs a gap-limit scan via ElectrumX first; --refresh, which
used to turn it on, is accepted and changes nothing.
pyrxd utxos [--min-photons N] [--addr ADDRESS]
List UTXOs (table or --json). Read-only diagnostic.
Glyph operations¶
This is where pyrxd differentiates. None of these have node-wallet equivalents.
pyrxd glyph init-metadata [--type {nft,ft,dmint-ft,mutable,container}] [--out FILE]
Scaffold a metadata.json template appropriate for the token type.
Default writes to stdout; --out writes to a file.
pyrxd glyph mint-nft <metadata.json>
Two-tx commit/reveal NFT mint. Polls confirmation between txs.
Prompts for confirmation before each broadcast unless --yes.
pyrxd glyph deploy-ft <metadata.json> --supply N --treasury ADDRESS
Deploy an FT premine. Single recipient receives the entire supply
at vout[0] of the reveal tx.
pyrxd glyph transfer-ft <ref> <amount> --to ADDRESS
Transfer FT units of <ref> (txid:vout) to ADDRESS. Builds a
conservation-enforcing transfer via FtUtxoSet.
pyrxd glyph transfer-nft <ref> --to ADDRESS
Transfer an NFT singleton.
pyrxd glyph list [--type {nft,ft}]
Gap-limit scan, then scan each used address for Glyph holdings via GlyphScanner.
Default human table; --json for scripting.
Onboarding¶
pyrxd setup
Interactive walkthrough:
1. Detect Radiant Core node locally; if missing, print install steps.
2. Detect ElectrumX in config; if missing, list known public servers.
3. Detect a wallet in --wallet path; if missing, run `pyrxd wallet new`.
Goal: takes a fresh install to "ready to mint a Glyph" in <5 minutes
without bundling anything.
Confirmation prompts¶
Every command that broadcasts (or builds-and-broadcasts) prints a summary and asks y/N before sending. The summary always shows:
net amount out
fee
recipient
change address
network
Skip with --yes. With --json (machine-readable mode), --yes is required for destructive ops; otherwise the command exits with code 1 and prints error: --json requires --yes for destructive operations.
Exit codes¶
0 success
1 user-error (bad input, file not found, insufficient funds, missing --yes in JSON mode;
in `pyrxd headers sync`, also a local header cache that could not be written)
2 network error (couldn't reach ElectrumX, broadcast rejected; in `pyrxd verify`, also a
server whose own block proof contradicts the height it reported for the mark; in
`pyrxd headers sync`, also servers whose answers refused the sync: too few operators,
a disagreement, a header that fails a check)
3 wallet decryption failed
4 unexpected error (bug — should not happen)
5 a verdict that does not hold (`pyrxd verify`): no single HashMark record in the
transaction passes every check asked of it (the signature, `--file`/`--digest` and
`--wave-name` must all hold for the SAME record), a record anywhere in it does not decode
or its signature does not verify, a block shallower than the floor the caller set, or a
question that was asked and could not be answered (`--wave-name` NOT ESTABLISHED, or a
`--file`/`--digest` with CANNOT COMPARE because the record is one this build cannot read).
The full report is still printed on stdout; only the status says no.
6 `pyrxd headers sync` stopped at a header below its floor. The status is still printed;
its reason says what gets past that header (a re-run, `--reset`, or only a newer release).
7 `pyrxd headers sync` added nothing only because some operators reported a tip at least 6
blocks below the others' (headers are cached only below the LOWEST tip), and those operators
are a strict minority of the operators that answered (an unreachable one counts for neither
side). The status names them, with each tip's count. A smaller lag (honest
servers differ by a block or two just after each block) is named in the note with exit 0.
Tips that differ when the low tip is not a strict minority (with two answering, always) are
listed with exit 0 and no one is named: a tip height is unauthenticated.
Configuration¶
~/.pyrxd/config.toml, created on first run:
network = "mainnet"
electrumx = "wss://electrumx.radiant4people.com:50022/"
fee_rate = 10000 # photons/byte
wallet_path = "~/.pyrxd/wallet.dat"
[networks.testnet]
electrumx = "wss://testnet-electrumx.example.com/"
Precedence (highest wins): CLI flags > environment variables (PYRXD_*) > config file > built-in defaults.
Storage permissions: ~/.pyrxd/ is 0700, wallet.dat is 0600, matching the existing HdWallet.save() invariants.
Mnemonic UX¶
pyrxd wallet new:
Generate the mnemonic with
secrets-backed entropy (existingfrom_mnemonicpath).Print the mnemonic to stdout once, in a clearly-flagged box:
╔════════════════════════════════════════════════════════════╗ ║ Recovery mnemonic — write this down, then never share it. ║ ║ pyrxd will NOT show this again. ║ ╚════════════════════════════════════════════════════════════╝ word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12 Press Enter once you have written it down.
Wait for Enter.
Optionally prompt for a BIP39 passphrase (can be empty; never echoed).
Save the wallet to
--walletpath with0600permissions.
pyrxd wallet load:
Prompts for mnemonic (input hidden via
getpass).Decrypts; on failure exits with code 3 and a generic message — never echoes the user’s input.
The user is responsible for terminal hygiene (no shared screens, no tee/grep piping). We document this clearly. See wallet-cli-decisions.md §6 for why we didn’t go with a temp file.
Output formats¶
Default output is human-readable, narrow-terminal-friendly:
$ pyrxd balance
Address 1A2b3c...xyz (next receive: 1X9y8z...abc)
Confirmed 1,234,567,890 photons (12.34567890 RXD)
Unconfirmed 0 photons
--json produces structured output for scripting:
{
"network": "mainnet",
"address": "1A2b3c...",
"next_receive": "1X9y8z...",
"confirmed_photons": 1234567890,
"unconfirmed_photons": 0
}
--quiet suppresses everything except the bare result of the command. For pyrxd send (when added) it prints the txid; for pyrxd balance it prints just the confirmed photon count.
Error messages¶
The CLI is the user’s first impression. Every error follows this pattern:
error: <short description>
cause: <what went wrong, with sensitive values redacted>
fix: <one-line concrete next step>
Example:
error: insufficient funds
cause: requested 100,000,000 photons but wallet has 50,000,000 confirmed
fix: try a smaller amount or wait for unconfirmed deposits to confirm
The library still raises typed exceptions (ValidationError, NetworkError, KeyMaterialError). The CLI catches them at the boundary and reformats. Raw Python tracebacks never reach the user unless they pass --debug.
Testing strategy¶
Unit tests (most of the budget): each command is a click callable. Tests use
click.testing.CliRunnerwith a mockedCliContext(mocked ElectrumX, tmp_path-backed wallet dir, fake config). No network, no real keys, no real filesystem.End-to-end fixture tests: spawn the CLI as a subprocess against a fake ElectrumX server. Cover the happy path of
wallet new,wallet load,address,balance,glyph mint-nft.Snapshot tests for output formatting: small fixtures, render in all three modes (default /
--json/--quiet), compare to checked-in snapshot files. Catches accidental output changes early.Coverage target: 90%+ on
pyrxd/cli/. This is what users see first; a regression here is highly visible.Existing test suite: all 2051 existing tests must still pass after
HdWallet.send()is added.
Phasing (three cuts, three PRs)¶
Cut 1: foundation — first PR¶
The minimum viable CLI that demonstrates the architecture works end-to-end. Lands first; subsequent cuts can ship after it stabilizes.
In scope:
HdWallet.send(),send_max(),build_send_tx(),build_send_max_tx()(library — used internally by glyph deploy-ft / transfer-ft for change handling).CLI infrastructure:
main.py,context.py,format.py,prompts.py,errors.py,config.py.Subcommands:
wallet new,wallet load,wallet info,address,balance.All tests for the above.
[project.scripts]entry,pyproject.tomlclick dep,__init__.pyre-exports.README CLI section,
docs/how-to/cli.mdskeleton.
Out of scope for Cut 1: glyph commands, utxos command, setup command, export-xpub.
Cut 2: glyph commands — second PR¶
The differentiation. Adds:
glyph init-metadata,glyph mint-nft,glyph deploy-ft,glyph transfer-ft,glyph transfer-nft,glyph list.All tests for the above.
docs/how-to/cli.mdglyph section.README example: “mint your first NFT” walkthrough.
Cut 3: polish + onboarding — third PR¶
Quality-of-life additions:
pyrxd setupinteractive flow.pyrxd utxos,pyrxd wallet export-xpub.Shell completion script (click can generate it; ship it as
share/completion/pyrxd.bash).Migration helper: detect
~/.pyrxd/from older versions, upgrade format if needed.
Cuts can ship as separate PRs over multiple weeks. Cut 1 alone is releasable as v0.3.0; Cuts 2 and 3 land as v0.3.1 and v0.3.2 (or a single v0.3 release if all three are ready in time).
Risk assessment¶
Risks¶
Click ergonomics turn out to feel heavy as the surface grows. Mitigation: Cut 1 is the test. If subcommand modules feel cluttered, refactor before Cut 2.
The
HdWallet.send()edit introduces a regression. Mitigation: pure additive code, all 2051 existing tests must stay green, plus new send-specific tests.Mnemonic display has a UX flaw on a platform we don’t test (Windows? VS Code terminal?). Mitigation: document the box-drawing fallback for terminals that can’t render Unicode. Test on macOS/Linux/Windows in CI.
Users miss the “this is Glyph-focused, not a full wallet” framing and ask “where’s
pyrxd send?” Mitigation:pyrxd --helptop-level message and README explicitly call this out. If the question becomes recurring after Cut 2, add a basicpyrxd sendin v0.3.x.pyrxd setupinteractive prompts surprise CI users. Mitigation: every prompt has a non-interactive fallback (env var or flag);pyrxd setup --no-interactiveskips and just emits a config file.
What we’re explicitly accepting¶
Some users with nodes will install pyrxd and find it doesn’t have
pyrxd send. That’s fine — they can useradiant-cli sendtoaddress. The README will say so.Users without nodes have to use
pyrxd glyph deploy-ft(etc.) which internally handles RXD; they don’t needpyrxd sendbecause their flows are token-centric.Power-user features like multi-account, watch-only mode, hardware wallet, gravity subcommands are deferred to v0.4+.
Estimated effort¶
Cut 1: ~3-5 days of focused work. Most of the time is in formatting, error messages, and tests; the actual command logic is thin.
Cut 2: ~3-5 days. Glyph subcommands are more complex (commit/reveal, two-tx flows, scan integration) but each maps directly to existing builder methods.
Cut 3: ~2-3 days. Polish work; smaller scope per item.
Total: ~8-13 working days for the full v0.3 wallet/CLI release.
Open implementation questions¶
These are details to settle while writing code, not show-stoppers:
Click
Context.objvs. dataclass passed explicitly? Click’s idiom isContext.obj. Keeping a customCliContextdataclass and passing it explicitly is more testable. Lean: explicit dataclass, even if it costs a bit of click idiom.How does
pyrxd setupknow if a Radiant Core node is “available”? Probe127.0.0.1:7332(default RPC port) with agetblockchaininfocall? Look for~/.radiant/? Lean: try the RPC probe with a 1s timeout; only fall back to filesystem hints if the user opts in.Confirmation prompts in
--quietmode?--quietis for output. Prompts are user input. They’re orthogonal —--quietdoesn’t suppress prompts. Document this clearly.pyrxd glyph init-metadataoutput: pretty-printed or canonical? Pretty-printed for human readability (this is a scaffold, not on-chain bytes). The actual on-chain CBOR encoding stays canonical.Where does
pyrxd glyph deploy-ftget its funding UTXO? Same wallet that signs the deploy. Sweep enough RXD viaHdWallet’s utxo selection. Document the minimum funding requirement.
These get answered during Cut 1 implementation by writing the code.