1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum Routing {
23 Owned,
25 Delegated,
27 OpenBootstrap,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub enum Category {
34 Status,
36 Config,
38 Log,
40 Cache,
42 HostedStores,
44 Sync,
46 Updater,
48 Pairing,
50 Peers,
52 Subscriptions,
54 Wallet,
57 Profile,
61}
62
63#[non_exhaustive]
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70pub enum ControlMethod {
71 Status,
74 ConfigGet,
76 ConfigSetUpstream,
78 LogSetLevel,
80
81 CacheGet,
84 CacheSetCap,
86 CacheClear,
88
89 HostedStoresList,
92 HostedStoresPin,
94 HostedStoresUnpin,
96 HostedStoresStatus,
98 CapsuleFetch,
101
102 SyncStatus,
105 SyncTrigger,
107
108 UpdaterStatus,
111 UpdaterSetChannel,
113 UpdaterPause,
115 UpdaterResume,
117 UpdaterCheckNow,
119
120 PairingList,
123 PairingApprove,
125 PairingRevoke,
127
128 PeerStatus,
131 PeerCounts,
133 PeersConnect,
135 PeersDisconnect,
137
138 ChiaPeersAdd,
154 ChiaPeersList,
156 ChiaPeersRemove,
158
159 Subscribe,
162 Unsubscribe,
164 ListSubscriptions,
166
167 WalletBalance,
170 WalletCoins,
172 WalletCoinById,
174 WalletCoinSpend,
176 WalletCoinsByParent,
178 WalletArrivals,
180 WalletPeak,
182 WalletSyncStatus,
184 WalletBroadcast,
186 WalletWatch,
188 WalletUnwatch,
190 WalletWatched,
192 WalletReservationsHeld,
194 WalletReservationsReserve,
196 WalletReservationsRelease,
198
199 ProfilePutBody,
202 ProfileGetBody,
204
205 PairingRequest,
208 PairingPoll,
210}
211
212impl ControlMethod {
213 pub const fn name(self) -> &'static str {
215 match self {
216 ControlMethod::Status => "control.status",
217 ControlMethod::ConfigGet => "control.config.get",
218 ControlMethod::ConfigSetUpstream => "control.config.setUpstream",
219 ControlMethod::LogSetLevel => "control.log.setLevel",
220 ControlMethod::CacheGet => "control.cache.get",
221 ControlMethod::CacheSetCap => "control.cache.setCap",
222 ControlMethod::CacheClear => "control.cache.clear",
223 ControlMethod::HostedStoresList => "control.hostedStores.list",
224 ControlMethod::HostedStoresPin => "control.hostedStores.pin",
225 ControlMethod::HostedStoresUnpin => "control.hostedStores.unpin",
226 ControlMethod::HostedStoresStatus => "control.hostedStores.status",
227 ControlMethod::CapsuleFetch => "control.capsule.fetch",
228 ControlMethod::SyncStatus => "control.sync.status",
229 ControlMethod::SyncTrigger => "control.sync.trigger",
230 ControlMethod::UpdaterStatus => "control.updater.status",
231 ControlMethod::UpdaterSetChannel => "control.updater.setChannel",
232 ControlMethod::UpdaterPause => "control.updater.pause",
233 ControlMethod::UpdaterResume => "control.updater.resume",
234 ControlMethod::UpdaterCheckNow => "control.updater.checkNow",
235 ControlMethod::PairingList => "control.pairing.list",
236 ControlMethod::PairingApprove => "control.pairing.approve",
237 ControlMethod::PairingRevoke => "control.pairing.revoke",
238 ControlMethod::PeerStatus => "control.peerStatus",
239 ControlMethod::PeerCounts => "control.peerCounts",
240 ControlMethod::PeersConnect => "control.peers.connect",
241 ControlMethod::PeersDisconnect => "control.peers.disconnect",
242 ControlMethod::ChiaPeersAdd => "control.chiaPeers.add",
243 ControlMethod::ChiaPeersList => "control.chiaPeers.list",
244 ControlMethod::ChiaPeersRemove => "control.chiaPeers.remove",
245 ControlMethod::Subscribe => "control.subscribe",
246 ControlMethod::Unsubscribe => "control.unsubscribe",
247 ControlMethod::ListSubscriptions => "control.listSubscriptions",
248 ControlMethod::WalletBalance => "control.wallet.balance",
249 ControlMethod::WalletCoins => "control.wallet.coins",
250 ControlMethod::WalletCoinById => "control.wallet.coinById",
251 ControlMethod::WalletCoinSpend => "control.wallet.coinSpend",
252 ControlMethod::WalletCoinsByParent => "control.wallet.coinsByParent",
253 ControlMethod::WalletArrivals => "control.wallet.arrivals",
254 ControlMethod::WalletPeak => "control.wallet.peak",
255 ControlMethod::WalletSyncStatus => "control.wallet.syncStatus",
256 ControlMethod::WalletBroadcast => "control.wallet.broadcast",
257 ControlMethod::WalletWatch => "control.wallet.watch",
258 ControlMethod::WalletUnwatch => "control.wallet.unwatch",
259 ControlMethod::WalletWatched => "control.wallet.watched",
260 ControlMethod::WalletReservationsHeld => "control.wallet.reservations.held",
261 ControlMethod::WalletReservationsReserve => "control.wallet.reservations.reserve",
262 ControlMethod::WalletReservationsRelease => "control.wallet.reservations.release",
263 ControlMethod::ProfilePutBody => "control.profile.putBody",
264 ControlMethod::ProfileGetBody => "control.profile.getBody",
265 ControlMethod::PairingRequest => "pairing.request",
266 ControlMethod::PairingPoll => "pairing.poll",
267 }
268 }
269
270 pub fn from_name(name: &str) -> Option<ControlMethod> {
272 ControlMethod::ALL
273 .iter()
274 .copied()
275 .find(|m| m.name() == name)
276 }
277
278 pub const fn requires_auth(self) -> bool {
305 !self.is_open_read()
306 && !matches!(
307 self,
308 ControlMethod::PairingRequest | ControlMethod::PairingPoll
309 )
310 }
311
312 pub const fn is_open_read(self) -> bool {
346 matches!(
347 self,
348 ControlMethod::WalletBalance
349 | ControlMethod::WalletCoins
350 | ControlMethod::WalletCoinById
351 | ControlMethod::WalletCoinSpend
352 | ControlMethod::WalletCoinsByParent
353 | ControlMethod::WalletPeak
354 | ControlMethod::WalletSyncStatus
355 | ControlMethod::PeerCounts
356 )
357 }
358
359 pub const fn is_pairing_admin(self) -> bool {
368 matches!(
369 self,
370 ControlMethod::PairingList
371 | ControlMethod::PairingApprove
372 | ControlMethod::PairingRevoke
373 )
374 }
375
376 pub const fn requires_master_token(self) -> bool {
403 self.is_pairing_admin()
404 || matches!(
405 self,
406 ControlMethod::ChiaPeersAdd | ControlMethod::ChiaPeersRemove
407 )
408 }
409
410 pub const fn routing(self) -> Routing {
412 match self {
413 ControlMethod::PeerStatus
414 | ControlMethod::PeerCounts
415 | ControlMethod::PeersConnect
416 | ControlMethod::PeersDisconnect
417 | ControlMethod::Subscribe
418 | ControlMethod::Unsubscribe
419 | ControlMethod::ListSubscriptions
420 | ControlMethod::WalletBalance
421 | ControlMethod::WalletCoins
422 | ControlMethod::WalletCoinById
423 | ControlMethod::WalletCoinSpend
424 | ControlMethod::WalletCoinsByParent
425 | ControlMethod::WalletArrivals
426 | ControlMethod::WalletPeak
427 | ControlMethod::WalletSyncStatus
428 | ControlMethod::WalletBroadcast
429 | ControlMethod::WalletWatch
430 | ControlMethod::WalletUnwatch
431 | ControlMethod::WalletWatched
432 | ControlMethod::WalletReservationsHeld
433 | ControlMethod::WalletReservationsReserve
434 | ControlMethod::WalletReservationsRelease
435 | ControlMethod::ProfilePutBody
436 | ControlMethod::ProfileGetBody => Routing::Delegated,
437 ControlMethod::PairingRequest | ControlMethod::PairingPoll => Routing::OpenBootstrap,
438 _ => Routing::Owned,
439 }
440 }
441
442 pub const fn category(self) -> Category {
444 match self {
445 ControlMethod::Status => Category::Status,
446 ControlMethod::ConfigGet | ControlMethod::ConfigSetUpstream => Category::Config,
447 ControlMethod::LogSetLevel => Category::Log,
448 ControlMethod::CacheGet | ControlMethod::CacheSetCap | ControlMethod::CacheClear => {
449 Category::Cache
450 }
451 ControlMethod::HostedStoresList
452 | ControlMethod::HostedStoresPin
453 | ControlMethod::HostedStoresUnpin
454 | ControlMethod::HostedStoresStatus
455 | ControlMethod::CapsuleFetch => Category::HostedStores,
456 ControlMethod::SyncStatus | ControlMethod::SyncTrigger => Category::Sync,
457 ControlMethod::UpdaterStatus
458 | ControlMethod::UpdaterSetChannel
459 | ControlMethod::UpdaterPause
460 | ControlMethod::UpdaterResume
461 | ControlMethod::UpdaterCheckNow => Category::Updater,
462 ControlMethod::PairingList
463 | ControlMethod::PairingApprove
464 | ControlMethod::PairingRevoke
465 | ControlMethod::PairingRequest
466 | ControlMethod::PairingPoll => Category::Pairing,
467 ControlMethod::PeerStatus
468 | ControlMethod::PeerCounts
469 | ControlMethod::PeersConnect
470 | ControlMethod::PeersDisconnect
471 | ControlMethod::ChiaPeersAdd
472 | ControlMethod::ChiaPeersList
473 | ControlMethod::ChiaPeersRemove => Category::Peers,
474 ControlMethod::Subscribe
475 | ControlMethod::Unsubscribe
476 | ControlMethod::ListSubscriptions => Category::Subscriptions,
477 ControlMethod::WalletBalance
478 | ControlMethod::WalletCoins
479 | ControlMethod::WalletCoinById
480 | ControlMethod::WalletCoinSpend
481 | ControlMethod::WalletCoinsByParent
482 | ControlMethod::WalletArrivals
483 | ControlMethod::WalletPeak
484 | ControlMethod::WalletSyncStatus
485 | ControlMethod::WalletBroadcast
486 | ControlMethod::WalletWatch
487 | ControlMethod::WalletUnwatch
488 | ControlMethod::WalletWatched
489 | ControlMethod::WalletReservationsHeld
490 | ControlMethod::WalletReservationsReserve
491 | ControlMethod::WalletReservationsRelease => Category::Wallet,
492 ControlMethod::ProfilePutBody | ControlMethod::ProfileGetBody => Category::Profile,
493 }
494 }
495
496 pub const fn summary(self) -> &'static str {
498 match self {
499 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.",
500 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.",
501 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.",
502 ControlMethod::Status => "A rich node status snapshot (version, uptime, addr, cache, hosted/pinned counts, sync availability).",
503 ControlMethod::ConfigGet => "The node's effective configuration (addr/port, upstream + override, cache dir/shared, config path, sync availability).",
504 ControlMethod::ConfigSetUpstream => "Persist an upstream-RPC override; takes effect on next node start (requires_restart).",
505 ControlMethod::LogSetLevel => "Live-swap the running node's tracing EnvFilter directive (not persisted).",
506 ControlMethod::CacheGet => "The on-disk content-cache view: cap_bytes, used_bytes, dir, shared.",
507 ControlMethod::CacheSetCap => "Set the on-disk cache size cap in bytes (floored at 64 MiB).",
508 ControlMethod::CacheClear => "Delete all locally cached DIG content.",
509 ControlMethod::HostedStoresList => "Every held/pinned store, merged, with each store's cached capsules and a pinned flag.",
510 ControlMethod::HostedStoresPin => "Pin a store (storeId[:rootHash]); pre-fetches the capsule when a root is given and §21 sync is available.",
511 ControlMethod::HostedStoresUnpin => "Unpin a store and evict its cached capsules.",
512 ControlMethod::HostedStoresStatus => "Per-store status: pinned flag, cached capsules, total bytes.",
513 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.",
514 ControlMethod::SyncStatus => "Whether authenticated §21 whole-store sync is available, plus pinned-store cache coverage.",
515 ControlMethod::SyncTrigger => "Trigger a §21 sync for one capsule (storeId + root).",
516 ControlMethod::UpdaterStatus => "The DIG auto-update beacon's current status (proxied from dig-updater).",
517 ControlMethod::UpdaterSetChannel => "Set the beacon's update channel (\"nightly\" | \"stable\").",
518 ControlMethod::UpdaterPause => "Suspend the beacon's auto-updates (optionally until a unix time).",
519 ControlMethod::UpdaterResume => "Resume the beacon's auto-updates.",
520 ControlMethod::UpdaterCheckNow => "Force an immediate beacon update check.",
521 ControlMethod::PairingList => "List pending pairing requests and issued paired tokens (MASTER token only).",
522 ControlMethod::PairingApprove => "Approve a pending pairing, minting a scoped token (MASTER token only).",
523 ControlMethod::PairingRevoke => "Revoke an issued paired token by token_id (MASTER token only).",
524 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.",
525 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.",
526 ControlMethod::PeersConnect => "Dial a peer by address, or resolve an already-connected peer_id, via the live gossip pool.",
527 ControlMethod::PeersDisconnect => "Drop a pooled peer by peer_id, closing its mTLS link (idempotent).",
528 ControlMethod::Subscribe => "Subscribe the node to a store it actively watches and gap-fills.",
529 ControlMethod::Unsubscribe => "Stop watching a store.",
530 ControlMethod::ListSubscriptions => "The node's persisted subscription set + count.",
531 ControlMethod::WalletCoins => "READ-only: the spendable coin records for an address + asset, with the tier that answered and the height they reflect.",
532 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.",
533 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.",
534 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.",
535 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`.",
536 ControlMethod::WalletPeak => "READ-only: the node's current chain peak height, independent of any address.",
537 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).",
538 ControlMethod::WalletBroadcast => "Push an ALREADY-SIGNED spend bundle to the network; the node never signs. TOKEN-GATED.",
539 ControlMethod::WalletBalance => "READ-only: the confirmed spendable balance for an address + asset (plus pending, sync freshness, and the peak height it reflects).",
540 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.",
541 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.",
542 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.",
543 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.",
544 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.",
545 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.",
546 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.",
547 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.",
548 ControlMethod::PairingRequest => "OPEN: request a control-token pairing; returns a pairing_id + pairing_code to compare.",
549 ControlMethod::PairingPoll => "OPEN: poll a pairing by id; once the operator approves, returns the scoped token once.",
550 }
551 }
552
553 pub const ALL: &'static [ControlMethod] = &[
556 ControlMethod::Status,
557 ControlMethod::ConfigGet,
558 ControlMethod::ConfigSetUpstream,
559 ControlMethod::LogSetLevel,
560 ControlMethod::CacheGet,
561 ControlMethod::CacheSetCap,
562 ControlMethod::CacheClear,
563 ControlMethod::HostedStoresList,
564 ControlMethod::HostedStoresPin,
565 ControlMethod::HostedStoresUnpin,
566 ControlMethod::HostedStoresStatus,
567 ControlMethod::CapsuleFetch,
568 ControlMethod::SyncStatus,
569 ControlMethod::SyncTrigger,
570 ControlMethod::UpdaterStatus,
571 ControlMethod::UpdaterSetChannel,
572 ControlMethod::UpdaterPause,
573 ControlMethod::UpdaterResume,
574 ControlMethod::UpdaterCheckNow,
575 ControlMethod::PairingList,
576 ControlMethod::PairingApprove,
577 ControlMethod::PairingRevoke,
578 ControlMethod::PeerStatus,
579 ControlMethod::PeerCounts,
580 ControlMethod::PeersConnect,
581 ControlMethod::PeersDisconnect,
582 ControlMethod::ChiaPeersAdd,
583 ControlMethod::ChiaPeersList,
584 ControlMethod::ChiaPeersRemove,
585 ControlMethod::Subscribe,
586 ControlMethod::Unsubscribe,
587 ControlMethod::ListSubscriptions,
588 ControlMethod::WalletBalance,
589 ControlMethod::WalletCoins,
590 ControlMethod::WalletCoinById,
591 ControlMethod::WalletCoinSpend,
592 ControlMethod::WalletCoinsByParent,
593 ControlMethod::WalletArrivals,
594 ControlMethod::WalletPeak,
595 ControlMethod::WalletSyncStatus,
596 ControlMethod::WalletBroadcast,
597 ControlMethod::WalletWatch,
598 ControlMethod::WalletUnwatch,
599 ControlMethod::WalletWatched,
600 ControlMethod::WalletReservationsHeld,
601 ControlMethod::WalletReservationsReserve,
602 ControlMethod::WalletReservationsRelease,
603 ControlMethod::ProfilePutBody,
604 ControlMethod::ProfileGetBody,
605 ControlMethod::PairingRequest,
606 ControlMethod::PairingPoll,
607 ];
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613 use std::collections::BTreeSet;
614
615 #[test]
616 fn every_method_has_a_unique_wire_name() {
617 let names: BTreeSet<&str> = ControlMethod::ALL.iter().map(|m| m.name()).collect();
618 assert_eq!(
619 names.len(),
620 ControlMethod::ALL.len(),
621 "duplicate or missing wire names in the catalog"
622 );
623 }
624
625 #[test]
626 fn from_name_round_trips_every_method() {
627 for &m in ControlMethod::ALL {
628 assert_eq!(ControlMethod::from_name(m.name()), Some(m));
629 }
630 assert_eq!(ControlMethod::from_name("control.nope"), None);
631 assert_eq!(ControlMethod::from_name(""), None);
632 }
633
634 #[test]
635 fn the_token_less_surface_is_exactly_the_bootstrap_plus_the_chain_reads() {
636 let expected_open: BTreeSet<&str> = [
640 "pairing.request",
641 "pairing.poll",
642 "control.wallet.balance",
643 "control.wallet.coins",
644 "control.wallet.coinById",
645 "control.wallet.coinSpend",
646 "control.wallet.coinsByParent",
647 "control.wallet.peak",
648 "control.wallet.syncStatus",
649 "control.peerCounts",
650 ]
651 .into_iter()
652 .collect();
653 assert_eq!(
654 expected_open.len(),
655 10,
656 "the open surface is ten named methods"
657 );
658 let actual_open: BTreeSet<&str> = ControlMethod::ALL
659 .iter()
660 .filter(|m| !m.requires_auth())
661 .map(|m| m.name())
662 .collect();
663 assert_eq!(actual_open, expected_open);
664 }
665
666 #[test]
675 fn the_gated_wallet_methods_are_the_push_the_cursor_and_enrolment() {
676 let gated: Vec<&str> = ControlMethod::ALL
677 .iter()
678 .filter(|m| m.category() == Category::Wallet && m.requires_auth())
679 .map(|m| m.name())
680 .collect();
681 assert_eq!(
682 gated,
683 vec![
684 "control.wallet.arrivals",
685 "control.wallet.broadcast",
686 "control.wallet.watch",
687 "control.wallet.unwatch",
688 "control.wallet.watched",
689 "control.wallet.reservations.held",
690 "control.wallet.reservations.reserve",
691 "control.wallet.reservations.release",
692 ]
693 );
694 assert!(!ControlMethod::WalletBroadcast.is_open_read());
695 }
696
697 #[test]
709 fn the_arrival_cursor_is_not_an_open_read() {
710 assert!(
711 !ControlMethod::WalletArrivals.is_open_read(),
712 "control.wallet.arrivals discloses this node's OWN watched puzzle hashes to a caller \
713 that supplied nothing, so it MUST NOT be served token-less"
714 );
715 assert!(ControlMethod::WalletArrivals.requires_auth());
716 assert!(
717 ControlMethod::WalletCoinById.is_open_read(),
718 "the caller-addressed reads stay open -- the fix is the membership rule, not gating \
719 the wallet category"
720 );
721 }
722
723 #[test]
742 fn the_catalog_serves_every_chain_source_primitive() {
743 for wire in [
744 "control.wallet.coinById", "control.wallet.coins", "control.wallet.peak", "control.wallet.coinsByParent", "control.wallet.coinSpend", ] {
750 assert!(
751 ControlMethod::from_name(wire).is_some(),
752 "{wire} is required to implement ChainSource over the control plane"
753 );
754 }
755 }
756
757 #[test]
764 fn the_chain_primitives_are_caller_named_open_reads() {
765 for method in [
766 ControlMethod::WalletCoinSpend,
767 ControlMethod::WalletCoinsByParent,
768 ] {
769 assert!(
770 method.is_open_read(),
771 "{} names its subject in the request and discloses no node-to-address \
772 association, exactly like control.wallet.coinById",
773 method.name()
774 );
775 assert!(!method.requires_auth());
776 }
777 assert!(
778 ControlMethod::WalletArrivals.requires_auth(),
779 "the caller-supplies-nothing read stays gated -- the rule is who names the subject, \
780 not whether the bytes are on chain"
781 );
782 assert!(ControlMethod::WalletBroadcast.requires_auth());
783 }
784
785 #[test]
799 fn the_enrolment_methods_are_gated_including_the_read() {
800 for wire in [
801 "control.wallet.watch",
802 "control.wallet.unwatch",
803 "control.wallet.watched",
804 ] {
805 let method = ControlMethod::from_name(wire)
806 .unwrap_or_else(|| panic!("{wire} must be in the catalog"));
807 assert!(
808 !method.is_open_read(),
809 "{wire} either aims this node's subscriptions or names the keys it already \
810 follows, so it MUST NOT be served token-less"
811 );
812 assert!(method.requires_auth(), "{wire} must require the token");
813 assert_eq!(method.category(), Category::Wallet);
814 assert_eq!(method.routing(), Routing::Delegated);
815 }
816 assert!(
817 ControlMethod::WalletCoinById.is_open_read(),
818 "the caller-addressed reads stay open -- enrolment is gated by the membership rule, \
819 not by gating the wallet category"
820 );
821 }
822
823 #[test]
824 fn only_pairing_bootstrap_is_open_bootstrap_routed() {
825 for &m in ControlMethod::ALL {
826 let open_bootstrap = matches!(
827 m,
828 ControlMethod::PairingRequest | ControlMethod::PairingPoll
829 );
830 assert_eq!(
831 m.routing() == Routing::OpenBootstrap,
832 open_bootstrap,
833 "{} routing mismatch",
834 m.name()
835 );
836 }
837 }
838
839 #[test]
840 fn pairing_admin_methods_are_exactly_three() {
841 let admin: Vec<&str> = ControlMethod::ALL
842 .iter()
843 .filter(|m| m.is_pairing_admin())
844 .map(|m| m.name())
845 .collect();
846 assert_eq!(
847 admin,
848 vec![
849 "control.pairing.list",
850 "control.pairing.approve",
851 "control.pairing.revoke"
852 ]
853 );
854 }
855
856 #[test]
863 fn the_master_token_tier_is_pairing_admin_plus_the_trusted_peer_mutations() {
864 let master: BTreeSet<&str> = ControlMethod::ALL
865 .iter()
866 .filter(|m| m.requires_master_token())
867 .map(|m| m.name())
868 .collect();
869 let expected: BTreeSet<&str> = [
870 "control.pairing.list",
871 "control.pairing.approve",
872 "control.pairing.revoke",
873 "control.chiaPeers.add",
874 "control.chiaPeers.remove",
875 ]
876 .into_iter()
877 .collect();
878 assert_eq!(master, expected);
879
880 for &m in ControlMethod::ALL {
883 assert!(
884 !m.is_pairing_admin() || m.requires_master_token(),
885 "{} is pairing-admin but not master-tier",
886 m.name()
887 );
888 }
889 assert!(
890 master.len()
891 > ControlMethod::ALL
892 .iter()
893 .filter(|m| m.is_pairing_admin())
894 .count(),
895 "the two predicates must not be interchangeable"
896 );
897
898 for &m in ControlMethod::ALL {
900 assert!(
901 !m.requires_master_token() || m.requires_auth(),
902 "{}",
903 m.name()
904 );
905 }
906 }
907
908 #[test]
915 fn the_add_summary_authorises_only_a_node_the_operator_runs() {
916 let summary = ControlMethod::ChiaPeersAdd.summary().to_lowercase();
917 assert!(
918 summary.contains("a node you run"),
919 "add must name the operator-run scope, got: {summary}"
920 );
921 for widened in ["vouch", "otherwise trust", "trust yourself", "recommend"] {
922 assert!(
923 !summary.contains(widened),
924 "add summary widens operator trust past NC-12 with {widened:?}: {summary}"
925 );
926 }
927 }
928
929 #[test]
930 fn delegated_set_matches_the_engine_surface() {
931 let delegated: BTreeSet<&str> = ControlMethod::ALL
932 .iter()
933 .filter(|m| m.routing() == Routing::Delegated)
934 .map(|m| m.name())
935 .collect();
936 let expected: BTreeSet<&str> = [
937 "control.wallet.coins",
938 "control.wallet.coinById",
939 "control.wallet.coinSpend",
940 "control.wallet.coinsByParent",
941 "control.wallet.arrivals",
942 "control.wallet.peak",
943 "control.wallet.syncStatus",
944 "control.wallet.broadcast",
945 "control.wallet.watch",
946 "control.wallet.unwatch",
947 "control.wallet.watched",
948 "control.wallet.reservations.held",
949 "control.wallet.reservations.reserve",
950 "control.wallet.reservations.release",
951 "control.profile.putBody",
952 "control.profile.getBody",
953 "control.peerStatus",
954 "control.peerCounts",
955 "control.peers.connect",
956 "control.peers.disconnect",
957 "control.subscribe",
958 "control.unsubscribe",
959 "control.listSubscriptions",
960 "control.wallet.balance",
961 ]
962 .into_iter()
963 .collect();
964 assert_eq!(delegated, expected);
965 }
966
967 #[test]
974 fn the_trusted_chia_peer_methods_are_gated_and_disclose_the_corroboration_bypass() {
975 let declared: BTreeSet<&str> = ControlMethod::ALL.iter().map(|m| m.name()).collect();
976 for name in [
977 "control.chiaPeers.add",
978 "control.chiaPeers.list",
979 "control.chiaPeers.remove",
980 ] {
981 assert!(declared.contains(name), "{name} is not in the catalog");
982 let m = ControlMethod::from_name(name).expect("from_name round-trips");
983 assert_eq!(m.category(), Category::Peers, "{name} is a peers method");
984 assert_eq!(m.routing(), Routing::Owned, "{name} is served by the shell");
985 assert!(m.requires_auth(), "{name} must require the control token");
986 assert!(!m.is_open_read(), "{name} is not an open read");
987 }
988 assert!(ControlMethod::ChiaPeersAdd.requires_master_token());
992 assert!(ControlMethod::ChiaPeersRemove.requires_master_token());
993 assert!(
994 !ControlMethod::ChiaPeersList.requires_master_token(),
995 "list grants nothing that outlives the token; gating it would blind a paired client \
996 to the trust state it is subject to"
997 );
998 for name in ["control.chiaPeers.add", "control.chiaPeers.remove"] {
1002 let summary = ControlMethod::from_name(name).unwrap().summary();
1003 assert!(
1004 summary.to_lowercase().contains("corroboration"),
1005 "{name} summary must name the corroboration bypass, got: {summary}"
1006 );
1007 }
1008 }
1009
1010 #[test]
1011 fn every_method_has_a_nonempty_summary() {
1012 for &m in ControlMethod::ALL {
1013 assert!(!m.summary().is_empty(), "{} has no summary", m.name());
1014 }
1015 }
1016}