1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum Routing {
23 Owned,
25 Delegated,
27 OpenBootstrap,
29}
30
31#[non_exhaustive]
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum Category {
39 Status,
41 Config,
43 Log,
45 Cache,
47 HostedStores,
49 Sync,
51 Updater,
53 Pairing,
55 Peers,
57 Subscriptions,
59 Wallet,
62 Spends,
65 Profile,
69 Collateral,
73}
74
75#[non_exhaustive]
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82pub enum ControlMethod {
83 Status,
86 ConfigGet,
88 ConfigSetUpstream,
90 ConfigSetMirrorAdvertiseUrls,
94 LogSetLevel,
96
97 CacheGet,
100 CacheSetCap,
102 CacheClear,
104
105 HostedStoresList,
108 HostedStoresPin,
110 HostedStoresUnpin,
112 HostedStoresStatus,
114 CapsuleFetch,
117
118 SyncStatus,
121 SyncTrigger,
123
124 UpdaterStatus,
127 UpdaterSetChannel,
129 UpdaterPause,
131 UpdaterResume,
133 UpdaterCheckNow,
135
136 PairingList,
139 PairingApprove,
141 PairingRevoke,
143
144 PeerStatus,
147 PeerCounts,
149 PeersConnect,
151 PeersDisconnect,
153
154 ChiaPeersAdd,
170 ChiaPeersList,
172 ChiaPeersRemove,
174
175 Subscribe,
178 Unsubscribe,
180 ListSubscriptions,
182
183 WalletBalance,
186 WalletCoins,
188 WalletCoinById,
190 WalletCoinSpend,
192 WalletCoinsByParent,
194 WalletArrivals,
196 WalletOperatorAddress,
198 WalletPeak,
200 WalletSyncStatus,
202 WalletBroadcast,
204 WalletWatch,
206 WalletUnwatch,
208 WalletWatched,
210 WalletReservationsHeld,
212 WalletReservationsReserve,
214 WalletReservationsRelease,
216 WalletResetCoinDb,
218
219 SpendsList,
222
223 CollateralRequirement,
226 CollateralMarginGet,
228 CollateralMarginSet,
230 CollateralBuffer,
232 MirrorBondStates,
235 MirrorReconcile,
242
243 ProfilePutBody,
246 ProfileGetBody,
248
249 PairingRequest,
252 PairingPoll,
254}
255
256impl ControlMethod {
257 pub const fn name(self) -> &'static str {
259 match self {
260 ControlMethod::Status => "control.status",
261 ControlMethod::ConfigGet => "control.config.get",
262 ControlMethod::ConfigSetUpstream => "control.config.setUpstream",
263 ControlMethod::ConfigSetMirrorAdvertiseUrls => "control.config.setMirrorAdvertiseUrls",
264 ControlMethod::LogSetLevel => "control.log.setLevel",
265 ControlMethod::CacheGet => "control.cache.get",
266 ControlMethod::CacheSetCap => "control.cache.setCap",
267 ControlMethod::CacheClear => "control.cache.clear",
268 ControlMethod::HostedStoresList => "control.hostedStores.list",
269 ControlMethod::HostedStoresPin => "control.hostedStores.pin",
270 ControlMethod::HostedStoresUnpin => "control.hostedStores.unpin",
271 ControlMethod::HostedStoresStatus => "control.hostedStores.status",
272 ControlMethod::CapsuleFetch => "control.capsule.fetch",
273 ControlMethod::SyncStatus => "control.sync.status",
274 ControlMethod::SyncTrigger => "control.sync.trigger",
275 ControlMethod::UpdaterStatus => "control.updater.status",
276 ControlMethod::UpdaterSetChannel => "control.updater.setChannel",
277 ControlMethod::UpdaterPause => "control.updater.pause",
278 ControlMethod::UpdaterResume => "control.updater.resume",
279 ControlMethod::UpdaterCheckNow => "control.updater.checkNow",
280 ControlMethod::PairingList => "control.pairing.list",
281 ControlMethod::PairingApprove => "control.pairing.approve",
282 ControlMethod::PairingRevoke => "control.pairing.revoke",
283 ControlMethod::PeerStatus => "control.peerStatus",
284 ControlMethod::PeerCounts => "control.peerCounts",
285 ControlMethod::PeersConnect => "control.peers.connect",
286 ControlMethod::PeersDisconnect => "control.peers.disconnect",
287 ControlMethod::ChiaPeersAdd => "control.chiaPeers.add",
288 ControlMethod::ChiaPeersList => "control.chiaPeers.list",
289 ControlMethod::ChiaPeersRemove => "control.chiaPeers.remove",
290 ControlMethod::Subscribe => "control.subscribe",
291 ControlMethod::Unsubscribe => "control.unsubscribe",
292 ControlMethod::ListSubscriptions => "control.listSubscriptions",
293 ControlMethod::WalletBalance => "control.wallet.balance",
294 ControlMethod::WalletCoins => "control.wallet.coins",
295 ControlMethod::WalletCoinById => "control.wallet.coinById",
296 ControlMethod::WalletCoinSpend => "control.wallet.coinSpend",
297 ControlMethod::WalletCoinsByParent => "control.wallet.coinsByParent",
298 ControlMethod::WalletArrivals => "control.wallet.arrivals",
299 ControlMethod::WalletOperatorAddress => "control.wallet.operatorAddress",
300 ControlMethod::WalletPeak => "control.wallet.peak",
301 ControlMethod::WalletSyncStatus => "control.wallet.syncStatus",
302 ControlMethod::WalletBroadcast => "control.wallet.broadcast",
303 ControlMethod::WalletWatch => "control.wallet.watch",
304 ControlMethod::WalletUnwatch => "control.wallet.unwatch",
305 ControlMethod::WalletWatched => "control.wallet.watched",
306 ControlMethod::WalletReservationsHeld => "control.wallet.reservations.held",
307 ControlMethod::WalletReservationsReserve => "control.wallet.reservations.reserve",
308 ControlMethod::WalletReservationsRelease => "control.wallet.reservations.release",
309 ControlMethod::WalletResetCoinDb => "control.wallet.resetCoinDb",
310 ControlMethod::SpendsList => "control.spends.list",
311 ControlMethod::CollateralRequirement => "control.collateral.requirement",
312 ControlMethod::CollateralMarginGet => "control.collateral.margin.get",
313 ControlMethod::CollateralMarginSet => "control.collateral.margin.set",
314 ControlMethod::CollateralBuffer => "control.collateral.buffer",
315 ControlMethod::MirrorBondStates => "control.mirror.bondStates",
316 ControlMethod::MirrorReconcile => "control.mirror.reconcile",
317 ControlMethod::ProfilePutBody => "control.profile.putBody",
318 ControlMethod::ProfileGetBody => "control.profile.getBody",
319 ControlMethod::PairingRequest => "pairing.request",
320 ControlMethod::PairingPoll => "pairing.poll",
321 }
322 }
323
324 pub fn from_name(name: &str) -> Option<ControlMethod> {
326 ControlMethod::ALL
327 .iter()
328 .copied()
329 .find(|m| m.name() == name)
330 }
331
332 pub const fn requires_auth(self) -> bool {
359 !self.is_open_read()
360 && !matches!(
361 self,
362 ControlMethod::PairingRequest | ControlMethod::PairingPoll
363 )
364 }
365
366 pub const fn is_open_read(self) -> bool {
400 matches!(
401 self,
402 ControlMethod::WalletBalance
403 | ControlMethod::WalletCoins
404 | ControlMethod::WalletCoinById
405 | ControlMethod::WalletCoinSpend
406 | ControlMethod::WalletCoinsByParent
407 | ControlMethod::WalletPeak
408 | ControlMethod::WalletSyncStatus
409 | ControlMethod::PeerCounts
410 )
411 }
412
413 pub const fn is_pairing_admin(self) -> bool {
422 matches!(
423 self,
424 ControlMethod::PairingList
425 | ControlMethod::PairingApprove
426 | ControlMethod::PairingRevoke
427 )
428 }
429
430 pub const fn requires_master_token(self) -> bool {
468 self.is_pairing_admin()
469 || matches!(
470 self,
471 ControlMethod::ChiaPeersAdd | ControlMethod::ChiaPeersRemove
472 )
473 }
474
475 pub const fn routing(self) -> Routing {
477 match self {
478 ControlMethod::PeerStatus
479 | ControlMethod::PeerCounts
480 | ControlMethod::PeersConnect
481 | ControlMethod::PeersDisconnect
482 | ControlMethod::Subscribe
483 | ControlMethod::Unsubscribe
484 | ControlMethod::ListSubscriptions
485 | ControlMethod::WalletBalance
486 | ControlMethod::WalletCoins
487 | ControlMethod::WalletCoinById
488 | ControlMethod::WalletCoinSpend
489 | ControlMethod::WalletCoinsByParent
490 | ControlMethod::WalletArrivals
491 | ControlMethod::WalletPeak
492 | ControlMethod::WalletSyncStatus
493 | ControlMethod::WalletBroadcast
494 | ControlMethod::WalletWatch
495 | ControlMethod::WalletUnwatch
496 | ControlMethod::WalletWatched
497 | ControlMethod::WalletReservationsHeld
498 | ControlMethod::WalletReservationsReserve
499 | ControlMethod::WalletReservationsRelease
500 | ControlMethod::WalletResetCoinDb
501 | ControlMethod::ProfilePutBody
502 | ControlMethod::ProfileGetBody => Routing::Delegated,
503 ControlMethod::PairingRequest | ControlMethod::PairingPoll => Routing::OpenBootstrap,
504 _ => Routing::Owned,
505 }
506 }
507
508 pub const fn category(self) -> Category {
510 match self {
511 ControlMethod::Status => Category::Status,
512 ControlMethod::ConfigGet
513 | ControlMethod::ConfigSetUpstream
514 | ControlMethod::ConfigSetMirrorAdvertiseUrls => Category::Config,
515 ControlMethod::LogSetLevel => Category::Log,
516 ControlMethod::CacheGet | ControlMethod::CacheSetCap | ControlMethod::CacheClear => {
517 Category::Cache
518 }
519 ControlMethod::HostedStoresList
520 | ControlMethod::HostedStoresPin
521 | ControlMethod::HostedStoresUnpin
522 | ControlMethod::HostedStoresStatus
523 | ControlMethod::CapsuleFetch => Category::HostedStores,
524 ControlMethod::SyncStatus | ControlMethod::SyncTrigger => Category::Sync,
525 ControlMethod::UpdaterStatus
526 | ControlMethod::UpdaterSetChannel
527 | ControlMethod::UpdaterPause
528 | ControlMethod::UpdaterResume
529 | ControlMethod::UpdaterCheckNow => Category::Updater,
530 ControlMethod::PairingList
531 | ControlMethod::PairingApprove
532 | ControlMethod::PairingRevoke
533 | ControlMethod::PairingRequest
534 | ControlMethod::PairingPoll => Category::Pairing,
535 ControlMethod::PeerStatus
536 | ControlMethod::PeerCounts
537 | ControlMethod::PeersConnect
538 | ControlMethod::PeersDisconnect
539 | ControlMethod::ChiaPeersAdd
540 | ControlMethod::ChiaPeersList
541 | ControlMethod::ChiaPeersRemove => Category::Peers,
542 ControlMethod::Subscribe
543 | ControlMethod::Unsubscribe
544 | ControlMethod::ListSubscriptions => Category::Subscriptions,
545 ControlMethod::WalletBalance
546 | ControlMethod::WalletCoins
547 | ControlMethod::WalletCoinById
548 | ControlMethod::WalletCoinSpend
549 | ControlMethod::WalletCoinsByParent
550 | ControlMethod::WalletArrivals
551 | ControlMethod::WalletPeak
552 | ControlMethod::WalletSyncStatus
553 | ControlMethod::WalletOperatorAddress
554 | ControlMethod::WalletBroadcast
555 | ControlMethod::WalletWatch
556 | ControlMethod::WalletUnwatch
557 | ControlMethod::WalletWatched
558 | ControlMethod::WalletReservationsHeld
559 | ControlMethod::WalletReservationsReserve
560 | ControlMethod::WalletReservationsRelease
561 | ControlMethod::WalletResetCoinDb => Category::Wallet,
562 ControlMethod::SpendsList => Category::Spends,
563 ControlMethod::CollateralRequirement
564 | ControlMethod::CollateralMarginGet
565 | ControlMethod::CollateralMarginSet
566 | ControlMethod::CollateralBuffer
567 | ControlMethod::MirrorBondStates
568 | ControlMethod::MirrorReconcile => Category::Collateral,
569 ControlMethod::ProfilePutBody | ControlMethod::ProfileGetBody => Category::Profile,
570 }
571 }
572
573 pub const fn summary(self) -> &'static str {
575 match self {
576 ControlMethod::ChiaPeersAdd => "Trust a Chia full node by IP. A trusted peer BYPASSES CORROBORATION: this node normally believes a chain answer only when several independently-dialled peers agree, and a trusted peer is believed on its own -- so a wrong or hostile one can feed this node a false view of the chain. Add only a node you run yourself.",
577 ControlMethod::ChiaPeersList => "The Chia full-node peers this node tracks, each flagged user_managed: true where a person added it by hand and it is therefore trusted without corroboration.",
578 ControlMethod::ChiaPeersRemove => "Stop trusting a Chia full node, optionally banning it. Removing restores corroboration for that peer: chain answers must once again be agreed by independently-dialled peers.",
579 ControlMethod::Status => "A rich node status snapshot (version, uptime, addr, cache, hosted/pinned counts, sync availability).",
580 ControlMethod::ConfigGet => "The node's effective configuration (addr/port, upstream + override, cache dir/shared, config path, sync availability, mirror advertise-URL view).",
581 ControlMethod::ConfigSetUpstream => "Persist an upstream-RPC override; takes effect on next node start (requires_restart).",
582 ControlMethod::ConfigSetMirrorAdvertiseUrls => "Override (urls: a non-empty list) or clear (urls: null/absent) the URLs this node advertises in its own mirror-coin memos. The result's requires_restart says whether that took effect now or needs a node restart -- check it before telling anyone the change is live. An explicit EMPTY list is refused rather than guessed at -- it is ambiguous between advertising nothing and reverting to the derived default. Checked only for a well-formed absolute URL (scheme + host): an operator's LAN or private address is accepted on purpose, the same derived-vs-operator asymmetry dig-node#562 established.",
583 ControlMethod::LogSetLevel => "Live-swap the running node's tracing EnvFilter directive (not persisted).",
584 ControlMethod::CacheGet => "The on-disk content-cache view: cap_bytes, used_bytes, dir, shared.",
585 ControlMethod::CacheSetCap => "Set the on-disk cache size cap in bytes (floored at 64 MiB).",
586 ControlMethod::CacheClear => "Delete all locally cached DIG content.",
587 ControlMethod::HostedStoresList => "Every held/pinned store, merged, with each store's cached capsules and a pinned flag.",
588 ControlMethod::HostedStoresPin => "Pin a store (storeId[:rootHash]); pre-fetches the capsule when a root is given and §21 sync is available.",
589 ControlMethod::HostedStoresUnpin => "Unpin a store and evict its cached capsules.",
590 ControlMethod::HostedStoresStatus => "Per-store status: pinned flag, cached capsules, total bytes.",
591 ControlMethod::CapsuleFetch => "Start a P2P whole-capsule pull for one store+root over the recursive discover-then-dial path (distinct from the §21 HTTP sync `control.sync.trigger` uses). Answers `already_cached` without dialling out when the capsule is already on disk.",
592 ControlMethod::SyncStatus => "Whether authenticated §21 whole-store sync is available, plus pinned-store cache coverage.",
593 ControlMethod::SyncTrigger => "Trigger a §21 sync for one capsule (storeId + root).",
594 ControlMethod::UpdaterStatus => "The DIG auto-update beacon's current status (proxied from dig-updater).",
595 ControlMethod::UpdaterSetChannel => "Set the beacon's update channel (\"nightly\" | \"stable\").",
596 ControlMethod::UpdaterPause => "Suspend the beacon's auto-updates (optionally until a unix time).",
597 ControlMethod::UpdaterResume => "Resume the beacon's auto-updates.",
598 ControlMethod::UpdaterCheckNow => "Force an immediate beacon update check.",
599 ControlMethod::PairingList => "List pending pairing requests and issued paired tokens (MASTER token only).",
600 ControlMethod::PairingApprove => "Approve a pending pairing, minting a scoped token (MASTER token only).",
601 ControlMethod::PairingRevoke => "Revoke an issued paired token by token_id (MASTER token only).",
602 ControlMethod::PeerStatus => "Live peer-pool + relay-reservation snapshot, including the per-peer connected array; each entry carries an always-present `software` field (the peer's advertised build). Its `relay.peer_count` counts peers connected to THE RELAY, not to this node, and is never the answer to \"how many peers does this node have\" -- that is control.peerCounts.",
603 ControlMethod::PeerCounts => "READ-only: how many peers this node holds on EACH network -- dig_peer_count (DIG content/gossip, port 9445) and chia_peer_count (Chia full nodes serving the wallet chain sync). Two unrelated numbers, each named for its network.",
604 ControlMethod::PeersConnect => "Dial a peer by address, or resolve an already-connected peer_id, via the live gossip pool.",
605 ControlMethod::PeersDisconnect => "Drop a pooled peer by peer_id, closing its mTLS link (idempotent).",
606 ControlMethod::Subscribe => "Subscribe the node to a store it actively watches and gap-fills.",
607 ControlMethod::Unsubscribe => "Stop watching a store.",
608 ControlMethod::ListSubscriptions => "The node's persisted subscription set + count.",
609 ControlMethod::WalletCoins => "READ-only: the spendable coin records for an address + asset, with the tier that answered and the height they reflect.",
610 ControlMethod::WalletCoinById => "READ-only: ONE coin record by coin id, spent or unspent, with no address and no asset scope; `coin: null` means the chain holds no such coin.",
611 ControlMethod::WalletCoinSpend => "READ-only: the SPEND that spent a coin -- its puzzle reveal, its solution and the coin itself -- named by the coin's own id. `spend: null` means the consulted chain shows that coin as unspent or unknown; it NEVER means the chain could not be reached, which is an error.",
612 ControlMethod::WalletCoinsByParent => "READ-only: the DIRECT children created by spending one coin, named by that parent's coin id. ONE hop, never a recursive walk: an empty list means the parent created no known children, and a caller wanting a lineage composes hops itself.",
613 ControlMethod::WalletArrivals => "READ-only: confirmed INCOMING funds recorded since a cursor position, oldest first -- the answer to `was I just paid?`, which no balance or coin list can give. Each row is a coin the node determined ARRIVED: confirmed on chain, above the wallet's arrival baseline, not previously reported, and not the wallet's own change. Resume from `cursor` (the last row you were handed), never from `latest`.",
614 ControlMethod::WalletOperatorAddress => "READ-only: the address of the node's OWN operator wallet -- the MACHINE-custody wallet that pays mirror-coin collateral, never the user's. Returns a public address and puzzle hash and NEVER any key, seed or derivation material. TOKEN-GATED, and answered by THIS node rather than forwarded upstream.",
615 ControlMethod::WalletPeak => "READ-only: the node's current chain peak height, independent of any address.",
616 ControlMethod::WalletSyncStatus => "READ-only: whether the wallet's CHAIN replica is being kept current (not_started/syncing/synced/no_wallet_enrolled/wallet_not_unlocked), the replica's own height, and its CHIA full-node peer count -- unrelated to control.sync.status (DIG stores) and to control.peerStatus (DIG peers).",
617 ControlMethod::WalletBroadcast => "Push an ALREADY-SIGNED spend bundle to the network; the node never signs. TOKEN-GATED.",
618 ControlMethod::WalletBalance => "READ-only: the confirmed spendable balance for an address + asset (plus pending, sync freshness, and the peak height it reflects).",
619 ControlMethod::WalletWatch => "Enrol PUBLIC keys (48-byte G1, lowercase 96-hex) for the node's chain replica to follow, so their addresses are synced and readable. IDEMPOTENT: re-enrolling a key already enrolled succeeds and changes nothing. Keys, never puzzle hashes -- the node derives the addresses itself, so one derivation serves every client. TOKEN-GATED.",
620 ControlMethod::WalletUnwatch => "Deregister enrolled public keys, so the node stops following their addresses. IDEMPOTENT: a key that was never enrolled is not an error. TOKEN-GATED.",
621 ControlMethod::SpendsList => "READ-only: the record of spends this node made WITHOUT per-transaction approval -- what moved, when, on whose standing authority, and whether the chain confirmed it. It NEVER initiates, signs, cancels or alters a spend, and there is no verb here that edits an entry. A failed spend is reported WITH the stage it died at, because only a signing failure means the money definitely did not move; a broadcast or confirmation failure is an UNKNOWN outcome, as is `unresolved`. A page is bounded and says so via `complete`; `unreadable_lines` reports entries the node could not parse, so an audit trail that lost rows can never read as a tidy shorter one. TOKEN-GATED although it is a read: the caller supplies no identifier, so the answer is this node's OWN state.",
622 ControlMethod::CollateralRequirement => "READ-only: this epoch's per-store mirror-collateral requirement in DIG base units, the collateral protocol version that computed it, and the census inputs behind it (advertised stores, collateralised owners, controller multiplier, small-network handicap) so a client can show WHY the figure moved rather than only that it did. A node that has not censused the epoch, or that is inside the census finality depth, answers `unknown` WITH the reason -- never a zero, which would read as a free requirement. `stores` counts qualifying (owner, store, root) advertisements and `owners` counts distinct owner puzzle hashes: neither is a node count. It NEVER returns the local safety margin, which is not a consensus value.",
623 ControlMethod::CollateralMarginGet => "Read the node's LOCAL safety margin in BASIS POINTS over the epoch requirement (`100` is +1%). The margin is an operator preference that changes only how much THIS node chooses to lock; it is never a census input and no value derived from it reaches another node. Basis points are the unit the collateral crate's own presets and rounding use, and are never converted.",
624 ControlMethod::CollateralMarginSet => "Set the node's LOCAL safety margin in BASIS POINTS (`100` is +1%), returning the margin now in force. Bounded at 10000 bp (+100%): the margin multiplies what the node locks on every store, so an unbounded value would commit the operator to an arbitrary posting. A margin above the bound is REFUSED as -32602 INVALID_PARAMS rather than clamped, so the applied value can never differ silently from the requested one. A margin gives room if the requirement rises; it does NOT guarantee a store is counted, because the requirement is re-derived every epoch and can rise by more than any margin.",
625 ControlMethod::ProfilePutBody => "Hand the node the dig-profile BODY that a chain root commits to. The node INDEPENDENTLY resolves that root on chain and REFUSES any body whose recomputed root is not the confirmed one -- the caller's `root` is a claim to be checked, never a fact to be trusted, and dig-app is a caller like any other. Bodies are capped at MAX_BODY_BYTES (4 MiB). TOKEN-GATED.",
626 ControlMethod::ProfileGetBody => "READ-only: the dig-profile body this node holds at a given store id + root, or `body: null` when it holds none. `null` NEVER means the body could not be read, which is an error. TOKEN-GATED.",
627 ControlMethod::WalletWatched => "READ-only: the public keys currently enrolled, so a client can reconcile what it asked for against what the node holds. TOKEN-GATED although it is a read -- the caller supplies nothing, so the answer is this node's OWN key set.",
628 ControlMethod::WalletReservationsHeld => "READ-only: every coin currently committed to an in-flight spend, each with the reservation holding it and the unix second that hold lapses, plus the node's own clock. `reserved: []` means NOTHING is held; a set that cannot be read is an error, never an empty list. Narrows what a caller may SELECT; never subtract these from a balance -- the coins are still the user's money. TOKEN-GATED although it is a read: the caller supplies nothing, so the answer is this node's OWN state.",
629 ControlMethod::WalletReservationsReserve => "Atomically hold coins against further selection: EVERY named coin or none. A coin already held refuses the whole call and reserves nothing, as WALLET_COINS_RESERVED -- a WAIT, never a shortfall. Reserving an empty list succeeds with a handle that releases nothing. The requested ttl_secs is clamped by the node, which returns the lifetime it actually applied. Bookkeeping only: it holds no key and authorizes nothing (§908). TOKEN-GATED.",
630 ControlMethod::WalletReservationsRelease => "Free a hold now rather than waiting out its TTL -- call it the moment a spend is known settled or known dead. A handle that names no live reservation is a SUCCESS with released: false, because a caller releasing on confirmation cannot know whether the TTL got there first. Every hold also lapses on its own, so an abandoned reservation is recoverable and never a permanent funds lockout. TOKEN-GATED.",
631 ControlMethod::WalletResetCoinDb => "DESTRUCTIVE: discard this node's cached coin database and re-sync it from chain. No key material is affected -- coins live on chain and are re-derived by the resync. Refuses with SpendInFlight while a spend is outstanding, so a reset can never race a hold. Requires params.confirm = true or refuses as INVALID_PARAMS; the on-wire acknowledgement is the confirmation, not a default. TOKEN-GATED.",
632 ControlMethod::CollateralBuffer => "READ-only: the $DIG this node recommends HOLDING, in DIG base units, and the funding state it is in -- plus the working behind the figure: the (owner, store, root) pairs THIS NODE serves, the epoch's pre-margin per-store requirement, the local margin in force (BASIS POINTS, `100` is +1%, never converted), the unreclaimed transition overlap, and the escalation headroom. Amounts are DIG base units (3 decimals, one base unit is 0.001 DIG) and never mojos, which are XCH's 1e-12 unit. The HORIZON the headroom assumed travels in the payload and is never implied: escalation is bounded at +12.5% per epoch and COMPOUNDS (x1.12 at one epoch, x1.60 at four, x4.62 at thirteen), so the same buffer over a different horizon is a different claim; `escalation_ceiling_micros` is a WORST CASE, not a forecast -- in the dead band the multiplier does not move. The FUNDING STATE is carried rather than left to each client to re-derive from thresholds, because two clients deriving it will disagree and the one that disagrees about a funding warning is the one an operator acts on; `short_now` and `dangerously_low` leave an epoch uncovered, `below_recommended_buffer` covers every epoch with no cushion and is a READOUT, never a notification. A node that cannot enumerate its served set, cannot read its reclaim state, cannot see its balance, or has no requirement to scale answers `unknown` WITH the reason -- never a zero, which here reads as NO BUFFER NEEDED and would have an operator post nothing. It is a SEPARATE method from `control.collateral.requirement` because that figure is consensus-derived while this one is local: it depends on this node's own served set, an operator preference, and a horizon this node chose. TOKEN-GATED although it is a read: the caller supplies nothing, so the answer is this node's OWN served set, preference and balance.",
633 ControlMethod::MirrorBondStates => "READ-only: the state of every mirror bond this node holds, keyed per (store, root), plus the $DIG those bonds have LOCKED. Eight states, and seven of them mean `no coin yet` for entirely different reasons: `bonded` (a coin id, epoch and the amount THAT COIN locks, read from the coin and not from today's requirement, plus `urls` -- the coin's own memo -- and `url_current` -- whether that memo still matches what this node advertises THIS PASS), `pending` (submitted, unconfirmed -- never a shortfall), `unfunded` (the ONLY genuine out-of-funds state, carrying how many DIG BASE UNITS this bond alone is short), `deferred` (the epoch requirement is unknown so no create can be priced -- the wallet may be full), `withheld` (Relayed provenance: held and deliberately never advertised), `disabled` (collateralisation is switched off node-wide), `unadvertised` (that switch ON, but no publishable advertise URL, so the node advertises nothing and a coin would bond nothing -- a fault a client MUST surface, and NOT the same as `disabled`) and `reclaiming` (a live coin whose money is STILL LOCKED until the reclaim confirms). Conflating `unfunded` with `withheld` or `disabled` produces hourly out-of-funds alarms about a healthy node, which is the defect this method removes. Amounts are DIG base units (3 decimals, one base unit is 0.001 DIG) and NEVER mojos, which are XCH's 1e-12 unit. `locked_dig_base_units` is the WHOLE-SET total including reclaiming coins, computed by the node: a client MUST NOT sum the page, which would under-report locked money by a page boundary and show unspendable funds as available. A `known` answer also carries `url_reconcile` beside `funding_wallet`: whether the daily URL-reconcile detector is armed, when it next runs, what it last observed, and how many bonds across the WHOLE set currently read `url_current: false` -- the standing `control.mirror.reconcile` exists to fix. A node that cannot enumerate its bonds, cannot read chain, cannot see its own in-flight creates, or cannot determine the provenance of what it holds answers `unknown` for the WHOLE call WITH the reason -- there is no per-row unknown and no empty-list fallback, because a truncated list and a complete one read the same. A page is bounded, ordered by ascending (store_id, root), and says via `complete` whether it is the whole set; resume from the `cursor` key you were HANDED. TOKEN-GATED although it is a read: the caller supplies nothing, so the answer is this node's OWN bond set and funding position.",
634 ControlMethod::MirrorReconcile => "Reconcile this node's mirror coins to its CURRENT advertise URL: reclaim every coin advertising a stale URL, then recreate it advertising the URL this node advertises today. Shared by the manual \"reset mirrors\" action and the same primitive the node's daily detector runs (dig-node#570) -- ONE reconcile primitive, not two implementations of a money-moving operation with different guards. `dry_run: true` prices the plan (capsules_stale, capsules_affordable, collateral reclaimed/relocked, fee) and spends nothing -- always price before executing for real. A refusal (`not_publishing`, `url_unchanged`, `no_mirror_coins`, `insufficient_funds`, `reconcile_in_progress`, `disabled`, `chain_unreadable`, `requirement_unknown`, `wallet_unavailable`, `funds_unmeasured`) means NOTHING was spent: the node validates the new URL and checks affordability BEFORE reclaiming anything, and does nothing at all rather than reclaim coins it cannot afford to recreate. There is no `completed` or `partial` outcome: a reclaim and a create are separate spend bundles, and a create's funding is a scan of CONFIRMED coins, so the node can only report `submitted` -- how many reclaims the mempool accepted/rejected and how many recreates are OWED to the ordinary create pass, never a `created` count it cannot back. The affordable prefix K is sized BEFORE any reclaim is attempted and exactly K are reclaimed, never more, so a `submitted` outcome always leaves the node's bond COUNT no worse off than before the call. TOKEN-GATED.",
635 ControlMethod::PairingRequest => "OPEN: request a control-token pairing; returns a pairing_id + pairing_code to compare.",
636 ControlMethod::PairingPoll => "OPEN: poll a pairing by id; once the operator approves, returns the scoped token once.",
637 }
638 }
639
640 pub const ALL: &'static [ControlMethod] = &[
643 ControlMethod::Status,
644 ControlMethod::ConfigGet,
645 ControlMethod::ConfigSetUpstream,
646 ControlMethod::ConfigSetMirrorAdvertiseUrls,
647 ControlMethod::LogSetLevel,
648 ControlMethod::CacheGet,
649 ControlMethod::CacheSetCap,
650 ControlMethod::CacheClear,
651 ControlMethod::HostedStoresList,
652 ControlMethod::HostedStoresPin,
653 ControlMethod::HostedStoresUnpin,
654 ControlMethod::HostedStoresStatus,
655 ControlMethod::CapsuleFetch,
656 ControlMethod::SyncStatus,
657 ControlMethod::SyncTrigger,
658 ControlMethod::UpdaterStatus,
659 ControlMethod::UpdaterSetChannel,
660 ControlMethod::UpdaterPause,
661 ControlMethod::UpdaterResume,
662 ControlMethod::UpdaterCheckNow,
663 ControlMethod::PairingList,
664 ControlMethod::PairingApprove,
665 ControlMethod::PairingRevoke,
666 ControlMethod::PeerStatus,
667 ControlMethod::PeerCounts,
668 ControlMethod::PeersConnect,
669 ControlMethod::PeersDisconnect,
670 ControlMethod::ChiaPeersAdd,
671 ControlMethod::ChiaPeersList,
672 ControlMethod::ChiaPeersRemove,
673 ControlMethod::Subscribe,
674 ControlMethod::Unsubscribe,
675 ControlMethod::ListSubscriptions,
676 ControlMethod::WalletBalance,
677 ControlMethod::WalletCoins,
678 ControlMethod::WalletCoinById,
679 ControlMethod::WalletCoinSpend,
680 ControlMethod::WalletCoinsByParent,
681 ControlMethod::WalletArrivals,
682 ControlMethod::WalletPeak,
683 ControlMethod::WalletSyncStatus,
684 ControlMethod::WalletOperatorAddress,
685 ControlMethod::WalletBroadcast,
686 ControlMethod::WalletWatch,
687 ControlMethod::WalletUnwatch,
688 ControlMethod::WalletWatched,
689 ControlMethod::WalletReservationsHeld,
690 ControlMethod::WalletReservationsReserve,
691 ControlMethod::WalletReservationsRelease,
692 ControlMethod::WalletResetCoinDb,
693 ControlMethod::SpendsList,
694 ControlMethod::CollateralRequirement,
695 ControlMethod::CollateralMarginGet,
696 ControlMethod::CollateralMarginSet,
697 ControlMethod::CollateralBuffer,
698 ControlMethod::MirrorBondStates,
699 ControlMethod::MirrorReconcile,
700 ControlMethod::ProfilePutBody,
701 ControlMethod::ProfileGetBody,
702 ControlMethod::PairingRequest,
703 ControlMethod::PairingPoll,
704 ];
705}
706
707#[cfg(test)]
708mod tests {
709 use super::*;
710 use std::collections::BTreeSet;
711
712 #[test]
713 fn every_method_has_a_unique_wire_name() {
714 let names: BTreeSet<&str> = ControlMethod::ALL.iter().map(|m| m.name()).collect();
715 assert_eq!(
716 names.len(),
717 ControlMethod::ALL.len(),
718 "duplicate or missing wire names in the catalog"
719 );
720 }
721
722 #[test]
723 fn from_name_round_trips_every_method() {
724 for &m in ControlMethod::ALL {
725 assert_eq!(ControlMethod::from_name(m.name()), Some(m));
726 }
727 assert_eq!(ControlMethod::from_name("control.nope"), None);
728 assert_eq!(ControlMethod::from_name(""), None);
729 }
730
731 #[test]
732 fn the_token_less_surface_is_exactly_the_bootstrap_plus_the_chain_reads() {
733 let expected_open: BTreeSet<&str> = [
737 "pairing.request",
738 "pairing.poll",
739 "control.wallet.balance",
740 "control.wallet.coins",
741 "control.wallet.coinById",
742 "control.wallet.coinSpend",
743 "control.wallet.coinsByParent",
744 "control.wallet.peak",
745 "control.wallet.syncStatus",
746 "control.peerCounts",
747 ]
748 .into_iter()
749 .collect();
750 assert_eq!(
751 expected_open.len(),
752 10,
753 "the open surface is ten named methods"
754 );
755 let actual_open: BTreeSet<&str> = ControlMethod::ALL
756 .iter()
757 .filter(|m| !m.requires_auth())
758 .map(|m| m.name())
759 .collect();
760 assert_eq!(actual_open, expected_open);
761 }
762
763 #[test]
772 fn the_gated_wallet_methods_are_the_push_the_cursor_and_enrolment() {
773 let gated: Vec<&str> = ControlMethod::ALL
774 .iter()
775 .filter(|m| m.category() == Category::Wallet && m.requires_auth())
776 .map(|m| m.name())
777 .collect();
778 assert_eq!(
779 gated,
780 vec![
781 "control.wallet.arrivals",
782 "control.wallet.operatorAddress",
786 "control.wallet.broadcast",
787 "control.wallet.watch",
788 "control.wallet.unwatch",
789 "control.wallet.watched",
790 "control.wallet.reservations.held",
791 "control.wallet.reservations.reserve",
792 "control.wallet.reservations.release",
793 "control.wallet.resetCoinDb",
794 ]
795 );
796 assert!(!ControlMethod::WalletBroadcast.is_open_read());
797 }
798
799 #[test]
811 fn the_arrival_cursor_is_not_an_open_read() {
812 assert!(
813 !ControlMethod::WalletArrivals.is_open_read(),
814 "control.wallet.arrivals discloses this node's OWN watched puzzle hashes to a caller \
815 that supplied nothing, so it MUST NOT be served token-less"
816 );
817 assert!(ControlMethod::WalletArrivals.requires_auth());
818 assert!(
819 ControlMethod::WalletCoinById.is_open_read(),
820 "the caller-addressed reads stay open -- the fix is the membership rule, not gating \
821 the wallet category"
822 );
823 }
824
825 #[test]
844 fn the_catalog_serves_every_chain_source_primitive() {
845 for wire in [
846 "control.wallet.coinById", "control.wallet.coins", "control.wallet.peak", "control.wallet.coinsByParent", "control.wallet.coinSpend", ] {
852 assert!(
853 ControlMethod::from_name(wire).is_some(),
854 "{wire} is required to implement ChainSource over the control plane"
855 );
856 }
857 }
858
859 #[test]
866 fn the_chain_primitives_are_caller_named_open_reads() {
867 for method in [
868 ControlMethod::WalletCoinSpend,
869 ControlMethod::WalletCoinsByParent,
870 ] {
871 assert!(
872 method.is_open_read(),
873 "{} names its subject in the request and discloses no node-to-address \
874 association, exactly like control.wallet.coinById",
875 method.name()
876 );
877 assert!(!method.requires_auth());
878 }
879 assert!(
880 ControlMethod::WalletArrivals.requires_auth(),
881 "the caller-supplies-nothing read stays gated -- the rule is who names the subject, \
882 not whether the bytes are on chain"
883 );
884 assert!(ControlMethod::WalletBroadcast.requires_auth());
885 }
886
887 #[test]
901 fn the_enrolment_methods_are_gated_including_the_read() {
902 for wire in [
903 "control.wallet.watch",
904 "control.wallet.unwatch",
905 "control.wallet.watched",
906 ] {
907 let method = ControlMethod::from_name(wire)
908 .unwrap_or_else(|| panic!("{wire} must be in the catalog"));
909 assert!(
910 !method.is_open_read(),
911 "{wire} either aims this node's subscriptions or names the keys it already \
912 follows, so it MUST NOT be served token-less"
913 );
914 assert!(method.requires_auth(), "{wire} must require the token");
915 assert_eq!(method.category(), Category::Wallet);
916 assert_eq!(method.routing(), Routing::Delegated);
917 }
918 assert!(
919 ControlMethod::WalletCoinById.is_open_read(),
920 "the caller-addressed reads stay open -- enrolment is gated by the membership rule, \
921 not by gating the wallet category"
922 );
923 }
924
925 #[test]
926 fn only_pairing_bootstrap_is_open_bootstrap_routed() {
927 for &m in ControlMethod::ALL {
928 let open_bootstrap = matches!(
929 m,
930 ControlMethod::PairingRequest | ControlMethod::PairingPoll
931 );
932 assert_eq!(
933 m.routing() == Routing::OpenBootstrap,
934 open_bootstrap,
935 "{} routing mismatch",
936 m.name()
937 );
938 }
939 }
940
941 #[test]
942 fn pairing_admin_methods_are_exactly_three() {
943 let admin: Vec<&str> = ControlMethod::ALL
944 .iter()
945 .filter(|m| m.is_pairing_admin())
946 .map(|m| m.name())
947 .collect();
948 assert_eq!(
949 admin,
950 vec![
951 "control.pairing.list",
952 "control.pairing.approve",
953 "control.pairing.revoke"
954 ]
955 );
956 }
957
958 #[test]
965 fn the_master_token_tier_is_pairing_admin_plus_the_trusted_peer_mutations() {
966 let master: BTreeSet<&str> = ControlMethod::ALL
967 .iter()
968 .filter(|m| m.requires_master_token())
969 .map(|m| m.name())
970 .collect();
971 let expected: BTreeSet<&str> = [
972 "control.pairing.list",
973 "control.pairing.approve",
974 "control.pairing.revoke",
975 "control.chiaPeers.add",
976 "control.chiaPeers.remove",
977 ]
978 .into_iter()
979 .collect();
980 assert_eq!(master, expected);
981
982 for &m in ControlMethod::ALL {
985 assert!(
986 !m.is_pairing_admin() || m.requires_master_token(),
987 "{} is pairing-admin but not master-tier",
988 m.name()
989 );
990 }
991 assert!(
992 master.len()
993 > ControlMethod::ALL
994 .iter()
995 .filter(|m| m.is_pairing_admin())
996 .count(),
997 "the two predicates must not be interchangeable"
998 );
999
1000 for &m in ControlMethod::ALL {
1002 assert!(
1003 !m.requires_master_token() || m.requires_auth(),
1004 "{}",
1005 m.name()
1006 );
1007 }
1008 }
1009
1010 #[test]
1017 fn the_add_summary_authorises_only_a_node_the_operator_runs() {
1018 let summary = ControlMethod::ChiaPeersAdd.summary().to_lowercase();
1019 assert!(
1020 summary.contains("a node you run"),
1021 "add must name the operator-run scope, got: {summary}"
1022 );
1023 for widened in ["vouch", "otherwise trust", "trust yourself", "recommend"] {
1024 assert!(
1025 !summary.contains(widened),
1026 "add summary widens operator trust past NC-12 with {widened:?}: {summary}"
1027 );
1028 }
1029 }
1030
1031 #[test]
1032 fn delegated_set_matches_the_engine_surface() {
1033 let delegated: BTreeSet<&str> = ControlMethod::ALL
1034 .iter()
1035 .filter(|m| m.routing() == Routing::Delegated)
1036 .map(|m| m.name())
1037 .collect();
1038 let expected: BTreeSet<&str> = [
1039 "control.wallet.coins",
1040 "control.wallet.coinById",
1041 "control.wallet.coinSpend",
1042 "control.wallet.coinsByParent",
1043 "control.wallet.arrivals",
1044 "control.wallet.peak",
1045 "control.wallet.syncStatus",
1046 "control.wallet.broadcast",
1047 "control.wallet.watch",
1048 "control.wallet.unwatch",
1049 "control.wallet.watched",
1050 "control.wallet.reservations.held",
1051 "control.wallet.reservations.reserve",
1052 "control.wallet.reservations.release",
1053 "control.wallet.resetCoinDb",
1054 "control.profile.putBody",
1055 "control.profile.getBody",
1056 "control.peerStatus",
1057 "control.peerCounts",
1058 "control.peers.connect",
1059 "control.peers.disconnect",
1060 "control.subscribe",
1061 "control.unsubscribe",
1062 "control.listSubscriptions",
1063 "control.wallet.balance",
1064 ]
1065 .into_iter()
1066 .collect();
1067 assert_eq!(delegated, expected);
1068 }
1069
1070 #[test]
1077 fn the_trusted_chia_peer_methods_are_gated_and_disclose_the_corroboration_bypass() {
1078 let declared: BTreeSet<&str> = ControlMethod::ALL.iter().map(|m| m.name()).collect();
1079 for name in [
1080 "control.chiaPeers.add",
1081 "control.chiaPeers.list",
1082 "control.chiaPeers.remove",
1083 ] {
1084 assert!(declared.contains(name), "{name} is not in the catalog");
1085 let m = ControlMethod::from_name(name).expect("from_name round-trips");
1086 assert_eq!(m.category(), Category::Peers, "{name} is a peers method");
1087 assert_eq!(m.routing(), Routing::Owned, "{name} is served by the shell");
1088 assert!(m.requires_auth(), "{name} must require the control token");
1089 assert!(!m.is_open_read(), "{name} is not an open read");
1090 }
1091 assert!(ControlMethod::ChiaPeersAdd.requires_master_token());
1095 assert!(ControlMethod::ChiaPeersRemove.requires_master_token());
1096 assert!(
1097 !ControlMethod::ChiaPeersList.requires_master_token(),
1098 "list grants nothing that outlives the token; gating it would blind a paired client \
1099 to the trust state it is subject to"
1100 );
1101 for name in ["control.chiaPeers.add", "control.chiaPeers.remove"] {
1105 let summary = ControlMethod::from_name(name).unwrap().summary();
1106 assert!(
1107 summary.to_lowercase().contains("corroboration"),
1108 "{name} summary must name the corroboration bypass, got: {summary}"
1109 );
1110 }
1111 }
1112
1113 #[test]
1126 fn set_mirror_advertise_urls_is_ordinary_tier_and_config_category() {
1127 let m = ControlMethod::ConfigSetMirrorAdvertiseUrls;
1128 assert_eq!(m.name(), "control.config.setMirrorAdvertiseUrls");
1129 assert_eq!(m.category(), Category::Config);
1130 assert_eq!(m.routing(), Routing::Owned);
1131 assert!(m.requires_auth(), "a mutation on this plane is never open");
1132 assert!(!m.requires_master_token());
1133 assert!(!m.is_open_read());
1134 assert!(!m.is_pairing_admin());
1135 }
1136
1137 #[test]
1147 fn mirror_reconcile_is_ordinary_tier_and_collateral_category() {
1148 let m = ControlMethod::MirrorReconcile;
1149 assert_eq!(m.name(), "control.mirror.reconcile");
1150 assert_eq!(m.category(), Category::Collateral);
1151 assert_eq!(m.routing(), Routing::Owned);
1152 assert!(m.requires_auth(), "a money-moving mutation is never open");
1153 assert!(!m.requires_master_token());
1154 assert!(!m.is_open_read());
1155 assert!(!m.is_pairing_admin());
1156 }
1157
1158 #[test]
1159 fn every_method_has_a_nonempty_summary() {
1160 for &m in ControlMethod::ALL {
1161 assert!(!m.summary().is_empty(), "{} has no summary", m.name());
1162 }
1163 }
1164}