diff --git a/Cargo.Bazel.lock b/Cargo.Bazel.lock index d92f360e..da000749 100644 --- a/Cargo.Bazel.lock +++ b/Cargo.Bazel.lock @@ -1,5 +1,5 @@ { - "checksum": "e6da14722080ba8a0b4069a6eb6deaec83172a87557d77964c9e84894b72a822", + "checksum": "a23efd2a9223be99e707de45f95191617e14ea319d026e8ae5eff0ec08ad9243", "crates": { "actix-codec 0.5.2": { "name": "actix-codec", @@ -11744,6 +11744,10 @@ "id": "ic-interfaces-registry 0.9.0", "target": "ic_interfaces_registry" }, + { + "id": "ic-nns-common 0.9.0", + "target": "ic_nns_common" + }, { "id": "ic-nns-constants 0.9.0", "target": "ic_nns_constants" diff --git a/Cargo.lock b/Cargo.lock index c314c85f..efcad755 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2333,6 +2333,7 @@ dependencies = [ "ic-interfaces-registry", "ic-management-backend", "ic-management-types", + "ic-nns-common", "ic-nns-constants", "ic-nns-governance", "ic-protobuf", diff --git a/rs/cli/Cargo.toml b/rs/cli/Cargo.toml index 9ff5e39b..29ae3eff 100644 --- a/rs/cli/Cargo.toml +++ b/rs/cli/Cargo.toml @@ -35,6 +35,7 @@ ic-management-backend = { workspace = true } ic-management-types = { workspace = true } ic-nns-constants = { workspace = true } ic-nns-governance = { workspace = true } +ic-nns-common = { workspace = true } ic-protobuf = { workspace = true } ic-registry-keys = { workspace = true } ic-registry-local-registry = { workspace = true } diff --git a/rs/cli/src/cli.rs b/rs/cli/src/cli.rs index 4b715c72..cda49796 100644 --- a/rs/cli/src/cli.rs +++ b/rs/cli/src/cli.rs @@ -154,6 +154,9 @@ pub enum Commands { #[clap(long, default_value = None, required = true)] summary: Option, }, + + /// Proposal Listing + Proposals(proposals::Cmd), } impl Default for Commands { @@ -423,3 +426,63 @@ pub mod nodes { }, } } + +pub mod proposals { + use super::*; + + #[derive(Parser, Clone)] + pub struct Cmd { + #[clap(subcommand)] + pub subcommand: Commands, + } + + #[derive(Subcommand, Clone)] + pub enum Commands { + /// Get list of pending proposals + Pending, + + /// Get list of filtered proposals + List { + /// Limit on the number of \[ProposalInfo\] to return. If no value is + /// specified, or if a value greater than 100 is specified, 100 + /// will be used. + #[clap(long, default_value = "100")] + limit: u32, + /// If specified, only return proposals that are strictly earlier than + /// the specified proposal according to the proposal ID. If not + /// specified, start with the most recent proposal. + #[clap(long)] + before_proposal: Option, + /// Exclude proposals with a topic in this list. This is particularly + /// useful to exclude proposals on the topics TOPIC_EXCHANGE_RATE and + /// TOPIC_KYC which most users are not likely to be interested in + /// seeing. + #[clap(long)] + exclude_topic: Vec, + /// Include proposals that have a reward status in this list (see + /// \[ProposalRewardStatus\] for more information). If this list is + /// empty, no restriction is applied. For example, many users listing + /// proposals will only be interested in proposals for which they can + /// receive voting rewards, i.e., with reward status + /// PROPOSAL_REWARD_STATUS_ACCEPT_VOTES. + #[clap(long)] + include_reward_status: Vec, + /// Include proposals that have a status in this list (see + /// \[ProposalStatus\] for more information). If this list is empty, no + /// restriction is applied. + #[clap(long)] + include_status: Vec, + /// Include all ManageNeuron proposals regardless of the visibility of the + /// proposal to the caller principal. Note that exclude_topic is still + /// respected even when this option is set to true. + #[clap(long)] + include_all_manage_neuron_proposals: Option, + /// Omits "large fields" from the response. Currently only omits the + /// `logo` and `token_logo` field of CreateServiceNervousSystem proposals. This + /// is useful to improve download times and to ensure that the response to the + /// request doesn't exceed the message size limit. + #[clap(long)] + omit_large_fields: Option, + }, + } +} diff --git a/rs/cli/src/main.rs b/rs/cli/src/main.rs index 60d66fb2..24a972cc 100644 --- a/rs/cli/src/main.rs +++ b/rs/cli/src/main.rs @@ -7,10 +7,13 @@ use dre::general::{get_node_metrics_history, vote_on_proposals}; use dre::operations::hostos_rollout::{NodeGroupUpdate, NumberOfNodes}; use dre::{cli, ic_admin, local_unused_port, registry_dump, runner}; use ic_base_types::CanisterId; -use ic_canisters::governance::governance_canister_version; +use ic_canisters::governance::{governance_canister_version, GovernanceCanisterWrapper}; +use ic_canisters::CanisterClient; use ic_management_backend::endpoints; use ic_management_types::requests::NodesRemoveRequest; use ic_management_types::{Artifact, MinNakamotoCoefficients, NodeFeature}; +use ic_nns_common::pb::v1::ProposalId; +use ic_nns_governance::pb::v1::ListProposalInfo; use log::{info, warn}; use serde_json::Value; use std::collections::BTreeMap; @@ -327,6 +330,32 @@ async fn async_main() -> Result<(), anyhow::Error> { ..Default::default() }, cli_opts.simulate).await } + cli::Commands::Proposals(p) => match &p.subcommand { + cli::proposals::Commands::Pending => { + let nns_url = target_network.get_nns_urls().first().expect("Should have at least one NNS URL"); + let client = GovernanceCanisterWrapper::from(CanisterClient::from_anonymous(nns_url)?); + let proposals = client.get_pending_proposals().await?; + let proposals = serde_json::to_string(&proposals).map_err(|e| anyhow::anyhow!("Couldn't serialize to string: {:?}", e))?; + println!("{}", proposals); + Ok(()) + } + cli::proposals::Commands::List { limit, before_proposal, exclude_topic, include_reward_status, include_status, include_all_manage_neuron_proposals, omit_large_fields } => { + let nns_url = target_network.get_nns_urls().first().expect("Should have at least one NNS URL"); + let client = GovernanceCanisterWrapper::from(CanisterClient::from_anonymous(nns_url)?); + let proposals = client.list_proposals(ListProposalInfo { + before_proposal: before_proposal.as_ref().map(|p| ProposalId { id: *p }), + exclude_topic: exclude_topic.clone(), + include_all_manage_neuron_proposals: *include_all_manage_neuron_proposals, + include_reward_status: include_reward_status.clone(), + include_status: include_status.clone(), + limit: *limit, + omit_large_fields: *omit_large_fields + }).await?; + let proposals = serde_json::to_string(&proposals).map_err(|e| anyhow::anyhow!("Couldn't serialize to string: {:?}", e))?; + println!("{}", proposals); + Ok(()) + }, + }, } }) .await; diff --git a/rs/ic-canisters/src/governance.rs b/rs/ic-canisters/src/governance.rs index 506d9dbe..2bd76386 100644 --- a/rs/ic-canisters/src/governance.rs +++ b/rs/ic-canisters/src/governance.rs @@ -4,6 +4,8 @@ use ic_nns_common::pb::v1::NeuronId; use ic_nns_common::pb::v1::ProposalId; use ic_nns_constants::GOVERNANCE_CANISTER_ID; use ic_nns_governance::pb::v1::manage_neuron::RegisterVote; +use ic_nns_governance::pb::v1::ListProposalInfo; +use ic_nns_governance::pb::v1::ListProposalInfoResponse; use ic_nns_governance::pb::v1::ManageNeuron; use ic_nns_governance::pb::v1::ManageNeuronResponse; use ic_nns_governance::pb::v1::ProposalInfo; @@ -141,4 +143,21 @@ impl GovernanceCanisterWrapper { Err(err) => Err(anyhow::anyhow!("Error executing update: {}", err)), } } + + pub async fn list_proposals(&self, contract: ListProposalInfo) -> anyhow::Result> { + let args = Encode! { &contract }?; + match self + .client + .agent + .execute_query(&GOVERNANCE_CANISTER_ID, "list_proposals", args) + .await + { + Ok(Some(response)) => match Decode!(response.as_slice(), ListProposalInfoResponse) { + Ok(response) => Ok(response.proposal_info), + Err(e) => Err(anyhow::anyhow!("Error deserializing response: {:?}", e)), + }, + Ok(None) => Ok(vec![]), + Err(e) => Err(anyhow::anyhow!("Error executing query: {}", e)), + } + } } diff --git a/rs/ic-canisters/src/lib.rs b/rs/ic-canisters/src/lib.rs index 502187c0..57f8d1da 100644 --- a/rs/ic-canisters/src/lib.rs +++ b/rs/ic-canisters/src/lib.rs @@ -1,5 +1,6 @@ use candid::CandidType; use ic_agent::agent::http_transport::ReqwestTransport; +use ic_agent::identity::AnonymousIdentity; use ic_agent::identity::BasicIdentity; use ic_agent::identity::Secp256k1Identity; use ic_agent::Agent; @@ -51,6 +52,12 @@ impl CanisterClient { agent: CanisterClientAgent::new(nns_url.clone(), sender), }) } + + pub fn from_anonymous(nns_url: &Url) -> anyhow::Result { + Ok(Self { + agent: CanisterClientAgent::new(nns_url.clone(), Sender::Anonymous), + }) + } } pub struct IcAgentCanisterClient { @@ -66,18 +73,20 @@ impl IcAgentCanisterClient { .map_err(|e| anyhow::anyhow!("Couldn't load identity: {:?}", e))?; Box::new(identity) }; - Ok(Self { - agent: Agent::builder() - .with_identity(identity) - .with_transport(ReqwestTransport::create(url)?) - .with_verify_query_signatures(false) - .build()?, - }) + Self::build_agent(url, identity) } pub fn from_hsm(pin: String, slot: u64, key_id: String, url: Url, lock: Option>) -> anyhow::Result { let pin_fn = || Ok(pin); let identity = ParallelHardwareIdentity::new(pkcs11_lib_path()?, slot as usize, &key_id, pin_fn, lock)?; + Self::build_agent(url, Box::new(identity)) + } + + pub fn from_anonymous(url: Url) -> anyhow::Result { + Self::build_agent(url, Box::new(AnonymousIdentity)) + } + + fn build_agent(url: Url, identity: Box) -> anyhow::Result { Ok(Self { agent: Agent::builder() .with_identity(identity)