decdn_common/cli/publish.rs
1//! CLI argument parsing for `decdn publish` — the origin-publisher control
2//! plane (issues #1029 / #1491, ADR 002 / ADR 011). Three write commands:
3//! `namespace create` (`PublisherRegistry.createNamespace`), and then `assign` /
4//! `revoke` (`OriginAssignment.addOrigin` / `removeOrigin`), which a vetted
5//! publisher self-serves with no waiting. Vetting itself is granted out-of-band
6//! by a `VETTER_ROLE` holder on the installed vetting policy (an operator,
7//! multisig, or governance), not through this CLI. `assign` submits one
8//! `addOrigin` per operator, so it is N transactions rather than one. Content is
9//! bound to a namespace off-chain at fetch time, so there is no per-hash
10//! on-chain claim.
11
12use clap::{Args, Subcommand};
13
14use super::common::CommonChainArgs;
15
16/// Parse a namespace id, rejecting the reserved `0`. Namespace ids start at 1
17/// (`PublisherRegistry` pre-increments from 0, and id 0 is reserved for content
18/// published without a namespace), so `0` can never be owned by a publisher —
19/// failing fast here saves the gas of a guaranteed-revert `addOrigin`
20/// transaction, matching the client-side duplicate-operator guard.
21fn parse_namespace_id(s: &str) -> Result<u64, String> {
22 let id: u64 = s
23 .parse()
24 .map_err(|_| format!("invalid namespace id {s:?}: expected a non-negative integer"))?;
25 if id == 0 {
26 return Err("namespace id must be >= 1 (0 is reserved)".to_string());
27 }
28 Ok(id)
29}
30
31/// `decdn publish <subcommand>`.
32#[derive(Args, Debug)]
33pub struct PublishArgs {
34 /// Publisher control-plane subcommand.
35 #[command(subcommand)]
36 pub command: PublishCommand,
37}
38
39/// Publisher control-plane operations.
40#[derive(Subcommand, Debug)]
41pub enum PublishCommand {
42 /// Create a new namespace owned by the signer (`createNamespace`).
43 Namespace(NamespaceArgs),
44 /// Seat authorized origins for a namespace (`addOrigin` per operator).
45 /// Requires a signer the installed vetting policy has vetted; effective
46 /// immediately. Vetting is granted out-of-band by a `VETTER_ROLE` holder
47 /// (an operator, multisig, or governance), not through this CLI.
48 Assign(AssignArgs),
49 /// Unseat one authorized origin from a namespace (`removeOrigin`).
50 Revoke(RevokeArgs),
51}
52
53/// `decdn publish namespace <subcommand>` (nested to match the issue's
54/// `namespace create` phrasing and leave room for future `namespace transfer`).
55#[derive(Args, Debug)]
56pub struct NamespaceArgs {
57 /// Namespace lifecycle subcommand.
58 #[command(subcommand)]
59 pub command: NamespaceCommand,
60}
61
62/// Namespace lifecycle operations.
63#[derive(Subcommand, Debug)]
64pub enum NamespaceCommand {
65 /// Mint a fresh namespace; prints its id.
66 Create(NamespaceCreateArgs),
67}
68
69/// `decdn publish namespace create` flags.
70#[derive(Args, Debug)]
71pub struct NamespaceCreateArgs {
72 /// Chain coordinates and the publisher contract addresses.
73 #[command(flatten)]
74 pub chain: PublishChainArgs,
75}
76
77/// `decdn publish assign <namespace> <ops…>` flags.
78#[derive(Args, Debug)]
79pub struct AssignArgs {
80 /// Namespace id whose authorized-origin set is being extended.
81 #[arg(value_name = "NAMESPACE", value_parser = parse_namespace_id)]
82 pub namespace: u64,
83
84 /// Operator Ethereum addresses to authorize. Each must be an active
85 /// bonded node and is seated by its own transaction, in the order given.
86 /// At least one; no duplicates.
87 #[arg(value_name = "OPERATORS", required = true, num_args = 1..)]
88 pub operators: Vec<String>,
89
90 /// Chain coordinates and the publisher contract addresses.
91 #[command(flatten)]
92 pub chain: PublishChainArgs,
93}
94
95/// `decdn publish revoke <namespace> <operator>` flags.
96#[derive(Args, Debug)]
97pub struct RevokeArgs {
98 /// Namespace id to unseat the origin from.
99 #[arg(value_name = "NAMESPACE", value_parser = parse_namespace_id)]
100 pub namespace: u64,
101
102 /// Operator Ethereum address to unseat. Must currently be authorized.
103 #[arg(value_name = "OPERATOR")]
104 pub operator: String,
105
106 /// Chain coordinates and the publisher contract addresses.
107 #[command(flatten)]
108 pub chain: PublishChainArgs,
109}
110
111/// Chain coordinates shared by the publish subcommands. Mirrors `ChainArgs`
112/// but targets the publisher contracts (`PublisherRegistry`,
113/// `OriginAssignment`) instead of `CapacityBond`. The common coordinates live
114/// in [`CommonChainArgs`]; this struct adds the two publisher-contract address
115/// flags.
116#[derive(Args, Debug)]
117pub struct PublishChainArgs {
118 /// RPC endpoint, chain id, keystore, and data-dir flags shared by every
119 /// chain-touching command.
120 #[command(flatten)]
121 pub common: CommonChainArgs,
122
123 /// `PublisherRegistry` address (namespace lifecycle). Overrides
124 /// `blockchain.publisher_registry_address`.
125 #[arg(long, value_name = "ADDR")]
126 pub publisher_registry_address: Option<String>,
127
128 /// `OriginAssignment` address (assign). Overrides
129 /// `blockchain.origin_assignment_address`.
130 #[arg(long, value_name = "ADDR")]
131 pub origin_assignment_address: Option<String>,
132}
133
134#[cfg(test)]
135#[allow(clippy::unwrap_used)]
136mod tests;