Merge pull request #17 from poanetwork/afck-refactor
Use VotingKeyChanged events to determine voter set; refactor.
This commit is contained in:
commit
d33cf1586f
24
README.md
24
README.md
|
@ -1,16 +1,22 @@
|
|||
# POA ballot stats
|
||||
|
||||
A command line tool that displays voting statistics for the [POA network](https://poa.network/).
|
||||
It requires a recent version of [Rust](https://www.rust-lang.org/), and needs to communicate with a
|
||||
fully synchronized node that is connected to the network:
|
||||
It and needs to communicate with a fully synchronized node that is connected to the network:
|
||||
[POA installation](https://github.com/poanetwork/wiki/wiki/POA-Installation).
|
||||
Note that `poa-ballot-stats` needs access to the network's full logs, so the node must run with
|
||||
`--pruning=archive --no-warp`.
|
||||
Initial requirements for the tool described in RFC9 "Statistics of ballots." https://github.com/poanetwork/RFC/issues/9
|
||||
|
||||
# Usage
|
||||
|
||||
## Stable release
|
||||
|
||||
Download the archive for your platform from the latest [release](https://github.com/poanetwork/poa-ballot-stats/releases) and unpack it. Run the tool with `./poa-ballot-stats <options>`.
|
||||
|
||||
You can view the command line options with `-h`, and specify a different endpoint if your node e.g.
|
||||
uses a non-standard port. By default, it tries to connect to a local node `http://127.0.0.1:8545`.
|
||||
In verbose mode, with `-v`, the complete list of collected events is displayed.
|
||||
|
||||
In verbose mode, with `-v`, the list of collected ballot and key change events is displayed, and for each ballot the list of participating and abstaining voters.
|
||||
|
||||
The `-c` option takes a map with the POA contracts' addresses in JSON format. You can find the
|
||||
current maps for the main and test network the `contracts` folder. By default, it uses `core.json`,
|
||||
|
@ -21,12 +27,16 @@ The `-p` option takes a time interval in hours, days, months, etc. E.g. `-p "10
|
|||
Examples:
|
||||
|
||||
```bash
|
||||
$ cargo run -- -h
|
||||
$ cargo run
|
||||
$ cargo run -- https://core.poa.network -v -p "10 weeks"
|
||||
$ cargo run -- -c contracts/sokol.json https://sokol.poa.network -v
|
||||
$ ./poa-ballot-stats -h
|
||||
$ ./poa-ballot-stats
|
||||
$ ./poa-ballot-stats https://core.poa.network -v -p "10 weeks"
|
||||
$ ./poa-ballot-stats -c contracts/sokol.json https://sokol.poa.network -v
|
||||
```
|
||||
|
||||
## Latest code
|
||||
|
||||
If you have a recent version of [Rust](https://www.rust-lang.org/), you can clone this repository and use `cargo run --` instead of `./poa-ballot-stats` to compile and run the latest version of the code.
|
||||
|
||||
## Screenshot
|
||||
|
||||
![Screenshot](screenshot.png)
|
||||
|
|
|
@ -0,0 +1,217 @@
|
|||
use colored::{Color, Colorize};
|
||||
use contracts::{key_mgr, val_meta, voting};
|
||||
use error::{Error, ErrorKind};
|
||||
use ethabi::Address;
|
||||
use stats::Stats;
|
||||
use std::collections::BTreeSet;
|
||||
use std::default::Default;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use util::{self, HexList, TopicFilterExt, Web3LogExt};
|
||||
use web3;
|
||||
use web3::futures::Future;
|
||||
|
||||
/// The maximum age in seconds of the latest block.
|
||||
const MAX_BLOCK_AGE: u64 = 60 * 60;
|
||||
|
||||
const ERR_BLOCK_NUM: &str = "event is missing block number";
|
||||
const ERR_ADDR: &str = "parse contract address";
|
||||
const ERR_EPOCH: &str = "current timestamp is earlier than the Unix epoch";
|
||||
const ERR_BLOCK: &str = "failed to retrieve block";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub struct ContractAddresses {
|
||||
metadata_address: String,
|
||||
keys_manager_address: String,
|
||||
voting_to_change_keys_address: String,
|
||||
}
|
||||
|
||||
/// A vote counter, to read ballot statistics from the blockchain.
|
||||
pub struct Counter {
|
||||
verbose: bool,
|
||||
start_time: SystemTime,
|
||||
start_block: u64,
|
||||
val_meta_addr: Address,
|
||||
key_mgr_addr: Address,
|
||||
voting_addr: Address,
|
||||
web3: web3::Web3<web3::transports::Http>,
|
||||
_eloop: web3::transports::EventLoopHandle,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
/// Creates a new vote counter.
|
||||
pub fn new(url: &str, contract_addrs: &ContractAddresses) -> Counter {
|
||||
let (_eloop, transport) = web3::transports::Http::new(url).unwrap();
|
||||
let web3 = web3::Web3::new(transport);
|
||||
|
||||
let val_meta_addr = util::parse_address(&contract_addrs.metadata_address).expect(ERR_ADDR);
|
||||
let key_mgr_addr =
|
||||
util::parse_address(&contract_addrs.keys_manager_address).expect(ERR_ADDR);
|
||||
let voting_addr =
|
||||
util::parse_address(&contract_addrs.voting_to_change_keys_address).expect(ERR_ADDR);
|
||||
|
||||
Counter {
|
||||
verbose: false,
|
||||
start_time: UNIX_EPOCH,
|
||||
start_block: 0,
|
||||
val_meta_addr,
|
||||
key_mgr_addr,
|
||||
voting_addr,
|
||||
web3,
|
||||
_eloop,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables verbose mode.
|
||||
pub fn set_verbose(&mut self) {
|
||||
self.verbose = true;
|
||||
}
|
||||
|
||||
/// Sets the first block to be taken into account, by creation time.
|
||||
pub fn set_start_time(&mut self, start_time: SystemTime) {
|
||||
self.start_time = start_time;
|
||||
}
|
||||
|
||||
/// Sets the first block to be taken into account, by number.
|
||||
pub fn set_start_block(&mut self, start_block: u64) {
|
||||
self.start_block = start_block;
|
||||
}
|
||||
|
||||
/// Finds all logged ballots and returns statistics about how many were missed by each voter.
|
||||
pub fn count_votes(&mut self) -> Result<Stats, Error> {
|
||||
self.check_synced();
|
||||
|
||||
// Calls `println!` if `verbose` is `true`.
|
||||
macro_rules! vprintln { ($($arg:tt)*) => { if self.verbose { println!($($arg)*); } } }
|
||||
|
||||
let voting_contract = voting::VotingToChangeKeys::default();
|
||||
let val_meta_contract = val_meta::ValidatorMetadata::default();
|
||||
let key_mgr_contract = key_mgr::KeysManager::default();
|
||||
|
||||
let ballot_event = voting_contract.events().ballot_created();
|
||||
let vote_event = voting_contract.events().vote();
|
||||
let change_event = key_mgr_contract.events().voting_key_changed();
|
||||
|
||||
// Find all ballots and voter changes. We don't filter by contract address, so we can make
|
||||
// a single pass. Contract addresses are checked inside the loop.
|
||||
let ballot_or_change_filter =
|
||||
(ballot_event.create_filter(None, None, None)).or(change_event.create_filter(None));
|
||||
|
||||
let mut voters: BTreeSet<Address> = BTreeSet::new();
|
||||
let mut stats = Stats::default();
|
||||
|
||||
vprintln!("Collecting events…");
|
||||
let mut event_found = false;
|
||||
|
||||
// Iterate over all ballot and voter change events.
|
||||
for log in ballot_or_change_filter.logs(&self.web3)? {
|
||||
event_found = true;
|
||||
let block_num = log.block_number.expect(ERR_BLOCK_NUM).into();
|
||||
if let Ok(change) = change_event.parse_log(log.clone().into_raw()) {
|
||||
if log.address != self.key_mgr_addr {
|
||||
continue; // Event from another contract instance.
|
||||
}
|
||||
// If it is a `VotingKeyChanged`, update the current set of voters.
|
||||
vprintln!("• {} {:?}", format!("#{}", block_num).bold(), change);
|
||||
match change.action.as_str() {
|
||||
"added" => {
|
||||
voters.insert(change.key);
|
||||
}
|
||||
"removed" => {
|
||||
voters.remove(&change.key);
|
||||
}
|
||||
_ => vprintln!(" Unexpected key change action."),
|
||||
}
|
||||
} else if let Ok(ballot) = ballot_event.parse_log(log.clone().into_raw()) {
|
||||
if log.address != self.voting_addr {
|
||||
continue; // Event from another contract instance.
|
||||
}
|
||||
if block_num < self.start_block || self.is_block_too_old(block_num) {
|
||||
let num = format!("#{}", block_num);
|
||||
vprintln!("• {} Ballot too old; skipping: {:?}", num.bold(), ballot);
|
||||
continue;
|
||||
}
|
||||
// If it is a `BallotCreated`, find the corresponding votes and update the stats.
|
||||
vprintln!("• {} {:?}", format!("#{}", block_num).bold(), ballot);
|
||||
let mut unexpected = BTreeSet::new();
|
||||
let mut voted = BTreeSet::new();
|
||||
let mut votes = Vec::new();
|
||||
for vote_log in vote_event.create_filter(ballot.id, None).logs(&self.web3)? {
|
||||
let vote = vote_event.parse_log(vote_log.into_raw())?;
|
||||
if voters.insert(vote.voter) {
|
||||
unexpected.insert(vote.voter);
|
||||
} else {
|
||||
voted.insert(vote.voter);
|
||||
}
|
||||
votes.push(vote);
|
||||
}
|
||||
let missed_filter =
|
||||
|voter: &&Address| !votes.iter().any(|vote| vote.voter == **voter);
|
||||
let missed: BTreeSet<_> = voters.iter().filter(missed_filter).collect();
|
||||
if !missed.is_empty() {
|
||||
vprintln!(" Missed: {}", HexList(&missed, Color::Red));
|
||||
}
|
||||
if !voted.is_empty() {
|
||||
vprintln!(" Voted: {}", HexList(&voted, Color::Green));
|
||||
}
|
||||
if !unexpected.is_empty() {
|
||||
vprintln!(" Unexpected: {}", HexList(&unexpected, Color::Yellow));
|
||||
}
|
||||
stats.add_ballot(&voters, &votes);
|
||||
} else {
|
||||
return Err(ErrorKind::UnexpectedLogParams.into());
|
||||
}
|
||||
}
|
||||
|
||||
if !event_found {
|
||||
return Err(ErrorKind::NoEventsFound.into());
|
||||
}
|
||||
|
||||
vprintln!(""); // Add a new line between event log and table.
|
||||
|
||||
// Finally, gather the metadata for all voters.
|
||||
let raw_call = util::raw_call(self.val_meta_addr, self.web3.eth());
|
||||
let get_mining_by_voting_key_fn = val_meta_contract.functions().get_mining_by_voting_key();
|
||||
let validators_fn = val_meta_contract.functions().validators();
|
||||
for voter in voters {
|
||||
let mining_key = match get_mining_by_voting_key_fn.call(voter, &*raw_call) {
|
||||
Err(err) => {
|
||||
eprintln!("Failed to find mining key for voter {}: {:?}", voter, err);
|
||||
continue;
|
||||
}
|
||||
Ok(key) => key,
|
||||
};
|
||||
if mining_key.is_zero() {
|
||||
eprintln!("Mining key for voter {} is zero. Skipping.", voter);
|
||||
continue;
|
||||
}
|
||||
let validator = validators_fn.call(mining_key, &*raw_call)?.into();
|
||||
stats.set_metadata(&voter, mining_key, validator);
|
||||
}
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
/// Returns `true` if the block with the given number is older than `start_time`.
|
||||
fn is_block_too_old(&self, block_num: u64) -> bool {
|
||||
self.is_block_older_than(
|
||||
web3::types::BlockNumber::Number(block_num),
|
||||
&self.start_time,
|
||||
)
|
||||
}
|
||||
|
||||
/// Shows a warning if the node's latest block is outdated.
|
||||
fn check_synced(&self) {
|
||||
let min_time = SystemTime::now() - Duration::from_secs(MAX_BLOCK_AGE);
|
||||
if self.is_block_older_than(web3::types::BlockNumber::Latest, &min_time) {
|
||||
eprintln!("WARNING: The node is not fully synchronized. Stats may be inaccurate.");
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the block with the given number was created before the given time.
|
||||
fn is_block_older_than(&self, number: web3::types::BlockNumber, time: &SystemTime) -> bool {
|
||||
let id = web3::types::BlockId::Number(number);
|
||||
let block = self.web3.eth().block(id).wait().expect(ERR_BLOCK);
|
||||
let seconds = time.duration_since(UNIX_EPOCH).expect(ERR_EPOCH).as_secs();
|
||||
block.timestamp < seconds.into()
|
||||
}
|
||||
}
|
221
src/main.rs
221
src/main.rs
|
@ -15,32 +15,18 @@ extern crate serde_json;
|
|||
extern crate web3;
|
||||
|
||||
mod cli;
|
||||
mod counter;
|
||||
mod error;
|
||||
mod stats;
|
||||
mod util;
|
||||
mod validator;
|
||||
|
||||
use colored::Colorize;
|
||||
use error::{Error, ErrorKind};
|
||||
use ethabi::Address;
|
||||
use stats::Stats;
|
||||
use std::default::Default;
|
||||
use std::fs::File;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use util::{HexBytes, HexList, TopicFilterExt, Web3LogExt};
|
||||
use web3::futures::Future;
|
||||
|
||||
/// The maximum age in seconds of the latest block.
|
||||
const MAX_BLOCK_AGE: u64 = 60 * 60;
|
||||
use std::time::SystemTime;
|
||||
|
||||
// The `use_contract!` macro triggers several Clippy warnings.
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(too_many_arguments, redundant_closure, needless_update))]
|
||||
mod contracts {
|
||||
use_contract!(
|
||||
net_con,
|
||||
"NetworkConsensus",
|
||||
"abi/PoaNetworkConsensus.abi.json"
|
||||
);
|
||||
use_contract!(
|
||||
voting,
|
||||
"VotingToChangeKeys",
|
||||
|
@ -56,193 +42,36 @@ mod contracts {
|
|||
|
||||
use contracts::*;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
struct ContractAddresses {
|
||||
metadata_address: String,
|
||||
keys_manager_address: String,
|
||||
}
|
||||
|
||||
/// Returns `true` if the block with the given number was created before the given time.
|
||||
fn is_block_older_than<T: web3::Transport>(
|
||||
web3: &web3::Web3<T>,
|
||||
number: web3::types::BlockNumber,
|
||||
time: &SystemTime,
|
||||
) -> bool {
|
||||
let id = web3::types::BlockId::Number(number);
|
||||
let block = web3.eth().block(id).wait().expect("get block");
|
||||
let seconds = time
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Current timestamp is earlier than the Unix epoch!")
|
||||
.as_secs();
|
||||
block.timestamp < seconds.into()
|
||||
}
|
||||
|
||||
/// Shows a warning if the node's latest block is outdated.
|
||||
fn check_synced<T: web3::Transport>(web3: &web3::Web3<T>) {
|
||||
let min_time = SystemTime::now() - Duration::from_secs(MAX_BLOCK_AGE);
|
||||
if is_block_older_than(web3, web3::types::BlockNumber::Latest, &min_time) {
|
||||
eprintln!("WARNING: The node is not fully synchronized. Stats may be inaccurate.");
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds all logged ballots and returns statistics about how many were missed by each voter.
|
||||
fn count_votes(
|
||||
url: &str,
|
||||
verbose: bool,
|
||||
contract_addrs: &ContractAddresses,
|
||||
start: SystemTime,
|
||||
start_block: u64,
|
||||
) -> Result<Stats, Error> {
|
||||
// Calls `println!` if `verbose` is `true`.
|
||||
macro_rules! vprintln { ($($arg:tt)*) => { if verbose { println!($($arg)*); } } }
|
||||
|
||||
let (_eloop, transport) = web3::transports::Http::new(url).unwrap();
|
||||
let web3 = web3::Web3::new(transport);
|
||||
|
||||
check_synced(&web3);
|
||||
|
||||
let voting_contract = voting::VotingToChangeKeys::default();
|
||||
let net_con_contract = net_con::NetworkConsensus::default();
|
||||
let val_meta_contract = val_meta::ValidatorMetadata::default();
|
||||
let key_mgr_contract = key_mgr::KeysManager::default();
|
||||
|
||||
let val_meta_addr =
|
||||
util::parse_address(&contract_addrs.metadata_address).expect("parse contract address");
|
||||
let key_mgr_addr =
|
||||
util::parse_address(&contract_addrs.keys_manager_address).expect("parse contract address");
|
||||
|
||||
let ballot_event = voting_contract.events().ballot_created();
|
||||
let vote_event = voting_contract.events().vote();
|
||||
let change_event = net_con_contract.events().change_finalized();
|
||||
let init_change_event = net_con_contract.events().initiate_change();
|
||||
|
||||
// Find all ballots and voter changes.
|
||||
let ballot_or_change_filter = (ballot_event.create_filter(None, None, None))
|
||||
.or(change_event.create_filter())
|
||||
.or(init_change_event.create_filter(None));
|
||||
|
||||
// FIXME: Find out why we see no `ChangeFinalized` events, and how to obtain the initial voters.
|
||||
let mut voters: Vec<Address> = Vec::new();
|
||||
let mut stats = Stats::default();
|
||||
let mut prev_init_change: Option<net_con::logs::InitiateChange> = None;
|
||||
|
||||
vprintln!("Collecting events…");
|
||||
let mut event_found = false;
|
||||
|
||||
// Iterate over all ballot and voter change events.
|
||||
for log in ballot_or_change_filter.logs(&web3)? {
|
||||
event_found = true;
|
||||
let block_num = log
|
||||
.block_number
|
||||
.expect("event is missing block number")
|
||||
.into();
|
||||
if let Ok(change) = change_event.parse_log(log.clone().into_raw()) {
|
||||
// If it is a `ChangeFinalized`, update the current set of voters.
|
||||
vprintln!(
|
||||
"• {} ChangeFinalized {{ new_set: {} }}",
|
||||
format!("#{}", block_num).bold(),
|
||||
HexList(&change.new_set)
|
||||
);
|
||||
voters = change.new_set;
|
||||
} else if let Ok(init_change) = init_change_event.parse_log(log.clone().into_raw()) {
|
||||
// If it is an `InitiateChange`, update the current set of voters.
|
||||
vprintln!(
|
||||
"• {} InitiateChange {{ parent_hash: {}, new_set: {} }}",
|
||||
format!("#{}", block_num).bold(),
|
||||
HexBytes(&init_change.parent_hash),
|
||||
HexList(&init_change.new_set)
|
||||
);
|
||||
if let Some(prev) = prev_init_change.take() {
|
||||
let raw_call = util::raw_call(key_mgr_addr, web3.eth());
|
||||
let get_voting_by_mining_fn = key_mgr_contract.functions().get_voting_by_mining();
|
||||
voters = vec![];
|
||||
for mining_key in prev.new_set {
|
||||
let voter = get_voting_by_mining_fn.call(mining_key, &*raw_call)?;
|
||||
if voter != Address::zero() {
|
||||
voters.push(voter);
|
||||
}
|
||||
}
|
||||
}
|
||||
prev_init_change = Some(init_change);
|
||||
} else if let Ok(ballot) = ballot_event.parse_log(log.clone().into_raw()) {
|
||||
let block_number = web3::types::BlockNumber::Number(block_num);
|
||||
if block_num < start_block || is_block_older_than(&web3, block_number, &start) {
|
||||
vprintln!(
|
||||
"• {} Ballot event too old; skipping: {:?}",
|
||||
format!("#{}", block_num).bold(),
|
||||
ballot
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// If it is a `BallotCreated`, find the corresponding votes and update the stats.
|
||||
vprintln!("• {} {:?}", format!("#{}", block_num).bold(), ballot);
|
||||
let votes = vote_event
|
||||
.create_filter(ballot.id, None)
|
||||
.logs(&web3)?
|
||||
.into_iter()
|
||||
.map(|vote_log| {
|
||||
let vote = vote_event.parse_log(vote_log.into_raw())?;
|
||||
if !voters.contains(&vote.voter) {
|
||||
vprintln!(" Unexpected voter {}", vote.voter);
|
||||
voters.push(vote.voter);
|
||||
}
|
||||
Ok(vote)
|
||||
})
|
||||
.collect::<Result<Vec<_>, Error>>()?;
|
||||
stats.add_ballot(&voters, &votes);
|
||||
} else {
|
||||
return Err(ErrorKind::UnexpectedLogParams.into());
|
||||
}
|
||||
}
|
||||
|
||||
if !event_found {
|
||||
return Err(ErrorKind::NoEventsFound.into());
|
||||
}
|
||||
|
||||
vprintln!(""); // Add a new line between event log and table.
|
||||
|
||||
// Finally, gather the metadata for all voters.
|
||||
let raw_call = util::raw_call(val_meta_addr, web3.eth());
|
||||
let get_mining_by_voting_key_fn = val_meta_contract.functions().get_mining_by_voting_key();
|
||||
let validators_fn = val_meta_contract.functions().validators();
|
||||
for voter in voters {
|
||||
let mining_key = match get_mining_by_voting_key_fn.call(voter, &*raw_call) {
|
||||
Err(err) => {
|
||||
eprintln!("Failed to find mining key for voter {}: {:?}", voter, err);
|
||||
continue;
|
||||
}
|
||||
Ok(key) => key,
|
||||
};
|
||||
let validator = validators_fn.call(mining_key, &*raw_call)?.into();
|
||||
stats.set_metadata(&voter, mining_key, validator);
|
||||
}
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let matches = cli::get_matches();
|
||||
|
||||
let url = matches.value_of("url").unwrap_or("http://127.0.0.1:8545");
|
||||
let verbose = matches.is_present("verbose");
|
||||
let contract_file = matches
|
||||
.value_of("contracts")
|
||||
.unwrap_or("contracts/core.json");
|
||||
let file = File::open(contract_file).expect("open contracts file");
|
||||
let contract_addrs = serde_json::from_reader(file).expect("parse contracts file");
|
||||
let start = matches
|
||||
.value_of("period")
|
||||
.map(|period| {
|
||||
let duration = parse_duration::parse(period)
|
||||
.expect("period must be in the format '5 days', '2 months', etc.");
|
||||
SystemTime::now() - duration
|
||||
})
|
||||
.unwrap_or(UNIX_EPOCH);
|
||||
let start_block = matches.value_of("block").map_or(0, |block| {
|
||||
block
|
||||
.parse()
|
||||
.expect("block number must be a non-negative integer")
|
||||
});
|
||||
let stats =
|
||||
count_votes(url, verbose, &contract_addrs, start, start_block).expect("count votes");
|
||||
|
||||
let mut counter = counter::Counter::new(url, &contract_addrs);
|
||||
|
||||
if matches.is_present("verbose") {
|
||||
counter.set_verbose();
|
||||
}
|
||||
|
||||
if let Some(period) = matches.value_of("period") {
|
||||
let duration = parse_duration::parse(period)
|
||||
.expect("period must be in the format '5 days', '2 months', etc.");
|
||||
counter.set_start_time(SystemTime::now() - duration);
|
||||
}
|
||||
|
||||
if let Some(start_block) = matches.value_of("block") {
|
||||
counter.set_start_block(
|
||||
start_block
|
||||
.parse()
|
||||
.expect("block number must be a non-negative integer"),
|
||||
);
|
||||
}
|
||||
|
||||
let stats = counter.count_votes().expect("count votes");
|
||||
println!("{}", stats);
|
||||
}
|
||||
|
|
|
@ -27,7 +27,10 @@ pub struct Stats {
|
|||
impl Stats {
|
||||
/// Adds a ballot: `voters` are the voting keys of everyone who was allowed to cast a vote, and
|
||||
/// `votes` are the ones that were actually cast.
|
||||
pub fn add_ballot(&mut self, voters: &[Address], votes: &[voting::logs::Vote]) {
|
||||
pub fn add_ballot<'a, I>(&mut self, voters: I, votes: &[voting::logs::Vote])
|
||||
where
|
||||
I: IntoIterator<Item = &'a Address>,
|
||||
{
|
||||
for voter in voters {
|
||||
let mut vs = self
|
||||
.voter_stats
|
||||
|
|
13
src/util.rs
13
src/util.rs
|
@ -1,3 +1,4 @@
|
|||
use colored::{Color, Colorize};
|
||||
use ethabi::{self, Address, Bytes};
|
||||
use std::str::FromStr;
|
||||
use std::{fmt, u8};
|
||||
|
@ -150,21 +151,21 @@ impl<'a> fmt::Display for HexBytes<'a> {
|
|||
|
||||
/// Wrapper for a list of byte arrays, whose `Display` implementation outputs shortened hexadecimal
|
||||
/// strings.
|
||||
pub struct HexList<'a, T: 'a>(pub &'a [T]);
|
||||
pub struct HexList<'a, T: 'a, I: IntoIterator<Item = &'a T>>(pub I, pub Color);
|
||||
|
||||
impl<'a, T: 'a> fmt::Display for HexList<'a, T>
|
||||
impl<'a, T: 'a, I: IntoIterator<Item = &'a T> + Clone> fmt::Display for HexList<'a, T, I>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "[")?;
|
||||
for (i, item) in self.0.iter().enumerate() {
|
||||
for (i, item) in self.0.clone().into_iter().enumerate() {
|
||||
if i > 0 {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
write!(f, "{}", HexBytes(item.as_ref()))?;
|
||||
let item = format!("{}", HexBytes(item.as_ref()));
|
||||
write!(f, "{}", item.color(self.1))?;
|
||||
}
|
||||
write!(f, "]")
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue