Skip to content

[VIBE DRAFT - do not implement] console-cli: command-line interface for Polkadot Bulletin Chain #458

@karolk91

Description

@karolk91

console-cli: command-line interface for Polkadot Bulletin Chain

Summary

Build console-cli — a CLI tool that exposes every workflow console-ui offers (store, download, renew, authorize, query, faucet, status) — backed by @parity/bulletin-sdk. Starting point is the CLI half of #135 (with the web frontend stripped out). Scope, decisions, and risks below were vetted in a plan-review pass.

Goals

  • Cover every workflow console-ui supports, scriptable and headless.
  • Reuse @parity/bulletin-sdk as the single source of truth — no duplicated chain logic.
  • Same network configs as console-ui (Westend, Paseo, local, previewnet).
  • Two output modes: human-readable tables and --output json / NDJSON for piping.

Non-goals

  • No reactive state (RxJS) — CLI is request/response.
  • No browser wallet extensions.
  • No persistent history UI — defer to shell history + JSON output.

What already exists (reuse, don't rebuild)

Asset Reuse plan
#135 examples/bulletin-helia/src/cli.ts Promote to console-cli/src/commands/download.ts, wrap in commander
#135 src/ipfs.ts (319 lines) Kept as Helia retrieval impl behind a new RetrievalClient interface
#135 logger-base.ts + logger-cli.ts Adopted; replaces ora/chalk
#135 tsconfig.cli.json Promoted to tsconfig.json
examples/authorize_and_store_papi*.js, store_*.js, cid_dag_metadata.js, native_ipfs_dag_pb_chunked_data.js, typescript/authorize_and_store.js Logic patterns reused; scripts retired post-launch
console-ui/src/config/networks.ts Copied into CLI (decision: copy over shared package — see below)
console-ui/src/state/storage.state.ts read patterns Reference for query commands
@parity/bulletin-sdk Sole interface to chain ops; CLI is a thin shell
examples/authorize_and_store_papi_smoldot.js Preserved via --smoldot global flag

#135's frontend half (index.html, main.ts, style.css, logger.ts, web tsconfig.json, vite deps) is dropped.

Out of scope

  • Shared packages/bulletin-config workspace package — chose to copy networks.ts (~50 lines, rarely changes; revisit on drift).
  • Full keystore (HD paths, lockout, OS keychain) — deferred to TODOs; Phase 2 ships an env/stdin signer.
  • bulletin daemon (keep-warm Helia/PAPI) — multi-CID download covers the batch case.
  • Streaming chunker — belongs in SDK; current 64 MiB ceiling acceptable.
  • Re-implementing examples/upgrade_runtime.js (operator-only set_code) and examples/typescript/export_*.js (bulk export) — kept as standalone scripts.

Design decisions

# Decision Rationale
D1 RetrievalClient interface defined in Phase 0; Helia wrapped behind it Locks the seam now so smoldot retrieval can swap in cleanly when SDK ships it
D2 Copy networks.ts into CLI; no shared package Minimal diff; avoids cross-cutting refactor of working console-ui
D3 Signer precedence: --accountBULLETIN_SEED env → stdin prompt. No --seed flag. CLI flags leak via ps/shell history; remove the worst-leak path
D4 withClient(opts, async client => …) helper used by every command; retries on transient WS disconnect, fails on persistent Deterministic teardown; prevents 12+ inline try/finally blocks; handles WS-drop mid-multi-step
D5 Retire 7 fully-covered examples/*.js scripts as their CLI command lands; keep 4 specialty scripts (smoldot demo, runtime upgrade, chopsticks check, bulk export) One canonical implementation per workflow
D6 runCommand(commandFn) wrapper in Phase 1 — error → exit code, SIGINT cleanup, output mode switching Avoids rewriting all commands in a final hardening phase
D7 Phase 2 ships env/stdin signer only; full keystore deferred to TODO Hidden complexity (encryption, lockout, HD paths, file perms, concurrent access) doesn't fit a 0.5-day phase
D8 One print(obj, mode) formatter; commands return plain objects; per-command human override only when default tabulation looks bad One formatter, no drift between human/JSON modes
D9 download --timeout <seconds> flag, default 30s Without it, dead-peer dial hangs forever — silent failure in cron
D10 Detect process.stdin.isTTY === false; fail fast with "no signer; set BULLETIN_SEED" Prevents silent stdin-prompt hang under cron/CI
D11 Unit-heavy + zombienet smoke tests per phase Unit covers branching/error mapping; zombienet confirms tx + retrieval end-to-end
D12 Promise.all([...]) for independent chain reads in status / query 4× RTT → 1× RTT on Westend
D13 Validate file size before reading; clear error if >64 MiB SDK ceiling Fail at boundary; capture streaming chunker as TODO in SDK
D14 Multi-CID download <cid1> <cid2> … form, batched via Bitswap wantlist (≤16 CIDs/request) Protocol-level batching, not just one Helia init

Architecture

                                  ┌─────────────────────────────────────┐
                                  │           bulletin <cmd>            │
                                  │           (commander root)          │
                                  └─────────────────┬───────────────────┘
                                                    │
                                          runCommand() wrapper           ← D6
                                                    │
                  ┌──────────┬──────────┬───────────┼───────────┬─────────────┬──────────┐
                  ▼          ▼          ▼           ▼           ▼             ▼          ▼
              download    store      renew     auth account  query block   prepare   status
              (P0)        (P3)       (P4)      preimage      tx cid auth   (P3)      (P1)
                                                refresh      (P1)
                                                remove
                                                (P4)
                                                    │
                  ┌────────────────┐                │                 ┌──────────────────┐
                  │ RetrievalClient│ ◄─ download    │ withClient()    │  signer.ts       │
                  │   iface (P0)   │                │ (PAPI WS) ← D4  │  env→stdin (P2)  │
                  │     │          │                │  │              └──────────────────┘
                  │     ▼          │                │  ▼
                  │  ipfs.ts       │                │  AsyncBulletinClient (SDK)
                  │  (Helia/P2P)   │                │   .store / .renew / .authorize…
                  └────────────────┘                │   builders → tx submit → events
                                                    │
                                            ┌───────▼────────┐
                                            │ ProgressEvent  │
                                            │ → logger-cli   │
                                            └────────────────┘
                                                    │
                                            ┌───────▼────────┐
                                            │ print(obj,mode)│ ← D8
                                            │  human / json  │
                                            └────────────────┘
                                                    │
                                          ┌─────────▼──────────┐
                                          │ exit code mapping  │
                                          │  BulletinError →   │
                                          │  64/69/75/…        │
                                          └────────────────────┘

Proposed layout

console-cli/
├── package.json                # bin: { "bulletin": "./dist/cli.js" }
├── src/
│   ├── cli.ts                  # commander entry, registers all commands
│   ├── commands/
│   │   ├── store.ts            # store (signed + preimage)
│   │   ├── download.ts         # P2P fetch via RetrievalClient
│   │   ├── renew.ts
│   │   ├── auth/               # authorize / refresh / remove-expired
│   │   ├── query/              # cid, tx, authorization, block
│   │   ├── prepare.ts          # offline CID/chunk preview
│   │   ├── faucet.ts           # testnet auth grants
│   │   └── status.ts
│   ├── lib/
│   │   ├── client.ts           # withClient() — D4
│   │   ├── runCommand.ts       # error/exit-code/SIGINT wrapper — D6
│   │   ├── retrieval.ts        # RetrievalClient interface — D1
│   │   ├── retrieval/helia.ts  # lifted from #135's ipfs.ts
│   │   ├── signer.ts           # env → stdin — D3
│   │   ├── output.ts           # print(obj, mode) — D8
│   │   └── progress.ts         # SDK ProgressEvent → logger-cli
│   ├── config/
│   │   └── networks.ts         # copied from console-ui — D2
│   └── logger-base.ts, logger-cli.ts  # from #135
└── test/

Phased plan

Phase 0 — Strip #135 frontend + retrieval interface

  • Cherry-pick examples: Fetch from Bulletin node directly via P2P #135. Drop frontend (index.html, main.ts, style.css, logger.ts, web tsconfig, vite deps).
  • Move to console-cli/. Promote tsconfig.cli.jsontsconfig.json.
  • (D1) Define RetrievalClient interface; wrap ipfs.ts as HeliaRetrievalClient.
  • (D9) Add --timeout <seconds> flag to download, default 30s.
  • (D2) Copy networks.ts into CLI; add comments cross-referencing console-ui copy.
  • Wire commander skeleton; register download as first subcommand.

Exit criteria: bulletin download <cid> --network westend works without explicit multiaddrs; --timeout honored.

Phase 1 — Read-only queries + error/lifecycle wrappers

  • (D4) lib/client.ts exports withClient(opts, async client => …) — connects, retries on transient WS disconnect, tears down on success/throw/SIGINT.
  • (D6) lib/runCommand.ts wraps every commander action: parse → withClient → command → format → exit-code.
  • (D8) lib/output.ts exports print(obj, mode); default tabulation via cli-table3, JSON via stringify, NDJSON for streaming.
  • (D12) Promise.all([...]) for independent reads in status and query.
  • Commands: query block, query tx, query cid, query authorization, status.
  • Global flags: --network, --rpc, --account, --output {human|json}, --smoldot, --verbose.

Phase 2 — Simple signer

  • (D3) lib/signer.ts resolves: BULLETIN_SEED env → stdin prompt. No --seed flag.
  • (D10) Detect process.stdin.isTTY === false; fail fast.
  • BIP39 mnemonic validation via @polkadot/util-crypto.

Phase 3 — Storage uploads

  • bulletin store <file> with --codec, --hash, --chunk-size, --wait, --preimage.
  • (D13) Validate file size before reading; fail clearly if >64 MiB.
  • bulletin prepare <file> (offline BulletinPreparer).
  • progress.ts bridges SDK ProgressEventlogger-cli; NDJSON in --output json.
  • (D5 — partial) Retire examples/authorize_and_store_papi*.js, store_*.js, cid_dag_metadata.js, native_ipfs_dag_pb_chunked_data.js, typescript/authorize_and_store.js.

Phase 4 — Renew + Authorization writes

  • renew <block> <index>.
  • auth account|preimage create / refresh / remove-expired, --sudo flag.

Phase 5 — Faucet + multi-CID download + UX polish

  • bulletin faucet.
  • (D14) Multi-CID download <cid1> <cid2> ... form, batched via Bitswap wantlist (≤16 CIDs/request); reuses one Helia init.
  • --interactive mode (@inquirer/prompts) for store / auth account.
  • Shell completion (bulletin completion bash|zsh|fish).

Phase 6 — Hardening

  • BulletinError → exit codes finalized (slots into runCommand from Phase 1).
  • Integration tests via zombienet (per D11).
  • README, man pages from commander.

Test plan

# Codepath Type
T1 runCommand: success / BulletinError → exit code / unknown → exit 1 / SIGINT cleanup Unit
T2 withClient: connect, run, destroy on success and on throw; retry on transient WS disconnect Unit
T3 RetrievalClient interface — Helia impl pluggability Unit
T4 download — CID parse error path Unit
T5 download — peer dial timeout (D9) Integration
T6 download — raw + dag-pb fetch Integration (zombienet)
T7 signer.ts — env → stdin precedence; non-tty fail-fast Unit
T8 signer.ts — invalid mnemonic Unit
T9 store — empty file → ErrorCode.EMPTY_DATA Unit
T10 store — chunked path (>2 MiB), manifest CID Integration
T11 store — tx timeout Integration (chopsticks)
T12 store --preimage — unsigned tx Integration
T13 auth account/preimage happy paths Integration
T14 auth refresh/remove-expired both scopes Integration
T15 --sudo flag wraps in sudo call Unit
T16 query block/tx/cid/authorization reads Integration
T17 query authorization for non-authorized address — clear "not authorized" UX Unit
T18 prepare — offline CID matches what store would produce Unit
T19 print(obj, mode) — human / JSON / NDJSON snapshots Unit
T20 --output json — every command emits valid JSON to stdout, errors to stderr Unit
T21 faucet — testnet endpoint contract Integration

Failure modes

# Failure Test Handling User sees
F1 download — dead peer T5 D9 --timeout 30s default Clear error
F2 store — RPC drop mid-tx T11 SDK txTimeout 7 min Clear error after wait
F3 WS disconnect mid-multi-step T2 (extended) D4 transient retry Clear error if persistent
F4 stdin prompt under non-tty T7 D10 isTTY check Clear error
F5 query auth for non-authorized address T17 SDK returns null "not authorized" message
F6 insufficient balance for fees extend T13 SDK surfaces; D6 maps Clear error
F7 FS read error mid-stream extend unit Node throws; runCommand catches Visible
F8 --smoldot cold-sync (minutes) n/a doc only Slow but not silent

No silent-failure gaps remain.

Code-comment ASCII diagrams (per repo convention)

File Diagram
src/lib/client.ts withClient lifecycle: connect → run → finally(destroy); transient retry path
src/lib/retrieval.ts Helia today / smoldot tomorrow — interface seam
src/lib/runCommand.ts Pipeline: parse → withClient → cmd → format → exit-code; SIGINT branch
src/commands/status.ts Read fan-out: Promise.all([finalized, auth, balance])

Deferred

  • Full keystore — HD paths, lockout, OS keychain integration. MVP env/stdin signer ships first.
  • Smoldot retrieval via bitswap_block RPC — replaces Helia behind the same RetrievalClient interface when SDK ships retrieval.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions