Initial commit.
This commit is contained in:
commit
79723c1a31
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
/target
|
||||||
|
**/*.rs.bk
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "poa-ballot-stats"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Andreas Fackler <AndreasFackler@gmx.de>"]
|
||||||
|
description = "Read POA voting records and rank voters by how many ballots they missed."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = "2.31.2"
|
||||||
|
colored = "1.6.0"
|
||||||
|
error-chain = { version = "0.11", default-features = false }
|
||||||
|
ethabi = "5.1.1"
|
||||||
|
web3 = "0.3.0"
|
|
@ -0,0 +1,19 @@
|
||||||
|
# POA ballot stats
|
||||||
|
|
||||||
|
**Note**: This is still work in progress. It doesn't yet correctly determine the initial set of
|
||||||
|
validators.
|
||||||
|
|
||||||
|
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:
|
||||||
|
[POA installation](https://github.com/poanetwork/wiki/wiki/POA-Installation).
|
||||||
|
|
||||||
|
With the default setup, it should work without any additional 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.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cargo run
|
||||||
|
$ cargo run -- -h
|
||||||
|
$ cargo run -- http://127.0.0.1:8545
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
[{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"pendingList","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCurrentValidatorsLength","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_newAddress","type":"address"}],"name":"setProxyStorage","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_validator","type":"address"},{"name":"_shouldFireEvent","type":"bool"}],"name":"addValidator","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"currentValidatorsLength","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"validatorsState","outputs":[{"name":"isValidator","type":"bool"},{"name":"index","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getPendingList","outputs":[{"name":"","type":"address[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getVotingToChangeKeys","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"finalizeChange","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_newKey","type":"address"},{"name":"_oldKey","type":"address"}],"name":"swapValidatorKey","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"currentValidators","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getKeysManager","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isMasterOfCeremonyInitialized","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"proxyStorage","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"finalized","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getValidators","outputs":[{"name":"","type":"address[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"systemAddress","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_validator","type":"address"},{"name":"_shouldFireEvent","type":"bool"}],"name":"removeValidator","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"masterOfCeremony","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_someone","type":"address"}],"name":"isValidator","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_masterOfCeremony","type":"address"},{"name":"validators","type":"address[]"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"parentHash","type":"bytes32"},{"indexed":false,"name":"newSet","type":"address[]"}],"name":"InitiateChange","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"newSet","type":"address[]"}],"name":"ChangeFinalized","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"nameOfContract","type":"string"},{"indexed":false,"name":"newAddress","type":"address"}],"name":"ChangeReference","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"proxyStorage","type":"address"}],"name":"MoCInitializedProxyStorage","type":"event"}]
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,27 @@
|
||||||
|
use clap::{App, Arg, ArgMatches};
|
||||||
|
|
||||||
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
const AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
||||||
|
const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
||||||
|
|
||||||
|
/// Returns the matched command line arguments.
|
||||||
|
pub fn get_matches() -> ArgMatches<'static> {
|
||||||
|
App::new("POA ballot statistics")
|
||||||
|
.author(AUTHORS)
|
||||||
|
.version(VERSION)
|
||||||
|
.about(DESCRIPTION)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("url")
|
||||||
|
.value_name("URL")
|
||||||
|
.help("The JSON-RPC endpoint")
|
||||||
|
.takes_value(true),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("verbose")
|
||||||
|
.short("v")
|
||||||
|
.long("verbose")
|
||||||
|
.help("More detailed output")
|
||||||
|
.takes_value(false),
|
||||||
|
)
|
||||||
|
.get_matches()
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
use ethabi;
|
||||||
|
use web3;
|
||||||
|
|
||||||
|
error_chain! {
|
||||||
|
foreign_links {
|
||||||
|
Ethabi(ethabi::Error);
|
||||||
|
Web3(web3::Error);
|
||||||
|
Contract(web3::contract::Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
errors {
|
||||||
|
UnexpectedLogParams {
|
||||||
|
description("Unexpected parameter types in log"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
use error::{ErrorKind, Result};
|
||||||
|
use ethabi::{Address, Log, RawTopicFilter, Token, Topic, Uint};
|
||||||
|
use util::LogExt;
|
||||||
|
|
||||||
|
/// An event that is logged when the current set of validators has changed.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ChangeFinalized {
|
||||||
|
/// The new set of validators.
|
||||||
|
pub new_set: Vec<Address>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChangeFinalized {
|
||||||
|
/// Parses the log and returns a `ChangeFinalized`, if the log corresponded to such an event.
|
||||||
|
pub fn from_log(log: &Log) -> Result<ChangeFinalized> {
|
||||||
|
log.param(0, "newSet")
|
||||||
|
.cloned()
|
||||||
|
.and_then(Token::to_array)
|
||||||
|
.map(|tokens| ChangeFinalized {
|
||||||
|
new_set: tokens.into_iter().filter_map(Token::to_address).collect(),
|
||||||
|
})
|
||||||
|
.ok_or_else(|| ErrorKind::UnexpectedLogParams.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An event that is logged when a new ballot is started.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BallotCreated {
|
||||||
|
/// The ballot ID.
|
||||||
|
pub id: Uint,
|
||||||
|
/// The ballot type.
|
||||||
|
ballot_type: Uint,
|
||||||
|
/// The creator's voting key.
|
||||||
|
creator: Address,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BallotCreated {
|
||||||
|
/// Parses the log and returns a `BallotCreated`, if the log corresponded to such an event.
|
||||||
|
pub fn from_log(log: &Log) -> Result<BallotCreated> {
|
||||||
|
match (
|
||||||
|
log.uint_param(0, "id"),
|
||||||
|
log.uint_param(1, "ballotType"),
|
||||||
|
log.address_param(2, "creator"),
|
||||||
|
) {
|
||||||
|
(Some(&id), Some(&ballot_type), Some(&creator)) => Ok(BallotCreated {
|
||||||
|
id,
|
||||||
|
ballot_type,
|
||||||
|
creator,
|
||||||
|
}),
|
||||||
|
_ => Err(ErrorKind::UnexpectedLogParams.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a topic filter to find the votes corresponding to this ballot.
|
||||||
|
pub fn vote_topic_filter(&self) -> RawTopicFilter {
|
||||||
|
RawTopicFilter {
|
||||||
|
topic0: Topic::This(Token::Uint(self.id)),
|
||||||
|
..RawTopicFilter::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An event that is logged whenever someone casts a vote in a ballot.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Vote {
|
||||||
|
/// The ballot ID.
|
||||||
|
id: Uint,
|
||||||
|
/// The decision this vote is for.
|
||||||
|
decision: Uint,
|
||||||
|
/// The voter's voting key.
|
||||||
|
pub voter: Address,
|
||||||
|
/// The timestamp of this vote.
|
||||||
|
time: Uint,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vote {
|
||||||
|
/// Parses the log and returns a `Vote`, if the log corresponded to such an event.
|
||||||
|
pub fn from_log(log: &Log) -> Result<Vote> {
|
||||||
|
match (
|
||||||
|
log.uint_param(0, "id"),
|
||||||
|
log.uint_param(1, "decision"),
|
||||||
|
log.address_param(2, "voter"),
|
||||||
|
log.uint_param(3, "time"),
|
||||||
|
) {
|
||||||
|
(Some(&id), Some(&decision), Some(&voter), Some(&time)) => Ok(Vote {
|
||||||
|
id,
|
||||||
|
decision,
|
||||||
|
voter,
|
||||||
|
time,
|
||||||
|
}),
|
||||||
|
_ => Err(ErrorKind::UnexpectedLogParams.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
extern crate clap;
|
||||||
|
extern crate colored;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate error_chain;
|
||||||
|
extern crate ethabi;
|
||||||
|
extern crate web3;
|
||||||
|
|
||||||
|
mod cli;
|
||||||
|
mod error;
|
||||||
|
mod events;
|
||||||
|
mod stats;
|
||||||
|
mod util;
|
||||||
|
mod validator;
|
||||||
|
|
||||||
|
use error::{Error, ErrorKind};
|
||||||
|
use events::{BallotCreated, ChangeFinalized, Vote};
|
||||||
|
use stats::Stats;
|
||||||
|
use std::fs::File;
|
||||||
|
use util::{ContractExt, TopicFilterExt, Web3LogExt};
|
||||||
|
use web3::futures::Future;
|
||||||
|
|
||||||
|
// TODO: `ethabi_derive` produces unparseable tokens.
|
||||||
|
// mod voting_to_change_keys {
|
||||||
|
// #[derive(EthabiContract)]
|
||||||
|
// #[ethabi_contract_options(name = "VotingToChangeKeys", path = "abi/VotingToChangeKeys.json")]
|
||||||
|
// struct _Dummy;
|
||||||
|
// }
|
||||||
|
|
||||||
|
/// Finds all logged ballots and returns statistics about how many were missed by each voter.
|
||||||
|
fn count_votes(
|
||||||
|
url: &str,
|
||||||
|
verbose: bool,
|
||||||
|
voting_abi: &File,
|
||||||
|
net_con_abi: &File,
|
||||||
|
val_meta_abi: &File,
|
||||||
|
) -> Result<Stats, Error> {
|
||||||
|
let (_eloop, transport) = web3::transports::Http::new(url).unwrap();
|
||||||
|
let web3 = web3::Web3::new(transport);
|
||||||
|
let voting_contract = ethabi::Contract::load(voting_abi)?;
|
||||||
|
let net_con_contract = ethabi::Contract::load(net_con_abi)?;
|
||||||
|
let val_meta_contract = ethabi::Contract::load(val_meta_abi)?;
|
||||||
|
|
||||||
|
// TODO: Read contract addresses from chain spec.
|
||||||
|
let val_meta_addr = util::parse_address("0xfb9c7fC2a00DfFc53948e3bbeb11F3D4b56C31B8").unwrap();
|
||||||
|
let web3_val_meta = web3::contract::Contract::new(web3.eth(), val_meta_addr, val_meta_contract);
|
||||||
|
|
||||||
|
let ballot_event = voting_contract.event("BallotCreated")?;
|
||||||
|
let vote_event = voting_contract.event("Vote")?;
|
||||||
|
let change_event = net_con_contract.event("ChangeFinalized")?;
|
||||||
|
|
||||||
|
// Find all ballots and voter changes.
|
||||||
|
let ballot_or_change_filter = ethabi::TopicFilter {
|
||||||
|
topic0: ethabi::Topic::OneOf(vec![ballot_event.signature(), change_event.signature()]),
|
||||||
|
..ethabi::TopicFilter::default()
|
||||||
|
}.to_filter_builder()
|
||||||
|
.build();
|
||||||
|
let ballot_change_logs_filter = web3.eth_filter()
|
||||||
|
.create_logs_filter(ballot_or_change_filter)
|
||||||
|
.wait()?;
|
||||||
|
|
||||||
|
// FIXME: Find out why we see no `ChangeFinalized` events, and how to obtain the initial voters.
|
||||||
|
let mut voters: Vec<ethabi::Address> = Vec::new();
|
||||||
|
let mut stats = Stats::default();
|
||||||
|
|
||||||
|
// Iterate over all ballot and voter change events.
|
||||||
|
for log in ballot_change_logs_filter.logs().wait()? {
|
||||||
|
if let Ok(change_log) = change_event.parse_log(log.clone().into_raw()) {
|
||||||
|
// If it is a `ChangeFinalized`, update the current set of voters.
|
||||||
|
let change = ChangeFinalized::from_log(&change_log)?;
|
||||||
|
if verbose {
|
||||||
|
println!("{:?}", change);
|
||||||
|
}
|
||||||
|
voters = change.new_set;
|
||||||
|
} else if let Ok(ballot_log) = ballot_event.parse_log(log.into_raw()) {
|
||||||
|
// If it is a `BallotCreated`, find the corresponding votes and update the stats.
|
||||||
|
let ballot = BallotCreated::from_log(&ballot_log)?;
|
||||||
|
if verbose {
|
||||||
|
println!("{:?}", ballot);
|
||||||
|
}
|
||||||
|
let vote_filter = vote_event
|
||||||
|
.create_filter(ballot.vote_topic_filter())?
|
||||||
|
.to_filter_builder()
|
||||||
|
.build();
|
||||||
|
let vote_logs_filter = web3.eth_filter().create_logs_filter(vote_filter).wait()?;
|
||||||
|
let mut votes: Vec<Vote> = Vec::new();
|
||||||
|
for vote_log in vote_logs_filter.logs().wait()? {
|
||||||
|
let vote = Vote::from_log(&vote_event.parse_log(vote_log.into_raw())?)?;
|
||||||
|
if !voters.contains(&vote.voter) {
|
||||||
|
eprintln!("Unexpected voter {} for ballot {}", vote.voter, ballot.id);
|
||||||
|
voters.push(vote.voter);
|
||||||
|
}
|
||||||
|
votes.push(vote);
|
||||||
|
}
|
||||||
|
stats.add_ballot(&voters, &votes);
|
||||||
|
} else {
|
||||||
|
return Err(ErrorKind::UnexpectedLogParams.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, gather the metadata for all voters.
|
||||||
|
for voter in voters {
|
||||||
|
let mining_key = web3_val_meta.simple_query("getMiningByVotingKey", voter)?;
|
||||||
|
let validator = web3_val_meta.simple_query("validators", mining_key)?;
|
||||||
|
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 voting_abi = File::open("abi/VotingToChangeKeys.json").expect("read voting abi");
|
||||||
|
let net_con_abi = File::open("abi/PoaNetworkConsensus.json").expect("read consensus abi");
|
||||||
|
let val_meta_abi = File::open("abi/ValidatorMetadata.json").expect("read val meta abi");
|
||||||
|
let stats =
|
||||||
|
count_votes(url, verbose, &voting_abi, &net_con_abi, &val_meta_abi).expect("count votes");
|
||||||
|
println!("{}", stats);
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
use colored::{Color, Colorize};
|
||||||
|
use ethabi::Address;
|
||||||
|
use events::Vote;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
use validator::Validator;
|
||||||
|
|
||||||
|
/// The count of ballots and cast votes, as well as metadata for a particular voter.
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
struct VoterStats {
|
||||||
|
/// The number of ballots where this voter had a right to vote.
|
||||||
|
ballots: usize,
|
||||||
|
/// The number of votes cast by this voter.
|
||||||
|
voted: usize,
|
||||||
|
/// The validator metadata.
|
||||||
|
validator: Option<Validator>,
|
||||||
|
/// The mining key.
|
||||||
|
mining_key: Option<Address>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A map of vote counts, by voting key.
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct Stats {
|
||||||
|
voter_stats: HashMap<Address, VoterStats>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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: &[Vote]) {
|
||||||
|
for voter in voters {
|
||||||
|
let mut vs = self.voter_stats
|
||||||
|
.entry(voter.clone())
|
||||||
|
.or_insert_with(VoterStats::default);
|
||||||
|
vs.ballots += 1;
|
||||||
|
if votes.iter().any(|vote| vote.voter == *voter) {
|
||||||
|
vs.voted += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts metadata about a voter: the mining key and the `Validator` information.
|
||||||
|
pub fn set_metadata(
|
||||||
|
&mut self,
|
||||||
|
voter: &Address,
|
||||||
|
mining_key: Address,
|
||||||
|
validator: Validator,
|
||||||
|
) -> bool {
|
||||||
|
match self.voter_stats.get_mut(voter) {
|
||||||
|
None => false,
|
||||||
|
Some(vs) => {
|
||||||
|
vs.validator = Some(validator);
|
||||||
|
vs.mining_key = Some(mining_key);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Stats {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
let mut lines: Vec<DisplayLine> = self.voter_stats
|
||||||
|
.iter()
|
||||||
|
.map(|(addr, s)| DisplayLine {
|
||||||
|
votes_per_thousand: s.voted * 1000 / s.ballots,
|
||||||
|
voting_address: *addr,
|
||||||
|
mining_key: match s.mining_key {
|
||||||
|
None => "".to_string(),
|
||||||
|
Some(ref key) => format!("{}", key),
|
||||||
|
},
|
||||||
|
name: match s.validator {
|
||||||
|
None => "".to_string(),
|
||||||
|
Some(ref v) => format!("{} {}", v.first_name, v.last_name),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
lines.sort();
|
||||||
|
writeln!(f, "{}", "Missed Voting key Mining key Name".bold())?;
|
||||||
|
for line in lines {
|
||||||
|
line.fmt(f)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A line in the output, corresponding to a particular voter.
|
||||||
|
#[derive(Ord, PartialOrd, Eq, PartialEq)]
|
||||||
|
struct DisplayLine {
|
||||||
|
votes_per_thousand: usize,
|
||||||
|
voting_address: Address,
|
||||||
|
mining_key: String,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for DisplayLine {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
let text = format!(
|
||||||
|
"{:5.1}% {} {} {}",
|
||||||
|
100.0 - (self.votes_per_thousand as f32) / 10.0,
|
||||||
|
self.voting_address,
|
||||||
|
self.mining_key,
|
||||||
|
self.name
|
||||||
|
);
|
||||||
|
let c = if self.votes_per_thousand <= 500 {
|
||||||
|
Color::BrightRed
|
||||||
|
} else if self.votes_per_thousand <= 750 {
|
||||||
|
Color::BrightYellow
|
||||||
|
} else if self.votes_per_thousand < 1000 {
|
||||||
|
Color::White
|
||||||
|
} else {
|
||||||
|
Color::BrightGreen
|
||||||
|
};
|
||||||
|
writeln!(f, "{}", text.color(c))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
use ethabi;
|
||||||
|
use std::u8;
|
||||||
|
use web3;
|
||||||
|
use web3::futures::Future;
|
||||||
|
|
||||||
|
// TODO: Evaluate whether any of these would make sense to include in `web3`.
|
||||||
|
|
||||||
|
/// Converts the bytes to a string, interpreting them as null-terminated UTF-8.
|
||||||
|
pub fn bytes_to_string(bytes: &[u8]) -> String {
|
||||||
|
let zero = bytes
|
||||||
|
.iter()
|
||||||
|
.position(|b| *b == 0)
|
||||||
|
.unwrap_or_else(|| bytes.len());
|
||||||
|
String::from_utf8_lossy(&bytes[..zero]).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the string as a 40-digit hexadecimal number, and returns the corresponding `Address`.
|
||||||
|
pub fn parse_address(mut s: &str) -> Option<ethabi::Address> {
|
||||||
|
let mut bytes = [0u8; 20];
|
||||||
|
if &s[..2] == "0x" {
|
||||||
|
s = &s[2..];
|
||||||
|
}
|
||||||
|
for i in 0..20 {
|
||||||
|
match u8::from_str_radix(&s[(2 * i)..(2 * i + 2)], 16) {
|
||||||
|
Ok(b) => bytes[i] = b,
|
||||||
|
Err(_) => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(ethabi::Address::from_slice(&bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ContractExt {
|
||||||
|
fn simple_query<P, R>(&self, func: &str, params: P) -> Result<R, web3::contract::Error>
|
||||||
|
where
|
||||||
|
R: web3::contract::tokens::Detokenize,
|
||||||
|
P: web3::contract::tokens::Tokenize;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContractExt for web3::contract::Contract<web3::transports::Http> {
|
||||||
|
/// Calls a constant function with the latest block and default parameters.
|
||||||
|
fn simple_query<P, R>(&self, func: &str, params: P) -> Result<R, web3::contract::Error>
|
||||||
|
where
|
||||||
|
R: web3::contract::tokens::Detokenize,
|
||||||
|
P: web3::contract::tokens::Tokenize,
|
||||||
|
{
|
||||||
|
self.query(
|
||||||
|
func,
|
||||||
|
params,
|
||||||
|
None,
|
||||||
|
web3::contract::Options::default(),
|
||||||
|
web3::types::BlockNumber::Latest,
|
||||||
|
).wait()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TopicFilterExt {
|
||||||
|
/// Returns a `web3::types::FilterBuilder` with these topics, starting from the first block.
|
||||||
|
fn to_filter_builder(self) -> web3::types::FilterBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TopicFilterExt for ethabi::TopicFilter {
|
||||||
|
fn to_filter_builder(self) -> web3::types::FilterBuilder {
|
||||||
|
web3::types::FilterBuilder::default()
|
||||||
|
.topics(
|
||||||
|
to_topic(self.topic0),
|
||||||
|
to_topic(self.topic1),
|
||||||
|
to_topic(self.topic2),
|
||||||
|
to_topic(self.topic3),
|
||||||
|
)
|
||||||
|
.from_block(web3::types::BlockNumber::Earliest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Web3LogExt {
|
||||||
|
fn into_raw(self) -> ethabi::RawLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Web3LogExt for web3::types::Log {
|
||||||
|
fn into_raw(self) -> ethabi::RawLog {
|
||||||
|
(self.topics, self.data.0).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts an `ethabi::Topic<T>` into an `Option<Vec<T>>`, where `Any` corresponds to `None`,
|
||||||
|
/// `This` to a vector with one element, and `OneOf` to any vector.
|
||||||
|
fn to_topic<T>(topic: ethabi::Topic<T>) -> Option<Vec<T>> {
|
||||||
|
match topic {
|
||||||
|
ethabi::Topic::Any => None,
|
||||||
|
ethabi::Topic::OneOf(v) => Some(v),
|
||||||
|
ethabi::Topic::This(t) => Some(vec![t]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait LogExt {
|
||||||
|
/// Returns the `i`-th parameter, if it has the given name, otherwise `None`.
|
||||||
|
fn param(&self, i: usize, name: &str) -> Option<ðabi::Token>;
|
||||||
|
|
||||||
|
/// Returns the `i`-th parameter, if it is an `Address` and has the given name, otherwise
|
||||||
|
/// `None`.
|
||||||
|
fn address_param(&self, i: usize, name: &str) -> Option<ðabi::Address>;
|
||||||
|
|
||||||
|
/// Returns the `i`-th parameter, if it is a `Uint` and has the given name, otherwise `None`.
|
||||||
|
fn uint_param(&self, i: usize, name: &str) -> Option<ðabi::Uint>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogExt for ethabi::Log {
|
||||||
|
fn param(&self, i: usize, name: &str) -> Option<ðabi::Token> {
|
||||||
|
self.params.get(i).and_then(|param| {
|
||||||
|
if param.name == name {
|
||||||
|
Some(¶m.value)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn address_param(&self, i: usize, name: &str) -> Option<ðabi::Address> {
|
||||||
|
match self.param(i, name) {
|
||||||
|
Some(ðabi::Token::Address(ref address)) => Some(address),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uint_param(&self, i: usize, name: &str) -> Option<ðabi::Uint> {
|
||||||
|
match self.param(i, name) {
|
||||||
|
Some(ðabi::Token::Uint(ref i)) => Some(i),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
use ethabi::Token;
|
||||||
|
use util;
|
||||||
|
use web3::contract::{tokens, Error, ErrorKind};
|
||||||
|
|
||||||
|
/// Validator metadata.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Validator {
|
||||||
|
pub first_name: String,
|
||||||
|
pub last_name: String,
|
||||||
|
// bytes32 licenseId,
|
||||||
|
// string fullAddress,
|
||||||
|
// bytes32 state,
|
||||||
|
// uint256 zipcode,
|
||||||
|
// uint256 expirationDate,
|
||||||
|
// uint256 createdDate,
|
||||||
|
// uint256 updatedDate,
|
||||||
|
// uint256 minThreshold,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl tokens::Detokenize for Validator {
|
||||||
|
/// Returns a `Validator` if the token's types match the fields.
|
||||||
|
fn from_tokens(tokens: Vec<Token>) -> Result<Validator, Error> {
|
||||||
|
match (tokens.get(0), tokens.get(1)) {
|
||||||
|
(Some(&Token::FixedBytes(ref first)), Some(&Token::FixedBytes(ref last))) => {
|
||||||
|
Ok(Validator {
|
||||||
|
first_name: util::bytes_to_string(first),
|
||||||
|
last_name: util::bytes_to_string(last),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => Err(ErrorKind::InvalidOutputType("Validator".to_string()).into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue