Skip to main content

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;