Skip to content

Commit 5ee0318

Browse files
authored
Add dreps incentives command (#155)
* Add command to calculate incentives for dreps * add tests * use prop_assert_eq instead of assert_eq * fix imports
1 parent bc5239a commit 5ee0318

File tree

7 files changed

+494
-116
lines changed

7 files changed

+494
-116
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use catalyst_toolbox::rewards::voters::calc_voter_rewards;
2+
use catalyst_toolbox::rewards::{Rewards, Threshold};
3+
use color_eyre::Report;
4+
use jcli_lib::jcli_lib::block::Common;
5+
use jormungandr_lib::{crypto::account::Identifier, interfaces::AccountVotes};
6+
use snapshot_lib::{registration::MainnetRewardAddress, SnapshotInfo};
7+
use structopt::StructOpt;
8+
use vit_servicing_station_lib::db::models::proposals::FullProposalInfo;
9+
10+
use std::collections::{BTreeMap, HashMap};
11+
use std::path::PathBuf;
12+
13+
#[derive(StructOpt)]
14+
#[structopt(rename_all = "kebab-case")]
15+
pub struct DrepsRewards {
16+
#[structopt(flatten)]
17+
common: Common,
18+
/// Reward (in dollars) to be distributed proportionally to delegated stake with respect to total stake.
19+
/// The total amount will only be awarded if dreps control all of the stake.
20+
#[structopt(long)]
21+
total_rewards: u64,
22+
23+
/// Path to a json encoded list of `SnapshotInfo`
24+
#[structopt(long)]
25+
snapshot_info_path: PathBuf,
26+
27+
/// Path to a json-encoded list of proposal every user has voted for.
28+
/// This can be retrived from the v1/account-votes-all endpoint exposed
29+
/// by a Jormungandr node.
30+
#[structopt(long)]
31+
votes_count_path: PathBuf,
32+
33+
/// Number of global votes required to be able to receive voter rewards
34+
#[structopt(long, default_value)]
35+
vote_threshold: u64,
36+
37+
/// Path to a json-encoded map from challenge id to an optional required threshold
38+
/// per-challenge in order to receive rewards.
39+
#[structopt(long)]
40+
per_challenge_threshold: Option<PathBuf>,
41+
42+
/// Path to the list of proposals active in this election.
43+
/// Can be obtained from /api/v0/proposals.
44+
#[structopt(long)]
45+
proposals: PathBuf,
46+
}
47+
48+
fn write_rewards_results(
49+
common: Common,
50+
rewards: &BTreeMap<MainnetRewardAddress, Rewards>,
51+
) -> Result<(), Report> {
52+
let writer = common.open_output()?;
53+
let header = ["Address", "Reward for the voter (lovelace)"];
54+
let mut csv_writer = csv::Writer::from_writer(writer);
55+
csv_writer.write_record(&header)?;
56+
57+
for (address, rewards) in rewards.iter() {
58+
let record = [address.to_string(), rewards.trunc().to_string()];
59+
csv_writer.write_record(&record)?;
60+
}
61+
62+
Ok(())
63+
}
64+
65+
impl DrepsRewards {
66+
pub fn exec(self) -> Result<(), Report> {
67+
let DrepsRewards {
68+
common,
69+
total_rewards,
70+
snapshot_info_path,
71+
votes_count_path,
72+
vote_threshold,
73+
per_challenge_threshold,
74+
proposals,
75+
} = self;
76+
77+
let proposals = serde_json::from_reader::<_, Vec<FullProposalInfo>>(
78+
jcli_lib::utils::io::open_file_read(&Some(proposals))?,
79+
)?;
80+
81+
let vote_count = super::extract_individual_votes(
82+
proposals.clone(),
83+
serde_json::from_reader::<_, HashMap<Identifier, Vec<AccountVotes>>>(
84+
jcli_lib::utils::io::open_file_read(&Some(votes_count_path))?,
85+
)?,
86+
)?;
87+
88+
let snapshot: Vec<SnapshotInfo> = serde_json::from_reader(
89+
jcli_lib::utils::io::open_file_read(&Some(snapshot_info_path))?,
90+
)?;
91+
92+
let additional_thresholds: HashMap<i32, usize> = if let Some(file) = per_challenge_threshold
93+
{
94+
serde_json::from_reader(jcli_lib::utils::io::open_file_read(&Some(file))?)?
95+
} else {
96+
HashMap::new()
97+
};
98+
99+
let results = calc_voter_rewards(
100+
vote_count,
101+
snapshot,
102+
Threshold::new(
103+
vote_threshold
104+
.try_into()
105+
.expect("vote threshold is too big"),
106+
additional_thresholds,
107+
proposals,
108+
)?,
109+
Rewards::from(total_rewards),
110+
)?;
111+
112+
write_rewards_results(common, &results)?;
113+
Ok(())
114+
}
115+
}

catalyst-toolbox/src/bin/cli/rewards/mod.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
11
mod community_advisors;
2+
mod dreps;
23
mod veterans;
34
mod voters;
45

5-
use color_eyre::Report;
6+
use catalyst_toolbox::rewards::VoteCount;
7+
use color_eyre::{eyre::eyre, Report};
8+
use jormungandr_lib::{
9+
crypto::{account::Identifier, hash::Hash},
10+
interfaces::AccountVotes,
11+
};
12+
use std::collections::HashMap;
613
use structopt::StructOpt;
14+
use vit_servicing_station_lib::db::models::proposals::FullProposalInfo;
715

816
#[derive(StructOpt)]
917
#[structopt(rename_all = "kebab-case")]
1018
pub enum Rewards {
1119
/// Calculate rewards for voters base on their stake
1220
Voters(voters::VotersRewards),
1321

22+
/// Calculate rewards for dreps based on their delegated stake
23+
Dreps(dreps::DrepsRewards),
24+
1425
/// Calculate community advisors rewards
1526
CommunityAdvisors(community_advisors::CommunityAdvisors),
1627

@@ -24,6 +35,48 @@ impl Rewards {
2435
Rewards::Voters(cmd) => cmd.exec(),
2536
Rewards::CommunityAdvisors(cmd) => cmd.exec(),
2637
Rewards::Veterans(cmd) => cmd.exec(),
38+
Rewards::Dreps(cmd) => cmd.exec(),
2739
}
2840
}
2941
}
42+
43+
fn extract_individual_votes(
44+
proposals: Vec<FullProposalInfo>,
45+
votes: HashMap<Identifier, Vec<AccountVotes>>,
46+
) -> Result<VoteCount, Report> {
47+
let proposals_per_voteplan =
48+
proposals
49+
.into_iter()
50+
.fold(<HashMap<_, Vec<_>>>::new(), |mut acc, prop| {
51+
let entry = acc
52+
.entry(prop.voteplan.chain_voteplan_id.clone())
53+
.or_default();
54+
entry.push(prop);
55+
entry.sort_by_key(|p| p.voteplan.chain_proposal_index);
56+
acc
57+
});
58+
59+
votes
60+
.into_iter()
61+
.try_fold(VoteCount::new(), |mut acc, (account, votes)| {
62+
for vote in &votes {
63+
let voteplan = vote.vote_plan_id;
64+
let props = proposals_per_voteplan
65+
.get(&voteplan.to_string())
66+
.iter()
67+
.flat_map(|p| p.iter())
68+
.enumerate()
69+
.filter(|(i, _p)| vote.votes.contains(&(*i as u8)))
70+
.map(|(_, p)| {
71+
Ok::<_, Report>(Hash::from(
72+
<[u8; 32]>::try_from(p.proposal.chain_proposal_id.clone()).map_err(
73+
|v| eyre!("Invalid proposal hash length {}, expected 32", v.len()),
74+
)?,
75+
))
76+
})
77+
.collect::<Result<Vec<_>, _>>()?;
78+
acc.entry(account.clone()).or_default().extend(props);
79+
}
80+
Ok::<_, Report>(acc)
81+
})
82+
}

catalyst-toolbox/src/bin/cli/rewards/voters.rs

Lines changed: 16 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
use catalyst_toolbox::rewards::voters::{calc_voter_rewards, Rewards, Threshold, VoteCount};
1+
use catalyst_toolbox::rewards::voters::calc_voter_rewards;
2+
use catalyst_toolbox::rewards::{Rewards, Threshold};
23
use catalyst_toolbox::utils::assert_are_close;
3-
use jormungandr_lib::{
4-
crypto::{account::Identifier, hash::Hash},
5-
interfaces::AccountVotes,
6-
};
7-
use snapshot_lib::{registration::MainnetRewardAddress, SnapshotInfo};
8-
use vit_servicing_station_lib::db::models::proposals::FullProposalInfo;
9-
10-
use color_eyre::{eyre::eyre, Report};
4+
use color_eyre::Report;
115
use jcli_lib::jcli_lib::block::Common;
6+
use jormungandr_lib::{crypto::account::Identifier, interfaces::AccountVotes};
7+
use snapshot_lib::{registration::MainnetRewardAddress, SnapshotInfo};
128
use structopt::StructOpt;
9+
use vit_servicing_station_lib::db::models::proposals::FullProposalInfo;
1310

1411
use std::collections::{BTreeMap, HashMap};
1512
use std::path::PathBuf;
@@ -78,43 +75,16 @@ impl VotersRewards {
7875
proposals,
7976
} = self;
8077

81-
let proposals_per_voteplan = serde_json::from_reader::<_, Vec<FullProposalInfo>>(
78+
let proposals = serde_json::from_reader::<_, Vec<FullProposalInfo>>(
8279
jcli_lib::utils::io::open_file_read(&Some(proposals))?,
83-
)?
84-
.into_iter()
85-
.fold(<HashMap<_, Vec<_>>>::new(), |mut acc, prop| {
86-
let entry = acc
87-
.entry(prop.voteplan.chain_voteplan_id.clone())
88-
.or_default();
89-
entry.push(prop);
90-
entry.sort_by_key(|p| p.voteplan.chain_proposal_index);
91-
acc
92-
});
93-
let vote_count = serde_json::from_reader::<_, HashMap<Identifier, Vec<AccountVotes>>>(
94-
jcli_lib::utils::io::open_file_read(&Some(votes_count_path))?,
95-
)?
96-
.into_iter()
97-
.try_fold(VoteCount::new(), |mut acc, (account, votes)| {
98-
for vote in &votes {
99-
let voteplan = vote.vote_plan_id;
100-
let props = proposals_per_voteplan
101-
.get(&voteplan.to_string())
102-
.iter()
103-
.flat_map(|p| p.iter())
104-
.enumerate()
105-
.filter(|(i, _p)| vote.votes.contains(&(*i as u8)))
106-
.map(|(_, p)| {
107-
Ok::<_, Report>(Hash::from(
108-
<[u8; 32]>::try_from(p.proposal.chain_proposal_id.clone()).map_err(
109-
|v| eyre!("Invalid proposal hash length {}, expected 32", v.len()),
110-
)?,
111-
))
112-
})
113-
.collect::<Result<Vec<_>, _>>()?;
114-
acc.entry(account.clone()).or_default().extend(props);
115-
}
116-
Ok::<_, Report>(acc)
117-
})?;
80+
)?;
81+
82+
let vote_count = super::extract_individual_votes(
83+
proposals.clone(),
84+
serde_json::from_reader::<_, HashMap<Identifier, Vec<AccountVotes>>>(
85+
jcli_lib::utils::io::open_file_read(&Some(votes_count_path))?,
86+
)?,
87+
)?;
11888

11989
let snapshot: Vec<SnapshotInfo> = serde_json::from_reader(
12090
jcli_lib::utils::io::open_file_read(&Some(snapshot_info_path))?,
@@ -130,14 +100,7 @@ impl VotersRewards {
130100
let results = calc_voter_rewards(
131101
vote_count,
132102
snapshot,
133-
Threshold::new(
134-
vote_threshold,
135-
additional_thresholds,
136-
proposals_per_voteplan
137-
.into_iter()
138-
.flat_map(|(_k, v)| v.into_iter())
139-
.collect(),
140-
)?,
103+
Threshold::new(vote_threshold, additional_thresholds, proposals)?,
141104
Rewards::from(total_rewards),
142105
)?;
143106

0 commit comments

Comments
 (0)