Initial commit.

This commit is contained in:
Peter van Nostrand 2018-04-21 12:59:11 -04:00
commit 7a6e5f9dfe
16 changed files with 4092 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/target
**/*.rs.bk
Cargo.lock
.env

23
Cargo.toml Normal file
View File

@ -0,0 +1,23 @@
[package]
name = "poagov"
version = "1.0.0"
authors = ["Peter van Nostrand <jnz@riseup.net>"]
[dependencies]
chrono = "0.4"
clap = "2.31.2"
dotenv = "0.11.0"
ethabi = "5.1.1"
ethereum-types = "0.3.1"
hex = "0.3.1"
jsonrpc-core = "8.0.1"
lazy_static = "1.0.0"
lettre = "0.8"
lettre_email = "0.8"
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"] }
slog-term = "2.4.0"
web3 = "0.3.0"

96
README.md Normal file
View File

@ -0,0 +1,96 @@
# poagov
A tool to monitor the POA Network's blockchain for goverance events
[governance events](https://github.com/poanetwork/wiki/wiki/Governance-Overview).
# Building
To build the `poagov` CLI tool, run the following:
$ git clone https://gitlab.com/DrPeterVanNostrand/poagov.git
$ cd poagov
$ cargo build --release
# Usage
Once you have built `poagov`, you can print out the CLI usage by running:
$ ./target/release/poagov --help
poagov 1.0
Monitores the POA Network's 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
OPTIONS:
--block-time <value> the average time it takes to mine a new block
--monitor <value> a comma-separated list of ballot types to monitor for governance events; the available values are: keys, threshold, proxy
--network <value> the name of the network to monitor for ballots; the values available for this option are: core, sokol, local
--rpc <value> the URL for the RPC endpoint
--start <value> start monitoring for governance events at this block (inclusive)
--tail <value> start monitoring for governance events for the `n` blocks prior to the last mined block in the chain
# 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:
SMTP_HOST_DOMAIN=
SMTP_USERNAME=
SMTP_PASSWORD=
OUTGOING_EMAIL_ADDRESS=
Add a comma-separated list of email address to the "VALIDATORS" config
option in your .env file. These addresses will be sent emails when `poagov`
encounters governance events on the POA blockchain.
# An Explained Example
$ ./target/release/poagov --sokol --earliest -kt --email
- `--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`.
# 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`:
Apr 21 08:31:54.219 INFO notification, data: Threshold(
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
}
)

773
abis/core/keys.json Normal file
View File

@ -0,0 +1,773 @@
[
{
"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": "_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": "",
"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": 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": [],
"name": "activeBallotsLength",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"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": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "isActive",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_ballotType",
"type": "uint256"
},
{
"name": "_affectedKey",
"type": "address"
},
{
"name": "_affectedKeyType",
"type": "uint256"
},
{
"name": "_miningKey",
"type": "address"
}
],
"name": "areBallotParamsValid",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"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"
}
],
"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": "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": "getBallotType",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"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": "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",
"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"
}
]

0
abis/locol/.gitkeep Normal file
View File

773
abis/sokol/keys.json Normal file
View File

@ -0,0 +1,773 @@
[
{
"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": "_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": "",
"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": 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": [],
"name": "activeBallotsLength",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"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": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "isActive",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_ballotType",
"type": "uint256"
},
{
"name": "_affectedKey",
"type": "address"
},
{
"name": "_affectedKeyType",
"type": "uint256"
},
{
"name": "_miningKey",
"type": "address"
}
],
"name": "areBallotParamsValid",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"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"
}
],
"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": "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": "getBallotType",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"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": "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",
"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"
}
]

665
abis/sokol/proxy.json Normal file
View File

@ -0,0 +1,665 @@
[
{
"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"
}
]

638
abis/sokol/threshold.json Normal file
View File

@ -0,0 +1,638 @@
[
{
"constant": false,
"inputs": [
{
"name": "_id",
"type": "uint256"
}
],
"name": "finalize",
"outputs": [],
"payable": false,
"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": [
{
"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": 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": "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": "uint256"
}
],
"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": "uint256"
},
{
"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"
}
]

34
sample.env Normal file
View File

@ -0,0 +1,34 @@
# 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
# Email configuration.
SMTP_HOST_DOMAIN=
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=

30
src/cli.rs Normal file
View File

@ -0,0 +1,30 @@
use clap::{ArgMatches, App};
pub struct Cli;
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.")
.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)'
[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()
}
}

279
src/config.rs Normal file
View File

@ -0,0 +1,279 @@
use std::env;
use std::fmt::{self, Debug, Display, 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 web3::types::BlockNumber;
use cli::Cli;
use utils::hex_string_to_u64;
#[derive(Clone, Copy, Debug)]
pub enum Network { Core, Sokol, Local }
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")
}
}
}
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)
}
}
}
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")
}
}
}
impl ContractType {
fn to_uppercase(&self) -> String {
format!("{}", self).to_uppercase()
}
}
#[derive(Clone, Copy, Debug)]
pub struct StartBlock {
pub block: BlockNumber,
pub tail: u64
}
impl StartBlock {
fn latest() -> Self {
StartBlock { block: BlockNumber::Latest, tail: 0 }
}
fn earliest() -> Self {
StartBlock { block: BlockNumber::Earliest, tail: 0 }
}
fn tail(tail: u64) -> Self {
StartBlock { block: BlockNumber::Latest, tail }
}
}
impl<'a> From<&'a str> for StartBlock {
fn from(s: &'a str) -> Self {
let mut start_block = StartBlock::latest();
if s.starts_with("-") {
start_block.tail = s[1..].parse().expect("Invalid start-block");
} else if s == "earliest" {
start_block.block = BlockNumber::Number(0);
} else if s.starts_with("0x") {
start_block.block = hex_string_to_u64(s).expect("Invalid start-block").into();
} else if s != "latest" {
start_block.block = s.parse::<u64>().expect("Invalid start-block").into();
}
start_block
}
}
#[derive(Clone, Debug)]
pub struct Validator {
pub name: String,
pub email: String
}
pub struct PoaContract {
pub kind: ContractType,
pub addr: Address,
pub abi: Contract
}
impl PoaContract {
fn new(kind: ContractType, addr: Address, abi: Contract) -> Self {
PoaContract { kind, addr, abi }
}
pub fn event(&self, event: &str) -> Event {
self.abi.event(event).unwrap().clone()
}
pub fn function(&self, function: &str) -> Function {
self.abi.function(function).unwrap().clone()
}
}
impl Display for PoaContract {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "PoaContract({:?}, {:?})", self.kind, self.addr)
}
}
impl Debug for PoaContract {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self)
}
}
#[derive(Debug)]
pub struct Config {
pub network: Network,
pub endpoint: String,
pub contracts: Vec<PoaContract>,
pub start_block: StartBlock,
pub send_email_notifications: bool,
pub send_push_notifications: bool,
pub validators: Vec<Validator>,
pub avg_block_time: Duration,
pub smtp_host_domain: String,
pub smtp_username: String,
pub smtp_password: String,
pub outgoing_email: String
}
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") {
Network::Core
} else if cli.is_present("sokol") {
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()
} else {
let env_var = format!("{}_RPC_ENDPOINT", network_uppercase);
env::var(&env_var).unwrap()
};
let mut contract_types: Vec<ContractType> = vec![];
if let Some(s) = cli.value_of("monitor") {
s.split(',').for_each(|s| contract_types.push(s.into()));
}
if cli.is_present("keys") {
contract_types.push(ContractType::Keys);
}
if cli.is_present("threshold") {
contract_types.push(ContractType::Threshold);
}
if cli.is_present("proxy") {
contract_types.push(ContractType::Proxy);
}
if contract_types.is_empty() {
env::var("MONITOR_BALLOTS").unwrap().split(',')
.for_each(|s| contract_types.push(s.into()));
}
let contracts: Vec<PoaContract> = contract_types.iter()
.map(|contract_type| {
let env_var = format!(
"{}_{}_CONTRACT_ADDRESS",
network_uppercase,
contract_type.to_uppercase()
);
let hex = env::var(&env_var)
.expect(&format!("Contract address not found: {}", env_var));
let addr = Address::from_str(hex.trim_left_matches("0x")).unwrap();
let abi_path = format!("abis/{}/{}.json", network, contract_type);
let file = File::open(&abi_path)
.expect(&format!("ABI file not found: {}", abi_path));
let abi = Contract::load(&file)
.expect(&format!("Invalid ABI file: {}", abi_path));
PoaContract::new(*contract_type, addr, abi)
})
.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"))
} else {
env::var("START_BLOCK").unwrap().as_str().into()
};
let send_email_notifications = if cli.is_present("email") {
true
} else {
env::var("SEND_EMAIL_NOTIFICATIONS").unwrap().parse().unwrap()
};
let send_push_notifications = if cli.is_present("push") {
true
} else {
env::var("SEND_PUSH_NOTIFICATIONS").unwrap().parse().unwrap()
};
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())
} else {
let s = env::var("AVG_BLOCK_TIME_SECS").unwrap();
Duration::from_secs(s.parse().unwrap())
};
let smtp_host_domain = env::var("SMTP_HOST_DOMAIN").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();
Config {
network, endpoint, contracts, start_block,
send_email_notifications,
send_push_notifications,
validators, avg_block_time,
smtp_host_domain, smtp_username,
smtp_password, outgoing_email
}
}
}

32
src/logging.rs Normal file
View File

@ -0,0 +1,32 @@
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) {
info!(LOGGER, "notification"; "data" => format!("{:#?}", notif));
}
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)
);
}

48
src/main.rs Normal file
View File

@ -0,0 +1,48 @@
extern crate chrono;
extern crate clap;
extern crate dotenv;
extern crate ethabi;
extern crate ethereum_types;
extern crate hex;
extern crate jsonrpc_core;
#[macro_use] extern crate lazy_static;
extern crate lettre;
extern crate lettre_email;
extern crate reqwest;
extern crate serde;
extern crate serde_json;
#[macro_use] extern crate slog;
extern crate slog_term;
extern crate web3;
mod cli;
mod config;
mod logging;
mod notify;
mod rpc;
mod utils;
use config::Config;
use notify::Notifier;
use rpc::{BlockWindows, RpcClient};
fn main() {
let config = Config::load();
let client = RpcClient::new(&config.endpoint);
let block_windows = BlockWindows::new(&client, config.avg_block_time);
let mut notifier = Notifier::new(&config).unwrap();
for (start, stop) in block_windows {
for contract in &config.contracts {
let ballot_created_logs = client
.get_ballot_created_logs(contract, start, stop)
.unwrap();
for log in &ballot_created_logs {
let voting_data = client.get_voting_state(contract, log.ballot_id).unwrap();
let notif = notifier.build_notification(log, &voting_data);
notifier.notify_validators(&notif);
}
}
}
}

185
src/notify.rs Normal file
View File

@ -0,0 +1,185 @@
use chrono::{DateTime, Utc};
use ethereum_types::Address;
use lettre::{EmailTransport, SmtpTransport};
use lettre::smtp::{self, ConnectionReuseParameters};
use lettre::smtp::authentication::{Credentials, Mechanism};
use lettre_email::{self, Email, EmailBuilder};
use config::{Config, ContractType, Network, Validator};
use logging::{log_email_failed, log_email_sent, log_notification};
use rpc::{BallotCreatedLog, BallotType, KeyType, VotingData};
type BuildEmailResult = Result<Email, lettre_email::error::Error>;
#[derive(Debug)]
pub enum Notification {
Keys(KeysNotification),
Threshold(ThresholdNotification),
Proxy(ProxyNotification)
}
#[derive(Debug)]
pub struct KeysNotification {
pub network: Network,
pub endpoint: String,
pub block_number: u64,
pub contract_type: ContractType,
pub ballot_type: BallotType,
pub ballot_id: u64,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub memo: String,
pub affected_key: Address,
pub affected_key_type: KeyType
}
#[derive(Debug)]
pub struct ThresholdNotification {
pub network: Network,
pub endpoint: String,
pub block_number: u64,
pub contract_type: ContractType,
pub ballot_type: BallotType,
pub ballot_id: u64,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub memo: String,
pub proposed_value: u64
}
#[derive(Debug)]
pub struct ProxyNotification {
pub network: Network,
pub endpoint: String,
pub block_number: u64,
pub contract_type: ContractType,
pub ballot_type: BallotType,
pub ballot_id: u64,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub memo: String,
pub proposed_value: Address
}
impl Notification {
fn new(config: &Config, log: &BallotCreatedLog, voting_data: &VotingData) -> Self {
let network = config.network;
let endpoint = config.endpoint.clone();
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)
}
}
}
}
pub struct Notifier<'a> {
config: &'a Config,
mailer: Option<SmtpTransport>
}
impl<'a> Notifier<'a> {
pub fn new(config: &'a Config) -> Result<Self, smtp::error::Error> {
let mailer = if config.send_email_notifications {
let creds = Credentials::new(
config.smtp_username.clone(),
config.smtp_password.clone()
);
let mailer = SmtpTransport::simple_builder(&config.smtp_host_domain)?
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
.authentication_mechanism(Mechanism::Plain)
.credentials(creds)
.build();
Some(mailer)
} else {
None
};
Ok(Notifier { config, mailer })
}
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.send_push_notifications {
println!("Push Notifications not yet implemented.");
}
}
}
fn build_email(&self, validator: &Validator, notif: &Notification) -> BuildEmailResult {
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)
};
EmailBuilder::new()
.to(validator.email.as_str())
.from(self.config.outgoing_email.as_str())
.subject("POA Network Governance Notification")
.text(body)
.build()
}
}
impl<'a> Drop for Notifier<'a> {
fn drop(&mut self) {
if let Some(ref mut mailer) = self.mailer {
mailer.close();
}
}
}

494
src/rpc.rs Normal file
View File

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

18
src/utils.rs Normal file
View File

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