Skip to main content

decdn_cli/
known_chains.rs

1//! Registry of deployment targets `decdn config init --chain <name>` can bake in.
2//!
3//! Each [`KnownChain`] pairs a small hand-written descriptor (name, label,
4//! chain id, public RPC endpoint) with the on-disk deployment manifest embedded
5//! at compile time. Contract addresses are never written here — they are read
6//! from the manifest the Foundry `DeployProtocol` script writes, so a redeploy
7//! flows into `config init` automatically with no addresses to hand-sync.
8//!
9//! The manifest is embedded from `crates/cli/deployments/`, not from
10//! `contracts/deployments/` where the deploy script writes it, because
11//! `include_str!` may not escape the package: a path outside the crate is
12//! unreachable from the published `.crate` and `cargo install decdn-cli` would
13//! fail to build. `contracts/deployments/` stays canonical and this is a
14//! byte-identical mirror — the `deployment-manifest-mirror` pre-commit hook and
15//! its CI counterpart `cmp` the two and fail on any drift, so re-copy the file
16//! as part of a redeploy.
17
18use std::collections::BTreeMap;
19
20use anyhow::Context;
21use serde::Deserialize;
22
23/// A deployment target `config init` knows how to bake into a fresh config.
24#[derive(Debug)]
25pub struct KnownChain {
26    /// `--chain` selector, e.g. `"arbitrum-sepolia"`.
27    pub name: &'static str,
28    /// Human-readable label for generated comments and error messages.
29    pub label: &'static str,
30    /// EIP-155 chain id; must match the embedded manifest's `chainId`.
31    pub chain_id: u64,
32    /// Public JSON-RPC endpoint baked into the emitted config. Fine for light
33    /// use; operators point this at their own provider for production.
34    pub public_rpc: &'static str,
35    /// The embedded `deployments/<chainId>.json` manifest — the sole source of
36    /// this chain's contract addresses.
37    manifest_json: &'static str,
38}
39
40/// Every chain `config init` can target. Adding an entry here — plus its
41/// manifest under `contracts/deployments/` and a copy of that file in
42/// `crates/cli/deployments/` — is all it takes to offer a new network; today
43/// there is exactly one, so `--chain` may be omitted entirely.
44pub const KNOWN_CHAINS: &[KnownChain] = &[KnownChain {
45    name: "arbitrum-sepolia",
46    label: "Arbitrum Sepolia testnet",
47    chain_id: 421_614,
48    public_rpc: "https://sepolia-rollup.arbitrum.io/rpc",
49    manifest_json: include_str!("../deployments/421614.json"),
50}];
51
52/// Contract addresses read from a chain's manifest, mapped to `[blockchain]`
53/// config keys. Every field is an EIP-55 checksummed `0x` string exactly as the
54/// deploy script recorded it.
55#[derive(Debug)]
56pub struct ChainAddresses {
57    /// `blockchain.payment_pool_address`.
58    pub payment_pool: String,
59    /// `blockchain.capacity_bond_address`.
60    pub capacity_bond: String,
61    /// `blockchain.slash_judge_address`.
62    pub slash_judge: String,
63    /// `blockchain.content_blacklist_address`.
64    pub content_blacklist: String,
65    /// `blockchain.origin_assignment_address`.
66    pub origin_assignment: String,
67    /// `blockchain.publisher_registry_address`.
68    pub publisher_registry: String,
69    /// `blockchain.slash_appeal_address`.
70    pub slash_appeal: String,
71    /// `blockchain.usdc_address` — the payment token on this chain.
72    pub usdc: String,
73}
74
75/// The subset of `deployments/<chainId>.json` `config init` consumes. Unknown
76/// manifest fields (deploy block, governance params, …) are ignored.
77#[derive(Deserialize)]
78struct DeploymentManifest {
79    #[serde(rename = "chainId")]
80    chain_id: u64,
81    contracts: BTreeMap<String, String>,
82    #[serde(rename = "externalDeps")]
83    external_deps: ExternalDeps,
84}
85
86#[derive(Deserialize)]
87struct ExternalDeps {
88    usdc: String,
89}
90
91impl KnownChain {
92    /// Parse the embedded manifest and pull the addresses `config init` bakes
93    /// in. Fails (rather than panics) if the manifest is malformed, its
94    /// `chainId` disagrees with this entry, or an expected contract is absent.
95    pub fn addresses(&self) -> anyhow::Result<ChainAddresses> {
96        let manifest: DeploymentManifest =
97            serde_json::from_str(self.manifest_json).with_context(|| {
98                format!(
99                    "parsing the embedded deployment manifest for `{}`",
100                    self.name
101                )
102            })?;
103
104        anyhow::ensure!(
105            manifest.chain_id == self.chain_id,
106            "deployment manifest chainId {} does not match registry chain_id {} for `{}`",
107            manifest.chain_id,
108            self.chain_id,
109            self.name,
110        );
111
112        let contract = |key: &str| -> anyhow::Result<String> {
113            manifest.contracts.get(key).cloned().ok_or_else(|| {
114                anyhow::anyhow!(
115                    "deployment manifest for `{}` is missing contract `{key}`",
116                    self.name
117                )
118            })
119        };
120
121        Ok(ChainAddresses {
122            payment_pool: contract("PaymentPool")?,
123            capacity_bond: contract("CapacityBond")?,
124            slash_judge: contract("SlashJudge")?,
125            content_blacklist: contract("ContentBlacklist")?,
126            origin_assignment: contract("OriginAssignment")?,
127            publisher_registry: contract("PublisherRegistry")?,
128            slash_appeal: contract("SlashAppeal")?,
129            usdc: manifest.external_deps.usdc,
130        })
131    }
132}
133
134/// Comma-separated list of known selectors, for help and error text.
135fn known_names() -> String {
136    KNOWN_CHAINS
137        .iter()
138        .map(|c| c.name)
139        .collect::<Vec<_>>()
140        .join(", ")
141}
142
143/// Resolve a `--chain` selector to the chain whose values to bake in.
144///
145/// - `Some("none")` => `Ok(None)`: emit the blank generic template.
146/// - `Some(name)` => the matching chain, or an error naming the known chains.
147/// - `None` (flag omitted) => the sole known chain when exactly one exists;
148///   otherwise an error asking the operator to choose.
149pub fn resolve(selector: Option<&str>) -> anyhow::Result<Option<&'static KnownChain>> {
150    match selector {
151        Some("none") => Ok(None),
152        Some(name) => KNOWN_CHAINS
153            .iter()
154            .find(|c| c.name == name)
155            .map(Some)
156            .ok_or_else(|| {
157                anyhow::anyhow!(
158                    "unknown --chain `{name}`; known chains: {} (or `none` for a blank template)",
159                    known_names()
160                )
161            }),
162        None => match KNOWN_CHAINS {
163            [only] => Ok(Some(only)),
164            _ => Err(anyhow::anyhow!(
165                "several chains are available; pass --chain <name> (one of: {}) or --chain none",
166                known_names()
167            )),
168        },
169    }
170}
171
172#[cfg(test)]
173#[allow(clippy::unwrap_used, clippy::expect_used)]
174mod tests;