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}
58
59#[non_exhaustive]
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
66pub enum ControlMethod {
67 Status,
70 ConfigGet,
72 ConfigSetUpstream,
74 LogSetLevel,
76
77 CacheGet,
80 CacheSetCap,
82 CacheClear,
84
85 HostedStoresList,
88 HostedStoresPin,
90 HostedStoresUnpin,
92 HostedStoresStatus,
94
95 SyncStatus,
98 SyncTrigger,
100
101 UpdaterStatus,
104 UpdaterSetChannel,
106 UpdaterPause,
108 UpdaterResume,
110 UpdaterCheckNow,
112
113 PairingList,
116 PairingApprove,
118 PairingRevoke,
120
121 PeerStatus,
124 PeerCounts,
126 PeersConnect,
128 PeersDisconnect,
130
131 Subscribe,
134 Unsubscribe,
136 ListSubscriptions,
138
139 WalletBalance,
142 WalletCoins,
144 WalletCoinById,
146 WalletPeak,
148 WalletSyncStatus,
150 WalletBroadcast,
152
153 PairingRequest,
156 PairingPoll,
158}
159
160impl ControlMethod {
161 pub const fn name(self) -> &'static str {
163 match self {
164 ControlMethod::Status => "control.status",
165 ControlMethod::ConfigGet => "control.config.get",
166 ControlMethod::ConfigSetUpstream => "control.config.setUpstream",
167 ControlMethod::LogSetLevel => "control.log.setLevel",
168 ControlMethod::CacheGet => "control.cache.get",
169 ControlMethod::CacheSetCap => "control.cache.setCap",
170 ControlMethod::CacheClear => "control.cache.clear",
171 ControlMethod::HostedStoresList => "control.hostedStores.list",
172 ControlMethod::HostedStoresPin => "control.hostedStores.pin",
173 ControlMethod::HostedStoresUnpin => "control.hostedStores.unpin",
174 ControlMethod::HostedStoresStatus => "control.hostedStores.status",
175 ControlMethod::SyncStatus => "control.sync.status",
176 ControlMethod::SyncTrigger => "control.sync.trigger",
177 ControlMethod::UpdaterStatus => "control.updater.status",
178 ControlMethod::UpdaterSetChannel => "control.updater.setChannel",
179 ControlMethod::UpdaterPause => "control.updater.pause",
180 ControlMethod::UpdaterResume => "control.updater.resume",
181 ControlMethod::UpdaterCheckNow => "control.updater.checkNow",
182 ControlMethod::PairingList => "control.pairing.list",
183 ControlMethod::PairingApprove => "control.pairing.approve",
184 ControlMethod::PairingRevoke => "control.pairing.revoke",
185 ControlMethod::PeerStatus => "control.peerStatus",
186 ControlMethod::PeerCounts => "control.peerCounts",
187 ControlMethod::PeersConnect => "control.peers.connect",
188 ControlMethod::PeersDisconnect => "control.peers.disconnect",
189 ControlMethod::Subscribe => "control.subscribe",
190 ControlMethod::Unsubscribe => "control.unsubscribe",
191 ControlMethod::ListSubscriptions => "control.listSubscriptions",
192 ControlMethod::WalletBalance => "control.wallet.balance",
193 ControlMethod::WalletCoins => "control.wallet.coins",
194 ControlMethod::WalletCoinById => "control.wallet.coinById",
195 ControlMethod::WalletPeak => "control.wallet.peak",
196 ControlMethod::WalletSyncStatus => "control.wallet.syncStatus",
197 ControlMethod::WalletBroadcast => "control.wallet.broadcast",
198 ControlMethod::PairingRequest => "pairing.request",
199 ControlMethod::PairingPoll => "pairing.poll",
200 }
201 }
202
203 pub fn from_name(name: &str) -> Option<ControlMethod> {
205 ControlMethod::ALL
206 .iter()
207 .copied()
208 .find(|m| m.name() == name)
209 }
210
211 pub const fn requires_auth(self) -> bool {
229 !self.is_open_read()
230 && !matches!(
231 self,
232 ControlMethod::PairingRequest | ControlMethod::PairingPoll
233 )
234 }
235
236 pub const fn is_open_read(self) -> bool {
259 matches!(
260 self,
261 ControlMethod::WalletBalance
262 | ControlMethod::WalletCoins
263 | ControlMethod::WalletCoinById
264 | ControlMethod::WalletPeak
265 | ControlMethod::WalletSyncStatus
266 | ControlMethod::PeerCounts
267 )
268 }
269
270 pub const fn is_pairing_admin(self) -> bool {
276 matches!(
277 self,
278 ControlMethod::PairingList
279 | ControlMethod::PairingApprove
280 | ControlMethod::PairingRevoke
281 )
282 }
283
284 pub const fn routing(self) -> Routing {
286 match self {
287 ControlMethod::PeerStatus
288 | ControlMethod::PeerCounts
289 | ControlMethod::PeersConnect
290 | ControlMethod::PeersDisconnect
291 | ControlMethod::Subscribe
292 | ControlMethod::Unsubscribe
293 | ControlMethod::ListSubscriptions
294 | ControlMethod::WalletBalance
295 | ControlMethod::WalletCoins
296 | ControlMethod::WalletCoinById
297 | ControlMethod::WalletPeak
298 | ControlMethod::WalletSyncStatus
299 | ControlMethod::WalletBroadcast => Routing::Delegated,
300 ControlMethod::PairingRequest | ControlMethod::PairingPoll => Routing::OpenBootstrap,
301 _ => Routing::Owned,
302 }
303 }
304
305 pub const fn category(self) -> Category {
307 match self {
308 ControlMethod::Status => Category::Status,
309 ControlMethod::ConfigGet | ControlMethod::ConfigSetUpstream => Category::Config,
310 ControlMethod::LogSetLevel => Category::Log,
311 ControlMethod::CacheGet | ControlMethod::CacheSetCap | ControlMethod::CacheClear => {
312 Category::Cache
313 }
314 ControlMethod::HostedStoresList
315 | ControlMethod::HostedStoresPin
316 | ControlMethod::HostedStoresUnpin
317 | ControlMethod::HostedStoresStatus => Category::HostedStores,
318 ControlMethod::SyncStatus | ControlMethod::SyncTrigger => Category::Sync,
319 ControlMethod::UpdaterStatus
320 | ControlMethod::UpdaterSetChannel
321 | ControlMethod::UpdaterPause
322 | ControlMethod::UpdaterResume
323 | ControlMethod::UpdaterCheckNow => Category::Updater,
324 ControlMethod::PairingList
325 | ControlMethod::PairingApprove
326 | ControlMethod::PairingRevoke
327 | ControlMethod::PairingRequest
328 | ControlMethod::PairingPoll => Category::Pairing,
329 ControlMethod::PeerStatus
330 | ControlMethod::PeerCounts
331 | ControlMethod::PeersConnect
332 | ControlMethod::PeersDisconnect => Category::Peers,
333 ControlMethod::Subscribe
334 | ControlMethod::Unsubscribe
335 | ControlMethod::ListSubscriptions => Category::Subscriptions,
336 ControlMethod::WalletBalance
337 | ControlMethod::WalletCoins
338 | ControlMethod::WalletCoinById
339 | ControlMethod::WalletPeak
340 | ControlMethod::WalletSyncStatus
341 | ControlMethod::WalletBroadcast => Category::Wallet,
342 }
343 }
344
345 pub const fn summary(self) -> &'static str {
347 match self {
348 ControlMethod::Status => "A rich node status snapshot (version, uptime, addr, cache, hosted/pinned counts, sync availability).",
349 ControlMethod::ConfigGet => "The node's effective configuration (addr/port, upstream + override, cache dir/shared, config path, sync availability).",
350 ControlMethod::ConfigSetUpstream => "Persist an upstream-RPC override; takes effect on next node start (requires_restart).",
351 ControlMethod::LogSetLevel => "Live-swap the running node's tracing EnvFilter directive (not persisted).",
352 ControlMethod::CacheGet => "The on-disk content-cache view: cap_bytes, used_bytes, dir, shared.",
353 ControlMethod::CacheSetCap => "Set the on-disk cache size cap in bytes (floored at 64 MiB).",
354 ControlMethod::CacheClear => "Delete all locally cached DIG content.",
355 ControlMethod::HostedStoresList => "Every held/pinned store, merged, with each store's cached capsules and a pinned flag.",
356 ControlMethod::HostedStoresPin => "Pin a store (storeId[:rootHash]); pre-fetches the capsule when a root is given and §21 sync is available.",
357 ControlMethod::HostedStoresUnpin => "Unpin a store and evict its cached capsules.",
358 ControlMethod::HostedStoresStatus => "Per-store status: pinned flag, cached capsules, total bytes.",
359 ControlMethod::SyncStatus => "Whether authenticated §21 whole-store sync is available, plus pinned-store cache coverage.",
360 ControlMethod::SyncTrigger => "Trigger a §21 sync for one capsule (storeId + root).",
361 ControlMethod::UpdaterStatus => "The DIG auto-update beacon's current status (proxied from dig-updater).",
362 ControlMethod::UpdaterSetChannel => "Set the beacon's update channel (\"nightly\" | \"stable\").",
363 ControlMethod::UpdaterPause => "Suspend the beacon's auto-updates (optionally until a unix time).",
364 ControlMethod::UpdaterResume => "Resume the beacon's auto-updates.",
365 ControlMethod::UpdaterCheckNow => "Force an immediate beacon update check.",
366 ControlMethod::PairingList => "List pending pairing requests and issued paired tokens (MASTER token only).",
367 ControlMethod::PairingApprove => "Approve a pending pairing, minting a scoped token (MASTER token only).",
368 ControlMethod::PairingRevoke => "Revoke an issued paired token by token_id (MASTER token only).",
369 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.",
370 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.",
371 ControlMethod::PeersConnect => "Dial a peer by address, or resolve an already-connected peer_id, via the live gossip pool.",
372 ControlMethod::PeersDisconnect => "Drop a pooled peer by peer_id, closing its mTLS link (idempotent).",
373 ControlMethod::Subscribe => "Subscribe the node to a store it actively watches and gap-fills.",
374 ControlMethod::Unsubscribe => "Stop watching a store.",
375 ControlMethod::ListSubscriptions => "The node's persisted subscription set + count.",
376 ControlMethod::WalletCoins => "READ-only: the spendable coin records for an address + asset, with the tier that answered and the height they reflect.",
377 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.",
378 ControlMethod::WalletPeak => "READ-only: the node's current chain peak height, independent of any address.",
379 ControlMethod::WalletSyncStatus => "READ-only: whether the wallet's CHAIN replica is being kept current (not_started/syncing/synced), 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).",
380 ControlMethod::WalletBroadcast => "Push an ALREADY-SIGNED spend bundle to the network; the node never signs. TOKEN-GATED.",
381 ControlMethod::WalletBalance => "READ-only: the confirmed spendable balance for an address + asset (plus pending, sync freshness, and the peak height it reflects).",
382 ControlMethod::PairingRequest => "OPEN: request a control-token pairing; returns a pairing_id + pairing_code to compare.",
383 ControlMethod::PairingPoll => "OPEN: poll a pairing by id; once the operator approves, returns the scoped token once.",
384 }
385 }
386
387 pub const ALL: &'static [ControlMethod] = &[
390 ControlMethod::Status,
391 ControlMethod::ConfigGet,
392 ControlMethod::ConfigSetUpstream,
393 ControlMethod::LogSetLevel,
394 ControlMethod::CacheGet,
395 ControlMethod::CacheSetCap,
396 ControlMethod::CacheClear,
397 ControlMethod::HostedStoresList,
398 ControlMethod::HostedStoresPin,
399 ControlMethod::HostedStoresUnpin,
400 ControlMethod::HostedStoresStatus,
401 ControlMethod::SyncStatus,
402 ControlMethod::SyncTrigger,
403 ControlMethod::UpdaterStatus,
404 ControlMethod::UpdaterSetChannel,
405 ControlMethod::UpdaterPause,
406 ControlMethod::UpdaterResume,
407 ControlMethod::UpdaterCheckNow,
408 ControlMethod::PairingList,
409 ControlMethod::PairingApprove,
410 ControlMethod::PairingRevoke,
411 ControlMethod::PeerStatus,
412 ControlMethod::PeerCounts,
413 ControlMethod::PeersConnect,
414 ControlMethod::PeersDisconnect,
415 ControlMethod::Subscribe,
416 ControlMethod::Unsubscribe,
417 ControlMethod::ListSubscriptions,
418 ControlMethod::WalletBalance,
419 ControlMethod::WalletCoins,
420 ControlMethod::WalletCoinById,
421 ControlMethod::WalletPeak,
422 ControlMethod::WalletSyncStatus,
423 ControlMethod::WalletBroadcast,
424 ControlMethod::PairingRequest,
425 ControlMethod::PairingPoll,
426 ];
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432 use std::collections::BTreeSet;
433
434 #[test]
435 fn every_method_has_a_unique_wire_name() {
436 let names: BTreeSet<&str> = ControlMethod::ALL.iter().map(|m| m.name()).collect();
437 assert_eq!(
438 names.len(),
439 ControlMethod::ALL.len(),
440 "duplicate or missing wire names in the catalog"
441 );
442 }
443
444 #[test]
445 fn from_name_round_trips_every_method() {
446 for &m in ControlMethod::ALL {
447 assert_eq!(ControlMethod::from_name(m.name()), Some(m));
448 }
449 assert_eq!(ControlMethod::from_name("control.nope"), None);
450 assert_eq!(ControlMethod::from_name(""), None);
451 }
452
453 #[test]
454 fn the_token_less_surface_is_exactly_the_bootstrap_plus_the_chain_reads() {
455 let expected_open: BTreeSet<&str> = [
459 "pairing.request",
460 "pairing.poll",
461 "control.wallet.balance",
462 "control.wallet.coins",
463 "control.wallet.coinById",
464 "control.wallet.peak",
465 "control.wallet.syncStatus",
466 "control.peerCounts",
467 ]
468 .into_iter()
469 .collect();
470 assert_eq!(
471 expected_open.len(),
472 8,
473 "the open surface is eight named methods"
474 );
475 let actual_open: BTreeSet<&str> = ControlMethod::ALL
476 .iter()
477 .filter(|m| !m.requires_auth())
478 .map(|m| m.name())
479 .collect();
480 assert_eq!(actual_open, expected_open);
481 }
482
483 #[test]
487 fn the_push_is_the_one_wallet_method_behind_the_token() {
488 let gated: Vec<&str> = ControlMethod::ALL
489 .iter()
490 .filter(|m| m.category() == Category::Wallet && m.requires_auth())
491 .map(|m| m.name())
492 .collect();
493 assert_eq!(gated, vec!["control.wallet.broadcast"]);
494 assert!(!ControlMethod::WalletBroadcast.is_open_read());
495 }
496
497 #[test]
498 fn only_pairing_bootstrap_is_open_bootstrap_routed() {
499 for &m in ControlMethod::ALL {
500 let open_bootstrap = matches!(
501 m,
502 ControlMethod::PairingRequest | ControlMethod::PairingPoll
503 );
504 assert_eq!(
505 m.routing() == Routing::OpenBootstrap,
506 open_bootstrap,
507 "{} routing mismatch",
508 m.name()
509 );
510 }
511 }
512
513 #[test]
514 fn pairing_admin_methods_are_exactly_three() {
515 let admin: Vec<&str> = ControlMethod::ALL
516 .iter()
517 .filter(|m| m.is_pairing_admin())
518 .map(|m| m.name())
519 .collect();
520 assert_eq!(
521 admin,
522 vec![
523 "control.pairing.list",
524 "control.pairing.approve",
525 "control.pairing.revoke"
526 ]
527 );
528 }
529
530 #[test]
531 fn delegated_set_matches_the_engine_surface() {
532 let delegated: BTreeSet<&str> = ControlMethod::ALL
533 .iter()
534 .filter(|m| m.routing() == Routing::Delegated)
535 .map(|m| m.name())
536 .collect();
537 let expected: BTreeSet<&str> = [
538 "control.wallet.coins",
539 "control.wallet.coinById",
540 "control.wallet.peak",
541 "control.wallet.syncStatus",
542 "control.wallet.broadcast",
543 "control.peerStatus",
544 "control.peerCounts",
545 "control.peers.connect",
546 "control.peers.disconnect",
547 "control.subscribe",
548 "control.unsubscribe",
549 "control.listSubscriptions",
550 "control.wallet.balance",
551 ]
552 .into_iter()
553 .collect();
554 assert_eq!(delegated, expected);
555 }
556
557 #[test]
558 fn every_method_has_a_nonempty_summary() {
559 for &m in ControlMethod::ALL {
560 assert!(!m.summary().is_empty(), "{} has no summary", m.name());
561 }
562 }
563}