Added configurable logging, added V2 Core contract addresses.
This commit is contained in:
parent
139fe74fc5
commit
80c1303fe6
|
@ -2,3 +2,4 @@
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
.env
|
.env
|
||||||
|
/logs
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
[package]
|
[package]
|
||||||
name = "poagov"
|
name = "poagov"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
license = "GPL-3.0"
|
||||||
authors = ["DrPeterVanNostrand <jnz@riseup.net>"]
|
authors = ["DrPeterVanNostrand <jnz@riseup.net>"]
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = "0.4.6"
|
chrono = "0.4.6"
|
||||||
|
@ -18,7 +20,6 @@ jsonrpc-core = "8.0.1"
|
||||||
lettre = { git = "https://github.com/lettre/lettre.git" }
|
lettre = { git = "https://github.com/lettre/lettre.git" }
|
||||||
lettre_email = { git = "https://github.com/lettre/lettre.git" }
|
lettre_email = { git = "https://github.com/lettre/lettre.git" }
|
||||||
native-tls = "0.2"
|
native-tls = "0.2"
|
||||||
lazy_static = "1.1.0"
|
|
||||||
reqwest = "0.8.8"
|
reqwest = "0.8.8"
|
||||||
serde_json = "1.0.27"
|
serde_json = "1.0.27"
|
||||||
slog = { version = "2.3.3", features = ["release_max_level_trace"] }
|
slog = { version = "2.3.3", features = ["release_max_level_trace"] }
|
||||||
|
|
196
README.md
196
README.md
|
@ -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)
|
[![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
|
A tool to monitor a POA Network blockchain for
|
||||||
[governance events](https://github.com/poanetwork/wiki/wiki/Governance-Overview).
|
[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
|
The `poagov` command line tool is distributed as a binary for Linux and
|
||||||
OSX; it can also be built from source for both platforms.
|
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
|
# Installing the `poagov` Binary
|
||||||
|
|
||||||
*Note:* the `poagov` binary requires libssl to be installed prior to
|
*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:
|
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
|
$ 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
|
$ rm poagov-1.0.0-linux-x86_64.tar.gz
|
||||||
$ cd poagov
|
$ cd poagov
|
||||||
|
$ cp sample.env .env
|
||||||
$ chmod +x poagov
|
$ chmod +x poagov
|
||||||
$ ./poagov --help
|
$ ./poagov --help
|
||||||
|
|
||||||
|
@ -29,12 +38,10 @@ On OSX:
|
||||||
$ tar -xvzf poagov-1.0.0-osx-x86_64.tar.gz
|
$ tar -xvzf poagov-1.0.0-osx-x86_64.tar.gz
|
||||||
$ rm poagov-1.0.0-osx-x86_64.tar.gz
|
$ rm poagov-1.0.0-osx-x86_64.tar.gz
|
||||||
$ cd poagov
|
$ cd poagov
|
||||||
|
$ cp sample.env .env
|
||||||
$ chmod +x poagov
|
$ chmod +x poagov
|
||||||
$ ./poagov --help
|
$ ./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
|
# 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
|
`poagov` can be built using Rust 1.29 stable and requires `libssl` to be
|
||||||
installed; see the following "Requires libssl" section for more information.
|
installed; see the following "Requires libssl" section for more information.
|
||||||
|
|
||||||
You can run `poagov`'s tests via the following command (make sure to copy
|
Building `poagov` requires Rust `1.29.0-stable` or later and `libssl`; see the
|
||||||
`sample.env` into `.env` before testing):
|
"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
|
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,
|
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
|
binary that you may or may not already have installed at
|
||||||
`/usr/bin/openssl`).
|
`/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
|
"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
|
$ cargo clean
|
||||||
$ OPENSSL_INCLUDE_DIR=$(brew --prefix openssl)/include \
|
$ OPENSSL_INCLUDE_DIR=$(brew --prefix openssl)/include \
|
||||||
OPENSSL_LIB_DIR=$(brew --prefix openssl)/lib \
|
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
|
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
|
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
|
Once you have built or downloaded `poagov`, you can print out the CLI usage by
|
||||||
running:
|
running:
|
||||||
|
|
||||||
|
# If you downloaded the `poagov` binary run:
|
||||||
$ poagov --help
|
$ poagov --help
|
||||||
# If built from source run:
|
# Or, if you built `poagov` from source run:
|
||||||
# $ ./target/{debug, release}/poagov --help
|
$ target/release/poagov --help
|
||||||
|
|
||||||
poagov 1.0.0
|
poagov 1.0.0
|
||||||
Monitors a POA Network blockchain for governance events.
|
Monitors a POA Network blockchain for governance events.
|
||||||
|
@ -117,50 +133,51 @@ running:
|
||||||
poagov [FLAGS] [OPTIONS]
|
poagov [FLAGS] [OPTIONS]
|
||||||
|
|
||||||
FLAGS:
|
FLAGS:
|
||||||
--core monitor voting contracts deployed to the Core network
|
--core monitor voting contracts deployed to the Core network
|
||||||
--earliest begin monitoring for governance events starting at the first block in the blockchain
|
--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)
|
--email enables email notifications (SMTP configurations must be set in your `.env` file)
|
||||||
-e, --emission monitors the blockchain for ballots to manage emission funds
|
-e, --emission monitors the blockchain for ballots to manage emission funds
|
||||||
-h, --help Prints help information
|
-h, --help prints help information
|
||||||
-k, --keys monitors the blockchain for ballots to change keys
|
-k, --keys monitors the blockchain for ballots to change keys
|
||||||
--latest begin monitoring for governance events starting at the last block mined
|
--latest begin monitoring for governance events starting at the last block mined
|
||||||
-p, --proxy monitors the blockchain for ballots to change the proxy address
|
-p, --proxy monitors the blockchain for ballots to change the proxy address
|
||||||
--sokol monitor voting contracts deployed to the Sokol network
|
--sokol monitor voting contracts deployed to the Sokol network
|
||||||
-t, --threshold monitors the blockchain for ballots to change the minimum threshold
|
-t, --threshold monitors the blockchain for ballots to change the minimum threshold
|
||||||
--v1 monitors the v1 voting contracts
|
--v1 monitors the v1 voting contracts
|
||||||
--v2 monitors the v2 voting contracts
|
--v2 monitors the v2 voting contracts
|
||||||
-V, --version Prints version information
|
-V, --version prints version information
|
||||||
--verbose prints the full notification email's body when logging
|
--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:
|
OPTIONS:
|
||||||
--block-time <value> the average number of seconds it takes to mine a new block
|
--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
|
-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)
|
--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
|
--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
|
### Required Arguments
|
||||||
|
|
||||||
Each time you run `poagov`, four CLI arguments are required:
|
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
|
1. The chain (specify only one): `--core`, `--sokol`.
|
||||||
the following arguments: `--core` or `--sokol`.
|
2. The contracts to monitor (specify at least one): `--keys`, `--threshold`, `--proxy`, `--emission`.
|
||||||
2. The hardfork version. You must specify one of the following: `--v1` or `--v2`.
|
2. The hardfork version (specify only one): `--v1`, `--v2`.
|
||||||
- `--v1` indicates that you want to monitor for governance events prior to
|
4. The block in the chain from where to start monitoring (specify only one): `--earliest`, `--latest`, `--start=<block_number>`, `--tail=<value>`.
|
||||||
the Sokol and Core hardforks that will occur in September-2018 and
|
|
||||||
November-2018 respectively.
|
### Notes on the hardfork version options `--v1` and `--v2`
|
||||||
- `--v2` indicates that you want to monitor for governance events that
|
|
||||||
occured after the above hardfork dates.
|
`--v1` indicates that you want to monitor for governance events prior to the
|
||||||
- More information regarding the planned hardforks for the Sokol and Core
|
Sokol and Core hardforks that will occur in September-2018 and November-2018
|
||||||
chains in September and November 2018 can be found
|
respectively.
|
||||||
[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
|
`--v2` indicates that you want to monitor for governance events that occured
|
||||||
one or more of the following arguments: `-k`/`--keys`, `-t`/`--threshold`,
|
after the above hardfork dates.
|
||||||
`-p`/`--proxy`, and/or `-e`/`--emission`.
|
|
||||||
- Note that the `VotingToManageEmissionFunds.sol` contract (i.e. the
|
- More information regarding the planned hardforks for the Sokol and Core
|
||||||
`--emission` option) is not available in `--v1`.
|
chains in September and November 2018 can be found
|
||||||
4. The point in the chain for where to start monitoring. You must specify one
|
[here](https://medium.com/poa-network/poa-network-news-and-updates-36-2e6e00550c15).
|
||||||
and only one of the following: `--earliest`, `--latest`, `--start=<value>`, or
|
|
||||||
`--tail=<value>`.
|
|
||||||
|
|
||||||
### Optional Arguments
|
### 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
|
Providing the `--block-time=<value>` will set how often `poagov` will query the
|
||||||
blockchain for new governance events. Defaults to 30 seconds.
|
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 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
|
email text will be logged regardless of whether or not the `--email` flag is
|
||||||
set.
|
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
|
# Setting up the `.env` File
|
||||||
|
|
||||||
When the `poagov` CLI tool is run, the process' environment variables are
|
When the `poagov` CLI tool is run, the process' environment variables are
|
||||||
loaded via an `.env` file. Before running `poagove` copy `sample.env` into
|
loaded via an `.env` file.
|
||||||
`.env`:
|
|
||||||
|
|
||||||
$ 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
|
When building from source, the `sample.env` file will be copied into the `.env`
|
||||||
notifications, you must add the required SMTP configuration values to your
|
file. This `.env` file will contain the default configuration values required
|
||||||
`.env` file.
|
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
|
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
|
`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
|
# 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.
|
- `--sokol` monitors the Sokol chain.
|
||||||
- `--earliest` starts monitoring from the first block in the blockchain.
|
- `--v1` monitors the governance contracts deployed prior to September-2018.
|
||||||
- `-k` get notifications for ballots to change keys.
|
- `-k` monitors the `VotingToChangeKeys` contract.
|
||||||
- `-t` get notifications for ballots to change the min threshold.
|
- `-t` monitors the `VotingToChangeMinThreshold` contract.
|
||||||
- `--email` sends out email notifications to each address in the
|
- `--earliest` start monitoring from the first block in the blockchain.
|
||||||
`EMAIL_RECIPIENTS` env-var.
|
- `--email` sends out email notifications to each address in the `EMAIL_RECIPIENTS` env-var.
|
||||||
- `--verbose` writes each governance notification email to stderr.
|
- `--log-emails` for each governance notification generated, log the corresponding email body.
|
||||||
|
- `--limit=1` stop running `poagov` after one ballot notification has been generated.
|
||||||
Press [ctrl-c] to exit `poagov`.
|
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
|
|
||||||
Logs are output to stderr. Logs include: governance notifications, email
|
Logs are output to `stderr` unless the `--log-file` CLI flag is set. Events
|
||||||
successes/failures, and blocks that have been successfully monitored for
|
that are logged include: the generation of governance notifications, sending an
|
||||||
governance events. The following is an example command with its corresponding
|
email successesfully or failing to send an email, aned what range of blocks
|
||||||
logs:
|
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
|
$ poagov --sokol --v1 --threshold --earliest --limit=3
|
||||||
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
|
Oct 10 15:18:09.863 INFO starting poagov...
|
||||||
Sep 25 13:43:16.712 INFO finished checking blocks, block_range: Number(0)...Number(4729306)
|
Oct 10 15:18:10.287 INFO governance notification, block_number: 525296, ballot_id: 0, ballot: Threshold
|
||||||
Sep 25 13:43:46.761 INFO finished checking blocks, block_range: Number(4729307)...Number(4729312)
|
Oct 10 15:18:10.287 INFO governance notification, block_number: 599789, ballot_id: 1, ballot: Threshold
|
||||||
Sep 25 13:43:48.503 WARN recieved ctrl-c signal, gracefully shutting down...
|
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
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
24
sample.env
24
sample.env
|
@ -2,23 +2,27 @@
|
||||||
CORE_RPC_ENDPOINT=https://core.poa.network
|
CORE_RPC_ENDPOINT=https://core.poa.network
|
||||||
SOKOL_RPC_ENDPOINT=https://sokol.poa.network
|
SOKOL_RPC_ENDPOINT=https://sokol.poa.network
|
||||||
|
|
||||||
# Core contract addresses.
|
# Core Network V1 contract addresses:
|
||||||
# V1: https://github.com/poanetwork/poa-chain-spec/blob/f037171b344d6138a6a7a7217ee6d2c85dbfd466/contracts.json
|
# github.com/poanetwork/poa-chain-spec/blob/f037171b344d6138a6a7a7217ee6d2c85dbfd466/contracts.json
|
||||||
KEYS_CONTRACT_ADDRESS_CORE_V1=0x215794efe4b86a2fbcbf706bc9ade63663f1eae1
|
KEYS_CONTRACT_ADDRESS_CORE_V1=0x215794efe4b86a2fbcbf706bc9ade63663f1eae1
|
||||||
THRESHOLD_CONTRACT_ADDRESS_CORE_V1=0xca863b0d12193a87b5173fd51fa4aa1703fb8a32
|
THRESHOLD_CONTRACT_ADDRESS_CORE_V1=0xca863b0d12193a87b5173fd51fa4aa1703fb8a32
|
||||||
PROXY_CONTRACT_ADDRESS_CORE_V1=0x9c8a06f0197ee718cd820adeb48a88ea2a9b5c48
|
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.
|
# Core Network V2 contract addresses:
|
||||||
# V1: https://github.com/poanetwork/poa-chain-spec/blob/821dc399471c80c6b0c6a95a2ba77d7c064eb321/contracts.json
|
# 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
|
KEYS_CONTRACT_ADDRESS_SOKOL_V1=0xc40cdf254a4a35498aa84f35e9842c110729a2a0
|
||||||
THRESHOLD_CONTRACT_ADDRESS_SOKOL_V1=0x700db8ba3128087f3b23f60de4bc3179bafa467d
|
THRESHOLD_CONTRACT_ADDRESS_SOKOL_V1=0x700db8ba3128087f3b23f60de4bc3179bafa467d
|
||||||
PROXY_CONTRACT_ADDRESS_SOKOL_V1=0x0aa4a75549757a90f62f88b3b96b69bead2db0ff
|
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
|
KEYS_CONTRACT_ADDRESS_SOKOL_V2=0xb974df531c1b27324618175b442edf95f7f7a621
|
||||||
THRESHOLD_CONTRACT_ADDRESS_SOKOL_V2=0xd75ad6e3840a18dacc67bf3cd2080b24be409f79
|
THRESHOLD_CONTRACT_ADDRESS_SOKOL_V2=0xd75ad6e3840a18dacc67bf3cd2080b24be409f79
|
||||||
PROXY_CONTRACT_ADDRESS_SOKOL_V2=0x604cdc518f3eb0446e15fc05a22923c82d8a8e21
|
PROXY_CONTRACT_ADDRESS_SOKOL_V2=0x604cdc518f3eb0446e15fc05a22923c82d8a8e21
|
||||||
|
|
11
src/cli.rs
11
src/cli.rs
|
@ -26,7 +26,8 @@ impl Cli {
|
||||||
[email] --email 'enables email notifications (SMTP configurations must be set in your `.env` file)'
|
[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'
|
[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'
|
[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();
|
).get_matches();
|
||||||
Cli(cli_args)
|
Cli(cli_args)
|
||||||
}
|
}
|
||||||
|
@ -108,7 +109,11 @@ impl Cli {
|
||||||
self.0.value_of("notification_limit")
|
self.0.value_of("notification_limit")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verbose_logs(&self) -> bool {
|
pub fn log_emails(&self) -> bool {
|
||||||
self.0.is_present("verbose_logs")
|
self.0.is_present("log_emails")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_to_file(&self) -> bool {
|
||||||
|
self.0.is_present("log_to_file")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -210,20 +210,20 @@ impl RpcClient {
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use dotenv::dotenv;
|
|
||||||
use web3::types::BlockNumber;
|
use web3::types::BlockNumber;
|
||||||
|
|
||||||
|
use super::super::tests::setup;
|
||||||
use super::RpcClient;
|
use super::RpcClient;
|
||||||
use config::{ContractType, ContractVersion, Network, PoaContract};
|
use config::{ContractType, ContractVersion, Network, PoaContract};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_last_mined_block() {
|
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 sokol_url = env::var("SOKOL_RPC_ENDPOINT").expect("Missing env-var: `SOKOL_RPC_ENDPOINT`");
|
||||||
let client = RpcClient::new(sokol_url);
|
let client = RpcClient::new(sokol_url);
|
||||||
let res = client.get_last_mined_block_number();
|
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());
|
assert!(res.is_ok());
|
||||||
|
|
||||||
let core_url = env::var("CORE_RPC_ENDPOINT").expect("Missing env-var: `CORE_RPC_ENDPOINT`");
|
let core_url = env::var("CORE_RPC_ENDPOINT").expect("Missing env-var: `CORE_RPC_ENDPOINT`");
|
||||||
|
@ -235,7 +235,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_ballot_created_logs() {
|
fn test_get_ballot_created_logs() {
|
||||||
dotenv().expect("Missing .env file");
|
setup();
|
||||||
let contract = PoaContract::read(
|
let contract = PoaContract::read(
|
||||||
ContractType::Keys,
|
ContractType::Keys,
|
||||||
Network::Sokol,
|
Network::Sokol,
|
||||||
|
@ -252,9 +252,31 @@ mod tests {
|
||||||
assert!(res.is_ok());
|
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]
|
#[test]
|
||||||
fn test_get_voting_state() {
|
fn test_get_voting_state() {
|
||||||
dotenv().expect("Missing .env file");
|
setup();
|
||||||
let contract = PoaContract::read(
|
let contract = PoaContract::read(
|
||||||
ContractType::Threshold,
|
ContractType::Threshold,
|
||||||
Network::Sokol,
|
Network::Sokol,
|
||||||
|
@ -267,7 +289,22 @@ mod tests {
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: write this tests once the V2 contracts are released
|
// TODO: uncomment this test once V2 ballots are created.
|
||||||
// #[test]
|
/*
|
||||||
// fn test_get_ballot_info() {}
|
#[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());
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ use ethabi::{Address, Contract, Event, Function};
|
||||||
|
|
||||||
use cli::Cli;
|
use cli::Cli;
|
||||||
use error::{Error, Result};
|
use error::{Error, Result};
|
||||||
use logger::log_no_email_recipients_configured;
|
|
||||||
use response::common::BallotType;
|
use response::common::BallotType;
|
||||||
|
|
||||||
const DEFAULT_BLOCK_TIME_SECS: u64 = 30;
|
const DEFAULT_BLOCK_TIME_SECS: u64 = 30;
|
||||||
|
@ -178,8 +177,9 @@ pub struct Config {
|
||||||
pub smtp_username: Option<String>,
|
pub smtp_username: Option<String>,
|
||||||
pub smtp_password: Option<String>,
|
pub smtp_password: Option<String>,
|
||||||
pub outgoing_email_addr: Option<String>,
|
pub outgoing_email_addr: Option<String>,
|
||||||
pub notification_limit: Option<u64>,
|
pub notification_limit: Option<usize>,
|
||||||
pub verbose_logs: bool,
|
pub log_emails: bool,
|
||||||
|
pub log_to_file: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
@ -281,10 +281,6 @@ impl Config {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if email_notifications && email_recipients.is_empty() {
|
|
||||||
log_no_email_recipients_configured();
|
|
||||||
}
|
|
||||||
|
|
||||||
let smtp_host_domain = if email_notifications {
|
let smtp_host_domain = if email_notifications {
|
||||||
let host = env::var("SMTP_HOST_DOMAIN")
|
let host = env::var("SMTP_HOST_DOMAIN")
|
||||||
.map_err(|_| Error::MissingEnvVar("SMTP_HOST_DOMAIN".into()))?;
|
.map_err(|_| Error::MissingEnvVar("SMTP_HOST_DOMAIN".into()))?;
|
||||||
|
@ -335,7 +331,8 @@ impl Config {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let verbose_logs = cli.verbose_logs();
|
let log_emails = cli.log_emails();
|
||||||
|
let log_to_file = cli.log_to_file();
|
||||||
|
|
||||||
let config = Config {
|
let config = Config {
|
||||||
network,
|
network,
|
||||||
|
@ -352,7 +349,8 @@ impl Config {
|
||||||
smtp_password,
|
smtp_password,
|
||||||
outgoing_email_addr,
|
outgoing_email_addr,
|
||||||
notification_limit,
|
notification_limit,
|
||||||
verbose_logs,
|
log_emails,
|
||||||
|
log_to_file,
|
||||||
};
|
};
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
@ -362,9 +360,8 @@ impl Config {
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use dotenv::dotenv;
|
use super::super::tests::setup;
|
||||||
|
use super::{ContractType, ContractVersion, PoaContract, Network};
|
||||||
use super::*;
|
|
||||||
|
|
||||||
const CONTRACT_TYPES: [ContractType; 4] = [
|
const CONTRACT_TYPES: [ContractType; 4] = [
|
||||||
ContractType::Keys,
|
ContractType::Keys,
|
||||||
|
@ -376,8 +373,8 @@ mod tests {
|
||||||
const VERSIONS: [ContractVersion; 2] = [ContractVersion::V1, ContractVersion::V2];
|
const VERSIONS: [ContractVersion; 2] = [ContractVersion::V1, ContractVersion::V2];
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_load_env_vars() {
|
fn test_env_file_integrity() {
|
||||||
assert!(dotenv().is_ok(), "Missing .env file");
|
setup();
|
||||||
for network in NETWORKS.iter() {
|
for network in NETWORKS.iter() {
|
||||||
let env_var = format!("{}_RPC_ENDPOINT", network.uppercase());
|
let env_var = format!("{}_RPC_ENDPOINT", network.uppercase());
|
||||||
assert!(env::var(&env_var).is_ok());
|
assert!(env::var(&env_var).is_ok());
|
||||||
|
@ -386,10 +383,6 @@ mod tests {
|
||||||
if *contract_type == ContractType::Emission && *version == ContractVersion::V1 {
|
if *contract_type == ContractType::Emission && *version == ContractVersion::V1 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// TODO: remove this block once V2 is published to core.
|
|
||||||
if *network == Network::Core && *version == ContractVersion::V2 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let env_var = format!(
|
let env_var = format!(
|
||||||
"{}_CONTRACT_ADDRESS_{}_{:?}",
|
"{}_CONTRACT_ADDRESS_{}_{:?}",
|
||||||
contract_type.uppercase(),
|
contract_type.uppercase(),
|
||||||
|
@ -404,14 +397,10 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_load_contract_abis() {
|
fn test_load_contract_abis() {
|
||||||
dotenv().expect("Missing .env file");
|
setup();
|
||||||
for contract_type in CONTRACT_TYPES.iter() {
|
for contract_type in CONTRACT_TYPES.iter() {
|
||||||
for version in VERSIONS.iter() {
|
for version in VERSIONS.iter() {
|
||||||
for network in NETWORKS.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);
|
let res = PoaContract::read(*contract_type, *network, *version);
|
||||||
if *contract_type == ContractType::Emission && *version == ContractVersion::V1 {
|
if *contract_type == ContractType::Emission && *version == ContractVersion::V1 {
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use ctrlc;
|
use ctrlc;
|
||||||
use dotenv;
|
|
||||||
use jsonrpc_core;
|
use jsonrpc_core;
|
||||||
use ethabi;
|
use ethabi;
|
||||||
use failure;
|
use failure;
|
||||||
|
@ -13,12 +12,10 @@ pub type Result<T> = ::std::result::Result<T, Error>;
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
CtrlcError(ctrlc::Error),
|
CtrlcError(ctrlc::Error),
|
||||||
EmissionFundsV1ContractDoesNotExist,
|
EmissionFundsV1ContractDoesNotExist,
|
||||||
EnvFileNotFound(dotenv::Error),
|
|
||||||
FailedToBuildEmail(failure::Error),
|
FailedToBuildEmail(failure::Error),
|
||||||
FailedToBuildRequest(reqwest::Error),
|
FailedToBuildRequest(reqwest::Error),
|
||||||
FailedToBuildTls(native_tls::Error),
|
FailedToBuildTls(native_tls::Error),
|
||||||
FailedToParseBallotCreatedLog(String),
|
FailedToParseBallotCreatedLog(String),
|
||||||
FailedToParseEnvFile(dotenv::Error),
|
|
||||||
FailedToParseRawLogToLog(ethabi::Error),
|
FailedToParseRawLogToLog(ethabi::Error),
|
||||||
FailedToResolveSmtpHostDomain(lettre::smtp::error::Error),
|
FailedToResolveSmtpHostDomain(lettre::smtp::error::Error),
|
||||||
FailedToSendEmail(lettre::smtp::error::Error),
|
FailedToSendEmail(lettre::smtp::error::Error),
|
||||||
|
|
257
src/logger.rs
257
src/logger.rs
|
@ -1,64 +1,245 @@
|
||||||
|
use std::fs::{self, create_dir, File, read_dir, remove_file};
|
||||||
use std::io::stderr;
|
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 slog_term::{FullFormat, PlainSyncDecorator};
|
||||||
use web3::types::BlockNumber;
|
use web3::types::BlockNumber;
|
||||||
|
|
||||||
|
use config::Config;
|
||||||
use error::Error;
|
use error::Error;
|
||||||
use notify::Notification;
|
use notify::Notification;
|
||||||
|
|
||||||
lazy_static! {
|
// The date format used to name log files; e.g. "Oct-08-2018-14:09:00".
|
||||||
pub static ref LOGGER: Logger = {
|
const FILE_NAME_DATE_FORMAT: &str = "%b-%d-%Y-%H:%M:%S";
|
||||||
let log_decorator = PlainSyncDecorator::new(stderr());
|
// The directory (relative to Cargo.toml) to store logs.
|
||||||
let drain = FullFormat::new(log_decorator).build().fuse();
|
const LOGS_DIR: &str = "logs";
|
||||||
Logger::root(drain, o!())
|
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() {
|
fn read_logs_dir() -> Vec<LogFile> {
|
||||||
warn!(LOGGER, "recieved ctrl-c signal, gracefully shutting down...");
|
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() {
|
fn rotate_log_files(log_files: &mut Vec<LogFile>) -> File {
|
||||||
warn!(LOGGER, "email notifications are enabled, but there are no email recipients");
|
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) {
|
fn get_file_size_in_bytes(path: &str) -> usize {
|
||||||
warn!(
|
fs::metadata(&path)
|
||||||
LOGGER,
|
.unwrap_or_else(|_| panic!("log file does not exist: {}", path))
|
||||||
"reached notification limit, gracefully shutting down...";
|
.len() as usize
|
||||||
"limit" => notification_limit
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn log_finished_block_window(start: BlockNumber, stop: BlockNumber) {
|
enum LogLocation {
|
||||||
let block_range = format!("{:?}...{:?}", start, stop);
|
Stderr,
|
||||||
info!(LOGGER, "finished checking blocks"; "block_range" => block_range);
|
File(File),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn log_notification(notif: &Notification) {
|
fn create_slog_logger(log_location: LogLocation) -> slog::Logger {
|
||||||
let log = notif.log();
|
if let LogLocation::File(file) = log_location {
|
||||||
info!(
|
let decorator = PlainSyncDecorator::new(file);
|
||||||
LOGGER,
|
let drain = FullFormat::new(decorator).build().fuse();
|
||||||
"governance notification";
|
slog::Logger::root(drain, o!())
|
||||||
"ballot" => format!("{:?}", log.ballot_type),
|
} else {
|
||||||
"ballot_id" => format!("{}", log.ballot_id),
|
let decorator = PlainSyncDecorator::new(stderr());
|
||||||
"block_number" => format!("{}", log.block_number)
|
let drain = FullFormat::new(decorator).build().fuse();
|
||||||
);
|
slog::Logger::root(drain, o!())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn log_notification_verbose(notif: &Notification) {
|
#[derive(Eq, Ord, PartialEq, PartialOrd)]
|
||||||
info!(LOGGER, "governance notification\n{}", notif.email_text());
|
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) {
|
pub struct Logger {
|
||||||
warn!(LOGGER, "failed to build email"; "error" => format!("{:?}", e));
|
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) {
|
impl Logger {
|
||||||
warn!(LOGGER, "failed to send email"; "recipient" => recipient, "error" => format!("{:?}", e));
|
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) {
|
fn should_rotate_log_file(&mut self) -> bool {
|
||||||
info!(LOGGER, "email sent"; "to" => recipient);
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
78
src/main.rs
78
src/main.rs
|
@ -7,8 +7,6 @@ extern crate ethereum_types;
|
||||||
extern crate failure;
|
extern crate failure;
|
||||||
extern crate hex;
|
extern crate hex;
|
||||||
extern crate jsonrpc_core;
|
extern crate jsonrpc_core;
|
||||||
#[macro_use]
|
|
||||||
extern crate lazy_static;
|
|
||||||
extern crate lettre;
|
extern crate lettre;
|
||||||
extern crate lettre_email;
|
extern crate lettre_email;
|
||||||
extern crate native_tls;
|
extern crate native_tls;
|
||||||
|
@ -28,7 +26,7 @@ mod logger;
|
||||||
mod notify;
|
mod notify;
|
||||||
mod response;
|
mod response;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
use blockchain::BlockchainIter;
|
use blockchain::BlockchainIter;
|
||||||
|
@ -36,41 +34,42 @@ use cli::Cli;
|
||||||
use client::RpcClient;
|
use client::RpcClient;
|
||||||
use config::{Config, ContractVersion};
|
use config::{Config, ContractVersion};
|
||||||
use error::{Error, Result};
|
use error::{Error, Result};
|
||||||
use logger::{log_ctrlc, log_finished_block_window, log_reached_notification_limit};
|
use logger::Logger;
|
||||||
use notify::{Notification, Notifier};
|
use notify::{Notification, Notifier};
|
||||||
|
|
||||||
fn load_dotenv_file() -> Result<()> {
|
fn load_env_file() {
|
||||||
if let Err(e) = dotenv::dotenv() {
|
if let Err(e) = dotenv::dotenv() {
|
||||||
if let dotenv::Error::Io(_) = e {
|
match e {
|
||||||
Err(Error::EnvFileNotFound(e))
|
dotenv::Error::Io(_) => panic!("could not find .env file"),
|
||||||
} else {
|
_ => panic!("coule not parse .env file"),
|
||||||
Err(Error::FailedToParseEnvFile(e))
|
};
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = Arc::new(AtomicBool::new(true));
|
||||||
{
|
let result = Ok(running.clone());
|
||||||
let running = running.clone();
|
ctrlc::set_handler(move || {
|
||||||
ctrlc::set_handler(move || {
|
logger.lock().unwrap().log_ctrlc();
|
||||||
log_ctrlc();
|
running.store(false, Ordering::SeqCst);
|
||||||
running.store(false, Ordering::SeqCst);
|
}).map_err(|e| Error::CtrlcError(e))?;
|
||||||
}).map_err(|e| Error::CtrlcError(e))?;
|
result
|
||||||
}
|
|
||||||
Ok(running)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
load_dotenv_file()?;
|
load_env_file();
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let config = Config::new(&cli)?;
|
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 client = RpcClient::new(config.endpoint.clone());
|
||||||
let mut notifier = Notifier::new(&config)?;
|
let mut notifier = Notifier::new(&config, logger.clone())?;
|
||||||
let mut notification_count = 0;
|
logger.lock().unwrap().log_starting_poagov();
|
||||||
|
|
||||||
'main_loop: for iter_res in BlockchainIter::new(&client, &config, running)? {
|
'main_loop: for iter_res in BlockchainIter::new(&client, &config, running)? {
|
||||||
let (start_block, stop_block) = iter_res?;
|
let (start_block, stop_block) = iter_res?;
|
||||||
let mut notifications = vec![];
|
let mut notifications = vec![];
|
||||||
|
@ -96,15 +95,30 @@ fn main() -> Result<()> {
|
||||||
});
|
});
|
||||||
for notification in notifications.iter() {
|
for notification in notifications.iter() {
|
||||||
notifier.notify(notification);
|
notifier.notify(notification);
|
||||||
if let Some(notification_limit) = config.notification_limit {
|
if notifier.reached_limit() {
|
||||||
notification_count += 1;
|
let limit = config.notification_limit.unwrap();
|
||||||
if notification_count >= notification_limit {
|
logger.lock().unwrap().log_reached_notification_limit(limit);
|
||||||
log_reached_notification_limit(notification_limit);
|
break 'main_loop;
|
||||||
break 'main_loop;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log_finished_block_window(start_block, stop_block);
|
logger.lock().unwrap().log_finished_block_window(start_block, stop_block);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use lettre::{SendableEmail, Transport};
|
use lettre::{SendableEmail, Transport};
|
||||||
use lettre::smtp::{ClientSecurity, ConnectionReuseParameters, SmtpClient, SmtpTransport};
|
use lettre::smtp::{ClientSecurity, ConnectionReuseParameters, SmtpClient, SmtpTransport};
|
||||||
use lettre::smtp::authentication::{Credentials, Mechanism};
|
use lettre::smtp::authentication::{Credentials, Mechanism};
|
||||||
|
@ -7,10 +9,7 @@ use native_tls::TlsConnector;
|
||||||
|
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use error::{Error, Result};
|
use error::{Error, Result};
|
||||||
use logger::{
|
use logger::Logger;
|
||||||
log_email_sent, log_failed_to_build_email, log_failed_to_send_email, log_notification,
|
|
||||||
log_notification_verbose,
|
|
||||||
};
|
|
||||||
use response::common::BallotCreatedLog;
|
use response::common::BallotCreatedLog;
|
||||||
use response::v1::VotingState;
|
use response::v1::VotingState;
|
||||||
use response::v2::BallotInfo;
|
use response::v2::BallotInfo;
|
||||||
|
@ -99,10 +98,12 @@ impl<'a> Notification<'a> {
|
||||||
pub struct Notifier<'a> {
|
pub struct Notifier<'a> {
|
||||||
config: &'a Config,
|
config: &'a Config,
|
||||||
emailer: Option<SmtpTransport>,
|
emailer: Option<SmtpTransport>,
|
||||||
|
logger: Arc<Mutex<Logger>>,
|
||||||
|
notification_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Notifier<'a> {
|
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 emailer = if config.email_notifications {
|
||||||
let domain = config.smtp_host_domain.clone().unwrap();
|
let domain = config.smtp_host_domain.clone().unwrap();
|
||||||
let port = config.smtp_port.unwrap();
|
let port = config.smtp_port.unwrap();
|
||||||
|
@ -126,31 +127,40 @@ impl<'a> Notifier<'a> {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
Ok(Notifier { config, emailer })
|
Ok(Notifier { config, emailer, logger, notification_count: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn notify(&mut self, notif: &Notification) {
|
pub fn notify(&mut self, notif: &Notification) {
|
||||||
if self.config.verbose_logs {
|
if self.config.log_emails {
|
||||||
log_notification_verbose(notif);
|
self.logger.lock().unwrap().log_notification_email_body(notif);
|
||||||
} else {
|
} else {
|
||||||
log_notification(notif);
|
self.logger.lock().unwrap().log_notification(notif);
|
||||||
}
|
}
|
||||||
if self.config.email_notifications {
|
if self.config.email_notifications {
|
||||||
for recipient in self.config.email_recipients.iter() {
|
for recipient in self.config.email_recipients.iter() {
|
||||||
let email: SendableEmail = match self.build_email(notif, recipient) {
|
let email: SendableEmail = match self.build_email(notif, recipient) {
|
||||||
Ok(email) => email.into(),
|
Ok(email) => email.into(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log_failed_to_build_email(e);
|
self.logger.lock().unwrap().log_failed_to_build_email(e);
|
||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if let Err(e) = self.send_email(email) {
|
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 {
|
} 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> {
|
fn build_email(&self, notif: &Notification, recipient: &str) -> Result<Email> {
|
||||||
|
|
|
@ -302,7 +302,7 @@ pub struct EmissionBallotInfo {
|
||||||
pub is_finalized: bool,
|
pub is_finalized: bool,
|
||||||
pub creator: Address,
|
pub creator: Address,
|
||||||
pub memo: String,
|
pub memo: String,
|
||||||
pub ammount: U256,
|
pub amount: U256,
|
||||||
pub burn_votes: U256,
|
pub burn_votes: U256,
|
||||||
pub freeze_votes: U256,
|
pub freeze_votes: U256,
|
||||||
pub send_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 is_finalized = tokens[4].clone().to_bool().unwrap();
|
||||||
let creator = tokens[5].clone().to_address().unwrap();
|
let creator = tokens[5].clone().to_address().unwrap();
|
||||||
let memo = tokens[6].clone().to_string().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 burn_votes = tokens[8].clone().to_uint().unwrap();
|
||||||
let freeze_votes = tokens[9].clone().to_uint().unwrap();
|
let freeze_votes = tokens[9].clone().to_uint().unwrap();
|
||||||
let send_votes = tokens[10].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,
|
is_finalized,
|
||||||
creator,
|
creator,
|
||||||
memo,
|
memo,
|
||||||
ammount,
|
amount,
|
||||||
burn_votes,
|
burn_votes,
|
||||||
freeze_votes,
|
freeze_votes,
|
||||||
send_votes,
|
send_votes,
|
||||||
|
@ -355,7 +355,7 @@ impl EmissionBallotInfo {
|
||||||
"Creation Time: {}\n\
|
"Creation Time: {}\n\
|
||||||
Voting Start Time: {}\n\
|
Voting Start Time: {}\n\
|
||||||
Voting End Time: {}\n\
|
Voting End Time: {}\n\
|
||||||
Ammount: {}\n\
|
Amount: {}\n\
|
||||||
Burn Votes: {}\n\
|
Burn Votes: {}\n\
|
||||||
Freeze Votes: {}\n\
|
Freeze Votes: {}\n\
|
||||||
Send Votes: {}\n\
|
Send Votes: {}\n\
|
||||||
|
@ -367,7 +367,7 @@ impl EmissionBallotInfo {
|
||||||
self.creation_time,
|
self.creation_time,
|
||||||
self.start_time,
|
self.start_time,
|
||||||
self.end_time,
|
self.end_time,
|
||||||
self.ammount,
|
convert_wei_to_poa(self.amount),
|
||||||
self.burn_votes,
|
self.burn_votes,
|
||||||
self.freeze_votes,
|
self.freeze_votes,
|
||||||
self.send_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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue