Skip to content

Commit

Permalink
feat: Implement rich output for quill sns (#234)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamspofford-dfinity authored Jul 9, 2024
1 parent 2e0a7b0 commit b10252c
Show file tree
Hide file tree
Showing 8 changed files with 438 additions and 3 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased

- Overhauled PEM auth. PEM files are now password-protected by default, and must be used instead of seed files. Passwords can be provided interactively or with `--password-file`. Keys can be generated unencrypted with `quill generate --storage-mode plaintext`, and encrypted keys can be converted to plaintext with `quill decrypt-pem`.
- Overhauled output format. All commands besides `quill sns` should have human-readable output instead of candid IDL. Candid IDL format can be forced with `--raw`.
- Overhauled output format. All commands should have human-readable output instead of candid IDL. Candid IDL format can be forced with `--raw`.

- Added support for setting the install mode for UpgradeSnsControlledCanister proposals.

Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 55 additions & 0 deletions src/lib/format/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
use bigdecimal::BigDecimal;
use candid::{Nat, Principal};
use chrono::{DateTime, TimeZone, Utc};
use icrc_ledger_types::icrc1::account::Account;
use itertools::Itertools;
use num_bigint::ToBigInt;

use super::ParsedAccount;

pub mod ckbtc;
pub mod gtc;
pub mod icp_ledger;
pub mod icrc1;
pub mod nns_governance;
pub mod registry;
pub mod sns_governance;
pub mod sns_root;
pub mod sns_swap;
pub mod sns_wasm;

pub fn format_datetime(datetime: DateTime<Utc>) -> String {
format!("{} UTC", datetime.format("%b %d %Y %X"))
Expand Down Expand Up @@ -52,8 +62,53 @@ pub fn format_duration_seconds(mut seconds: u64) -> String {
.to_string()
}

pub fn icrc1_account(owner: Principal, subaccount: Option<[u8; 32]>) -> ParsedAccount {
ParsedAccount(Account { owner, subaccount })
}

pub fn format_t_cycles(cycles: Nat) -> String {
let t_cycles = BigDecimal::new(cycles.0.into(), 12);
let e10 = t_cycles.digits();
if e10 < 14 {
format!("{:.1}T", t_cycles)
} else {
format!("{:.0}T", t_cycles)
}
}

pub fn format_n_cycles(cycles: Nat) -> String {
let e10 = BigDecimal::from(cycles.0.to_bigint().unwrap()).digits();
if e10 < 4 {
return cycles.to_string();
}
let unit = (e10 - 1) / 3;
let letter = b"KMBTQ"[unit as usize - 1] as char;
let scale = unit * 3;
let printable = BigDecimal::new(cycles.0.into(), scale as i64);
if e10 - scale == 1 {
format!("{printable:.1}{letter}")
} else {
format!("{printable:.0}{letter}")
}
}

#[test]
fn magic_durations() {
assert_eq!(format_duration_seconds(15_778_800), "6 months");
assert_eq!(format_duration_seconds(252_460_800), "8 years");
}

#[test]
fn cycle_units() {
assert_eq!(format_t_cycles(100_000_000_000_u64.into()), "0.1T");
assert_eq!(format_t_cycles(1_100_000_000_000_u64.into()), "1.1T");
assert_eq!(format_t_cycles(10_100_000_000_000_u64.into()), "10T");
assert_eq!(format_t_cycles(1_000_000_000_000_000_u64.into()), "1000T");
assert_eq!(format_n_cycles(1_000_000_000_000_000_u64.into()), "1.0Q");
assert_eq!(format_n_cycles(100_000_000_000_u64.into()), "100B");
assert_eq!(format_n_cycles(10_100_000_000_u64.into()), "10B");
assert_eq!(format_n_cycles(1_100_000_000_u64.into()), "1.1B");
assert_eq!(format_n_cycles(1_000_u64.into()), "1.0K");
assert_eq!(format_n_cycles(1_100_u64.into()), "1.1K");
assert_eq!(format_n_cycles(100_u64.into()), "100");
}
64 changes: 64 additions & 0 deletions src/lib/format/sns_governance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use anyhow::Context;
use candid::Decode;
use ic_sns_governance::pb::v1::{
manage_neuron_response::Command, GovernanceError, ManageNeuronResponse,
};

use crate::lib::{e8s_to_tokens, AnyhowResult};

pub fn display_manage_neuron(blob: &[u8]) -> AnyhowResult<String> {
let response = Decode!(blob, ManageNeuronResponse)?;
let command = response.command.context("command was null")?;
let fmt = match command {
Command::Error(error) => display_governance_error(error),
Command::Configure(_) => "Neuron successfully configured".to_string(),
Command::RegisterVote(_) => "Successfully voted".to_string(),
Command::Follow(_) => "Successfully set following relationship".to_string(),
Command::AddNeuronPermission(_) => "Successfully added neuron permissions".to_string(),
Command::RemoveNeuronPermission(_) => "Successfully removed neuron permissions".to_string(),
Command::Disburse(c) => format!(
"Successfully disbursed ICP at block index {}",
c.transfer_block_height
),
Command::ClaimOrRefresh(c) => {
if let Some(id) = c.refreshed_neuron_id {
format!("Successfully updated the stake of neuron {id}")
} else {
"Successfully updated the stake of unknown neuron".to_string()
}
}
Command::DisburseMaturity(c) => format!(
"Successfully disbursed {} maturity",
c.amount_deducted_e8s()
),
Command::MakeProposal(c) => {
if let Some(id) = c.proposal_id {
format!("Successfully created new proposal with ID {id}", id = id.id)
} else {
"Successfully created new proposal with unknown ID".to_string()
}
}
Command::MergeMaturity(c) => format!(
"Successfully merged {merged} maturity (total stake now {total})",
merged = e8s_to_tokens(c.merged_maturity_e8s.into()),
total = e8s_to_tokens(c.new_stake_e8s.into())
),
Command::Split(c) => {
if let Some(id) = c.created_neuron_id {
format!("Neuron successfully split off to new neuron {id}")
} else {
"Neuron successfully split off to unknown new neuron".to_string()
}
}
Command::StakeMaturity(c) => format!(
"Successfully staked maturity ({staked} staked maturity total, {remaining} unstaked)",
staked = e8s_to_tokens(c.staked_maturity_e8s.into()),
remaining = e8s_to_tokens(c.maturity_e8s.into())
),
};
Ok(fmt)
}

pub fn display_governance_error(err: GovernanceError) -> String {
format!("SNS governance error: {}", err.error_message)
}
114 changes: 114 additions & 0 deletions src/lib/format/sns_root.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use candid::{Decode, Principal};
use ic_sns_root::{CanisterSummary, GetSnsCanistersSummaryResponse};
use indicatif::HumanBytes;
use itertools::Itertools;
use std::fmt::Write;

use crate::lib::{format::format_n_cycles, AnyhowResult};

use super::format_t_cycles;

pub fn display_canisters_summary(blob: &[u8]) -> AnyhowResult<String> {
let response = Decode!(blob, GetSnsCanistersSummaryResponse)?;
let root = response.root_canister_summary().canister_id().0;
let governance = response.governance_canister_summary().canister_id().0;
let mut fmt = format!(
"\
System canisters:
Root:
{root}
Governance:
{governance}
Ledger:
{ledger}
Index:
{index}
Swap:
{swap}",
root = display_canister_summary(response.root_canister_summary(), root, governance)?,
governance =
display_canister_summary(response.governance_canister_summary(), root, governance)?,
ledger = display_canister_summary(response.ledger_canister_summary(), root, governance)?,
index = display_canister_summary(response.index_canister_summary(), root, governance)?,
swap = display_canister_summary(response.swap_canister_summary(), root, governance)?,
);
if !response.dapps.is_empty() {
fmt.push_str("\n\nDapp canisters:");
for dapp in &response.dapps {
write!(
fmt,
"\n\n{}",
display_canister_summary(dapp, root, governance)?
)?;
}
}
if !response.archives.is_empty() {
fmt.push_str("\n\nArchive canisters:");
for archive in &response.archives {
write!(
fmt,
"\n\n{}",
display_canister_summary(archive, root, governance)?
)?;
}
}
Ok(fmt)
}

fn display_canister_summary(
summary: &CanisterSummary,
root: Principal,
governance: Principal,
) -> AnyhowResult<String> {
const NNS_ROOT: Principal =
Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x01, 0x01]);
let status = summary.status();
let canister_id = summary.canister_id();
let mut fmt = format!(
"\
Canister ID: {canister_id}, status: {status:?}
Cycles: {cycles}, memory usage: {memory}",
status = status.status(),
cycles = format_t_cycles(status.cycles.clone()),
memory = HumanBytes(status.memory_size().get())
);
if let Some(hash) = &status.module_hash {
write!(fmt, "\nInstalled module: hash {}", hex::encode(hash))?;
}
let freezing = &status.settings.freezing_threshold;
let idle = &status.idle_cycles_burned_per_day;
let freezing_time = freezing.clone() / idle.clone();
write!(
fmt,
"
Freezing threshold: {freezing} cycles ({freezing_time} days at current idle usage of {idle}/day)
Memory allocation: {memory}%, compute allocation: {compute}%
Controllers: {controllers}",
freezing = format_t_cycles(freezing.clone()),
idle = format_n_cycles(idle.clone()),
memory = status.settings.memory_allocation,
compute = status.settings.compute_allocation,
controllers = status
.settings
.controllers
.iter()
.format_with(", ", |c, f| if c.0 == NNS_ROOT {
f(&"NNS root")
} else if c.0 == governance {
f(&"SNS governance")
} else if c.0 == root {
f(&"SNS root")
} else if *c == canister_id {
f(&"self")
} else {
f(c)
})
)?;

Ok(fmt)
}
Loading

0 comments on commit b10252c

Please sign in to comment.