dig-node-control-interface
The canonical client ⇄ dig-node CONTROL interface contract. One ecosystem definition of the
management/query surface a client (the CLI dign, the browser extension, dig-app, hub) uses to
control and query a running dig-node — status, config, cache, hosted/pinned stores, §21 sync, the peer
network, subscriptions, the auto-update beacon, live log level, and control-token pairing — so the
client side and the node side can never silently drift. Transport-agnostic: rides the
dig-ipc-protocol local session, or loopback-mTLS + a signed control token over HTTP/WebSocket for
clients that aren't on the local pipe.
- License: Apache-2.0 OR MIT
- Spec:
SPEC.md(normative) — this README is the at-a-glance interface reference.
[]
= "0.2"
Usage
Client side — build a typed request, carry it over your transport, parse the typed result:
use ;
let call = SetCapParams ;
let request = build_request; // → control.cache.setCap envelope
// ... send `request` over dig-ipc / loopback-mTLS, receive `response` ...
# let response = success;
let result = ?; // → SetCapResult { cap_bytes }
# Ok::
Node side — implement ControlHandler (one typed method per control method); the provided dispatch
routes a raw request to the right method and builds the response.
Authorization
Every control.* method is token-gated unless the tables below mark it —: present the node's local
control token as the X-Dig-Control-Token header (preferred) or a params._control_token field.
Open today are pairing.request / pairing.poll (a token-less client uses them to obtain a scoped
token after local operator approval), the wallet CHAIN READS (they need only public chain data — an
address or a coin id, never a key), and control.peerCounts (two integers about this node's own
connectivity). The three control.pairing.* admin methods require the MASTER token specifically.
UNAUTHORIZED from an OPEN method means the node predates it — upgrade the node. From
control.wallet.broadcast it means what it says — find the token. See SPEC.md §4.2.
Full interface reference
Every control method, for LLM/agent lookup without reading code. Auth: T = requires the control
token, M = requires the MASTER token, — = open. Route: how the node resolves it — own
(service shell), del (engine), open (bootstrap).
Status / config / log
| Method | Auth | Route | Params | Result |
|---|---|---|---|---|
control.status |
T | own | — | {running, service, version, commit, protocol, uptime_secs, addr, upstream, cache:{cap_bytes,used_bytes,dir,shared}, hosted_store_count, cached_capsule_count, pinned_store_count, sync:{available}} |
control.config.get |
T | own | — | {addr, port, upstream, upstream_override:string|null, cache_dir, cache_shared, config_path, sync_available} |
control.config.setUpstream |
T | own | {upstream:string} |
{upstream, requires_restart:true} |
control.log.setLevel |
T | own | {filter:string} (an EnvFilter directive) |
{filter} |
Cache
| Method | Auth | Route | Params | Result |
|---|---|---|---|---|
control.cache.get |
T | own | — | {cap_bytes, used_bytes, dir, shared} |
control.cache.setCap |
T | own | {cap_bytes:u64} |
{cap_bytes} (floored at 64 MiB) |
control.cache.clear |
T | own | — | {cleared:true} |
Hosted / pinned stores
| Method | Auth | Route | Params | Result |
|---|---|---|---|---|
control.hostedStores.list |
T | own | — | {stores:[{store_id, pinned, capsule_count, total_bytes, capsules:[{capsule,root,size_bytes,last_used_unix_ms}]}]} |
control.hostedStores.pin |
T | own | {store:"storeId[:root]"} |
{store_id, root:string|null, pinned:true, fetch:{status,…}} |
control.hostedStores.unpin |
T | own | {store} |
{store_id, unpinned:bool, evicted_capsules:u64} |
control.hostedStores.status |
T | own | {store} |
{store_id, pinned, capsule_count, total_bytes, capsules} |
§21 whole-store sync
| Method | Auth | Route | Params | Result |
|---|---|---|---|---|
control.sync.status |
T | own | — | {available, method, pinned_total, pinned_synced, whole_store_trigger_supported} |
control.sync.trigger |
T | own | {store:"storeId:root"} |
{store_id, root, status:"synced", size_bytes, served_root} |
Auto-update beacon (proxied to dig-updater)
| Method | Auth | Route | Params | Result |
|---|---|---|---|---|
control.updater.status |
T | own | — | (proxied beacon status) |
control.updater.setChannel |
T | own | {channel:"nightly"|"stable"} |
(proxied) |
control.updater.pause |
T | own | {until?:u64} (unix secs; omit = indefinite) |
(proxied) |
control.updater.resume |
T | own | — | (proxied) |
control.updater.checkNow |
T | own | — | (proxied) |
Pairing (control-token lifecycle)
| Method | Auth | Route | Params | Result |
|---|---|---|---|---|
control.pairing.list |
M | own | — | (pending requests + issued tokens) |
control.pairing.approve |
M | own | {pairing_id:string} |
{approved:true, client_name, token_id} |
control.pairing.revoke |
M | own | {token_id:string} |
{revoked:bool, token_id} |
pairing.request |
— | open | {client_name:string} |
{pairing_id, pairing_code, expires_ms} |
pairing.poll |
— | open | {pairing_id:string} |
{status, token?} (token present once, after approval) |
Peers (delegated to the engine)
| Method | Auth | Route | Params | Result |
|---|---|---|---|---|
control.peerStatus |
T | del | — | (peer-pool + relay-reservation snapshot, incl. per-peer peers[]); its relay.peer_count counts THE RELAY's peers, not this node's |
control.peerCounts |
— | del | — | {dig_peer_count:u32|null, chia_peer_count:u32|null} — DIG content/gossip peers and CHIA full-node peers; 0 is observed, null unobservable |
control.peers.connect |
T | del | {peer:string} (address or peer_id) |
{connected:true, peer_id} |
control.peers.disconnect |
T | del | {peer:string} (peer_id) |
{disconnected:true, peer_id} |
Wallet chain transport (delegated to the engine)
The caller-addressed reads and the chain-position reads are OPEN; the arrival cursor and the push are token-gated. The node never signs — it reads chain state and pushes bytes somebody else signed.
| Method | Auth | Route | Params | Result |
|---|---|---|---|---|
control.wallet.balance |
— | del | {address:string, asset:"xch"|"dig"} |
{balance, pending, source, synced, peak_height} |
control.wallet.coins |
— | del | {address:string, asset:"xch"|"dig"} |
{coins:[WalletCoinRecord], source, synced, peak_height} |
control.wallet.coinById |
— | del | {coin_id:string} (64-hex, 0x accepted) |
{coin:WalletCoinRecord|null, source, synced, peak_height}; coin:null = the chain holds no such coin |
control.wallet.coinSpend |
— | del | {coin_id:string} (64-hex, 0x accepted) |
{spend:{coin,puzzle_reveal,solution}|null, source, synced, peak_height}; spend:null = the chain holds no spend of that coin (unspent, or unknown) — NEVER "could not answer", which is an error. puzzle_reveal MUST tree-hash to the coin's puzzle_hash |
control.wallet.coinsByParent |
— | del | {parent_coin_id:string, after_coin_id?:string, limit?:u32} (ids 64-hex, 0x accepted; limit 1..=1000, default 100, REFUSED out of range — never clamped) |
{coins:[WalletCoinRecord], complete:bool, cursor:string|null, source, synced, peak_height}; ONE PAGE of the parent's DIRECT children, ascending by coin_id, ONE hop, never a recursive walk. complete:false = TRUNCATED, resume from cursor — never infer completeness from the page length. coins:[] = no known children, never an unreachable chain |
control.wallet.arrivals |
T | del | {after_seq:u64=0, limit?:u32} |
{arrivals:[WalletArrivalRecord], cursor, latest}; TOKEN-GATED: the caller supplies only a cursor, so the answer names this node's own watched puzzle hashes. CONFIRMED incoming funds only, never the wallet's own change. Resume from cursor, never latest |
control.wallet.peak |
— | del | — | {peak_height:u32|null, synced:bool}; synced = catch-up COMPLETED, weaker than syncStatus's |
control.wallet.syncStatus |
— | del | — | {phase:"not_started"|"syncing"|"synced", peak_height:u32|null, chia_peer_count:u32|null}; synced also requires a LIVE Chia peer |
control.wallet.broadcast |
T | del | {signed_bundle_hex:string} |
{accepted, transaction_id, rejection}; accepted = mempool admission, NOT confirmation |
Subscriptions (delegated to the engine)
| Method | Auth | Route | Params | Result |
|---|---|---|---|---|
control.subscribe |
T | del | {store_id:string} |
{subscribed:true, added:bool, store_id} |
control.unsubscribe |
T | del | {store_id:string} |
{subscribed:false, removed:bool, store_id} |
control.listSubscriptions |
T | del | — | {subscriptions:[string], count} |
Error codes
Every control error is {code:int, message:string, data:{code:SYMBOL, origin:string}}. Branch on
data.code (the stable symbol), never the message.
| Code | Symbol | Origin | Meaning |
|---|---|---|---|
-32700 |
PARSE_ERROR |
shell | request body was not valid JSON |
-32600 |
INVALID_REQUEST |
shell | not a single JSON-RPC object |
-32601 |
METHOD_NOT_FOUND |
boundary | control method is not resolved |
-32602 |
INVALID_PARAMS |
node | missing/malformed params |
-32000 |
DISPATCH_FAILED |
shell | the node failed to dispatch a well-formed call |
-32030 |
UNAUTHORIZED |
shell | control.* called without a valid token |
-32031 |
NOT_SUPPORTED |
shell | control op unsupported on this build |
-32032 |
CONTROL_ERROR |
shell | control op failed at runtime |
Boundary
dig-rpc-protocol— node ⇄ node peer wire.dig-ipc-protocol— app ⇄ node local session/signing envelope (the transport this catalog rides over, for local clients).dig-node-control-interface(this crate) — the control METHOD CATALOG carried inside that authenticated channel, or over loopback-mTLS for remote-transport clients.
See SPEC.md for the normative contract and the superproject SYSTEM.md for the wider cross-repo map.
Consumers
dig-node (server-side dispatch — implements ControlHandler), the CLI dign, dig-chrome-extension,
dig-app, hub — all consuming this one control contract per epic #1110 (dig-node adoption + the
dig-rpc Control-tier removal land in later tasks of the cascade).