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)]
27struct BalanceOutput {
28 balance: String,
29}
30
31impl IcpCli {
32 pub fn identity_cycles_balance(&self) -> Result<u128, IcpBalanceError> {
34 let mut command = self.request_command();
35 command.args(["cycles", "balance", "--json"]);
36 self.add_target_args(&mut command);
37 self.record_remote_call();
38 let output = run_json::<BalanceOutput>(&mut command, self)?;
39 parse_cycles(&output.balance)
40 }
41}
42
43fn parse_cycles(value: &str) -> Result<u128, IcpBalanceError> {
44 let amount = strip_unit(value, "cycles")?;
45 amount
46 .replace('_', "")
47 .parse()
48 .map_err(|_| invalid_amount("cycles", value))
49}
50
51fn strip_unit<'a>(value: &'a str, unit: &'static str) -> Result<&'a str, IcpBalanceError> {
52 value
53 .trim()
54 .strip_suffix(unit)
55 .map(str::trim)
56 .filter(|amount| !amount.is_empty())
57 .ok_or_else(|| invalid_amount(unit, value))
58}
59
60fn invalid_amount(unit: &'static str, value: &str) -> IcpBalanceError {
61 IcpBalanceError::InvalidAmount {
62 unit,
63 value: value.to_string(),
64 }
65}
66
67#[cfg(test)]
71mod tests {
72 use super::*;
73
74 const ICP_CLI_1_5_CYCLES_BALANCE_JSON: &str = r#"{"balance":"3_519_900_000_000 cycles"}"#;
75
76 #[test]
77 fn decodes_icp_cli_one_five_balance_json_goldens() {
78 let cycles: BalanceOutput = serde_json::from_str(ICP_CLI_1_5_CYCLES_BALANCE_JSON)
79 .expect("ICP CLI 1.5 cycles balance JSON");
80 assert_eq!(parse_cycles(&cycles.balance).unwrap(), 3_519_900_000_000);
81 }
82
83 #[test]
84 fn parses_exact_cycles_ledger_amounts() {
85 assert_eq!(
86 parse_cycles("3_519_900_000_000 cycles").unwrap(),
87 3_519_900_000_000
88 );
89 assert_eq!(parse_cycles("0 cycles").unwrap(), 0);
90 assert!(matches!(
91 parse_cycles("1.5 cycles"),
92 Err(IcpBalanceError::InvalidAmount { unit: "cycles", .. })
93 ));
94 }
95
96 #[test]
97 fn balance_json_accepts_additive_fields_and_requires_the_balance() {
98 let output: BalanceOutput =
99 serde_json::from_str(r#"{"balance":"42 cycles","account":{"owner":"2vxsx-fae"}}"#)
100 .expect("additional upstream fields are informational");
101 assert_eq!(parse_cycles(&output.balance).unwrap(), 42);
102
103 for invalid in [r#"{"account":{}}"#, r#"{"balance":42}"#] {
104 assert!(serde_json::from_str::<BalanceOutput>(invalid).is_err());
105 }
106 }
107}