1use std::io;
26use std::path::Path;
27
28use alloy::primitives::{Address, B256, Bytes};
29use alloy::providers::Provider;
30use anyhow::Context;
31use decdn_common::cli;
32use decdn_incentive::capacity_bond::CapacityBond;
33use decdn_incentive::node_register;
34
35use crate::commands::chain_ctx;
36
37pub async fn run(
39 args: &cli::UpdateMultiaddrsArgs,
40 global_config: Option<&Path>,
41) -> anyhow::Result<()> {
42 let config_path = args.chain.common.config.as_deref().or(global_config);
43 let file = chain_ctx::load_optional_config(config_path)?;
44 let resolved = chain_ctx::resolve(&args.chain, &file)?;
45 let cb_addr = resolved.capacity_bond_address;
46
47 reject_blank_multiaddrs(&args.multiaddrs)?;
52
53 let packed = node_register::pack_multiaddrs(&args.multiaddrs)?;
57
58 let signer = chain_ctx::load_operator_signer(&args.chain.common, &resolved.keystore).await?;
59 let operator = signer.address();
60 let provider = decdn_client::provider::build_provider(&resolved.rpc_url, &signer)?;
61 let bond = CapacityBond::new(cb_addr, &provider);
62
63 let plan = build_plan(&bond, operator, cb_addr, args.multiaddrs.len(), &packed).await?;
64
65 let json = args.chain.common.json;
66 if args.chain.common.dry_run {
67 let mut out = io::stdout().lock();
68 write_plan(&mut out, &plan, json, None, true).context("failed to write dry-run output")?;
69 return Ok(());
70 }
71
72 ensure_submittable(&plan)?;
76
77 let mut tx = None;
81 let result = chain_ctx::send(
82 bond.updateMultiaddrs(Bytes::from(packed)),
83 "updateMultiaddrs",
84 Some(
85 "the node must be active and within the maxMultiaddrSize ceiling, and the \
86 multiaddr-update cooldown must have elapsed",
87 ),
88 &mut tx,
89 )
90 .await;
91
92 let mut out = io::stdout().lock();
93 write_plan(&mut out, &plan, json, tx.as_ref(), false).context("failed to write result")?;
94 drop(out);
95
96 result.map(|_| ())
97}
98
99#[derive(Clone, Debug, PartialEq, Eq)]
103pub(crate) struct Plan {
104 pub(crate) capacity_bond: Address,
106 pub(crate) operator: Address,
108 pub(crate) node_id: B256,
111 pub(crate) active: bool,
114 pub(crate) multiaddr_count: usize,
116 pub(crate) packed_size: u64,
118 pub(crate) max_multiaddr_size: u64,
121 pub(crate) last_update: u64,
123 pub(crate) cooldown_secs: u64,
125 pub(crate) ready_at: u64,
128 pub(crate) head_timestamp: u64,
131}
132
133async fn build_plan<P: Provider + Clone>(
136 bond: &CapacityBond::CapacityBondInstance<P>,
137 operator: Address,
138 capacity_bond: Address,
139 multiaddr_count: usize,
140 packed: &[u8],
141) -> anyhow::Result<Plan> {
142 let ctx = |what: &str| format!("failed to read {what} from CapacityBond at {capacity_bond}");
143
144 let info = bond
145 .getNodeByAddress(operator)
146 .call()
147 .await
148 .with_context(|| ctx("getNodeByAddress"))?;
149 let max_multiaddr_size = bond
150 .maxMultiaddrSize()
151 .call()
152 .await
153 .with_context(|| ctx("maxMultiaddrSize"))?;
154 let cooldown = bond
155 .multiaddrUpdateCooldown()
156 .call()
157 .await
158 .with_context(|| ctx("multiaddrUpdateCooldown"))?;
159 let head_timestamp = chain_ctx::head_timestamp(bond.provider()).await?;
160
161 let max_multiaddr_size: u64 = max_multiaddr_size.saturating_to();
165 let cooldown_secs: u64 = cooldown.saturating_to();
166 let ready_at = info.lastMultiaddrUpdate.saturating_add(cooldown_secs);
167 let packed_size = u64::try_from(packed.len()).unwrap_or(u64::MAX);
170
171 Ok(Plan {
172 capacity_bond,
173 operator,
174 node_id: info.nodeId,
175 active: info.active,
176 multiaddr_count,
177 packed_size,
178 max_multiaddr_size,
179 last_update: info.lastMultiaddrUpdate,
180 cooldown_secs,
181 ready_at,
182 head_timestamp,
183 })
184}
185
186pub(crate) fn reject_blank_multiaddrs(multiaddrs: &[String]) -> anyhow::Result<()> {
192 for (i, ma) in multiaddrs.iter().enumerate() {
193 anyhow::ensure!(
194 !ma.trim().is_empty(),
195 "--multiaddr #{} is empty or whitespace — publish a real QUIC address like \
196 `/ip4/203.0.113.10/udp/4433/quic-v1`, or omit it (at least one non-blank \
197 address is required).",
198 i + 1,
199 );
200 }
201 Ok(())
202}
203
204pub(crate) fn ensure_submittable(plan: &Plan) -> anyhow::Result<()> {
209 anyhow::ensure!(
210 plan.active,
211 "this operator is not in the active node set — `updateMultiaddrs` would revert \
212 NodeNotActive. A registered node re-enters with `decdn node register`. An ejected \
213 operator cannot register (it would revert too) and instead exits the bond with \
214 `decdn node unbond --all`."
215 );
216 anyhow::ensure!(
217 plan.packed_size <= plan.max_multiaddr_size,
218 "packed multiaddrs are {} bytes, over the maxMultiaddrSize ceiling of {} bytes — \
219 `updateMultiaddrs` would revert MultiaddrsTooLarge. Publish fewer or shorter \
220 addresses.",
221 plan.packed_size,
222 plan.max_multiaddr_size,
223 );
224 if plan.head_timestamp < plan.ready_at {
225 let wait = plan.ready_at.saturating_sub(plan.head_timestamp);
226 anyhow::bail!(
227 "the multiaddr-update cooldown is active — `updateMultiaddrs` would revert \
228 MultiaddrCooldownActive. Last update was at {} (unix); the next is accepted at \
229 {} (unix), ~{} s from the current head at {}.",
230 plan.last_update,
231 plan.ready_at,
232 wait,
233 plan.head_timestamp,
234 );
235 }
236 Ok(())
237}
238
239pub(crate) fn write_plan(
246 w: &mut impl io::Write,
247 p: &Plan,
248 json: bool,
249 tx: Option<&B256>,
250 dry_run: bool,
251) -> io::Result<()> {
252 let tx_hex = tx.map(|v| format!("{v:#x}"));
253 if json {
254 let value = serde_json::json!({
255 "submitted": tx.is_some(),
256 "dry_run": dry_run,
257 "capacity_bond": format!("{:#x}", p.capacity_bond),
258 "operator": format!("{:#x}", p.operator),
259 "node_id": format!("{:#x}", p.node_id),
260 "active": p.active,
261 "multiaddrs": p.multiaddr_count,
262 "packed_bytes": p.packed_size,
263 "max_multiaddr_size": p.max_multiaddr_size,
264 "last_update": p.last_update,
265 "cooldown_secs": p.cooldown_secs,
266 "ready_at": p.ready_at,
267 "head_timestamp": p.head_timestamp,
268 "update_tx": tx_hex,
269 });
270 return writeln!(w, "{value}");
271 }
272 writeln!(w, "capacity_bond={:#x}", p.capacity_bond)?;
273 writeln!(w, "operator={:#x}", p.operator)?;
274 writeln!(w, "node_id={:#x}", p.node_id)?;
275 writeln!(w, "active={}", p.active)?;
276 writeln!(w, "multiaddrs={}", p.multiaddr_count)?;
277 writeln!(w, "packed_bytes={}", p.packed_size)?;
278 writeln!(w, "max_multiaddr_size={}", p.max_multiaddr_size)?;
279 writeln!(w, "last_update={}", p.last_update)?;
280 writeln!(w, "cooldown_secs={}", p.cooldown_secs)?;
281 writeln!(w, "ready_at={}", p.ready_at)?;
282 writeln!(w, "head_timestamp={}", p.head_timestamp)?;
283 match tx_hex {
284 Some(h) => writeln!(w, "update_tx={h}")?,
285 None => writeln!(w, "update_tx=skipped")?,
286 }
287 writeln!(w, "submitted={} dry_run={dry_run}", tx.is_some())
288}
289
290#[cfg(test)]
291#[allow(clippy::unwrap_used, clippy::expect_used)]
292mod tests;