Update to V2 contracts, remove nightly requirement.

This commit is contained in:
DrPeterVanNostrand 2018-09-25 13:56:44 +00:00
parent 1d60440de9
commit 139fe74fc5
28 changed files with 3823 additions and 2488 deletions

View File

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

View File

@ -1,24 +1,26 @@
[package]
name = "poagov"
version = "1.0.0"
authors = ["Peter van Nostrand <jnz@riseup.net>"]
authors = ["DrPeterVanNostrand <jnz@riseup.net>"]
[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"
lazy_static = "1.1.0"
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"

199
README.md
View File

@ -6,7 +6,7 @@ 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.
# Installing the `poagov` Binary
@ -16,23 +16,27 @@ 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-linux-x86_64.tar.gz
$ rm poagov-1.0.0-linux-x86_64.tar.gz
$ cd poagov
$ 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
$ chmod +x poagov
$ ./poagov --help
# Building the `poagov` Binary from Source
Make sure you have an `.env` file in the same directory as the `poagov`
binary; see the section "Setting up the `.env` File" for more
information.
# Building `poagov` from Source
To build the `poagov` CLI tool, run the following:
@ -40,19 +44,15 @@ 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:
You can run `poagov`'s tests via the following command (make sure to copy
`sample.env` into `.env` before testing):
$ rustc --version
$ cargo test
If you are not using Rust >= 1.26.0-nightly, you can switch to it using:
$ rustup default nightly
### Requires libssl
### 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,
@ -60,18 +60,18 @@ otherwise known as `libssl` (you will need more than just the OpenSSL
binary that you may or may not already have installed at
`/usr/bin/openssl`).
If running `cargo build --release` panics with an error like:
If running `cargo build [--release]` panics with an error like:
"error: failed to run custom build command for `openssl-sys v0.9.28
...
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,69 +85,107 @@ 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
$ poagov --help
# If built from source run:
# $ ./target/{debug, 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
--verbose prints the full notification email's body when logging
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
--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 minedV
# Setting up the Config File
### Required Arguments
Each time you run `poagov`, four CLI arguments are required:
1. The chain that you want to monitor. Uou must specify one and only one of
the following arguments: `--core` or `--sokol`.
2. The hardfork version. You must specify one of the following: `--v1` or `--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).
3. The ballots that you want to monitor for governance events. You must specify
one or more of the following arguments: `-k`/`--keys`, `-t`/`--threshold`,
`-p`/`--proxy`, and/or `-e`/`--emission`.
- Note that the `VotingToManageEmissionFunds.sol` contract (i.e. the
`--emission` option) is not available in `--v1`.
4. The point in the chain for where to start monitoring. You must specify one
and only one of the following: `--earliest`, `--latest`, `--start=<value>`, or
`--tail=<value>`.
### 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 `--verbose` 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 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. Before running `poagove` copy `sample.env` into
`.env`:
$ mv sample.env .env
$ cp sample.env .env
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.
This will enable `poagov's` default configuration. Before enabling email
notifications, you must add the required SMTP configuration values to your
`.env` file.
# Setting up Email Notifications
@ -155,6 +193,7 @@ 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 +211,42 @@ 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 --earliest -kt --email --verbose
- `--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).
`EMAIL_RECIPIENTS` env-var.
- `--verbose` writes each governance notification email to stderr.
Press [ctrl-c] to exit `poagov`.
# 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. Logs include: governance notifications, email
successes/failures, and blocks that have been successfully monitored for
governance events. The following is an example command with its corresponding
logs:
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
}
$ poagov --sokol --v1 --threshold --earliest
Sep 25 13:43:16.712 INFO governance notification, block_number: 525296, ballot_id: 0, ballot: Threshold
Sep 25 13:43:16.712 INFO governance notification, block_number: 599789, ballot_id: 1, ballot: Threshold
Sep 25 13:43:16.712 INFO governance notification, block_number: 1078816, ballot_id: 2, ballot: Threshold
Sep 25 13:43:16.712 INFO finished checking blocks, block_range: Number(0)...Number(4729306)
Sep 25 13:43:46.761 INFO finished checking blocks, block_range: Number(4729307)...Number(4729312)
Sep 25 13:43:48.503 WARN recieved ctrl-c signal, gracefully shutting down...

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"
}
]

View File

@ -1,35 +1,35 @@
# 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 contract addresses.
# V1: https://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
# V2: https://github.com/poanetwork/poa-chain-spec/blob/core/contracts.json
KEYS_CONTRACT_ADDRESS_CORE_V2=
THRESHOLD_CONTRACT_ADDRESS_CORE_V2=
PROXY_CONTRACT_ADDRESS_CORE_V2=
EMISSION_FUNDS_CONTRACT_ADDRESS_CORE_V2=
# Sokol network configuration.
# V1: https://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
# V2: https://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,114 @@
#![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'
[verbose_logs] --verbose 'prints the full notification email's body when logging'"
).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 verbose_logs(&self) -> bool {
self.0.is_present("verbose_logs")
}
}

273
src/client.rs Normal file
View File

@ -0,0 +1,273 @@
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 dotenv::dotenv;
use web3::types::BlockNumber;
use super::RpcClient;
use config::{ContractType, ContractVersion, Network, PoaContract};
#[test]
fn test_get_last_mined_block() {
dotenv().expect("Missing .env file");
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!("sokol 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() {
dotenv().expect("Missing .env file");
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());
}
#[test]
fn test_get_voting_state() {
dotenv().expect("Missing .env file");
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: write this tests once the V2 contracts are released
// #[test]
// fn test_get_ballot_info() {}
}

View File

@ -1,118 +1,149 @@
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 logger::log_no_email_recipients_configured;
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 +155,271 @@ 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<u64>,
pub verbose_logs: 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"))
if email_notifications && email_recipients.is_empty() {
log_no_email_recipients_configured();
}
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 verbose_logs = cli.verbose_logs();
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,
verbose_logs,
};
Ok(config)
}
}
#[cfg(test)]
mod tests {
use std::env;
use dotenv::dotenv;
use super::*;
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_load_env_vars() {
assert!(dotenv().is_ok(), "Missing .env file");
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;
}
// TODO: remove this block once V2 is published to core.
if *network == Network::Core && *version == ContractVersion::V2 {
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() {
dotenv().expect("Missing .env file");
for contract_type in CONTRACT_TYPES.iter() {
for version in VERSIONS.iter() {
for network in NETWORKS.iter() {
// TODO: remove this block once V2 is published to core.
if *network == Network::Core && *version == ContractVersion::V2 {
continue;
}
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());
}
}
}
}
}
}

42
src/error.rs Normal file
View File

@ -0,0 +1,42 @@
use ctrlc;
use dotenv;
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,
EnvFileNotFound(dotenv::Error),
FailedToBuildEmail(failure::Error),
FailedToBuildRequest(reqwest::Error),
FailedToBuildTls(native_tls::Error),
FailedToParseBallotCreatedLog(String),
FailedToParseEnvFile(dotenv::Error),
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,
},
}

64
src/logger.rs Normal file
View File

@ -0,0 +1,64 @@
use std::io::stderr;
use slog::{Drain, Logger};
use slog_term::{FullFormat, PlainSyncDecorator};
use web3::types::BlockNumber;
use error::Error;
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_ctrlc() {
warn!(LOGGER, "recieved ctrl-c signal, gracefully shutting down...");
}
pub fn log_no_email_recipients_configured() {
warn!(LOGGER, "email notifications are enabled, but there are no email recipients");
}
pub fn log_reached_notification_limit(notification_limit: u64) {
warn!(
LOGGER,
"reached notification limit, gracefully shutting down...";
"limit" => notification_limit
);
}
pub fn log_finished_block_window(start: BlockNumber, stop: BlockNumber) {
let block_range = format!("{:?}...{:?}", start, stop);
info!(LOGGER, "finished checking blocks"; "block_range" => block_range);
}
pub fn log_notification(notif: &Notification) {
let log = notif.log();
info!(
LOGGER,
"governance notification";
"ballot" => format!("{:?}", log.ballot_type),
"ballot_id" => format!("{}", log.ballot_id),
"block_number" => format!("{}", log.block_number)
);
}
pub fn log_notification_verbose(notif: &Notification) {
info!(LOGGER, "governance notification\n{}", notif.email_text());
}
pub fn log_failed_to_build_email(e: Error) {
warn!(LOGGER, "failed to build email"; "error" => format!("{:?}", e));
}
pub fn log_failed_to_send_email(recipient: &str, e: Error) {
warn!(LOGGER, "failed to send email"; "recipient" => recipient, "error" => format!("{:?}", e));
}
pub fn log_email_sent(recipient: &str) {
info!(LOGGER, "email sent"; "to" => recipient);
}

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,110 @@
#![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;
#[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;
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();
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();
use blockchain::BlockchainIter;
use cli::Cli;
use client::RpcClient;
use config::{Config, ContractVersion};
use error::{Error, Result};
use logger::{log_ctrlc, log_finished_block_window, log_reached_notification_limit};
use notify::{Notification, Notifier};
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);
}
fn load_dotenv_file() -> Result<()> {
if let Err(e) = dotenv::dotenv() {
if let dotenv::Error::Io(_) = e {
Err(Error::EnvFileNotFound(e))
} else {
Err(Error::FailedToParseEnvFile(e))
}
} else {
Ok(())
}
}
fn set_ctrlc_handler() -> Result<Arc<AtomicBool>> {
let running = Arc::new(AtomicBool::new(true));
{
let running = running.clone();
ctrlc::set_handler(move || {
log_ctrlc();
running.store(false, Ordering::SeqCst);
}).map_err(|e| Error::CtrlcError(e))?;
}
Ok(running)
}
fn main() -> Result<()> {
load_dotenv_file()?;
let cli = Cli::parse();
let config = Config::new(&cli)?;
let running = set_ctrlc_handler()?;
let client = RpcClient::new(config.endpoint.clone());
let mut notifier = Notifier::new(&config)?;
let mut notification_count = 0;
'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 let Some(notification_limit) = config.notification_limit {
notification_count += 1;
if notification_count >= notification_limit {
log_reached_notification_limit(notification_limit);
break 'main_loop;
}
}
}
log_finished_block_window(start_block, stop_block);
}
Ok(())
}

View File

@ -1,199 +1,177 @@
use chrono::{DateTime, Utc};
use ethereum_types::Address;
use lettre::{EmailTransport, SmtpTransport};
use lettre::smtp::{ClientSecurity, ConnectionReuseParameters, SmtpTransportBuilder};
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::{
log_email_sent, log_failed_to_build_email, log_failed_to_send_email, log_notification,
log_notification_verbose,
};
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>,
}
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) -> 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 })
}
notifier.mailer = Some(mailer);
pub fn notify(&mut self, notif: &Notification) {
if self.config.verbose_logs {
log_notification_verbose(notif);
} else {
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) => {
log_failed_to_build_email(e);
continue;
},
};
if let Err(e) = self.send_email(email) {
log_failed_to_send_email(recipient, e);
} else {
log_email_sent(recipient);
}
}
if self.config.send_push_notifications {
println!("Push Notifications not yet implemented.");
}
}
}
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,
)
}
}

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

@ -0,0 +1,381 @@
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 ammount: 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 ammount = 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,
ammount,
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\
Ammount: {}\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,
self.ammount,
self.burn_votes,
self.freeze_votes,
self.send_votes,
self.receiver,
self.is_canceled,
self.is_finalized,
self.creator,
self.memo,
)
}
}

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)
}