use crate::{
cli::{
clap::{
parse_matches_or_usage, parse_required_subcommand_or_usage, passthrough_subcommand,
render_help, required_string,
},
common::{
COLLECTION_MODE_LIVE, CurrentUnixSecsError, SOURCE_ENDPOINT_ARG, collection_help,
current_unix_secs, json_arg, output_format, source_endpoint_arg, write_text_or_json,
},
globals::internal_network_arg,
help::collect_args_or_print_help_or_version,
},
version_text,
};
use clap::Command as ClapCommand;
use ic_query::{
subnet_catalog::MAINNET_NETWORK,
system::cmc::{
CmcHostError, CmcSourceRequest, DEFAULT_CMC_SOURCE_ENDPOINT, build_cmc_cycles_report,
build_cmc_xdr_report, cmc_cycles_report_text, cmc_xdr_report_text,
},
};
use serde::Serialize;
use std::{ffi::OsString, io};
use thiserror::Error as ThisError;
const NETWORK_ARG: &str = "network";
const SYSTEM_HELP_AFTER: &str = "\
Examples:
icq system xdr
icq system cycles";
const XDR_HELP_AFTER: &str = "\
Examples:
icq system xdr
icq system xdr --json";
const CYCLES_HELP_AFTER: &str = "\
Examples:
icq system cycles
icq system cycles --json";
#[derive(Debug, ThisError)]
pub enum SystemCommandError {
#[error("{0}")]
Usage(String),
#[error(transparent)]
CmcHost(#[from] CmcHostError),
#[error(transparent)]
Clock(#[from] CurrentUnixSecsError),
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Json(#[from] serde_json::Error),
}
pub fn run<I>(args: I) -> Result<(), SystemCommandError>
where
I: IntoIterator<Item = OsString>,
{
let Some(args) = command_args(args, system_usage) else {
return Ok(());
};
let (command, args) = parse_required_subcommand_or_usage(system_command(), args)
.map_err(SystemCommandError::Usage)?;
match command.as_str() {
"xdr" => run_report(
args,
report_command(
"xdr",
"Show the certified CMC ICP/XDR conversion rate",
XDR_HELP_AFTER,
),
xdr_usage,
build_cmc_xdr_report,
cmc_xdr_report_text,
),
"cycles" => run_report(
args,
report_command(
"cycles",
"Show cycles conversions derived from the certified CMC rate",
CYCLES_HELP_AFTER,
),
cycles_usage,
build_cmc_cycles_report,
cmc_cycles_report_text,
),
_ => unreachable!("system dispatch only defines known commands"),
}
}
fn run_report<I, Report>(
args: I,
command: ClapCommand,
usage: fn() -> String,
build: fn(&CmcSourceRequest) -> Result<Report, CmcHostError>,
render_text: fn(&Report) -> String,
) -> Result<(), SystemCommandError>
where
I: IntoIterator<Item = OsString>,
Report: Serialize,
{
let Some(args) = command_args(args, usage) else {
return Ok(());
};
let matches = parse_matches_or_usage(command, args).map_err(SystemCommandError::Usage)?;
let request = CmcSourceRequest::from_unix_secs(
required_string(&matches, NETWORK_ARG),
required_string(&matches, SOURCE_ENDPOINT_ARG),
current_unix_secs()?,
"ic-query",
);
let format = output_format(&matches);
let report = build(&request)?;
write_text_or_json(format, &report, render_text)
}
fn command_args<I>(args: I, usage: impl FnOnce() -> String) -> Option<Vec<OsString>>
where
I: IntoIterator<Item = OsString>,
{
collect_args_or_print_help_or_version(args, usage, version_text())
}
fn system_command() -> ClapCommand {
ClapCommand::new("system")
.bin_name("icq system")
.about("Inspect native IC system-canister metadata")
.disable_help_flag(true)
.subcommand(passthrough_subcommand(
ClapCommand::new("xdr").about("Show the certified CMC ICP/XDR conversion rate"),
))
.subcommand(passthrough_subcommand(ClapCommand::new("cycles").about(
"Show cycles conversions derived from the certified CMC rate",
)))
.after_help(SYSTEM_HELP_AFTER)
}
fn report_command(name: &'static str, about: &'static str, examples: &'static str) -> ClapCommand {
ClapCommand::new(name)
.bin_name(format!("icq system {name}"))
.about(about)
.disable_help_flag(true)
.arg(json_arg())
.arg(
source_endpoint_arg(DEFAULT_CMC_SOURCE_ENDPOINT)
.help("IC API endpoint used for the native CMC query"),
)
.arg(internal_network_arg().default_value(MAINNET_NETWORK))
.after_help(collection_help(COLLECTION_MODE_LIVE, examples))
}
fn system_usage() -> String {
render_help(system_command())
}
fn xdr_usage() -> String {
render_help(report_command(
"xdr",
"Show the certified CMC ICP/XDR conversion rate",
XDR_HELP_AFTER,
))
}
fn cycles_usage() -> String {
render_help(report_command(
"cycles",
"Show cycles conversions derived from the certified CMC rate",
CYCLES_HELP_AFTER,
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn usage_describes_bounded_native_reports() {
let text = system_usage();
assert!(text.contains("Usage: icq system [COMMAND]"));
assert!(text.contains("xdr"));
assert!(text.contains("cycles"));
for text in [xdr_usage(), cycles_usage()] {
assert!(text.contains("--source-endpoint <url>"));
assert!(text.contains("--json"));
assert!(text.contains(COLLECTION_MODE_LIVE));
}
}
#[test]
fn help_and_version_do_not_make_live_calls() {
for args in [
&["help"][..],
&["xdr", "help"],
&["cycles", "help"],
&["--version"],
] {
assert!(run(args.iter().map(OsString::from)).is_ok());
}
}
#[test]
fn report_options_default_to_mainnet_and_native_endpoint() {
let matches = parse_matches_or_usage(
report_command("xdr", "test", XDR_HELP_AFTER),
Vec::<OsString>::new(),
)
.expect("parse default CMC options");
assert_eq!(required_string(&matches, NETWORK_ARG), MAINNET_NETWORK);
assert_eq!(
required_string(&matches, SOURCE_ENDPOINT_ARG),
DEFAULT_CMC_SOURCE_ENDPOINT
);
assert_eq!(
output_format(&matches),
crate::cli::common::OutputFormat::Text
);
}
}