pcli/command/query/
governance.rs1use 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 ListProposals {
25 #[clap(short, long)]
27 inactive: bool,
28 },
29 Proposal {
31 proposal_id: u64,
33 #[clap(subcommand)]
35 query: PerProposalCmd,
36 },
37}
38
39#[derive(Debug, clap::Subcommand)]
40pub enum PerProposalCmd {
41 Definition,
43 State,
45 Period,
47 Tally,
49}
50
51impl GovernanceCmd {
52 pub async fn exec(&self, app: &mut App) -> Result<()> {
53 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 .try_collect::<Vec<_>>()
198 .await?
199 .into_iter()
200 .collect::<Result<BTreeMap<_, _>>>()?;
201
202 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 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}