Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ All notable changes to this project will be documented in this file.
- SDK (Rust)
- Add `DZClient::from_context` and `GeoClient::from_context`, which build clients directly from a resolved RFC-20 `CliContext` instead of re-reading `~/.config/doublezero/cli/config.yml` and re-applying moniker conversion. The context already carries the fully resolved ledger RPC/WS URLs and program IDs, so these constructors consume them verbatim, making the context the single source of truth and removing the double-resolution the binary previously incurred. Keypair precedence is preserved exactly (CLI flag > `DOUBLEZERO_KEYPAIR` > stdin > context keypair path > default): the raw `--keypair` flag is passed as the highest-precedence source and the context keypair path is used only as the low-precedence fallback, so the env var still wins. The new constructors and their `doublezero-cli-core` dependency are gated behind a `cli-context` cargo feature so non-CLI SDK consumers (controlplane, telemetry, e2e) keep a dependency-light default build. `DZClient::new` / `GeoClient::new` are unchanged for callers that do not build a `CliContext` (e.g. `controlplane/doublezero-admin`).
- CLI
- geolocation `user get`: Show probe code, rather than probe pubkey in target list.
- geolocation `probe get`: Show exchange code, rather than exchange pubkeys.
- Honor the build-configured default environment (`Testnet` by default, `MainnetBeta` under the `default-mainnet-beta` feature) when neither `--env` nor a persisted `config.yml` selects one. The RFC-20 context-build previously fell back to `Environment::default()`, which is always `Devnet` regardless of the build, so a testnet build with no config silently targeted Devnet's ledger URLs and program IDs. The binary now resolves the fallback through the new `doublezero_sdk::default_environment()`, matching the legacy `DZClient::new` defaults (`default_program_id`, `ClientConfig::default`) which already key off the compiled-in environment ([#3810](https://github.com/malbeclabs/doublezero/pull/3810))
- Construct the serviceability and geolocation SDK clients in the `doublezero` binary via `DZClient::from_context` / `GeoClient::from_context`, replacing the legacy `DZClient::new(Option<String>, ...)` bridge. The binary no longer round-trips the already-resolved `CliContext` values back through the SDK's config-file re-resolution. No user-facing command, flag, or output change.
- Restore environment-moniker support for the `--program-id` and `--geo-program-id` global flags. The context-build resolved both flags with a raw `parse::<Pubkey>()`, so a moniker (e.g. `--geo-program-id testnet`) failed to parse, was silently dropped, and the binary fell back to the environment default. Both flags now accept monikers in their full (`mainnet-beta`, `testnet`, `devnet`, `local`) and short (`m`, `t`, `d`, `l`) forms, resolving to the matching program ID; a literal pubkey still passes through. A value that is neither a known moniker nor a valid pubkey is now a hard error instead of being silently ignored. `convert_program_moniker` is broadened to cover all four environments (previously only `devnet`/`testnet`), matching `convert_geo_program_moniker`.
Expand Down
126 changes: 124 additions & 2 deletions crates/doublezero-geolocation-cli/src/probe/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ struct GeoProbeGetDisplay {
#[serde(serialize_with = "serializer::serialize_pubkey_as_string")]
pub owner: Pubkey,
#[serde(serialize_with = "serializer::serialize_pubkey_as_string")]
#[tabled(skip)]
pub exchange: Pubkey,
#[tabled(rename = "exchange")]
pub exchange_code: String,
pub public_ip: Ipv4Addr,
pub port: u16,
#[serde(serialize_with = "serialize_pubkey_vec_as_string_array")]
Expand All @@ -64,6 +67,15 @@ impl GetGeoProbeCliCommand {
pubkey_or_code: self.probe,
})?;

let exchanges = client.list_exchanges().unwrap_or_else(|e| {
tracing::warn!(error = %e, "failed to list exchanges; showing exchange pubkey");
Default::default()
});
let exchange_code = exchanges
.get(&probe.exchange_pk)
.map(|ex| ex.code.clone())
.unwrap_or_else(|| probe.exchange_pk.to_string());

let parent_devices_display = format!(
"[{}]",
probe
Expand All @@ -78,6 +90,7 @@ impl GetGeoProbeCliCommand {
code: probe.code,
owner: probe.owner,
exchange: probe.exchange_pk,
exchange_code,
public_ip: probe.public_ip,
port: probe.location_offset_port,
parent_devices: probe.parent_devices,
Expand Down Expand Up @@ -112,9 +125,34 @@ mod tests {
use crate::client::MockGeoCliCommand;
use doublezero_cli_core::testing::{block_on, cli_context_default_for_tests};
use doublezero_geolocation::state::{accounttype::AccountType, geo_probe::GeoProbe};
use doublezero_sdk::{AccountType as SvcAccountType, Exchange, ExchangeStatus};
use mockall::predicate;
use solana_sdk::pubkey::Pubkey;
use std::net::Ipv4Addr;
use std::{collections::HashMap, net::Ipv4Addr};

fn make_exchanges(exchange_pk: Pubkey, code: &str) -> HashMap<Pubkey, Exchange> {
let mut exchanges = HashMap::new();
exchanges.insert(
exchange_pk,
Exchange {
account_type: SvcAccountType::Exchange,
owner: exchange_pk,
index: 0,
bump_seed: 0,
reference_count: 0,
device1_pk: Pubkey::default(),
device2_pk: Pubkey::default(),
lat: 0.0,
lng: 0.0,
bgp_community: 0,
unused: 0,
status: ExchangeStatus::Activated,
code: code.to_string(),
name: code.to_string(),
},
);
exchanges
}

fn setup_client() -> (MockGeoCliCommand, Pubkey, Pubkey, Pubkey, Pubkey) {
let client = MockGeoCliCommand::new();
Expand Down Expand Up @@ -161,6 +199,11 @@ mod tests {
.expect_get_geo_probe()
.returning(move |_| Err(eyre::eyre!("not found")));

let exchanges = make_exchanges(exchange_pk, "ams");
client
.expect_list_exchanges()
.returning(move || Ok(exchanges.clone()));

let ctx = cli_context_default_for_tests();
let mut output = Vec::new();
let res = block_on(
Expand Down Expand Up @@ -192,7 +235,9 @@ mod tests {
assert!(has_row("account", &probe_pk.to_string()));
assert!(has_row("code", "ams-probe-01"));
assert!(has_row("owner", &owner_pk.to_string()));
assert!(has_row("exchange", &exchange_pk.to_string()));
// exchange column shows the code, not the pubkey
assert!(has_row("exchange", "ams"));
assert!(!output_str.contains(&exchange_pk.to_string()));
assert!(has_row("public_ip", "10.0.0.1"));
assert!(has_row("port", "8923"));
assert!(has_row("signing_pubkey", &metrics_pk.to_string()));
Expand All @@ -212,6 +257,11 @@ mod tests {
}))
.returning(move |_| Ok((probe_pk, probe.clone())));

let exchanges = make_exchanges(exchange_pk, "ams");
client
.expect_list_exchanges()
.returning(move || Ok(exchanges.clone()));

let ctx = cli_context_default_for_tests();
let mut output = Vec::new();
let res = block_on(
Expand All @@ -228,7 +278,9 @@ mod tests {
assert_eq!(json["account"].as_str().unwrap(), probe_pk.to_string());
assert_eq!(json["code"].as_str().unwrap(), "ams-probe-01");
assert_eq!(json["owner"].as_str().unwrap(), owner_pk.to_string());
// exchange retains the canonical pubkey; the code is a separate field
assert_eq!(json["exchange"].as_str().unwrap(), exchange_pk.to_string());
assert_eq!(json["exchange_code"].as_str().unwrap(), "ams");
assert_eq!(json["public_ip"].as_str().unwrap(), "10.0.0.1");
assert_eq!(json["port"].as_u64().unwrap(), 8923);
let parents = json["parent_devices"].as_array().unwrap();
Expand All @@ -254,6 +306,11 @@ mod tests {
}))
.returning(move |_| Ok((probe_pk, probe.clone())));

let exchanges = make_exchanges(exchange_pk, "ams");
client
.expect_list_exchanges()
.returning(move || Ok(exchanges.clone()));

let ctx = cli_context_default_for_tests();
let mut output = Vec::new();
let res = block_on(
Expand All @@ -271,6 +328,71 @@ mod tests {
let json: serde_json::Value = serde_json::from_str(trimmed).unwrap();
assert_eq!(json["account"].as_str().unwrap(), probe_pk.to_string());
assert_eq!(json["code"].as_str().unwrap(), "ams-probe-01");
assert_eq!(json["exchange"].as_str().unwrap(), exchange_pk.to_string());
assert_eq!(json["exchange_code"].as_str().unwrap(), "ams");
assert_eq!(json["parent_devices"].as_array().unwrap().len(), 1);
}

#[test]
fn test_cli_geo_probe_get_unknown_exchange_falls_back_to_pubkey() {
let (mut client, probe_pk, owner_pk, exchange_pk, metrics_pk) = setup_client();
let probe = make_probe(owner_pk, exchange_pk, metrics_pk, vec![]);

client
.expect_get_geo_probe()
.with(predicate::eq(GetGeoProbeCommand {
pubkey_or_code: probe_pk.to_string(),
}))
.returning(move |_| Ok((probe_pk, probe.clone())));

client
.expect_list_exchanges()
.returning(|| Ok(HashMap::new()));

let ctx = cli_context_default_for_tests();
let mut output = Vec::new();
let res = block_on(
GetGeoProbeCliCommand {
probe: probe_pk.to_string(),
json: false,
json_compact: false,
}
.execute(&ctx, &client, &mut output),
);
assert!(res.is_ok());
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains(&exchange_pk.to_string()));
}

#[test]
fn test_cli_geo_probe_get_exchange_list_error_falls_back_to_pubkey() {
let (mut client, probe_pk, owner_pk, exchange_pk, metrics_pk) = setup_client();
let probe = make_probe(owner_pk, exchange_pk, metrics_pk, vec![]);

client
.expect_get_geo_probe()
.with(predicate::eq(GetGeoProbeCommand {
pubkey_or_code: probe_pk.to_string(),
}))
.returning(move |_| Ok((probe_pk, probe.clone())));

// A failure to list exchanges must not abort the command; it falls back to the pubkey.
client
.expect_list_exchanges()
.returning(|| Err(eyre::eyre!("rpc error")));

let ctx = cli_context_default_for_tests();
let mut output = Vec::new();
let res = block_on(
GetGeoProbeCliCommand {
probe: probe_pk.to_string(),
json: false,
json_compact: false,
}
.execute(&ctx, &client, &mut output),
);
assert!(res.is_ok());
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains(&exchange_pk.to_string()));
}
}
Loading
Loading