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 LogSetLevel,
92
93 CacheGet,
96 CacheSetCap,
98 CacheClear,
100
101 HostedStoresList,
104 HostedStoresPin,
106 HostedStoresUnpin,
108 HostedStoresStatus,
110 CapsuleFetch,
113
114 SyncStatus,
117 SyncTrigger,
119
120 UpdaterStatus,
123 UpdaterSetChannel,
125 UpdaterPause,
127 UpdaterResume,
129 UpdaterCheckNow,
131
132 PairingList,
135 PairingApprove,
137 PairingRevoke,
139
140 PeerStatus,
143 PeerCounts,
145 PeersConnect,
147 PeersDisconnect,
149
150 ChiaPeersAdd,
166 ChiaPeersList,
168 ChiaPeersRemove,
170
171 Subscribe,
174 Unsubscribe,
176 ListSubscriptions,
178
179 WalletBalance,
182 WalletCoins,
184 WalletCoinById,
186 WalletCoinSpend,
188 WalletCoinsByParent,
190 WalletArrivals,
192 WalletOperatorAddress,
194 WalletPeak,
196 WalletSyncStatus,
198 WalletBroadcast,
200 WalletWatch,
202 WalletUnwatch,
204 WalletWatched,
206 WalletReservationsHeld,
208 WalletReservationsReserve,
210 WalletReservationsRelease,
212 WalletResetCoinDb,
214
215 SpendsList,
218
219 CollateralRequirement,
222 CollateralMarginGet,
224 CollateralMarginSet,
226 CollateralBuffer,
228 MirrorBondStates,
231
232 ProfilePutBody,
235 ProfileGetBody,
237
238 PairingRequest,
241 PairingPoll,
243}
244
245impl ControlMethod {
246 pub const fn name(self) -> &'static str {
248 match self {
249 ControlMethod::Status => "control.status",
250 ControlMethod::ConfigGet => "control.config.get",
251 ControlMethod::ConfigSetUpstream => "control.config.setUpstream",
252 ControlMethod::LogSetLevel => "control.log.setLevel",
253 ControlMethod::CacheGet => "control.cache.get",
254 ControlMethod::CacheSetCap => "control.cache.setCap",
255 ControlMethod::CacheClear => "control.cache.clear",
256 ControlMethod::HostedStoresList => "control.hostedStores.list",
257 ControlMethod::HostedStoresPin => "control.hostedStores.pin",
258 ControlMethod::HostedStoresUnpin => "control.hostedStores.unpin",
259 ControlMethod::HostedStoresStatus => "control.hostedStores.status",
260 ControlMethod::CapsuleFetch => "control.capsule.fetch",
261 ControlMethod::SyncStatus => "control.sync.status",
262 ControlMethod::SyncTrigger => "control.sync.trigger",
263 ControlMethod::UpdaterStatus => "control.updater.status",
264 ControlMethod::UpdaterSetChannel => "control.updater.setChannel",
265 ControlMethod::UpdaterPause => "control.updater.pause",
266 ControlMethod::UpdaterResume => "control.updater.resume",
267 ControlMethod::UpdaterCheckNow => "control.updater.checkNow",
268 ControlMethod::PairingList => "control.pairing.list",
269 ControlMethod::PairingApprove => "control.pairing.approve",
270 ControlMethod::PairingRevoke => "control.pairing.revoke",
271 ControlMethod::PeerStatus => "control.peerStatus",
272 ControlMethod::PeerCounts => "control.peerCounts",
273 ControlMethod::PeersConnect => "control.peers.connect",
274 ControlMethod::PeersDisconnect => "control.peers.disconnect",
275 ControlMethod::ChiaPeersAdd => "control.chiaPeers.add",
276 ControlMethod::ChiaPeersList => "control.chiaPeers.list",
277 ControlMethod::ChiaPeersRemove => "control.chiaPeers.remove",
278 ControlMethod::Subscribe => "control.subscribe",
279 ControlMethod::Unsubscribe => "control.unsubscribe",
280 ControlMethod::ListSubscriptions => "control.listSubscriptions",
281 ControlMethod::WalletBalance => "control.wallet.balance",
282 ControlMethod::WalletCoins => "control.wallet.coins",
283 ControlMethod::WalletCoinById => "control.wallet.coinById",
284 ControlMethod::WalletCoinSpend => "control.wallet.coinSpend",
285 ControlMethod::WalletCoinsByParent => "control.wallet.coinsByParent",
286 ControlMethod::WalletArrivals => "control.wallet.arrivals",
287 ControlMethod::WalletOperatorAddress => "control.wallet.operatorAddress",
288 ControlMethod::WalletPeak => "control.wallet.peak",
289 ControlMethod::WalletSyncStatus => "control.wallet.syncStatus",
290 ControlMethod::WalletBroadcast => "control.wallet.broadcast",
291 ControlMethod::WalletWatch => "control.wallet.watch",
292 ControlMethod::WalletUnwatch => "control.wallet.unwatch",
293 ControlMethod::WalletWatched => "control.wallet.watched",
294 ControlMethod::WalletReservationsHeld => "control.wallet.reservations.held",
295 ControlMethod::WalletReservationsReserve => "control.wallet.reservations.reserve",
296 ControlMethod::WalletReservationsRelease => "control.wallet.reservations.release",
297 ControlMethod::WalletResetCoinDb => "control.wallet.resetCoinDb",
298 ControlMethod::SpendsList => "control.spends.list",
299 ControlMethod::CollateralRequirement => "control.collateral.requirement",
300 ControlMethod::CollateralMarginGet => "control.collateral.margin.get",
301 ControlMethod::CollateralMarginSet => "control.collateral.margin.set",
302 ControlMethod::CollateralBuffer => "control.collateral.buffer",
303 ControlMethod::MirrorBondStates => "control.mirror.bondStates",
304 ControlMethod::ProfilePutBody => "control.profile.putBody",
305 ControlMethod::ProfileGetBody => "control.profile.getBody",
306 ControlMethod::PairingRequest => "pairing.request",
307 ControlMethod::PairingPoll => "pairing.poll",
308 }
309 }
310
311 pub fn from_name(name: &str) -> Option<ControlMethod> {
313 ControlMethod::ALL
314 .iter()
315 .copied()
316 .find(|m| m.name() == name)
317 }
318
319 pub const fn requires_auth(self) -> bool {
346 !self.is_open_read()
347 && !matches!(
348 self,
349 ControlMethod::PairingRequest | ControlMethod::PairingPoll
350 )
351 }
352
353 pub const fn is_open_read(self) -> bool {
387 matches!(
388 self,
389 ControlMethod::WalletBalance
390 | ControlMethod::WalletCoins
391 | ControlMethod::WalletCoinById
392 | ControlMethod::WalletCoinSpend
393 | ControlMethod::WalletCoinsByParent
394 | ControlMethod::WalletPeak
395 | ControlMethod::WalletSyncStatus
396 | ControlMethod::PeerCounts
397 )
398 }
399
400 pub const fn is_pairing_admin(self) -> bool {
409 matches!(
410 self,
411 ControlMethod::PairingList
412 | ControlMethod::PairingApprove
413 | ControlMethod::PairingRevoke
414 )
415 }
416
417 pub const fn requires_master_token(self) -> bool {
444 self.is_pairing_admin()
445 || matches!(
446 self,
447 ControlMethod::ChiaPeersAdd | ControlMethod::ChiaPeersRemove
448 )
449 }
450
451 pub const fn routing(self) -> Routing {
453 match self {
454 ControlMethod::PeerStatus
455 | ControlMethod::PeerCounts
456 | ControlMethod::PeersConnect
457 | ControlMethod::PeersDisconnect
458 | ControlMethod::Subscribe
459 | ControlMethod::Unsubscribe
460 | ControlMethod::ListSubscriptions
461 | ControlMethod::WalletBalance
462 | ControlMethod::WalletCoins
463 | ControlMethod::WalletCoinById
464 | ControlMethod::WalletCoinSpend
465 | ControlMethod::WalletCoinsByParent
466 | ControlMethod::WalletArrivals
467 | ControlMethod::WalletPeak
468 | ControlMethod::WalletSyncStatus
469 | ControlMethod::WalletBroadcast
470 | ControlMethod::WalletWatch
471 | ControlMethod::WalletUnwatch
472 | ControlMethod::WalletWatched
473 | ControlMethod::WalletReservationsHeld
474 | ControlMethod::WalletReservationsReserve
475 | ControlMethod::WalletReservationsRelease
476 | ControlMethod::WalletResetCoinDb
477 | ControlMethod::ProfilePutBody
478 | ControlMethod::ProfileGetBody => Routing::Delegated,
479 ControlMethod::PairingRequest | ControlMethod::PairingPoll => Routing::OpenBootstrap,
480 _ => Routing::Owned,
481 }
482 }
483
484 pub const fn category(self) -> Category {
486 match self {
487 ControlMethod::Status => Category::Status,
488 ControlMethod::ConfigGet | ControlMethod::ConfigSetUpstream => Category::Config,
489 ControlMethod::LogSetLevel => Category::Log,
490 ControlMethod::CacheGet | ControlMethod::CacheSetCap | ControlMethod::CacheClear => {
491 Category::Cache
492 }
493 ControlMethod::HostedStoresList
494 | ControlMethod::HostedStoresPin
495 | ControlMethod::HostedStoresUnpin
496 | ControlMethod::HostedStoresStatus
497 | ControlMethod::CapsuleFetch => Category::HostedStores,
498 ControlMethod::SyncStatus | ControlMethod::SyncTrigger => Category::Sync,
499 ControlMethod::UpdaterStatus
500 | ControlMethod::UpdaterSetChannel
501 | ControlMethod::UpdaterPause
502 | ControlMethod::UpdaterResume
503 | ControlMethod::UpdaterCheckNow => Category::Updater,
504 ControlMethod::PairingList
505 | ControlMethod::PairingApprove
506 | ControlMethod::PairingRevoke
507 | ControlMethod::PairingRequest
508 | ControlMethod::PairingPoll => Category::Pairing,
509 ControlMethod::PeerStatus
510 | ControlMethod::PeerCounts
511 | ControlMethod::PeersConnect
512 | ControlMethod::PeersDisconnect
513 | ControlMethod::ChiaPeersAdd
514 | ControlMethod::ChiaPeersList
515 | ControlMethod::ChiaPeersRemove => Category::Peers,
516 ControlMethod::Subscribe
517 | ControlMethod::Unsubscribe
518 | ControlMethod::ListSubscriptions => Category::Subscriptions,
519 ControlMethod::WalletBalance
520 | ControlMethod::WalletCoins
521 | ControlMethod::WalletCoinById
522 | ControlMethod::WalletCoinSpend
523 | ControlMethod::WalletCoinsByParent
524 | ControlMethod::WalletArrivals
525 | ControlMethod::WalletPeak
526 | ControlMethod::WalletSyncStatus
527 | ControlMethod::WalletOperatorAddress
528 | ControlMethod::WalletBroadcast
529 | ControlMethod::WalletWatch
530 | ControlMethod::WalletUnwatch
531 | ControlMethod::WalletWatched
532 | ControlMethod::WalletReservationsHeld
533 | ControlMethod::WalletReservationsReserve
534 | ControlMethod::WalletReservationsRelease
535 | ControlMethod::WalletResetCoinDb => Category::Wallet,
536 ControlMethod::SpendsList => Category::Spends,
537 ControlMethod::CollateralRequirement
538 | ControlMethod::CollateralMarginGet
539 | ControlMethod::CollateralMarginSet
540 | ControlMethod::CollateralBuffer
541 | ControlMethod::MirrorBondStates => Category::Collateral,
542 ControlMethod::ProfilePutBody | ControlMethod::ProfileGetBody => Category::Profile,
543 }
544 }
545
546 pub const fn summary(self) -> &'static str {
548 match self {
549 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.",
550 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.",
551 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.",
552 ControlMethod::Status => "A rich node status snapshot (version, uptime, addr, cache, hosted/pinned counts, sync availability).",
553 ControlMethod::ConfigGet => "The node's effective configuration (addr/port, upstream + override, cache dir/shared, config path, sync availability).",
554 ControlMethod::ConfigSetUpstream => "Persist an upstream-RPC override; takes effect on next node start (requires_restart).",
555 ControlMethod::LogSetLevel => "Live-swap the running node's tracing EnvFilter directive (not persisted).",
556 ControlMethod::CacheGet => "The on-disk content-cache view: cap_bytes, used_bytes, dir, shared.",
557 ControlMethod::CacheSetCap => "Set the on-disk cache size cap in bytes (floored at 64 MiB).",
558 ControlMethod::CacheClear => "Delete all locally cached DIG content.",
559 ControlMethod::HostedStoresList => "Every held/pinned store, merged, with each store's cached capsules and a pinned flag.",
560 ControlMethod::HostedStoresPin => "Pin a store (storeId[:rootHash]); pre-fetches the capsule when a root is given and §21 sync is available.",
561 ControlMethod::HostedStoresUnpin => "Unpin a store and evict its cached capsules.",
562 ControlMethod::HostedStoresStatus => "Per-store status: pinned flag, cached capsules, total bytes.",
563 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.",
564 ControlMethod::SyncStatus => "Whether authenticated §21 whole-store sync is available, plus pinned-store cache coverage.",
565 ControlMethod::SyncTrigger => "Trigger a §21 sync for one capsule (storeId + root).",
566 ControlMethod::UpdaterStatus => "The DIG auto-update beacon's current status (proxied from dig-updater).",
567 ControlMethod::UpdaterSetChannel => "Set the beacon's update channel (\"nightly\" | \"stable\").",
568 ControlMethod::UpdaterPause => "Suspend the beacon's auto-updates (optionally until a unix time).",
569 ControlMethod::UpdaterResume => "Resume the beacon's auto-updates.",
570 ControlMethod::UpdaterCheckNow => "Force an immediate beacon update check.",
571 ControlMethod::PairingList => "List pending pairing requests and issued paired tokens (MASTER token only).",
572 ControlMethod::PairingApprove => "Approve a pending pairing, minting a scoped token (MASTER token only).",
573 ControlMethod::PairingRevoke => "Revoke an issued paired token by token_id (MASTER token only).",
574 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.",
575 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.",
576 ControlMethod::PeersConnect => "Dial a peer by address, or resolve an already-connected peer_id, via the live gossip pool.",
577 ControlMethod::PeersDisconnect => "Drop a pooled peer by peer_id, closing its mTLS link (idempotent).",
578 ControlMethod::Subscribe => "Subscribe the node to a store it actively watches and gap-fills.",
579 ControlMethod::Unsubscribe => "Stop watching a store.",
580 ControlMethod::ListSubscriptions => "The node's persisted subscription set + count.",
581 ControlMethod::WalletCoins => "READ-only: the spendable coin records for an address + asset, with the tier that answered and the height they reflect.",
582 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.",
583 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.",
584 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.",
585 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`.",
586 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.",
587 ControlMethod::WalletPeak => "READ-only: the node's current chain peak height, independent of any address.",
588 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).",
589 ControlMethod::WalletBroadcast => "Push an ALREADY-SIGNED spend bundle to the network; the node never signs. TOKEN-GATED.",
590 ControlMethod::WalletBalance => "READ-only: the confirmed spendable balance for an address + asset (plus pending, sync freshness, and the peak height it reflects).",
591 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.",
592 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.",
593 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.",
594 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.",
595 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.",
596 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.",
597 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.",
598 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.",
599 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.",
600 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.",
601 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.",
602 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.",
603 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.",
604 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.",
605 ControlMethod::MirrorBondStates => "READ-only: the state of every mirror bond this node holds, keyed per (store, root), plus the $DIG those bonds have LOCKED. Seven states, and six 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), `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) 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 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.",
606 ControlMethod::PairingRequest => "OPEN: request a control-token pairing; returns a pairing_id + pairing_code to compare.",
607 ControlMethod::PairingPoll => "OPEN: poll a pairing by id; once the operator approves, returns the scoped token once.",
608 }
609 }
610
611 pub const ALL: &'static [ControlMethod] = &[
614 ControlMethod::Status,
615 ControlMethod::ConfigGet,
616 ControlMethod::ConfigSetUpstream,
617 ControlMethod::LogSetLevel,
618 ControlMethod::CacheGet,
619 ControlMethod::CacheSetCap,
620 ControlMethod::CacheClear,
621 ControlMethod::HostedStoresList,
622 ControlMethod::HostedStoresPin,
623 ControlMethod::HostedStoresUnpin,
624 ControlMethod::HostedStoresStatus,
625 ControlMethod::CapsuleFetch,
626 ControlMethod::SyncStatus,
627 ControlMethod::SyncTrigger,
628 ControlMethod::UpdaterStatus,
629 ControlMethod::UpdaterSetChannel,
630 ControlMethod::UpdaterPause,
631 ControlMethod::UpdaterResume,
632 ControlMethod::UpdaterCheckNow,
633 ControlMethod::PairingList,
634 ControlMethod::PairingApprove,
635 ControlMethod::PairingRevoke,
636 ControlMethod::PeerStatus,
637 ControlMethod::PeerCounts,
638 ControlMethod::PeersConnect,
639 ControlMethod::PeersDisconnect,
640 ControlMethod::ChiaPeersAdd,
641 ControlMethod::ChiaPeersList,
642 ControlMethod::ChiaPeersRemove,
643 ControlMethod::Subscribe,
644 ControlMethod::Unsubscribe,
645 ControlMethod::ListSubscriptions,
646 ControlMethod::WalletBalance,
647 ControlMethod::WalletCoins,
648 ControlMethod::WalletCoinById,
649 ControlMethod::WalletCoinSpend,
650 ControlMethod::WalletCoinsByParent,
651 ControlMethod::WalletArrivals,
652 ControlMethod::WalletPeak,
653 ControlMethod::WalletSyncStatus,
654 ControlMethod::WalletOperatorAddress,
655 ControlMethod::WalletBroadcast,
656 ControlMethod::WalletWatch,
657 ControlMethod::WalletUnwatch,
658 ControlMethod::WalletWatched,
659 ControlMethod::WalletReservationsHeld,
660 ControlMethod::WalletReservationsReserve,
661 ControlMethod::WalletReservationsRelease,
662 ControlMethod::WalletResetCoinDb,
663 ControlMethod::SpendsList,
664 ControlMethod::CollateralRequirement,
665 ControlMethod::CollateralMarginGet,
666 ControlMethod::CollateralMarginSet,
667 ControlMethod::CollateralBuffer,
668 ControlMethod::MirrorBondStates,
669 ControlMethod::ProfilePutBody,
670 ControlMethod::ProfileGetBody,
671 ControlMethod::PairingRequest,
672 ControlMethod::PairingPoll,
673 ];
674}
675
676#[cfg(test)]
677mod tests {
678 use super::*;
679 use std::collections::BTreeSet;
680
681 #[test]
682 fn every_method_has_a_unique_wire_name() {
683 let names: BTreeSet<&str> = ControlMethod::ALL.iter().map(|m| m.name()).collect();
684 assert_eq!(
685 names.len(),
686 ControlMethod::ALL.len(),
687 "duplicate or missing wire names in the catalog"
688 );
689 }
690
691 #[test]
692 fn from_name_round_trips_every_method() {
693 for &m in ControlMethod::ALL {
694 assert_eq!(ControlMethod::from_name(m.name()), Some(m));
695 }
696 assert_eq!(ControlMethod::from_name("control.nope"), None);
697 assert_eq!(ControlMethod::from_name(""), None);
698 }
699
700 #[test]
701 fn the_token_less_surface_is_exactly_the_bootstrap_plus_the_chain_reads() {
702 let expected_open: BTreeSet<&str> = [
706 "pairing.request",
707 "pairing.poll",
708 "control.wallet.balance",
709 "control.wallet.coins",
710 "control.wallet.coinById",
711 "control.wallet.coinSpend",
712 "control.wallet.coinsByParent",
713 "control.wallet.peak",
714 "control.wallet.syncStatus",
715 "control.peerCounts",
716 ]
717 .into_iter()
718 .collect();
719 assert_eq!(
720 expected_open.len(),
721 10,
722 "the open surface is ten named methods"
723 );
724 let actual_open: BTreeSet<&str> = ControlMethod::ALL
725 .iter()
726 .filter(|m| !m.requires_auth())
727 .map(|m| m.name())
728 .collect();
729 assert_eq!(actual_open, expected_open);
730 }
731
732 #[test]
741 fn the_gated_wallet_methods_are_the_push_the_cursor_and_enrolment() {
742 let gated: Vec<&str> = ControlMethod::ALL
743 .iter()
744 .filter(|m| m.category() == Category::Wallet && m.requires_auth())
745 .map(|m| m.name())
746 .collect();
747 assert_eq!(
748 gated,
749 vec![
750 "control.wallet.arrivals",
751 "control.wallet.operatorAddress",
755 "control.wallet.broadcast",
756 "control.wallet.watch",
757 "control.wallet.unwatch",
758 "control.wallet.watched",
759 "control.wallet.reservations.held",
760 "control.wallet.reservations.reserve",
761 "control.wallet.reservations.release",
762 "control.wallet.resetCoinDb",
763 ]
764 );
765 assert!(!ControlMethod::WalletBroadcast.is_open_read());
766 }
767
768 #[test]
780 fn the_arrival_cursor_is_not_an_open_read() {
781 assert!(
782 !ControlMethod::WalletArrivals.is_open_read(),
783 "control.wallet.arrivals discloses this node's OWN watched puzzle hashes to a caller \
784 that supplied nothing, so it MUST NOT be served token-less"
785 );
786 assert!(ControlMethod::WalletArrivals.requires_auth());
787 assert!(
788 ControlMethod::WalletCoinById.is_open_read(),
789 "the caller-addressed reads stay open -- the fix is the membership rule, not gating \
790 the wallet category"
791 );
792 }
793
794 #[test]
813 fn the_catalog_serves_every_chain_source_primitive() {
814 for wire in [
815 "control.wallet.coinById", "control.wallet.coins", "control.wallet.peak", "control.wallet.coinsByParent", "control.wallet.coinSpend", ] {
821 assert!(
822 ControlMethod::from_name(wire).is_some(),
823 "{wire} is required to implement ChainSource over the control plane"
824 );
825 }
826 }
827
828 #[test]
835 fn the_chain_primitives_are_caller_named_open_reads() {
836 for method in [
837 ControlMethod::WalletCoinSpend,
838 ControlMethod::WalletCoinsByParent,
839 ] {
840 assert!(
841 method.is_open_read(),
842 "{} names its subject in the request and discloses no node-to-address \
843 association, exactly like control.wallet.coinById",
844 method.name()
845 );
846 assert!(!method.requires_auth());
847 }
848 assert!(
849 ControlMethod::WalletArrivals.requires_auth(),
850 "the caller-supplies-nothing read stays gated -- the rule is who names the subject, \
851 not whether the bytes are on chain"
852 );
853 assert!(ControlMethod::WalletBroadcast.requires_auth());
854 }
855
856 #[test]
870 fn the_enrolment_methods_are_gated_including_the_read() {
871 for wire in [
872 "control.wallet.watch",
873 "control.wallet.unwatch",
874 "control.wallet.watched",
875 ] {
876 let method = ControlMethod::from_name(wire)
877 .unwrap_or_else(|| panic!("{wire} must be in the catalog"));
878 assert!(
879 !method.is_open_read(),
880 "{wire} either aims this node's subscriptions or names the keys it already \
881 follows, so it MUST NOT be served token-less"
882 );
883 assert!(method.requires_auth(), "{wire} must require the token");
884 assert_eq!(method.category(), Category::Wallet);
885 assert_eq!(method.routing(), Routing::Delegated);
886 }
887 assert!(
888 ControlMethod::WalletCoinById.is_open_read(),
889 "the caller-addressed reads stay open -- enrolment is gated by the membership rule, \
890 not by gating the wallet category"
891 );
892 }
893
894 #[test]
895 fn only_pairing_bootstrap_is_open_bootstrap_routed() {
896 for &m in ControlMethod::ALL {
897 let open_bootstrap = matches!(
898 m,
899 ControlMethod::PairingRequest | ControlMethod::PairingPoll
900 );
901 assert_eq!(
902 m.routing() == Routing::OpenBootstrap,
903 open_bootstrap,
904 "{} routing mismatch",
905 m.name()
906 );
907 }
908 }
909
910 #[test]
911 fn pairing_admin_methods_are_exactly_three() {
912 let admin: Vec<&str> = ControlMethod::ALL
913 .iter()
914 .filter(|m| m.is_pairing_admin())
915 .map(|m| m.name())
916 .collect();
917 assert_eq!(
918 admin,
919 vec![
920 "control.pairing.list",
921 "control.pairing.approve",
922 "control.pairing.revoke"
923 ]
924 );
925 }
926
927 #[test]
934 fn the_master_token_tier_is_pairing_admin_plus_the_trusted_peer_mutations() {
935 let master: BTreeSet<&str> = ControlMethod::ALL
936 .iter()
937 .filter(|m| m.requires_master_token())
938 .map(|m| m.name())
939 .collect();
940 let expected: BTreeSet<&str> = [
941 "control.pairing.list",
942 "control.pairing.approve",
943 "control.pairing.revoke",
944 "control.chiaPeers.add",
945 "control.chiaPeers.remove",
946 ]
947 .into_iter()
948 .collect();
949 assert_eq!(master, expected);
950
951 for &m in ControlMethod::ALL {
954 assert!(
955 !m.is_pairing_admin() || m.requires_master_token(),
956 "{} is pairing-admin but not master-tier",
957 m.name()
958 );
959 }
960 assert!(
961 master.len()
962 > ControlMethod::ALL
963 .iter()
964 .filter(|m| m.is_pairing_admin())
965 .count(),
966 "the two predicates must not be interchangeable"
967 );
968
969 for &m in ControlMethod::ALL {
971 assert!(
972 !m.requires_master_token() || m.requires_auth(),
973 "{}",
974 m.name()
975 );
976 }
977 }
978
979 #[test]
986 fn the_add_summary_authorises_only_a_node_the_operator_runs() {
987 let summary = ControlMethod::ChiaPeersAdd.summary().to_lowercase();
988 assert!(
989 summary.contains("a node you run"),
990 "add must name the operator-run scope, got: {summary}"
991 );
992 for widened in ["vouch", "otherwise trust", "trust yourself", "recommend"] {
993 assert!(
994 !summary.contains(widened),
995 "add summary widens operator trust past NC-12 with {widened:?}: {summary}"
996 );
997 }
998 }
999
1000 #[test]
1001 fn delegated_set_matches_the_engine_surface() {
1002 let delegated: BTreeSet<&str> = ControlMethod::ALL
1003 .iter()
1004 .filter(|m| m.routing() == Routing::Delegated)
1005 .map(|m| m.name())
1006 .collect();
1007 let expected: BTreeSet<&str> = [
1008 "control.wallet.coins",
1009 "control.wallet.coinById",
1010 "control.wallet.coinSpend",
1011 "control.wallet.coinsByParent",
1012 "control.wallet.arrivals",
1013 "control.wallet.peak",
1014 "control.wallet.syncStatus",
1015 "control.wallet.broadcast",
1016 "control.wallet.watch",
1017 "control.wallet.unwatch",
1018 "control.wallet.watched",
1019 "control.wallet.reservations.held",
1020 "control.wallet.reservations.reserve",
1021 "control.wallet.reservations.release",
1022 "control.wallet.resetCoinDb",
1023 "control.profile.putBody",
1024 "control.profile.getBody",
1025 "control.peerStatus",
1026 "control.peerCounts",
1027 "control.peers.connect",
1028 "control.peers.disconnect",
1029 "control.subscribe",
1030 "control.unsubscribe",
1031 "control.listSubscriptions",
1032 "control.wallet.balance",
1033 ]
1034 .into_iter()
1035 .collect();
1036 assert_eq!(delegated, expected);
1037 }
1038
1039 #[test]
1046 fn the_trusted_chia_peer_methods_are_gated_and_disclose_the_corroboration_bypass() {
1047 let declared: BTreeSet<&str> = ControlMethod::ALL.iter().map(|m| m.name()).collect();
1048 for name in [
1049 "control.chiaPeers.add",
1050 "control.chiaPeers.list",
1051 "control.chiaPeers.remove",
1052 ] {
1053 assert!(declared.contains(name), "{name} is not in the catalog");
1054 let m = ControlMethod::from_name(name).expect("from_name round-trips");
1055 assert_eq!(m.category(), Category::Peers, "{name} is a peers method");
1056 assert_eq!(m.routing(), Routing::Owned, "{name} is served by the shell");
1057 assert!(m.requires_auth(), "{name} must require the control token");
1058 assert!(!m.is_open_read(), "{name} is not an open read");
1059 }
1060 assert!(ControlMethod::ChiaPeersAdd.requires_master_token());
1064 assert!(ControlMethod::ChiaPeersRemove.requires_master_token());
1065 assert!(
1066 !ControlMethod::ChiaPeersList.requires_master_token(),
1067 "list grants nothing that outlives the token; gating it would blind a paired client \
1068 to the trust state it is subject to"
1069 );
1070 for name in ["control.chiaPeers.add", "control.chiaPeers.remove"] {
1074 let summary = ControlMethod::from_name(name).unwrap().summary();
1075 assert!(
1076 summary.to_lowercase().contains("corroboration"),
1077 "{name} summary must name the corroboration bypass, got: {summary}"
1078 );
1079 }
1080 }
1081
1082 #[test]
1083 fn every_method_has_a_nonempty_summary() {
1084 for &m in ControlMethod::ALL {
1085 assert!(!m.summary().is_empty(), "{} has no summary", m.name());
1086 }
1087 }
1088}