Merge pull request #7 from poanetwork/v2-contracts

Update to V2 contracts
This commit is contained in:
DrPeterVanNostrand 2018-10-10 11:37:37 -04:00 committed by GitHub
commit 41332c21dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 4138 additions and 2492 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
**/*.rs.bk
Cargo.lock
.env
/logs

View File

@ -1,10 +1,6 @@
language: rust
rust:
- nightly
- stable
cache: cargo
script:
- RUST_BACKTRACE=1 cargo build

View File

@ -1,24 +1,27 @@
[package]
name = "poagov"
version = "1.0.0"
authors = ["Peter van Nostrand <jnz@riseup.net>"]
license = "GPL-3.0"
authors = ["DrPeterVanNostrand <jnz@riseup.net>"]
build = "build.rs"
[dependencies]
chrono = "0.4"
clap = "2.31.2"
dotenv = "0.11.0"
ethabi = "5.1.1"
ethereum-types = "0.3.1"
hex = "0.3.1"
chrono = "0.4.6"
clap = "2.32.0"
ctrlc = "3.1.1"
dotenv = "0.13.0"
ethabi = "6.0.1"
ethereum-types = "0.4.0"
failure = "0.1.2"
hex = "0.3.2"
jsonrpc-core = "8.0.1"
lazy_static = "1.0.0"
lettre = "0.8"
lettre_email = "0.8"
native-tls = "0.1.5"
reqwest = "0.8.5"
serde = "1.0.36"
serde_derive = "1.0.36"
serde_json = "1.0.13"
slog = { version = "2.2.3", features = ["release_max_level_trace"] }
# TODO: after `lettre` and `lettre_email` v0.9.x have been published to crates.io, remove these
# GitHub dependencies.
lettre = { git = "https://github.com/lettre/lettre.git" }
lettre_email = { git = "https://github.com/lettre/lettre.git" }
native-tls = "0.2"
reqwest = "0.8.8"
serde_json = "1.0.27"
slog = { version = "2.3.3", features = ["release_max_level_trace"] }
slog-term = "2.4.0"
web3 = "0.3.0"
web3 = "0.4.0"

249
README.md
View File

@ -1,12 +1,20 @@
[![Build Status](https://travis-ci.org/poanetwork/poa-governance-notifications.svg?branch=master)](https://travis-ci.org/poanetwork/poa-governance-notifications)
# About
# `poa-governance-notifications`
A tool to monitor a POA Network blockchain for
[governance events](https://github.com/poanetwork/wiki/wiki/Governance-Overview).
The `poagov` command line tool is distributed as a binary for Linux and
OSX, or it can be built from source.
OSX; it can also be built from source for both platforms.
You can find the source code for the currently deployed governance contracts
[here](https://github.com/poanetwork/poa-network-consensus-contracts/tree/master/contracts).
You can find the addresses for governance contracts currently deployed to Core
[here](https://github.com/poanetwork/poa-chain-spec/blob/core/contracts.json)
and Sokol
[here](https://github.com/poanetwork/poa-chain-spec/blob/sokol/contracts.json).
# Installing the `poagov` Binary
@ -16,23 +24,26 @@ section in this README to find out how to download it.
On Debian/Ubuntu:
$ curl -OL https://github.com/poanetwork/poa-governance-notifications/releases/download/v0.1.0/poagov-0.1.0-linux-x86_64.tar.gz
$ tar -xvzf poagov-0.1.0-linux-x86_64.tar.gz
$ rm poagov-0.1.0-linux-x86_64.tar.gz
$ curl -OL https://github.com/poanetwork/poa-governance-notifications/releases/download/v1.0.0/poagov-1.0.0-linux-x86_64.tar.gz
$ tar -xvzf poagov-1.0.0-linux-x86_64.tar.gz
$ rm poagov-1.0.0-linux-x86_64.tar.gz
$ cd poagov
$ cp sample.env .env
$ chmod +x poagov
$ ./poagov --help
On OSX:
$ curl -OL https://github.com/poanetwork/poa-governance-notifications/releases/download/v0.1.0/poagov-0.1.0-osx-x86_64.tar.gz
$ tar -xvzf poagov-0.1.0-osx-x86_64.tar.gz
$ rm poagov-0.1.0-osx-x86_64.tar.gz
$ curl -OL https://github.com/poanetwork/poa-governance-notifications/releases/download/v1.0.0/poagov-1.0.0-osx-x86_64.tar.gz
$ tar -xvzf poagov-1.0.0-osx-x86_64.tar.gz
$ rm poagov-1.0.0-osx-x86_64.tar.gz
$ cd poagov
$ cp sample.env .env
$ chmod +x poagov
$ ./poagov --help
# Building the `poagov` Binary from Source
# Building `poagov` from Source
To build the `poagov` CLI tool, run the following:
@ -40,19 +51,23 @@ To build the `poagov` CLI tool, run the following:
$ cd poa-governance-notifications
$ cargo build --release
### Requires Rust Nightly
`poagov` can be built using Rust 1.29 stable and requires `libssl` to be
installed; see the following "Requires libssl" section for more information.
`poagov` uses experimental Rust features that are currently only available
in Rust version >= 1.26.0-nightly. You can check which version of Rust that
you are using by running:
Building `poagov` requires Rust `1.29.0-stable` or later and `libssl`; see the
"Requires `libssl`" section for more information.
$ rustc --version
### Testing
If you are not using Rust >= 1.26.0-nightly, you can switch to it using:
You can run `poagov`'s tests to ensure that it everything is working properly:
$ rustup default nightly
$ cargo test --release
### Requires libssl
The test suite will verify: that the required env-vars are found the `.env`
file, that each network's JSON-RPC server can be reached, and that each
contract ABI can be loaded.
# Requires `libssl`
SMTP over TLS requires that you have a native TLS library installed on your
machine, the preferred library for Linux and OSX is OpenSSL >= 1.0.1,
@ -67,11 +82,11 @@ If running `cargo build --release` panics with an error like:
Could not find directory of OpenSSL installation
..."
you probably do not have libssl installed.
you probably do not have `libssl` installed.
To install libssl on Debian/Ubuntu run the following:
To install `libssl` on Debian/Ubuntu run the following:
$ sudo apt-get update -y
$ sudo apt update
$ sudo apt-get install -y pkg-config libssl-dev
To install libssl on MacOS run the following:
@ -85,76 +100,136 @@ Then try to rebuild `poagov` using:
$ cargo build --release
If you are on OSX and installed OpenSSL using Homebrew and continue to get
compilation errors for any of the Rust crates: openssl, openssl-sys, or
openssl-sys-extras, try building with the following:
compilation errors for any of the Rust crates: `openssl`, `openssl-sys`, or
`openssl-sys-extras`, try building with the following:
$ cargo clean
$ OPENSSL_INCLUDE_DIR=$(brew --prefix openssl)/include \
OPENSSL_LIB_DIR=$(brew --prefix openssl)/lib \
cargo build
cargo build --release
There is a known issue regarding the openssl-sys crate not being able to
find libssl installed with Homebrew on OSX that is well documented on
There is a known issue regarding the `openssl-sys` crate not being able to
find `libssl` installed with Homebrew on OSX; more information can be found on
[Stack Overflow](https://stackoverflow.com/questions/34612395/openssl-crate-fails-compilation-on-mac-os-x-10-11/34615626#34615626).
The above solution comes from the linked Stack Overflow thread.
More information on common issues encountered while installing the
openssl Rust crate can be found [here](https://crates.io/crates/openssl).
`openssl` Rust crate can be found [here](https://crates.io/crates/openssl).
# Usage
Once you have built `poagov`, you can print out the CLI usage by running:
Once you have built or downloaded `poagov`, you can print out the CLI usage by
running:
$ ./target/release/poagov --help
# If you downloaded the `poagov` binary run:
$ poagov --help
# Or, if you built `poagov` from source run:
$ target/release/poagov --help
poagov 1.0
Monitores the POA Network's blockchain for governance events.
poagov 1.0.0
Monitors a POA Network blockchain for governance events.
USAGE:
poagov [FLAGS] [OPTIONS]
FLAGS:
--core monitor voting contracts deployed to the Core network (same as using --network=core)
--earliest start monitoring for goverance events starting from the first block in the chain
--email send governance notifications via email
-h, --help prints help information
-k monitor the blockchain for ballots to change keys (same as --monitor=keys)
--latest start monitoring for goverance events starting from the most recently mined block in the chain
--local monitor voting contracts deployed to a locally running POA chain (same as using --network=local)
-p monitor the change for ballots to change the proxy address (same as --monitor=proxy)
--push send governance notifications via push notification
--sokol monitor voting contracts deployed to the Sokol test network (same as using --network=sokol)
-t monitor the chain for ballots to change the minimum threshold (same as --monitor=threshold)
-V, --version prints version information
--core monitor voting contracts deployed to the Core network
--earliest begin monitoring for governance events starting at the first block in the blockchain
--email enables email notifications (SMTP configurations must be set in your `.env` file)
-e, --emission monitors the blockchain for ballots to manage emission funds
-h, --help prints help information
-k, --keys monitors the blockchain for ballots to change keys
--latest begin monitoring for governance events starting at the last block mined
-p, --proxy monitors the blockchain for ballots to change the proxy address
--sokol monitor voting contracts deployed to the Sokol network
-t, --threshold monitors the blockchain for ballots to change the minimum threshold
--v1 monitors the v1 voting contracts
--v2 monitors the v2 voting contracts
-V, --version prints version information
--log-emails logs each notification's email body; does not require the --email flag to be set
--log-file logs are written to files in the ./logs directory, log files are rotated when they reach a size of 4MB
OPTIONS:
--block-time <value> the average time it takes to mine a new block
--monitor <value> a comma-separated list of ballot types to monitor for governance events; the available values are: keys, threshold, proxy
--network <value> the name of the network to monitor for ballots; the values available for this option are: core, sokol, local
--rpc <value> the URL for the RPC endpoint
--start <value> start monitoring for governance events at this block (inclusive)
--tail <value> start monitoring for governance events for the `n` blocks prior to the last mined block in the chain
--block-time <value> the average number of seconds it takes to mine a new block
-n, --limit <value> shutdown `poagov` after this many notifications have been generated, useful when testing
--start <value> start monitoring for governance events at this block (inclusive)
--tail <value> start monitoring for governance events for the `n` blocks prior to the last block mined
# Setting up the Config File
Hitting `[ctrl-c]` while `poagov` is running will cause the process to gracefully shutdown.
### Required Arguments
Each time you run `poagov`, four CLI arguments are required:
1. The chain (specify only one): `--core`, `--sokol`.
2. The contracts to monitor (specify at least one): `--keys`, `--threshold`, `--proxy`, `--emission`.
2. The hardfork version (specify only one): `--v1`, `--v2`.
4. The block in the chain from where to start monitoring (specify only one): `--earliest`, `--latest`, `--start=<block_number>`, `--tail=<value>`.
### Notes on the hardfork version options `--v1` and `--v2`
`--v1` indicates that you want to monitor for governance events prior to the
Sokol and Core hardforks that will occur in September-2018 and November-2018
respectively.
`--v2` indicates that you want to monitor for governance events that occured
after the above hardfork dates.
- More information regarding the planned hardforks for the Sokol and Core
chains in September and November 2018 can be found
[here](https://medium.com/poa-network/poa-network-news-and-updates-36-2e6e00550c15).
### Optional Arguments
Providing the `--email` flag will enable governance notification emails. To use
this option, you must first configure SMTP in your `.env`.
Providing the `--block-time=<value>` will set how often `poagov` will query the
blockchain for new governance events. Defaults to 30 seconds.
Providing the `--log-emails` flag will print the full text for a notification
email to stderr when governance events are found. When this option is set,
email text will be logged regardless of whether or not the `--email` flag is
set.
Setting the `--log-file` flag will write logs to a file in the `./logs/`
directory. Logs are rotated chronologically across three files. Once the
`logs` directory has reached its max number of files, the oldest log file will
be deleted to make room for the next log file. Log files have a max size of
4MB; the log files will rotated once the current log file has reached the max
file size.
Setting the `--limit=<value>` option will cause `poagov` to stop once `value`
number of notifications have been generated. This option is useful when testing.
# Setting up the `.env` File
When the `poagov` CLI tool is run, the process' environment variables are
loaded via a `.env` file. An example `.env` file can be found at
`sample.env`. Before running this tool change the name of `sample.env` to
`.env` using:
loaded via an `.env` file.
$ mv sample.env .env
The `.env` file contains configuration variables that are not specified via the
command line. You are required to have an `.env` file in the same directory as
your `Cargo.toml` or `poagov` binary.
This will enable `poagov's` default configuration. You can update your
config file to allow `poagov` to use a locally running chain, governance
contracts that you have deployed locally, and to setup email
notifications.
When building from source, the `sample.env` file will be copied into the `.env`
file. This `.env` file will contain the default configuration values required
to run `poagov`.
# Setting up Email Notifications
If you did not build `poagov` from source, you will have to create an `.env`
file in the same directory as the `poagov` binary; then copy the contents of
`sample.env` into it.
If you wish to enable email notifications, you must add the required SMTP
config values to your `.env` file. See the "Setting up Email Notifications"
section for details.
### Setting up Email Notifications
In order to enable email notifications, you must change the name of the
`sample.env` file to `.env`. Then, you must add values for the following
SMTP config options in your `.env` file:
EMAIL_RECIPIENTS=
SMTP_HOST_DOMAIN=
SMTP_PORT=
SMTP_USERNAME=
@ -172,36 +247,44 @@ but you may use port 465 for TLS, or any other port that your outgoing
email server is lisening for secure connections. If you require unencrypted
SMTP, submit an issue and I can add it.
Your SMTP configuration should look something like the following:
EMAIL_RECIPIENTS=alice@poa.network,bob@poa.network
SMTP_HOST_DOMAIN=mail.riseup.net
SMTP_PORT=587
SMTP_USERNAME=evariste_galois
SMTP_PASSWORD='finteFIELDS#$!'
OUTGOING_EMAIL_ADDRESS=evariste_galois@riseup.net
# An Explained Example
$ ./target/release/poagov --sokol --earliest -kt --email
$ poagov --sokol --v1 -kt --earliest --email --log-emails --limit=1
- `--sokol` is used to monitor contracts deployed to POA's test network.
- `--earliest` starts monitoring from the first block in the blockchain.
- `-k` get notifications for ballots to change keys.
- `-t` get notifications for ballots to change the min threshold.
- `--email` sends out email notifications to each address in the
"VALIDATORS" config value (located in the .env file).
Press [ctrl-c] to exit `poagov`.
- `--sokol` monitors the Sokol chain.
- `--v1` monitors the governance contracts deployed prior to September-2018.
- `-k` monitors the `VotingToChangeKeys` contract.
- `-t` monitors the `VotingToChangeMinThreshold` contract.
- `--earliest` start monitoring from the first block in the blockchain.
- `--email` sends out email notifications to each address in the `EMAIL_RECIPIENTS` env-var.
- `--log-emails` for each governance notification generated, log the corresponding email body.
- `--limit=1` stop running `poagov` after one ballot notification has been generated.
# Logs
Logs are output to stderr. Any notifications that were generated, sent,
or failed to be sent will be logged. The following is an example log for a
a notification for a ballot to change the min threshold that was generated
using the command `$ poagov --earliest -t`:
Logs are output to `stderr` unless the `--log-file` CLI flag is set. Events
that are logged include: the generation of governance notifications, sending an
email successesfully or failing to send an email, aned what range of blocks
from the chain have been successfully monitored for governance events.
Optionally, you can log the email body for each governance notification
generated by setting the `--log-emails` CLI flag.
Apr 21 08:31:54.219 INFO notification, data: ThresholdNotification {
network: Sokol,
endpoint: "https://sokol.poa.network",
block_number: 1078816,
contract_type: Threshold,
ballot_type: ChangeMinThreshold,
ballot_id: 2,
start_time: 2018-02-23T05:28:22Z,
end_time: 2018-02-25T05:33:00Z,
memo: "*TEST* ballot to increase the consensus threshold to 51% (rounded to the higher integer) of the total number of validators. The idea is to legitimize passing the ballot by the majority participation.",
proposed_value: 4
}
The following is an example command with its corresponding logs:
$ poagov --sokol --v1 --threshold --earliest --limit=3
Oct 10 15:18:09.863 INFO starting poagov...
Oct 10 15:18:10.287 INFO governance notification, block_number: 525296, ballot_id: 0, ballot: Threshold
Oct 10 15:18:10.287 INFO governance notification, block_number: 599789, ballot_id: 1, ballot: Threshold
Oct 10 15:18:10.287 INFO governance notification, block_number: 1078816, ballot_id: 2, ballot: Threshold
Oct 10 15:18:10.287 WARN reached notification limit, gracefully shutting down..., limit: 3

View File

View File

@ -1,665 +0,0 @@
[
{
"constant": false,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "finalize",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getIsFinalized",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
},
{
"name": "_votingKey",
"type": "address"
}
],
"name": "isValidVote",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getBallotsStorage",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "uint256"
}
],
"name": "activeBallots",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getTotalVoters",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getTime",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_startTime",
"type": "uint256"
},
{
"name": "_endTime",
"type": "uint256"
},
{
"name": "_proposedValue",
"type": "address"
},
{
"name": "_contractType",
"type": "uint8"
},
{
"name": "memo",
"type": "string"
}
],
"name": "createBallotToChangeProxyAddress",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_miningKey",
"type": "address"
}
],
"name": "withinLimit",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_votingKey",
"type": "address"
}
],
"name": "getMiningByVotingKey",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "activeBallotsLength",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getMemo",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "isActive",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getEndTime",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_id",
"type": "uint256"
},
{
"name": "_choice",
"type": "uint8"
}
],
"name": "vote",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getKeysManager",
"outputs": [
{
"name": "",
"type": "address"
}
],
"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": "maxOldMiningKeysDeepCheck",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getStartTime",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getContractType",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getMinThresholdOfVoters",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
},
{
"name": "_votingKey",
"type": "address"
}
],
"name": "hasAlreadyVoted",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getGlobalMinThresholdOfVoters",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
},
{
"name": "_miningKey",
"type": "address"
}
],
"name": "areOldMiningKeysVoted",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getProposedValue",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "nextBallotId",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getProgress",
"outputs": [
{
"name": "",
"type": "int256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
}
],
"name": "validatorActiveBallots",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getBallotLimitPerValidator",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "uint256"
}
],
"name": "votingState",
"outputs": [
{
"name": "startTime",
"type": "uint256"
},
{
"name": "endTime",
"type": "uint256"
},
{
"name": "totalVoters",
"type": "uint256"
},
{
"name": "progress",
"type": "int256"
},
{
"name": "isFinalized",
"type": "bool"
},
{
"name": "quorumState",
"type": "uint8"
},
{
"name": "index",
"type": "uint256"
},
{
"name": "minThresholdOfVoters",
"type": "uint256"
},
{
"name": "proposedValue",
"type": "address"
},
{
"name": "contractType",
"type": "uint8"
},
{
"name": "creator",
"type": "address"
},
{
"name": "memo",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"name": "_proxyStorage",
"type": "address"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "id",
"type": "uint256"
},
{
"indexed": false,
"name": "decision",
"type": "uint256"
},
{
"indexed": true,
"name": "voter",
"type": "address"
},
{
"indexed": false,
"name": "time",
"type": "uint256"
}
],
"name": "Vote",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "id",
"type": "uint256"
},
{
"indexed": true,
"name": "voter",
"type": "address"
}
],
"name": "BallotFinalized",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "id",
"type": "uint256"
},
{
"indexed": true,
"name": "ballotType",
"type": "uint256"
},
{
"indexed": true,
"name": "creator",
"type": "address"
}
],
"name": "BallotCreated",
"type": "event"
}
]

View File

@ -770,4 +770,4 @@
"name": "BallotCreated",
"type": "event"
}
]
]

View File

@ -635,4 +635,4 @@
"name": "BallotCreated",
"type": "event"
}
]
]

View File

@ -662,4 +662,4 @@
"name": "BallotCreated",
"type": "event"
}
]
]

View File

@ -1,4 +1,37 @@
[
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "canBeFinalizedNow",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "minBallotDuration",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
@ -13,32 +46,6 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_startTime",
"type": "uint256"
},
{
"name": "_endTime",
"type": "uint256"
},
{
"name": "_proposedValue",
"type": "uint256"
},
{
"name": "memo",
"type": "string"
}
],
"name": "createBallotToChangeThreshold",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
@ -47,11 +54,11 @@
"type": "uint256"
}
],
"name": "getIsFinalized",
"name": "getQuorumState",
"outputs": [
{
"name": "",
"type": "bool"
"type": "uint256"
}
],
"payable": false,
@ -81,25 +88,11 @@
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getBallotsStorage",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"name": "_index",
"type": "uint256"
}
],
@ -116,17 +109,12 @@
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getTotalVoters",
"inputs": [],
"name": "initDisabled",
"outputs": [
{
"name": "",
"type": "uint256"
"type": "bool"
}
],
"payable": false,
@ -147,44 +135,6 @@
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_miningKey",
"type": "address"
}
],
"name": "withinLimit",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_votingKey",
"type": "address"
}
],
"name": "getMiningByVotingKey",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
@ -199,25 +149,6 @@
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getMemo",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
@ -245,7 +176,7 @@
"type": "uint256"
}
],
"name": "getEndTime",
"name": "getIndex",
"outputs": [
{
"name": "",
@ -256,38 +187,43 @@
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "maxBallotDuration",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "pure",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "migrateDisabled",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_id",
"type": "uint256"
},
{
"name": "_choice",
"type": "uint8"
}
],
"name": "vote",
"inputs": [],
"name": "migrateDisable",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getKeysManager",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
@ -303,36 +239,21 @@
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "maxOldMiningKeysDeepCheck",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"constant": false,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getStartTime",
"outputs": [
},
{
"name": "",
"name": "_choice",
"type": "uint256"
}
],
"name": "vote",
"outputs": [],
"payable": false,
"stateMutability": "view",
"stateMutability": "nonpayable",
"type": "function"
},
{
@ -377,20 +298,6 @@
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getGlobalMinThresholdOfVoters",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
@ -414,25 +321,6 @@
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getProposedValue",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
@ -451,26 +339,7 @@
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getProgress",
"outputs": [
{
"name": "",
"type": "int256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"name": "_miningKey",
"type": "address"
}
],
@ -486,88 +355,18 @@
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getBallotLimitPerValidator",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"constant": false,
"inputs": [
{
"name": "",
"type": "uint256"
}
],
"name": "votingState",
"outputs": [
{
"name": "startTime",
"type": "uint256"
},
{
"name": "endTime",
"type": "uint256"
},
{
"name": "totalVoters",
"type": "uint256"
},
{
"name": "progress",
"type": "int256"
},
{
"name": "isFinalized",
"type": "bool"
},
{
"name": "quorumState",
"type": "uint8"
},
{
"name": "index",
"type": "uint256"
},
{
"name": "minThresholdOfVoters",
"type": "uint256"
},
{
"name": "proposedValue",
"type": "uint256"
},
{
"name": "creator",
"type": "address"
},
{
"name": "memo",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"name": "_proxyStorage",
"name": "_prevVotingToChange",
"type": "address"
}
],
"name": "migrateBasicAll",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
"type": "function"
},
{
"anonymous": false,
@ -578,22 +377,17 @@
"type": "uint256"
},
{
"indexed": false,
"name": "decision",
"indexed": true,
"name": "ballotType",
"type": "uint256"
},
{
"indexed": true,
"name": "voter",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"name": "time",
"type": "uint256"
}
],
"name": "Vote",
"name": "BallotCreated",
"type": "event"
},
{
@ -622,17 +416,216 @@
"type": "uint256"
},
{
"indexed": true,
"name": "ballotType",
"indexed": false,
"name": "decision",
"type": "uint256"
},
{
"indexed": true,
"name": "creator",
"name": "voter",
"type": "address"
},
{
"indexed": false,
"name": "time",
"type": "uint256"
},
{
"indexed": false,
"name": "voterMiningKey",
"type": "address"
}
],
"name": "BallotCreated",
"name": "Vote",
"type": "event"
},
{
"constant": false,
"inputs": [
{
"name": "_startTime",
"type": "uint256"
},
{
"name": "_endTime",
"type": "uint256"
},
{
"name": "_ballotType",
"type": "uint256"
},
{
"name": "_affectedKeyType",
"type": "uint256"
},
{
"name": "_memo",
"type": "string"
},
{
"name": "_affectedKey",
"type": "address"
},
{
"name": "_miningKey",
"type": "address"
}
],
"name": "createBallot",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_startTime",
"type": "uint256"
},
{
"name": "_endTime",
"type": "uint256"
},
{
"name": "_memo",
"type": "string"
},
{
"name": "_newMiningKey",
"type": "address"
},
{
"name": "_newVotingKey",
"type": "address"
},
{
"name": "_newPayoutKey",
"type": "address"
}
],
"name": "createBallotToAddNewValidator",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getBallotInfo",
"outputs": [
{
"name": "startTime",
"type": "uint256"
},
{
"name": "endTime",
"type": "uint256"
},
{
"name": "affectedKey",
"type": "address"
},
{
"name": "affectedKeyType",
"type": "uint256"
},
{
"name": "newVotingKey",
"type": "address"
},
{
"name": "newPayoutKey",
"type": "address"
},
{
"name": "miningKey",
"type": "address"
},
{
"name": "totalVoters",
"type": "uint256"
},
{
"name": "progress",
"type": "int256"
},
{
"name": "isFinalized",
"type": "bool"
},
{
"name": "ballotType",
"type": "uint256"
},
{
"name": "creator",
"type": "address"
},
{
"name": "memo",
"type": "string"
},
{
"name": "canBeFinalizedNow",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_minBallotDuration",
"type": "uint256"
}
],
"name": "init",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_id",
"type": "uint256"
},
{
"name": "_prevVotingToChange",
"type": "address"
},
{
"name": "_voters",
"type": "address[]"
}
],
"name": "migrateBasicOne",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}
]
]

View File

@ -1,4 +1,37 @@
[
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "canBeFinalizedNow",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "minBallotDuration",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
@ -21,11 +54,11 @@
"type": "uint256"
}
],
"name": "getIsFinalized",
"name": "getQuorumState",
"outputs": [
{
"name": "",
"type": "bool"
"type": "uint256"
}
],
"payable": false,
@ -59,40 +92,7 @@
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getMiningKey",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getBallotsStorage",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"name": "_index",
"type": "uint256"
}
],
@ -109,17 +109,12 @@
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getTotalVoters",
"inputs": [],
"name": "initDisabled",
"outputs": [
{
"name": "",
"type": "uint256"
"type": "bool"
}
],
"payable": false,
@ -140,63 +135,6 @@
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_miningKey",
"type": "address"
}
],
"name": "withinLimit",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getAffectedKeyType",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_votingKey",
"type": "address"
}
],
"name": "getMiningByVotingKey",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
@ -211,48 +149,6 @@
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_currentKey",
"type": "address"
},
{
"name": "_affectedKey",
"type": "address"
}
],
"name": "checkIfMiningExisted",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getMemo",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
@ -276,23 +172,39 @@
"constant": true,
"inputs": [
{
"name": "_ballotType",
"name": "_id",
"type": "uint256"
},
{
"name": "_affectedKey",
"type": "address"
},
{
"name": "_affectedKeyType",
"type": "uint256"
},
{
"name": "_miningKey",
"type": "address"
}
],
"name": "areBallotParamsValid",
"name": "getIndex",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "maxBallotDuration",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "pure",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "migrateDisabled",
"outputs": [
{
"name": "",
@ -303,112 +215,13 @@
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getAffectedKey",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getEndTime",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_startTime",
"type": "uint256"
},
{
"name": "_endTime",
"type": "uint256"
},
{
"name": "_affectedKey",
"type": "address"
},
{
"name": "_affectedKeyType",
"type": "uint256"
},
{
"name": "_miningKey",
"type": "address"
},
{
"name": "_ballotType",
"type": "uint256"
},
{
"name": "memo",
"type": "string"
}
],
"name": "createVotingForKeys",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_id",
"type": "uint256"
},
{
"name": "_choice",
"type": "uint8"
}
],
"name": "vote",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getKeysManager",
"outputs": [
{
"name": "",
"type": "address"
}
],
"name": "migrateDisable",
"outputs": [],
"payable": false,
"stateMutability": "view",
"stateMutability": "nonpayable",
"type": "function"
},
{
@ -426,36 +239,21 @@
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "maxOldMiningKeysDeepCheck",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"constant": false,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getStartTime",
"outputs": [
},
{
"name": "",
"name": "_choice",
"type": "uint256"
}
],
"name": "vote",
"outputs": [],
"payable": false,
"stateMutability": "view",
"stateMutability": "nonpayable",
"type": "function"
},
{
@ -500,20 +298,6 @@
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getGlobalMinThresholdOfVoters",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
@ -537,25 +321,6 @@
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getBallotType",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
@ -574,26 +339,7 @@
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getProgress",
"outputs": [
{
"name": "",
"type": "int256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"name": "_miningKey",
"type": "address"
}
],
@ -609,100 +355,18 @@
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getBallotLimitPerValidator",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"constant": false,
"inputs": [
{
"name": "",
"type": "uint256"
}
],
"name": "votingState",
"outputs": [
{
"name": "startTime",
"type": "uint256"
},
{
"name": "endTime",
"type": "uint256"
},
{
"name": "affectedKey",
"type": "address"
},
{
"name": "affectedKeyType",
"type": "uint256"
},
{
"name": "miningKey",
"type": "address"
},
{
"name": "totalVoters",
"type": "uint256"
},
{
"name": "progress",
"type": "int256"
},
{
"name": "isFinalized",
"type": "bool"
},
{
"name": "quorumState",
"type": "uint8"
},
{
"name": "ballotType",
"type": "uint256"
},
{
"name": "index",
"type": "uint256"
},
{
"name": "minThresholdOfVoters",
"type": "uint256"
},
{
"name": "creator",
"type": "address"
},
{
"name": "memo",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"name": "_proxyStorage",
"name": "_prevVotingToChange",
"type": "address"
}
],
"name": "migrateBasicAll",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
"type": "function"
},
{
"anonymous": false,
@ -713,22 +377,17 @@
"type": "uint256"
},
{
"indexed": false,
"name": "decision",
"indexed": true,
"name": "ballotType",
"type": "uint256"
},
{
"indexed": true,
"name": "voter",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"name": "time",
"type": "uint256"
}
],
"name": "Vote",
"name": "BallotCreated",
"type": "event"
},
{
@ -757,17 +416,166 @@
"type": "uint256"
},
{
"indexed": true,
"name": "ballotType",
"indexed": false,
"name": "decision",
"type": "uint256"
},
{
"indexed": true,
"name": "creator",
"name": "voter",
"type": "address"
},
{
"indexed": false,
"name": "time",
"type": "uint256"
},
{
"indexed": false,
"name": "voterMiningKey",
"type": "address"
}
],
"name": "BallotCreated",
"name": "Vote",
"type": "event"
},
{
"constant": false,
"inputs": [
{
"name": "_startTime",
"type": "uint256"
},
{
"name": "_endTime",
"type": "uint256"
},
{
"name": "_proposedValue",
"type": "uint256"
},
{
"name": "_memo",
"type": "string"
}
],
"name": "createBallot",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
},
{
"name": "_votingKey",
"type": "address"
}
],
"name": "getBallotInfo",
"outputs": [
{
"name": "startTime",
"type": "uint256"
},
{
"name": "endTime",
"type": "uint256"
},
{
"name": "totalVoters",
"type": "uint256"
},
{
"name": "progress",
"type": "int256"
},
{
"name": "isFinalized",
"type": "bool"
},
{
"name": "proposedValue",
"type": "uint256"
},
{
"name": "creator",
"type": "address"
},
{
"name": "memo",
"type": "string"
},
{
"name": "canBeFinalizedNow",
"type": "bool"
},
{
"name": "alreadyVoted",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_minBallotDuration",
"type": "uint256"
},
{
"name": "_minPossibleThreshold",
"type": "uint256"
}
],
"name": "init",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_id",
"type": "uint256"
},
{
"name": "_prevVotingToChange",
"type": "address"
},
{
"name": "_voters",
"type": "address[]"
}
],
"name": "migrateBasicOne",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "minPossibleThreshold",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]
]

View File

@ -0,0 +1,571 @@
[
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "canBeFinalizedNow",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "minBallotDuration",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "finalize",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getQuorumState",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
},
{
"name": "_votingKey",
"type": "address"
}
],
"name": "isValidVote",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_index",
"type": "uint256"
}
],
"name": "activeBallots",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "initDisabled",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getTime",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "activeBallotsLength",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "isActive",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getIndex",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "maxBallotDuration",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "pure",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "migrateDisabled",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "migrateDisable",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "proxyStorage",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_id",
"type": "uint256"
},
{
"name": "_choice",
"type": "uint256"
}
],
"name": "vote",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getMinThresholdOfVoters",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
},
{
"name": "_votingKey",
"type": "address"
}
],
"name": "hasAlreadyVoted",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
},
{
"name": "_miningKey",
"type": "address"
}
],
"name": "areOldMiningKeysVoted",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "nextBallotId",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_miningKey",
"type": "address"
}
],
"name": "validatorActiveBallots",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_prevVotingToChange",
"type": "address"
}
],
"name": "migrateBasicAll",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "id",
"type": "uint256"
},
{
"indexed": true,
"name": "ballotType",
"type": "uint256"
},
{
"indexed": true,
"name": "creator",
"type": "address"
}
],
"name": "BallotCreated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "id",
"type": "uint256"
},
{
"indexed": true,
"name": "voter",
"type": "address"
}
],
"name": "BallotFinalized",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "id",
"type": "uint256"
},
{
"indexed": false,
"name": "decision",
"type": "uint256"
},
{
"indexed": true,
"name": "voter",
"type": "address"
},
{
"indexed": false,
"name": "time",
"type": "uint256"
},
{
"indexed": false,
"name": "voterMiningKey",
"type": "address"
}
],
"name": "Vote",
"type": "event"
},
{
"constant": false,
"inputs": [
{
"name": "_startTime",
"type": "uint256"
},
{
"name": "_endTime",
"type": "uint256"
},
{
"name": "_contractType",
"type": "uint256"
},
{
"name": "_memo",
"type": "string"
},
{
"name": "_proposedValue",
"type": "address"
}
],
"name": "createBallot",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
},
{
"name": "_votingKey",
"type": "address"
}
],
"name": "getBallotInfo",
"outputs": [
{
"name": "startTime",
"type": "uint256"
},
{
"name": "endTime",
"type": "uint256"
},
{
"name": "totalVoters",
"type": "uint256"
},
{
"name": "progress",
"type": "int256"
},
{
"name": "isFinalized",
"type": "bool"
},
{
"name": "proposedValue",
"type": "address"
},
{
"name": "contractType",
"type": "uint256"
},
{
"name": "creator",
"type": "address"
},
{
"name": "memo",
"type": "string"
},
{
"name": "canBeFinalizedNow",
"type": "bool"
},
{
"name": "alreadyVoted",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_minBallotDuration",
"type": "uint256"
}
],
"name": "init",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_id",
"type": "uint256"
},
{
"name": "_prevVotingToChange",
"type": "address"
},
{
"name": "_voters",
"type": "address[]"
}
],
"name": "migrateBasicOne",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}
]

View File

@ -0,0 +1,564 @@
[
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getQuorumState",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
},
{
"name": "_votingKey",
"type": "address"
}
],
"name": "isValidVote",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "initDisabled",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getTime",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "isActive",
"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": "_id",
"type": "uint256"
}
],
"name": "getMinThresholdOfVoters",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
},
{
"name": "_votingKey",
"type": "address"
}
],
"name": "hasAlreadyVoted",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
},
{
"name": "_miningKey",
"type": "address"
}
],
"name": "areOldMiningKeysVoted",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "nextBallotId",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "id",
"type": "uint256"
},
{
"indexed": true,
"name": "votingKey",
"type": "address"
}
],
"name": "BallotCanceled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "id",
"type": "uint256"
},
{
"indexed": true,
"name": "ballotType",
"type": "uint256"
},
{
"indexed": true,
"name": "creator",
"type": "address"
}
],
"name": "BallotCreated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "id",
"type": "uint256"
},
{
"indexed": true,
"name": "voter",
"type": "address"
}
],
"name": "BallotFinalized",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "id",
"type": "uint256"
},
{
"indexed": false,
"name": "decision",
"type": "uint256"
},
{
"indexed": true,
"name": "voter",
"type": "address"
},
{
"indexed": false,
"name": "time",
"type": "uint256"
},
{
"indexed": false,
"name": "voterMiningKey",
"type": "address"
}
],
"name": "Vote",
"type": "event"
},
{
"constant": true,
"inputs": [],
"name": "ballotCancelingThreshold",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "pure",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "canBeFinalizedNow",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "cancelNewBallot",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_startTime",
"type": "uint256"
},
{
"name": "_endTime",
"type": "uint256"
},
{
"name": "_receiver",
"type": "address"
},
{
"name": "_memo",
"type": "string"
}
],
"name": "createBallot",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "distributionThreshold",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "emissionFunds",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "emissionReleaseThreshold",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "emissionReleaseTime",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "finalize",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getBallotInfo",
"outputs": [
{
"name": "creationTime",
"type": "uint256"
},
{
"name": "startTime",
"type": "uint256"
},
{
"name": "endTime",
"type": "uint256"
},
{
"name": "isCanceled",
"type": "bool"
},
{
"name": "isFinalized",
"type": "bool"
},
{
"name": "creator",
"type": "address"
},
{
"name": "memo",
"type": "string"
},
{
"name": "amount",
"type": "uint256"
},
{
"name": "burnVotes",
"type": "uint256"
},
{
"name": "freezeVotes",
"type": "uint256"
},
{
"name": "sendVotes",
"type": "uint256"
},
{
"name": "receiver",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "getTotalVoters",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_emissionReleaseTime",
"type": "uint256"
},
{
"name": "_emissionReleaseThreshold",
"type": "uint256"
},
{
"name": "_distributionThreshold",
"type": "uint256"
},
{
"name": "_emissionFunds",
"type": "address"
}
],
"name": "init",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "noActiveBallotExists",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "refreshEmissionReleaseTime",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_id",
"type": "uint256"
},
{
"name": "_choice",
"type": "uint256"
}
],
"name": "vote",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}
]

18
build.rs Normal file
View File

@ -0,0 +1,18 @@
use std::fs;
use std::path::Path;
fn create_env_file_if_dne() {
let env_path = Path::new(".env");
if !env_path.exists() {
let sample_env_path = Path::new("sample.env");
if !sample_env_path.exists() {
panic!("neither the .env nor the sample.env files exist");
}
fs::copy(sample_env_path, env_path)
.unwrap_or_else(|e| panic!("could not create .env file from sample.env: {:?}", e));
}
}
fn main() {
create_env_file_if_dne();
}

View File

@ -1,35 +1,39 @@
# Defaults.
USE_NETWORK=sokol
MONITOR_BALLOTS=keys,threshold,proxy
START_BLOCK=latest
AVG_BLOCK_TIME_SECS=5
SEND_EMAIL_NOTIFICATIONS=false
SEND_PUSH_NOTIFICATIONS=false
# RPC endpoints: https://github.com/poanetwork/wiki
CORE_RPC_ENDPOINT=https://core.poa.network
SOKOL_RPC_ENDPOINT=https://sokol.poa.network
# Email configuration.
# Core Network V1 contract addresses:
# github.com/poanetwork/poa-chain-spec/blob/f037171b344d6138a6a7a7217ee6d2c85dbfd466/contracts.json
KEYS_CONTRACT_ADDRESS_CORE_V1=0x215794efe4b86a2fbcbf706bc9ade63663f1eae1
THRESHOLD_CONTRACT_ADDRESS_CORE_V1=0xca863b0d12193a87b5173fd51fa4aa1703fb8a32
PROXY_CONTRACT_ADDRESS_CORE_V1=0x9c8a06f0197ee718cd820adeb48a88ea2a9b5c48
# Core Network V2 contract addresses:
# github.com/poanetwork/poa-chain-spec/blob/38f3f634524923c4d74638852da1ace52e5a0cf0/contracts.json
KEYS_CONTRACT_ADDRESS_CORE_V2=0xa4508af18f1005943678769db3d95223c062258d
THRESHOLD_CONTRACT_ADDRESS_CORE_V2=0xa45e35472693ae60a95db8cB1ce73eea22ab5328
PROXY_CONTRACT_ADDRESS_CORE_V2=0x468758926c796722d85bded792d1831f0839caa6
EMISSION_FUNDS_CONTRACT_ADDRESS_CORE_V2=0x7e9b90b22cdd1f6aa206f0d852ac96212217d60e
# Sokol Network V1 contract addresses:
# github.com/poanetwork/poa-chain-spec/blob/821dc399471c80c6b0c6a95a2ba77d7c064eb321/contracts.json
KEYS_CONTRACT_ADDRESS_SOKOL_V1=0xc40cdf254a4a35498aa84f35e9842c110729a2a0
THRESHOLD_CONTRACT_ADDRESS_SOKOL_V1=0x700db8ba3128087f3b23f60de4bc3179bafa467d
PROXY_CONTRACT_ADDRESS_SOKOL_V1=0x0aa4a75549757a90f62f88b3b96b69bead2db0ff
# Sokol Network V2 contract addresses:
# github.com/poanetwork/poa-chain-spec/blob/36927101401de9dc3e21274d6f46e600d08fd1a7/contracts.json
KEYS_CONTRACT_ADDRESS_SOKOL_V2=0xb974df531c1b27324618175b442edf95f7f7a621
THRESHOLD_CONTRACT_ADDRESS_SOKOL_V2=0xd75ad6e3840a18dacc67bf3cd2080b24be409f79
PROXY_CONTRACT_ADDRESS_SOKOL_V2=0x604cdc518f3eb0446e15fc05a22923c82d8a8e21
EMISSION_FUNDS_CONTRACT_ADDRESS_SOKOL_V2=0x7cfa6f2c0d032f9dde652996e989a4d385b8b9d7
# Email notificaions settings.
# NOTE: if any of the following values contain special characters (as defined by your shell),
# wrap the entire env-var value in single quotations.
EMAIL_RECIPIENTS=
SMTP_HOST_DOMAIN=
SMTP_PORT=587
SMTP_USERNAME=
SMTP_PASSWORD=
OUTGOING_EMAIL_ADDRESS=
# Validators (comma-separated list of emails).
VALIDATORS=
# Core network configuration.
CORE_RPC_ENDPOINT=https://core.poa.network
CORE_KEYS_CONTRACT_ADDRESS=0x215794efe4b86a2fbcbf706bc9ade63663f1eae1
CORE_THRESHOLD_CONTRACT_ADDRESS=0x8829ebe113535826e8af17ed51f83755f675789a
CORE_PROXY_CONTRACT_ADDRESS=0x6b728399b41a38d4109f7af2213d4cc31ca87812
# Sokol network configuration.
SOKOL_RPC_ENDPOINT=https://sokol.poa.network
SOKOL_KEYS_CONTRACT_ADDRESS=0xc40cdf254a4a35498aa84f35e9842c110729a2a0
SOKOL_THRESHOLD_CONTRACT_ADDRESS=0x700db8ba3128087f3b23f60de4bc3179bafa467d
SOKOL_PROXY_CONTRACT_ADDRESS=0x0aa4a75549757a90f62f88b3b96b69bead2db0ff
# Personal/local network configuration.
LOCAL_RPC_ENDPOINT=http://127.0.0.1:8545
LOCAL_KEYS_CONTRACT_ADDRESS=
LOCAL_THRESHOLD_CONTRACT_ADDRESS=
LOCAL_PROXY_CONTRACT_ADDRESS=

87
src/blockchain.rs Normal file
View File

@ -0,0 +1,87 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::Duration;
use web3::types::BlockNumber;
use client::RpcClient;
use config::{Config, StartBlock};
use error::{Error, Result};
fn sleep_or_ctrlc(n_secs: u64, running: Arc<AtomicBool>) -> Option<()> {
let done_sleeping = Arc::new(AtomicBool::new(false));
{
let done_sleeping = done_sleeping.clone();
let _handle = thread::spawn(move || {
thread::sleep(Duration::from_secs(n_secs));
done_sleeping.store(true, Ordering::SeqCst);
});
}
loop {
if !running.load(Ordering::SeqCst) {
return None;
}
if done_sleeping.load(Ordering::SeqCst) {
return Some(());
}
}
}
pub struct BlockchainIter<'a> {
client: &'a RpcClient,
start_block: u64,
stop_block: u64,
on_first_iteration: bool,
block_time: u64,
running: Arc<AtomicBool>,
}
impl<'a> BlockchainIter<'a> {
pub fn new(client: &'a RpcClient, config: &Config, running: Arc<AtomicBool>) -> Result<Self> {
let last_mined_block = client.get_last_mined_block_number()?;
let start_block = match config.start_block {
StartBlock::Earliest => 0,
StartBlock::Latest => last_mined_block,
StartBlock::Number(block_number) => block_number,
StartBlock::Tail(tail) => last_mined_block - tail,
};
if start_block > last_mined_block {
return Err(Error::StartBlockExceedsLastBlockMined { start_block, last_mined_block });
}
let bc_iter = BlockchainIter {
client,
start_block,
stop_block: last_mined_block,
on_first_iteration: true,
block_time: config.block_time,
running,
};
Ok(bc_iter)
}
}
impl<'a> Iterator for BlockchainIter<'a> {
type Item = Result<(BlockNumber, BlockNumber)>;
fn next(&mut self) -> Option<Self::Item> {
if self.on_first_iteration {
self.on_first_iteration = false;
} else {
self.start_block = self.stop_block + 1;
while self.start_block >= self.stop_block {
sleep_or_ctrlc(self.block_time, self.running.clone())?;
match self.client.get_last_mined_block_number() {
Ok(last_mined) => self.stop_block = last_mined,
Err(e) => return Some(Err(e)),
};
}
};
if self.running.load(Ordering::SeqCst) {
let range = (self.start_block.into(), self.stop_block.into());
Some(Ok(range))
} else {
None
}
}
}

View File

@ -1,30 +1,119 @@
#![allow(dead_code)]
use clap::{ArgMatches, App};
pub struct Cli;
#[derive(Debug)]
pub struct Cli(ArgMatches<'static>);
impl Cli {
pub fn load() -> ArgMatches<'static> {
App::new("poagov")
.version("1.0")
.about("A tool to monitor POA Network's blockchain for governance events.")
pub fn parse() -> Self {
let cli_args = App::new("poagov")
.version("1.0.0")
.about("Monitors a POA Network blockchain for governance events.")
.args_from_usage(
"[network] --network [value] 'the name of the network to monitor for ballots; the values available for this option are: core, sokol, local'
[core] --core 'monitor voting contracts deployed to the Core network (same as using --network=core)'
[sokol] --sokol 'monitor voting contracts deployed to the Sokol test network (same as using --network=sokol)'
[local] --local 'monitor voting contracts deployed to a locally running POA chain (same as using --network=local)'
[rpc] --rpc [value] 'the URL for the RPC endpoint'
[monitor] --monitor [value] 'a comma-separated list of ballot types to monitor for governance events; the available values are: keys, threshold, proxy`
[keys] -k 'monitor the blockchain for ballots to change keys (same as --monitor=keys)'
[threshold] -t 'monitor the chain for ballots to change the minimum threshold (same as --monitor=threshold)'
[proxy] -p 'monitor the change for ballots to change the proxy address (same as --monitor=proxy)'
"[core] --core 'monitor voting contracts deployed to the Core network'
[sokol] --sokol 'monitor voting contracts deployed to the Sokol network'
[keys] -k --keys 'monitors the blockchain for ballots to change keys'
[threshold] -t --threshold 'monitors the blockchain for ballots to change the minimum threshold'
[proxy] -p --proxy 'monitors the blockchain for ballots to change the proxy address'
[emission] -e --emission 'monitors the blockchain for ballots to manage emission funds'
[v1] --v1 'monitors the v1 voting contracts'
[v2] --v2 'monitors the v2 voting contracts'
[earliest] --earliest 'begin monitoring for governance events starting at the first block in the blockchain'
[latest] --latest 'begin monitoring for governance events starting at the last block mined'
[start_block] --start [value] 'start monitoring for governance events at this block (inclusive)'
[tail] --tail [value] 'start monitoring for governance events for the `n` blocks prior to the last mined block in the chain'
[earliest] --earliest 'start monitoring for goverance events starting from the first block in the chain'
[latest] --latest 'start monitoring for goverance events starting from the most recently mined block in the chain'
[email] --email 'send governance notifications via email'
[push] --push 'send governance notifications via push notification'
[block_time] --block-time [value] 'the average time it takes to mine a new block'"
)
.get_matches()
[tail] --tail [value] 'start monitoring for governance events for the `n` blocks prior to the last block mined'
[email] --email 'enables email notifications (SMTP configurations must be set in your `.env` file)'
[block_time] --block-time [value] 'the average number of seconds it takes to mine a new block'
[notification_limit] -n --limit [value] 'shutdown `poagov` after this many notifications have been generated'
[log_emails] --log-emails 'logs each notification's email body; does not require the --email flag to be set'
[log_to_file] --log-file 'logs are written to files in the ./logs directory, logs are rotated chronologically across 3 files, each file has a max size of 8MB'"
).get_matches();
Cli(cli_args)
}
pub fn core(&self) -> bool {
self.0.is_present("core")
}
pub fn sokol(&self) -> bool {
self.0.is_present("sokol")
}
pub fn keys(&self) -> bool {
self.0.is_present("keys")
}
pub fn threshold(&self) -> bool {
self.0.is_present("threshold")
}
pub fn proxy(&self) -> bool {
self.0.is_present("proxy")
}
pub fn emission(&self) -> bool {
self.0.is_present("emission")
}
pub fn v1(&self) -> bool {
self.0.is_present("v1")
}
pub fn v2(&self) -> bool {
self.0.is_present("v2")
}
pub fn earliest(&self) -> bool {
self.0.is_present("earliest")
}
pub fn latest(&self) -> bool {
self.0.is_present("latest")
}
pub fn start_block(&self) -> Option<&str> {
self.0.value_of("start_block")
}
pub fn tail(&self) -> Option<&str> {
self.0.value_of("tail")
}
pub fn multiple_start_blocks_specified(&self) -> bool {
let mut count = 0;
if self.earliest() {
count += 1;
}
if self.latest() {
count += 1;
}
if self.start_block().is_some() {
count += 1;
}
if self.tail().is_some() {
count += 1;
}
count != 1
}
pub fn email(&self) -> bool {
self.0.is_present("email")
}
pub fn block_time(&self) -> Option<&str> {
self.0.value_of("block_time")
}
pub fn notification_limit(&self) -> Option<&str> {
self.0.value_of("notification_limit")
}
pub fn log_emails(&self) -> bool {
self.0.is_present("log_emails")
}
pub fn log_to_file(&self) -> bool {
self.0.is_present("log_to_file")
}
}

310
src/client.rs Normal file
View File

@ -0,0 +1,310 @@
use std::u64;
use ethabi;
use hex;
use jsonrpc_core as json_rpc;
use reqwest;
use serde_json as json;
use web3;
use web3::types::{Address, BlockNumber, Filter, FilterBuilder, U256};
use config::{ContractType, PoaContract};
use error::{Error, Result};
use response::{v1, v2};
use response::common::BallotCreatedLog;
#[derive(Debug)]
pub enum RpcMethod {
CallContractFunction,
GetLogs,
GetLastMinedBlockNumber,
}
impl Into<String> for RpcMethod {
fn into(self) -> String {
let s = match self {
RpcMethod::CallContractFunction => "eth_call",
RpcMethod::GetLogs => "eth_getLogs",
RpcMethod::GetLastMinedBlockNumber => "eth_blockNumber",
};
s.into()
}
}
#[derive(Debug)]
pub struct RpcClient {
endpoint: String,
client: reqwest::Client,
}
impl RpcClient {
pub fn new(endpoint: String) -> Self {
let client = reqwest::Client::new();
RpcClient { endpoint, client }
}
fn build_request(
&self,
method: RpcMethod,
params: Vec<json::Value>,
) -> Result<reqwest::Request>
{
let method_call = json_rpc::types::request::MethodCall {
jsonrpc: Some(json_rpc::types::version::Version::V2),
method: method.into(),
params: Some(json_rpc::types::Params::Array(params)),
id: json_rpc::types::id::Id::Num(1),
};
let request_data: json_rpc::types::request::Call = method_call.into();
self.client
.post(&self.endpoint)
.json(&request_data)
.build()
.map_err(|e| Error::FailedToBuildRequest(e))
}
fn send(&self, req: reqwest::Request) -> Result<json::Value> {
let resp: json_rpc::types::response::Response = self.client
.execute(req)
.map_err(|e| Error::RequestFailed(e))?
.json()
.unwrap();
if let json_rpc::types::response::Response::Single(resp_status) = resp {
match resp_status {
json_rpc::types::response::Output::Success(resp) => return Ok(resp.result),
json_rpc::types::response::Output::Failure(e) => return Err(Error::JsonRpcResponseFailure(e)),
};
}
unreachable!("Recieved multiple responses for single request");
}
pub fn get_last_mined_block_number(&self) -> Result<u64> {
let req = self.build_request(RpcMethod::GetLastMinedBlockNumber, vec![])?;
if let json::Value::String(s) = self.send(req)? {
let s = s.trim_left_matches("0x");
let block_number = u64::from_str_radix(s, 16).unwrap();
return Ok(block_number);
}
unreachable!("Received a non-string response from `eth_blockNumber` call");
}
fn get_logs(&self, filter: Filter) -> Result<Vec<web3::types::Log>> {
let params = vec![json::to_value(filter).unwrap()];
let req = self.build_request(RpcMethod::GetLogs, params)?;
let result = self.send(req)?;
Ok(json::from_value(result).unwrap())
}
/// V1 and V2
pub fn get_ballot_created_logs(
&self,
contract: &PoaContract,
start: BlockNumber,
stop: BlockNumber,
) -> Result<Vec<BallotCreatedLog>>
{
let event = contract.event("BallotCreated");
let event_sig = event.signature();
let filter = FilterBuilder::default()
.topics(Some(vec![event_sig]), None, None, None)
.address(vec![contract.addr])
.from_block(start)
.to_block(stop)
.build();
self.get_logs(filter)?
.into_iter()
.map(|web3_log| {
let web3::types::Log {topics, data, block_number, .. } = web3_log;
let raw_log = ethabi::RawLog::from((topics, data.0));
let ethabi_log = event.parse_log(raw_log)
.map_err(|e| Error::FailedToParseRawLogToLog(e))?;
BallotCreatedLog::from_ethabi_log(ethabi_log, block_number.unwrap())
})
.collect()
}
/// V1
pub fn get_voting_state(&self, contract: &PoaContract, ballot_id: U256) -> Result<v1::VotingState> {
let function = contract.function("votingState");
let tokens = vec![ethabi::Token::Uint(ballot_id)];
let encoded_input = function.encode_input(&tokens).unwrap();
let function_call_request = web3::types::CallRequest {
to: contract.addr,
data: Some(encoded_input.into()),
from: None,
gas: None,
gas_price: None,
value: None,
};
let rpc_method_params = vec![
json::to_value(function_call_request).unwrap(),
json::to_value(BlockNumber::Latest).unwrap(),
];
let req = self.build_request(RpcMethod::CallContractFunction, rpc_method_params)?;
if let json::Value::String(s) = self.send(req)? {
let s = s.trim_left_matches("0x");
let bytes = hex::decode(s).unwrap();
let outputs = function.decode_output(&bytes).unwrap();
let voting_state: v1::VotingState = match contract.kind {
ContractType::Keys => v1::KeysVotingState::from(outputs).into(),
ContractType::Threshold => v1::ThresholdVotingState::from(outputs).into(),
ContractType::Proxy => v1::ProxyVotingState::from(outputs).into(),
ContractType::Emission => return Err(Error::EmissionFundsV1ContractDoesNotExist),
};
return Ok(voting_state);
}
unreachable!("received non-string JSON response from `votingState`");
}
/// V2
// TODO: When V2 contracts have been published and ballots have begun, test that calling
// `.getBallotInfo()` with `Address::zero()` for the `votingKey` works (we don't care if
// `votingKey` has voted yet).
pub fn get_ballot_info(&self, contract: &PoaContract, ballot_id: U256) -> Result<v2::BallotInfo> {
// pub fn get_ballot_info(&self, contract: &PoaContract, ballot_id: U256, voting_key: Option<Address>) -> Result<v2::BallotInfo> {
let function = contract.function("getBallotInfo");
/*
let mut tokens = vec![ethabi::Token::Uint(ballot_id)];
if function.inputs.len() == 2 {
if let Some(voting_key) = voting_key {
tokens.push(ethabi::Token::Address(voting_key));
}
}
*/
let mut tokens = vec![ethabi::Token::Uint(ballot_id)];
if function.inputs.len() == 2 {
tokens.push(ethabi::Token::Address(Address::zero()));
}
let encoded_input = function.encode_input(&tokens).unwrap();
let function_call_request = web3::types::CallRequest {
to: contract.addr,
data: Some(encoded_input.into()),
from: None,
gas: None,
gas_price: None,
value: None,
};
let rpc_method_params = vec![
json::to_value(function_call_request).unwrap(),
json::to_value(BlockNumber::Latest).unwrap(),
];
let req = self.build_request(RpcMethod::CallContractFunction, rpc_method_params)?;
if let json::Value::String(s) = self.send(req)? {
let s = s.trim_left_matches("0x");
let bytes = hex::decode(s).unwrap();
let outputs = function.decode_output(&bytes).unwrap();
let ballot_info: v2::BallotInfo = match contract.kind {
ContractType::Keys => v2::KeysBallotInfo::from(outputs).into(),
ContractType::Threshold => v2::ThresholdBallotInfo::from(outputs).into(),
ContractType::Proxy => v2::ProxyBallotInfo::from(outputs).into(),
ContractType::Emission => v2::EmissionBallotInfo::from(outputs).into(),
};
return Ok(ballot_info);
}
unreachable!("received non-string JSON response from `getBallotInfo`");
}
}
#[cfg(test)]
mod tests {
use std::env;
use web3::types::BlockNumber;
use super::super::tests::setup;
use super::RpcClient;
use config::{ContractType, ContractVersion, Network, PoaContract};
#[test]
fn test_get_last_mined_block() {
setup();
let sokol_url = env::var("SOKOL_RPC_ENDPOINT").expect("Missing env-var: `SOKOL_RPC_ENDPOINT`");
let client = RpcClient::new(sokol_url);
let res = client.get_last_mined_block_number();
println!("\nsokol last mined block number => {:?}", res);
assert!(res.is_ok());
let core_url = env::var("CORE_RPC_ENDPOINT").expect("Missing env-var: `CORE_RPC_ENDPOINT`");
let client = RpcClient::new(core_url);
let res = client.get_last_mined_block_number();
println!("core last mined block number => {:?}", res);
assert!(res.is_ok());
}
#[test]
fn test_get_ballot_created_logs() {
setup();
let contract = PoaContract::read(
ContractType::Keys,
Network::Sokol,
ContractVersion::V1,
).unwrap_or_else(|e| panic!("Failed to load contract: {:?}", e));
let endpoint = env::var("SOKOL_RPC_ENDPOINT").expect("Missing env-var: `SOKOL_RPC_ENDPOINT`");
let client = RpcClient::new(endpoint);
let res = client.get_ballot_created_logs(
&contract,
BlockNumber::Earliest,
BlockNumber::Latest,
);
println!("{:#?}", res);
assert!(res.is_ok());
}
// TODO: uncomment this test once V2 ballots are created.
/*
#[test]
fn test_get_ballot_created_logs_v2() {
setup();
let contract = PoaContract::read(
ContractType::Keys,
Network::Core,
ContractVersion::V2
).unwrap_or_else(|e| panic!("Failed to load contract: {:?}", e));
let rpc_url = env::var("CORE_RPC_ENDPOINT").expect("Missing env-var: `CORE_RPC_ENDPOINT`");
let client = RpcClient::new(rpc_url);
let res = client.get_ballot_created_logs(
&contract,
BlockNumber::Earliest,
BlockNumber::Latest,
);
println!("RES => {:#?}", res);
assert!(res.is_ok());
}
*/
#[test]
fn test_get_voting_state() {
setup();
let contract = PoaContract::read(
ContractType::Threshold,
Network::Sokol,
ContractVersion::V1
).unwrap_or_else(|e| panic!("Failed to load contract: {:?}", e));
let sokol_url = env::var("SOKOL_RPC_ENDPOINT").expect("Missing env-var: `SOKOL_RPC_ENDPOINT`");
let client = RpcClient::new(sokol_url);
let res = client.get_voting_state(&contract, 2.into());
println!("{:#?}", res);
assert!(res.is_ok());
}
// TODO: uncomment this test once V2 ballots are created.
/*
#[test]
fn test_get_ballot_info() {
setup();
let contract = PoaContract::read(
ContractType::Emission,
Network::Core,
ContractVersion::V2
).unwrap_or_else(|e| panic!("Failed to load contract: {:?}", e));
let sokol_url = env::var("CORE_RPC_ENDPOINT").expect("Missing env-var: `SOKOL_RPC_ENDPOINT`");
let client = RpcClient::new(sokol_url);
let ballot_id
let res = client.get_ballot_info(&contract, 2.into());
println!("{:#?}", res);
assert!(res.is_ok());
}
*/
}

View File

@ -1,118 +1,148 @@
use std::env;
use std::fmt::{self, Debug, Display, Formatter};
use std::fmt::{self, Debug, Formatter};
use std::fs::File;
use std::str::FromStr;
use std::time::Duration;
use dotenv::dotenv;
use ethabi::{Contract, Event, Function};
use ethereum_types::Address;
use ethabi::{Address, Contract, Event, Function};
use cli::Cli;
use utils::hex_string_to_u64;
use error::{Error, Result};
use response::common::BallotType;
#[derive(Clone, Copy, Debug)]
pub enum Network { Core, Sokol, Local }
const DEFAULT_BLOCK_TIME_SECS: u64 = 30;
impl<'a> From<&'a str> for Network {
fn from(s: &'a str) -> Self {
match s {
"core" => Network::Core,
"sokol" => Network::Sokol,
"local" => Network::Local,
_ => panic!(format!("Invalid network: {}", s))
}
}
}
impl Display for Network {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
Network::Core => write!(f, "core"),
Network::Sokol => write!(f, "sokol"),
Network::Local => write!(f, "local")
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Network {
Core,
Sokol,
}
impl Network {
fn to_uppercase(&self) -> String {
format!("{}", self).to_uppercase()
}
}
#[derive(Clone, Copy, Debug)]
pub enum ContractType { Keys, Threshold, Proxy }
impl<'a> From<&'a str> for ContractType {
fn from(s: &'a str) -> Self {
match s {
"keys" => ContractType::Keys,
"threshold" => ContractType::Threshold,
"proxy" => ContractType::Proxy,
_ => panic!("Invalid contract type: {}", s)
fn uppercase(&self) -> &str {
match self {
Network::Core => "CORE",
Network::Sokol => "SOKOL",
}
}
}
impl Display for ContractType {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
ContractType::Keys => write!(f, "keys"),
ContractType::Threshold => write!(f, "threshold"),
ContractType::Proxy => write!(f, "proxy")
/// Note that the `Emission` contract is V2 only.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ContractType {
Keys,
Threshold,
Proxy,
Emission,
}
impl From<BallotType> for ContractType {
fn from(ballot_type: BallotType) -> Self {
match ballot_type {
BallotType::InvalidKey => ContractType::Keys,
BallotType::AddKey => ContractType::Keys,
BallotType::RemoveKey => ContractType::Keys,
BallotType::SwapKey => ContractType::Keys,
BallotType::Threshold => ContractType::Threshold,
BallotType::Proxy => ContractType::Proxy,
BallotType::Emission => ContractType::Emission,
}
}
}
impl ContractType {
fn to_uppercase(&self) -> String {
format!("{}", self).to_uppercase()
fn uppercase(&self) -> &str {
match self {
ContractType::Keys => "KEYS",
ContractType::Threshold => "THRESHOLD",
ContractType::Proxy => "PROXY",
ContractType::Emission => "EMISSION_FUNDS",
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum StartBlock {
Earliest,
Latest,
Number(u64),
Tail(u64)
}
impl<'a> From<&'a str> for StartBlock {
fn from(s: &'a str) -> Self {
if s == "earliest" {
StartBlock::Earliest
} else if s == "latest" {
StartBlock::Latest
} else if s.starts_with('-') {
let tail: u64 = s[1..].parse().expect("Invalid tail start-block");
StartBlock::Tail(tail)
} else if s.starts_with("0x") {
let block_number = hex_string_to_u64(s).expect("Invalid hex start-block");
StartBlock::Number(block_number)
} else {
let block_number: u64 = s.parse().expect("Invaild decimal start-block");
StartBlock::Number(block_number)
fn abi_file_name(&self) -> &str {
match self {
ContractType::Keys => "VotingToChangeKeys.abi.json",
ContractType::Threshold => "VotingToChangeMinThreshold.abi.json",
ContractType::Proxy => "VotingToChangeProxyAddress.abi.json",
ContractType::Emission => "VotingToManageEmissionFunds.abi.json",
}
}
}
#[derive(Clone, Debug)]
pub struct Validator {
pub name: String,
pub email: String
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ContractVersion {
V1,
V2,
}
impl ContractVersion {
fn lowercase(&self) -> &str {
match self {
ContractVersion::V1 => "v1",
ContractVersion::V2 => "v2",
}
}
}
#[derive(Clone)]
pub struct PoaContract {
pub kind: ContractType,
pub version: ContractVersion,
pub addr: Address,
pub abi: Contract
pub abi: Contract,
}
impl Debug for PoaContract {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_struct("PoaContract")
.field("kind", &self.kind)
.field("addr", &self.addr)
.field("abi", &"<ethabi::Contract>")
.finish()
}
}
impl PoaContract {
fn new(kind: ContractType, addr: Address, abi: Contract) -> Self {
PoaContract { kind, addr, abi }
pub fn read(
contract_type: ContractType,
network: Network,
version: ContractVersion,
) -> Result<Self>
{
if contract_type == ContractType::Emission && version == ContractVersion::V1 {
return Err(Error::EmissionFundsV1ContractDoesNotExist);
}
let addr_env_var = format!(
"{}_CONTRACT_ADDRESS_{}_{:?}",
contract_type.uppercase(),
network.uppercase(),
version,
);
let addr = if let Ok(s) = env::var(&addr_env_var) {
match Address::from_str(s.trim_left_matches("0x")) {
Ok(addr) => addr,
Err(_) => return Err(Error::InvalidContractAddr(s.into())),
}
} else {
return Err(Error::MissingEnvVar(addr_env_var));
};
let abi_path = format!(
"abis/{}/{}",
version.lowercase(),
contract_type.abi_file_name(),
);
let abi_file = File::open(&abi_path)
.map_err(|_| Error::MissingAbiFile(abi_path.clone()))?;
let abi = Contract::load(&abi_file)
.map_err(|_| Error::InvalidAbi(abi_path))?;
let contract = PoaContract { kind: contract_type, version, addr, abi };
Ok(contract)
}
pub fn event(&self, event: &str) -> Event {
@ -124,144 +154,261 @@ impl PoaContract {
}
}
impl Display for PoaContract {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "PoaContract({:?}, {:?})", self.kind, self.addr)
}
#[derive(Clone, Copy, Debug)]
pub enum StartBlock {
Earliest,
Latest,
Number(u64),
Tail(u64),
}
impl Debug for PoaContract {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self)
}
}
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct Config {
pub network: Network,
pub endpoint: String,
pub version: ContractVersion,
pub contracts: Vec<PoaContract>,
pub start_block: StartBlock,
pub send_email_notifications: bool,
pub send_push_notifications: bool,
pub validators: Vec<Validator>,
pub avg_block_time: Duration,
pub smtp_host_domain: String,
pub smtp_port: u16,
pub smtp_username: String,
pub smtp_password: String,
pub outgoing_email: String
pub block_time: u64,
pub email_notifications: bool,
pub email_recipients: Vec<String>,
pub smtp_host_domain: Option<String>,
pub smtp_port: Option<u16>,
pub smtp_username: Option<String>,
pub smtp_password: Option<String>,
pub outgoing_email_addr: Option<String>,
pub notification_limit: Option<usize>,
pub log_emails: bool,
pub log_to_file: bool,
}
impl Config {
pub fn load() -> Self {
dotenv().ok();
let cli = Cli::load();
let network = if let Some(s) = cli.value_of("network") {
s.into()
} else if cli.is_present("core") {
pub fn new(cli: &Cli) -> Result<Self> {
let network = if cli.core() == cli.sokol() {
return Err(Error::MustSpecifyOneCliArgument("`--core` or `--sokol`".into()));
} else if cli.core() {
Network::Core
} else if cli.is_present("sokol") {
} else {
Network::Sokol
} else if cli.is_present("local") {
Network::Local
} else {
env::var("USE_NETWORK").unwrap().as_str().into()
};
let network_uppercase = network.to_uppercase();
let endpoint = if let Some(s) = cli.value_of("rpc") {
s.into()
let endpoint_env_var = format!("{}_RPC_ENDPOINT", network.uppercase());
let endpoint = env::var(&endpoint_env_var)
.map_err(|_| Error::MissingEnvVar(endpoint_env_var))?;
let version = if cli.v1() == cli.v2(){
return Err(Error::MustSpecifyOneCliArgument("`--v1` or `--v2`".into()));
} else if cli.v1(){
ContractVersion::V1
} else {
let env_var = format!("{}_RPC_ENDPOINT", network_uppercase);
env::var(&env_var).unwrap()
ContractVersion::V2
};
let mut contracts = vec![];
if cli.keys() {
let keys = PoaContract::read(
ContractType::Keys,
network,
version
)?;
contracts.push(keys);
}
if cli.threshold() {
let threshold = PoaContract::read(
ContractType::Threshold,
network,
version
)?;
contracts.push(threshold);
}
if cli.proxy() {
let proxy = PoaContract::read(
ContractType::Proxy,
network,
version
)?;
contracts.push(proxy);
}
if cli.emission() {
let emission_funds = PoaContract::read(
ContractType::Emission,
network,
version,
)?;
contracts.push(emission_funds);
}
if contracts.is_empty() {
return Err(Error::MustSpecifyAtLeastOneCliArgument(
"`--keys`, `--threshold`, `--proxy`, `--emission`".into()
));
}
let start_block = if cli.multiple_start_blocks_specified() {
return Err(Error::MustSpecifyOneCliArgument(
"`--earliest` or `--latest` or `--start-block` or `--tail`".into()
));
} else if cli.earliest() {
StartBlock::Earliest
} else if cli.latest() {
StartBlock::Latest
} else if let Some(s) = cli.start_block() {
let block_number = s.parse().map_err(|_| Error::InvalidStartBlock(s.into()))?;
StartBlock::Number(block_number)
} else if let Some(s) = cli.tail() {
let tail = s.parse().map_err(|_| Error::InvalidTail(s.into()))?;
StartBlock::Tail(tail)
} else {
unreachable!();
};
let mut contract_types: Vec<ContractType> = vec![];
if let Some(s) = cli.value_of("monitor") {
s.split(',').for_each(|s| contract_types.push(s.into()));
}
if cli.is_present("keys") {
contract_types.push(ContractType::Keys);
}
if cli.is_present("threshold") {
contract_types.push(ContractType::Threshold);
}
if cli.is_present("proxy") {
contract_types.push(ContractType::Proxy);
}
if contract_types.is_empty() {
env::var("MONITOR_BALLOTS").unwrap().split(',')
.for_each(|s| contract_types.push(s.into()));
}
let block_time = if let Some(s) = cli.block_time() {
s.parse().map_err(|_| Error::InvalidBlockTime(s.into()))?
} else {
DEFAULT_BLOCK_TIME_SECS
};
let contracts: Vec<PoaContract> = contract_types.iter()
.map(|contract_type| {
let env_var = format!(
"{}_{}_CONTRACT_ADDRESS",
network_uppercase,
contract_type.to_uppercase()
);
let hex = env::var(&env_var)
.expect(&format!("Contract address not found: {}", env_var));
let addr = Address::from_str(hex.trim_left_matches("0x")).unwrap();
let abi_path = format!("abis/{}/{}.json", network, contract_type);
let file = File::open(&abi_path)
.expect(&format!("ABI file not found: {}", abi_path));
let abi = Contract::load(&file)
.expect(&format!("Invalid ABI file: {}", abi_path));
PoaContract::new(*contract_type, addr, abi)
let email_notifications = cli.email();
let email_recipients: Vec<String> = env::var("EMAIL_RECIPIENTS")
.map_err(|_| Error::MissingEnvVar("EMAIL_RECIPIENTS".into()))?
.split(',')
.filter_map(|s| {
if s.is_empty() {
None
} else {
Some(s.into())
}
})
.collect();
let start_block = if let Some(s) = cli.value_of("start_block") {
s.into()
} else if cli.is_present("earliest") {
StartBlock::Earliest
} else if cli.is_present("latest") {
StartBlock::Latest
} else if let Some(s) = cli.value_of("tail") {
StartBlock::Tail(s.parse().expect("Invalid tail value"))
let smtp_host_domain = if email_notifications {
let host = env::var("SMTP_HOST_DOMAIN")
.map_err(|_| Error::MissingEnvVar("SMTP_HOST_DOMAIN".into()))?;
Some(host)
} else {
env::var("START_BLOCK").unwrap().as_str().into()
None
};
let send_email_notifications = if cli.is_present("email") {
true
let smtp_port = if email_notifications {
if let Ok(s) = env::var("SMTP_PORT") {
let port = s.parse().map_err(|_| Error::InvalidSmtpPort(s.into()))?;
Some(port)
} else {
return Err(Error::MissingEnvVar("SMTP_PORT".into()));
}
} else {
env::var("SEND_EMAIL_NOTIFICATIONS").unwrap().parse().unwrap()
None
};
let send_push_notifications = if cli.is_present("push") {
true
let smtp_username = if email_notifications {
let username = env::var("SMTP_USERNAME")
.map_err(|_| Error::MissingEnvVar("SMTP_USERNAME".into()))?;
Some(username)
} else {
env::var("SEND_PUSH_NOTIFICATIONS").unwrap().parse().unwrap()
None
};
let validators = env::var("VALIDATORS").unwrap().split(',')
.map(|s| Validator { email: s.into(), name: "".into() })
.collect();
let avg_block_time = if let Some(s) = cli.value_of("block_time") {
Duration::from_secs(s.parse().unwrap())
let smtp_password = if email_notifications {
let password = env::var("SMTP_PASSWORD")
.map_err(|_| Error::MissingEnvVar("SMTP_PASSWORD".into()))?;
Some(password)
} else {
let s = env::var("AVG_BLOCK_TIME_SECS").unwrap();
Duration::from_secs(s.parse().unwrap())
None
};
let smtp_host_domain = env::var("SMTP_HOST_DOMAIN").unwrap();
let smtp_port = env::var("SMTP_PORT").unwrap().parse().unwrap();
let smtp_username = env::var("SMTP_USERNAME").unwrap();
let smtp_password = env::var("SMTP_PASSWORD").unwrap();
let outgoing_email = env::var("OUTGOING_EMAIL_ADDRESS").unwrap();
let outgoing_email_addr = if email_notifications {
let email_addr = env::var("OUTGOING_EMAIL_ADDRESS")
.map_err(|_| Error::MissingEnvVar("OUTGOING_EMAIL_ADDRESS".into()))?;
Some(email_addr)
} else {
None
};
Config {
network, endpoint, contracts, start_block,
send_email_notifications, send_push_notifications,
validators, avg_block_time, smtp_host_domain,
smtp_port, smtp_username, smtp_password, outgoing_email
let notification_limit = if let Some(s) = cli.notification_limit() {
let limit = s.parse().map_err(|_| Error::InvalidNotificationLimit(s.into()))?;
Some(limit)
} else {
None
};
let log_emails = cli.log_emails();
let log_to_file = cli.log_to_file();
let config = Config {
network,
endpoint,
version,
contracts,
start_block,
block_time,
email_notifications,
email_recipients,
smtp_host_domain,
smtp_port,
smtp_username,
smtp_password,
outgoing_email_addr,
notification_limit,
log_emails,
log_to_file,
};
Ok(config)
}
}
#[cfg(test)]
mod tests {
use std::env;
use super::super::tests::setup;
use super::{ContractType, ContractVersion, PoaContract, Network};
const CONTRACT_TYPES: [ContractType; 4] = [
ContractType::Keys,
ContractType::Threshold,
ContractType::Proxy,
ContractType::Emission,
];
const NETWORKS: [Network; 2] = [Network::Sokol, Network::Core];
const VERSIONS: [ContractVersion; 2] = [ContractVersion::V1, ContractVersion::V2];
#[test]
fn test_env_file_integrity() {
setup();
for network in NETWORKS.iter() {
let env_var = format!("{}_RPC_ENDPOINT", network.uppercase());
assert!(env::var(&env_var).is_ok());
for contract_type in CONTRACT_TYPES.iter() {
for version in VERSIONS.iter() {
if *contract_type == ContractType::Emission && *version == ContractVersion::V1 {
continue;
}
let env_var = format!(
"{}_CONTRACT_ADDRESS_{}_{:?}",
contract_type.uppercase(),
network.uppercase(),
version,
);
assert!(env::var(&env_var).is_ok());
}
}
}
}
#[test]
fn test_load_contract_abis() {
setup();
for contract_type in CONTRACT_TYPES.iter() {
for version in VERSIONS.iter() {
for network in NETWORKS.iter() {
let res = PoaContract::read(*contract_type, *network, *version);
if *contract_type == ContractType::Emission && *version == ContractVersion::V1 {
assert!(res.is_err());
} else {
assert!(res.is_ok());
}
}
}
}
}
}

39
src/error.rs Normal file
View File

@ -0,0 +1,39 @@
use ctrlc;
use jsonrpc_core;
use ethabi;
use failure;
use lettre;
use native_tls;
use reqwest;
pub type Result<T> = ::std::result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
CtrlcError(ctrlc::Error),
EmissionFundsV1ContractDoesNotExist,
FailedToBuildEmail(failure::Error),
FailedToBuildRequest(reqwest::Error),
FailedToBuildTls(native_tls::Error),
FailedToParseBallotCreatedLog(String),
FailedToParseRawLogToLog(ethabi::Error),
FailedToResolveSmtpHostDomain(lettre::smtp::error::Error),
FailedToSendEmail(lettre::smtp::error::Error),
InvalidAbi(String),
InvalidBlockTime(String),
InvalidContractAddr(String),
InvalidNotificationLimit(String),
InvalidSmtpPort(String),
InvalidStartBlock(String),
InvalidTail(String),
JsonRpcResponseFailure(jsonrpc_core::types::response::Failure),
MissingAbiFile(String),
MissingEnvVar(String),
MustSpecifyAtLeastOneCliArgument(String),
MustSpecifyOneCliArgument(String),
RequestFailed(reqwest::Error),
StartBlockExceedsLastBlockMined {
start_block: u64,
last_mined_block: u64,
},
}

245
src/logger.rs Normal file
View File

@ -0,0 +1,245 @@
use std::fs::{self, create_dir, File, read_dir, remove_file};
use std::io::stderr;
use std::path::Path;
use chrono::{DateTime, TimeZone, Utc};
use slog::{self, Drain};
use slog_term::{FullFormat, PlainSyncDecorator};
use web3::types::BlockNumber;
use config::Config;
use error::Error;
use notify::Notification;
// The date format used to name log files; e.g. "Oct-08-2018-14:09:00".
const FILE_NAME_DATE_FORMAT: &str = "%b-%d-%Y-%H:%M:%S";
// The directory (relative to Cargo.toml) to store logs.
const LOGS_DIR: &str = "logs";
const MAX_NUMBER_OF_LOG_FILES: usize = 3;
const MAX_LOG_FILE_SIZE_MB: usize = 4;
const MAX_LOG_FILE_SIZE_BYTES: usize = MAX_LOG_FILE_SIZE_MB * 1024 * 1024;
// We dont want to check the log file's size after every log that is written, this constant states
// "after this many logs have been written, check the log file's size". This value assumes an
// average log is around 100 ASCII characters (bytes) long.
const INITIAL_CHECK_FILE_SIZE_AT: usize = MAX_LOG_FILE_SIZE_BYTES / 100;
fn create_logs_dir() {
let logs_dir = Path::new(LOGS_DIR);
if !logs_dir.exists() {
create_dir(logs_dir)
.unwrap_or_else(|e| panic!("could not create ./logs directory: {:?}", e));
}
}
fn read_logs_dir() -> Vec<LogFile> {
let mut log_files: Vec<LogFile> = read_dir(LOGS_DIR)
.unwrap_or_else(|e| panic!("could not read ./logs directory: {:?}", e))
.filter_map(|res| {
let path = res.ok()?.path();
let file_name = path.file_name().unwrap().to_str().unwrap();
LogFile::from_file_name(file_name).ok()
}).collect();
log_files.sort_unstable();
log_files
}
fn rotate_log_files(log_files: &mut Vec<LogFile>) -> File {
while log_files.len() >= MAX_NUMBER_OF_LOG_FILES {
let log_file_to_remove = log_files.remove(0);
log_file_to_remove.remove_file();
}
let log_file = LogFile::now();
let file = log_file.create_file();
log_files.push(log_file);
file
}
fn get_file_size_in_bytes(path: &str) -> usize {
fs::metadata(&path)
.unwrap_or_else(|_| panic!("log file does not exist: {}", path))
.len() as usize
}
enum LogLocation {
Stderr,
File(File),
}
fn create_slog_logger(log_location: LogLocation) -> slog::Logger {
if let LogLocation::File(file) = log_location {
let decorator = PlainSyncDecorator::new(file);
let drain = FullFormat::new(decorator).build().fuse();
slog::Logger::root(drain, o!())
} else {
let decorator = PlainSyncDecorator::new(stderr());
let drain = FullFormat::new(decorator).build().fuse();
slog::Logger::root(drain, o!())
}
}
#[derive(Eq, Ord, PartialEq, PartialOrd)]
struct LogFile(DateTime<Utc>);
impl LogFile {
fn now() -> Self {
LogFile(Utc::now())
}
fn from_file_name(file_name: &str) -> Result<Self, ()> {
if let Ok(dt) = Utc.datetime_from_str(file_name, FILE_NAME_DATE_FORMAT) {
Ok(LogFile(dt))
} else {
Err(())
}
}
fn file_name(&self) -> String {
self.0.format(FILE_NAME_DATE_FORMAT).to_string()
}
fn path(&self) -> String {
format!("{}/{}", LOGS_DIR, self.file_name())
}
fn create_file(&self) -> File {
let path = self.path();
File::create(&path)
.unwrap_or_else(|_| panic!("failed to create log file: {}", path))
}
fn remove_file(&self) {
let path = self.path();
remove_file(&path)
.unwrap_or_else(|_| panic!("failed to delete log file: {}", path))
}
}
pub struct Logger {
logger: slog::Logger,
log_files: Vec<LogFile>,
log_count: usize,
check_file_size_at: usize,
}
impl Logger {
pub fn new(config: &Config) -> Self {
let (logger, log_files) = if config.log_to_file {
create_logs_dir();
let mut log_files = read_logs_dir();
let current_log_file = rotate_log_files(&mut log_files);
let logger = create_slog_logger(LogLocation::File(current_log_file));
(logger, log_files)
} else {
let logger = create_slog_logger(LogLocation::Stderr);
(logger, vec![])
};
Logger {
logger,
log_files,
log_count: 0,
check_file_size_at: INITIAL_CHECK_FILE_SIZE_AT,
}
}
fn logging_to_file(&self) -> bool {
!self.log_files.is_empty()
}
fn should_rotate_log_file(&mut self) -> bool {
if self.logging_to_file() {
if self.log_count >= self.check_file_size_at {
let path = self.log_files.last().unwrap().path();
let file_size = get_file_size_in_bytes(&path);
if file_size >= MAX_LOG_FILE_SIZE_BYTES {
return true;
}
let avg_bytes_per_log = file_size / self.log_count;
let remaining_bytes = MAX_LOG_FILE_SIZE_BYTES - file_size;
let remaining_logs = remaining_bytes / avg_bytes_per_log;
self.check_file_size_at += remaining_logs;
}
}
false
}
fn rotate_log_file(&mut self) {
let new_log_file = rotate_log_files(&mut self.log_files);
self.logger = create_slog_logger(LogLocation::File(new_log_file));
self.log_count = 0;
self.check_file_size_at = INITIAL_CHECK_FILE_SIZE_AT;
}
fn increment_log_count(&mut self) {
self.log_count += 1;
if self.should_rotate_log_file() {
self.rotate_log_file();
}
}
pub fn log_starting_poagov(&mut self) {
info!(&self.logger, "starting poagov...");
self.increment_log_count();
}
pub fn log_ctrlc(&mut self) {
warn!(&self.logger, "recieved ctrl-c signal, gracefully shutting down...");
self.increment_log_count();
}
pub fn log_no_email_recipients_configured(&mut self) {
warn!(&self.logger, "email notifications are enabled, but there are no email recipients");
self.increment_log_count();
}
pub fn log_notification_email_body(&mut self, notif: &Notification) {
info!(&self.logger, "governance notification\n{}", notif.email_text());
self.increment_log_count();
}
pub fn log_notification(&mut self, notif: &Notification) {
let ballot_created_log = notif.log();
info!(
&self.logger,
"governance notification";
"ballot" => format!("{:?}", ballot_created_log.ballot_type),
"ballot_id" => format!("{}", ballot_created_log.ballot_id),
"block_number" => format!("{}", ballot_created_log.block_number)
);
self.increment_log_count();
}
pub fn log_failed_to_build_email(&mut self, e: Error) {
warn!(&self.logger, "failed to build email"; "error" => format!("{:?}", e));
self.increment_log_count();
}
pub fn log_failed_to_send_email(&mut self, recipient: &str, e: Error) {
warn!(
&self.logger,
"failed to send email";
"recipient" => recipient,
"error" => format!("{:?}", e)
);
self.increment_log_count();
}
pub fn log_email_sent(&mut self, recipient: &str) {
info!(&self.logger, "email sent"; "to" => recipient);
self.increment_log_count();
}
pub fn log_reached_notification_limit(&mut self, notification_limit: usize) {
warn!(
&self.logger,
"reached notification limit, gracefully shutting down...";
"limit" => notification_limit
);
self.increment_log_count();
}
pub fn log_finished_block_window(&mut self, start: BlockNumber, stop: BlockNumber) {
let block_range = format!("{:?}...{:?}", start, stop);
info!(&self.logger, "finished checking blocks"; "block_range" => block_range);
self.increment_log_count();
}
}

View File

@ -1,37 +0,0 @@
use std::io::stderr;
use lettre::smtp;
use slog::{Drain, Logger};
use slog_term::{FullFormat, PlainSyncDecorator};
use notify::Notification;
lazy_static! {
pub static ref LOGGER: Logger = {
let log_decorator = PlainSyncDecorator::new(stderr());
let drain = FullFormat::new(log_decorator).build().fuse();
Logger::root(drain, o!())
};
}
pub fn log_notification(notif: &Notification) {
let notif_data = match *notif {
Notification::Keys(ref inner) => format!("{:#?}", inner),
Notification::Threshold(ref inner) => format!("{:#?}", inner),
Notification::Proxy(ref inner) => format!("{:#?}", inner)
};
info!(LOGGER, "notification"; "data" => notif_data);
}
pub fn log_email_sent(email: &str) {
info!(LOGGER, "email sent"; "to" => email);
}
pub fn log_email_failed(email: &str, error: smtp::error::Error) {
warn!(
LOGGER,
"email failed";
"to" => email,
"error" => format!("{}", error)
);
}

View File

@ -1,51 +1,124 @@
#![feature(try_from)]
extern crate chrono;
extern crate clap;
extern crate ctrlc;
extern crate dotenv;
extern crate ethabi;
extern crate ethereum_types;
extern crate failure;
extern crate hex;
extern crate jsonrpc_core;
#[macro_use] extern crate lazy_static;
extern crate lettre;
extern crate lettre_email;
extern crate native_tls;
extern crate reqwest;
extern crate serde;
extern crate serde_json;
#[macro_use] extern crate slog;
#[macro_use]
extern crate slog;
extern crate slog_term;
extern crate web3;
mod blockchain;
mod cli;
mod client;
mod config;
mod logging;
mod error;
mod logger;
mod notify;
mod rpc;
mod utils;
mod response;
use config::Config;
use notify::Notifier;
use rpc::{BlockchainIter, RpcClient};
use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicBool, Ordering};
fn main() {
let config = Config::load();
let client = RpcClient::new(&config.endpoint);
let mut notifier = Notifier::new(&config).unwrap();
use blockchain::BlockchainIter;
use cli::Cli;
use client::RpcClient;
use config::{Config, ContractVersion};
use error::{Error, Result};
use logger::Logger;
use notify::{Notification, Notifier};
fn load_env_file() {
if let Err(e) = dotenv::dotenv() {
match e {
dotenv::Error::Io(_) => panic!("could not find .env file"),
_ => panic!("coule not parse .env file"),
};
}
}
fn set_ctrlc_handler(logger: Arc<Mutex<Logger>>) -> Result<Arc<AtomicBool>> {
let running = Arc::new(AtomicBool::new(true));
let result = Ok(running.clone());
ctrlc::set_handler(move || {
logger.lock().unwrap().log_ctrlc();
running.store(false, Ordering::SeqCst);
}).map_err(|e| Error::CtrlcError(e))?;
result
}
fn main() -> Result<()> {
load_env_file();
for (start_block, stop_block) in BlockchainIter::new(&client, &config) {
for contract in &config.contracts {
let ballot_created_logs = client
.get_ballot_created_logs(contract, start_block, stop_block)
.unwrap();
let cli = Cli::parse();
let config = Config::new(&cli)?;
let logger = Arc::new(Mutex::new(Logger::new(&config)));
if config.email_notifications && config.email_recipients.is_empty() {
logger.lock().unwrap().log_no_email_recipients_configured();
}
let running = set_ctrlc_handler(logger.clone())?;
let client = RpcClient::new(config.endpoint.clone());
let mut notifier = Notifier::new(&config, logger.clone())?;
logger.lock().unwrap().log_starting_poagov();
for log in &ballot_created_logs {
let voting_data = client.get_voting_state(contract, log.ballot_id).unwrap();
let notif = notifier.build_notification(log, &voting_data);
notifier.notify_validators(&notif);
'main_loop: for iter_res in BlockchainIter::new(&client, &config, running)? {
let (start_block, stop_block) = iter_res?;
let mut notifications = vec![];
for contract in config.contracts.iter() {
let ballot_created_logs = client.get_ballot_created_logs(
contract,
start_block,
stop_block,
)?;
for log in ballot_created_logs.into_iter() {
let notification = if contract.version == ContractVersion::V1 {
let voting_state = client.get_voting_state(contract, log.ballot_id)?;
Notification::from_voting_state(&config, log, voting_state)
} else {
let ballot_info = client.get_ballot_info(contract, log.ballot_id)?;
Notification::from_ballot_info(&config, log, ballot_info)
};
notifications.push(notification);
}
}
notifications.sort_unstable_by(|notif1, notif2| {
notif1.log().block_number.cmp(&notif2.log().block_number)
});
for notification in notifications.iter() {
notifier.notify(notification);
if notifier.reached_limit() {
let limit = config.notification_limit.unwrap();
logger.lock().unwrap().log_reached_notification_limit(limit);
break 'main_loop;
}
}
logger.lock().unwrap().log_finished_block_window(start_block, stop_block);
}
Ok(())
}
#[cfg(test)]
pub mod tests {
use super::load_env_file;
static mut LOADED_ENV_FILE: bool = false;
pub fn setup() {
unsafe {
if !LOADED_ENV_FILE {
load_env_file();
LOADED_ENV_FILE = true;
}
}
}
}

View File

@ -1,199 +1,187 @@
use chrono::{DateTime, Utc};
use ethereum_types::Address;
use lettre::{EmailTransport, SmtpTransport};
use lettre::smtp::{ClientSecurity, ConnectionReuseParameters, SmtpTransportBuilder};
use std::sync::{Arc, Mutex};
use lettre::{SendableEmail, Transport};
use lettre::smtp::{ClientSecurity, ConnectionReuseParameters, SmtpClient, SmtpTransport};
use lettre::smtp::authentication::{Credentials, Mechanism};
use lettre::smtp::client::net::{ClientTlsParameters, DEFAULT_TLS_PROTOCOLS};
use lettre::smtp::error::Error as BuildSmtpError;
use lettre::smtp::client::net::ClientTlsParameters;
use lettre_email::{Email, EmailBuilder};
use lettre_email::error::Error as BuildEmailError;
use native_tls::TlsConnector;
use config::{Config, ContractType, Network, Validator};
use logging::{log_email_failed, log_email_sent, log_notification};
use rpc::{BallotCreatedLog, BallotType, KeyType, VotingData};
use config::Config;
use error::{Error, Result};
use logger::Logger;
use response::common::BallotCreatedLog;
use response::v1::VotingState;
use response::v2::BallotInfo;
#[derive(Debug)]
pub enum Notification {
Keys(KeysNotification),
Threshold(ThresholdNotification),
Proxy(ProxyNotification)
#[derive(Clone, Debug)]
pub enum Notification<'a> {
VotingState {
config: &'a Config,
log: BallotCreatedLog,
voting_state: VotingState,
},
BallotInfo {
config: &'a Config,
log: BallotCreatedLog,
ballot_info: BallotInfo,
},
}
#[derive(Debug)]
pub struct KeysNotification {
pub network: Network,
pub endpoint: String,
pub block_number: u64,
pub contract_type: ContractType,
pub ballot_type: BallotType,
pub ballot_id: u64,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub memo: String,
pub affected_key: Address,
pub affected_key_type: KeyType
}
impl<'a> Notification<'a> {
pub fn from_voting_state(
config: &'a Config,
log: BallotCreatedLog,
voting_state: VotingState,
) -> Self
{
Notification::VotingState { config, log, voting_state }
}
pub fn from_ballot_info(
config: &'a Config,
log: BallotCreatedLog,
ballot_info: BallotInfo,
) -> Self
{
Notification::BallotInfo { config, log, ballot_info }
}
#[derive(Debug)]
pub struct ThresholdNotification {
pub network: Network,
pub endpoint: String,
pub block_number: u64,
pub contract_type: ContractType,
pub ballot_type: BallotType,
pub ballot_id: u64,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub memo: String,
pub proposed_value: u64
}
pub fn email_text(&self) -> String {
format!(
"Network: {:?}\n\
RPC Endpoint: {}\n\
Block Number: {}\n\
Contract: {}\n\
Version: {:?}\n\
Ballot ID: {}\n\
{}\n",
self.config().network,
self.config().endpoint,
self.log().block_number,
self.contract_name(),
self.config().version,
self.log().ballot_id,
self.email_body(),
)
}
#[derive(Debug)]
pub struct ProxyNotification {
pub network: Network,
pub endpoint: String,
pub block_number: u64,
pub contract_type: ContractType,
pub ballot_type: BallotType,
pub ballot_id: u64,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub memo: String,
pub proposed_value: Address
}
fn config(&self) -> &Config {
match self {
Notification::VotingState { config, .. } => config,
Notification::BallotInfo { config, .. } => config,
}
}
impl Notification {
fn new(config: &Config, log: &BallotCreatedLog, voting_data: &VotingData) -> Self {
let network = config.network;
let endpoint = config.endpoint.clone();
pub fn log(&self) -> &BallotCreatedLog {
match self {
Notification::VotingState { log, .. } => log,
Notification::BallotInfo { log, .. } => log,
}
}
fn contract_name(&self) -> String {
match self {
Notification::VotingState { voting_state, .. } => voting_state.contract_name(),
Notification::BallotInfo { ballot_info, .. } => ballot_info.contract_name(),
}
}
let block_number = log.block_number;
let ballot_type = log.ballot_type;
let ballot_id = log.ballot_id;
let start_time = voting_data.start_time();
let end_time = voting_data.end_time();
let memo = voting_data.memo();
match *voting_data {
VotingData::Keys(ref data) => {
let contract_type = ContractType::Keys;
let affected_key = data.affected_key;
let affected_key_type = data.affected_key_type;
let notification = KeysNotification {
network, endpoint, block_number,
contract_type, ballot_type, ballot_id,
start_time, end_time, memo,
affected_key, affected_key_type
};
Notification::Keys(notification)
},
VotingData::Threshold(ref data) => {
let contract_type = ContractType::Threshold;
let proposed_value = data.proposed_value;
let notification = ThresholdNotification {
network, endpoint, block_number,
contract_type, ballot_type,
ballot_id, start_time, end_time,
memo, proposed_value
};
Notification::Threshold(notification)
},
VotingData::Proxy(ref data) => {
let contract_type = ContractType::Proxy;
let proposed_value = data.proposed_value;
let notification = ProxyNotification {
network, endpoint, block_number,
contract_type, ballot_type,
ballot_id, start_time, end_time,
memo, proposed_value
};
Notification::Proxy(notification)
}
fn email_body(&self) -> String {
match self {
Notification::VotingState { voting_state, .. } => voting_state.email_text(),
Notification::BallotInfo { ballot_info, .. } => ballot_info.email_text(),
}
}
}
pub struct Notifier<'a> {
config: &'a Config,
mailer: Option<SmtpTransport>
emailer: Option<SmtpTransport>,
logger: Arc<Mutex<Logger>>,
notification_count: usize,
}
impl<'a> Notifier<'a> {
pub fn new(config: &'a Config) -> Result<Self, BuildSmtpError> {
let mut notifier = Notifier { config, mailer: None };
if config.send_email_notifications {
let smtp_addr = (config.smtp_host_domain.as_str(), config.smtp_port);
let smtp_tls = {
let mut tls_builder = TlsConnector::builder().unwrap();
tls_builder.supported_protocols(DEFAULT_TLS_PROTOCOLS).unwrap();
let tls = tls_builder.build().unwrap();
let tls_params = ClientTlsParameters::new(config.smtp_host_domain.clone(), tls);
ClientSecurity::Required(tls_params)
pub fn new(config: &'a Config, logger: Arc<Mutex<Logger>>) -> Result<Self> {
let emailer = if config.email_notifications {
let domain = config.smtp_host_domain.clone().unwrap();
let port = config.smtp_port.unwrap();
let addr = (domain.as_str(), port);
let security = {
let tls = TlsConnector::new().map_err(|e| Error::FailedToBuildTls(e))?;
let smtp_security_setup = ClientTlsParameters::new(domain.clone(), tls);
ClientSecurity::Required(smtp_security_setup)
};
let smtp_creds = Credentials::new(
config.smtp_username.clone(),
config.smtp_password.clone()
let creds = Credentials::new(
config.smtp_username.clone().unwrap(),
config.smtp_password.clone().unwrap(),
);
let mailer = SmtpTransportBuilder::new(smtp_addr, smtp_tls)?
let smtp = SmtpClient::new(addr, security)
.map_err(|e| Error::FailedToResolveSmtpHostDomain(e))?
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
.authentication_mechanism(Mechanism::Plain)
.credentials(smtp_creds)
.build();
.credentials(creds)
.transport();
Some(smtp)
} else {
None
};
Ok(Notifier { config, emailer, logger, notification_count: 0 })
}
notifier.mailer = Some(mailer);
pub fn notify(&mut self, notif: &Notification) {
if self.config.log_emails {
self.logger.lock().unwrap().log_notification_email_body(notif);
} else {
self.logger.lock().unwrap().log_notification(notif);
}
Ok(notifier)
}
pub fn build_notification(&self, log: &BallotCreatedLog, voting_data: &VotingData) -> Notification {
Notification::new(&self.config, log, voting_data)
}
pub fn notify_validators(&mut self, notif: &Notification) {
log_notification(notif);
for validator in &self.config.validators {
if self.config.send_email_notifications {
let email = self.build_email(validator, notif).unwrap();
if let Some(ref mut mailer) = self.mailer {
match mailer.send(&email) {
Ok(_) => log_email_sent(&validator.email),
Err(e) => log_email_failed(&validator.email, e)
};
if self.config.email_notifications {
for recipient in self.config.email_recipients.iter() {
let email: SendableEmail = match self.build_email(notif, recipient) {
Ok(email) => email.into(),
Err(e) => {
self.logger.lock().unwrap().log_failed_to_build_email(e);
continue;
},
};
if let Err(e) = self.send_email(email) {
self.logger.lock().unwrap().log_failed_to_send_email(recipient, e);
} else {
self.logger.lock().unwrap().log_email_sent(recipient);
}
}
}
self.notification_count += 1;
}
if self.config.send_push_notifications {
println!("Push Notifications not yet implemented.");
}
pub fn reached_limit(&self) -> bool {
if let Some(limit) = self.config.notification_limit {
self.notification_count >= limit
} else {
false
}
}
fn build_email(&self, validator: &Validator, notif: &Notification) -> Result<Email, BuildEmailError> {
let body = match *notif {
Notification::Keys(ref inner) => format!("{:#?}\n", inner),
Notification::Threshold(ref inner) => format!("{:#?}\n", inner),
Notification::Proxy(ref inner) => format!("{:#?}\n", inner)
};
fn build_email(&self, notif: &Notification, recipient: &str) -> Result<Email> {
let outgoing_email = self.config.outgoing_email_addr.clone().unwrap();
EmailBuilder::new()
.to(validator.email.as_str())
.from(self.config.outgoing_email.as_str())
.to(recipient)
.from(outgoing_email.as_str())
.subject("POA Network Governance Notification")
.text(body)
.text(notif.email_text())
.build()
.map_err(|e| Error::FailedToBuildEmail(e))
}
}
impl<'a> Drop for Notifier<'a> {
fn drop(&mut self) {
if let Some(ref mut mailer) = self.mailer {
mailer.close();
fn send_email(&mut self, email: SendableEmail) -> Result<()> {
if let Some(ref mut emailer) = self.emailer {
match emailer.send(email) {
Ok(_response) => Ok(()),
Err(e) => Err(Error::FailedToSendEmail(e)),
}
} else {
unreachable!("Attempted to send email without SMTP client setup");
}
}
}

137
src/response/common.rs Normal file
View File

@ -0,0 +1,137 @@
use chrono::{DateTime, NaiveDateTime, Utc};
use ethabi;
use web3::types::{Address, H256, U256};
use error::{Error, Result};
/// Converts a `U256` timestamp to a UTC `DateTime`.
pub fn u256_to_datetime(uint: U256) -> DateTime<Utc> {
let timestamp = uint.low_u64() as i64;
let naive = NaiveDateTime::from_timestamp(timestamp, 0);
DateTime::from_utc(naive, Utc)
}
/// Identifies what type of key is being voted on by the `votingToChangeKeys.sol` contract. This
/// enum is used in the V1 and V2 Keys contracts.
///
/// V1 Keys Contract (`KeyType` is used within the contract's `votingState`):
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/aa45e19ca50f7cae308c1281d950245b0c65182a/contracts/VotingToChangeKeys.sol#L11
///
/// V2 `KeyTypes` enum (used by the V2 Keys Contract's `ballotInfo`):
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/ec307069302fdf6647e8b1bdc13093960913b266/contracts/abstracts/EnumKeyTypes.sol#L5
#[derive(Clone, Debug)]
pub enum KeyType {
InvalidKey,
MiningKey,
VotingKey,
PayoutKey,
}
impl From<U256> for KeyType {
fn from(key_type: U256) -> Self {
match key_type.low_u64() {
0 => KeyType::InvalidKey,
1 => KeyType::MiningKey,
2 => KeyType::VotingKey,
3 => KeyType::PayoutKey,
n => unreachable!("unrecognized `KeyType`: {}", n),
}
}
}
/// V1 Keys Contract (used in `BallotCreated` event and within the `votingState`):
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/aa45e19ca50f7cae308c1281d950245b0c65182a/contracts/VotingToChangeKeys.sol#L10
///
/// V1 Threshold Contract (used in `BallotCreated` event):
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/aa45e19ca50f7cae308c1281d950245b0c65182a/contracts/VotingToChangeMinThreshold.sol#L89
///
/// V1 Proxy Contract (used in `BallotCreated` event):
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/aa45e19ca50f7cae308c1281d950245b0c65182a/contracts/VotingToChangeProxyAddress.sol#L85
///
/// Note: V1 contracts do not use the `Emission` variant.
///
/// V2 - all contracts use the same enum:
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/ec307069302fdf6647e8b1bdc13093960913b266/contracts/abstracts/EnumBallotTypes.sol#L5
#[derive(Clone, Copy, Debug)]
pub enum BallotType {
InvalidKey,
AddKey,
RemoveKey,
SwapKey,
Threshold,
Proxy,
Emission,
}
/// Converts a `U256` (from a V2 keys contract's voting-state) into a `BallotType`.
impl From<U256> for BallotType {
fn from(uint: U256) -> Self {
BallotType::from(H256::from(uint))
}
}
/// Converts an `H256` from a `web3::types::Log`'s `topics` vector to a `BallotType`.
impl From<H256> for BallotType {
fn from(topic: H256) -> Self {
match topic.low_u64() {
0 => BallotType::InvalidKey,
1 => BallotType::AddKey,
2 => BallotType::RemoveKey,
3 => BallotType::SwapKey,
4 => BallotType::Threshold,
5 => BallotType::Proxy,
6 => BallotType::Emission,
n => unreachable!("unrecognized `BallotType`: {}", n),
}
}
}
/// A parsed `BallotCreated` event log. All V1 and V2 contracts use the same `BallotCreated` event.
///
/// V1 Keys Contract's `BallotCreated` event:
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/aa45e19ca50f7cae308c1281d950245b0c65182a/contracts/VotingToChangeKeys.sol#L45
///
/// V1 Threshold Contract's `BallotCreated` event:
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/aa45e19ca50f7cae308c1281d950245b0c65182a/contracts/VotingToChangeMinThreshold.sol#L40
///
/// V1 Proxy Contract's `BallotCreated` event:
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/aa45e19ca50f7cae308c1281d950245b0c65182a/contracts/VotingToChangeMinThreshold.sol#L40
///
/// V2 - all contracts use the same `BallotCreated` event:
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/ec307069302fdf6647e8b1bdc13093960913b266/contracts/abstracts/VotingTo.sol#L30
#[derive(Clone, Copy, Debug)]
pub struct BallotCreatedLog {
pub block_number: U256,
pub ballot_id: U256,
pub ballot_type: BallotType,
pub creator: Address,
}
impl BallotCreatedLog {
pub fn from_ethabi_log(log: ethabi::Log, block_number: U256) -> Result<Self> {
let mut ballot_id: Option<U256> = None;
let mut ballot_type: Option<BallotType> = None;
let mut creator: Option<Address> = None;
for ethabi::LogParam { name, value } in log.params {
match name.as_ref() {
"id" => ballot_id = value.to_uint(),
"ballotType" => ballot_type = value.to_uint().map(BallotType::from),
"creator" => creator = value.to_address(),
name => unreachable!("Found unknown `BallotCreated` event log field: {}", name),
};
}
let ballot_id = match ballot_id {
Some(id) => id,
None => return Err(Error::FailedToParseBallotCreatedLog("missing `id`".into())),
};
let ballot_type = match ballot_type {
Some(ballot_type) => ballot_type,
None => return Err(Error::FailedToParseBallotCreatedLog("missing `ballot_type`".into())),
};
let creator = match creator {
Some(creator) => creator,
None => return Err(Error::FailedToParseBallotCreatedLog("missing `creator`".into())),
};
Ok(BallotCreatedLog { ballot_id, ballot_type, creator, block_number })
}
}

3
src/response/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod common;
pub mod v1;
pub mod v2;

323
src/response/v1.rs Normal file
View File

@ -0,0 +1,323 @@
use chrono::{DateTime, Utc};
use ethabi;
use web3::types::{Address, U256};
use response::common::{u256_to_datetime, BallotType, KeyType};
/// Describes the current state of a given ballot.
///
/// The same `QuorumStates` enum is used by all V1 contracts.
///
/// V1 Keys Contract:
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/aa45e19ca50f7cae308c1281d950245b0c65182a/contracts/VotingToChangeKeys.sol#L12
///
/// V1 Threshold Contract:
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/aa45e19ca50f7cae308c1281d950245b0c65182a/contracts/VotingToChangeMinThreshold.sol#L10
///
/// V1 Proxy Contract:
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/aa45e19ca50f7cae308c1281d950245b0c65182a/contracts/VotingToChangeProxyAddress.sol#L10
#[derive(Clone, Copy, Debug)]
pub enum QuorumState {
Invalid,
InProgress,
Accepted,
Rejected,
}
impl From<U256> for QuorumState {
fn from(uint: U256) -> Self {
match uint.low_u64() {
0 => QuorumState::Invalid,
1 => QuorumState::InProgress,
2 => QuorumState::Accepted,
3 => QuorumState::Rejected,
_ => unreachable!("unrecognized `QuorumState`: {}", uint),
}
}
}
#[derive(Clone, Debug)]
pub enum VotingState {
Keys(KeysVotingState),
Threshold(ThresholdVotingState),
Proxy(ProxyVotingState),
}
impl From<KeysVotingState> for VotingState {
fn from(keys_voting_state: KeysVotingState) -> Self {
VotingState::Keys(keys_voting_state)
}
}
impl From<ThresholdVotingState> for VotingState {
fn from(threshold_voting_state: ThresholdVotingState) -> Self {
VotingState::Threshold(threshold_voting_state)
}
}
impl From<ProxyVotingState> for VotingState {
fn from(proxy_voting_state: ProxyVotingState) -> Self {
VotingState::Proxy(proxy_voting_state)
}
}
impl VotingState {
pub fn contract_name(&self) -> String {
match self {
VotingState::Keys(_) => "VotingToChangeKeys.sol".into(),
VotingState::Threshold(_) => "VotingToChangeMinThreshold.sol".into(),
VotingState::Proxy(_) => "VotingToChangeProxyAddress.sol".into(),
}
}
pub fn email_text(&self) -> String {
match self {
VotingState::Keys(state) => state.email_text(),
VotingState::Threshold(state) => state.email_text(),
VotingState::Proxy(state) => state.email_text(),
}
}
}
/// V1 Key's Contract:
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/aa45e19ca50f7cae308c1281d950245b0c65182a/contracts/VotingToChangeKeys.sol#L22
#[derive(Clone, Debug)]
pub struct KeysVotingState {
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub affected_key: Address,
pub affected_key_type: KeyType,
pub mining_key: Address,
pub total_voters: U256,
pub progress: U256,
pub is_finalized: bool,
pub quorum_state: QuorumState,
pub ballot_type: BallotType,
pub index: U256,
pub min_threshold_of_voters: U256,
pub creator: Address,
pub memo: String
}
impl From<Vec<ethabi::Token>> for KeysVotingState {
fn from(tokens: Vec<ethabi::Token>) -> Self {
let start_time = {
let uint = tokens[0].clone().to_uint().unwrap();
u256_to_datetime(uint)
};
let end_time = {
let uint = tokens[1].clone().to_uint().unwrap();
u256_to_datetime(uint)
};
let affected_key = tokens[2].clone().to_address().unwrap();
let affected_key_type = tokens[3].clone().to_uint().unwrap().into();
let mining_key = tokens[4].clone().to_address().unwrap();
let total_voters = tokens[5].clone().to_uint().unwrap();
let progress = tokens[6].clone().to_int().unwrap();
let is_finalized = tokens[7].clone().to_bool().unwrap();
let quorum_state = tokens[8].clone().to_uint().unwrap().into();
let ballot_type = tokens[9].clone().to_uint().unwrap().into();
let index = tokens[10].clone().to_uint().unwrap();
let min_threshold_of_voters = tokens[11].clone().to_uint().unwrap();
let creator = tokens[12].clone().to_address().unwrap();
let memo = tokens[13].clone().to_string().unwrap();
KeysVotingState {
start_time,
end_time,
affected_key,
affected_key_type,
mining_key,
total_voters,
progress,
is_finalized,
quorum_state,
ballot_type,
index,
min_threshold_of_voters,
creator,
memo,
}
}
}
impl KeysVotingState {
fn email_text(&self) -> String {
format!(
"Voting Start Time: {}\n\
Voting End Time: {}\n\
Ballot Type: {:?}\n\
Affected Key: {:?}\n\
Affected Key Type: {:?}\n\
Voting has Finished: {}\n\
Number of Votes Made: {}\n\
Number of Votes Required to Make Change: {}\n\
Mining Key: {:?}\n\
Ballot Creator: {:?}\n\
Memo: {}\n",
self.start_time,
self.end_time,
self.ballot_type,
self.affected_key,
self.affected_key_type,
self.is_finalized,
self.total_voters,
self.min_threshold_of_voters,
self.mining_key,
self.creator,
self.memo,
)
}
}
/// V1 Threshold Contract:
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/aa45e19ca50f7cae308c1281d950245b0c65182a/contracts/VotingToChangeMinThreshold.sol#L20
#[derive(Clone, Debug)]
pub struct ThresholdVotingState {
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub total_voters: U256,
pub progress: U256,
pub is_finalized: bool,
pub quorum_state: QuorumState,
pub index: U256,
pub min_threshold_of_voters: U256,
pub proposed_value: U256,
pub creator: Address,
pub memo: String,
}
impl From<Vec<ethabi::Token>> for ThresholdVotingState {
fn from(tokens: Vec<ethabi::Token>) -> Self {
let start_time = {
let uint = tokens[0].clone().to_uint().unwrap();
u256_to_datetime(uint)
};
let end_time = {
let uint = tokens[1].clone().to_uint().unwrap();
u256_to_datetime(uint)
};
let total_voters = tokens[2].clone().to_uint().unwrap();
let progress = tokens[3].clone().to_int().unwrap();
let is_finalized = tokens[4].clone().to_bool().unwrap();
let quorum_state = tokens[5].clone().to_uint().unwrap().into();
let index = tokens[6].clone().to_uint().unwrap();
let min_threshold_of_voters = tokens[7].clone().to_uint().unwrap();
let proposed_value = tokens[8].clone().to_uint().unwrap();
let creator = tokens[9].clone().to_address().unwrap();
let memo = tokens[10].clone().to_string().unwrap();
ThresholdVotingState {
start_time,
end_time,
total_voters,
progress,
is_finalized,
quorum_state,
index,
min_threshold_of_voters,
proposed_value,
creator,
memo,
}
}
}
impl ThresholdVotingState {
fn email_text(&self) -> String {
format!(
"Voting Start Time: {}\n\
Voting End Time: {}\n\
Proposed New Min. Threshold: {}\n\
Voting has Finished: {}\n\
Number of Votes Made: {}\n\
Number of Votes Required to Make Change: {}\n\
Ballot Creator: {:?}\n\
Memo: {}\n",
self.start_time,
self.end_time,
self.proposed_value,
self.is_finalized,
self.total_voters,
self.min_threshold_of_voters,
self.creator,
self.memo,
)
}
}
/// V1 Proxy Contract:
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/aa45e19ca50f7cae308c1281d950245b0c65182a/contracts/VotingToChangeProxyAddress.sol#L19
#[derive(Clone, Debug)]
pub struct ProxyVotingState {
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub total_voters: U256,
pub progress: U256,
pub is_finalized: bool,
pub quorum_state: QuorumState,
pub index: U256,
pub min_threshold_of_voters: U256,
pub proposed_value: Address,
pub contract_type: U256,
pub creator: Address,
pub memo: String,
}
impl From<Vec<ethabi::Token>> for ProxyVotingState {
fn from(tokens: Vec<ethabi::Token>) -> Self {
let start_time = {
let uint = tokens[0].clone().to_uint().unwrap();
u256_to_datetime(uint)
};
let end_time = {
let uint = tokens[1].clone().to_uint().unwrap();
u256_to_datetime(uint)
};
let total_voters = tokens[2].clone().to_uint().unwrap();
let progress = tokens[3].clone().to_int().unwrap();
let is_finalized = tokens[4].clone().to_bool().unwrap();
let quorum_state = tokens[5].clone().to_uint().unwrap().into();
let index = tokens[6].clone().to_uint().unwrap();
let min_threshold_of_voters = tokens[7].clone().to_uint().unwrap();
let proposed_value = tokens[8].clone().to_address().unwrap();
let contract_type = tokens[9].clone().to_uint().unwrap();
let creator = tokens[10].clone().to_address().unwrap();
let memo = tokens[11].clone().to_string().unwrap();
ProxyVotingState {
start_time,
end_time,
total_voters,
progress,
is_finalized,
quorum_state,
index,
min_threshold_of_voters,
proposed_value,
contract_type,
creator,
memo,
}
}
}
impl ProxyVotingState {
fn email_text(&self) -> String {
format!(
"Voting Start Time: {}\n\
Voting End Time: {}\n\
Proposed New Proxy Address: {:?}\n\
Voting has Finished: {}\n\
Number of Votes Made: {}\n\
Number of Votes Required for Change: {}\n\
Ballot Creator: {:?}\n\
Memo: {}\n",
self.start_time,
self.end_time,
self.proposed_value,
self.is_finalized,
self.total_voters,
self.min_threshold_of_voters,
self.creator,
self.memo,
)
}
}

397
src/response/v2.rs Normal file
View File

@ -0,0 +1,397 @@
use chrono::{DateTime, Utc};
use ethabi;
use web3::types::{Address, U256};
use response::common::{u256_to_datetime, BallotType, KeyType};
#[derive(Clone, Debug)]
pub enum BallotInfo {
Keys(KeysBallotInfo),
Threshold(ThresholdBallotInfo),
Proxy(ProxyBallotInfo),
Emission(EmissionBallotInfo),
}
impl From<KeysBallotInfo> for BallotInfo {
fn from(keys_ballot_info: KeysBallotInfo) -> Self {
BallotInfo::Keys(keys_ballot_info)
}
}
impl From<ThresholdBallotInfo> for BallotInfo {
fn from(threshold_ballot_info: ThresholdBallotInfo) -> Self {
BallotInfo::Threshold(threshold_ballot_info)
}
}
impl From<ProxyBallotInfo> for BallotInfo {
fn from(proxy_ballot_info: ProxyBallotInfo) -> Self {
BallotInfo::Proxy(proxy_ballot_info)
}
}
impl From<EmissionBallotInfo> for BallotInfo {
fn from(emission_ballot_info: EmissionBallotInfo) -> Self {
BallotInfo::Emission(emission_ballot_info)
}
}
impl BallotInfo {
pub fn contract_name(&self) -> String {
match self {
BallotInfo::Keys(_) => "VotingToChangeKeys.sol".into(),
BallotInfo::Threshold(_) => "VotingToChangeMinThreshold.sol".into(),
BallotInfo::Proxy(_) => "VotingToChangeProxyAddress.sol".into(),
BallotInfo::Emission(_) => "VotingToManageEmissionFunds.sol".into(),
}
}
pub fn email_text(&self) -> String {
match self {
BallotInfo::Keys(info) => info.email_text(),
BallotInfo::Threshold(info) => info.email_text(),
BallotInfo::Proxy(info) => info.email_text(),
BallotInfo::Emission(info) => info.email_text(),
}
}
}
/// Returned by the V2 Keys contract's `.getBallotInfo()` function:
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/ec307069302fdf6647e8b1bdc13093960913b266/contracts/VotingToChangeKeys.sol#L7
#[derive(Clone, Debug)]
pub struct KeysBallotInfo {
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub affected_key: Address,
pub affected_key_type: KeyType,
pub new_voting_key: Address,
pub new_payout_key: Address,
pub mining_key: Address,
pub total_voters: U256,
pub progress: U256,
pub is_finalized: bool,
pub ballot_type: BallotType,
pub creator: Address,
pub memo: String,
pub can_be_finalized_now: bool,
}
impl From<Vec<ethabi::Token>> for KeysBallotInfo {
fn from(tokens: Vec<ethabi::Token>) -> Self {
let start_time = {
let uint = tokens[0].clone().to_uint().unwrap();
u256_to_datetime(uint)
};
let end_time = {
let uint = tokens[1].clone().to_uint().unwrap();
u256_to_datetime(uint)
};
let affected_key = tokens[2].clone().to_address().unwrap();
let affected_key_type = tokens[3].clone().to_uint().unwrap().into();
let new_voting_key = tokens[4].clone().to_address().unwrap();
let new_payout_key = tokens[5].clone().to_address().unwrap();
let mining_key = tokens[6].clone().to_address().unwrap();
let total_voters = tokens[7].clone().to_uint().unwrap();
let progress = tokens[8].clone().to_int().unwrap();
let is_finalized = tokens[9].clone().to_bool().unwrap();
let ballot_type = tokens[10].clone().to_uint().unwrap().into();
let creator = tokens[11].clone().to_address().unwrap();
let memo = tokens[12].clone().to_string().unwrap();
let can_be_finalized_now = tokens[13].clone().to_bool().unwrap();
KeysBallotInfo {
start_time,
end_time,
affected_key,
affected_key_type,
new_voting_key,
new_payout_key,
mining_key,
total_voters,
progress,
is_finalized,
ballot_type,
creator,
memo,
can_be_finalized_now,
}
}
}
impl KeysBallotInfo {
fn email_text(&self) -> String {
format!(
"Voting Start Time: {}\n\
Voting End Time: {}\n\
Ballot Type: {:?}\n\
Affected Key: {:?}\n\
Affected Key Type: {:?}\n\
New Voting Key: {:?}\n\
New Payout Key: {:?}\n\
Voting has Finished: {}\n\
Number of Votes Made: {}\n\
Mining Key: {:?}\n\
Ballot Creator: {:?}\n\
Memo: {}\n",
self.start_time,
self.end_time,
self.ballot_type,
self.affected_key,
self.affected_key_type,
self.new_voting_key,
self.new_payout_key,
self.is_finalized,
self.total_voters,
self.mining_key,
self.creator,
self.memo,
)
}
}
/// Returned by the V2 Threshold Contract's `.getBallotInfo()` function:
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/ec307069302fdf6647e8b1bdc13093960913b266/contracts/VotingToChangeMinThreshold.sol#L30
#[derive(Clone, Debug)]
pub struct ThresholdBallotInfo {
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub total_voters: U256,
pub progress: U256,
pub is_finalized: bool,
pub proposed_value: U256,
pub creator: Address,
pub memo: String,
pub can_be_finalized_now: bool,
// pub already_voted: bool,
}
impl From<Vec<ethabi::Token>> for ThresholdBallotInfo {
fn from(tokens: Vec<ethabi::Token>) -> Self {
let start_time = {
let uint = tokens[0].clone().to_uint().unwrap();
u256_to_datetime(uint)
};
let end_time = {
let uint = tokens[1].clone().to_uint().unwrap();
u256_to_datetime(uint)
};
let total_voters = tokens[2].clone().to_uint().unwrap();
let progress = tokens[3].clone().to_uint().unwrap();
let is_finalized = tokens[4].clone().to_bool().unwrap();
let proposed_value = tokens[5].clone().to_uint().unwrap();
let creator = tokens[6].clone().to_address().unwrap();
let memo = tokens[7].clone().to_string().unwrap();
let can_be_finalized_now = tokens[8].clone().to_bool().unwrap();
// let already_voted = tokens[9].clone().to_bool().unwrap();
ThresholdBallotInfo {
start_time,
end_time,
total_voters,
progress,
is_finalized,
proposed_value,
creator,
memo,
can_be_finalized_now,
// already_voted,
}
}
}
impl ThresholdBallotInfo {
fn email_text(&self) -> String {
format!(
"Voting Start Time: {}\n\
Voting End Time: {}\n\
Proposed New Min. Threshold: {}\n\
Voting has Finished: {}\n\
Number of Votes Made: {}\n\
Ballot Creator: {:?}\n\
Memo: {}\n",
self.start_time,
self.end_time,
self.proposed_value,
self.is_finalized,
self.total_voters,
self.creator,
self.memo,
)
}
}
/// Returned by the V2 Proxy Contract's `.getBallotInfo()` function:
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/ec307069302fdf6647e8b1bdc13093960913b266/contracts/VotingToChangeProxyAddress.sol#L30
#[derive(Clone, Debug)]
pub struct ProxyBallotInfo {
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub total_voters: U256,
pub progress: U256,
pub is_finalized: bool,
pub proposed_value: Address,
pub contract_type: U256,
pub creator: Address,
pub memo: String,
pub can_be_finalized_now: bool,
// pub already_voted: bool,
}
impl From<Vec<ethabi::Token>> for ProxyBallotInfo {
fn from(tokens: Vec<ethabi::Token>) -> Self {
let start_time = {
let uint = tokens[0].clone().to_uint().unwrap();
u256_to_datetime(uint)
};
let end_time = {
let uint = tokens[1].clone().to_uint().unwrap();
u256_to_datetime(uint)
};
let total_voters = tokens[2].clone().to_uint().unwrap();
let progress = tokens[3].clone().to_uint().unwrap();
let is_finalized = tokens[4].clone().to_bool().unwrap();
let proposed_value = tokens[5].clone().to_address().unwrap();
let contract_type = tokens[6].clone().to_uint().unwrap();
let creator = tokens[7].clone().to_address().unwrap();
let memo = tokens[8].clone().to_string().unwrap();
let can_be_finalized_now = tokens[9].clone().to_bool().unwrap();
// let already_voted = tokens[10].clone().to_bool().unwrap();
ProxyBallotInfo {
start_time,
end_time,
total_voters,
progress,
is_finalized,
proposed_value,
contract_type,
creator,
memo,
can_be_finalized_now,
// already_voted,
}
}
}
impl ProxyBallotInfo {
fn email_text(&self) -> String {
format!(
"Voting Start Time: {}\n\
Voting End Time: {}\n\
Proposed New Proxy Address: {:?}\n\
Voting has Finished: {}\n\
Number of Votes Made: {}\n\
Ballot Creator: {:?}\n\
Memo: {}\n",
self.start_time,
self.end_time,
self.proposed_value,
self.is_finalized,
self.total_voters,
self.creator,
self.memo,
)
}
}
/// Returned by the V2 Emission Contract's `.getBallotInfo()` function:
/// https://github.com/poanetwork/poa-network-consensus-contracts/blob/ec307069302fdf6647e8b1bdc13093960913b266/contracts/VotingToManageEmissionFunds.sol#L126
#[derive(Clone, Debug)]
pub struct EmissionBallotInfo {
pub creation_time: DateTime<Utc>,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub is_canceled: bool,
pub is_finalized: bool,
pub creator: Address,
pub memo: String,
pub amount: U256,
pub burn_votes: U256,
pub freeze_votes: U256,
pub send_votes: U256,
pub receiver: Address,
}
impl From<Vec<ethabi::Token>> for EmissionBallotInfo {
fn from(tokens: Vec<ethabi::Token>) -> Self {
let creation_time = {
let uint = tokens[0].clone().to_uint().unwrap();
u256_to_datetime(uint)
};
let start_time = {
let uint = tokens[1].clone().to_uint().unwrap();
u256_to_datetime(uint)
};
let end_time = {
let uint = tokens[2].clone().to_uint().unwrap();
u256_to_datetime(uint)
};
let is_canceled = tokens[3].clone().to_bool().unwrap();
let is_finalized = tokens[4].clone().to_bool().unwrap();
let creator = tokens[5].clone().to_address().unwrap();
let memo = tokens[6].clone().to_string().unwrap();
let amount = tokens[7].clone().to_uint().unwrap();
let burn_votes = tokens[8].clone().to_uint().unwrap();
let freeze_votes = tokens[9].clone().to_uint().unwrap();
let send_votes = tokens[10].clone().to_uint().unwrap();
let receiver = tokens[11].clone().to_address().unwrap();
EmissionBallotInfo {
creation_time,
start_time,
end_time,
is_canceled,
is_finalized,
creator,
memo,
amount,
burn_votes,
freeze_votes,
send_votes,
receiver,
}
}
}
impl EmissionBallotInfo {
fn email_text(&self) -> String {
format!(
"Creation Time: {}\n\
Voting Start Time: {}\n\
Voting End Time: {}\n\
Amount: {}\n\
Burn Votes: {}\n\
Freeze Votes: {}\n\
Send Votes: {}\n\
Receiver: {:?}\n\
Voting was Canceled: {}\n\
Voting has Finished: {}\n\
Ballot Creator: {:?}\n\
Memo: {}\n",
self.creation_time,
self.start_time,
self.end_time,
convert_wei_to_poa(self.amount),
self.burn_votes,
self.freeze_votes,
self.send_votes,
self.receiver,
self.is_canceled,
self.is_finalized,
self.creator,
self.memo,
)
}
}
/// Converts the `amount` field found in the `VotingToManageEmissionFunds` contract from Wei to
/// POA.
fn convert_wei_to_poa(amount_in_wei: U256) -> f64 {
let whole_poa = amount_in_wei / U256::exp10(18);
let remaining_poa = {
let whole_with_padding = whole_poa * U256::exp10(18);
(amount_in_wei - whole_with_padding).low_u64() as f64
};
let fraction_of_a_poa = {
let eighteen_zeros: String = (0..18).map(|_| '0').collect();
let max_fract: f64 = format!("1{}", eighteen_zeros).parse().unwrap();
remaining_poa / max_fract
};
whole_poa.low_u64() as f64 + fraction_of_a_poa
}

View File

@ -1,513 +0,0 @@
use std::convert::TryFrom;
use std::i64;
use std::thread;
use std::time::Duration;
use std::u64;
use chrono::{DateTime, Utc};
use ethabi::{Event, Token};
use ethereum_types::{Address, H256, U256};
use hex;
use jsonrpc_core as rpc;
use reqwest;
use serde_json as json;
use web3::types::{BlockNumber, Bytes, CallRequest, Filter, FilterBuilder, Log};
use config::{Config, ContractType, PoaContract, StartBlock};
use utils::{hex_string_to_u64, u256_to_datetime};
const JSONRPC_VERSION: rpc::Version = rpc::Version::V2;
const REQUEST_ID: rpc::Id = rpc::Id::Num(1);
#[derive(Clone, Copy, Debug)]
pub enum BallotType {
InvalidKey,
AddKey,
RemoveKey,
SwapKey,
ChangeMinThreshold,
ChangeProxyAddress
}
// Used when converting from an element in a Log's "topics" vector.
impl From<H256> for BallotType {
fn from(topic: H256) -> Self {
match topic.low_u64() {
0 => BallotType::InvalidKey,
1 => BallotType::AddKey,
2 => BallotType::RemoveKey,
3 => BallotType::SwapKey,
4 => BallotType::ChangeMinThreshold,
5 => BallotType::ChangeProxyAddress,
_ => unreachable!()
}
}
}
// Used when converting from an output-token returned from a contract's
// `votingState` function.
impl From<U256> for BallotType {
fn from(output: U256) -> Self {
match output.as_u64() {
0 => BallotType::InvalidKey,
1 => BallotType::AddKey,
2 => BallotType::RemoveKey,
3 => BallotType::SwapKey,
4 => BallotType::ChangeMinThreshold,
5 => BallotType::ChangeProxyAddress,
_ => unreachable!()
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum KeyType {
Invalid,
MiningKey,
VotingKey,
PayoutKey
}
impl From<U256> for KeyType {
fn from(key_type: U256) -> Self {
match key_type.as_u64() {
0 => KeyType::Invalid,
1 => KeyType::MiningKey,
2 => KeyType::VotingKey,
3 => KeyType::PayoutKey,
_ => unreachable!()
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum QuorumState {
Invalid,
InProgress,
Accepted,
Rejected
}
impl From<U256> for QuorumState {
fn from(quorum_state: U256) -> Self {
match quorum_state.as_u64() {
0 => QuorumState::Invalid,
1 => QuorumState::InProgress,
2 => QuorumState::Accepted,
3 => QuorumState::Rejected,
_ => unreachable!()
}
}
}
#[derive(Debug)]
pub struct BallotCreatedLog {
pub block_number: u64,
pub ballot_id: u64,
pub ballot_type: BallotType
}
impl From<Log> for BallotCreatedLog {
fn from(log: Log) -> Self {
BallotCreatedLog {
block_number: log.block_number.unwrap().as_u64(),
ballot_id: log.topics[1].low_u64(),
ballot_type: log.topics[2].into()
}
}
}
#[derive(Debug)]
pub enum VotingData {
Keys(KeysVotingData),
Threshold(ThresholdVotingData),
Proxy(ProxyVotingData)
}
impl VotingData {
pub fn keys(tokens: Vec<Token>) -> Self {
let voting_data: KeysVotingData = tokens.into();
VotingData::Keys(voting_data)
}
pub fn threshold(tokens: Vec<Token>) -> Self {
let voting_data: ThresholdVotingData = tokens.into();
VotingData::Threshold(voting_data)
}
pub fn proxy(tokens: Vec<Token>) -> Self {
let voting_data: ProxyVotingData = tokens.into();
VotingData::Proxy(voting_data)
}
pub fn start_time(&self) -> DateTime<Utc> {
match *self {
VotingData::Keys(ref inner) => inner.start_time,
VotingData::Threshold(ref inner) => inner.start_time,
VotingData::Proxy(ref inner) => inner.start_time
}
}
pub fn end_time(&self) -> DateTime<Utc> {
match *self {
VotingData::Keys(ref inner) => inner.end_time,
VotingData::Threshold(ref inner) => inner.end_time,
VotingData::Proxy(ref inner) => inner.end_time
}
}
pub fn memo(&self) -> String {
match *self {
VotingData::Keys(ref inner) => inner.memo.clone(),
VotingData::Threshold(ref inner) => inner.memo.clone(),
VotingData::Proxy(ref inner) => inner.memo.clone()
}
}
}
#[derive(Debug)]
pub struct KeysVotingData {
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub affected_key: Address,
pub affected_key_type: KeyType,
pub mining_key: Address,
pub total_voters: u64,
pub progress: i64,
pub is_finalized: bool,
pub quorum_state: QuorumState,
pub ballot_type: BallotType,
pub index: u64,
pub min_threshold_of_voters: u64,
pub creator: Address,
pub memo: String
}
impl From<Vec<Token>> for KeysVotingData {
fn from(tokens: Vec<Token>) -> Self {
let start_time = tokens[0].clone().to_uint()
.map(|uint| u256_to_datetime(uint))
.unwrap();
let end_time = tokens[1].clone().to_uint()
.map(|uint| u256_to_datetime(uint))
.unwrap();
let affected_key = tokens[2].clone().to_address().unwrap();
let affected_key_type = tokens[3].clone().to_uint().unwrap().into();
let mining_key = tokens[4].clone().to_address().unwrap();
let total_voters = tokens[5].clone().to_uint().unwrap().as_u64();
let progress = match tokens[6].clone().to_int().unwrap().low_u64() {
lsb if lsb <= total_voters => lsb as i64,
lsb => i64::try_from(u64::MAX - lsb + 1).unwrap()
};
let is_finalized = tokens[7].clone().to_bool().unwrap();
let quorum_state = tokens[8].clone().to_uint().unwrap().into();
let ballot_type = tokens[9].clone().to_uint().unwrap().into();
let index = tokens[10].clone().to_uint().unwrap().as_u64();
let min_threshold_of_voters = tokens[11].clone().to_uint().unwrap().as_u64();
let creator = tokens[12].clone().to_address().unwrap();
let memo = tokens[13].clone().to_string().unwrap();
KeysVotingData {
start_time, end_time,
affected_key, affected_key_type,
mining_key, total_voters,
progress, is_finalized,
quorum_state, ballot_type,
index, min_threshold_of_voters,
creator, memo
}
}
}
#[derive(Debug)]
pub struct ThresholdVotingData {
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub total_voters: u64,
pub progress: i64,
pub is_finalized: bool,
pub quorum_state: QuorumState,
pub index: u64,
pub proposed_value: u64,
pub min_threshold_of_voters: u64,
pub creator: Address,
pub memo: String
}
impl From<Vec<Token>> for ThresholdVotingData {
fn from(tokens: Vec<Token>) -> Self {
let start_time = tokens[0].clone().to_uint()
.map(|uint| u256_to_datetime(uint))
.unwrap();
let end_time = tokens[1].clone().to_uint()
.map(|uint| u256_to_datetime(uint))
.unwrap();
let total_voters = tokens[2].clone().to_uint().unwrap().as_u64();
let progress = match tokens[3].clone().to_int().unwrap().low_u64() {
lsb if lsb <= total_voters => lsb as i64,
lsb => i64::try_from(u64::MAX - lsb + 1).unwrap()
};
let is_finalized = tokens[4].clone().to_bool().unwrap();
let quorum_state = tokens[5].clone().to_uint().unwrap().into();
let index = tokens[6].clone().to_uint().unwrap().as_u64();
let proposed_value = tokens[7].clone().to_uint().unwrap().as_u64();
let min_threshold_of_voters = tokens[8].clone().to_uint().unwrap().as_u64();
let creator = tokens[9].clone().to_address().unwrap();
let memo = tokens[10].clone().to_string().unwrap();
ThresholdVotingData {
start_time, end_time,
total_voters, progress,
is_finalized, quorum_state,
index, proposed_value,
min_threshold_of_voters, creator,
memo
}
}
}
#[derive(Debug)]
pub struct ProxyVotingData {
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub total_voters: u64,
pub progress: i64,
pub is_finalized: bool,
pub quorum_state: QuorumState,
pub index: u64,
pub min_threshold_of_voters: u64,
pub proposed_value: Address,
pub contract_type: u64,
pub creator: Address,
pub memo: String
}
impl From<Vec<Token>> for ProxyVotingData {
fn from(tokens: Vec<Token>) -> Self {
let start_time = tokens[0].clone().to_uint()
.map(|uint| u256_to_datetime(uint))
.unwrap();
let end_time = tokens[1].clone().to_uint()
.map(|uint| u256_to_datetime(uint))
.unwrap();
let total_voters = tokens[2].clone().to_uint().unwrap().as_u64();
let progress = match tokens[3].clone().to_int().unwrap().low_u64() {
lsb if lsb <= total_voters => lsb as i64,
lsb => i64::try_from(u64::MAX - lsb + 1).unwrap()
};
let is_finalized = tokens[4].clone().to_bool().unwrap();
let quorum_state = tokens[5].clone().to_uint().unwrap().into();
let index = tokens[6].clone().to_uint().unwrap().as_u64();
let min_threshold_of_voters = tokens[7].clone().to_uint().unwrap().as_u64();
let proposed_value = tokens[8].clone().to_address().unwrap();
let contract_type = tokens[9].clone().to_uint().unwrap().as_u64();
let creator = tokens[10].clone().to_address().unwrap();
let memo = tokens[11].clone().to_string().unwrap();
ProxyVotingData {
start_time, end_time,
total_voters, progress,
is_finalized, quorum_state,
index, min_threshold_of_voters,
proposed_value, contract_type,
creator, memo
}
}
}
pub struct BlockchainIter<'a> {
client: &'a RpcClient,
start_block: u64,
stop_block: u64,
on_first_iteration: bool,
avg_block_time: Duration
}
impl<'a> BlockchainIter<'a> {
pub fn new(client: &'a RpcClient, config: &Config) -> Self {
let latest = client.latest_block_number().unwrap();
let start_block = match config.start_block {
StartBlock::Earliest => 0,
StartBlock::Latest => latest,
StartBlock::Number(block_number) => block_number,
StartBlock::Tail(tail) => latest - tail
};
if start_block > latest {
panic!(
"Provided start-block ({}) exceeds latest mined block number ({})",
start_block,
latest
);
}
BlockchainIter {
client,
start_block,
stop_block: latest,
on_first_iteration: true,
avg_block_time: config.avg_block_time
}
}
}
impl<'a> Iterator for BlockchainIter<'a> {
type Item = (BlockNumber, BlockNumber);
fn next(&mut self) -> Option<Self::Item> {
if self.on_first_iteration {
self.on_first_iteration = false;
} else {
self.start_block = self.stop_block + 1;
while self.start_block >= self.stop_block {
thread::sleep(self.avg_block_time);
self.stop_block = self.client.latest_block_number().unwrap();
}
}
Some((
BlockNumber::Number(self.start_block),
BlockNumber::Number(self.stop_block)
))
}
}
#[derive(Debug)]
pub enum Method {
CallContractFunction,
GetLogs,
LastMinedBlockNumber
}
impl From<Method> for String {
fn from(method: Method) -> Self {
let s = match method {
Method::CallContractFunction => "eth_call",
Method::GetLogs => "eth_getLogs",
Method::LastMinedBlockNumber => "eth_blockNumber"
};
s.into()
}
}
pub struct EventFilter;
impl EventFilter {
pub fn new(ev: Event, addr: Address, from: BlockNumber, to: BlockNumber) -> Filter {
let topic = vec![ev.signature()];
FilterBuilder::default()
.topics(Some(topic), None, None, None)
.address(vec![addr])
.from_block(from)
.to_block(to)
.build()
}
}
#[derive(Debug)]
pub struct RpcClient {
endpoint: String,
client: reqwest::Client
}
impl RpcClient {
pub fn new(endpoint: &str) -> Self {
let endpoint = endpoint.into();
let client = reqwest::Client::new();
RpcClient { endpoint, client }
}
fn build_request(&self, method: Method, params: Vec<json::Value>) -> reqwest::Request {
let jsonrpc = Some(JSONRPC_VERSION);
let id = REQUEST_ID.clone();
let method = method.into();
let params = Some(rpc::Params::Array(params));
let method_call = rpc::MethodCall { jsonrpc, method, params, id };
let body = rpc::Call::MethodCall(method_call);
self.client.post(&self.endpoint).json(&body).build().unwrap()
}
fn send(&self, req: reqwest::Request) -> reqwest::Result<json::Value> {
let resp: rpc::Response = self.client.execute(req)?.json().unwrap();
if let rpc::Response::Single(resp_status) = resp {
if let rpc::Output::Success(resp) = resp_status {
return Ok(resp.result);
}
}
unreachable!();
}
fn latest_block_number(&self) -> reqwest::Result<u64> {
let req = self.build_request(Method::LastMinedBlockNumber, vec![]);
if let json::Value::String(hex) = self.send(req)? {
return Ok(hex_string_to_u64(&hex).unwrap());
}
unreachable!();
}
fn get_logs(&self, filter: Filter) -> reqwest::Result<Vec<Log>> {
let params = vec![json::to_value(filter).unwrap()];
let req = self.build_request(Method::GetLogs, params);
let result = self.send(req)?;
Ok(json::from_value(result).unwrap())
}
pub fn get_ballot_created_logs(
&self,
contract: &PoaContract,
start: BlockNumber,
stop: BlockNumber
) -> reqwest::Result<Vec<BallotCreatedLog>>
{
let event = contract.event("BallotCreated");
let filter = EventFilter::new(event, contract.addr, start, stop);
let logs = self.get_logs(filter)?;
let ballot_created_logs: Vec<BallotCreatedLog> = logs.into_iter()
.map(|log| log.into())
.collect();
Ok(ballot_created_logs)
}
pub fn get_voting_state(&self, contract: &PoaContract, ballot_id: u64) -> reqwest::Result<VotingData> {
let function = contract.function("votingState");
let tokens = vec![Token::Uint(U256::from(ballot_id))];
let encoded_input: Bytes = function.encode_input(&tokens).unwrap().into();
let call = CallRequest {
to: contract.addr,
data: Some(encoded_input),
from: None,
gas: None,
gas_price: None,
value: None
};
let params = vec![
json::to_value(call).unwrap(),
json::to_value(BlockNumber::Latest).unwrap()
];
let req = self.build_request(Method::CallContractFunction, params);
let result = self.send(req)?;
if let json::Value::String(hex) = result {
let bytes = hex::decode(hex.trim_left_matches("0x")).unwrap();
let outputs = function.decode_output(&bytes).unwrap();
let voting_data = match contract.kind {
ContractType::Keys => VotingData::keys(outputs.into()),
ContractType::Threshold => VotingData::threshold(outputs.into()),
ContractType::Proxy => VotingData::proxy(outputs.into())
};
return Ok(voting_data);
}
unreachable!();
}
}

View File

@ -1,18 +0,0 @@
use std::convert::TryFrom;
use std::i64;
use std::num::ParseIntError;
use std::u64;
use chrono::{DateTime, NaiveDateTime, Utc};
use ethereum_types::U256;
pub fn hex_string_to_u64(hex: &str) -> Result<u64, ParseIntError> {
let hex = hex.trim_left_matches("0x");
u64::from_str_radix(hex, 16)
}
pub fn u256_to_datetime(uint: U256) -> DateTime<Utc> {
let n_secs = i64::try_from(uint.as_u64()).unwrap();
let timestamp = NaiveDateTime::from_timestamp(n_secs, 0);
DateTime::from_utc(timestamp, Utc)
}