diff --git a/.gitignore b/.gitignore index 707b866..f09be1b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ **/*.rs.bk Cargo.lock .env +/logs diff --git a/.travis.yml b/.travis.yml index 44cca9d..7dd6422 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,6 @@ language: rust - rust: - - nightly - + - stable cache: cargo - script: - RUST_BACKTRACE=1 cargo build - diff --git a/Cargo.toml b/Cargo.toml index d82bfc4..91e137a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,24 +1,27 @@ [package] name = "poagov" version = "1.0.0" -authors = ["Peter van Nostrand "] +license = "GPL-3.0" +authors = ["DrPeterVanNostrand "] +build = "build.rs" [dependencies] -chrono = "0.4" -clap = "2.31.2" -dotenv = "0.11.0" -ethabi = "5.1.1" -ethereum-types = "0.3.1" -hex = "0.3.1" +chrono = "0.4.6" +clap = "2.32.0" +ctrlc = "3.1.1" +dotenv = "0.13.0" +ethabi = "6.0.1" +ethereum-types = "0.4.0" +failure = "0.1.2" +hex = "0.3.2" jsonrpc-core = "8.0.1" -lazy_static = "1.0.0" -lettre = "0.8" -lettre_email = "0.8" -native-tls = "0.1.5" -reqwest = "0.8.5" -serde = "1.0.36" -serde_derive = "1.0.36" -serde_json = "1.0.13" -slog = { version = "2.2.3", features = ["release_max_level_trace"] } +# TODO: after `lettre` and `lettre_email` v0.9.x have been published to crates.io, remove these +# GitHub dependencies. +lettre = { git = "https://github.com/lettre/lettre.git" } +lettre_email = { git = "https://github.com/lettre/lettre.git" } +native-tls = "0.2" +reqwest = "0.8.8" +serde_json = "1.0.27" +slog = { version = "2.3.3", features = ["release_max_level_trace"] } slog-term = "2.4.0" -web3 = "0.3.0" +web3 = "0.4.0" diff --git a/README.md b/README.md index afe8e5c..044bde1 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,20 @@ [![Build Status](https://travis-ci.org/poanetwork/poa-governance-notifications.svg?branch=master)](https://travis-ci.org/poanetwork/poa-governance-notifications) -# About +# `poa-governance-notifications` A tool to monitor a POA Network blockchain for [governance events](https://github.com/poanetwork/wiki/wiki/Governance-Overview). The `poagov` command line tool is distributed as a binary for Linux and -OSX, or it can be built from source. +OSX; it can also be built from source for both platforms. + +You can find the source code for the currently deployed governance contracts +[here](https://github.com/poanetwork/poa-network-consensus-contracts/tree/master/contracts). + +You can find the addresses for governance contracts currently deployed to Core +[here](https://github.com/poanetwork/poa-chain-spec/blob/core/contracts.json) +and Sokol +[here](https://github.com/poanetwork/poa-chain-spec/blob/sokol/contracts.json). # Installing the `poagov` Binary @@ -16,23 +24,26 @@ section in this README to find out how to download it. On Debian/Ubuntu: - $ curl -OL https://github.com/poanetwork/poa-governance-notifications/releases/download/v0.1.0/poagov-0.1.0-linux-x86_64.tar.gz - $ tar -xvzf poagov-0.1.0-linux-x86_64.tar.gz - $ rm poagov-0.1.0-linux-x86_64.tar.gz + $ curl -OL https://github.com/poanetwork/poa-governance-notifications/releases/download/v1.0.0/poagov-1.0.0-linux-x86_64.tar.gz + $ tar -xvzf poagov-1.0.0-linux-x86_64.tar.gz + $ rm poagov-1.0.0-linux-x86_64.tar.gz $ cd poagov + $ cp sample.env .env $ chmod +x poagov $ ./poagov --help On OSX: - $ curl -OL https://github.com/poanetwork/poa-governance-notifications/releases/download/v0.1.0/poagov-0.1.0-osx-x86_64.tar.gz - $ tar -xvzf poagov-0.1.0-osx-x86_64.tar.gz - $ rm poagov-0.1.0-osx-x86_64.tar.gz + $ curl -OL https://github.com/poanetwork/poa-governance-notifications/releases/download/v1.0.0/poagov-1.0.0-osx-x86_64.tar.gz + $ tar -xvzf poagov-1.0.0-osx-x86_64.tar.gz + $ rm poagov-1.0.0-osx-x86_64.tar.gz $ cd poagov + $ cp sample.env .env $ chmod +x poagov $ ./poagov --help -# Building the `poagov` Binary from Source + +# Building `poagov` from Source To build the `poagov` CLI tool, run the following: @@ -40,19 +51,23 @@ To build the `poagov` CLI tool, run the following: $ cd poa-governance-notifications $ cargo build --release -### Requires Rust Nightly +`poagov` can be built using Rust 1.29 stable and requires `libssl` to be +installed; see the following "Requires libssl" section for more information. -`poagov` uses experimental Rust features that are currently only available -in Rust version >= 1.26.0-nightly. You can check which version of Rust that -you are using by running: +Building `poagov` requires Rust `1.29.0-stable` or later and `libssl`; see the +"Requires `libssl`" section for more information. - $ rustc --version +### Testing -If you are not using Rust >= 1.26.0-nightly, you can switch to it using: +You can run `poagov`'s tests to ensure that it everything is working properly: - $ rustup default nightly + $ cargo test --release -### Requires libssl +The test suite will verify: that the required env-vars are found the `.env` +file, that each network's JSON-RPC server can be reached, and that each +contract ABI can be loaded. + +# Requires `libssl` SMTP over TLS requires that you have a native TLS library installed on your machine, the preferred library for Linux and OSX is OpenSSL >= 1.0.1, @@ -67,11 +82,11 @@ If running `cargo build --release` panics with an error like: Could not find directory of OpenSSL installation ..." -you probably do not have libssl installed. +you probably do not have `libssl` installed. -To install libssl on Debian/Ubuntu run the following: +To install `libssl` on Debian/Ubuntu run the following: - $ sudo apt-get update -y + $ sudo apt update $ sudo apt-get install -y pkg-config libssl-dev To install libssl on MacOS run the following: @@ -85,76 +100,136 @@ Then try to rebuild `poagov` using: $ cargo build --release If you are on OSX and installed OpenSSL using Homebrew and continue to get -compilation errors for any of the Rust crates: openssl, openssl-sys, or -openssl-sys-extras, try building with the following: +compilation errors for any of the Rust crates: `openssl`, `openssl-sys`, or +`openssl-sys-extras`, try building with the following: $ cargo clean $ OPENSSL_INCLUDE_DIR=$(brew --prefix openssl)/include \ OPENSSL_LIB_DIR=$(brew --prefix openssl)/lib \ - cargo build + cargo build --release -There is a known issue regarding the openssl-sys crate not being able to -find libssl installed with Homebrew on OSX that is well documented on +There is a known issue regarding the `openssl-sys` crate not being able to +find `libssl` installed with Homebrew on OSX; more information can be found on [Stack Overflow](https://stackoverflow.com/questions/34612395/openssl-crate-fails-compilation-on-mac-os-x-10-11/34615626#34615626). The above solution comes from the linked Stack Overflow thread. More information on common issues encountered while installing the -openssl Rust crate can be found [here](https://crates.io/crates/openssl). +`openssl` Rust crate can be found [here](https://crates.io/crates/openssl). # Usage -Once you have built `poagov`, you can print out the CLI usage by running: +Once you have built or downloaded `poagov`, you can print out the CLI usage by +running: - $ ./target/release/poagov --help + # If you downloaded the `poagov` binary run: + $ poagov --help + # Or, if you built `poagov` from source run: + $ target/release/poagov --help - poagov 1.0 - Monitores the POA Network's blockchain for governance events. + poagov 1.0.0 + Monitors a POA Network blockchain for governance events. USAGE: poagov [FLAGS] [OPTIONS] FLAGS: - --core monitor voting contracts deployed to the Core network (same as using --network=core) - --earliest start monitoring for goverance events starting from the first block in the chain - --email send governance notifications via email - -h, --help prints help information - -k monitor the blockchain for ballots to change keys (same as --monitor=keys) - --latest start monitoring for goverance events starting from the most recently mined block in the chain - --local monitor voting contracts deployed to a locally running POA chain (same as using --network=local) - -p monitor the change for ballots to change the proxy address (same as --monitor=proxy) - --push send governance notifications via push notification - --sokol monitor voting contracts deployed to the Sokol test network (same as using --network=sokol) - -t monitor the chain for ballots to change the minimum threshold (same as --monitor=threshold) - -V, --version prints version information + --core monitor voting contracts deployed to the Core network + --earliest begin monitoring for governance events starting at the first block in the blockchain + --email enables email notifications (SMTP configurations must be set in your `.env` file) + -e, --emission monitors the blockchain for ballots to manage emission funds + -h, --help prints help information + -k, --keys monitors the blockchain for ballots to change keys + --latest begin monitoring for governance events starting at the last block mined + -p, --proxy monitors the blockchain for ballots to change the proxy address + --sokol monitor voting contracts deployed to the Sokol network + -t, --threshold monitors the blockchain for ballots to change the minimum threshold + --v1 monitors the v1 voting contracts + --v2 monitors the v2 voting contracts + -V, --version prints version information + --log-emails logs each notification's email body; does not require the --email flag to be set + --log-file logs are written to files in the ./logs directory, log files are rotated when they reach a size of 4MB OPTIONS: - --block-time the average time it takes to mine a new block - --monitor a comma-separated list of ballot types to monitor for governance events; the available values are: keys, threshold, proxy - --network the name of the network to monitor for ballots; the values available for this option are: core, sokol, local - --rpc the URL for the RPC endpoint - --start start monitoring for governance events at this block (inclusive) - --tail start monitoring for governance events for the `n` blocks prior to the last mined block in the chain + --block-time the average number of seconds it takes to mine a new block + -n, --limit shutdown `poagov` after this many notifications have been generated, useful when testing + --start start monitoring for governance events at this block (inclusive) + --tail start monitoring for governance events for the `n` blocks prior to the last block mined -# Setting up the Config File +Hitting `[ctrl-c]` while `poagov` is running will cause the process to gracefully shutdown. + +### Required Arguments + +Each time you run `poagov`, four CLI arguments are required: + +1. The chain (specify only one): `--core`, `--sokol`. +2. The contracts to monitor (specify at least one): `--keys`, `--threshold`, `--proxy`, `--emission`. +2. The hardfork version (specify only one): `--v1`, `--v2`. +4. The block in the chain from where to start monitoring (specify only one): `--earliest`, `--latest`, `--start=`, `--tail=`. + +### Notes on the hardfork version options `--v1` and `--v2` + +`--v1` indicates that you want to monitor for governance events prior to the +Sokol and Core hardforks that will occur in September-2018 and November-2018 +respectively. + +`--v2` indicates that you want to monitor for governance events that occured +after the above hardfork dates. + +- More information regarding the planned hardforks for the Sokol and Core +chains in September and November 2018 can be found +[here](https://medium.com/poa-network/poa-network-news-and-updates-36-2e6e00550c15). + +### Optional Arguments + +Providing the `--email` flag will enable governance notification emails. To use +this option, you must first configure SMTP in your `.env`. + +Providing the `--block-time=` will set how often `poagov` will query the +blockchain for new governance events. Defaults to 30 seconds. + +Providing the `--log-emails` flag will print the full text for a notification +email to stderr when governance events are found. When this option is set, +email text will be logged regardless of whether or not the `--email` flag is +set. + +Setting the `--log-file` flag will write logs to a file in the `./logs/` +directory. Logs are rotated chronologically across three files. Once the +`logs` directory has reached its max number of files, the oldest log file will +be deleted to make room for the next log file. Log files have a max size of +4MB; the log files will rotated once the current log file has reached the max +file size. + +Setting the `--limit=` option will cause `poagov` to stop once `value` +number of notifications have been generated. This option is useful when testing. + +# Setting up the `.env` File When the `poagov` CLI tool is run, the process' environment variables are -loaded via a `.env` file. An example `.env` file can be found at -`sample.env`. Before running this tool change the name of `sample.env` to -`.env` using: +loaded via an `.env` file. - $ mv sample.env .env +The `.env` file contains configuration variables that are not specified via the +command line. You are required to have an `.env` file in the same directory as +your `Cargo.toml` or `poagov` binary. -This will enable `poagov's` default configuration. You can update your -config file to allow `poagov` to use a locally running chain, governance -contracts that you have deployed locally, and to setup email -notifications. +When building from source, the `sample.env` file will be copied into the `.env` +file. This `.env` file will contain the default configuration values required +to run `poagov`. -# Setting up Email Notifications +If you did not build `poagov` from source, you will have to create an `.env` +file in the same directory as the `poagov` binary; then copy the contents of +`sample.env` into it. + +If you wish to enable email notifications, you must add the required SMTP +config values to your `.env` file. See the "Setting up Email Notifications" +section for details. + +### Setting up Email Notifications In order to enable email notifications, you must change the name of the `sample.env` file to `.env`. Then, you must add values for the following SMTP config options in your `.env` file: + EMAIL_RECIPIENTS= SMTP_HOST_DOMAIN= SMTP_PORT= SMTP_USERNAME= @@ -172,36 +247,44 @@ but you may use port 465 for TLS, or any other port that your outgoing email server is lisening for secure connections. If you require unencrypted SMTP, submit an issue and I can add it. +Your SMTP configuration should look something like the following: + + EMAIL_RECIPIENTS=alice@poa.network,bob@poa.network + SMTP_HOST_DOMAIN=mail.riseup.net + SMTP_PORT=587 + SMTP_USERNAME=evariste_galois + SMTP_PASSWORD='finteFIELDS#$!' + OUTGOING_EMAIL_ADDRESS=evariste_galois@riseup.net + # An Explained Example - $ ./target/release/poagov --sokol --earliest -kt --email + $ poagov --sokol --v1 -kt --earliest --email --log-emails --limit=1 -- `--sokol` is used to monitor contracts deployed to POA's test network. -- `--earliest` starts monitoring from the first block in the blockchain. -- `-k` get notifications for ballots to change keys. -- `-t` get notifications for ballots to change the min threshold. -- `--email` sends out email notifications to each address in the -"VALIDATORS" config value (located in the .env file). - -Press [ctrl-c] to exit `poagov`. +- `--sokol` monitors the Sokol chain. +- `--v1` monitors the governance contracts deployed prior to September-2018. +- `-k` monitors the `VotingToChangeKeys` contract. +- `-t` monitors the `VotingToChangeMinThreshold` contract. +- `--earliest` start monitoring from the first block in the blockchain. +- `--email` sends out email notifications to each address in the `EMAIL_RECIPIENTS` env-var. +- `--log-emails` for each governance notification generated, log the corresponding email body. +- `--limit=1` stop running `poagov` after one ballot notification has been generated. # Logs -Logs are output to stderr. Any notifications that were generated, sent, -or failed to be sent will be logged. The following is an example log for a -a notification for a ballot to change the min threshold that was generated -using the command `$ poagov --earliest -t`: +Logs are output to `stderr` unless the `--log-file` CLI flag is set. Events +that are logged include: the generation of governance notifications, sending an +email successesfully or failing to send an email, aned what range of blocks +from the chain have been successfully monitored for governance events. +Optionally, you can log the email body for each governance notification +generated by setting the `--log-emails` CLI flag. - Apr 21 08:31:54.219 INFO notification, data: ThresholdNotification { - network: Sokol, - endpoint: "https://sokol.poa.network", - block_number: 1078816, - contract_type: Threshold, - ballot_type: ChangeMinThreshold, - ballot_id: 2, - start_time: 2018-02-23T05:28:22Z, - end_time: 2018-02-25T05:33:00Z, - memo: "*TEST* ballot to increase the consensus threshold to 51% (rounded to the higher integer) of the total number of validators. The idea is to legitimize passing the ballot by the majority participation.", - proposed_value: 4 - } +The following is an example command with its corresponding logs: + + $ poagov --sokol --v1 --threshold --earliest --limit=3 + + Oct 10 15:18:09.863 INFO starting poagov... + Oct 10 15:18:10.287 INFO governance notification, block_number: 525296, ballot_id: 0, ballot: Threshold + Oct 10 15:18:10.287 INFO governance notification, block_number: 599789, ballot_id: 1, ballot: Threshold + Oct 10 15:18:10.287 INFO governance notification, block_number: 1078816, ballot_id: 2, ballot: Threshold + Oct 10 15:18:10.287 WARN reached notification limit, gracefully shutting down..., limit: 3 diff --git a/abis/local/.gitkeep b/abis/local/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/abis/sokol/proxy.json b/abis/sokol/proxy.json deleted file mode 100644 index 87166d8..0000000 --- a/abis/sokol/proxy.json +++ /dev/null @@ -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" - } - ] diff --git a/abis/core/keys.json b/abis/v1/VotingToChangeKeys.abi.json similarity index 99% rename from abis/core/keys.json rename to abis/v1/VotingToChangeKeys.abi.json index 42d89e6..d9de48f 100644 --- a/abis/core/keys.json +++ b/abis/v1/VotingToChangeKeys.abi.json @@ -770,4 +770,4 @@ "name": "BallotCreated", "type": "event" } -] +] \ No newline at end of file diff --git a/abis/sokol/threshold.json b/abis/v1/VotingToChangeMinThreshold.abi.json similarity index 99% rename from abis/sokol/threshold.json rename to abis/v1/VotingToChangeMinThreshold.abi.json index 7484d31..c4f4239 100644 --- a/abis/sokol/threshold.json +++ b/abis/v1/VotingToChangeMinThreshold.abi.json @@ -635,4 +635,4 @@ "name": "BallotCreated", "type": "event" } -] +] \ No newline at end of file diff --git a/abis/core/proxy.json b/abis/v1/VotingToChangeProxyAddress.abi.json similarity index 99% rename from abis/core/proxy.json rename to abis/v1/VotingToChangeProxyAddress.abi.json index 8641edf..2e0842b 100644 --- a/abis/core/proxy.json +++ b/abis/v1/VotingToChangeProxyAddress.abi.json @@ -662,4 +662,4 @@ "name": "BallotCreated", "type": "event" } -] +] \ No newline at end of file diff --git a/abis/core/threshold.json b/abis/v2/VotingToChangeKeys.abi.json similarity index 81% rename from abis/core/threshold.json rename to abis/v2/VotingToChangeKeys.abi.json index 7484d31..f21dc99 100644 --- a/abis/core/threshold.json +++ b/abis/v2/VotingToChangeKeys.abi.json @@ -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" } -] +] \ No newline at end of file diff --git a/abis/sokol/keys.json b/abis/v2/VotingToChangeMinThreshold.abi.json similarity index 66% rename from abis/sokol/keys.json rename to abis/v2/VotingToChangeMinThreshold.abi.json index 42d89e6..ee1ac0a 100644 --- a/abis/sokol/keys.json +++ b/abis/v2/VotingToChangeMinThreshold.abi.json @@ -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" } -] +] \ No newline at end of file diff --git a/abis/v2/VotingToChangeProxyAddress.abi.json b/abis/v2/VotingToChangeProxyAddress.abi.json new file mode 100644 index 0000000..804be4b --- /dev/null +++ b/abis/v2/VotingToChangeProxyAddress.abi.json @@ -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" + } +] \ No newline at end of file diff --git a/abis/v2/VotingToManageEmissionFunds.abi.json b/abis/v2/VotingToManageEmissionFunds.abi.json new file mode 100644 index 0000000..ccfd719 --- /dev/null +++ b/abis/v2/VotingToManageEmissionFunds.abi.json @@ -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" + } +] \ No newline at end of file diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..7bad688 --- /dev/null +++ b/build.rs @@ -0,0 +1,18 @@ +use std::fs; +use std::path::Path; + +fn create_env_file_if_dne() { + let env_path = Path::new(".env"); + if !env_path.exists() { + let sample_env_path = Path::new("sample.env"); + if !sample_env_path.exists() { + panic!("neither the .env nor the sample.env files exist"); + } + fs::copy(sample_env_path, env_path) + .unwrap_or_else(|e| panic!("could not create .env file from sample.env: {:?}", e)); + } +} + +fn main() { + create_env_file_if_dne(); +} diff --git a/sample.env b/sample.env index 1fff21f..f218038 100644 --- a/sample.env +++ b/sample.env @@ -1,35 +1,39 @@ -# Defaults. -USE_NETWORK=sokol -MONITOR_BALLOTS=keys,threshold,proxy -START_BLOCK=latest -AVG_BLOCK_TIME_SECS=5 -SEND_EMAIL_NOTIFICATIONS=false -SEND_PUSH_NOTIFICATIONS=false +# RPC endpoints: https://github.com/poanetwork/wiki +CORE_RPC_ENDPOINT=https://core.poa.network +SOKOL_RPC_ENDPOINT=https://sokol.poa.network -# Email configuration. +# Core Network V1 contract addresses: +# github.com/poanetwork/poa-chain-spec/blob/f037171b344d6138a6a7a7217ee6d2c85dbfd466/contracts.json +KEYS_CONTRACT_ADDRESS_CORE_V1=0x215794efe4b86a2fbcbf706bc9ade63663f1eae1 +THRESHOLD_CONTRACT_ADDRESS_CORE_V1=0xca863b0d12193a87b5173fd51fa4aa1703fb8a32 +PROXY_CONTRACT_ADDRESS_CORE_V1=0x9c8a06f0197ee718cd820adeb48a88ea2a9b5c48 + +# Core Network V2 contract addresses: +# github.com/poanetwork/poa-chain-spec/blob/38f3f634524923c4d74638852da1ace52e5a0cf0/contracts.json +KEYS_CONTRACT_ADDRESS_CORE_V2=0xa4508af18f1005943678769db3d95223c062258d +THRESHOLD_CONTRACT_ADDRESS_CORE_V2=0xa45e35472693ae60a95db8cB1ce73eea22ab5328 +PROXY_CONTRACT_ADDRESS_CORE_V2=0x468758926c796722d85bded792d1831f0839caa6 +EMISSION_FUNDS_CONTRACT_ADDRESS_CORE_V2=0x7e9b90b22cdd1f6aa206f0d852ac96212217d60e + +# Sokol Network V1 contract addresses: +# github.com/poanetwork/poa-chain-spec/blob/821dc399471c80c6b0c6a95a2ba77d7c064eb321/contracts.json +KEYS_CONTRACT_ADDRESS_SOKOL_V1=0xc40cdf254a4a35498aa84f35e9842c110729a2a0 +THRESHOLD_CONTRACT_ADDRESS_SOKOL_V1=0x700db8ba3128087f3b23f60de4bc3179bafa467d +PROXY_CONTRACT_ADDRESS_SOKOL_V1=0x0aa4a75549757a90f62f88b3b96b69bead2db0ff + +# Sokol Network V2 contract addresses: +# github.com/poanetwork/poa-chain-spec/blob/36927101401de9dc3e21274d6f46e600d08fd1a7/contracts.json +KEYS_CONTRACT_ADDRESS_SOKOL_V2=0xb974df531c1b27324618175b442edf95f7f7a621 +THRESHOLD_CONTRACT_ADDRESS_SOKOL_V2=0xd75ad6e3840a18dacc67bf3cd2080b24be409f79 +PROXY_CONTRACT_ADDRESS_SOKOL_V2=0x604cdc518f3eb0446e15fc05a22923c82d8a8e21 +EMISSION_FUNDS_CONTRACT_ADDRESS_SOKOL_V2=0x7cfa6f2c0d032f9dde652996e989a4d385b8b9d7 + +# Email notificaions settings. +# NOTE: if any of the following values contain special characters (as defined by your shell), +# wrap the entire env-var value in single quotations. +EMAIL_RECIPIENTS= SMTP_HOST_DOMAIN= SMTP_PORT=587 SMTP_USERNAME= SMTP_PASSWORD= OUTGOING_EMAIL_ADDRESS= - -# Validators (comma-separated list of emails). -VALIDATORS= - -# Core network configuration. -CORE_RPC_ENDPOINT=https://core.poa.network -CORE_KEYS_CONTRACT_ADDRESS=0x215794efe4b86a2fbcbf706bc9ade63663f1eae1 -CORE_THRESHOLD_CONTRACT_ADDRESS=0x8829ebe113535826e8af17ed51f83755f675789a -CORE_PROXY_CONTRACT_ADDRESS=0x6b728399b41a38d4109f7af2213d4cc31ca87812 - -# Sokol network configuration. -SOKOL_RPC_ENDPOINT=https://sokol.poa.network -SOKOL_KEYS_CONTRACT_ADDRESS=0xc40cdf254a4a35498aa84f35e9842c110729a2a0 -SOKOL_THRESHOLD_CONTRACT_ADDRESS=0x700db8ba3128087f3b23f60de4bc3179bafa467d -SOKOL_PROXY_CONTRACT_ADDRESS=0x0aa4a75549757a90f62f88b3b96b69bead2db0ff - -# Personal/local network configuration. -LOCAL_RPC_ENDPOINT=http://127.0.0.1:8545 -LOCAL_KEYS_CONTRACT_ADDRESS= -LOCAL_THRESHOLD_CONTRACT_ADDRESS= -LOCAL_PROXY_CONTRACT_ADDRESS= diff --git a/src/blockchain.rs b/src/blockchain.rs new file mode 100644 index 0000000..93f6936 --- /dev/null +++ b/src/blockchain.rs @@ -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) -> 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, +} + +impl<'a> BlockchainIter<'a> { + pub fn new(client: &'a RpcClient, config: &Config, running: Arc) -> Result { + 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 { + 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 + } + } +} diff --git a/src/cli.rs b/src/cli.rs index 244ac5c..4da8732 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,30 +1,119 @@ +#![allow(dead_code)] + use clap::{ArgMatches, App}; -pub struct Cli; +#[derive(Debug)] +pub struct Cli(ArgMatches<'static>); impl Cli { - pub fn load() -> ArgMatches<'static> { - App::new("poagov") - .version("1.0") - .about("A tool to monitor POA Network's blockchain for governance events.") + pub fn parse() -> Self { + let cli_args = App::new("poagov") + .version("1.0.0") + .about("Monitors a POA Network blockchain for governance events.") .args_from_usage( - "[network] --network [value] 'the name of the network to monitor for ballots; the values available for this option are: core, sokol, local' - [core] --core 'monitor voting contracts deployed to the Core network (same as using --network=core)' - [sokol] --sokol 'monitor voting contracts deployed to the Sokol test network (same as using --network=sokol)' - [local] --local 'monitor voting contracts deployed to a locally running POA chain (same as using --network=local)' - [rpc] --rpc [value] 'the URL for the RPC endpoint' - [monitor] --monitor [value] 'a comma-separated list of ballot types to monitor for governance events; the available values are: keys, threshold, proxy` - [keys] -k 'monitor the blockchain for ballots to change keys (same as --monitor=keys)' - [threshold] -t 'monitor the chain for ballots to change the minimum threshold (same as --monitor=threshold)' - [proxy] -p 'monitor the change for ballots to change the proxy address (same as --monitor=proxy)' + "[core] --core 'monitor voting contracts deployed to the Core network' + [sokol] --sokol 'monitor voting contracts deployed to the Sokol network' + [keys] -k --keys 'monitors the blockchain for ballots to change keys' + [threshold] -t --threshold 'monitors the blockchain for ballots to change the minimum threshold' + [proxy] -p --proxy 'monitors the blockchain for ballots to change the proxy address' + [emission] -e --emission 'monitors the blockchain for ballots to manage emission funds' + [v1] --v1 'monitors the v1 voting contracts' + [v2] --v2 'monitors the v2 voting contracts' + [earliest] --earliest 'begin monitoring for governance events starting at the first block in the blockchain' + [latest] --latest 'begin monitoring for governance events starting at the last block mined' [start_block] --start [value] 'start monitoring for governance events at this block (inclusive)' - [tail] --tail [value] 'start monitoring for governance events for the `n` blocks prior to the last mined block in the chain' - [earliest] --earliest 'start monitoring for goverance events starting from the first block in the chain' - [latest] --latest 'start monitoring for goverance events starting from the most recently mined block in the chain' - [email] --email 'send governance notifications via email' - [push] --push 'send governance notifications via push notification' - [block_time] --block-time [value] 'the average time it takes to mine a new block'" - ) - .get_matches() + [tail] --tail [value] 'start monitoring for governance events for the `n` blocks prior to the last block mined' + [email] --email 'enables email notifications (SMTP configurations must be set in your `.env` file)' + [block_time] --block-time [value] 'the average number of seconds it takes to mine a new block' + [notification_limit] -n --limit [value] 'shutdown `poagov` after this many notifications have been generated' + [log_emails] --log-emails 'logs each notification's email body; does not require the --email flag to be set' + [log_to_file] --log-file 'logs are written to files in the ./logs directory, logs are rotated chronologically across 3 files, each file has a max size of 8MB'" + ).get_matches(); + Cli(cli_args) + } + + pub fn core(&self) -> bool { + self.0.is_present("core") + } + + pub fn sokol(&self) -> bool { + self.0.is_present("sokol") + } + + pub fn keys(&self) -> bool { + self.0.is_present("keys") + } + + pub fn threshold(&self) -> bool { + self.0.is_present("threshold") + } + + pub fn proxy(&self) -> bool { + self.0.is_present("proxy") + } + + pub fn emission(&self) -> bool { + self.0.is_present("emission") + } + + pub fn v1(&self) -> bool { + self.0.is_present("v1") + } + + pub fn v2(&self) -> bool { + self.0.is_present("v2") + } + + pub fn earliest(&self) -> bool { + self.0.is_present("earliest") + } + + pub fn latest(&self) -> bool { + self.0.is_present("latest") + } + + pub fn start_block(&self) -> Option<&str> { + self.0.value_of("start_block") + } + + pub fn tail(&self) -> Option<&str> { + self.0.value_of("tail") + } + + pub fn multiple_start_blocks_specified(&self) -> bool { + let mut count = 0; + if self.earliest() { + count += 1; + } + if self.latest() { + count += 1; + } + if self.start_block().is_some() { + count += 1; + } + if self.tail().is_some() { + count += 1; + } + count != 1 + } + + pub fn email(&self) -> bool { + self.0.is_present("email") + } + + pub fn block_time(&self) -> Option<&str> { + self.0.value_of("block_time") + } + + pub fn notification_limit(&self) -> Option<&str> { + self.0.value_of("notification_limit") + } + + pub fn log_emails(&self) -> bool { + self.0.is_present("log_emails") + } + + pub fn log_to_file(&self) -> bool { + self.0.is_present("log_to_file") } } diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..202fd38 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,310 @@ +use std::u64; + +use ethabi; +use hex; +use jsonrpc_core as json_rpc; +use reqwest; +use serde_json as json; +use web3; +use web3::types::{Address, BlockNumber, Filter, FilterBuilder, U256}; + +use config::{ContractType, PoaContract}; +use error::{Error, Result}; +use response::{v1, v2}; +use response::common::BallotCreatedLog; + +#[derive(Debug)] +pub enum RpcMethod { + CallContractFunction, + GetLogs, + GetLastMinedBlockNumber, +} + +impl Into 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, + ) -> Result + { + 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 { + 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 { + 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> { + 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> + { + 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 { + 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 { + // pub fn get_ballot_info(&self, contract: &PoaContract, ballot_id: U256, voting_key: Option
) -> Result { + let function = contract.function("getBallotInfo"); + /* + let mut tokens = vec![ethabi::Token::Uint(ballot_id)]; + if function.inputs.len() == 2 { + if let Some(voting_key) = voting_key { + tokens.push(ethabi::Token::Address(voting_key)); + } + } + */ + let mut tokens = vec![ethabi::Token::Uint(ballot_id)]; + if function.inputs.len() == 2 { + tokens.push(ethabi::Token::Address(Address::zero())); + } + + let encoded_input = function.encode_input(&tokens).unwrap(); + let function_call_request = web3::types::CallRequest { + to: contract.addr, + data: Some(encoded_input.into()), + from: None, + gas: None, + gas_price: None, + value: None, + }; + let rpc_method_params = vec![ + json::to_value(function_call_request).unwrap(), + json::to_value(BlockNumber::Latest).unwrap(), + ]; + let req = self.build_request(RpcMethod::CallContractFunction, rpc_method_params)?; + if let json::Value::String(s) = self.send(req)? { + let s = s.trim_left_matches("0x"); + let bytes = hex::decode(s).unwrap(); + let outputs = function.decode_output(&bytes).unwrap(); + let ballot_info: v2::BallotInfo = match contract.kind { + ContractType::Keys => v2::KeysBallotInfo::from(outputs).into(), + ContractType::Threshold => v2::ThresholdBallotInfo::from(outputs).into(), + ContractType::Proxy => v2::ProxyBallotInfo::from(outputs).into(), + ContractType::Emission => v2::EmissionBallotInfo::from(outputs).into(), + }; + return Ok(ballot_info); + } + unreachable!("received non-string JSON response from `getBallotInfo`"); + } +} + +#[cfg(test)] +mod tests { + use std::env; + + use web3::types::BlockNumber; + + use super::super::tests::setup; + use super::RpcClient; + use config::{ContractType, ContractVersion, Network, PoaContract}; + + #[test] + fn test_get_last_mined_block() { + setup(); + + let sokol_url = env::var("SOKOL_RPC_ENDPOINT").expect("Missing env-var: `SOKOL_RPC_ENDPOINT`"); + let client = RpcClient::new(sokol_url); + let res = client.get_last_mined_block_number(); + println!("\nsokol last mined block number => {:?}", res); + assert!(res.is_ok()); + + let core_url = env::var("CORE_RPC_ENDPOINT").expect("Missing env-var: `CORE_RPC_ENDPOINT`"); + let client = RpcClient::new(core_url); + let res = client.get_last_mined_block_number(); + println!("core last mined block number => {:?}", res); + assert!(res.is_ok()); + } + + #[test] + fn test_get_ballot_created_logs() { + setup(); + let contract = PoaContract::read( + ContractType::Keys, + Network::Sokol, + ContractVersion::V1, + ).unwrap_or_else(|e| panic!("Failed to load contract: {:?}", e)); + let endpoint = env::var("SOKOL_RPC_ENDPOINT").expect("Missing env-var: `SOKOL_RPC_ENDPOINT`"); + let client = RpcClient::new(endpoint); + let res = client.get_ballot_created_logs( + &contract, + BlockNumber::Earliest, + BlockNumber::Latest, + ); + println!("{:#?}", res); + assert!(res.is_ok()); + } + + // TODO: uncomment this test once V2 ballots are created. + /* + #[test] + fn test_get_ballot_created_logs_v2() { + setup(); + let contract = PoaContract::read( + ContractType::Keys, + Network::Core, + ContractVersion::V2 + ).unwrap_or_else(|e| panic!("Failed to load contract: {:?}", e)); + let rpc_url = env::var("CORE_RPC_ENDPOINT").expect("Missing env-var: `CORE_RPC_ENDPOINT`"); + let client = RpcClient::new(rpc_url); + let res = client.get_ballot_created_logs( + &contract, + BlockNumber::Earliest, + BlockNumber::Latest, + ); + println!("RES => {:#?}", res); + assert!(res.is_ok()); + } + */ + + #[test] + fn test_get_voting_state() { + setup(); + let contract = PoaContract::read( + ContractType::Threshold, + Network::Sokol, + ContractVersion::V1 + ).unwrap_or_else(|e| panic!("Failed to load contract: {:?}", e)); + let sokol_url = env::var("SOKOL_RPC_ENDPOINT").expect("Missing env-var: `SOKOL_RPC_ENDPOINT`"); + let client = RpcClient::new(sokol_url); + let res = client.get_voting_state(&contract, 2.into()); + println!("{:#?}", res); + assert!(res.is_ok()); + } + + // TODO: uncomment this test once V2 ballots are created. + /* + #[test] + fn test_get_ballot_info() { + setup(); + let contract = PoaContract::read( + ContractType::Emission, + Network::Core, + ContractVersion::V2 + ).unwrap_or_else(|e| panic!("Failed to load contract: {:?}", e)); + let sokol_url = env::var("CORE_RPC_ENDPOINT").expect("Missing env-var: `SOKOL_RPC_ENDPOINT`"); + let client = RpcClient::new(sokol_url); + let ballot_id + let res = client.get_ballot_info(&contract, 2.into()); + println!("{:#?}", res); + assert!(res.is_ok()); + } + */ +} diff --git a/src/config.rs b/src/config.rs index e265845..160ccdf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,118 +1,148 @@ use std::env; -use std::fmt::{self, Debug, Display, Formatter}; +use std::fmt::{self, Debug, Formatter}; use std::fs::File; use std::str::FromStr; -use std::time::Duration; -use dotenv::dotenv; -use ethabi::{Contract, Event, Function}; -use ethereum_types::Address; +use ethabi::{Address, Contract, Event, Function}; use cli::Cli; -use utils::hex_string_to_u64; +use error::{Error, Result}; +use response::common::BallotType; -#[derive(Clone, Copy, Debug)] -pub enum Network { Core, Sokol, Local } +const DEFAULT_BLOCK_TIME_SECS: u64 = 30; -impl<'a> From<&'a str> for Network { - fn from(s: &'a str) -> Self { - match s { - "core" => Network::Core, - "sokol" => Network::Sokol, - "local" => Network::Local, - _ => panic!(format!("Invalid network: {}", s)) - } - } -} - -impl Display for Network { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match *self { - Network::Core => write!(f, "core"), - Network::Sokol => write!(f, "sokol"), - Network::Local => write!(f, "local") - } - } +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Network { + Core, + Sokol, } impl Network { - fn to_uppercase(&self) -> String { - format!("{}", self).to_uppercase() - } -} - -#[derive(Clone, Copy, Debug)] -pub enum ContractType { Keys, Threshold, Proxy } - -impl<'a> From<&'a str> for ContractType { - fn from(s: &'a str) -> Self { - match s { - "keys" => ContractType::Keys, - "threshold" => ContractType::Threshold, - "proxy" => ContractType::Proxy, - _ => panic!("Invalid contract type: {}", s) + fn uppercase(&self) -> &str { + match self { + Network::Core => "CORE", + Network::Sokol => "SOKOL", } } } -impl Display for ContractType { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match *self { - ContractType::Keys => write!(f, "keys"), - ContractType::Threshold => write!(f, "threshold"), - ContractType::Proxy => write!(f, "proxy") +/// Note that the `Emission` contract is V2 only. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ContractType { + Keys, + Threshold, + Proxy, + Emission, +} + +impl From 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", &"") + .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 + { + if contract_type == ContractType::Emission && version == ContractVersion::V1 { + return Err(Error::EmissionFundsV1ContractDoesNotExist); + } + + let addr_env_var = format!( + "{}_CONTRACT_ADDRESS_{}_{:?}", + contract_type.uppercase(), + network.uppercase(), + version, + ); + + let addr = if let Ok(s) = env::var(&addr_env_var) { + match Address::from_str(s.trim_left_matches("0x")) { + Ok(addr) => addr, + Err(_) => return Err(Error::InvalidContractAddr(s.into())), + } + } else { + return Err(Error::MissingEnvVar(addr_env_var)); + }; + + let abi_path = format!( + "abis/{}/{}", + version.lowercase(), + contract_type.abi_file_name(), + ); + + let abi_file = File::open(&abi_path) + .map_err(|_| Error::MissingAbiFile(abi_path.clone()))?; + + let abi = Contract::load(&abi_file) + .map_err(|_| Error::InvalidAbi(abi_path))?; + + let contract = PoaContract { kind: contract_type, version, addr, abi }; + Ok(contract) } pub fn event(&self, event: &str) -> Event { @@ -124,144 +154,261 @@ impl PoaContract { } } -impl Display for PoaContract { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "PoaContract({:?}, {:?})", self.kind, self.addr) - } +#[derive(Clone, Copy, Debug)] +pub enum StartBlock { + Earliest, + Latest, + Number(u64), + Tail(u64), } -impl Debug for PoaContract { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}", self) - } -} - -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Config { pub network: Network, pub endpoint: String, + pub version: ContractVersion, pub contracts: Vec, pub start_block: StartBlock, - pub send_email_notifications: bool, - pub send_push_notifications: bool, - pub validators: Vec, - 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, + pub smtp_host_domain: Option, + pub smtp_port: Option, + pub smtp_username: Option, + pub smtp_password: Option, + pub outgoing_email_addr: Option, + pub notification_limit: Option, + pub log_emails: bool, + pub log_to_file: bool, } impl Config { - pub fn load() -> Self { - dotenv().ok(); - let cli = Cli::load(); - - let network = if let Some(s) = cli.value_of("network") { - s.into() - } else if cli.is_present("core") { + pub fn new(cli: &Cli) -> Result { + 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 = 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 = 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 = env::var("EMAIL_RECIPIENTS") + .map_err(|_| Error::MissingEnvVar("EMAIL_RECIPIENTS".into()))? + .split(',') + .filter_map(|s| { + if s.is_empty() { + None + } else { + Some(s.into()) + } }) .collect(); - let start_block = if let Some(s) = cli.value_of("start_block") { - s.into() - } else if cli.is_present("earliest") { - StartBlock::Earliest - } else if cli.is_present("latest") { - StartBlock::Latest - } else if let Some(s) = cli.value_of("tail") { - StartBlock::Tail(s.parse().expect("Invalid tail value")) + let smtp_host_domain = if email_notifications { + let host = env::var("SMTP_HOST_DOMAIN") + .map_err(|_| Error::MissingEnvVar("SMTP_HOST_DOMAIN".into()))?; + Some(host) } else { - env::var("START_BLOCK").unwrap().as_str().into() + None }; - let send_email_notifications = if cli.is_present("email") { - true + let smtp_port = if email_notifications { + if let Ok(s) = env::var("SMTP_PORT") { + let port = s.parse().map_err(|_| Error::InvalidSmtpPort(s.into()))?; + Some(port) + } else { + return Err(Error::MissingEnvVar("SMTP_PORT".into())); + } } else { - env::var("SEND_EMAIL_NOTIFICATIONS").unwrap().parse().unwrap() + None }; - let send_push_notifications = if cli.is_present("push") { - true + let smtp_username = if email_notifications { + let username = env::var("SMTP_USERNAME") + .map_err(|_| Error::MissingEnvVar("SMTP_USERNAME".into()))?; + Some(username) } else { - env::var("SEND_PUSH_NOTIFICATIONS").unwrap().parse().unwrap() + None }; - let validators = env::var("VALIDATORS").unwrap().split(',') - .map(|s| Validator { email: s.into(), name: "".into() }) - .collect(); - - let avg_block_time = if let Some(s) = cli.value_of("block_time") { - Duration::from_secs(s.parse().unwrap()) + let smtp_password = if email_notifications { + let password = env::var("SMTP_PASSWORD") + .map_err(|_| Error::MissingEnvVar("SMTP_PASSWORD".into()))?; + Some(password) } else { - let s = env::var("AVG_BLOCK_TIME_SECS").unwrap(); - Duration::from_secs(s.parse().unwrap()) + None }; - let smtp_host_domain = env::var("SMTP_HOST_DOMAIN").unwrap(); - let smtp_port = env::var("SMTP_PORT").unwrap().parse().unwrap(); - let smtp_username = env::var("SMTP_USERNAME").unwrap(); - let smtp_password = env::var("SMTP_PASSWORD").unwrap(); - let outgoing_email = env::var("OUTGOING_EMAIL_ADDRESS").unwrap(); + let outgoing_email_addr = if email_notifications { + let email_addr = env::var("OUTGOING_EMAIL_ADDRESS") + .map_err(|_| Error::MissingEnvVar("OUTGOING_EMAIL_ADDRESS".into()))?; + Some(email_addr) + } else { + None + }; - Config { - network, endpoint, contracts, start_block, - send_email_notifications, send_push_notifications, - validators, avg_block_time, smtp_host_domain, - smtp_port, smtp_username, smtp_password, outgoing_email + let notification_limit = if let Some(s) = cli.notification_limit() { + let limit = s.parse().map_err(|_| Error::InvalidNotificationLimit(s.into()))?; + Some(limit) + } else { + None + }; + + let log_emails = cli.log_emails(); + let log_to_file = cli.log_to_file(); + + let config = Config { + network, + endpoint, + version, + contracts, + start_block, + block_time, + email_notifications, + email_recipients, + smtp_host_domain, + smtp_port, + smtp_username, + smtp_password, + outgoing_email_addr, + notification_limit, + log_emails, + log_to_file, + }; + Ok(config) + } +} + +#[cfg(test)] +mod tests { + use std::env; + + use super::super::tests::setup; + use super::{ContractType, ContractVersion, PoaContract, Network}; + + const CONTRACT_TYPES: [ContractType; 4] = [ + ContractType::Keys, + ContractType::Threshold, + ContractType::Proxy, + ContractType::Emission, + ]; + const NETWORKS: [Network; 2] = [Network::Sokol, Network::Core]; + const VERSIONS: [ContractVersion; 2] = [ContractVersion::V1, ContractVersion::V2]; + + #[test] + fn test_env_file_integrity() { + setup(); + for network in NETWORKS.iter() { + let env_var = format!("{}_RPC_ENDPOINT", network.uppercase()); + assert!(env::var(&env_var).is_ok()); + for contract_type in CONTRACT_TYPES.iter() { + for version in VERSIONS.iter() { + if *contract_type == ContractType::Emission && *version == ContractVersion::V1 { + continue; + } + let env_var = format!( + "{}_CONTRACT_ADDRESS_{}_{:?}", + contract_type.uppercase(), + network.uppercase(), + version, + ); + assert!(env::var(&env_var).is_ok()); + } + } + } + } + + #[test] + fn test_load_contract_abis() { + setup(); + for contract_type in CONTRACT_TYPES.iter() { + for version in VERSIONS.iter() { + for network in NETWORKS.iter() { + let res = PoaContract::read(*contract_type, *network, *version); + if *contract_type == ContractType::Emission && *version == ContractVersion::V1 { + assert!(res.is_err()); + } else { + assert!(res.is_ok()); + } + } + } } } } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..bd04ae3 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,39 @@ +use ctrlc; +use jsonrpc_core; +use ethabi; +use failure; +use lettre; +use native_tls; +use reqwest; + +pub type Result = ::std::result::Result; + +#[derive(Debug)] +pub enum Error { + CtrlcError(ctrlc::Error), + EmissionFundsV1ContractDoesNotExist, + FailedToBuildEmail(failure::Error), + FailedToBuildRequest(reqwest::Error), + FailedToBuildTls(native_tls::Error), + FailedToParseBallotCreatedLog(String), + FailedToParseRawLogToLog(ethabi::Error), + FailedToResolveSmtpHostDomain(lettre::smtp::error::Error), + FailedToSendEmail(lettre::smtp::error::Error), + InvalidAbi(String), + InvalidBlockTime(String), + InvalidContractAddr(String), + InvalidNotificationLimit(String), + InvalidSmtpPort(String), + InvalidStartBlock(String), + InvalidTail(String), + JsonRpcResponseFailure(jsonrpc_core::types::response::Failure), + MissingAbiFile(String), + MissingEnvVar(String), + MustSpecifyAtLeastOneCliArgument(String), + MustSpecifyOneCliArgument(String), + RequestFailed(reqwest::Error), + StartBlockExceedsLastBlockMined { + start_block: u64, + last_mined_block: u64, + }, +} diff --git a/src/logger.rs b/src/logger.rs new file mode 100644 index 0000000..72c3be6 --- /dev/null +++ b/src/logger.rs @@ -0,0 +1,245 @@ +use std::fs::{self, create_dir, File, read_dir, remove_file}; +use std::io::stderr; +use std::path::Path; + +use chrono::{DateTime, TimeZone, Utc}; +use slog::{self, Drain}; +use slog_term::{FullFormat, PlainSyncDecorator}; +use web3::types::BlockNumber; + +use config::Config; +use error::Error; +use notify::Notification; + +// The date format used to name log files; e.g. "Oct-08-2018-14:09:00". +const FILE_NAME_DATE_FORMAT: &str = "%b-%d-%Y-%H:%M:%S"; +// The directory (relative to Cargo.toml) to store logs. +const LOGS_DIR: &str = "logs"; +const MAX_NUMBER_OF_LOG_FILES: usize = 3; +const MAX_LOG_FILE_SIZE_MB: usize = 4; +const MAX_LOG_FILE_SIZE_BYTES: usize = MAX_LOG_FILE_SIZE_MB * 1024 * 1024; +// We dont want to check the log file's size after every log that is written, this constant states +// "after this many logs have been written, check the log file's size". This value assumes an +// average log is around 100 ASCII characters (bytes) long. +const INITIAL_CHECK_FILE_SIZE_AT: usize = MAX_LOG_FILE_SIZE_BYTES / 100; + +fn create_logs_dir() { + let logs_dir = Path::new(LOGS_DIR); + if !logs_dir.exists() { + create_dir(logs_dir) + .unwrap_or_else(|e| panic!("could not create ./logs directory: {:?}", e)); + } +} + +fn read_logs_dir() -> Vec { + let mut log_files: Vec = read_dir(LOGS_DIR) + .unwrap_or_else(|e| panic!("could not read ./logs directory: {:?}", e)) + .filter_map(|res| { + let path = res.ok()?.path(); + let file_name = path.file_name().unwrap().to_str().unwrap(); + LogFile::from_file_name(file_name).ok() + }).collect(); + log_files.sort_unstable(); + log_files +} + +fn rotate_log_files(log_files: &mut Vec) -> File { + while log_files.len() >= MAX_NUMBER_OF_LOG_FILES { + let log_file_to_remove = log_files.remove(0); + log_file_to_remove.remove_file(); + } + let log_file = LogFile::now(); + let file = log_file.create_file(); + log_files.push(log_file); + file +} + +fn get_file_size_in_bytes(path: &str) -> usize { + fs::metadata(&path) + .unwrap_or_else(|_| panic!("log file does not exist: {}", path)) + .len() as usize +} + +enum LogLocation { + Stderr, + File(File), +} + +fn create_slog_logger(log_location: LogLocation) -> slog::Logger { + if let LogLocation::File(file) = log_location { + let decorator = PlainSyncDecorator::new(file); + let drain = FullFormat::new(decorator).build().fuse(); + slog::Logger::root(drain, o!()) + } else { + let decorator = PlainSyncDecorator::new(stderr()); + let drain = FullFormat::new(decorator).build().fuse(); + slog::Logger::root(drain, o!()) + } +} + +#[derive(Eq, Ord, PartialEq, PartialOrd)] +struct LogFile(DateTime); + +impl LogFile { + fn now() -> Self { + LogFile(Utc::now()) + } + + fn from_file_name(file_name: &str) -> Result { + if let Ok(dt) = Utc.datetime_from_str(file_name, FILE_NAME_DATE_FORMAT) { + Ok(LogFile(dt)) + } else { + Err(()) + } + } + + fn file_name(&self) -> String { + self.0.format(FILE_NAME_DATE_FORMAT).to_string() + } + + fn path(&self) -> String { + format!("{}/{}", LOGS_DIR, self.file_name()) + } + + fn create_file(&self) -> File { + let path = self.path(); + File::create(&path) + .unwrap_or_else(|_| panic!("failed to create log file: {}", path)) + } + + fn remove_file(&self) { + let path = self.path(); + remove_file(&path) + .unwrap_or_else(|_| panic!("failed to delete log file: {}", path)) + } +} + +pub struct Logger { + logger: slog::Logger, + log_files: Vec, + log_count: usize, + check_file_size_at: usize, +} + +impl Logger { + pub fn new(config: &Config) -> Self { + let (logger, log_files) = if config.log_to_file { + create_logs_dir(); + let mut log_files = read_logs_dir(); + let current_log_file = rotate_log_files(&mut log_files); + let logger = create_slog_logger(LogLocation::File(current_log_file)); + (logger, log_files) + } else { + let logger = create_slog_logger(LogLocation::Stderr); + (logger, vec![]) + }; + Logger { + logger, + log_files, + log_count: 0, + check_file_size_at: INITIAL_CHECK_FILE_SIZE_AT, + } + } + + fn logging_to_file(&self) -> bool { + !self.log_files.is_empty() + } + + fn should_rotate_log_file(&mut self) -> bool { + if self.logging_to_file() { + if self.log_count >= self.check_file_size_at { + let path = self.log_files.last().unwrap().path(); + let file_size = get_file_size_in_bytes(&path); + if file_size >= MAX_LOG_FILE_SIZE_BYTES { + return true; + } + let avg_bytes_per_log = file_size / self.log_count; + let remaining_bytes = MAX_LOG_FILE_SIZE_BYTES - file_size; + let remaining_logs = remaining_bytes / avg_bytes_per_log; + self.check_file_size_at += remaining_logs; + } + } + false + } + + fn rotate_log_file(&mut self) { + let new_log_file = rotate_log_files(&mut self.log_files); + self.logger = create_slog_logger(LogLocation::File(new_log_file)); + self.log_count = 0; + self.check_file_size_at = INITIAL_CHECK_FILE_SIZE_AT; + } + + fn increment_log_count(&mut self) { + self.log_count += 1; + if self.should_rotate_log_file() { + self.rotate_log_file(); + } + } + + pub fn log_starting_poagov(&mut self) { + info!(&self.logger, "starting poagov..."); + self.increment_log_count(); + } + + pub fn log_ctrlc(&mut self) { + warn!(&self.logger, "recieved ctrl-c signal, gracefully shutting down..."); + self.increment_log_count(); + } + + pub fn log_no_email_recipients_configured(&mut self) { + warn!(&self.logger, "email notifications are enabled, but there are no email recipients"); + self.increment_log_count(); + } + + pub fn log_notification_email_body(&mut self, notif: &Notification) { + info!(&self.logger, "governance notification\n{}", notif.email_text()); + self.increment_log_count(); + } + + pub fn log_notification(&mut self, notif: &Notification) { + let ballot_created_log = notif.log(); + info!( + &self.logger, + "governance notification"; + "ballot" => format!("{:?}", ballot_created_log.ballot_type), + "ballot_id" => format!("{}", ballot_created_log.ballot_id), + "block_number" => format!("{}", ballot_created_log.block_number) + ); + self.increment_log_count(); + } + + pub fn log_failed_to_build_email(&mut self, e: Error) { + warn!(&self.logger, "failed to build email"; "error" => format!("{:?}", e)); + self.increment_log_count(); + } + + pub fn log_failed_to_send_email(&mut self, recipient: &str, e: Error) { + warn!( + &self.logger, + "failed to send email"; + "recipient" => recipient, + "error" => format!("{:?}", e) + ); + self.increment_log_count(); + } + + pub fn log_email_sent(&mut self, recipient: &str) { + info!(&self.logger, "email sent"; "to" => recipient); + self.increment_log_count(); + } + + pub fn log_reached_notification_limit(&mut self, notification_limit: usize) { + warn!( + &self.logger, + "reached notification limit, gracefully shutting down..."; + "limit" => notification_limit + ); + self.increment_log_count(); + } + + pub fn log_finished_block_window(&mut self, start: BlockNumber, stop: BlockNumber) { + let block_range = format!("{:?}...{:?}", start, stop); + info!(&self.logger, "finished checking blocks"; "block_range" => block_range); + self.increment_log_count(); + } +} diff --git a/src/logging.rs b/src/logging.rs deleted file mode 100644 index 88ea932..0000000 --- a/src/logging.rs +++ /dev/null @@ -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) - ); -} diff --git a/src/main.rs b/src/main.rs index 589c96d..3d19c76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,51 +1,124 @@ -#![feature(try_from)] - extern crate chrono; extern crate clap; +extern crate ctrlc; extern crate dotenv; extern crate ethabi; extern crate ethereum_types; +extern crate failure; extern crate hex; extern crate jsonrpc_core; -#[macro_use] extern crate lazy_static; extern crate lettre; extern crate lettre_email; extern crate native_tls; extern crate reqwest; -extern crate serde; extern crate serde_json; -#[macro_use] extern crate slog; +#[macro_use] +extern crate slog; extern crate slog_term; extern crate web3; +mod blockchain; mod cli; +mod client; mod config; -mod logging; +mod error; +mod logger; mod notify; -mod rpc; -mod utils; +mod response; -use config::Config; -use notify::Notifier; -use rpc::{BlockchainIter, RpcClient}; +use std::sync::{Arc, Mutex}; +use std::sync::atomic::{AtomicBool, Ordering}; -fn main() { - let config = Config::load(); - let client = RpcClient::new(&config.endpoint); - let mut notifier = Notifier::new(&config).unwrap(); +use blockchain::BlockchainIter; +use cli::Cli; +use client::RpcClient; +use config::{Config, ContractVersion}; +use error::{Error, Result}; +use logger::Logger; +use notify::{Notification, Notifier}; + +fn load_env_file() { + if let Err(e) = dotenv::dotenv() { + match e { + dotenv::Error::Io(_) => panic!("could not find .env file"), + _ => panic!("coule not parse .env file"), + }; + } +} + +fn set_ctrlc_handler(logger: Arc>) -> Result> { + let running = Arc::new(AtomicBool::new(true)); + let result = Ok(running.clone()); + ctrlc::set_handler(move || { + logger.lock().unwrap().log_ctrlc(); + running.store(false, Ordering::SeqCst); + }).map_err(|e| Error::CtrlcError(e))?; + result +} + +fn main() -> Result<()> { + load_env_file(); - for (start_block, stop_block) in BlockchainIter::new(&client, &config) { - for contract in &config.contracts { - let ballot_created_logs = client - .get_ballot_created_logs(contract, start_block, stop_block) - .unwrap(); + let cli = Cli::parse(); + let config = Config::new(&cli)?; + let logger = Arc::new(Mutex::new(Logger::new(&config))); + if config.email_notifications && config.email_recipients.is_empty() { + logger.lock().unwrap().log_no_email_recipients_configured(); + } + let running = set_ctrlc_handler(logger.clone())?; + let client = RpcClient::new(config.endpoint.clone()); + let mut notifier = Notifier::new(&config, logger.clone())?; + logger.lock().unwrap().log_starting_poagov(); - for log in &ballot_created_logs { - let voting_data = client.get_voting_state(contract, log.ballot_id).unwrap(); - let notif = notifier.build_notification(log, &voting_data); - notifier.notify_validators(¬if); + '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(¬if2.log().block_number) + }); + for notification in notifications.iter() { + notifier.notify(notification); + if notifier.reached_limit() { + let limit = config.notification_limit.unwrap(); + logger.lock().unwrap().log_reached_notification_limit(limit); + break 'main_loop; + } + } + logger.lock().unwrap().log_finished_block_window(start_block, stop_block); + } + + Ok(()) +} + +#[cfg(test)] +pub mod tests { + use super::load_env_file; + + static mut LOADED_ENV_FILE: bool = false; + + pub fn setup() { + unsafe { + if !LOADED_ENV_FILE { + load_env_file(); + LOADED_ENV_FILE = true; } } } } - diff --git a/src/notify.rs b/src/notify.rs index ab34160..193070a 100644 --- a/src/notify.rs +++ b/src/notify.rs @@ -1,199 +1,187 @@ -use chrono::{DateTime, Utc}; -use ethereum_types::Address; -use lettre::{EmailTransport, SmtpTransport}; -use lettre::smtp::{ClientSecurity, ConnectionReuseParameters, SmtpTransportBuilder}; +use std::sync::{Arc, Mutex}; + +use lettre::{SendableEmail, Transport}; +use lettre::smtp::{ClientSecurity, ConnectionReuseParameters, SmtpClient, SmtpTransport}; use lettre::smtp::authentication::{Credentials, Mechanism}; -use lettre::smtp::client::net::{ClientTlsParameters, DEFAULT_TLS_PROTOCOLS}; -use lettre::smtp::error::Error as BuildSmtpError; +use lettre::smtp::client::net::ClientTlsParameters; use lettre_email::{Email, EmailBuilder}; -use lettre_email::error::Error as BuildEmailError; use native_tls::TlsConnector; -use config::{Config, ContractType, Network, Validator}; -use logging::{log_email_failed, log_email_sent, log_notification}; -use rpc::{BallotCreatedLog, BallotType, KeyType, VotingData}; +use config::Config; +use error::{Error, Result}; +use logger::Logger; +use response::common::BallotCreatedLog; +use response::v1::VotingState; +use response::v2::BallotInfo; -#[derive(Debug)] -pub enum Notification { - Keys(KeysNotification), - Threshold(ThresholdNotification), - Proxy(ProxyNotification) +#[derive(Clone, Debug)] +pub enum Notification<'a> { + VotingState { + config: &'a Config, + log: BallotCreatedLog, + voting_state: VotingState, + }, + BallotInfo { + config: &'a Config, + log: BallotCreatedLog, + ballot_info: BallotInfo, + }, } -#[derive(Debug)] -pub struct KeysNotification { - pub network: Network, - pub endpoint: String, - pub block_number: u64, - pub contract_type: ContractType, - pub ballot_type: BallotType, - pub ballot_id: u64, - pub start_time: DateTime, - pub end_time: DateTime, - 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, - pub end_time: DateTime, - 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, - pub end_time: DateTime, - 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 + emailer: Option, + logger: Arc>, + notification_count: usize, } impl<'a> Notifier<'a> { - pub fn new(config: &'a Config) -> Result { - let mut notifier = Notifier { config, mailer: None }; - - if config.send_email_notifications { - let smtp_addr = (config.smtp_host_domain.as_str(), config.smtp_port); - - let smtp_tls = { - let mut tls_builder = TlsConnector::builder().unwrap(); - tls_builder.supported_protocols(DEFAULT_TLS_PROTOCOLS).unwrap(); - let tls = tls_builder.build().unwrap(); - let tls_params = ClientTlsParameters::new(config.smtp_host_domain.clone(), tls); - ClientSecurity::Required(tls_params) + pub fn new(config: &'a Config, logger: Arc>) -> Result { + let emailer = if config.email_notifications { + let domain = config.smtp_host_domain.clone().unwrap(); + let port = config.smtp_port.unwrap(); + let addr = (domain.as_str(), port); + let security = { + let tls = TlsConnector::new().map_err(|e| Error::FailedToBuildTls(e))?; + let smtp_security_setup = ClientTlsParameters::new(domain.clone(), tls); + ClientSecurity::Required(smtp_security_setup) }; - - let smtp_creds = Credentials::new( - config.smtp_username.clone(), - config.smtp_password.clone() + let creds = Credentials::new( + config.smtp_username.clone().unwrap(), + config.smtp_password.clone().unwrap(), ); - - let mailer = SmtpTransportBuilder::new(smtp_addr, smtp_tls)? + let smtp = SmtpClient::new(addr, security) + .map_err(|e| Error::FailedToResolveSmtpHostDomain(e))? .connection_reuse(ConnectionReuseParameters::ReuseUnlimited) .authentication_mechanism(Mechanism::Plain) - .credentials(smtp_creds) - .build(); + .credentials(creds) + .transport(); + Some(smtp) + } else { + None + }; + Ok(Notifier { config, emailer, logger, notification_count: 0 }) + } - notifier.mailer = Some(mailer); + pub fn notify(&mut self, notif: &Notification) { + if self.config.log_emails { + self.logger.lock().unwrap().log_notification_email_body(notif); + } else { + self.logger.lock().unwrap().log_notification(notif); } - - Ok(notifier) - } - - pub fn build_notification(&self, log: &BallotCreatedLog, voting_data: &VotingData) -> Notification { - Notification::new(&self.config, log, voting_data) - } - - pub fn notify_validators(&mut self, notif: &Notification) { - log_notification(notif); - for validator in &self.config.validators { - if self.config.send_email_notifications { - let email = self.build_email(validator, notif).unwrap(); - if let Some(ref mut mailer) = self.mailer { - match mailer.send(&email) { - Ok(_) => log_email_sent(&validator.email), - Err(e) => log_email_failed(&validator.email, e) - }; + if self.config.email_notifications { + for recipient in self.config.email_recipients.iter() { + let email: SendableEmail = match self.build_email(notif, recipient) { + Ok(email) => email.into(), + Err(e) => { + self.logger.lock().unwrap().log_failed_to_build_email(e); + continue; + }, + }; + if let Err(e) = self.send_email(email) { + self.logger.lock().unwrap().log_failed_to_send_email(recipient, e); + } else { + self.logger.lock().unwrap().log_email_sent(recipient); } } + } + self.notification_count += 1; + } - if self.config.send_push_notifications { - println!("Push Notifications not yet implemented."); - } + pub fn reached_limit(&self) -> bool { + if let Some(limit) = self.config.notification_limit { + self.notification_count >= limit + } else { + false } } - fn build_email(&self, validator: &Validator, notif: &Notification) -> Result { - 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 { + 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"); } } } diff --git a/src/response/common.rs b/src/response/common.rs new file mode 100644 index 0000000..400e8f7 --- /dev/null +++ b/src/response/common.rs @@ -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 { + 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 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 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 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 { + let mut ballot_id: Option = None; + let mut ballot_type: Option = None; + let mut creator: Option
= 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 }) + } +} diff --git a/src/response/mod.rs b/src/response/mod.rs new file mode 100644 index 0000000..75aa03f --- /dev/null +++ b/src/response/mod.rs @@ -0,0 +1,3 @@ +pub mod common; +pub mod v1; +pub mod v2; diff --git a/src/response/v1.rs b/src/response/v1.rs new file mode 100644 index 0000000..3d63265 --- /dev/null +++ b/src/response/v1.rs @@ -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 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 for VotingState { + fn from(keys_voting_state: KeysVotingState) -> Self { + VotingState::Keys(keys_voting_state) + } +} + +impl From for VotingState { + fn from(threshold_voting_state: ThresholdVotingState) -> Self { + VotingState::Threshold(threshold_voting_state) + } +} + +impl From 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, + pub end_time: DateTime, + 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> for KeysVotingState { + fn from(tokens: Vec) -> 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, + pub end_time: DateTime, + 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> for ThresholdVotingState { + fn from(tokens: Vec) -> 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, + pub end_time: DateTime, + 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> for ProxyVotingState { + fn from(tokens: Vec) -> 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, + ) + } +} diff --git a/src/response/v2.rs b/src/response/v2.rs new file mode 100644 index 0000000..c71e9c6 --- /dev/null +++ b/src/response/v2.rs @@ -0,0 +1,397 @@ +use chrono::{DateTime, Utc}; +use ethabi; +use web3::types::{Address, U256}; + +use response::common::{u256_to_datetime, BallotType, KeyType}; + +#[derive(Clone, Debug)] +pub enum BallotInfo { + Keys(KeysBallotInfo), + Threshold(ThresholdBallotInfo), + Proxy(ProxyBallotInfo), + Emission(EmissionBallotInfo), +} + +impl From for BallotInfo { + fn from(keys_ballot_info: KeysBallotInfo) -> Self { + BallotInfo::Keys(keys_ballot_info) + } +} + +impl From for BallotInfo { + fn from(threshold_ballot_info: ThresholdBallotInfo) -> Self { + BallotInfo::Threshold(threshold_ballot_info) + } +} + +impl From for BallotInfo { + fn from(proxy_ballot_info: ProxyBallotInfo) -> Self { + BallotInfo::Proxy(proxy_ballot_info) + } +} + +impl From 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, + pub end_time: DateTime, + 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> for KeysBallotInfo { + fn from(tokens: Vec) -> 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, + pub end_time: DateTime, + 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> for ThresholdBallotInfo { + fn from(tokens: Vec) -> 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, + pub end_time: DateTime, + 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> for ProxyBallotInfo { + fn from(tokens: Vec) -> 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, + pub start_time: DateTime, + pub end_time: DateTime, + pub is_canceled: bool, + pub is_finalized: bool, + pub creator: Address, + pub memo: String, + pub amount: U256, + pub burn_votes: U256, + pub freeze_votes: U256, + pub send_votes: U256, + pub receiver: Address, +} + +impl From> for EmissionBallotInfo { + fn from(tokens: Vec) -> Self { + let creation_time = { + let uint = tokens[0].clone().to_uint().unwrap(); + u256_to_datetime(uint) + }; + let start_time = { + let uint = tokens[1].clone().to_uint().unwrap(); + u256_to_datetime(uint) + }; + let end_time = { + let uint = tokens[2].clone().to_uint().unwrap(); + u256_to_datetime(uint) + }; + let is_canceled = tokens[3].clone().to_bool().unwrap(); + let is_finalized = tokens[4].clone().to_bool().unwrap(); + let creator = tokens[5].clone().to_address().unwrap(); + let memo = tokens[6].clone().to_string().unwrap(); + let amount = tokens[7].clone().to_uint().unwrap(); + let burn_votes = tokens[8].clone().to_uint().unwrap(); + let freeze_votes = tokens[9].clone().to_uint().unwrap(); + let send_votes = tokens[10].clone().to_uint().unwrap(); + let receiver = tokens[11].clone().to_address().unwrap(); + EmissionBallotInfo { + creation_time, + start_time, + end_time, + is_canceled, + is_finalized, + creator, + memo, + amount, + burn_votes, + freeze_votes, + send_votes, + receiver, + } + } +} + +impl EmissionBallotInfo { + fn email_text(&self) -> String { + format!( + "Creation Time: {}\n\ + Voting Start Time: {}\n\ + Voting End Time: {}\n\ + Amount: {}\n\ + Burn Votes: {}\n\ + Freeze Votes: {}\n\ + Send Votes: {}\n\ + Receiver: {:?}\n\ + Voting was Canceled: {}\n\ + Voting has Finished: {}\n\ + Ballot Creator: {:?}\n\ + Memo: {}\n", + self.creation_time, + self.start_time, + self.end_time, + convert_wei_to_poa(self.amount), + self.burn_votes, + self.freeze_votes, + self.send_votes, + self.receiver, + self.is_canceled, + self.is_finalized, + self.creator, + self.memo, + ) + } +} + +/// Converts the `amount` field found in the `VotingToManageEmissionFunds` contract from Wei to +/// POA. +fn convert_wei_to_poa(amount_in_wei: U256) -> f64 { + let whole_poa = amount_in_wei / U256::exp10(18); + let remaining_poa = { + let whole_with_padding = whole_poa * U256::exp10(18); + (amount_in_wei - whole_with_padding).low_u64() as f64 + }; + let fraction_of_a_poa = { + let eighteen_zeros: String = (0..18).map(|_| '0').collect(); + let max_fract: f64 = format!("1{}", eighteen_zeros).parse().unwrap(); + remaining_poa / max_fract + }; + whole_poa.low_u64() as f64 + fraction_of_a_poa +} diff --git a/src/rpc.rs b/src/rpc.rs deleted file mode 100644 index d696b31..0000000 --- a/src/rpc.rs +++ /dev/null @@ -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 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 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 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 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 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) -> Self { - let voting_data: KeysVotingData = tokens.into(); - VotingData::Keys(voting_data) - } - - pub fn threshold(tokens: Vec) -> Self { - let voting_data: ThresholdVotingData = tokens.into(); - VotingData::Threshold(voting_data) - } - - pub fn proxy(tokens: Vec) -> Self { - let voting_data: ProxyVotingData = tokens.into(); - VotingData::Proxy(voting_data) - } - - pub fn start_time(&self) -> DateTime { - 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 { - 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, - pub end_time: DateTime, - 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> for KeysVotingData { - fn from(tokens: Vec) -> 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, - pub end_time: DateTime, - 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> for ThresholdVotingData { - fn from(tokens: Vec) -> 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, - pub end_time: DateTime, - 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> for ProxyVotingData { - fn from(tokens: Vec) -> 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 { - 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 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) -> 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 { - 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 { - 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> { - 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> - { - 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 = 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 { - 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!(); - } -} diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index 8348f1a..0000000 --- a/src/utils.rs +++ /dev/null @@ -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 { - let hex = hex.trim_left_matches("0x"); - u64::from_str_radix(hex, 16) -} - -pub fn u256_to_datetime(uint: U256) -> DateTime { - let n_secs = i64::try_from(uint.as_u64()).unwrap(); - let timestamp = NaiveDateTime::from_timestamp(n_secs, 0); - DateTime::from_utc(timestamp, Utc) -}