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 PeersConnect,
126 PeersDisconnect,
128
129 Subscribe,
132 Unsubscribe,
134 ListSubscriptions,
136
137 WalletBalance,
140 WalletCoins,
142 WalletPeak,
144 WalletBroadcast,
146
147 PairingRequest,
150 PairingPoll,
152}
153
154impl ControlMethod {
155 pub const fn name(self) -> &'static str {
157 match self {
158 ControlMethod::Status => "control.status",
159 ControlMethod::ConfigGet => "control.config.get",
160 ControlMethod::ConfigSetUpstream => "control.config.setUpstream",
161 ControlMethod::LogSetLevel => "control.log.setLevel",
162 ControlMethod::CacheGet => "control.cache.get",
163 ControlMethod::CacheSetCap => "control.cache.setCap",
164 ControlMethod::CacheClear => "control.cache.clear",
165 ControlMethod::HostedStoresList => "control.hostedStores.list",
166 ControlMethod::HostedStoresPin => "control.hostedStores.pin",
167 ControlMethod::HostedStoresUnpin => "control.hostedStores.unpin",
168 ControlMethod::HostedStoresStatus => "control.hostedStores.status",
169 ControlMethod::SyncStatus => "control.sync.status",
170 ControlMethod::SyncTrigger => "control.sync.trigger",
171 ControlMethod::UpdaterStatus => "control.updater.status",
172 ControlMethod::UpdaterSetChannel => "control.updater.setChannel",
173 ControlMethod::UpdaterPause => "control.updater.pause",
174 ControlMethod::UpdaterResume => "control.updater.resume",
175 ControlMethod::UpdaterCheckNow => "control.updater.checkNow",
176 ControlMethod::PairingList => "control.pairing.list",
177 ControlMethod::PairingApprove => "control.pairing.approve",
178 ControlMethod::PairingRevoke => "control.pairing.revoke",
179 ControlMethod::PeerStatus => "control.peerStatus",
180 ControlMethod::PeersConnect => "control.peers.connect",
181 ControlMethod::PeersDisconnect => "control.peers.disconnect",
182 ControlMethod::Subscribe => "control.subscribe",
183 ControlMethod::Unsubscribe => "control.unsubscribe",
184 ControlMethod::ListSubscriptions => "control.listSubscriptions",
185 ControlMethod::WalletBalance => "control.wallet.balance",
186 ControlMethod::WalletCoins => "control.wallet.coins",
187 ControlMethod::WalletPeak => "control.wallet.peak",
188 ControlMethod::WalletBroadcast => "control.wallet.broadcast",
189 ControlMethod::PairingRequest => "pairing.request",
190 ControlMethod::PairingPoll => "pairing.poll",
191 }
192 }
193
194 pub fn from_name(name: &str) -> Option<ControlMethod> {
196 ControlMethod::ALL
197 .iter()
198 .copied()
199 .find(|m| m.name() == name)
200 }
201
202 pub const fn requires_auth(self) -> bool {
217 !self.is_open_read()
218 && !matches!(
219 self,
220 ControlMethod::PairingRequest | ControlMethod::PairingPoll
221 )
222 }
223
224 pub const fn is_open_read(self) -> bool {
232 matches!(
233 self,
234 ControlMethod::WalletBalance | ControlMethod::WalletCoins | ControlMethod::WalletPeak
235 )
236 }
237
238 pub const fn is_pairing_admin(self) -> bool {
244 matches!(
245 self,
246 ControlMethod::PairingList
247 | ControlMethod::PairingApprove
248 | ControlMethod::PairingRevoke
249 )
250 }
251
252 pub const fn routing(self) -> Routing {
254 match self {
255 ControlMethod::PeerStatus
256 | ControlMethod::PeersConnect
257 | ControlMethod::PeersDisconnect
258 | ControlMethod::Subscribe
259 | ControlMethod::Unsubscribe
260 | ControlMethod::ListSubscriptions
261 | ControlMethod::WalletBalance
262 | ControlMethod::WalletCoins
263 | ControlMethod::WalletPeak
264 | ControlMethod::WalletBroadcast => Routing::Delegated,
265 ControlMethod::PairingRequest | ControlMethod::PairingPoll => Routing::OpenBootstrap,
266 _ => Routing::Owned,
267 }
268 }
269
270 pub const fn category(self) -> Category {
272 match self {
273 ControlMethod::Status => Category::Status,
274 ControlMethod::ConfigGet | ControlMethod::ConfigSetUpstream => Category::Config,
275 ControlMethod::LogSetLevel => Category::Log,
276 ControlMethod::CacheGet | ControlMethod::CacheSetCap | ControlMethod::CacheClear => {
277 Category::Cache
278 }
279 ControlMethod::HostedStoresList
280 | ControlMethod::HostedStoresPin
281 | ControlMethod::HostedStoresUnpin
282 | ControlMethod::HostedStoresStatus => Category::HostedStores,
283 ControlMethod::SyncStatus | ControlMethod::SyncTrigger => Category::Sync,
284 ControlMethod::UpdaterStatus
285 | ControlMethod::UpdaterSetChannel
286 | ControlMethod::UpdaterPause
287 | ControlMethod::UpdaterResume
288 | ControlMethod::UpdaterCheckNow => Category::Updater,
289 ControlMethod::PairingList
290 | ControlMethod::PairingApprove
291 | ControlMethod::PairingRevoke
292 | ControlMethod::PairingRequest
293 | ControlMethod::PairingPoll => Category::Pairing,
294 ControlMethod::PeerStatus
295 | ControlMethod::PeersConnect
296 | ControlMethod::PeersDisconnect => Category::Peers,
297 ControlMethod::Subscribe
298 | ControlMethod::Unsubscribe
299 | ControlMethod::ListSubscriptions => Category::Subscriptions,
300 ControlMethod::WalletBalance
301 | ControlMethod::WalletCoins
302 | ControlMethod::WalletPeak
303 | ControlMethod::WalletBroadcast => Category::Wallet,
304 }
305 }
306
307 pub const fn summary(self) -> &'static str {
309 match self {
310 ControlMethod::Status => "A rich node status snapshot (version, uptime, addr, cache, hosted/pinned counts, sync availability).",
311 ControlMethod::ConfigGet => "The node's effective configuration (addr/port, upstream + override, cache dir/shared, config path, sync availability).",
312 ControlMethod::ConfigSetUpstream => "Persist an upstream-RPC override; takes effect on next node start (requires_restart).",
313 ControlMethod::LogSetLevel => "Live-swap the running node's tracing EnvFilter directive (not persisted).",
314 ControlMethod::CacheGet => "The on-disk content-cache view: cap_bytes, used_bytes, dir, shared.",
315 ControlMethod::CacheSetCap => "Set the on-disk cache size cap in bytes (floored at 64 MiB).",
316 ControlMethod::CacheClear => "Delete all locally cached DIG content.",
317 ControlMethod::HostedStoresList => "Every held/pinned store, merged, with each store's cached capsules and a pinned flag.",
318 ControlMethod::HostedStoresPin => "Pin a store (storeId[:rootHash]); pre-fetches the capsule when a root is given and §21 sync is available.",
319 ControlMethod::HostedStoresUnpin => "Unpin a store and evict its cached capsules.",
320 ControlMethod::HostedStoresStatus => "Per-store status: pinned flag, cached capsules, total bytes.",
321 ControlMethod::SyncStatus => "Whether authenticated §21 whole-store sync is available, plus pinned-store cache coverage.",
322 ControlMethod::SyncTrigger => "Trigger a §21 sync for one capsule (storeId + root).",
323 ControlMethod::UpdaterStatus => "The DIG auto-update beacon's current status (proxied from dig-updater).",
324 ControlMethod::UpdaterSetChannel => "Set the beacon's update channel (\"nightly\" | \"stable\").",
325 ControlMethod::UpdaterPause => "Suspend the beacon's auto-updates (optionally until a unix time).",
326 ControlMethod::UpdaterResume => "Resume the beacon's auto-updates.",
327 ControlMethod::UpdaterCheckNow => "Force an immediate beacon update check.",
328 ControlMethod::PairingList => "List pending pairing requests and issued paired tokens (MASTER token only).",
329 ControlMethod::PairingApprove => "Approve a pending pairing, minting a scoped token (MASTER token only).",
330 ControlMethod::PairingRevoke => "Revoke an issued paired token by token_id (MASTER token only).",
331 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).",
332 ControlMethod::PeersConnect => "Dial a peer by address, or resolve an already-connected peer_id, via the live gossip pool.",
333 ControlMethod::PeersDisconnect => "Drop a pooled peer by peer_id, closing its mTLS link (idempotent).",
334 ControlMethod::Subscribe => "Subscribe the node to a store it actively watches and gap-fills.",
335 ControlMethod::Unsubscribe => "Stop watching a store.",
336 ControlMethod::ListSubscriptions => "The node's persisted subscription set + count.",
337 ControlMethod::WalletCoins => "READ-only: the spendable coin records for an address + asset, with the tier that answered and the height they reflect.",
338 ControlMethod::WalletPeak => "READ-only: the node's current chain peak height, independent of any address.",
339 ControlMethod::WalletBroadcast => "Push an ALREADY-SIGNED spend bundle to the network; the node never signs. TOKEN-GATED.",
340 ControlMethod::WalletBalance => "READ-only: the confirmed spendable balance for an address + asset (plus pending, sync freshness, and the peak height it reflects).",
341 ControlMethod::PairingRequest => "OPEN: request a control-token pairing; returns a pairing_id + pairing_code to compare.",
342 ControlMethod::PairingPoll => "OPEN: poll a pairing by id; once the operator approves, returns the scoped token once.",
343 }
344 }
345
346 pub const ALL: &'static [ControlMethod] = &[
349 ControlMethod::Status,
350 ControlMethod::ConfigGet,
351 ControlMethod::ConfigSetUpstream,
352 ControlMethod::LogSetLevel,
353 ControlMethod::CacheGet,
354 ControlMethod::CacheSetCap,
355 ControlMethod::CacheClear,
356 ControlMethod::HostedStoresList,
357 ControlMethod::HostedStoresPin,
358 ControlMethod::HostedStoresUnpin,
359 ControlMethod::HostedStoresStatus,
360 ControlMethod::SyncStatus,
361 ControlMethod::SyncTrigger,
362 ControlMethod::UpdaterStatus,
363 ControlMethod::UpdaterSetChannel,
364 ControlMethod::UpdaterPause,
365 ControlMethod::UpdaterResume,
366 ControlMethod::UpdaterCheckNow,
367 ControlMethod::PairingList,
368 ControlMethod::PairingApprove,
369 ControlMethod::PairingRevoke,
370 ControlMethod::PeerStatus,
371 ControlMethod::PeersConnect,
372 ControlMethod::PeersDisconnect,
373 ControlMethod::Subscribe,
374 ControlMethod::Unsubscribe,
375 ControlMethod::ListSubscriptions,
376 ControlMethod::WalletBalance,
377 ControlMethod::WalletCoins,
378 ControlMethod::WalletPeak,
379 ControlMethod::WalletBroadcast,
380 ControlMethod::PairingRequest,
381 ControlMethod::PairingPoll,
382 ];
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use std::collections::BTreeSet;
389
390 #[test]
391 fn every_method_has_a_unique_wire_name() {
392 let names: BTreeSet<&str> = ControlMethod::ALL.iter().map(|m| m.name()).collect();
393 assert_eq!(
394 names.len(),
395 ControlMethod::ALL.len(),
396 "duplicate or missing wire names in the catalog"
397 );
398 }
399
400 #[test]
401 fn from_name_round_trips_every_method() {
402 for &m in ControlMethod::ALL {
403 assert_eq!(ControlMethod::from_name(m.name()), Some(m));
404 }
405 assert_eq!(ControlMethod::from_name("control.nope"), None);
406 assert_eq!(ControlMethod::from_name(""), None);
407 }
408
409 #[test]
410 fn the_token_less_surface_is_exactly_the_bootstrap_plus_the_chain_reads() {
411 let expected_open: BTreeSet<&str> = [
415 "pairing.request",
416 "pairing.poll",
417 "control.wallet.balance",
418 "control.wallet.coins",
419 "control.wallet.peak",
420 ]
421 .into_iter()
422 .collect();
423 let actual_open: BTreeSet<&str> = ControlMethod::ALL
424 .iter()
425 .filter(|m| !m.requires_auth())
426 .map(|m| m.name())
427 .collect();
428 assert_eq!(actual_open, expected_open);
429 }
430
431 #[test]
435 fn the_push_is_the_one_wallet_method_behind_the_token() {
436 let gated: Vec<&str> = ControlMethod::ALL
437 .iter()
438 .filter(|m| m.category() == Category::Wallet && m.requires_auth())
439 .map(|m| m.name())
440 .collect();
441 assert_eq!(gated, vec!["control.wallet.broadcast"]);
442 assert!(!ControlMethod::WalletBroadcast.is_open_read());
443 }
444
445 #[test]
446 fn only_pairing_bootstrap_is_open_bootstrap_routed() {
447 for &m in ControlMethod::ALL {
448 let open_bootstrap = matches!(
449 m,
450 ControlMethod::PairingRequest | ControlMethod::PairingPoll
451 );
452 assert_eq!(
453 m.routing() == Routing::OpenBootstrap,
454 open_bootstrap,
455 "{} routing mismatch",
456 m.name()
457 );
458 }
459 }
460
461 #[test]
462 fn pairing_admin_methods_are_exactly_three() {
463 let admin: Vec<&str> = ControlMethod::ALL
464 .iter()
465 .filter(|m| m.is_pairing_admin())
466 .map(|m| m.name())
467 .collect();
468 assert_eq!(
469 admin,
470 vec![
471 "control.pairing.list",
472 "control.pairing.approve",
473 "control.pairing.revoke"
474 ]
475 );
476 }
477
478 #[test]
479 fn delegated_set_matches_the_engine_surface() {
480 let delegated: BTreeSet<&str> = ControlMethod::ALL
481 .iter()
482 .filter(|m| m.routing() == Routing::Delegated)
483 .map(|m| m.name())
484 .collect();
485 let expected: BTreeSet<&str> = [
486 "control.wallet.coins",
487 "control.wallet.peak",
488 "control.wallet.broadcast",
489 "control.peerStatus",
490 "control.peers.connect",
491 "control.peers.disconnect",
492 "control.subscribe",
493 "control.unsubscribe",
494 "control.listSubscriptions",
495 "control.wallet.balance",
496 ]
497 .into_iter()
498 .collect();
499 assert_eq!(delegated, expected);
500 }
501
502 #[test]
503 fn every_method_has_a_nonempty_summary() {
504 for &m in ControlMethod::ALL {
505 assert!(!m.summary().is_empty(), "{} has no summary", m.name());
506 }
507 }
508}