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;