From e207617e0ad5a3933df356e7de35f272cbdccd47 Mon Sep 17 00:00:00 2001 From: Adam Spofford Date: Wed, 15 Jan 2025 07:04:05 -0800 Subject: [PATCH] Update display output --- src/lib/format/ckbtc.rs | 7 +- src/lib/format/nns_governance.rs | 330 ++++++++++++++----------------- src/lib/mod.rs | 193 +++--------------- 3 files changed, 179 insertions(+), 351 deletions(-) diff --git a/src/lib/format/ckbtc.rs b/src/lib/format/ckbtc.rs index 8c4aec3..13e3b05 100644 --- a/src/lib/format/ckbtc.rs +++ b/src/lib/format/ckbtc.rs @@ -37,16 +37,17 @@ pub fn display_update_balance(blob: &[u8]) -> AnyhowResult { current_confirmations, required_confirmations, pending_utxos, - .. + suspended_utxos, } => { let mut fmt = "ckBTC error: no new confirmed UTXOs to process".to_string(); if let Some(pending_utxos) = pending_utxos { write!( fmt, - " ({} unconfirmed, needing {} confirmations but having {})", + " ({} unconfirmed, needing {} confirmations but having {}, {} ignored)", pending_utxos.len(), required_confirmations, - current_confirmations.unwrap_or_default() + current_confirmations.unwrap_or_default(), + suspended_utxos.unwrap_or_default().len(), )?; } fmt diff --git a/src/lib/format/nns_governance.rs b/src/lib/format/nns_governance.rs index 619403e..03c57eb 100644 --- a/src/lib/format/nns_governance.rs +++ b/src/lib/format/nns_governance.rs @@ -1,4 +1,4 @@ -use std::fmt::{Display, Write}; +use std::fmt::Write; use anyhow::{anyhow, bail, Context}; use bigdecimal::BigDecimal; @@ -6,38 +6,33 @@ use candid::Decode; use chrono::Utc; use ic_base_types::CanisterId; use ic_nns_constants::canister_id_to_nns_canister_name; -use ic_nns_governance::pb::v1::{ - add_or_remove_node_provider::Change, - claim_or_refresh_neuron_from_account_response::Result as ClaimResult, - install_code::CanisterInstallMode, - manage_neuron::{ - configure::Operation, Command as ProposalCommand, NeuronIdOrSubaccount, SetVisibility, +use ic_nns_governance::{ + pb::v1::{ + add_or_remove_node_provider::Change, + claim_or_refresh_neuron_from_account_response::Result as ClaimResult, + install_code::CanisterInstallMode, + manage_neuron::{configure::Operation, Command as ProposalCommand, NeuronIdOrSubaccount}, + manage_neuron_response::Command, + neuron::DissolveState, + proposal::Action, + reward_node_provider::{RewardMode, RewardToAccount}, + stop_or_start_canister::CanisterAction, + update_canister_settings::CanisterSettings, + ClaimOrRefreshNeuronFromAccountResponse, GovernanceError, ListNeuronsResponse, + ListProposalInfoResponse, ManageNeuronResponse, NeuronInfo, ProposalInfo, Topic, }, - manage_neuron_response::Command, - neuron::DissolveState, - proposal::Action, - reward_node_provider::{RewardMode, RewardToAccount}, - stop_or_start_canister, - update_canister_settings::CanisterSettings, - ClaimOrRefreshNeuronFromAccountResponse, GovernanceError, InstallCode, ListNeuronsResponse, - ListProposalInfoResponse, ManageNeuronResponse, NeuronInfo, ProposalInfo, StopOrStartCanister, - Topic, UpdateCanisterSettings, Visibility, + proposals::call_canister::CallCanister, }; +use indicatif::HumanBytes; use itertools::Itertools; use sha2::{Digest, Sha256}; use crate::lib::{ - display_init_args, e8s_to_tokens, - format::{format_duration_seconds, format_timestamp_seconds}, + e8s_to_tokens, + format::{format_duration_seconds, format_t_cycles, format_timestamp_seconds}, get_default_role, get_idl_string, AnyhowResult, }; -fn hash_sha256(blob: &[u8]) -> [u8; 32] { - let mut hasher = Sha256::new(); - hasher.update(blob); - <[u8; 32]>::from(hasher.finalize()) -} - pub fn display_get_neuron_info(blob: &[u8]) -> AnyhowResult { let info = Decode!(blob, Result)?; let fmt = match info { @@ -46,18 +41,30 @@ pub fn display_get_neuron_info(blob: &[u8]) -> AnyhowResult { "\ Age: {age} Total stake: {icp} ICP -Voting power: {power} +Voting power: {power} {warning} +Voting power last refreshed: {last_refreshed} State: {state:?} Dissolve delay: {delay} Created {creation} ", age = format_duration_seconds(info.age_seconds), icp = e8s_to_tokens(info.stake_e8s.into()), - power = e8s_to_tokens(info.voting_power.into()), + power = e8s_to_tokens(info.deciding_voting_power().into()), + warning = if info.deciding_voting_power() != info.potential_voting_power() { + format!( + "(decayed, could be {})", + e8s_to_tokens(info.potential_voting_power().into()) + ) + } else { + "".into() + }, + last_refreshed = + format_timestamp_seconds(info.voting_power_refreshed_timestamp_seconds()), state = info.state(), delay = format_duration_seconds(info.dissolve_delay_seconds), creation = format_timestamp_seconds(info.created_timestamp_seconds) ); + let visibility = info.visibility(); if let Some(cf) = info.joined_community_fund_timestamp_seconds { writeln!( fmt, @@ -71,6 +78,9 @@ Created {creation} writeln!(fmt, "Description: \"{desc}\"")?; } } + if info.visibility.is_some() { + writeln!(fmt, "Neuron visibility: {visibility:?}")?; + } write!( fmt, "Accurate as of {}", @@ -89,6 +99,7 @@ pub fn display_list_neurons(blob: &[u8]) -> AnyhowResult { let mut fmt = String::new(); for neuron in neurons.full_neurons { let neuron_type = neuron.neuron_type(); + let neuron_visibility = neuron.visibility(); if let Some(id) = neuron.id { writeln!(fmt, "Neuron {}", id.id)?; } else { @@ -117,6 +128,26 @@ pub fn display_list_neurons(blob: &[u8]) -> AnyhowResult { if neuron.auto_stake_maturity() { writeln!(fmt, "Auto staking maturity: Yes")?; } + if neuron.deciding_voting_power() == neuron.potential_voting_power() { + writeln!( + fmt, + "Voting power: {}", + e8s_to_tokens(neuron.deciding_voting_power().into()) + )?; + } else { + writeln!( + fmt, + "Voting power: {} (decayed, could be {})", + e8s_to_tokens(neuron.deciding_voting_power().into()), + e8s_to_tokens(neuron.potential_voting_power().into()) + )?; + } + writeln!( + fmt, + "Voting power last refreshed: {}", + format_timestamp_seconds(neuron.voting_power_refreshed_timestamp_seconds()) + )?; + if let Some(timestamp) = neuron.spawn_at_timestamp_seconds { writeln!( fmt, @@ -211,6 +242,9 @@ pub fn display_list_neurons(blob: &[u8]) -> AnyhowResult { } fmt.push('\n'); } + if neuron.visibility.is_some() { + writeln!(fmt, "Neuron visibility: {neuron_visibility:?}")?; + } } Ok(fmt) } @@ -589,119 +623,63 @@ fn display_proposal_info(proposal_info: ProposalInfo) -> AnyhowResult { } } } - Action::InstallCode(install_code) => { - let InstallCode { - canister_id, - install_mode, - wasm_module, - arg, - skip_stopping_before_installing, - } = install_code; - - // Unwrap required fields. - let canister_id = canister_id - .context("canister ID was not specified within an InstallCode proposal")?; - let install_mode = install_mode - .context("install mode was not specified within an InstallCode proposal")?; - let wasm_module = wasm_module - .context("WASM was not specified within an InstallCode proposal")?; - - // Humanify fields (aka interpret them). - - let canister_principal_id = canister_id; - let canister_id = canister_id_to_nns_canister_name( - CanisterId::unchecked_from_principal(canister_id), - ); - - let install_mode = CanisterInstallMode::try_from(install_mode) - .map_err(|err| anyhow::Error::msg(format!("{}", err))) - .context( - "interpretting the install_mode field in an InstallCode proposal", - )?; - - let wasm_module = hex::encode(hash_sha256(&wasm_module)); - - let skip_stopping_before_installing = - if skip_stopping_before_installing.unwrap_or_default() { - " (Warning: canister will NOT be stopped before installing new WASM!)" - } else { - "" - }; - - let arg = match arg { - None => "no arg".to_string(), - Some(arg) => { - let args = display_init_args(&arg, canister_principal_id); - format!("init args = {}", args) - } + Action::InstallCode(a) => { + let install_mode = match a.install_mode() { + CanisterInstallMode::Unspecified => "Install (unspecified mode)", + CanisterInstallMode::Install => "Install", + CanisterInstallMode::Reinstall => "Reinstall", + CanisterInstallMode::Upgrade => "Upgrade", }; - + let (canister_id, _) = a + .canister_and_function() + .map_err(|e| anyhow!(display_governance_error(e)))?; + let canister_name = canister_id_to_nns_canister_name(canister_id); + writeln!(fmt, "{install_mode} canister {canister_name}")?; writeln!( fmt, - "{:?} {} to WASM with SHA256 = {} with {}{}.", - install_mode, - canister_id, - wasm_module, - arg, - skip_stopping_before_installing, + "WASM blob hash: {}", + hex::encode(Sha256::digest(a.wasm_module())) )?; + if a.skip_stopping_before_installing() { + writeln!( + fmt, + "Canister will NOT be stopped before installing new WASM" + )?; + } + if let Some(arg) = &a.arg { + if let Ok(payload) = get_idl_string( + arg, + canister_id.into(), + get_default_role(canister_id.into()).unwrap_or_default(), + ".", + "args", + ) { + writeln!(fmt, "Init args: {payload}",)?; + } else { + writeln!(fmt, "Init args: {}", hex::encode(arg))?; + } + } } - Action::StopOrStartCanister(stop_or_start_canister) => { - let StopOrStartCanister { - canister_id, - action, - } = stop_or_start_canister; - - // Unwrap required fields. - let canister_id = canister_id.context( - "canister ID was not specified within a StopOrStartCanister proposal", - )?; - let action = action - .context("no action (e.g. Upgrade) was specified within a StopOrStartCanister proposal")?; - - // Humanify fields (aka interpret them). - - let canister_id = canister_id_to_nns_canister_name( - CanisterId::unchecked_from_principal(canister_id), - ); - - let action = stop_or_start_canister::CanisterAction::try_from(action) - .with_context(|| { - format!( - "interpretting {} as an action within a StopOrStartCanister proposal.", - action, - ) - })?; + Action::StopOrStartCanister(a) => { + let action = match a.action() { + CanisterAction::Start => "Start", + CanisterAction::Stop => "Stop", + CanisterAction::Unspecified => "Start/stop (unspecified)", + }; + let (canister_id, _) = a + .canister_and_function() + .map_err(|e| anyhow!(display_governance_error(e)))?; + let canister_name = canister_id_to_nns_canister_name(canister_id); - writeln!(fmt, "{:?} {}", action, canister_id)?; + writeln!(fmt, "{action} canister {canister_name}")?; } - Action::UpdateCanisterSettings(update_canister_settings) => { - let UpdateCanisterSettings { - canister_id, - settings, - } = update_canister_settings; - - // Unwrap required fields. - let canister_id = canister_id.context( - "canister ID was not specified within an UpdateCanisterSettings proposal", - )?; - let settings = settings.context( - "settings not specified within an UpdateCanisterSettings proposal", - )?; - - // Humanify fields (aka interpret them). - - let canister_id = canister_id_to_nns_canister_name( - CanisterId::unchecked_from_principal(canister_id), - ); - - let settings = display_canister_settings(settings); - - writeln!( - fmt, - "Make the following changes to {}: {}.", - canister_id, settings, - )?; + Action::UpdateCanisterSettings(a) => { + let (canister_id, _) = a + .canister_and_function() + .map_err(|e| anyhow!(display_governance_error(e)))?; + let canister_name = canister_id_to_nns_canister_name(canister_id); + writeln!(fmt, "Update settings of canister {canister_name}")?; + fmt.push_str(&display_canister_settings(a.settings.unwrap())?); } Action::ManageNeuron(a) => { let neuron = a @@ -847,20 +825,11 @@ fn display_proposal_info(proposal_info: ProposalInfo) -> AnyhowResult { Operation::StopDissolving(_) => { writeln!(fmt, "Stop dissolving neuron {neuron}")? } - Operation::SetVisibility(set_visibility) => { - let SetVisibility { visibility } = set_visibility; - - let visibility = visibility - .context("visibility not specified within a SetVisibility (ManageNeuron) proposal")?; - - let visibility = Visibility::try_from(visibility) - .map_err(|err| anyhow!( - "unable to interpret the visibility field within a SetVisibility (ManageNeuron) proposal: {}", - err, - ))?; - - writeln!(fmt, "Set visibility of {neuron} to {visibility:?}")? - } + Operation::SetVisibility(set_visibility) => writeln!( + fmt, + "Set visibility of {neuron} to {visibility:?}", + visibility = set_visibility.visibility() + )?, } } } @@ -986,46 +955,41 @@ pub fn display_governance_error(err: GovernanceError) -> String { format!("NNS error: {}", err.error_message) } -fn display_canister_settings(settings: CanisterSettings) -> String { - let CanisterSettings { - controllers, - compute_allocation, - memory_allocation, - freezing_threshold, - log_visibility, - wasm_memory_limit, - } = settings; - - let mut chunks = vec![]; - - if let Some(controllers) = controllers { +fn display_canister_settings(settings: CanisterSettings) -> AnyhowResult { + let mut fmt = String::new(); + if let Some(controllers) = &settings.controllers { let controllers = controllers .controllers - .into_iter() - .map(CanisterId::unchecked_from_principal) - .map(canister_id_to_nns_canister_name) - .join(", "); - chunks.push(format!("set controllers to [{}]", controllers)); + .iter() + .map(|&c| { + CanisterId::try_from_principal_id(c) + .map_or_else(|c| c.to_string(), canister_id_to_nns_canister_name) + }) + .format(", "); + writeln!(fmt, "Controllers: {}", controllers)?; } - - fn display_set_field(name: &str, value: Option, chunks: &mut Vec) { - let Some(value) = value else { - return; - }; - - // TODO: Units and SI prefixes (e.g. GiB). - chunks.push(format!("set {} to {}", name, value)); + if let Some(freezing) = settings.freezing_threshold { + writeln!( + fmt, + "Freezing threshold: {} cycles", + format_t_cycles(freezing.into()) + )?; } - - display_set_field("compute allocation", compute_allocation, &mut chunks); - display_set_field("memory allocation", memory_allocation, &mut chunks); - display_set_field("freezing threshold", freezing_threshold, &mut chunks); - display_set_field("log visibility", log_visibility, &mut chunks); - display_set_field("WASM memory limit", wasm_memory_limit, &mut chunks); - - if chunks.is_empty() { - return "no changes".to_string(); + if let Some(memory) = settings.memory_allocation { + writeln!(fmt, "Memory allocation: {memory}%")?; + } + if let Some(compute) = settings.compute_allocation { + writeln!(fmt, "Compute allocation: {compute}%")?; + } + if settings.log_visibility.is_some() { + writeln!(fmt, "Log visibility: {:?}", settings.log_visibility())?; + } + if let Some(limit) = settings.wasm_memory_limit { + writeln!(fmt, "WASM memory limit: {}", HumanBytes(limit))?; + } + if fmt.is_empty() { + Ok("No changes to canister settings\n".into()) + } else { + Ok(fmt) } - - chunks.join("; ") } diff --git a/src/lib/mod.rs b/src/lib/mod.rs index 3993a24..19d712f 100644 --- a/src/lib/mod.rs +++ b/src/lib/mod.rs @@ -4,20 +4,23 @@ use anyhow::{anyhow, bail, ensure, Context}; use bigdecimal::BigDecimal; use bip32::DerivationPath; use bip39::{Mnemonic, Seed}; -use candid::{types::Function, Nat, Principal, TypeEnv}; -use candid_parser::{typing::check_prog, utils::CandidSource, IDLProg}; +use candid::{ + types::{Function, TypeInner}, + Nat, Principal, TypeEnv, +}; +use candid_parser::{typing::check_prog, IDLProg}; use crc32fast::Hasher; use data_encoding::BASE32_NOPAD; use ic_agent::{ identity::{AnonymousIdentity, BasicIdentity, Secp256k1Identity}, Agent, Identity, }; -use ic_base_types::{CanisterId, PrincipalId}; +use ic_base_types::PrincipalId; #[cfg(feature = "hsm")] use ic_identity_hsm::HardwareIdentity; use ic_nns_constants::{ - canister_id_to_nns_canister_name, GENESIS_TOKEN_CANISTER_ID, GOVERNANCE_CANISTER_ID, - LEDGER_CANISTER_ID, REGISTRY_CANISTER_ID, SNS_WASM_CANISTER_ID, + GENESIS_TOKEN_CANISTER_ID, GOVERNANCE_CANISTER_ID, LEDGER_CANISTER_ID, REGISTRY_CANISTER_ID, + SNS_WASM_CANISTER_ID, }; use icp_ledger::{AccountIdentifier, Subaccount}; use icrc_ledger_types::icrc1::account::Account; @@ -29,7 +32,6 @@ use std::{ env, fmt::{self, Display, Formatter}, path::Path, - rc::Rc, str::FromStr, time::{Duration, SystemTime}, }; @@ -241,108 +243,6 @@ pub fn get_idl_string( Ok(format!("{}", result?)) } -/// Returns a string representation of init_args. -/// -/// Similar to get_idl_string, but that assumes you have a blob that gets passed -/// to, or returned from a method. Whereas, this deals with data that you pass -/// during installation (or upgrade). -/// -/// Ideally, the string is human-readable. Otherwise, this falls back to -/// encoding init_args as in hex. -/// -/// This only works well in for some NNS canisters, specifically, -/// -/// - governance -/// - ledger -/// - gtc -/// - registry -/// - sns-wasm -fn display_init_args(init_args: &[u8], canister_id: PrincipalId) -> String { - let canister_name = - canister_id_to_nns_canister_name(CanisterId::unchecked_from_principal(canister_id)); - - let main = || { - let canister_role = get_default_role(canister_id.0).with_context(|| { - format!( - "unable to humanize install args, because the role of {} is unknown.", - canister_id, - ) - })?; - - // Glean supporting information about how to (decode and) interpret args - // from (embedded) .did file. - let interface = get_local_candid(canister_id.0, canister_role).with_context(|| { - format!( - "unable to humanize install args, because we do not have \ - the interface definition of the {} canister", - canister_name, - ) - })?; - - let (name_to_type, service) = CandidSource::Text(interface).load().with_context(|| { - format!( - "unable to humanize install args, because we could not \ - parse the interface definition of the {} canister.", - canister_name, - ) - })?; - - let service = service.with_context(|| { - format!( - "unable to humanize install args, because there seems to \ - be no service in the interface definition of the {} canister.", - canister_name, - ) - })?; - let service = unwrap_type(service).context("unable to humanize install args")?; - - let init_args_type = match service { - candid::types::TypeInner::Class(init_args_type, _methods) => init_args_type, - not_class => bail!("Somehow, service is not a service??? {:?}", not_class), - }; - - let init_args_type = init_args_type - .into_iter() - .map(|arg_type| { - let arg_type = unwrap_type(arg_type).context("unable to humanize install args")?; - match arg_type { - candid::types::TypeInner::Var(name) => name_to_type - .find_type(&name) - .with_context(|| format!("unable find the type definition of {}", name,)) - .cloned(), - arg_type => Ok(candid::types::Type::from(arg_type)), - } - }) - .collect::, _>>() - .with_context(|| { - format!( - "unable to humanize install args, because init args \ - type could not be determined from the {} interface \ - definition (i.e. .did file).", - canister_name, - ) - })?; - - // Finally, decode and interpret init args. - candid::IDLArgs::from_bytes_with_types(init_args, &name_to_type, &init_args_type) - .with_context(|| format!("unable to decode install args of {}", canister_name,)) - }; - - match main() { - Ok(ok) => format!("{}", ok), - Err(err) => { - eprintln!("Warning: Unable to humanify init args. Reason: {:#?}", err); - hex::encode(init_args) - } - } -} - -fn unwrap_type(type_: candid::types::Type) -> anyhow::Result { - let candid::types::Type(type_) = type_; - - Rc::into_inner(type_).context("unable to unwrap type") -} - /// Returns pretty-printed encoding of a candid value. pub fn display_response( blob: &[u8], @@ -424,8 +324,24 @@ pub fn display_response( pub fn get_candid_type(idl: &str, method_name: &str) -> Option<(TypeEnv, Function)> { let ast = candid_parser::pretty_parse::("/dev/null", idl).ok()?; let mut env = TypeEnv::new(); - let actor = check_prog(&mut env, &ast).ok()?; - let method = env.get_method(&actor?, method_name).ok()?.clone(); + let actor = check_prog(&mut env, &ast).ok()??; + let method = if method_name != "." { + env.get_method(&actor, method_name).ok()?.clone() + } else { + match *actor.0 { + TypeInner::Class(ref args, _) => Function { + args: args.clone(), + rets: vec![], + modes: vec![], + }, + TypeInner::Service(_) => Function { + args: vec![], + modes: vec![], + rets: vec![], + }, + _ => return None, + } + }; Some((env, method)) } @@ -710,11 +626,8 @@ pub fn key_encryption_params<'a>(salt: &'a [u8; 16], iv: &'a [u8; 16]) -> Parame #[cfg(test)] mod tests { - use super::{display_init_args, ParsedAccount, ParsedSubaccount}; - use candid::{Encode, Principal}; - use ic_base_types::PrincipalId; - use ic_nns_constants::GOVERNANCE_CANISTER_ID; - use ic_nns_governance::pb::v1::Governance as GovernanceProto; + use super::{ParsedAccount, ParsedSubaccount}; + use candid::Principal; use pretty_assertions::assert_eq; use std::str::FromStr; @@ -830,54 +743,4 @@ mod tests { *b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\x02" ); } - - #[test] - fn test_display_init_args() { - // Step 1: Construct input. - let governance_proto = GovernanceProto { - wait_for_quiet_threshold_seconds: 123_456_789, - ..Default::default() - }; - let encoded = Encode!(&governance_proto).unwrap(); - - // Step 2: Call code under test. - let decoded = display_init_args(&encoded, PrincipalId::from(GOVERNANCE_CANISTER_ID)); - - // Step 3: Inspect results. - assert_eq!( - decoded, - // If you dig deep enough, you will see a line in this string that - // looks like the field value we choose at the beginning of this - // test. Other fields are "false-y" (according to their type). - // Common "false-y" values are null, vec {}, and 0. (This assert - // needs to be updated every time a field is added to - // GovernanceProto.) - "( - record { - default_followees = vec {}; - making_sns_proposal = null; - most_recent_monthly_node_provider_rewards = null; - maturity_modulation_last_updated_at_timestamp_seconds = null; - wait_for_quiet_threshold_seconds = 123_456_789 : nat64; - metrics = null; - neuron_management_voting_period_seconds = null; - node_providers = vec {}; - cached_daily_maturity_modulation_basis_points = null; - economics = null; - restore_aging_summary = null; - spawning_neurons = null; - latest_reward_event = null; - to_claim_transfers = vec {}; - short_voting_period_seconds = 0 : nat64; - topic_followee_index = vec {}; - migrations = null; - proposals = vec {}; - xdr_conversion_rate = null; - in_flight_commands = vec {}; - neurons = vec {}; - genesis_timestamp_seconds = 0 : nat64; - }, -)", - ); - } }