use super::IcCommandError;
use crate::cli::{
clap::{required_string, typed_option, value_arg},
common::{
COLLECTION_MODE_LIVE, OutputFormat, collection_help, current_unix_secs, json_arg,
output_format, source_endpoint_arg, write_text_or_json,
},
};
use clap::{ArgMatches, Command as ClapCommand, builder::RangedU64ValueParser};
use ic_query::ic::{
DEFAULT_IC_BOUNDARY_NODE_DATA_CENTERS_SOURCE_ENDPOINT, DEFAULT_IC_DAILY_STATS_WINDOW_SECS,
DEFAULT_IC_DASHBOARD_SOURCE_ENDPOINT, IcBoundaryNodeDataCentersRequest, IcDailyStatsQuery,
IcDailyStatsRequest, MIN_IC_DAILY_STATS_TIMESTAMP, build_ic_boundary_node_data_centers_report,
build_ic_daily_stats_report, ic_boundary_node_data_centers_report_text,
ic_daily_stats_report_text,
};
const BOUNDARY_NODE_DATA_CENTERS_HELP_AFTER: &str = "\
Examples:
icq ic network boundary-node-data-centers
icq ic network boundary-node-data-centers --json
This command makes exactly one official Dashboard v4 request for the complete
boundary-node data-center resource. It does not issue per-location follow-up
calls or create a cache. Rows preserve the API's raw owner, region, coordinate,
and node-count strings, including locations that currently report zero nodes.
The Dashboard response is off-chain and non-certified.";
const DAILY_STATS_HELP_AFTER: &str = "\
Examples:
icq ic network daily-stats
icq ic network daily-stats --start 1784937600 --end 1785542400
icq ic network daily-stats --json
This command makes exactly one official Dashboard v3 request for an explicitly
bounded daily network-activity window. The default is the preceding seven days,
and every request is capped at one year and 366 returned rows. It never follows
up, paginates, or creates a cache. Rate values remain the raw strings returned
by this off-chain, non-certified API.";
pub(super) fn run_matches(matches: &ArgMatches) -> Result<(), IcCommandError> {
match matches.subcommand() {
Some(("boundary-node-data-centers", matches)) => run_boundary_node_data_centers(matches),
Some(("daily-stats", matches)) => run_daily_stats(matches),
_ => unreachable!("clap requires a known ic network subcommand"),
}
}
fn run_boundary_node_data_centers(matches: &ArgMatches) -> Result<(), IcCommandError> {
let options = NetworkReportOptions::from_matches(matches);
let request =
IcBoundaryNodeDataCentersRequest::new(options.source_endpoint, current_unix_secs()?);
let report = build_ic_boundary_node_data_centers_report(&request)?;
write_text_or_json(
options.format,
&report,
ic_boundary_node_data_centers_report_text,
)
}
fn run_daily_stats(matches: &ArgMatches) -> Result<(), IcCommandError> {
let options = DailyStatsOptions::from_matches(matches);
let now_unix_secs = current_unix_secs()?;
let end_unix_secs = options.end_unix_secs.unwrap_or(now_unix_secs);
let start_unix_secs = options
.start_unix_secs
.unwrap_or_else(|| end_unix_secs.saturating_sub(DEFAULT_IC_DAILY_STATS_WINDOW_SECS));
let request = IcDailyStatsRequest::new(
options.source_endpoint,
now_unix_secs,
IcDailyStatsQuery::new(start_unix_secs, end_unix_secs),
);
let report = build_ic_daily_stats_report(&request)?;
write_text_or_json(options.format, &report, ic_daily_stats_report_text)
}
pub(super) fn command() -> ClapCommand {
ClapCommand::new("network")
.bin_name("icq ic network")
.about("Inspect bounded official Dashboard network analytics")
.subcommand_required(true)
.subcommand(boundary_node_data_centers_command())
.subcommand(daily_stats_command())
}
fn boundary_node_data_centers_command() -> ClapCommand {
ClapCommand::new("boundary-node-data-centers")
.bin_name("icq ic network boundary-node-data-centers")
.about("List official Dashboard boundary-node data-center aggregates")
.arg(json_arg())
.arg(
source_endpoint_arg(DEFAULT_IC_BOUNDARY_NODE_DATA_CENTERS_SOURCE_ENDPOINT)
.help("Official IC Dashboard API v4 base endpoint"),
)
.after_help(collection_help(
COLLECTION_MODE_LIVE,
BOUNDARY_NODE_DATA_CENTERS_HELP_AFTER,
))
}
fn daily_stats_command() -> ClapCommand {
ClapCommand::new("daily-stats")
.bin_name("icq ic network daily-stats")
.about("Query bounded official Dashboard daily network activity")
.arg(
value_arg("start")
.long("start")
.value_name("unix-seconds")
.value_parser(
RangedU64ValueParser::<u64>::new().range(MIN_IC_DAILY_STATS_TIMESTAMP..),
)
.help("Inclusive start; defaults to seven days before end"),
)
.arg(
value_arg("end")
.long("end")
.value_name("unix-seconds")
.value_parser(
RangedU64ValueParser::<u64>::new().range(MIN_IC_DAILY_STATS_TIMESTAMP..),
)
.help("Inclusive end; defaults to the current time"),
)
.arg(json_arg())
.arg(
source_endpoint_arg(DEFAULT_IC_DASHBOARD_SOURCE_ENDPOINT)
.help("Official IC Dashboard API v3 base endpoint"),
)
.after_help(collection_help(
COLLECTION_MODE_LIVE,
DAILY_STATS_HELP_AFTER,
))
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct NetworkReportOptions {
format: OutputFormat,
source_endpoint: String,
}
impl NetworkReportOptions {
fn from_matches(matches: &ArgMatches) -> Self {
Self {
format: output_format(matches),
source_endpoint: required_string(matches, "source-endpoint"),
}
}
#[cfg(test)]
fn parse_boundary_node_data_centers<I>(args: I) -> Result<Self, IcCommandError>
where
I: IntoIterator<Item = std::ffi::OsString>,
{
let matches =
crate::cli::clap::parse_matches_or_usage(boundary_node_data_centers_command(), args)
.map_err(IcCommandError::Usage)?;
Ok(Self::from_matches(&matches))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct DailyStatsOptions {
start_unix_secs: Option<u64>,
end_unix_secs: Option<u64>,
format: OutputFormat,
source_endpoint: String,
}
impl DailyStatsOptions {
fn from_matches(matches: &ArgMatches) -> Self {
Self {
start_unix_secs: typed_option(matches, "start"),
end_unix_secs: typed_option(matches, "end"),
format: output_format(matches),
source_endpoint: required_string(matches, "source-endpoint"),
}
}
#[cfg(test)]
fn parse<I>(args: I) -> Result<Self, IcCommandError>
where
I: IntoIterator<Item = std::ffi::OsString>,
{
let matches = crate::cli::clap::parse_matches_or_usage(daily_stats_command(), args)
.map_err(IcCommandError::Usage)?;
Ok(Self::from_matches(&matches))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::clap::render_help;
use std::ffi::OsString;
#[test]
fn usage_discloses_live_dashboard_authority_and_bounds() {
let network = render_help(command());
let boundary_nodes = render_help(boundary_node_data_centers_command());
let daily_stats = render_help(daily_stats_command());
assert!(network.contains("Usage: icq ic network <COMMAND>"));
assert!(network.contains("boundary-node-data-centers"));
assert!(network.contains("daily-stats"));
assert!(
boundary_nodes.contains("Usage: icq ic network boundary-node-data-centers [OPTIONS]")
);
assert!(boundary_nodes.contains("exactly one official Dashboard v4 request"));
assert!(boundary_nodes.contains("locations that currently report zero nodes"));
assert!(daily_stats.contains("Usage: icq ic network daily-stats [OPTIONS]"));
assert!(daily_stats.contains("exactly one official Dashboard v3 request"));
assert!(daily_stats.contains("capped at one year and 366 returned rows"));
}
#[test]
fn boundary_node_data_center_options_preserve_format_and_endpoint() {
let options = NetworkReportOptions::parse_boundary_node_data_centers([
OsString::from("--json"),
OsString::from("--source-endpoint"),
OsString::from("https://example.com/api/v4"),
])
.expect("boundary-node options");
assert_eq!(options.format, OutputFormat::Json);
assert_eq!(options.source_endpoint, "https://example.com/api/v4");
}
#[test]
fn daily_stats_options_preserve_bounds_format_and_endpoint() {
let options = DailyStatsOptions::parse([
OsString::from("--start"),
OsString::from("1784937600"),
OsString::from("--end"),
OsString::from("1785542400"),
OsString::from("--json"),
OsString::from("--source-endpoint"),
OsString::from("https://example.com/api/v3"),
])
.expect("daily-statistics options");
assert_eq!(options.start_unix_secs, Some(1_784_937_600));
assert_eq!(options.end_unix_secs, Some(1_785_542_400));
assert_eq!(options.format, OutputFormat::Json);
assert_eq!(options.source_endpoint, "https://example.com/api/v3");
}
#[test]
fn daily_stats_options_use_live_bounded_defaults() {
let options = DailyStatsOptions::parse([]).expect("default daily-statistics options");
assert_eq!(options.start_unix_secs, None);
assert_eq!(options.end_unix_secs, None);
assert_eq!(options.format, OutputFormat::Text);
assert_eq!(
options.source_endpoint,
DEFAULT_IC_DASHBOARD_SOURCE_ENDPOINT
);
}
}