Merge pull request #3 from poanetwork/afck--logging-json
Improve logging, add contract files.
This commit is contained in:
commit
ac7e6e80d2
|
@ -0,0 +1,65 @@
|
|||
# Based on the "trust" template v0.1.2
|
||||
# https://github.com/japaric/trust/tree/v0.1.2
|
||||
|
||||
dist: trusty
|
||||
language: rust
|
||||
services: docker
|
||||
sudo: required
|
||||
rust: nightly-2018-04-19
|
||||
cache: cargo
|
||||
|
||||
env:
|
||||
global:
|
||||
- CRATE_NAME=poa-ballot-stats
|
||||
- RUST_BACKTRACE=1
|
||||
- RUSTFLAGS="-D warnings"
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- env: TARGET=x86_64-unknown-linux-gnu
|
||||
- env: TARGET=x86_64-apple-darwin
|
||||
os: osx
|
||||
|
||||
before_install:
|
||||
- set -e
|
||||
- rustup self update
|
||||
- rustup component add rustfmt-preview
|
||||
# - cargo install clippy -f --vers=0.0.195
|
||||
|
||||
install:
|
||||
- sh ci/install.sh
|
||||
- source ~/.cargo/env || true
|
||||
|
||||
script:
|
||||
- bash ci/script.sh
|
||||
|
||||
after_script: set +e
|
||||
|
||||
before_deploy:
|
||||
- sh ci/before_deploy.sh
|
||||
|
||||
deploy:
|
||||
api_key:
|
||||
secure: mj9bnqoc/lbsLv5Wiek37YY7BvOp+CN/Ier/uXC32uSRsJp5ue5/aru181oEhMI+6yeRPQyCrzzpoqMHcM5ARAp0GFW1YOkwqsnzaaiTMYTLGdLxtYBVNCrcLlUU474fvw92GCjhX2Ag8NpaQRYAaD1DMrkBTJ2qYJfm5zaXwByVu1bP1JX2zQl5mx6+/5j2DtrzQwMzRNBGzrJDuodRaeZ+/+cZvTLKkP4JaV7/iSuQ19NptSkLfflvB28J/XOnZ6m4mHbqGAfeNOB9YtH2ag70bvM1qfz4Y1fbphh8NJ4tngslxptOQ2oktAglebmthjiUAu8rhE1V63YioY2e/GaLIq3GSWOCleU6/1IvAac6VjjPBpaw6RqQgCVUCc+w6+CxmaY1CZZII2whM5FqE9AC22oTvK2SUfIzRXolYqUVYZsHI+75DBqExyE3QwW1T7IMTzqK25uokdSZBHdvC5yxZQVjDsuD+TukCOyPDmKW8Dlnn6B5XVTWruF+qALzQA1T8qy++QPz1O5i2lM61SR+iYlQQ/+IEr/je0rw1cpC9zp2/FFb/FAOyXFrGk5UGRSsewBz7j0gsqDin5zrqqBuRv+/hlEgXBJ7xFpOQVPJDk7n/Wz86whQLu7VUcKB3w/3+MO20i0DhXI4Rx8otrVWgDMq8cJ1mGN8bZhl0k8=
|
||||
file_glob: true
|
||||
file: $CRATE_NAME-$TRAVIS_TAG-$TARGET.*
|
||||
on:
|
||||
condition: $TRAVIS_RUST_VERSION = nightly-2018-04-19
|
||||
tags: true
|
||||
provider: releases
|
||||
skip_cleanup: true
|
||||
|
||||
cache: cargo
|
||||
before_cache:
|
||||
# Travis can't cache files that are not readable by "others"
|
||||
- chmod -R a+r $HOME/.cargo
|
||||
|
||||
branches:
|
||||
only:
|
||||
# release tags
|
||||
- /^v\d+\.\d+\.\d+.*$/
|
||||
- master
|
||||
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
File diff suppressed because it is too large
Load Diff
|
@ -9,6 +9,8 @@ clap = "2.31.2"
|
|||
colored = "1.6.0"
|
||||
error-chain = { version = "0.11", default-features = false }
|
||||
ethabi = "5.1.1"
|
||||
ethabi-contract = "5.1.0"
|
||||
ethabi-derive = "5.1.2"
|
||||
serde = "1.0.36"
|
||||
serde_derive = "1.0.36"
|
||||
serde_json = "1.0.13"
|
||||
|
|
22
README.md
22
README.md
|
@ -1,20 +1,28 @@
|
|||
# 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).
|
||||
Note that `poa-ballot-stats` needs access to the network's full logs, so the node must run with
|
||||
`--pruning=archive --no-warp`.
|
||||
|
||||
You can view the command line options with `-h`, and specify a different endpoint if your node e.g.
|
||||
uses a non-standard port. 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 in
|
||||
[poa-chain-spec](https://github.com/poanetwork/poa-chain-spec)'s `core` and `sokol` branches.
|
||||
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.
|
||||
|
||||
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`,
|
||||
for the main network.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
$ cargo run -- -h
|
||||
$ cargo run -- -c ../poa-chain-spec/contracts.json http://127.0.0.1:8545
|
||||
$ cargo run
|
||||
$ cargo run -- -c contracts/sokol.json https://sokol.poa.network -v
|
||||
```
|
||||
|
||||
## Screenshot
|
||||
|
||||
![Screenshot](screenshot.png)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"name": "_miningKey",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
|
@ -31,7 +31,7 @@
|
|||
},
|
||||
{
|
||||
"name": "zipcode",
|
||||
"type": "uint256"
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "expirationDate",
|
||||
|
@ -58,7 +58,7 @@
|
|||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"name": "_newProxyAddress",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
|
@ -67,12 +67,54 @@
|
|||
{
|
||||
"name": "count",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "voters",
|
||||
"type": "address[]"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_firstName",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_lastName",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_licenseId",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_fullAddress",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "_state",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_zipcode",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_expirationDate",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "createMetadata",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
|
@ -105,7 +147,7 @@
|
|||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"name": "_miningKey",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
|
@ -114,6 +156,10 @@
|
|||
{
|
||||
"name": "count",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "voters",
|
||||
"type": "address[]"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
|
@ -134,6 +180,20 @@
|
|||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "version",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
|
@ -149,50 +209,17 @@
|
|||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_firstName",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_lastName",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_licenseId",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_fullAddress",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "_state",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_zipcode",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "_expirationDate",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "_miningKey",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "changeRequestForValidator",
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "implementation",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
|
@ -239,20 +266,31 @@
|
|||
},
|
||||
{
|
||||
"name": "_zipcode",
|
||||
"type": "uint256"
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_expirationDate",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "changeRequest",
|
||||
"outputs": [
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
"name": "_createdDate",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "_updatedDate",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "_minThreshold",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "_miningKey",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "initMetadata",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
|
@ -313,6 +351,96 @@
|
|||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_firstName",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_lastName",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_licenseId",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_fullAddress",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "_state",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_zipcode",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_expirationDate",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "_miningKey",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "changeRequestForValidator",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_firstName",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_lastName",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_licenseId",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_fullAddress",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "_state",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_zipcode",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_expirationDate",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "changeRequest",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
|
@ -408,37 +536,8 @@
|
|||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_firstName",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_lastName",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_licenseId",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_fullAddress",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "_state",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "_zipcode",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "_expirationDate",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "createMetadata",
|
||||
"inputs": [],
|
||||
"name": "initMetadataDisable",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
|
@ -462,7 +561,7 @@
|
|||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"name": "_miningKey",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
|
@ -490,7 +589,7 @@
|
|||
},
|
||||
{
|
||||
"name": "zipcode",
|
||||
"type": "uint256"
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"name": "expirationDate",
|
||||
|
@ -513,17 +612,6 @@
|
|||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_proxyStorage",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# This script takes care of building your crate and packaging it for release
|
||||
|
||||
set -ex
|
||||
|
||||
main() {
|
||||
local src=$(pwd) \
|
||||
stage=
|
||||
|
||||
case $TRAVIS_OS_NAME in
|
||||
linux)
|
||||
stage=$(mktemp -d)
|
||||
;;
|
||||
osx)
|
||||
stage=$(mktemp -d -t tmp)
|
||||
;;
|
||||
esac
|
||||
|
||||
test -f Cargo.lock || cargo generate-lockfile
|
||||
|
||||
cross rustc --bin poa-ballot-stats --target $TARGET --release -- -C lto
|
||||
|
||||
cp target/$TARGET/release/poa-ballot-stats $stage/
|
||||
cp -r contracts $stage/
|
||||
|
||||
cd $stage
|
||||
tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz *
|
||||
cd $src
|
||||
|
||||
rm -rf $stage
|
||||
}
|
||||
|
||||
main
|
|
@ -0,0 +1,27 @@
|
|||
set -ex
|
||||
|
||||
main() {
|
||||
local target=
|
||||
if [ $TRAVIS_OS_NAME = linux ]; then
|
||||
target=x86_64-unknown-linux-musl
|
||||
sort=sort
|
||||
else
|
||||
target=x86_64-apple-darwin
|
||||
sort=gsort # for `sort --sort-version`, from brew's coreutils.
|
||||
fi
|
||||
|
||||
# This fetches latest stable release
|
||||
local tag=$(git ls-remote --tags --refs --exit-code https://github.com/japaric/cross \
|
||||
| cut -d/ -f3 \
|
||||
| grep -E '^v[0.1.0-9.]+$' \
|
||||
| $sort --version-sort \
|
||||
| tail -n1)
|
||||
curl -LSfs https://japaric.github.io/trust/install.sh | \
|
||||
sh -s -- \
|
||||
--force \
|
||||
--git japaric/cross \
|
||||
--tag $tag \
|
||||
--target $target
|
||||
}
|
||||
|
||||
main
|
|
@ -0,0 +1,22 @@
|
|||
set -ex
|
||||
|
||||
main() {
|
||||
cargo fmt -- --write-mode=diff
|
||||
|
||||
cross build --target $TARGET
|
||||
cross build --target $TARGET --release
|
||||
|
||||
if [ ! -z $DISABLE_TESTS ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
cross test --target $TARGET
|
||||
cross test --target $TARGET --release
|
||||
|
||||
cross build --target $TARGET --release
|
||||
}
|
||||
|
||||
# we don't run the "test phase" when doing deploys
|
||||
if [ -z $TRAVIS_TAG ]; then
|
||||
main
|
||||
fi
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"VOTING_TO_CHANGE_KEYS_ADDRESS": "0x215794efe4b86a2fbcbf706bc9ade63663f1eae1",
|
||||
"VOTING_TO_CHANGE_MIN_THRESHOLD_ADDRESS": "0xca863b0d12193a87b5173fd51fa4aa1703fb8a32",
|
||||
"VOTING_TO_CHANGE_PROXY_ADDRESS": "0x9c8a06f0197ee718cd820adeb48a88ea2a9b5c48",
|
||||
"BALLOTS_STORAGE_ADDRESS": "0x3a28ecc276d222829f78c98d43d719eafda0a6fe",
|
||||
"KEYS_MANAGER_ADDRESS": "0x2b1dbc7390a65dc40f7d64d67ea11b4d627dd1bf",
|
||||
"METADATA_ADDRESS": "0x4c0eb450d8dfa6e89eb14ac154867bc86b3c559c",
|
||||
"PROXY_ADDRESS": "0x6f4aadbb17789b4f5e9e97d456dc4e01b117ccb3",
|
||||
"POA_ADDRESS": "0x83451c8bc04d4ee9745ccc58edfab88037bc48cc",
|
||||
"MOC": "0xCf260eA317555637C55F70e55dbA8D5ad8414Cb0"
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"VOTING_TO_CHANGE_KEYS_ADDRESS": "0xc40cdf254a4a35498aa84f35e9842c110729a2a0",
|
||||
"VOTING_TO_CHANGE_MIN_THRESHOLD_ADDRESS": "0x700db8ba3128087f3b23f60de4bc3179bafa467d",
|
||||
"VOTING_TO_CHANGE_PROXY_ADDRESS": "0x0aa4a75549757a90f62f88b3b96b69bead2db0ff",
|
||||
"BALLOTS_STORAGE_ADDRESS": "0x27e7d2572aa37bec2ed30795f2fabccda4781f86",
|
||||
"KEYS_MANAGER_ADDRESS": "0x1aa02bd52fe418ac70263351282f66f1dacf898c",
|
||||
"METADATA_ADDRESS": "0xf71dd3797e4f173c2c08f2cebe8a6801d8191b42",
|
||||
"PROXY_ADDRESS": "0x3f918617a055d48e90f9fe06c168a75134565190",
|
||||
"POA_ADDRESS": "0x03048F666359CFD3C74a1A5b9a97848BF71d5038",
|
||||
"MOC": "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca"
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 206 KiB |
|
@ -12,5 +12,9 @@ error_chain! {
|
|||
UnexpectedLogParams {
|
||||
description("Unexpected parameter types in log"),
|
||||
}
|
||||
NoEventsFound {
|
||||
description("No events found. \
|
||||
Make sure your node is running in 'full' mode, not 'light'."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
119
src/events.rs
119
src/events.rs
|
@ -1,119 +0,0 @@
|
|||
use error::{ErrorKind, Result};
|
||||
use ethabi::{Address, FixedBytes, 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())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InitiateChange {
|
||||
/// The previous voter set's hash.
|
||||
parent_hash: FixedBytes,
|
||||
/// The new set of validators.
|
||||
pub new_set: Vec<Address>,
|
||||
}
|
||||
|
||||
impl InitiateChange {
|
||||
/// Parses the log and returns a `InitiateChange`, if the log corresponded to such an event.
|
||||
pub fn from_log(log: &Log) -> Result<InitiateChange> {
|
||||
match (
|
||||
log.param(0, "parentHash")
|
||||
.cloned()
|
||||
.and_then(Token::to_fixed_bytes),
|
||||
log.param(1, "newSet").cloned().and_then(Token::to_array),
|
||||
) {
|
||||
(Some(parent_hash), Some(tokens)) => Ok(InitiateChange {
|
||||
parent_hash,
|
||||
new_set: tokens.into_iter().filter_map(Token::to_address).collect(),
|
||||
}),
|
||||
_ => Err(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()),
|
||||
}
|
||||
}
|
||||
}
|
199
src/main.rs
199
src/main.rs
|
@ -3,33 +3,50 @@ extern crate colored;
|
|||
#[macro_use]
|
||||
extern crate error_chain;
|
||||
extern crate ethabi;
|
||||
#[macro_use(EthabiContract)]
|
||||
extern crate ethabi_derive;
|
||||
#[macro_use(use_contract)]
|
||||
extern crate ethabi_contract;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
#[macro_use(Deserialize)]
|
||||
extern crate serde_derive;
|
||||
extern crate serde_json;
|
||||
extern crate web3;
|
||||
|
||||
mod cli;
|
||||
mod error;
|
||||
mod events;
|
||||
mod stats;
|
||||
mod util;
|
||||
mod validator;
|
||||
|
||||
use error::{Error, ErrorKind};
|
||||
use events::{BallotCreated, ChangeFinalized, InitiateChange, Vote};
|
||||
use ethabi::Address;
|
||||
use stats::Stats;
|
||||
use std::default::Default;
|
||||
use std::fs::File;
|
||||
use util::{ContractExt, TopicFilterExt, Web3LogExt};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use util::{HexBytes, HexList, 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;
|
||||
// }
|
||||
/// The maximum age in seconds of the latest block.
|
||||
const MAX_BLOCK_AGE: u64 = 60 * 60;
|
||||
|
||||
use_contract!(
|
||||
net_con,
|
||||
"NetworkConsensus",
|
||||
"abi/PoaNetworkConsensus.abi.json"
|
||||
);
|
||||
use_contract!(
|
||||
voting,
|
||||
"VotingToChangeKeys",
|
||||
"abi/VotingToChangeKeys.abi.json"
|
||||
);
|
||||
use_contract!(
|
||||
val_meta,
|
||||
"ValidatorMetadata",
|
||||
"abi/ValidatorMetadata.abi.json"
|
||||
);
|
||||
use_contract!(key_mgr, "KeysManager", "abi/KeysManager.abi.json");
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
|
@ -38,12 +55,15 @@ struct ContractAddresses {
|
|||
keys_manager_address: String,
|
||||
}
|
||||
|
||||
impl Default for ContractAddresses {
|
||||
fn default() -> ContractAddresses {
|
||||
ContractAddresses {
|
||||
metadata_address: "0x4c0eb450d8dfa6e89eb14ac154867bc86b3c559c".to_string(),
|
||||
keys_manager_address: "0x2b1dbc7390a65dc40f7d64d67ea11b4d627dd1bf".to_string(),
|
||||
}
|
||||
/// Shows a warning if the node's latest block is outdated.
|
||||
fn check_synced<T: web3::Transport>(web3: &web3::Web3<T>) {
|
||||
let id = web3::types::BlockId::Number(web3::types::BlockNumber::Latest);
|
||||
let block = web3.eth().block(id).wait().expect("get latest block");
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Current timestamp is earlier than the Unix epoch!");
|
||||
if block.timestamp < (now.as_secs() - MAX_BLOCK_AGE).into() {
|
||||
eprintln!("WARNING: The node is not fully synchronized. Stats may be inaccurate.");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,105 +73,112 @@ fn count_votes(
|
|||
verbose: bool,
|
||||
contract_addrs: &ContractAddresses,
|
||||
) -> 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);
|
||||
|
||||
let voting_abi = File::open("abi/VotingToChangeKeys.abi.json").expect("read voting abi");
|
||||
let net_con_abi = File::open("abi/PoaNetworkConsensus.abi.json").expect("read consensus abi");
|
||||
let val_meta_abi = File::open("abi/ValidatorMetadata.abi.json").expect("read val meta abi");
|
||||
let key_mgr_abi = File::open("abi/KeysManager.abi.json").expect("read key mgr abi");
|
||||
check_synced(&web3);
|
||||
|
||||
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)?;
|
||||
let key_mgr_contract = ethabi::Contract::load(key_mgr_abi)?;
|
||||
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).unwrap();
|
||||
let web3_val_meta = web3::contract::Contract::new(web3.eth(), val_meta_addr, val_meta_contract);
|
||||
let key_mgr_addr = util::parse_address(&contract_addrs.keys_manager_address).unwrap();
|
||||
let web3_key_mgr = web3::contract::Contract::new(web3.eth(), key_mgr_addr, key_mgr_contract);
|
||||
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.event("BallotCreated")?;
|
||||
let vote_event = voting_contract.event("Vote")?;
|
||||
let change_event = net_con_contract.event("ChangeFinalized")?;
|
||||
let init_change_event = net_con_contract.event("InitiateChange")?;
|
||||
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 = ethabi::TopicFilter {
|
||||
topic0: ethabi::Topic::OneOf(vec![
|
||||
ballot_event.signature(),
|
||||
change_event.signature(),
|
||||
init_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()?;
|
||||
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<ethabi::Address> = Vec::new();
|
||||
let mut voters: Vec<Address> = Vec::new();
|
||||
let mut stats = Stats::default();
|
||||
let mut prev_init_change: Option<InitiateChange> = None;
|
||||
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_change_logs_filter.logs().wait()? {
|
||||
if let Ok(change_log) = change_event.parse_log(log.clone().into_raw()) {
|
||||
for log in ballot_or_change_filter.logs(&web3)? {
|
||||
event_found = true;
|
||||
if let Ok(change) = 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);
|
||||
}
|
||||
vprintln!(
|
||||
"• ChangeFinalized {{ new_set: {} }}",
|
||||
HexList(&change.new_set)
|
||||
);
|
||||
voters = change.new_set;
|
||||
} else if let Ok(init_change_log) = init_change_event.parse_log(log.clone().into_raw()) {
|
||||
} 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.
|
||||
let init_change = InitiateChange::from_log(&init_change_log)?;
|
||||
if verbose {
|
||||
println!("{:?}", init_change);
|
||||
}
|
||||
vprintln!(
|
||||
"• InitiateChange {{ parent_hash: {}, new_set: {} }}",
|
||||
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 = web3_key_mgr.simple_query("getVotingByMining", mining_key)?;
|
||||
if voter != ethabi::Address::zero() {
|
||||
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_log) = ballot_event.parse_log(log.into_raw()) {
|
||||
} else if let Ok(ballot) = 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) {
|
||||
if verbose {
|
||||
eprintln!("Unexpected voter {} for ballot {}", vote.voter, ballot.id);
|
||||
vprintln!("• {:?}", 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);
|
||||
}
|
||||
voters.push(vote.voter);
|
||||
}
|
||||
votes.push(vote);
|
||||
}
|
||||
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 = web3_val_meta.simple_query("getMiningByVotingKey", voter)?;
|
||||
let validator = web3_val_meta.simple_query("validators", mining_key)?;
|
||||
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)
|
||||
|
@ -161,13 +188,11 @@ 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_addrs = matches
|
||||
let contract_file = matches
|
||||
.value_of("contracts")
|
||||
.map(|filename| {
|
||||
let file = File::open(filename).expect("open contracts file");
|
||||
serde_json::from_reader(file).expect("parse contracts file")
|
||||
})
|
||||
.unwrap_or_default();
|
||||
.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 stats = count_votes(url, verbose, &contract_addrs).expect("count votes");
|
||||
println!("{}", stats);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use colored::{Color, Colorize};
|
||||
use ethabi::Address;
|
||||
use events::Vote;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use validator::Validator;
|
||||
use voting;
|
||||
|
||||
/// The count of ballots and cast votes, as well as metadata for a particular voter.
|
||||
#[derive(Clone, Default)]
|
||||
|
@ -27,7 +27,7 @@ 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: &[Vote]) {
|
||||
pub fn add_ballot(&mut self, voters: &[Address], votes: &[voting::logs::Vote]) {
|
||||
for voter in voters {
|
||||
let mut vs = self.voter_stats
|
||||
.entry(voter.clone())
|
||||
|
|
216
src/util.rs
216
src/util.rs
|
@ -1,75 +1,124 @@
|
|||
use ethabi;
|
||||
use std::u8;
|
||||
use ethabi::{self, Address, Bytes};
|
||||
use std::str::FromStr;
|
||||
use std::{fmt, 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];
|
||||
pub fn parse_address(mut s: &str) -> Option<Address> {
|
||||
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,
|
||||
Address::from_str(s).ok()
|
||||
}
|
||||
|
||||
/// Returns a wrapper of a contract address, to make function calls using the latest block.
|
||||
pub fn raw_call<T: web3::Transport + 'static>(
|
||||
to: Address,
|
||||
eth: web3::api::Eth<T>,
|
||||
) -> Box<Fn(Bytes) -> Result<Bytes, String>> {
|
||||
Box::new(move |bytes: Bytes| -> Result<Bytes, String> {
|
||||
let req = web3::types::CallRequest {
|
||||
from: None,
|
||||
to,
|
||||
gas: None,
|
||||
gas_price: None,
|
||||
value: None,
|
||||
data: Some(bytes.into()),
|
||||
};
|
||||
eth.call(req, Some(web3::types::BlockNumber::Latest))
|
||||
.wait()
|
||||
.map(|bytes| bytes.0)
|
||||
.map_err(|err| err.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
trait TopicExt<T> {
|
||||
/// Returns the union of the two topics.
|
||||
fn or(self, other: Self) -> Self;
|
||||
|
||||
/// Converts this topic into an `Option<Vec<T>>`, where `Any` corresponds to `None`,
|
||||
/// `This` to a vector with one element, and `OneOf` to any vector.
|
||||
fn to_opt_vec(self) -> Option<Vec<T>>;
|
||||
}
|
||||
|
||||
impl<T: Ord> TopicExt<T> for ethabi::Topic<T> {
|
||||
fn or(self, other: Self) -> Self {
|
||||
match (self.to_opt_vec(), other.to_opt_vec()) {
|
||||
(Some(mut v0), Some(v1)) => {
|
||||
for e in v1 {
|
||||
if !v0.contains(&e) {
|
||||
v0.push(e);
|
||||
}
|
||||
}
|
||||
if v0.len() == 1 {
|
||||
ethabi::Topic::This(v0.into_iter().next().expect("has a single element; qed"))
|
||||
} else {
|
||||
ethabi::Topic::OneOf(v0)
|
||||
}
|
||||
}
|
||||
(_, _) => ethabi::Topic::Any,
|
||||
}
|
||||
}
|
||||
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()
|
||||
fn to_opt_vec(self) -> Option<Vec<T>> {
|
||||
match self {
|
||||
ethabi::Topic::Any => None,
|
||||
ethabi::Topic::OneOf(v) => Some(v),
|
||||
ethabi::Topic::This(t) => Some(vec![t]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TopicFilterExt {
|
||||
/// Returns a `web3::types::FilterBuilder` with these topics, starting from the first block.
|
||||
fn to_filter_builder(self) -> web3::types::FilterBuilder;
|
||||
|
||||
/// Returns the "disjunction" of the two filters, i.e. it filters for everything that matches
|
||||
/// at least one of the two in every topic.
|
||||
fn or(self, other: ethabi::TopicFilter) -> ethabi::TopicFilter;
|
||||
|
||||
/// Returns the vector of logs that match this filter.
|
||||
fn logs<T: web3::Transport>(
|
||||
self,
|
||||
web3: &web3::Web3<T>,
|
||||
) -> Result<Vec<web3::types::Log>, web3::error::Error>;
|
||||
}
|
||||
|
||||
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),
|
||||
self.topic0.to_opt_vec(),
|
||||
self.topic1.to_opt_vec(),
|
||||
self.topic2.to_opt_vec(),
|
||||
self.topic3.to_opt_vec(),
|
||||
)
|
||||
.from_block(web3::types::BlockNumber::Earliest)
|
||||
.to_block(web3::types::BlockNumber::Latest)
|
||||
}
|
||||
|
||||
fn or(self, other: ethabi::TopicFilter) -> ethabi::TopicFilter {
|
||||
ethabi::TopicFilter {
|
||||
topic0: self.topic0.or(other.topic0),
|
||||
topic1: self.topic1.or(other.topic1),
|
||||
topic2: self.topic2.or(other.topic2),
|
||||
topic3: self.topic3.or(other.topic3),
|
||||
}
|
||||
}
|
||||
|
||||
fn logs<T: web3::Transport>(
|
||||
self,
|
||||
web3: &web3::Web3<T>,
|
||||
) -> Result<Vec<web3::types::Log>, web3::error::Error> {
|
||||
web3.eth_filter()
|
||||
.create_logs_filter(self.to_filter_builder().build())
|
||||
.wait()?
|
||||
.logs()
|
||||
.wait()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Web3LogExt {
|
||||
|
@ -82,50 +131,51 @@ impl Web3LogExt for web3::types::Log {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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]),
|
||||
/// Wrapper for a byte array, whose `Display` implementation outputs shortened hexadecimal strings.
|
||||
pub struct HexBytes<'a>(pub &'a [u8]);
|
||||
|
||||
impl<'a> fmt::Display for HexBytes<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "0x")?;
|
||||
for i in &self.0[..2] {
|
||||
write!(f, "{:02x}", i)?;
|
||||
}
|
||||
write!(f, "…")?;
|
||||
for i in &self.0[(self.0.len() - 2)..] {
|
||||
write!(f, "{:02x}", i)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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>;
|
||||
/// Wrapper for a list of byte arrays, whose `Display` implementation outputs shortened hexadecimal
|
||||
/// strings.
|
||||
pub struct HexList<'a, T: 'a>(pub &'a [T]);
|
||||
|
||||
/// 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
|
||||
impl<'a, T: 'a> fmt::Display for HexList<'a, T>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "[")?;
|
||||
for (i, item) in self.0.iter().enumerate() {
|
||||
if i > 0 {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
write!(f, "{}", HexBytes(item.as_ref()))?;
|
||||
}
|
||||
write!(f, "]")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_parse_address() {
|
||||
let addr_str = "0x2b1dbc7390a65dc40f7d64d67ea11b4d627dd1bf";
|
||||
let addr = super::parse_address(addr_str).expect("parse address with 0x");
|
||||
let addr2 = super::parse_address(&addr_str[2..]).expect("parse address without 0x");
|
||||
assert_eq!(addr, addr2);
|
||||
assert_eq!(addr_str, &format!("{:?}", addr));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
use ethabi::Token;
|
||||
use util;
|
||||
use web3::contract::{tokens, Error, ErrorKind};
|
||||
use ethabi;
|
||||
|
||||
/// Validator metadata.
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -17,17 +15,28 @@ pub struct Validator {
|
|||
// 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()),
|
||||
type ValidatorTuple = (
|
||||
ethabi::Hash,
|
||||
ethabi::Hash,
|
||||
ethabi::Hash,
|
||||
String,
|
||||
ethabi::Hash,
|
||||
ethabi::Hash,
|
||||
ethabi::Uint,
|
||||
ethabi::Uint,
|
||||
ethabi::Uint,
|
||||
ethabi::Uint,
|
||||
);
|
||||
|
||||
impl From<ValidatorTuple> for Validator {
|
||||
fn from((first_name_h, last_name_h, ..): ValidatorTuple) -> Validator {
|
||||
Validator {
|
||||
first_name: String::from_utf8_lossy(&*first_name_h)
|
||||
.to_owned()
|
||||
.to_string(),
|
||||
last_name: String::from_utf8_lossy(&*last_name_h)
|
||||
.to_owned()
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue