canic_host/icp/
balance.rs1use super::{error::IcpCommandError, model::IcpCli, run::run_json};
8
9use serde::Deserialize;
10use thiserror::Error as ThisError;
11
12#[derive(Debug, ThisError)]
18pub enum IcpBalanceError {
19 #[error("ICP CLI returned an invalid {unit} balance: {value}")]
20 InvalidAmount { unit: &'static str, value: String },
21
22 #[error(transparent)]
23 Icp(#[from] IcpCommandError),
24}
25
26#[derive(Deserialize)]
27#[serde(deny_unknown_fields)]
28struct BalanceOutput {
29 balance: String,
30}
31
32impl IcpCli {
33 pub fn identity_cycles_balance(&self) -> Result<u128, IcpBalanceError> {
35 let mut command = self.request_command();
36 command.args(["cycles", "balance", "--json"]);
37 self.add_target_args(&mut command);
38 self.record_remote_call();
39 let output = run_json::<BalanceOutput>(&mut command, self)?;
40 parse_cycles(&output.balance)
41 }
42}
43
44fn parse_cycles(value: &str) -> Result<u128, IcpBalanceError> {
45 let amount = strip_unit(value, "cycles")?;
46 amount
47 .replace('_', "")
48 .parse()
49 .map_err(|_| invalid_amount("cycles", value))
50}
51
52fn strip_unit<'a>(value: &'a str, unit: &'static str) -> Result<&'a str, IcpBalanceError> {
53 value
54 .trim()
55 .strip_suffix(unit)
56 .map(str::trim)
57 .filter(|amount| !amount.is_empty())
58 .ok_or_else(|| invalid_amount(unit, value))
59}
60
61fn invalid_amount(unit: &'static str, value: &str) -> IcpBalanceError {
62 IcpBalanceError::InvalidAmount {
63 unit,
64 value: value.to_string(),
65 }
66}
67
68#[cfg(test)]
72mod tests {
73 use super::*;
74
75 const ICP_CLI_1_5_CYCLES_BALANCE_JSON: &str = r#"{"balance":"3_519_900_000_000 cycles"}"#;
76
77 #[test]
78 fn decodes_icp_cli_one_five_balance_json_goldens() {
79 let cycles: BalanceOutput = serde_json::from_str(ICP_CLI_1_5_CYCLES_BALANCE_JSON)
80 .expect("ICP CLI 1.5 cycles balance JSON");
81 assert_eq!(parse_cycles(&cycles.balance).unwrap(), 3_519_900_000_000);
82 }
83
84 #[test]
85 fn parses_exact_cycles_ledger_amounts() {
86 assert_eq!(
87 parse_cycles("3_519_900_000_000 cycles").unwrap(),
88 3_519_900_000_000
89 );
90 assert_eq!(parse_cycles("0 cycles").unwrap(), 0);
91 assert!(matches!(
92 parse_cycles("1.5 cycles"),
93 Err(IcpBalanceError::InvalidAmount { unit: "cycles", .. })
94 ));
95 }
96}