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