You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
174 lines
8.2 KiB
174 lines
8.2 KiB
#!/usr/bin/python3
|
|
# keymaster.py
|
|
# Used to perform agent wallet maintenance like:
|
|
# top-up - ensures configured addresses have sufficient funds
|
|
|
|
from utils import dispatch_signed_transaction
|
|
from web3 import Web3
|
|
from prometheus_client import start_http_server, Counter, Gauge
|
|
from config import load_config
|
|
from utils import create_transaction, is_wallet_below_threshold, get_nonce, get_balance
|
|
|
|
import click
|
|
import logging
|
|
import json
|
|
import sys
|
|
import time
|
|
|
|
@click.group()
|
|
@click.option('--debug/--no-debug', default=False)
|
|
@click.option('--config-path', default="./config/keymaster.json")
|
|
@click.pass_context
|
|
def cli(ctx, debug, config_path):
|
|
ctx.ensure_object(dict)
|
|
ctx.obj['DEBUG'] = debug
|
|
|
|
conf = load_config(config_path)
|
|
|
|
if conf:
|
|
ctx.obj['CONFIG'] = conf
|
|
else:
|
|
# Failed to load config, barf
|
|
click.echo(f"Failed to load config from {config_path}, check the file and try again.")
|
|
sys.exit(1)
|
|
|
|
|
|
# Set up logging
|
|
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
|
|
|
if debug:
|
|
click.echo(f"Loaded config from {config_path}")
|
|
click.echo(json.dumps(ctx.obj['CONFIG'], indent=2))
|
|
|
|
@cli.command()
|
|
@click.pass_context
|
|
@click.option('--metrics-port', default=9090, help="Port to bind metrics server to.")
|
|
@click.option('--pause-duration', default=30, help="Number of seconds to sleep between polling.")
|
|
def monitor(ctx, metrics_port, pause_duration):
|
|
"""Simple program that polls one or more ethereum accounts and reports metrics on them."""
|
|
# Get config
|
|
config = ctx.obj["CONFIG"]
|
|
|
|
# Set up prometheus metrics
|
|
metrics = {
|
|
"wallet_balance": Gauge("ethereum_wallet_balance", "ETH Wallet Balance", ["role", "home", "address", "network"]),
|
|
"transaction_count": Gauge("ethereum_transaction_count", "ETH Wallet Balance", ["role", "home", "address", "network"]),
|
|
"block_number": Gauge("ethereum_block_height", "Block Height", ["network"])
|
|
}
|
|
|
|
# Set up logging
|
|
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
|
|
|
# run metrics endpoint
|
|
start_http_server(metrics_port)
|
|
logging.info(f"Running Prometheus endpoint on port {metrics_port}")
|
|
|
|
logging.info("Executing event loop, Ctrl+C to exit.")
|
|
# main event loop
|
|
while True:
|
|
# for each rpc
|
|
for name, network in config["networks"].items():
|
|
endpoint = network["endpoint"]
|
|
w3 = Web3(Web3.HTTPProvider(endpoint))
|
|
|
|
# Fetch block height
|
|
block_height = w3.eth.get_block_number()
|
|
metrics["block_number"].labels(network=name).set(block_height)
|
|
|
|
# for each account
|
|
for home_name, home in config["homes"].items():
|
|
for role, account in home["addresses"].items():
|
|
logging.info(f"Fetching metrics for {account} via {endpoint}")
|
|
# fetch balance
|
|
wallet_wei = get_balance(account, endpoint)
|
|
logging.info(f"Wallet Balance: {wallet_wei * 10**-18}")
|
|
# fetch tx count
|
|
tx_count = get_nonce(account, endpoint)
|
|
logging.info(f"Transaction Count: {tx_count}")
|
|
# report metrics
|
|
metrics["wallet_balance"].labels(role=role, home=home_name, address=account, network=name).set(wallet_wei)
|
|
metrics["transaction_count"].labels(role=role, home=home_name, address=account, network=name).set(tx_count)
|
|
logging.info(f"Sleeping for {pause_duration} seconds.")
|
|
time.sleep(pause_duration)
|
|
|
|
|
|
@cli.command()
|
|
@click.pass_context
|
|
def top_up(ctx):
|
|
click.echo(f"Debug is {'on' if ctx.obj['DEBUG'] else 'off'}")
|
|
config = ctx.obj["CONFIG"]
|
|
transaction_queue = {}
|
|
# Init transaction queue for each network
|
|
for network in config["networks"]:
|
|
transaction_queue[network] = []
|
|
|
|
for home in config["homes"]:
|
|
|
|
for role, address in config["homes"][home]["addresses"].items():
|
|
logging.info(f"Processing {role}-{address} on {home}")
|
|
# fetch config params
|
|
home_upper_bound = config["networks"][home]["threshold"]
|
|
# don't top up until balance has gone beneath lower bound
|
|
home_lower_bound = home_upper_bound / 3
|
|
home_endpoint = config["networks"][home]["endpoint"]
|
|
home_bank_signer = config["networks"][home]["bank"]["signer"]
|
|
home_bank_address = config["networks"][home]["bank"]["address"]
|
|
|
|
# check if balance is below threshold at home
|
|
threshold_difference = is_wallet_below_threshold(address, home_lower_bound, home_upper_bound, home_endpoint)
|
|
# get nonce
|
|
home_bank_nonce = get_nonce(home_bank_address, home_endpoint)
|
|
|
|
if threshold_difference:
|
|
logging.info(f"Threshold difference is {threshold_difference} for {role}-{address} on {home}, enqueueing transaction.")
|
|
# if so, enqueue top up with (threshold - balance) ether
|
|
transaction = create_transaction(home_bank_signer, address, threshold_difference, home_bank_nonce + len(transaction_queue[home]), home_endpoint)
|
|
transaction_queue[home].append(transaction)
|
|
else:
|
|
logging.info(f"Threshold difference is satisfactory for {role}-{address} on {home}, no action.")
|
|
|
|
for replica in config["homes"][home]["replicas"]:
|
|
# fetch config params
|
|
replica_upper_bound = config["networks"][replica]["threshold"]
|
|
# don't top up until balance has gone beneath lower bound
|
|
replica_lower_bound = replica_upper_bound / 3
|
|
replica_endpoint = config["networks"][replica]["endpoint"]
|
|
replica_bank_signer = config["networks"][replica]["bank"]["signer"]
|
|
replica_bank_address = config["networks"][replica]["bank"]["address"]
|
|
# check if balance is below threshold at replica
|
|
threshold_difference = is_wallet_below_threshold(address, replica_lower_bound, replica_upper_bound, replica_endpoint)
|
|
# get nonce
|
|
replica_bank_nonce = get_nonce(replica_bank_address, replica_endpoint)
|
|
# if so, enqueue top up with (threshold - balance) ether
|
|
if threshold_difference:
|
|
logging.info(f"Threshold difference is {threshold_difference} for {role}-{address} on {replica}, enqueueing transaction.")
|
|
transaction = create_transaction(replica_bank_signer, address, threshold_difference, replica_bank_nonce + len(transaction_queue[replica]), replica_endpoint)
|
|
transaction_queue[replica].append(transaction)
|
|
else:
|
|
logging.info(f"Threshold difference is satisfactory for {role}-{address} on {replica}, no action.")
|
|
|
|
# compute analytics about enqueued transactions
|
|
click.echo("\n Transaction Stats:")
|
|
for network in transaction_queue:
|
|
if len(transaction_queue[network]) > 0:
|
|
amount_sum = sum(tx[0]["value"] for tx in transaction_queue[network])
|
|
bank_balance = get_balance(config["networks"][network]["bank"]["address"], config["networks"][network]["endpoint"])
|
|
click.echo(f"\t {network} Bank has {Web3.fromWei(bank_balance, 'ether')} ETH")
|
|
click.echo(f"\t About to send {len(transaction_queue[network])} transactions on {network} - Total of {Web3.fromWei(amount_sum, 'ether')} ETH \n")
|
|
|
|
click.confirm("Would you like to proceed with dispatching these transactions?", abort=True)
|
|
|
|
# Process enqueued transactions
|
|
click.echo(f"Processing transactions for {network}")
|
|
for transaction_tuple in transaction_queue[network]:
|
|
click.echo(f"Attempting to send transaction: {json.dumps(transaction_tuple[0], indent=2, default=str)}")
|
|
hash = dispatch_signed_transaction(transaction_tuple[1], config["networks"][network]["endpoint"])
|
|
click.echo(f"Dispatched Transaction: {hash}")
|
|
time.sleep(3)
|
|
else:
|
|
click.echo(f"\t No transactions to process for {network}, continuing...")
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
cli(obj={}) |