Skip to content

Commit 25fd802

Browse files
authored
[cli] improve democracy + commitments commands (#368)
* add tally counts and thresholds * cosmetics * add reputation commitment commands * cleanup * bump version 1.8.3
1 parent ba9c25d commit 25fd802

File tree

12 files changed

+284
-13
lines changed

12 files changed

+284
-13
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "encointer-client-notee"
33
authors = ["encointer.org <alain@encointer.org>"]
44
edition = "2021"
55
#keep with node version. major, minor and patch
6-
version = "1.8.2"
6+
version = "1.8.3"
77

88
[dependencies]
99
# todo migrate to clap >=3 https://github.com/encointer/encointer-node/issues/107

client/encointer-api-client-extension/src/democracy.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
11
use crate::{Api, Moment, Result};
2+
use encointer_node_notee_runtime::Hash;
3+
use encointer_primitives::{
4+
ceremonies::ReputationCountType,
5+
democracy::{ProposalIdType, Tally},
6+
reputation_commitments::PurposeIdType,
7+
};
28
use std::time::Duration;
39
use substrate_api_client::GetStorage;
410

511
#[maybe_async::maybe_async(?Send)]
612
pub trait DemocracyApi {
713
async fn get_proposal_lifetime(&self) -> Result<Duration>;
814
async fn get_confirmation_period(&self) -> Result<Duration>;
15+
async fn get_min_turnout(&self) -> Result<ReputationCountType>;
16+
async fn get_tally(
17+
&self,
18+
proposal_id: ProposalIdType,
19+
maybe_at: Option<Hash>,
20+
) -> Result<Option<Tally>>;
21+
async fn get_purpose_id(
22+
&self,
23+
proposal_id: ProposalIdType,
24+
maybe_at: Option<Hash>,
25+
) -> Result<Option<PurposeIdType>>;
926
}
1027

1128
#[maybe_async::maybe_async(?Send)]
@@ -20,4 +37,24 @@ impl DemocracyApi for Api {
2037
self.get_constant::<Moment>("EncointerDemocracy", "ConfirmationPeriod").await?,
2138
))
2239
}
40+
async fn get_min_turnout(&self) -> Result<ReputationCountType> {
41+
self.get_constant("EncointerDemocracy", "MinTurnout").await
42+
}
43+
async fn get_tally(
44+
&self,
45+
proposal_id: ProposalIdType,
46+
maybe_at: Option<Hash>,
47+
) -> Result<Option<Tally>> {
48+
self.get_storage_map("EncointerDemocracy", "Tallies", proposal_id, maybe_at)
49+
.await
50+
}
51+
52+
async fn get_purpose_id(
53+
&self,
54+
proposal_id: ProposalIdType,
55+
maybe_at: Option<Hash>,
56+
) -> Result<Option<PurposeIdType>> {
57+
self.get_storage_map("EncointerDemocracy", "PurposeIds", proposal_id, maybe_at)
58+
.await
59+
}
2360
}

client/encointer-api-client-extension/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ pub use ceremonies::*;
2121
pub use communities::*;
2222
pub use democracy::*;
2323
pub use extrinsic_params::*;
24+
pub use reputation_commitments::*;
2425
pub use scheduler::*;
2526

2627
mod bazaar;
2728
mod ceremonies;
2829
mod communities;
2930
mod democracy;
3031
mod extrinsic_params;
32+
mod reputation_commitments;
3133
mod scheduler;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use crate::{Api, Result};
2+
use encointer_node_notee_runtime::{AccountId, Hash};
3+
use encointer_primitives::{
4+
ceremonies::CommunityCeremony,
5+
reputation_commitments::{DescriptorType, PurposeIdType},
6+
};
7+
use substrate_api_client::GetStorage;
8+
9+
#[maybe_async::maybe_async(?Send)]
10+
pub trait ReputationCommitmentsApi {
11+
async fn get_commitment(
12+
&self,
13+
community_ceremony: &CommunityCeremony,
14+
purpose_account: (PurposeIdType, AccountId),
15+
maybe_at: Option<Hash>,
16+
) -> Result<Option<Option<Hash>>>;
17+
async fn get_current_purpose_id(&self, maybe_at: Option<Hash>)
18+
-> Result<Option<PurposeIdType>>;
19+
async fn get_purpose_descriptor(
20+
&self,
21+
purpose_id: PurposeIdType,
22+
maybe_at: Option<Hash>,
23+
) -> Result<Option<DescriptorType>>;
24+
}
25+
26+
#[maybe_async::maybe_async(?Send)]
27+
impl ReputationCommitmentsApi for Api {
28+
async fn get_commitment(
29+
&self,
30+
community_ceremony: &CommunityCeremony,
31+
purpose_account: (PurposeIdType, AccountId),
32+
maybe_at: Option<Hash>,
33+
) -> Result<Option<Option<Hash>>> {
34+
self.get_storage_double_map(
35+
"EncointerReputationCommitments",
36+
"Commitments",
37+
community_ceremony,
38+
purpose_account,
39+
maybe_at,
40+
)
41+
.await
42+
}
43+
async fn get_current_purpose_id(
44+
&self,
45+
maybe_at: Option<Hash>,
46+
) -> Result<Option<PurposeIdType>> {
47+
self.get_storage("EncointerReputationCommitments", "CurrentPurposeId", maybe_at)
48+
.await
49+
}
50+
async fn get_purpose_descriptor(
51+
&self,
52+
purpose_id: PurposeIdType,
53+
maybe_at: Option<Hash>,
54+
) -> Result<Option<DescriptorType>> {
55+
self.get_storage_map("EncointerReputationCommitments", "Purposes", purpose_id, maybe_at)
56+
.await
57+
}
58+
}

client/src/cli_args.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use clap::{App, Arg, ArgMatches};
2-
use encointer_primitives::balances::BalanceType;
2+
use encointer_primitives::{balances::BalanceType, reputation_commitments::PurposeIdType};
33
use sp_core::{bytes, H256 as Hash};
44

55
const ACCOUNT_ARG: &str = "accountid";
@@ -35,6 +35,8 @@ const REPUTATION_VEC_ARG: &str = "reputation-vec";
3535
const INACTIVITY_TIMEOUT_ARG: &str = "inactivity-timeout";
3636
const NOMINAL_INCOME_ARG: &str = "nominal-income";
3737

38+
const PURPOSE_ID_ARG: &str = "purpose-id";
39+
3840
pub trait EncointerArgs<'b> {
3941
fn account_arg(self) -> Self;
4042
fn faucet_account_arg(self) -> Self;
@@ -70,6 +72,7 @@ pub trait EncointerArgs<'b> {
7072
fn reputation_vec_arg(self) -> Self;
7173
fn inactivity_timeout_arg(self) -> Self;
7274
fn nominal_income_arg(self) -> Self;
75+
fn purpose_id_arg(self) -> Self;
7376
}
7477

7578
pub trait EncointerArgsExtractor {
@@ -106,6 +109,7 @@ pub trait EncointerArgsExtractor {
106109
fn reputation_vec_arg(&self) -> Option<Vec<&str>>;
107110
fn inactivity_timeout_arg(&self) -> Option<u32>;
108111
fn nominal_income_arg(&self) -> Option<BalanceType>;
112+
fn purpose_id_arg(&self) -> Option<PurposeIdType>;
109113
}
110114

111115
impl<'a, 'b> EncointerArgs<'b> for App<'a, 'b> {
@@ -432,6 +436,15 @@ impl<'a, 'b> EncointerArgs<'b> for App<'a, 'b> {
432436
.help("nominal income"),
433437
)
434438
}
439+
fn purpose_id_arg(self) -> Self {
440+
self.arg(
441+
Arg::with_name(PURPOSE_ID_ARG)
442+
.takes_value(true)
443+
.required(false)
444+
.value_name("PURPOSE_ID")
445+
.help("reputation commitment purpose id"),
446+
)
447+
}
435448
}
436449

437450
impl<'a> EncointerArgsExtractor for ArgMatches<'a> {
@@ -553,4 +566,7 @@ impl<'a> EncointerArgsExtractor for ArgMatches<'a> {
553566
self.value_of(NOMINAL_INCOME_ARG)
554567
.map(|v| BalanceType::from_num(v.parse::<f64>().unwrap()))
555568
}
569+
fn purpose_id_arg(&self) -> Option<PurposeIdType> {
570+
self.value_of(PURPOSE_ID_ARG).map(|v| v.parse().unwrap())
571+
}
556572
}

client/src/commands/encointer_democracy.rs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ pub fn list_proposals(_args: &str, matches: &ArgMatches<'_>) -> Result<(), clap:
9797
}
9898
let confirmation_period = api.get_confirmation_period().await.unwrap();
9999
let proposal_lifetime = api.get_proposal_lifetime().await.unwrap();
100+
let min_turnout_permill = api.get_min_turnout().await.unwrap();
100101
for storage_key in storage_keys.iter() {
101102
let key_postfix = storage_key.as_ref();
102103
let proposal_id =
@@ -107,7 +108,6 @@ pub fn list_proposals(_args: &str, matches: &ArgMatches<'_>) -> Result<(), clap:
107108
if !matches.all_flag() && matches!(proposal.state, ProposalState::Cancelled) {
108109
continue
109110
}
110-
println!("id: {}", proposal_id);
111111
let start = DateTime::<Utc>::from_timestamp_millis(
112112
TryInto::<i64>::try_into(proposal.start).unwrap(),
113113
)
@@ -134,18 +134,37 @@ pub fn list_proposals(_args: &str, matches: &ArgMatches<'_>) -> Result<(), clap:
134134
maybe_at,
135135
)
136136
.await;
137-
println!("action: {:?}", proposal.action);
138-
println!("started at: {}", start.format("%Y-%m-%d %H:%M:%S %Z").to_string());
137+
let tally = api.get_tally(proposal_id, maybe_at).await.unwrap().unwrap_or_default();
138+
let purpose_id = api.get_purpose_id(proposal_id, maybe_at).await.unwrap().unwrap();
139139
println!(
140-
"ends after: {}",
140+
"Proposal id: {} (reputation commitment purpose id: {})",
141+
proposal_id, purpose_id
142+
);
143+
println!("🛠 action: {:?}", proposal.action);
144+
println!("▶️ started at: {}", start.format("%Y-%m-%d %H:%M:%S %Z").to_string());
145+
println!(
146+
"🏁 ends after: {}",
141147
(start + proposal_lifetime.clone()).format("%Y-%m-%d %H:%M:%S %Z").to_string()
142148
);
143-
println!("start cindex: {}", proposal.start_cindex);
144-
println!("current electorate estimate: {electorate}");
149+
println!("🔄 start cindex: {}", proposal.start_cindex);
150+
println!("👥 electorate: {electorate}");
151+
println!(
152+
"🗳 turnout: {} votes = {:.3}% of electorate (turnout threshold {} votes = {:.3}%)",
153+
tally.turnout,
154+
100f64 * tally.turnout as f64 / electorate as f64,
155+
min_turnout_permill as f64 * electorate as f64 / 1000f64,
156+
min_turnout_permill as f64 / 10f64
157+
);
158+
println!(
159+
"🗳 approval: {} votes = {:.3}% Aye (AQB approval threshold: {:.3}%)",
160+
tally.ayes,
161+
100f64 * tally.ayes as f64 / tally.turnout as f64,
162+
approval_threshold_percent(electorate, tally.turnout)
163+
);
145164
println!("state: {:?}", proposal.state);
146165
if let Some(since) = maybe_confirming_since {
147166
println!(
148-
"confirming since: {} until {}",
167+
"👍 confirming since: {} until {}",
149168
since.format("%Y-%m-%d %H:%M:%S %Z").to_string(),
150169
(since + confirmation_period).format("%Y-%m-%d %H:%M:%S %Z").to_string()
151170
)
@@ -293,3 +312,7 @@ async fn get_relevant_electorate(
293312
panic!("couldn't fetch some values")
294313
}
295314
}
315+
316+
fn approval_threshold_percent(electorate: u128, turnout: u128) -> f64 {
317+
100f64 / (1f64 + (turnout as f64 / electorate as f64).sqrt())
318+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use crate::{cli_args::EncointerArgsExtractor, utils::get_chain_api};
2+
use clap::ArgMatches;
3+
use encointer_api_client_extension::{
4+
CeremoniesApi, CommunitiesApi, ReputationCommitmentsApi, SchedulerApi,
5+
};
6+
use encointer_node_notee_runtime::{AccountId, Hash};
7+
use encointer_primitives::reputation_commitments::{DescriptorType, PurposeIdType};
8+
use log::{debug, error};
9+
use parity_scale_codec::{Decode, Encode};
10+
use sp_core::crypto::Ss58Codec;
11+
use substrate_api_client::GetStorage;
12+
13+
pub fn list_commitments(_args: &str, matches: &ArgMatches<'_>) -> Result<(), clap::Error> {
14+
let rt = tokio::runtime::Runtime::new().unwrap();
15+
rt.block_on(async {
16+
let api = get_chain_api(matches).await;
17+
let maybe_at = matches.at_block_arg();
18+
let cid = api.verify_cid(matches.cid_arg().unwrap(), None).await;
19+
let maybe_purpose_id = matches.purpose_id_arg();
20+
let cindex = api.get_ceremony_index(None).await;
21+
if let Ok((reputation_lifetime, max_purpose_id)) = tokio::try_join!(
22+
api.get_reputation_lifetime(maybe_at),
23+
api.get_current_purpose_id(maybe_at)
24+
) {
25+
let relevant_cindexes = cindex.saturating_sub(reputation_lifetime)..=cindex;
26+
debug!("relevant ceremony indexes: {:?}", &relevant_cindexes);
27+
let pids = match maybe_purpose_id {
28+
Some(pid) => pid..=pid,
29+
_ => 0..=max_purpose_id.unwrap_or(0),
30+
};
31+
debug!("scanning for purpose_id's: {:?}", pids);
32+
for purpose_id in pids {
33+
for c in relevant_cindexes.clone() {
34+
let mut key_prefix = api
35+
.get_storage_double_map_key_prefix(
36+
"EncointerReputationCommitments",
37+
"Commitments",
38+
(cid, c),
39+
)
40+
.await
41+
.unwrap();
42+
43+
// thanks to Identity hashing we can get all accounts for one specific PurposeId and community_ceremony
44+
key_prefix.0.append(&mut purpose_id.encode());
45+
46+
let max_keys = 1000;
47+
let storage_keys = api
48+
.get_storage_keys_paged(Some(key_prefix), max_keys, None, maybe_at)
49+
.await
50+
.unwrap();
51+
if storage_keys.len() == max_keys as usize {
52+
error!("results can be wrong because max keys reached for query")
53+
}
54+
for storage_key in storage_keys.iter() {
55+
let maybe_commitment: Option<Option<Hash>> =
56+
api.get_storage_by_key(storage_key.clone(), maybe_at).await.unwrap();
57+
if let Some(maybe_hash) = maybe_commitment {
58+
let account = AccountId::decode(
59+
&mut storage_key.0[storage_key.0.len() - 32..].as_ref(),
60+
)
61+
.unwrap();
62+
if let Some(hash) = maybe_hash {
63+
println!(
64+
"{cid}, {c}, {purpose_id}, {}, {}",
65+
account.to_ss58check(),
66+
hash
67+
);
68+
} else {
69+
println!(
70+
"{cid}, {c}, {purpose_id}, {}, None",
71+
account.to_ss58check()
72+
);
73+
}
74+
}
75+
}
76+
}
77+
}
78+
}
79+
Ok(())
80+
})
81+
.into()
82+
}
83+
84+
pub fn list_purposes(_args: &str, matches: &ArgMatches<'_>) -> Result<(), clap::Error> {
85+
let rt = tokio::runtime::Runtime::new().unwrap();
86+
rt.block_on(async {
87+
let api = get_chain_api(matches).await;
88+
let maybe_at = matches.at_block_arg();
89+
let key_prefix = api
90+
.get_storage_map_key_prefix("EncointerReputationCommitments", "Purposes")
91+
.await
92+
.unwrap();
93+
94+
let max_keys = 1000;
95+
let storage_keys = api
96+
.get_storage_keys_paged(Some(key_prefix), max_keys, None, maybe_at)
97+
.await
98+
.unwrap();
99+
if storage_keys.len() == max_keys as usize {
100+
error!("results can be wrong because max keys reached for query")
101+
}
102+
for storage_key in storage_keys.iter() {
103+
let maybe_purpose: Option<DescriptorType> =
104+
api.get_storage_by_key(storage_key.clone(), maybe_at).await.unwrap();
105+
if let Some(descriptor) = maybe_purpose {
106+
let purpose_id =
107+
PurposeIdType::decode(&mut storage_key.0[storage_key.0.len() - 8..].as_ref())
108+
.unwrap();
109+
println!("{purpose_id}: {}", String::from_utf8_lossy(descriptor.as_ref()));
110+
}
111+
}
112+
Ok(())
113+
})
114+
.into()
115+
}

0 commit comments

Comments
 (0)