Added configurable logging, added V2 Core contract addresses.

This commit is contained in:
DrPeterVanNostrand 2018-10-10 14:46:20 +00:00
parent 139fe74fc5
commit 80c1303fe6
13 changed files with 526 additions and 215 deletions

1
.gitignore vendored
View File

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

View File

@ -1,7 +1,9 @@
[package]
name = "poagov"
version = "1.0.0"
license = "GPL-3.0"
authors = ["DrPeterVanNostrand <jnz@riseup.net>"]
build = "build.rs"
[dependencies]
chrono = "0.4.6"
@ -18,7 +20,6 @@ jsonrpc-core = "8.0.1"
lettre = { git = "https://github.com/lettre/lettre.git" }
lettre_email = { git = "https://github.com/lettre/lettre.git" }
native-tls = "0.2"
lazy_static = "1.1.0"
reqwest = "0.8.8"
serde_json = "1.0.27"
slog = { version = "2.3.3", features = ["release_max_level_trace"] }

196
README.md
View File

@ -1,6 +1,6 @@
[![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).
@ -8,6 +8,14 @@ A tool to monitor a POA Network blockchain for
The `poagov` command line tool is distributed as a binary for Linux and
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
*Note:* the `poagov` binary requires libssl to be installed prior to
@ -17,9 +25,10 @@ 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/v1.0.0/poagov-1.0.0-linux-x86_64.tar.gz
$ tar -xvzf poagov-1..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
@ -29,12 +38,10 @@ On OSX:
$ 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
Make sure you have an `.env` file in the same directory as the `poagov`
binary; see the section "Setting up the `.env` File" for more
information.
# Building `poagov` from Source
@ -47,12 +54,20 @@ To build the `poagov` CLI tool, run the following:
`poagov` can be built using Rust 1.29 stable and requires `libssl` to be
installed; see the following "Requires libssl" section for more information.
You can run `poagov`'s tests via the following command (make sure to copy
`sample.env` into `.env` before testing):
Building `poagov` requires Rust `1.29.0-stable` or later and `libssl`; see the
"Requires `libssl`" section for more information.
$ cargo test
### Testing
### Requires `libssl`
You can run `poagov`'s tests to ensure that it everything is working properly:
$ cargo test --release
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,
@ -60,7 +75,7 @@ otherwise known as `libssl` (you will need more than just the OpenSSL
binary that you may or may not already have installed at
`/usr/bin/openssl`).
If running `cargo build [--release]` panics with an error like:
If running `cargo build --release` panics with an error like:
"error: failed to run custom build command for `openssl-sys v0.9.28
...
@ -91,7 +106,7 @@ compilation errors for any of the Rust crates: `openssl`, `openssl-sys`, or
$ cargo clean
$ OPENSSL_INCLUDE_DIR=$(brew --prefix openssl)/include \
OPENSSL_LIB_DIR=$(brew --prefix openssl)/lib \
cargo build [--release]
cargo build --release
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
@ -106,9 +121,10 @@ More information on common issues encountered while installing the
Once you have built or downloaded `poagov`, you can print out the CLI usage by
running:
# If you downloaded the `poagov` binary run:
$ poagov --help
# If built from source run:
# $ ./target/{debug, release}/poagov --help
# Or, if you built `poagov` from source run:
$ target/release/poagov --help
poagov 1.0.0
Monitors a POA Network blockchain for governance events.
@ -117,50 +133,51 @@ running:
poagov [FLAGS] [OPTIONS]
FLAGS:
--core monitor voting contracts deployed to the Core network
--earliest begin monitoring for governance events starting at the first block in the blockchain
--email enables email notifications (SMTP configurations must be set in your `.env` file)
-e, --emission monitors the blockchain for ballots to manage emission funds
-h, --help Prints help information
-k, --keys monitors the blockchain for ballots to change keys
--latest begin monitoring for governance events starting at the last block mined
-p, --proxy monitors the blockchain for ballots to change the proxy address
--sokol monitor voting contracts deployed to the Sokol network
-t, --threshold monitors the blockchain for ballots to change the minimum threshold
--v1 monitors the v1 voting contracts
--v2 monitors the v2 voting contracts
-V, --version Prints version information
--verbose prints the full notification email's body when logging
--core monitor voting contracts deployed to the Core network
--earliest begin monitoring for governance events starting at the first block in the blockchain
--email enables email notifications (SMTP configurations must be set in your `.env` file)
-e, --emission monitors the blockchain for ballots to manage emission funds
-h, --help prints help information
-k, --keys monitors the blockchain for ballots to change keys
--latest begin monitoring for governance events starting at the last block mined
-p, --proxy monitors the blockchain for ballots to change the proxy address
--sokol monitor voting contracts deployed to the Sokol network
-t, --threshold monitors the blockchain for ballots to change the minimum threshold
--v1 monitors the v1 voting contracts
--v2 monitors the v2 voting contracts
-V, --version prints version information
--log-emails logs each notification's email body; does not require the --email flag to be set
--log-file logs are written to files in the ./logs directory, log files are rotated when they reach a size of 4MB
OPTIONS:
--block-time <value> the average number of seconds it takes to mine a new block
-n, --limit <value> shutdown `poagov` after this many notifications have been generated
--start <value> start monitoring for governance events at this block (inclusive)
--tail <value> start monitoring for governance events for the `n` blocks prior to the last block minedV
--block-time <value> the average number of seconds it takes to mine a new block
-n, --limit <value> shutdown `poagov` after this many notifications have been generated, useful when testing
--start <value> start monitoring for governance events at this block (inclusive)
--tail <value> start monitoring for governance events for the `n` blocks prior to the last block mined
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 that you want to monitor. Uou must specify one and only one of
the following arguments: `--core` or `--sokol`.
2. The hardfork version. You must specify one of the following: `--v1` or `--v2`.
- `--v1` indicates that you want to monitor for governance events prior to
the Sokol and Core hardforks that will occur in September-2018 and
November-2018 respectively.
- `--v2` indicates that you want to monitor for governance events that
occured after the above hardfork dates.
- More information regarding the planned hardforks for the Sokol and Core
chains in September and November 2018 can be found
[here](https://medium.com/poa-network/poa-network-news-and-updates-36-2e6e00550c15).
3. The ballots that you want to monitor for governance events. You must specify
one or more of the following arguments: `-k`/`--keys`, `-t`/`--threshold`,
`-p`/`--proxy`, and/or `-e`/`--emission`.
- Note that the `VotingToManageEmissionFunds.sol` contract (i.e. the
`--emission` option) is not available in `--v1`.
4. The point in the chain for where to start monitoring. You must specify one
and only one of the following: `--earliest`, `--latest`, `--start=<value>`, or
`--tail=<value>`.
1. The chain (specify only one): `--core`, `--sokol`.
2. The contracts to monitor (specify at least one): `--keys`, `--threshold`, `--proxy`, `--emission`.
2. The hardfork version (specify only one): `--v1`, `--v2`.
4. The block in the chain from where to start monitoring (specify only one): `--earliest`, `--latest`, `--start=<block_number>`, `--tail=<value>`.
### Notes on the hardfork version options `--v1` and `--v2`
`--v1` indicates that you want to monitor for governance events prior to the
Sokol and Core hardforks that will occur in September-2018 and November-2018
respectively.
`--v2` indicates that you want to monitor for governance events that occured
after the above hardfork dates.
- More information regarding the planned hardforks for the Sokol and Core
chains in September and November 2018 can be found
[here](https://medium.com/poa-network/poa-network-news-and-updates-36-2e6e00550c15).
### Optional Arguments
@ -170,24 +187,43 @@ this option, you must first configure SMTP in your `.env`.
Providing the `--block-time=<value>` will set how often `poagov` will query the
blockchain for new governance events. Defaults to 30 seconds.
Providing the `--verbose` flag will print the full text for a notification
Providing the `--log-emails` flag will print the full text for a notification
email to stderr when governance events are found. When this option is set,
email text will be logged regardless of whether or not the `--email` flag is
set.
Setting the `--log-file` flag will write logs to a file in the `./logs/`
directory. Logs are rotated chronologically across three files. Once the
`logs` directory has reached its max number of files, the oldest log file will
be deleted to make room for the next log file. Log files have a max size of
4MB; the log files will rotated once the current log file has reached the max
file size.
Setting the `--limit=<value>` option will cause `poagov` to stop once `value`
number of notifications have been generated. This option is useful when testing.
# Setting up the `.env` File
When the `poagov` CLI tool is run, the process' environment variables are
loaded via an `.env` file. Before running `poagove` copy `sample.env` into
`.env`:
loaded via an `.env` file.
$ cp 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. Before enabling email
notifications, you must add the required SMTP configuration values to your
`.env` file.
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
@ -222,31 +258,33 @@ Your SMTP configuration should look something like the following:
# An Explained Example
$ poagov --sokol --earliest -kt --email --verbose
$ 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
`EMAIL_RECIPIENTS` env-var.
- `--verbose` writes each governance notification email to stderr.
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. Logs include: governance notifications, email
successes/failures, and blocks that have been successfully monitored for
governance events. The following is an example command with its corresponding
logs:
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.
$ poagov --sokol --v1 --threshold --earliest
The following is an example command with its corresponding logs:
Sep 25 13:43:16.712 INFO governance notification, block_number: 525296, ballot_id: 0, ballot: Threshold
Sep 25 13:43:16.712 INFO governance notification, block_number: 599789, ballot_id: 1, ballot: Threshold
Sep 25 13:43:16.712 INFO governance notification, block_number: 1078816, ballot_id: 2, ballot: Threshold
Sep 25 13:43:16.712 INFO finished checking blocks, block_range: Number(0)...Number(4729306)
Sep 25 13:43:46.761 INFO finished checking blocks, block_range: Number(4729307)...Number(4729312)
Sep 25 13:43:48.503 WARN recieved ctrl-c signal, gracefully shutting down...
$ 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

18
build.rs Normal file
View File

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

View File

@ -2,23 +2,27 @@
CORE_RPC_ENDPOINT=https://core.poa.network
SOKOL_RPC_ENDPOINT=https://sokol.poa.network
# Core contract addresses.
# V1: https://github.com/poanetwork/poa-chain-spec/blob/f037171b344d6138a6a7a7217ee6d2c85dbfd466/contracts.json
# 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
# V2: https://github.com/poanetwork/poa-chain-spec/blob/core/contracts.json
KEYS_CONTRACT_ADDRESS_CORE_V2=
THRESHOLD_CONTRACT_ADDRESS_CORE_V2=
PROXY_CONTRACT_ADDRESS_CORE_V2=
EMISSION_FUNDS_CONTRACT_ADDRESS_CORE_V2=
# Sokol network configuration.
# V1: https://github.com/poanetwork/poa-chain-spec/blob/821dc399471c80c6b0c6a95a2ba77d7c064eb321/contracts.json
# 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
# V2: https://github.com/poanetwork/poa-chain-spec/blob/36927101401de9dc3e21274d6f46e600d08fd1a7/contracts.json
# 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

View File

@ -26,7 +26,8 @@ impl Cli {
[email] --email 'enables email notifications (SMTP configurations must be set in your `.env` file)'
[block_time] --block-time [value] 'the average number of seconds it takes to mine a new block'
[notification_limit] -n --limit [value] 'shutdown `poagov` after this many notifications have been generated'
[verbose_logs] --verbose 'prints the full notification email's body when logging'"
[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)
}
@ -108,7 +109,11 @@ impl Cli {
self.0.value_of("notification_limit")
}
pub fn verbose_logs(&self) -> bool {
self.0.is_present("verbose_logs")
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")
}
}

View File

@ -210,20 +210,20 @@ impl RpcClient {
mod tests {
use std::env;
use dotenv::dotenv;
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() {
dotenv().expect("Missing .env file");
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!("sokol last mined block number => {:?}", res);
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`");
@ -235,7 +235,7 @@ mod tests {
#[test]
fn test_get_ballot_created_logs() {
dotenv().expect("Missing .env file");
setup();
let contract = PoaContract::read(
ContractType::Keys,
Network::Sokol,
@ -252,9 +252,31 @@ mod tests {
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() {
dotenv().expect("Missing .env file");
setup();
let contract = PoaContract::read(
ContractType::Threshold,
Network::Sokol,
@ -267,7 +289,22 @@ mod tests {
assert!(res.is_ok());
}
// TODO: write this tests once the V2 contracts are released
// #[test]
// fn test_get_ballot_info() {}
// TODO: uncomment this test once V2 ballots are created.
/*
#[test]
fn test_get_ballot_info() {
setup();
let contract = PoaContract::read(
ContractType::Emission,
Network::Core,
ContractVersion::V2
).unwrap_or_else(|e| panic!("Failed to load contract: {:?}", e));
let sokol_url = env::var("CORE_RPC_ENDPOINT").expect("Missing env-var: `SOKOL_RPC_ENDPOINT`");
let client = RpcClient::new(sokol_url);
let ballot_id
let res = client.get_ballot_info(&contract, 2.into());
println!("{:#?}", res);
assert!(res.is_ok());
}
*/
}

View File

@ -7,7 +7,6 @@ use ethabi::{Address, Contract, Event, Function};
use cli::Cli;
use error::{Error, Result};
use logger::log_no_email_recipients_configured;
use response::common::BallotType;
const DEFAULT_BLOCK_TIME_SECS: u64 = 30;
@ -178,8 +177,9 @@ pub struct Config {
pub smtp_username: Option<String>,
pub smtp_password: Option<String>,
pub outgoing_email_addr: Option<String>,
pub notification_limit: Option<u64>,
pub verbose_logs: bool,
pub notification_limit: Option<usize>,
pub log_emails: bool,
pub log_to_file: bool,
}
impl Config {
@ -281,10 +281,6 @@ impl Config {
})
.collect();
if email_notifications && email_recipients.is_empty() {
log_no_email_recipients_configured();
}
let smtp_host_domain = if email_notifications {
let host = env::var("SMTP_HOST_DOMAIN")
.map_err(|_| Error::MissingEnvVar("SMTP_HOST_DOMAIN".into()))?;
@ -335,7 +331,8 @@ impl Config {
None
};
let verbose_logs = cli.verbose_logs();
let log_emails = cli.log_emails();
let log_to_file = cli.log_to_file();
let config = Config {
network,
@ -352,7 +349,8 @@ impl Config {
smtp_password,
outgoing_email_addr,
notification_limit,
verbose_logs,
log_emails,
log_to_file,
};
Ok(config)
}
@ -362,9 +360,8 @@ impl Config {
mod tests {
use std::env;
use dotenv::dotenv;
use super::*;
use super::super::tests::setup;
use super::{ContractType, ContractVersion, PoaContract, Network};
const CONTRACT_TYPES: [ContractType; 4] = [
ContractType::Keys,
@ -376,8 +373,8 @@ mod tests {
const VERSIONS: [ContractVersion; 2] = [ContractVersion::V1, ContractVersion::V2];
#[test]
fn test_load_env_vars() {
assert!(dotenv().is_ok(), "Missing .env file");
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());
@ -386,10 +383,6 @@ mod tests {
if *contract_type == ContractType::Emission && *version == ContractVersion::V1 {
continue;
}
// TODO: remove this block once V2 is published to core.
if *network == Network::Core && *version == ContractVersion::V2 {
continue;
}
let env_var = format!(
"{}_CONTRACT_ADDRESS_{}_{:?}",
contract_type.uppercase(),
@ -404,14 +397,10 @@ mod tests {
#[test]
fn test_load_contract_abis() {
dotenv().expect("Missing .env file");
setup();
for contract_type in CONTRACT_TYPES.iter() {
for version in VERSIONS.iter() {
for network in NETWORKS.iter() {
// TODO: remove this block once V2 is published to core.
if *network == Network::Core && *version == ContractVersion::V2 {
continue;
}
let res = PoaContract::read(*contract_type, *network, *version);
if *contract_type == ContractType::Emission && *version == ContractVersion::V1 {
assert!(res.is_err());

View File

@ -1,5 +1,4 @@
use ctrlc;
use dotenv;
use jsonrpc_core;
use ethabi;
use failure;
@ -13,12 +12,10 @@ pub type Result<T> = ::std::result::Result<T, Error>;
pub enum Error {
CtrlcError(ctrlc::Error),
EmissionFundsV1ContractDoesNotExist,
EnvFileNotFound(dotenv::Error),
FailedToBuildEmail(failure::Error),
FailedToBuildRequest(reqwest::Error),
FailedToBuildTls(native_tls::Error),
FailedToParseBallotCreatedLog(String),
FailedToParseEnvFile(dotenv::Error),
FailedToParseRawLogToLog(ethabi::Error),
FailedToResolveSmtpHostDomain(lettre::smtp::error::Error),
FailedToSendEmail(lettre::smtp::error::Error),

View File

@ -1,64 +1,245 @@
use std::fs::{self, create_dir, File, read_dir, remove_file};
use std::io::stderr;
use std::path::Path;
use slog::{Drain, Logger};
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;
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!())
};
// 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));
}
}
pub fn log_ctrlc() {
warn!(LOGGER, "recieved ctrl-c signal, gracefully shutting down...");
fn read_logs_dir() -> Vec<LogFile> {
let mut log_files: Vec<LogFile> = read_dir(LOGS_DIR)
.unwrap_or_else(|e| panic!("could not read ./logs directory: {:?}", e))
.filter_map(|res| {
let path = res.ok()?.path();
let file_name = path.file_name().unwrap().to_str().unwrap();
LogFile::from_file_name(file_name).ok()
}).collect();
log_files.sort_unstable();
log_files
}
pub fn log_no_email_recipients_configured() {
warn!(LOGGER, "email notifications are enabled, but there are no email recipients");
fn rotate_log_files(log_files: &mut Vec<LogFile>) -> File {
while log_files.len() >= MAX_NUMBER_OF_LOG_FILES {
let log_file_to_remove = log_files.remove(0);
log_file_to_remove.remove_file();
}
let log_file = LogFile::now();
let file = log_file.create_file();
log_files.push(log_file);
file
}
pub fn log_reached_notification_limit(notification_limit: u64) {
warn!(
LOGGER,
"reached notification limit, gracefully shutting down...";
"limit" => notification_limit
);
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
}
pub fn log_finished_block_window(start: BlockNumber, stop: BlockNumber) {
let block_range = format!("{:?}...{:?}", start, stop);
info!(LOGGER, "finished checking blocks"; "block_range" => block_range);
enum LogLocation {
Stderr,
File(File),
}
pub fn log_notification(notif: &Notification) {
let log = notif.log();
info!(
LOGGER,
"governance notification";
"ballot" => format!("{:?}", log.ballot_type),
"ballot_id" => format!("{}", log.ballot_id),
"block_number" => format!("{}", log.block_number)
);
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!())
}
}
pub fn log_notification_verbose(notif: &Notification) {
info!(LOGGER, "governance notification\n{}", notif.email_text());
#[derive(Eq, Ord, PartialEq, PartialOrd)]
struct LogFile(DateTime<Utc>);
impl LogFile {
fn now() -> Self {
LogFile(Utc::now())
}
fn from_file_name(file_name: &str) -> Result<Self, ()> {
if let Ok(dt) = Utc.datetime_from_str(file_name, FILE_NAME_DATE_FORMAT) {
Ok(LogFile(dt))
} else {
Err(())
}
}
fn file_name(&self) -> String {
self.0.format(FILE_NAME_DATE_FORMAT).to_string()
}
fn path(&self) -> String {
format!("{}/{}", LOGS_DIR, self.file_name())
}
fn create_file(&self) -> File {
let path = self.path();
File::create(&path)
.unwrap_or_else(|_| panic!("failed to create log file: {}", path))
}
fn remove_file(&self) {
let path = self.path();
remove_file(&path)
.unwrap_or_else(|_| panic!("failed to delete log file: {}", path))
}
}
pub fn log_failed_to_build_email(e: Error) {
warn!(LOGGER, "failed to build email"; "error" => format!("{:?}", e));
pub struct Logger {
logger: slog::Logger,
log_files: Vec<LogFile>,
log_count: usize,
check_file_size_at: usize,
}
pub fn log_failed_to_send_email(recipient: &str, e: Error) {
warn!(LOGGER, "failed to send email"; "recipient" => recipient, "error" => format!("{:?}", e));
}
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()
}
pub fn log_email_sent(recipient: &str) {
info!(LOGGER, "email sent"; "to" => recipient);
fn should_rotate_log_file(&mut self) -> bool {
if self.logging_to_file() {
if self.log_count >= self.check_file_size_at {
let path = self.log_files.last().unwrap().path();
let file_size = get_file_size_in_bytes(&path);
if file_size >= MAX_LOG_FILE_SIZE_BYTES {
return true;
}
let avg_bytes_per_log = file_size / self.log_count;
let remaining_bytes = MAX_LOG_FILE_SIZE_BYTES - file_size;
let remaining_logs = remaining_bytes / avg_bytes_per_log;
self.check_file_size_at += remaining_logs;
}
}
false
}
fn rotate_log_file(&mut self) {
let new_log_file = rotate_log_files(&mut self.log_files);
self.logger = create_slog_logger(LogLocation::File(new_log_file));
self.log_count = 0;
self.check_file_size_at = INITIAL_CHECK_FILE_SIZE_AT;
}
fn increment_log_count(&mut self) {
self.log_count += 1;
if self.should_rotate_log_file() {
self.rotate_log_file();
}
}
pub fn log_starting_poagov(&mut self) {
info!(&self.logger, "starting poagov...");
self.increment_log_count();
}
pub fn log_ctrlc(&mut self) {
warn!(&self.logger, "recieved ctrl-c signal, gracefully shutting down...");
self.increment_log_count();
}
pub fn log_no_email_recipients_configured(&mut self) {
warn!(&self.logger, "email notifications are enabled, but there are no email recipients");
self.increment_log_count();
}
pub fn log_notification_email_body(&mut self, notif: &Notification) {
info!(&self.logger, "governance notification\n{}", notif.email_text());
self.increment_log_count();
}
pub fn log_notification(&mut self, notif: &Notification) {
let ballot_created_log = notif.log();
info!(
&self.logger,
"governance notification";
"ballot" => format!("{:?}", ballot_created_log.ballot_type),
"ballot_id" => format!("{}", ballot_created_log.ballot_id),
"block_number" => format!("{}", ballot_created_log.block_number)
);
self.increment_log_count();
}
pub fn log_failed_to_build_email(&mut self, e: Error) {
warn!(&self.logger, "failed to build email"; "error" => format!("{:?}", e));
self.increment_log_count();
}
pub fn log_failed_to_send_email(&mut self, recipient: &str, e: Error) {
warn!(
&self.logger,
"failed to send email";
"recipient" => recipient,
"error" => format!("{:?}", e)
);
self.increment_log_count();
}
pub fn log_email_sent(&mut self, recipient: &str) {
info!(&self.logger, "email sent"; "to" => recipient);
self.increment_log_count();
}
pub fn log_reached_notification_limit(&mut self, notification_limit: usize) {
warn!(
&self.logger,
"reached notification limit, gracefully shutting down...";
"limit" => notification_limit
);
self.increment_log_count();
}
pub fn log_finished_block_window(&mut self, start: BlockNumber, stop: BlockNumber) {
let block_range = format!("{:?}...{:?}", start, stop);
info!(&self.logger, "finished checking blocks"; "block_range" => block_range);
self.increment_log_count();
}
}

View File

@ -7,8 +7,6 @@ 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;
@ -28,7 +26,7 @@ mod logger;
mod notify;
mod response;
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicBool, Ordering};
use blockchain::BlockchainIter;
@ -36,41 +34,42 @@ use cli::Cli;
use client::RpcClient;
use config::{Config, ContractVersion};
use error::{Error, Result};
use logger::{log_ctrlc, log_finished_block_window, log_reached_notification_limit};
use logger::Logger;
use notify::{Notification, Notifier};
fn load_dotenv_file() -> Result<()> {
fn load_env_file() {
if let Err(e) = dotenv::dotenv() {
if let dotenv::Error::Io(_) = e {
Err(Error::EnvFileNotFound(e))
} else {
Err(Error::FailedToParseEnvFile(e))
}
} else {
Ok(())
match e {
dotenv::Error::Io(_) => panic!("could not find .env file"),
_ => panic!("coule not parse .env file"),
};
}
}
fn set_ctrlc_handler() -> Result<Arc<AtomicBool>> {
fn set_ctrlc_handler(logger: Arc<Mutex<Logger>>) -> Result<Arc<AtomicBool>> {
let running = Arc::new(AtomicBool::new(true));
{
let running = running.clone();
ctrlc::set_handler(move || {
log_ctrlc();
running.store(false, Ordering::SeqCst);
}).map_err(|e| Error::CtrlcError(e))?;
}
Ok(running)
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_dotenv_file()?;
load_env_file();
let cli = Cli::parse();
let config = Config::new(&cli)?;
let running = set_ctrlc_handler()?;
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)?;
let mut notification_count = 0;
let mut notifier = Notifier::new(&config, logger.clone())?;
logger.lock().unwrap().log_starting_poagov();
'main_loop: for iter_res in BlockchainIter::new(&client, &config, running)? {
let (start_block, stop_block) = iter_res?;
let mut notifications = vec![];
@ -96,15 +95,30 @@ fn main() -> Result<()> {
});
for notification in notifications.iter() {
notifier.notify(notification);
if let Some(notification_limit) = config.notification_limit {
notification_count += 1;
if notification_count >= notification_limit {
log_reached_notification_limit(notification_limit);
break 'main_loop;
}
if notifier.reached_limit() {
let limit = config.notification_limit.unwrap();
logger.lock().unwrap().log_reached_notification_limit(limit);
break 'main_loop;
}
}
log_finished_block_window(start_block, stop_block);
logger.lock().unwrap().log_finished_block_window(start_block, stop_block);
}
Ok(())
}
#[cfg(test)]
pub mod tests {
use super::load_env_file;
static mut LOADED_ENV_FILE: bool = false;
pub fn setup() {
unsafe {
if !LOADED_ENV_FILE {
load_env_file();
LOADED_ENV_FILE = true;
}
}
}
}

View File

@ -1,3 +1,5 @@
use std::sync::{Arc, Mutex};
use lettre::{SendableEmail, Transport};
use lettre::smtp::{ClientSecurity, ConnectionReuseParameters, SmtpClient, SmtpTransport};
use lettre::smtp::authentication::{Credentials, Mechanism};
@ -7,10 +9,7 @@ use native_tls::TlsConnector;
use config::Config;
use error::{Error, Result};
use logger::{
log_email_sent, log_failed_to_build_email, log_failed_to_send_email, log_notification,
log_notification_verbose,
};
use logger::Logger;
use response::common::BallotCreatedLog;
use response::v1::VotingState;
use response::v2::BallotInfo;
@ -99,10 +98,12 @@ impl<'a> Notification<'a> {
pub struct Notifier<'a> {
config: &'a Config,
emailer: Option<SmtpTransport>,
logger: Arc<Mutex<Logger>>,
notification_count: usize,
}
impl<'a> Notifier<'a> {
pub fn new(config: &'a Config) -> Result<Self> {
pub fn new(config: &'a Config, logger: Arc<Mutex<Logger>>) -> Result<Self> {
let emailer = if config.email_notifications {
let domain = config.smtp_host_domain.clone().unwrap();
let port = config.smtp_port.unwrap();
@ -126,31 +127,40 @@ impl<'a> Notifier<'a> {
} else {
None
};
Ok(Notifier { config, emailer })
Ok(Notifier { config, emailer, logger, notification_count: 0 })
}
pub fn notify(&mut self, notif: &Notification) {
if self.config.verbose_logs {
log_notification_verbose(notif);
if self.config.log_emails {
self.logger.lock().unwrap().log_notification_email_body(notif);
} else {
log_notification(notif);
self.logger.lock().unwrap().log_notification(notif);
}
if self.config.email_notifications {
for recipient in self.config.email_recipients.iter() {
let email: SendableEmail = match self.build_email(notif, recipient) {
Ok(email) => email.into(),
Err(e) => {
log_failed_to_build_email(e);
self.logger.lock().unwrap().log_failed_to_build_email(e);
continue;
},
};
if let Err(e) = self.send_email(email) {
log_failed_to_send_email(recipient, e);
self.logger.lock().unwrap().log_failed_to_send_email(recipient, e);
} else {
log_email_sent(recipient);
self.logger.lock().unwrap().log_email_sent(recipient);
}
}
}
self.notification_count += 1;
}
pub fn reached_limit(&self) -> bool {
if let Some(limit) = self.config.notification_limit {
self.notification_count >= limit
} else {
false
}
}
fn build_email(&self, notif: &Notification, recipient: &str) -> Result<Email> {

View File

@ -302,7 +302,7 @@ pub struct EmissionBallotInfo {
pub is_finalized: bool,
pub creator: Address,
pub memo: String,
pub ammount: U256,
pub amount: U256,
pub burn_votes: U256,
pub freeze_votes: U256,
pub send_votes: U256,
@ -327,7 +327,7 @@ impl From<Vec<ethabi::Token>> for EmissionBallotInfo {
let is_finalized = tokens[4].clone().to_bool().unwrap();
let creator = tokens[5].clone().to_address().unwrap();
let memo = tokens[6].clone().to_string().unwrap();
let ammount = tokens[7].clone().to_uint().unwrap();
let 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();
@ -340,7 +340,7 @@ impl From<Vec<ethabi::Token>> for EmissionBallotInfo {
is_finalized,
creator,
memo,
ammount,
amount,
burn_votes,
freeze_votes,
send_votes,
@ -355,7 +355,7 @@ impl EmissionBallotInfo {
"Creation Time: {}\n\
Voting Start Time: {}\n\
Voting End Time: {}\n\
Ammount: {}\n\
Amount: {}\n\
Burn Votes: {}\n\
Freeze Votes: {}\n\
Send Votes: {}\n\
@ -367,7 +367,7 @@ impl EmissionBallotInfo {
self.creation_time,
self.start_time,
self.end_time,
self.ammount,
convert_wei_to_poa(self.amount),
self.burn_votes,
self.freeze_votes,
self.send_votes,
@ -379,3 +379,19 @@ impl EmissionBallotInfo {
)
}
}
/// 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
}