From 76af8bdeafbd2e9f1cc7f9d8cbd8f3075d5e42cc Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Fri, 22 May 2026 10:55:11 +0000 Subject: [PATCH 1/2] cli: build CliContext in main and centralize error rendering --- CHANGELOG.md | 2 + client/doublezero/src/main.rs | 75 +++++++++++++++++++++++++---------- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef9e2159d..201dc2fea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,8 @@ All notable changes to this project will be documented in this file. - Add `solana_l1_rpc_url` to `doublezero-config::NetworkConfig`. Per RFC-20 §Environments: `mainnet-beta` resolves to `https://api.mainnet-beta.solana.com`, `testnet` to `https://api.testnet.solana.com`, `devnet` to `https://api.testnet.solana.com` (intentional asymmetry, see RFC), and `local` to `http://localhost:8899`. A new `DZ_SOLANA_RPC_URL` environment variable overrides the resolved value, mirroring the existing `DZ_LEDGER_RPC_URL` / `DZ_LEDGER_WS_RPC_URL` overrides. - Add `--solana-url ` global flag to `doublezero` per RFC-20 §Global flags. Distinct from `--url`, which continues to override the DZ ledger transport; `--solana-url` targets the Solana L1 transport. The flag is parsed and exposed on the binary's `App` struct; per-verb consumption lands when verbs migrate to construct typed Solana L1 clients from `CliContext`. - Add `--log-verbose` (repeatable) global flag and initialize the `tracing` subscriber at startup. Default level is `warn`; one `--log-verbose` raises to `debug`, two raise to `trace`. Diagnostic logs go to stderr so `--json` output on stdout remains parseable. Honors the `RUST_LOG` environment variable when set, overriding the CLI-flag verbosity for per-module filtering. Replaces the previous `println!("using keypair: ...")` stdout line with a `tracing::info!` event; the keypair confirmation now appears only at `--log-verbose` or higher and no longer pollutes parseable stdout. (Named `--log-verbose` rather than the RFC-20 §Global-flags suggested `--verbose` / `-v` because the existing `doublezero connect` / `disconnect` subcommands already own a `--verbose` flag with `bool` type; the global flag deviation will be revisited when the daemon-control module crate is carved out.) + - Build a `CliContext` once at binary startup from `--env` and the per-field global overrides (`--url`, `--ws`, `--solana-url`, `--keypair`, `--sock-file`), per RFC-20 (§CliContext). The context resolves to `Environment::default()` (`devnet`) when `--env` is absent. `DZClient` continues to consume the legacy `Option` tuple via a thin bridge that forwards `None` when neither `--env` nor a per-field override is set, preserving today's fall-back to `~/.config/solana/cli/config.yml`. Verbs that migrate to the RFC-20 module contract will consume `CliContext` directly and the bridge shrinks. + - Centralize top-level error rendering through `doublezero_cli_core::error::render_eyre`. Replaces three ad-hoc `eprintln!("Error: {e}")` sites in `client/doublezero/src/main.rs` (env-parse failure, env-config resolution failure, top-level command failure) with a single helper that prints `Error: ` followed by the full chain of causes on stderr. - Drop the activator-only pollers from `doublezero` (user and multicastgroup activation waits). The `--wait` flag on `user create`, `user create-subscribe`, `user subscribe`, `multicastgroup create`, and `multicastgroup update` now fetches the post-create state once instead of polling; creates are atomic to `Activated` post-RFC-11, so the wait loop was watching a transition that no longer happens ([#3614](https://github.com/malbeclabs/doublezero/issues/3614)) - `doublezero geolocation` `probe ...` and `user ...` mirrors `doublezero-geolocation` versions; new `--geo-program-id` global flag, `config get/set` include Geolocation Program ID; new `-init-geolocation-config` for init of geolocation program - cli: `doublezero geolocation` `probe ...` and `user ...` mirrors `doublezero-geolocation` versions; new `--geo-program-id` global flag, `config get/set` include Geolocation Program ID. diff --git a/client/doublezero/src/main.rs b/client/doublezero/src/main.rs index d6b8b7dae..ca4732752 100644 --- a/client/doublezero/src/main.rs +++ b/client/doublezero/src/main.rs @@ -114,30 +114,61 @@ async fn main() -> eyre::Result<()> { tracing::info!(keypair = %keypair.display(), "using keypair"); } - let (url, ws, program_id) = if let Some(env) = app.env { - let config = match env.parse::() { - Ok(env) => match env.config() { - Ok(config) => config, - Err(e) => { - eprintln!("Error: {e}"); - std::process::exit(1); - } - }, - Err(e) => { - eprintln!("Error: {e}"); - std::process::exit(1); - } - }; - ( - Some(config.ledger_public_rpc_url), - Some(config.ledger_public_ws_rpc_url), - Some(config.serviceability_program_id.to_string()), - ) + // Resolve global configuration into a CliContext per RFC-20 (§CliContext). + // The binary populates it once at startup; future verbs read from it. + let env_explicit = app.env.is_some(); + let env = match app.env.as_deref() { + Some(s) => s.parse::().unwrap_or_else(|e| { + doublezero_cli_core::error::render_eyre(&e); + std::process::exit(1); + }), + None => Environment::default(), + }; + let mut ctx_builder = doublezero_cli_core::CliContextBuilder::new().with_env(env); + if let Some(u) = app.url.clone() { + ctx_builder = ctx_builder.with_ledger_rpc_url(u); + } + if let Some(w) = app.ws.clone() { + ctx_builder = ctx_builder.with_ledger_ws_rpc_url(w); + } + if let Some(s) = app.solana_url.clone() { + ctx_builder = ctx_builder.with_solana_l1_rpc_url(s); + } + if let Some(k) = app.keypair.clone() { + ctx_builder = ctx_builder.with_keypair_path(k); + } + if let Some(s) = app.sock_file.clone() { + ctx_builder = ctx_builder.with_daemon_socket_path(s); + } + let ctx = ctx_builder.build().unwrap_or_else(|e| { + doublezero_cli_core::error::render_eyre(&e); + std::process::exit(1); + }); + + // Bridge to the legacy `DZClient::new(Option, ...)` signature. + // When neither `--env` nor a per-field override is set, forward `None` + // so `DZClient` keeps falling back to the user's + // `~/.config/solana/cli/config.yml`. As verbs migrate to construct typed + // clients from `CliContext` directly, this bridge shrinks. + let url = if env_explicit || app.url.is_some() { + Some(ctx.ledger_rpc_url.clone()) } else { - (app.url, app.ws, app.program_id) + None }; + let ws = if env_explicit || app.ws.is_some() { + Some(ctx.ledger_ws_rpc_url.clone()) + } else { + None + }; + let program_id = app.program_id.clone().or_else(|| { + if env_explicit { + Some(ctx.serviceability_program_id.to_string()) + } else { + None + } + }); - let dzclient = DZClient::new(url.clone(), ws, program_id, app.keypair.clone())?; + let dzclient = DZClient::new(url.clone(), ws, program_id, ctx.keypair_path.clone())?; let client = CliCommandImpl::new(&dzclient); let stdout = std::io::stdout(); @@ -444,7 +475,7 @@ async fn main() -> eyre::Result<()> { match res { Ok(_) => {} Err(e) => { - eprintln!("Error: {e}"); + doublezero_cli_core::error::render_eyre(&e); std::process::exit(1); } }; From d121d836269e9195f4f5f612cb093fd857ae3055 Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Sun, 24 May 2026 14:36:17 +0000 Subject: [PATCH 2/2] cli: collapse CliContext bridge and fix config-path comment Replaces the three explicit if/else blocks that translated CliContext into DZClient::new arguments with a flat `any_url_explicit.then(...)` form. The behavior is identical: when no env or per-field override is present, all three fields stay None so DZClient falls through to the on-disk config; otherwise the resolved CliContext values flow through. Drops the assumption that env_explicit alone resolves URL/program-id fields: any explicit override (--url, --ws, --program-id) now also opts into using the resolved context, which keeps the bridge in step with the builder's WS-from-RPC derivation introduced in jo/1. Also corrects the stale comment that referenced `~/.config/solana/cli/config.yml`; DZClient actually reads `~/.config/doublezero/cli/config.yml`. --- client/doublezero/src/main.rs | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/client/doublezero/src/main.rs b/client/doublezero/src/main.rs index ca4732752..31416122b 100644 --- a/client/doublezero/src/main.rs +++ b/client/doublezero/src/main.rs @@ -148,25 +148,17 @@ async fn main() -> eyre::Result<()> { // Bridge to the legacy `DZClient::new(Option, ...)` signature. // When neither `--env` nor a per-field override is set, forward `None` // so `DZClient` keeps falling back to the user's - // `~/.config/solana/cli/config.yml`. As verbs migrate to construct typed - // clients from `CliContext` directly, this bridge shrinks. - let url = if env_explicit || app.url.is_some() { - Some(ctx.ledger_rpc_url.clone()) - } else { - None - }; - let ws = if env_explicit || app.ws.is_some() { - Some(ctx.ledger_ws_rpc_url.clone()) - } else { - None - }; - let program_id = app.program_id.clone().or_else(|| { - if env_explicit { - Some(ctx.serviceability_program_id.to_string()) - } else { - None - } - }); + // `~/.config/doublezero/cli/config.yml`. As verbs migrate to construct + // typed clients from `CliContext` directly, this bridge shrinks. + // + // `CliContextBuilder::build` derives WS from RPC when only `--url` is + // overridden, so `ctx.ledger_ws_rpc_url` stays consistent with + // `ctx.ledger_rpc_url` on every path that reaches here. + let any_url_explicit = env_explicit || app.url.is_some() || app.ws.is_some(); + let url = any_url_explicit.then(|| ctx.ledger_rpc_url.clone()); + let ws = any_url_explicit.then(|| ctx.ledger_ws_rpc_url.clone()); + let program_id = (env_explicit || app.program_id.is_some()) + .then(|| ctx.serviceability_program_id.to_string()); let dzclient = DZClient::new(url.clone(), ws, program_id, ctx.keypair_path.clone())?; let client = CliCommandImpl::new(&dzclient);