diff --git a/Cargo.lock b/Cargo.lock index 7537a9db..75e83918 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -749,9 +749,9 @@ dependencies = [ [[package]] name = "candid" -version = "0.10.8" +version = "0.10.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5902d37352dffd8bd9177a2daa6444ce3cd0279c91763fb0171c053aa04335" +checksum = "51e129c4051c57daf943586e01ef72faae48b04a8f692d5f646febf17a264c38" dependencies = [ "anyhow", "binread", diff --git a/src/commands/make_proposal.rs b/src/commands/make_proposal.rs new file mode 100644 index 00000000..cc0babbe --- /dev/null +++ b/src/commands/make_proposal.rs @@ -0,0 +1,88 @@ +use std::{fs, path::PathBuf}; + +use crate::{ + lib::{ + governance_canister_id, + signing::{sign_ingress_with_request_status_query, IngressWithRequestId}, + AuthInfo, ROLE_NNS_GOVERNANCE, + }, + AnyhowResult, +}; +use anyhow::Error; +use candid::{CandidType, Decode, Encode, TypeEnv}; +use candid_parser::parse_idl_args; +use clap::Parser; +use ic_nns_common::pb::v1::NeuronId; +use ic_nns_governance::pb::v1::{ + manage_neuron::{self, NeuronIdOrSubaccount}, + ManageNeuron, Proposal, +}; + +use super::neuron_manage::parse_neuron_id; + +/// Creates an NNS proposal for others to vote on. +#[derive(Parser)] +pub struct MakeProposalOpts { + /// The id of the neuron making the proposal. + #[arg(value_parser = parse_neuron_id)] + proposer_neuron_id: u64, + + /// The proposal to be submitted. The proposal must be formatted as a string + /// wrapped candid record. + /// + /// For example: + /// '(record { + /// title=opt "Known Neuron Proposal"; + /// url="http://example.com"; + /// summary="A proposal to become a named neuron"; + /// action=opt variant { + /// RegisterKnownNeuron = record { + /// id=opt record { id=773; }; + /// known_neuron_data=opt record { name="Me!" }; + /// } + /// }; + /// })' + #[arg(long)] + proposal: Option, + + /// Path to a file containing the proposal. The proposal must be the binary encoding of + /// the proposal candid record. + #[arg( + long, + conflicts_with = "proposal", + required_unless_present = "proposal" + )] + proposal_path: Option, +} + +pub fn exec(auth: &AuthInfo, opts: MakeProposalOpts) -> AnyhowResult> { + let neuron_id = opts.proposer_neuron_id; + + let proposal = if let Some(proposal) = opts.proposal { + parse_nns_proposal_from_candid_string(proposal)? + } else { + Decode!(&fs::read(opts.proposal_path.unwrap())?, Proposal)? + }; + + let args = Encode!(&ManageNeuron { + id: None, + neuron_id_or_subaccount: Some(NeuronIdOrSubaccount::NeuronId(NeuronId { id: neuron_id })), + command: Some(manage_neuron::Command::MakeProposal(Box::new(proposal))) + })?; + + let msg = sign_ingress_with_request_status_query( + auth, + governance_canister_id(), + ROLE_NNS_GOVERNANCE, + "manage_neuron", + args, + )?; + + Ok(vec![msg]) +} + +fn parse_nns_proposal_from_candid_string(proposal_candid: String) -> AnyhowResult { + let args = parse_idl_args(&proposal_candid)?; + let args: Vec = args.to_bytes_with_types(&TypeEnv::default(), &[Proposal::ty()])?; + Decode!(args.as_slice(), Proposal).map_err(Error::msg) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 3d25cdcb..35243664 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -15,6 +15,7 @@ mod get_neuron_info; mod get_proposal_info; mod list_neurons; mod list_proposals; +mod make_proposal; mod neuron_manage; mod neuron_stake; mod public; @@ -37,6 +38,7 @@ pub enum Command { ListNeurons(list_neurons::ListNeuronsOpts), ListProposals(list_proposals::ListProposalsOpts), GetProposalInfo(get_proposal_info::GetProposalInfoOpts), + MakeProposal(make_proposal::MakeProposalOpts), GetNeuronInfo(get_neuron_info::GetNeuronInfoOpts), AccountBalance(account_balance::AccountBalanceOpts), UpdateNodeProvider(update_node_provider::UpdateNodeProviderOpts), @@ -79,6 +81,10 @@ pub fn dispatch(auth: &AuthInfo, cmd: Command, fetch_root_key: bool, qr: bool) - Command::GetProposalInfo(opts) => { get_proposal_info::exec(opts, fetch_root_key)?; } + Command::MakeProposal(opts) => { + let out = make_proposal::exec(auth, opts)?; + print_vec(qr, &out)?; + } Command::GetNeuronInfo(opts) => { get_neuron_info::exec(opts, fetch_root_key)?; } diff --git a/src/commands/neuron_manage.rs b/src/commands/neuron_manage.rs index ddc67cf0..0ae7cf18 100644 --- a/src/commands/neuron_manage.rs +++ b/src/commands/neuron_manage.rs @@ -167,7 +167,7 @@ Cannot use --ledger with these flags. This version of quill only supports the fo let mut msgs = Vec::new(); let id = NeuronId { - id: parse_neuron_id(opts.neuron_id)?, + id: parse_neuron_id(&opts.neuron_id)?, }; let id = Some(NeuronIdOrSubaccount::NeuronId(id)); if opts.add_hot_key.is_some() { @@ -316,7 +316,7 @@ Cannot use --ledger with these flags. This version of quill only supports the fo id: None, command: Some(Command::Merge(Merge { source_neuron_id: Some(NeuronId { - id: parse_neuron_id(neuron_id)? + id: parse_neuron_id(&neuron_id)? }), })), neuron_id_or_subaccount: id.clone(), @@ -448,7 +448,7 @@ Cannot use --ledger with these flags. This version of quill only supports the fo Ok(generated) } -fn parse_neuron_id(id: String) -> AnyhowResult { +pub fn parse_neuron_id(id: &str) -> AnyhowResult { id.replace('_', "") .parse() .context("Failed to parse the neuron id") diff --git a/src/commands/sns/make_proposal.rs b/src/commands/sns/make_proposal.rs index 4f53ac50..71c0143d 100644 --- a/src/commands/sns/make_proposal.rs +++ b/src/commands/sns/make_proposal.rs @@ -62,7 +62,7 @@ pub fn exec( let governance_canister_id = sns_canister_ids.governance_canister_id; let proposal = if let Some(proposal) = opts.proposal { - parse_proposal_from_candid_string(proposal)? + parse_sns_proposal_from_candid_string(proposal)? } else { Decode!(&fs::read(opts.proposal_path.unwrap())?, Proposal)? }; @@ -83,7 +83,7 @@ pub fn exec( Ok(vec![msg]) } -fn parse_proposal_from_candid_string(proposal_candid: String) -> AnyhowResult { +fn parse_sns_proposal_from_candid_string(proposal_candid: String) -> AnyhowResult { let args = parse_idl_args(&proposal_candid)?; let args: Vec = args.to_bytes_with_types(&TypeEnv::default(), &[Proposal::ty()])?; Decode!(args.as_slice(), Proposal).map_err(Error::msg) diff --git a/src/lib/format/nns_governance.rs b/src/lib/format/nns_governance.rs index 03c57eb2..6e2ab3a2 100644 --- a/src/lib/format/nns_governance.rs +++ b/src/lib/format/nns_governance.rs @@ -29,7 +29,7 @@ use sha2::{Digest, Sha256}; use crate::lib::{ e8s_to_tokens, - format::{format_duration_seconds, format_t_cycles, format_timestamp_seconds}, + format::{format_duration_seconds, format_timestamp_seconds}, get_default_role, get_idl_string, AnyhowResult, }; @@ -971,8 +971,8 @@ fn display_canister_settings(settings: CanisterSettings) -> AnyhowResult if let Some(freezing) = settings.freezing_threshold { writeln!( fmt, - "Freezing threshold: {} cycles", - format_t_cycles(freezing.into()) + "Freezing threshold: {}", + format_duration_seconds(freezing), )?; } if let Some(memory) = settings.memory_allocation { diff --git a/tests/output/default/make_proposal/known_neuron.txt b/tests/output/default/make_proposal/known_neuron.txt new file mode 100644 index 00000000..a5159876 --- /dev/null +++ b/tests/output/default/make_proposal/known_neuron.txt @@ -0,0 +1,32 @@ +Sending message with + + Call type: update + Sender: fdsgv-62ihb-nbiqv-xgic5-iefsv-3cscz-tmbzv-63qd5-vh43v-dqfrt-pae + Canister id: rrkah-fqaaa-aaaaa-aaaaq-cai + Method name: manage_neuron + Arguments: ( + record { + id = null; + command = opt variant { + MakeProposal = record { + title = opt "Known Neuron Proposal"; + url = "http://example.com"; + summary = "A proposal to become a named neuron"; + action = opt variant { + RegisterKnownNeuron = record { + id = opt record { + id = 773 : nat64; + }; + known_neuron_data = opt record { + name = "Me!"; + }; + } + }; + } + }; + neuron_id_or_subaccount = opt variant { + NeuronId = record { id = 2_313_380_519_530_470_538 : nat64 } + }; + }, +) + diff --git a/tests/output/neuron_manage.rs b/tests/output/neuron_manage.rs index 68ccb063..ee99f2b2 100644 --- a/tests/output/neuron_manage.rs +++ b/tests/output/neuron_manage.rs @@ -1,6 +1,6 @@ use crate::{ledger_compatible, quill_send, OutputExt, ALICE, PRINCIPAL}; -const NEURON_ID: &str = "2313380519530470538"; +pub const NEURON_ID: &str = "2313380519530470538"; // uncomment tests on next ledger app update ledger_compatible![ diff --git a/tests/output/root.rs b/tests/output/root.rs index 4e8fe4fa..168eec7a 100644 --- a/tests/output/root.rs +++ b/tests/output/root.rs @@ -3,8 +3,8 @@ use std::io::Write; use tempfile::NamedTempFile; use crate::{ - escape_p, ledger_compatible, quill, quill_authed, quill_query, quill_query_authed, quill_send, - OutputExt, + escape_p, ledger_compatible, neuron_manage::NEURON_ID, quill, quill_authed, quill_query, + quill_query_authed, quill_send, OutputExt, }; // Uncomment tests on next ledger app update @@ -195,3 +195,24 @@ fn ledger_fail_early() { quill("neuron-manage 1 --ledger --join-community-fund") .diff_err("ledger_incompatible/by_flag.txt"); } + +#[test] +fn make_proposal() { + let proposal = r#"(record{ + title=opt "Known Neuron Proposal"; + url="http://example.com"; + summary="A proposal to become a named neuron"; + action=opt variant { + RegisterKnownNeuron = record { + id=opt record { id=773; }; + known_neuron_data=opt record { + name="Me!"; + }; + } + }; + })"#; + quill_send(&format!( + "make-proposal {NEURON_ID} --proposal '{proposal}'" + )) + .diff("make_proposal/known_neuron.txt"); +}