pcli/command/query/
governance.rs

1use std::{
2    collections::BTreeMap,
3    io::{stdout, Write},
4};
5
6use anyhow::{Context, Result};
7use futures::TryStreamExt;
8use penumbra_sdk_governance::Vote;
9use penumbra_sdk_proto::core::component::governance::v1::{
10    query_service_client::QueryServiceClient as GovernanceQueryServiceClient,
11    AllTalliedDelegatorVotesForProposalRequest, ProposalDataRequest, ProposalListRequest,
12    ProposalListResponse, ValidatorVotesRequest, ValidatorVotesResponse,
13    VotingPowerAtProposalStartRequest,
14};
15use penumbra_sdk_stake::IdentityKey;
16use serde::Serialize;
17use serde_json::json;
18
19use crate::App;
20
21#[derive(Debug, clap::Subcommand)]
22pub enum GovernanceCmd {
23    /// List all governance proposals by number.
24    ListProposals {
25        /// Whether to include proposals which have already finished voting.
26        #[clap(short, long)]
27        inactive: bool,
28    },
29    /// Query for information about a particular proposal.
30    Proposal {
31        /// The proposal id to query.
32        proposal_id: u64,
33        /// The query to ask of it.
34        #[clap(subcommand)]
35        query: PerProposalCmd,
36    },
37}
38
39#[derive(Debug, clap::Subcommand)]
40pub enum PerProposalCmd {
41    /// Fetch the details of a proposal, as submitted to the chain.
42    Definition,
43    /// Display the current state of a proposal.
44    State,
45    /// Display the voting period of a proposal.
46    Period,
47    /// Display the most recent tally of votes on the proposal.
48    Tally,
49}
50
51impl GovernanceCmd {
52    pub async fn exec(&self, app: &mut App) -> Result<()> {
53        // use PerProposalCmd::*;
54
55        let mut client = GovernanceQueryServiceClient::new(app.pd_channel().await?);
56        match self {
57            GovernanceCmd::ListProposals { inactive } => {
58                let proposals: Vec<ProposalListResponse> = client
59                    .proposal_list(ProposalListRequest {
60                        inactive: *inactive,
61                        ..Default::default()
62                    })
63                    .await?
64                    .into_inner()
65                    .try_collect::<Vec<_>>()
66                    .await
67                    .context("cannot process proposal list data")?;
68                let mut writer = stdout();
69                for proposal_response in proposals {
70                    let proposal = proposal_response
71                        .proposal
72                        .expect("proposal should always be set");
73                    let proposal_title = proposal.title;
74
75                    let proposal_state = proposal_response
76                        .state
77                        .expect("proposal state should always be set");
78
79                    let proposal_id = proposal.id;
80
81                    writeln!(
82                        writer,
83                        "#{proposal_id} {proposal_state:?}    {proposal_title}"
84                    )?;
85                }
86                Ok(())
87            }
88            GovernanceCmd::Proposal { proposal_id, query } => {
89                match query {
90                    &PerProposalCmd::Definition => {
91                        let proposal = client
92                            .proposal_data(ProposalDataRequest {
93                                proposal_id: *proposal_id,
94                                ..Default::default()
95                            })
96                            .await?
97                            .into_inner();
98                        toml(
99                            &proposal
100                                .proposal
101                                .expect("proposal should always be populated"),
102                        )?;
103                    }
104                    PerProposalCmd::State => {
105                        let proposal = client
106                            .proposal_data(ProposalDataRequest {
107                                proposal_id: *proposal_id,
108                                ..Default::default()
109                            })
110                            .await?
111                            .into_inner();
112                        json(
113                            &proposal
114                                .state
115                                .expect("proposal state should always be populated"),
116                        )?;
117                    }
118                    PerProposalCmd::Period => {
119                        let proposal = client
120                            .proposal_data(ProposalDataRequest {
121                                proposal_id: *proposal_id,
122                                ..Default::default()
123                            })
124                            .await?
125                            .into_inner();
126                        let start: u64 = proposal.start_block_height;
127                        let end: u64 = proposal.end_block_height;
128                        let period = json!({
129                            "voting_start_block": start,
130                            "voting_end_block": end,
131                        });
132                        json(&period)?;
133                    }
134                    PerProposalCmd::Tally => {
135                        let validator_votes: Vec<ValidatorVotesResponse> = client
136                            .validator_votes(ValidatorVotesRequest {
137                                proposal_id: *proposal_id,
138                                ..Default::default()
139                            })
140                            .await?
141                            .into_inner()
142                            .try_collect::<Vec<_>>()
143                            .await?;
144
145                        let mut validator_votes_and_power: BTreeMap<IdentityKey, (Vote, u64)> =
146                            BTreeMap::new();
147                        for vote_response in validator_votes {
148                            let identity_key: IdentityKey = vote_response
149                                .identity_key
150                                .expect("identity key must be set for vote response")
151                                .try_into()?;
152                            let vote: Vote = vote_response
153                                .vote
154                                .expect("vote must be set for vote response")
155                                .try_into()?;
156                            let power: u64 = client
157                                .voting_power_at_proposal_start(VotingPowerAtProposalStartRequest {
158                                    proposal_id: *proposal_id,
159                                    identity_key: Some(identity_key.into()),
160                                    ..Default::default()
161                                })
162                                .await
163                                .context("Error looking for validator power")?
164                                .into_inner()
165                                .voting_power;
166
167                            validator_votes_and_power.insert(identity_key, (vote, power));
168                        }
169
170                        let mut delegator_tallies: BTreeMap<
171                            IdentityKey,
172                            penumbra_sdk_governance::Tally,
173                        > = client
174                            .all_tallied_delegator_votes_for_proposal(
175                                AllTalliedDelegatorVotesForProposalRequest {
176                                    proposal_id: *proposal_id,
177                                    ..Default::default()
178                                },
179                            )
180                            .await?
181                            .into_inner()
182                            .map_ok(|response| {
183                                let identity_key: IdentityKey = response
184                                    .identity_key
185                                    .expect("identity key must be set for vote response")
186                                    .try_into()?;
187                                let tally: penumbra_sdk_governance::Tally = response
188                                    .tally
189                                    .expect("tally must be set for vote response")
190                                    .try_into()?;
191                                Ok::<(IdentityKey, penumbra_sdk_governance::Tally), anyhow::Error>(
192                                    (identity_key, tally),
193                                )
194                            })
195                            // TODO: double iterator here is suboptimal but trying to collect
196                            // `Result<Vec<_>>` was annoying
197                            .try_collect::<Vec<_>>()
198                            .await?
199                            .into_iter()
200                            .collect::<Result<BTreeMap<_, _>>>()?;
201
202                        // Combine the two mappings
203                        let mut total = penumbra_sdk_governance::Tally::default();
204                        let mut all_votes_and_power: BTreeMap<String, serde_json::Value> =
205                            BTreeMap::new();
206                        for (identity_key, (vote, power)) in validator_votes_and_power.into_iter() {
207                            all_votes_and_power.insert(identity_key.to_string(), {
208                                let mut map = serde_json::Map::new();
209                                map.insert(
210                                    "validator".to_string(),
211                                    json!({
212                                        vote.to_string(): power,
213                                    }),
214                                );
215                                let delegator_tally =
216                                    if let Some(tally) = delegator_tallies.remove(&identity_key) {
217                                        map.insert("delegators".to_string(), json_tally(&tally));
218                                        tally
219                                    } else {
220                                        Default::default()
221                                    };
222                                // Subtract delegator total from validator power, then add delegator
223                                // tally in to get the total tally for this validator:
224                                let sub_total = penumbra_sdk_governance::Tally::from((
225                                    vote,
226                                    power - delegator_tally.total(),
227                                )) + delegator_tally;
228                                map.insert("sub_total".to_string(), json_tally(&sub_total));
229                                total += sub_total;
230                                map.into()
231                            });
232                        }
233                        for (identity_key, tally) in delegator_tallies.into_iter() {
234                            all_votes_and_power.insert(identity_key.to_string(), {
235                                let mut map = serde_json::Map::new();
236                                let sub_total = tally;
237                                map.insert("delegators".to_string(), json_tally(&tally));
238                                map.insert("sub_total".to_string(), json_tally(&sub_total));
239                                total += sub_total;
240                                map.into()
241                            });
242                        }
243
244                        json(&json!({
245                        "total": json_tally(&total),
246                        "details": all_votes_and_power,
247                        }))?;
248                    }
249                };
250                Ok(())
251            }
252        }
253    }
254}
255
256fn json<T: Serialize>(value: &T) -> Result<()> {
257    let mut writer = stdout();
258    serde_json::to_writer_pretty(&mut writer, value)?;
259    writer.write_all(b"\n")?;
260    Ok(())
261}
262
263fn json_tally(tally: &penumbra_sdk_governance::Tally) -> serde_json::Value {
264    let mut map = serde_json::Map::new();
265    if tally.yes() > 0 {
266        map.insert("yes".to_string(), tally.yes().into());
267    }
268    if tally.no() > 0 {
269        map.insert("no".to_string(), tally.no().into());
270    }
271    if tally.abstain() > 0 {
272        map.insert("abstain".to_string(), tally.abstain().into());
273    }
274    map.into()
275}
276
277fn toml<T: Serialize>(value: &T) -> Result<()> {
278    let mut writer = stdout();
279    let string = toml::to_string_pretty(value)?;
280    writer.write_all(string.as_bytes())?;
281    Ok(())
282}