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 WalletPeak,
194 WalletSyncStatus,
196 WalletBroadcast,
198 WalletWatch,
200 WalletUnwatch,
202 WalletWatched,
204 WalletReservationsHeld,
206 WalletReservationsReserve,
208 WalletReservationsRelease,
210
211 SpendsList,
214
215 CollateralRequirement,
218 CollateralMarginGet,
220 CollateralMarginSet,
222
223 ProfilePutBody,
226 ProfileGetBody,
228
229 PairingRequest,
232 PairingPoll,
234}
235
236impl ControlMethod {
237 pub const fn name(self) -> &'static str {
239 match self {
240 ControlMethod::Status => "control.status",
241 ControlMethod::ConfigGet => "control.config.get",
242 ControlMethod::ConfigSetUpstream => "control.config.setUpstream",
243 ControlMethod::LogSetLevel => "control.log.setLevel",
244 ControlMethod::CacheGet => "control.cache.get",
245 ControlMethod::CacheSetCap => "control.cache.setCap",
246 ControlMethod::CacheClear => "control.cache.clear",
247 ControlMethod::HostedStoresList => "control.hostedStores.list",
248 ControlMethod::HostedStoresPin => "control.hostedStores.pin",
249 ControlMethod::HostedStoresUnpin => "control.hostedStores.unpin",
250 ControlMethod::HostedStoresStatus => "control.hostedStores.status",
251 ControlMethod::CapsuleFetch => "control.capsule.fetch",
252 ControlMethod::SyncStatus => "control.sync.status",
253 ControlMethod::SyncTrigger => "control.sync.trigger",
254 ControlMethod::UpdaterStatus => "control.updater.status",
255 ControlMethod::UpdaterSetChannel => "control.updater.setChannel",
256 ControlMethod::UpdaterPause => "control.updater.pause",
257 ControlMethod::UpdaterResume => "control.updater.resume",
258 ControlMethod::UpdaterCheckNow => "control.updater.checkNow",
259 ControlMethod::PairingList => "control.pairing.list",
260 ControlMethod::PairingApprove => "control.pairing.approve",
261 ControlMethod::PairingRevoke => "control.pairing.revoke",
262 ControlMethod::PeerStatus => "control.peerStatus",
263 ControlMethod::PeerCounts => "control.peerCounts",
264 ControlMethod::PeersConnect => "control.peers.connect",
265 ControlMethod::PeersDisconnect => "control.peers.disconnect",
266 ControlMethod::ChiaPeersAdd => "control.chiaPeers.add",
267 ControlMethod::ChiaPeersList => "control.chiaPeers.list",
268 ControlMethod::ChiaPeersRemove => "control.chiaPeers.remove",
269 ControlMethod::Subscribe => "control.subscribe",
270 ControlMethod::Unsubscribe => "control.unsubscribe",
271 ControlMethod::ListSubscriptions => "control.listSubscriptions",
272 ControlMethod::WalletBalance => "control.wallet.balance",
273 ControlMethod::WalletCoins => "control.wallet.coins",
274 ControlMethod::WalletCoinById => "control.wallet.coinById",
275 ControlMethod::WalletCoinSpend => "control.wallet.coinSpend",
276 ControlMethod::WalletCoinsByParent => "control.wallet.coinsByParent",
277 ControlMethod::WalletArrivals => "control.wallet.arrivals",
278 ControlMethod::WalletPeak => "control.wallet.peak",
279 ControlMethod::WalletSyncStatus => "control.wallet.syncStatus",
280 ControlMethod::WalletBroadcast => "control.wallet.broadcast",
281 ControlMethod::WalletWatch => "control.wallet.watch",
282 ControlMethod::WalletUnwatch => "control.wallet.unwatch",
283 ControlMethod::WalletWatched => "control.wallet.watched",
284 ControlMethod::WalletReservationsHeld => "control.wallet.reservations.held",
285 ControlMethod::WalletReservationsReserve => "control.wallet.reservations.reserve",
286 ControlMethod::WalletReservationsRelease => "control.wallet.reservations.release",
287 ControlMethod::SpendsList => "control.spends.list",
288 ControlMethod::CollateralRequirement => "control.collateral.requirement",
289 ControlMethod::CollateralMarginGet => "control.collateral.margin.get",
290 ControlMethod::CollateralMarginSet => "control.collateral.margin.set",
291 ControlMethod::ProfilePutBody => "control.profile.putBody",
292 ControlMethod::ProfileGetBody => "control.profile.getBody",
293 ControlMethod::PairingRequest => "pairing.request",
294 ControlMethod::PairingPoll => "pairing.poll",
295 }
296 }
297
298 pub fn from_name(name: &str) -> Option<ControlMethod> {
300 ControlMethod::ALL
301 .iter()
302 .copied()
303 .find(|m| m.name() == name)
304 }
305
306 pub const fn requires_auth(self) -> bool {
333 !self.is_open_read()
334 && !matches!(
335 self,
336 ControlMethod::PairingRequest | ControlMethod::PairingPoll
337 )
338 }
339
340 pub const fn is_open_read(self) -> bool {
374 matches!(
375 self,
376 ControlMethod::WalletBalance
377 | ControlMethod::WalletCoins
378 | ControlMethod::WalletCoinById
379 | ControlMethod::WalletCoinSpend
380 | ControlMethod::WalletCoinsByParent
381 | ControlMethod::WalletPeak
382 | ControlMethod::WalletSyncStatus
383 | ControlMethod::PeerCounts
384 )
385 }
386
387 pub const fn is_pairing_admin(self) -> bool {
396 matches!(
397 self,
398 ControlMethod::PairingList
399 | ControlMethod::PairingApprove
400 | ControlMethod::PairingRevoke
401 )
402 }
403
404 pub const fn requires_master_token(self) -> bool {
431 self.is_pairing_admin()
432 || matches!(
433 self,
434 ControlMethod::ChiaPeersAdd | ControlMethod::ChiaPeersRemove
435 )
436 }
437
438 pub const fn routing(self) -> Routing {
440 match self {
441 ControlMethod::PeerStatus
442 | ControlMethod::PeerCounts
443 | ControlMethod::PeersConnect
444 | ControlMethod::PeersDisconnect
445 | ControlMethod::Subscribe
446 | ControlMethod::Unsubscribe
447 | ControlMethod::ListSubscriptions
448 | ControlMethod::WalletBalance
449 | ControlMethod::WalletCoins
450 | ControlMethod::WalletCoinById
451 | ControlMethod::WalletCoinSpend
452 | ControlMethod::WalletCoinsByParent
453 | ControlMethod::WalletArrivals
454 | ControlMethod::WalletPeak
455 | ControlMethod::WalletSyncStatus
456 | ControlMethod::WalletBroadcast
457 | ControlMethod::WalletWatch
458 | ControlMethod::WalletUnwatch
459 | ControlMethod::WalletWatched
460 | ControlMethod::WalletReservationsHeld
461 | ControlMethod::WalletReservationsReserve
462 | ControlMethod::WalletReservationsRelease
463 | ControlMethod::ProfilePutBody
464 | ControlMethod::ProfileGetBody => Routing::Delegated,
465 ControlMethod::PairingRequest | ControlMethod::PairingPoll => Routing::OpenBootstrap,
466 _ => Routing::Owned,
467 }
468 }
469
470 pub const fn category(self) -> Category {
472 match self {
473 ControlMethod::Status => Category::Status,
474 ControlMethod::ConfigGet | ControlMethod::ConfigSetUpstream => Category::Config,
475 ControlMethod::LogSetLevel => Category::Log,
476 ControlMethod::CacheGet | ControlMethod::CacheSetCap | ControlMethod::CacheClear => {
477 Category::Cache
478 }
479 ControlMethod::HostedStoresList
480 | ControlMethod::HostedStoresPin
481 | ControlMethod::HostedStoresUnpin
482 | ControlMethod::HostedStoresStatus
483 | ControlMethod::CapsuleFetch => Category::HostedStores,
484 ControlMethod::SyncStatus | ControlMethod::SyncTrigger => Category::Sync,
485 ControlMethod::UpdaterStatus
486 | ControlMethod::UpdaterSetChannel
487 | ControlMethod::UpdaterPause
488 | ControlMethod::UpdaterResume
489 | ControlMethod::UpdaterCheckNow => Category::Updater,
490 ControlMethod::PairingList
491 | ControlMethod::PairingApprove
492 | ControlMethod::PairingRevoke
493 | ControlMethod::PairingRequest
494 | ControlMethod::PairingPoll => Category::Pairing,
495 ControlMethod::PeerStatus
496 | ControlMethod::PeerCounts
497 | ControlMethod::PeersConnect
498 | ControlMethod::PeersDisconnect
499 | ControlMethod::ChiaPeersAdd
500 | ControlMethod::ChiaPeersList
501 | ControlMethod::ChiaPeersRemove => Category::Peers,
502 ControlMethod::Subscribe
503 | ControlMethod::Unsubscribe
504 | ControlMethod::ListSubscriptions => Category::Subscriptions,
505 ControlMethod::WalletBalance
506 | ControlMethod::WalletCoins
507 | ControlMethod::WalletCoinById
508 | ControlMethod::WalletCoinSpend
509 | ControlMethod::WalletCoinsByParent
510 | ControlMethod::WalletArrivals
511 | ControlMethod::WalletPeak
512 | ControlMethod::WalletSyncStatus
513 | ControlMethod::WalletBroadcast
514 | ControlMethod::WalletWatch
515 | ControlMethod::WalletUnwatch
516 | ControlMethod::WalletWatched
517 | ControlMethod::WalletReservationsHeld
518 | ControlMethod::WalletReservationsReserve
519 | ControlMethod::WalletReservationsRelease => Category::Wallet,
520 ControlMethod::SpendsList => Category::Spends,
521 ControlMethod::CollateralRequirement
522 | ControlMethod::CollateralMarginGet
523 | ControlMethod::CollateralMarginSet => Category::Collateral,
524 ControlMethod::ProfilePutBody | ControlMethod::ProfileGetBody => Category::Profile,
525 }
526 }
527
528 pub const fn summary(self) -> &'static str {
530 match self {
531 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.",
532 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.",
533 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.",
534 ControlMethod::Status => "A rich node status snapshot (version, uptime, addr, cache, hosted/pinned counts, sync availability).",
535 ControlMethod::ConfigGet => "The node's effective configuration (addr/port, upstream + override, cache dir/shared, config path, sync availability).",
536 ControlMethod::ConfigSetUpstream => "Persist an upstream-RPC override; takes effect on next node start (requires_restart).",
537 ControlMethod::LogSetLevel => "Live-swap the running node's tracing EnvFilter directive (not persisted).",
538 ControlMethod::CacheGet => "The on-disk content-cache view: cap_bytes, used_bytes, dir, shared.",
539 ControlMethod::CacheSetCap => "Set the on-disk cache size cap in bytes (floored at 64 MiB).",
540 ControlMethod::CacheClear => "Delete all locally cached DIG content.",
541 ControlMethod::HostedStoresList => "Every held/pinned store, merged, with each store's cached capsules and a pinned flag.",
542 ControlMethod::HostedStoresPin => "Pin a store (storeId[:rootHash]); pre-fetches the capsule when a root is given and §21 sync is available.",
543 ControlMethod::HostedStoresUnpin => "Unpin a store and evict its cached capsules.",
544 ControlMethod::HostedStoresStatus => "Per-store status: pinned flag, cached capsules, total bytes.",
545 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.",
546 ControlMethod::SyncStatus => "Whether authenticated §21 whole-store sync is available, plus pinned-store cache coverage.",
547 ControlMethod::SyncTrigger => "Trigger a §21 sync for one capsule (storeId + root).",
548 ControlMethod::UpdaterStatus => "The DIG auto-update beacon's current status (proxied from dig-updater).",
549 ControlMethod::UpdaterSetChannel => "Set the beacon's update channel (\"nightly\" | \"stable\").",
550 ControlMethod::UpdaterPause => "Suspend the beacon's auto-updates (optionally until a unix time).",
551 ControlMethod::UpdaterResume => "Resume the beacon's auto-updates.",
552 ControlMethod::UpdaterCheckNow => "Force an immediate beacon update check.",
553 ControlMethod::PairingList => "List pending pairing requests and issued paired tokens (MASTER token only).",
554 ControlMethod::PairingApprove => "Approve a pending pairing, minting a scoped token (MASTER token only).",
555 ControlMethod::PairingRevoke => "Revoke an issued paired token by token_id (MASTER token only).",
556 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.",
557 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.",
558 ControlMethod::PeersConnect => "Dial a peer by address, or resolve an already-connected peer_id, via the live gossip pool.",
559 ControlMethod::PeersDisconnect => "Drop a pooled peer by peer_id, closing its mTLS link (idempotent).",
560 ControlMethod::Subscribe => "Subscribe the node to a store it actively watches and gap-fills.",
561 ControlMethod::Unsubscribe => "Stop watching a store.",
562 ControlMethod::ListSubscriptions => "The node's persisted subscription set + count.",
563 ControlMethod::WalletCoins => "READ-only: the spendable coin records for an address + asset, with the tier that answered and the height they reflect.",
564 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.",
565 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.",
566 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.",
567 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`.",
568 ControlMethod::WalletPeak => "READ-only: the node's current chain peak height, independent of any address.",
569 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).",
570 ControlMethod::WalletBroadcast => "Push an ALREADY-SIGNED spend bundle to the network; the node never signs. TOKEN-GATED.",
571 ControlMethod::WalletBalance => "READ-only: the confirmed spendable balance for an address + asset (plus pending, sync freshness, and the peak height it reflects).",
572 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.",
573 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.",
574 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.",
575 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.",
576 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.",
577 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.",
578 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.",
579 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.",
580 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.",
581 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.",
582 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.",
583 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.",
584 ControlMethod::PairingRequest => "OPEN: request a control-token pairing; returns a pairing_id + pairing_code to compare.",
585 ControlMethod::PairingPoll => "OPEN: poll a pairing by id; once the operator approves, returns the scoped token once.",
586 }
587 }
588
589 pub const ALL: &'static [ControlMethod] = &[
592 ControlMethod::Status,
593 ControlMethod::ConfigGet,
594 ControlMethod::ConfigSetUpstream,
595 ControlMethod::LogSetLevel,
596 ControlMethod::CacheGet,
597 ControlMethod::CacheSetCap,
598 ControlMethod::CacheClear,
599 ControlMethod::HostedStoresList,
600 ControlMethod::HostedStoresPin,
601 ControlMethod::HostedStoresUnpin,
602 ControlMethod::HostedStoresStatus,
603 ControlMethod::CapsuleFetch,
604 ControlMethod::SyncStatus,
605 ControlMethod::SyncTrigger,
606 ControlMethod::UpdaterStatus,
607 ControlMethod::UpdaterSetChannel,
608 ControlMethod::UpdaterPause,
609 ControlMethod::UpdaterResume,
610 ControlMethod::UpdaterCheckNow,
611 ControlMethod::PairingList,
612 ControlMethod::PairingApprove,
613 ControlMethod::PairingRevoke,
614 ControlMethod::PeerStatus,
615 ControlMethod::PeerCounts,
616 ControlMethod::PeersConnect,
617 ControlMethod::PeersDisconnect,
618 ControlMethod::ChiaPeersAdd,
619 ControlMethod::ChiaPeersList,
620 ControlMethod::ChiaPeersRemove,
621 ControlMethod::Subscribe,
622 ControlMethod::Unsubscribe,
623 ControlMethod::ListSubscriptions,
624 ControlMethod::WalletBalance,
625 ControlMethod::WalletCoins,
626 ControlMethod::WalletCoinById,
627 ControlMethod::WalletCoinSpend,
628 ControlMethod::WalletCoinsByParent,
629 ControlMethod::WalletArrivals,
630 ControlMethod::WalletPeak,
631 ControlMethod::WalletSyncStatus,
632 ControlMethod::WalletBroadcast,
633 ControlMethod::WalletWatch,
634 ControlMethod::WalletUnwatch,
635 ControlMethod::WalletWatched,
636 ControlMethod::WalletReservationsHeld,
637 ControlMethod::WalletReservationsReserve,
638 ControlMethod::WalletReservationsRelease,
639 ControlMethod::SpendsList,
640 ControlMethod::CollateralRequirement,
641 ControlMethod::CollateralMarginGet,
642 ControlMethod::CollateralMarginSet,
643 ControlMethod::ProfilePutBody,
644 ControlMethod::ProfileGetBody,
645 ControlMethod::PairingRequest,
646 ControlMethod::PairingPoll,
647 ];
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653 use std::collections::BTreeSet;
654
655 #[test]
656 fn every_method_has_a_unique_wire_name() {
657 let names: BTreeSet<&str> = ControlMethod::ALL.iter().map(|m| m.name()).collect();
658 assert_eq!(
659 names.len(),
660 ControlMethod::ALL.len(),
661 "duplicate or missing wire names in the catalog"
662 );
663 }
664
665 #[test]
666 fn from_name_round_trips_every_method() {
667 for &m in ControlMethod::ALL {
668 assert_eq!(ControlMethod::from_name(m.name()), Some(m));
669 }
670 assert_eq!(ControlMethod::from_name("control.nope"), None);
671 assert_eq!(ControlMethod::from_name(""), None);
672 }
673
674 #[test]
675 fn the_token_less_surface_is_exactly_the_bootstrap_plus_the_chain_reads() {
676 let expected_open: BTreeSet<&str> = [
680 "pairing.request",
681 "pairing.poll",
682 "control.wallet.balance",
683 "control.wallet.coins",
684 "control.wallet.coinById",
685 "control.wallet.coinSpend",
686 "control.wallet.coinsByParent",
687 "control.wallet.peak",
688 "control.wallet.syncStatus",
689 "control.peerCounts",
690 ]
691 .into_iter()
692 .collect();
693 assert_eq!(
694 expected_open.len(),
695 10,
696 "the open surface is ten named methods"
697 );
698 let actual_open: BTreeSet<&str> = ControlMethod::ALL
699 .iter()
700 .filter(|m| !m.requires_auth())
701 .map(|m| m.name())
702 .collect();
703 assert_eq!(actual_open, expected_open);
704 }
705
706 #[test]
715 fn the_gated_wallet_methods_are_the_push_the_cursor_and_enrolment() {
716 let gated: Vec<&str> = ControlMethod::ALL
717 .iter()
718 .filter(|m| m.category() == Category::Wallet && m.requires_auth())
719 .map(|m| m.name())
720 .collect();
721 assert_eq!(
722 gated,
723 vec![
724 "control.wallet.arrivals",
725 "control.wallet.broadcast",
726 "control.wallet.watch",
727 "control.wallet.unwatch",
728 "control.wallet.watched",
729 "control.wallet.reservations.held",
730 "control.wallet.reservations.reserve",
731 "control.wallet.reservations.release",
732 ]
733 );
734 assert!(!ControlMethod::WalletBroadcast.is_open_read());
735 }
736
737 #[test]
749 fn the_arrival_cursor_is_not_an_open_read() {
750 assert!(
751 !ControlMethod::WalletArrivals.is_open_read(),
752 "control.wallet.arrivals discloses this node's OWN watched puzzle hashes to a caller \
753 that supplied nothing, so it MUST NOT be served token-less"
754 );
755 assert!(ControlMethod::WalletArrivals.requires_auth());
756 assert!(
757 ControlMethod::WalletCoinById.is_open_read(),
758 "the caller-addressed reads stay open -- the fix is the membership rule, not gating \
759 the wallet category"
760 );
761 }
762
763 #[test]
782 fn the_catalog_serves_every_chain_source_primitive() {
783 for wire in [
784 "control.wallet.coinById", "control.wallet.coins", "control.wallet.peak", "control.wallet.coinsByParent", "control.wallet.coinSpend", ] {
790 assert!(
791 ControlMethod::from_name(wire).is_some(),
792 "{wire} is required to implement ChainSource over the control plane"
793 );
794 }
795 }
796
797 #[test]
804 fn the_chain_primitives_are_caller_named_open_reads() {
805 for method in [
806 ControlMethod::WalletCoinSpend,
807 ControlMethod::WalletCoinsByParent,
808 ] {
809 assert!(
810 method.is_open_read(),
811 "{} names its subject in the request and discloses no node-to-address \
812 association, exactly like control.wallet.coinById",
813 method.name()
814 );
815 assert!(!method.requires_auth());
816 }
817 assert!(
818 ControlMethod::WalletArrivals.requires_auth(),
819 "the caller-supplies-nothing read stays gated -- the rule is who names the subject, \
820 not whether the bytes are on chain"
821 );
822 assert!(ControlMethod::WalletBroadcast.requires_auth());
823 }
824
825 #[test]
839 fn the_enrolment_methods_are_gated_including_the_read() {
840 for wire in [
841 "control.wallet.watch",
842 "control.wallet.unwatch",
843 "control.wallet.watched",
844 ] {
845 let method = ControlMethod::from_name(wire)
846 .unwrap_or_else(|| panic!("{wire} must be in the catalog"));
847 assert!(
848 !method.is_open_read(),
849 "{wire} either aims this node's subscriptions or names the keys it already \
850 follows, so it MUST NOT be served token-less"
851 );
852 assert!(method.requires_auth(), "{wire} must require the token");
853 assert_eq!(method.category(), Category::Wallet);
854 assert_eq!(method.routing(), Routing::Delegated);
855 }
856 assert!(
857 ControlMethod::WalletCoinById.is_open_read(),
858 "the caller-addressed reads stay open -- enrolment is gated by the membership rule, \
859 not by gating the wallet category"
860 );
861 }
862
863 #[test]
864 fn only_pairing_bootstrap_is_open_bootstrap_routed() {
865 for &m in ControlMethod::ALL {
866 let open_bootstrap = matches!(
867 m,
868 ControlMethod::PairingRequest | ControlMethod::PairingPoll
869 );
870 assert_eq!(
871 m.routing() == Routing::OpenBootstrap,
872 open_bootstrap,
873 "{} routing mismatch",
874 m.name()
875 );
876 }
877 }
878
879 #[test]
880 fn pairing_admin_methods_are_exactly_three() {
881 let admin: Vec<&str> = ControlMethod::ALL
882 .iter()
883 .filter(|m| m.is_pairing_admin())
884 .map(|m| m.name())
885 .collect();
886 assert_eq!(
887 admin,
888 vec![
889 "control.pairing.list",
890 "control.pairing.approve",
891 "control.pairing.revoke"
892 ]
893 );
894 }
895
896 #[test]
903 fn the_master_token_tier_is_pairing_admin_plus_the_trusted_peer_mutations() {
904 let master: BTreeSet<&str> = ControlMethod::ALL
905 .iter()
906 .filter(|m| m.requires_master_token())
907 .map(|m| m.name())
908 .collect();
909 let expected: BTreeSet<&str> = [
910 "control.pairing.list",
911 "control.pairing.approve",
912 "control.pairing.revoke",
913 "control.chiaPeers.add",
914 "control.chiaPeers.remove",
915 ]
916 .into_iter()
917 .collect();
918 assert_eq!(master, expected);
919
920 for &m in ControlMethod::ALL {
923 assert!(
924 !m.is_pairing_admin() || m.requires_master_token(),
925 "{} is pairing-admin but not master-tier",
926 m.name()
927 );
928 }
929 assert!(
930 master.len()
931 > ControlMethod::ALL
932 .iter()
933 .filter(|m| m.is_pairing_admin())
934 .count(),
935 "the two predicates must not be interchangeable"
936 );
937
938 for &m in ControlMethod::ALL {
940 assert!(
941 !m.requires_master_token() || m.requires_auth(),
942 "{}",
943 m.name()
944 );
945 }
946 }
947
948 #[test]
955 fn the_add_summary_authorises_only_a_node_the_operator_runs() {
956 let summary = ControlMethod::ChiaPeersAdd.summary().to_lowercase();
957 assert!(
958 summary.contains("a node you run"),
959 "add must name the operator-run scope, got: {summary}"
960 );
961 for widened in ["vouch", "otherwise trust", "trust yourself", "recommend"] {
962 assert!(
963 !summary.contains(widened),
964 "add summary widens operator trust past NC-12 with {widened:?}: {summary}"
965 );
966 }
967 }
968
969 #[test]
970 fn delegated_set_matches_the_engine_surface() {
971 let delegated: BTreeSet<&str> = ControlMethod::ALL
972 .iter()
973 .filter(|m| m.routing() == Routing::Delegated)
974 .map(|m| m.name())
975 .collect();
976 let expected: BTreeSet<&str> = [
977 "control.wallet.coins",
978 "control.wallet.coinById",
979 "control.wallet.coinSpend",
980 "control.wallet.coinsByParent",
981 "control.wallet.arrivals",
982 "control.wallet.peak",
983 "control.wallet.syncStatus",
984 "control.wallet.broadcast",
985 "control.wallet.watch",
986 "control.wallet.unwatch",
987 "control.wallet.watched",
988 "control.wallet.reservations.held",
989 "control.wallet.reservations.reserve",
990 "control.wallet.reservations.release",
991 "control.profile.putBody",
992 "control.profile.getBody",
993 "control.peerStatus",
994 "control.peerCounts",
995 "control.peers.connect",
996 "control.peers.disconnect",
997 "control.subscribe",
998 "control.unsubscribe",
999 "control.listSubscriptions",
1000 "control.wallet.balance",
1001 ]
1002 .into_iter()
1003 .collect();
1004 assert_eq!(delegated, expected);
1005 }
1006
1007 #[test]
1014 fn the_trusted_chia_peer_methods_are_gated_and_disclose_the_corroboration_bypass() {
1015 let declared: BTreeSet<&str> = ControlMethod::ALL.iter().map(|m| m.name()).collect();
1016 for name in [
1017 "control.chiaPeers.add",
1018 "control.chiaPeers.list",
1019 "control.chiaPeers.remove",
1020 ] {
1021 assert!(declared.contains(name), "{name} is not in the catalog");
1022 let m = ControlMethod::from_name(name).expect("from_name round-trips");
1023 assert_eq!(m.category(), Category::Peers, "{name} is a peers method");
1024 assert_eq!(m.routing(), Routing::Owned, "{name} is served by the shell");
1025 assert!(m.requires_auth(), "{name} must require the control token");
1026 assert!(!m.is_open_read(), "{name} is not an open read");
1027 }
1028 assert!(ControlMethod::ChiaPeersAdd.requires_master_token());
1032 assert!(ControlMethod::ChiaPeersRemove.requires_master_token());
1033 assert!(
1034 !ControlMethod::ChiaPeersList.requires_master_token(),
1035 "list grants nothing that outlives the token; gating it would blind a paired client \
1036 to the trust state it is subject to"
1037 );
1038 for name in ["control.chiaPeers.add", "control.chiaPeers.remove"] {
1042 let summary = ControlMethod::from_name(name).unwrap().summary();
1043 assert!(
1044 summary.to_lowercase().contains("corroboration"),
1045 "{name} summary must name the corroboration bypass, got: {summary}"
1046 );
1047 }
1048 }
1049
1050 #[test]
1051 fn every_method_has_a_nonempty_summary() {
1052 for &m in ControlMethod::ALL {
1053 assert!(!m.summary().is_empty(), "{} has no summary", m.name());
1054 }
1055 }
1056}