rm tools/keymaster (#461)
parent
2a96c607f5
commit
5ab8e221db
@ -1,15 +0,0 @@ |
||||
FROM python:3.9-bullseye |
||||
WORKDIR /code |
||||
|
||||
# Allows docker to cache installed dependencies between builds |
||||
COPY ./requirements.txt requirements.txt |
||||
RUN pip install -r requirements.txt |
||||
|
||||
COPY keymaster.py keymaster.py |
||||
COPY __init__.py __init__.py |
||||
COPY config.py config.py |
||||
COPY utils.py utils.py |
||||
|
||||
RUN adduser --disabled-password --gecos '' unpriv |
||||
RUN chown -R unpriv: /code |
||||
CMD python3 keymaster.py monitor |
@ -1,104 +0,0 @@ |
||||
# The Keymaster |
||||
|
||||
[*I am Vinz, Vinz Clortho, Keymaster of Gozer...Volguus Zildrohoar, Lord of the Seboullia. Are you the Gatekeeper?*](https://www.youtube.com/watch?v=xSp5QwKRwqM) |
||||
|
||||
![Keymaster from Ghostbusters](https://i.pinimg.com/originals/9d/5b/a0/9d5ba02875ce7921d092038d1543b1f4.jpg) |
||||
|
||||
## Summary |
||||
The Keymaster is a tool that is used to manage funds for Optics Agent Wallets. Due to the sheer number of networks Optics supports, and the necessity for having a unique set of keys for each home, managing funds and ensuring agents can continue to function quickly becomes difficult as the network of Optics channels grows. |
||||
|
||||
Example: |
||||
|
||||
For 4 homes (alfajores, kovan, rinkeby, rinkarby) with 5 addresses each (kathy, watcher, updater, processor, relayer), this means there will be 20 unique addresses and each address has to be funded on each network resulting in 20 * 4 = 80 unique accounts across all networks which must be funded and topped up regularly. |
||||
|
||||
Generalized: num_homes^2 * num_addresses |
||||
|
||||
The Keymaster stores metadata about addresses, sources of funds, network RPC endpoints, and more to facilitate solving this problem. |
||||
|
||||
## Using The Keymaster |
||||
|
||||
Note: Before you do *anything*, [call the Ghostbusters](https://www.youtube.com/watch?v=Fe93CLbHjxQ). |
||||
|
||||
The Keymaster is a simple Python-based CLI program, the entrypoint is `keymaster.py` |
||||
|
||||
Install the requirements via pip: |
||||
|
||||
`pip3 install -r requirements.txt` |
||||
|
||||
The Keymaster can be invoked via `python3` like so: |
||||
|
||||
``` |
||||
$ python3 keymaster.py --help |
||||
Usage: keymaster.py [OPTIONS] COMMAND [ARGS]... |
||||
|
||||
Options: |
||||
--debug / --no-debug |
||||
--config-path TEXT |
||||
--help Show this message and exit. |
||||
|
||||
Commands: |
||||
top-up |
||||
``` |
||||
|
||||
Subcommands can be invoked by passing them as arguments to the CLI: |
||||
|
||||
``` |
||||
$ python3 keymaster.py top-up --help |
||||
Usage: keymaster.py top-up [OPTIONS] |
||||
|
||||
Options: |
||||
--help Show this message and exit. |
||||
``` |
||||
|
||||
## Configuration File |
||||
|
||||
The Keymaster relies on a JSON configuration file, by default located at `./keymaster.json`. You can pass a new path to the file using the `--config-path` argument. |
||||
|
||||
An example can be found at `./keymaster-example.json` and its contents are repeated here for convenience: |
||||
|
||||
``` |
||||
{ |
||||
"networks": { |
||||
"alfajores": { |
||||
"endpoint": "https://alfajores-forno.celo-testnet.org", |
||||
"bank": { |
||||
"signer": "<hexKey>", |
||||
"address": "<address>" |
||||
}, |
||||
"threshold": 500000000000000000 |
||||
}, |
||||
"kovan": { |
||||
"endpoint": "<RPCEndpoint>", |
||||
"bank": { |
||||
"signer": "<hexKey>", |
||||
"address": "<address>" |
||||
}, |
||||
"threshold": 500000000000000000 |
||||
} |
||||
}, |
||||
"homes": { |
||||
"alfajores": { |
||||
"replicas": ["kovan"], |
||||
"addresses": { |
||||
"kathy": "<address>", |
||||
"watcher": "<address>", |
||||
"updater": "<address>", |
||||
"relayer": "<address>", |
||||
"processor": "<address>" |
||||
} |
||||
}, |
||||
"kovan": { |
||||
"replicas": ["alfajores"], |
||||
"addresses": { |
||||
"kathy": "<address>", |
||||
"watcher": "<address>", |
||||
"updater": "<address>", |
||||
"relayer": "<address>", |
||||
"processor": "<address>" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
In the `top-up` command, The Keymaster will load the contents of this file and use it to dynamically query the configured accounts and determine if they need to be topped up. |
@ -1 +0,0 @@ |
||||
docker build -t gcr.io/clabs-optics/keymaster:$1 . |
@ -1,28 +0,0 @@ |
||||
from python_json_config import ConfigBuilder |
||||
import json |
||||
|
||||
def load_config(path: str): |
||||
# create config parser |
||||
builder = ConfigBuilder() |
||||
|
||||
# parse config |
||||
try: |
||||
config = builder.parse_config(path) |
||||
config.merge_with_env_variables(["KEYMASTER"]) |
||||
|
||||
# TODO: Validate config |
||||
# networks.name.rpc must be a uri |
||||
# networks.name.bank must be a hexkey |
||||
# networks.name.threshold must be in gwei |
||||
# homes.name must be present in networks |
||||
# homes.name.replicas must be present in networks |
||||
# homes.name.addresses must be unique (?) |
||||
|
||||
return config.to_dict() |
||||
except json.decoder.JSONDecodeError: |
||||
# Failed to load config |
||||
return False |
||||
# merge config with environment variables |
||||
|
||||
|
||||
|
@ -1,23 +0,0 @@ |
||||
# Patterns to ignore when building packages. |
||||
# This supports shell glob matching, relative path matching, and |
||||
# negation (prefixed with !). Only one pattern per line. |
||||
.DS_Store |
||||
# Common VCS dirs |
||||
.git/ |
||||
.gitignore |
||||
.bzr/ |
||||
.bzrignore |
||||
.hg/ |
||||
.hgignore |
||||
.svn/ |
||||
# Common backup files |
||||
*.swp |
||||
*.bak |
||||
*.tmp |
||||
*.orig |
||||
*~ |
||||
# Various IDEs |
||||
.project |
||||
.idea/ |
||||
*.tmproj |
||||
.vscode/ |
@ -1,24 +0,0 @@ |
||||
apiVersion: v2 |
||||
name: keymaster |
||||
description: A tool that queries metrics about Ethereum accounts |
||||
|
||||
# A chart can be either an 'application' or a 'library' chart. |
||||
# |
||||
# Application charts are a collection of templates that can be packaged into versioned archives |
||||
# to be deployed. |
||||
# |
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as |
||||
# a dependency of application charts to inject those utilities and functions into the rendering |
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed. |
||||
type: application |
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes |
||||
# to the chart and its templates, including the app version. |
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/) |
||||
version: 0.1.0 |
||||
|
||||
# This is the version number of the application being deployed. This version number should be |
||||
# incremented each time you make changes to the application. Versions are not expected to |
||||
# follow Semantic Versioning. They should reflect the version the application is using. |
||||
# It is recommended to use it with quotes. |
||||
appVersion: "1.16.0" |
@ -1,62 +0,0 @@ |
||||
{{/* |
||||
Expand the name of the chart. |
||||
*/}} |
||||
{{- define "keymaster.name" -}} |
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} |
||||
{{- end }} |
||||
|
||||
{{/* |
||||
Create a default fully qualified app name. |
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). |
||||
If release name contains chart name it will be used as a full name. |
||||
*/}} |
||||
{{- define "keymaster.fullname" -}} |
||||
{{- if .Values.fullnameOverride }} |
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} |
||||
{{- else }} |
||||
{{- $name := default .Chart.Name .Values.nameOverride }} |
||||
{{- if contains $name .Release.Name }} |
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }} |
||||
{{- else }} |
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} |
||||
{{- end }} |
||||
{{- end }} |
||||
{{- end }} |
||||
|
||||
{{/* |
||||
Create chart name and version as used by the chart label. |
||||
*/}} |
||||
{{- define "keymaster.chart" -}} |
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} |
||||
{{- end }} |
||||
|
||||
{{/* |
||||
Common labels |
||||
*/}} |
||||
{{- define "keymaster.labels" -}} |
||||
helm.sh/chart: {{ include "keymaster.chart" . }} |
||||
{{ include "keymaster.selectorLabels" . }} |
||||
{{- if .Chart.AppVersion }} |
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} |
||||
{{- end }} |
||||
app.kubernetes.io/managed-by: {{ .Release.Service }} |
||||
{{- end }} |
||||
|
||||
{{/* |
||||
Selector labels |
||||
*/}} |
||||
{{- define "keymaster.selectorLabels" -}} |
||||
app.kubernetes.io/name: {{ include "keymaster.name" . }} |
||||
app.kubernetes.io/instance: {{ .Release.Name }} |
||||
{{- end }} |
||||
|
||||
{{/* |
||||
Create the name of the service account to use |
||||
*/}} |
||||
{{- define "keymaster.serviceAccountName" -}} |
||||
{{- if .Values.serviceAccount.create }} |
||||
{{- default (include "keymaster.fullname" .) .Values.serviceAccount.name }} |
||||
{{- else }} |
||||
{{- default "default" .Values.serviceAccount.name }} |
||||
{{- end }} |
||||
{{- end }} |
@ -1,7 +0,0 @@ |
||||
kind: ConfigMap |
||||
apiVersion: v1 |
||||
metadata: |
||||
name: {{.Release.Name}}-config |
||||
data: |
||||
keymaster.json: | |
||||
{{.Values.keymaster.config | indent 4}} |
@ -1,40 +0,0 @@ |
||||
apiVersion: apps/v1 |
||||
kind: Deployment |
||||
metadata: |
||||
name: {{ include "keymaster.fullname" . }} |
||||
labels: |
||||
{{- include "keymaster.labels" . | nindent 4 }} |
||||
spec: |
||||
replicas: 1 |
||||
selector: |
||||
matchLabels: |
||||
{{- include "keymaster.selectorLabels" . | nindent 6 }} |
||||
template: |
||||
metadata: |
||||
{{- with .Values.podAnnotations }} |
||||
annotations: |
||||
{{- toYaml . | nindent 8 }} |
||||
{{- end }} |
||||
labels: |
||||
{{- include "keymaster.selectorLabels" . | nindent 8 }} |
||||
spec: |
||||
{{- with .Values.imagePullSecrets }} |
||||
imagePullSecrets: |
||||
{{- toYaml . | nindent 8 }} |
||||
{{- end }} |
||||
volumes: |
||||
- name: keymaster-config |
||||
configMap: |
||||
name: {{.Release.Name}}-config |
||||
containers: |
||||
- name: {{ .Chart.Name }} |
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" |
||||
imagePullPolicy: {{ .Values.image.pullPolicy }} |
||||
resources: |
||||
{{- toYaml .Values.resources | nindent 12 }} |
||||
ports: |
||||
- name: metrics |
||||
containerPort: 9090 |
||||
volumeMounts: |
||||
- name: keymaster-config |
||||
mountPath: /code/config |
@ -1,31 +0,0 @@ |
||||
replicaCount: 1 |
||||
|
||||
image: |
||||
repository: gcr.io/clabs-optics/keymaster |
||||
pullPolicy: Always |
||||
# Overrides the image tag whose default is the chart appVersion. |
||||
tag: 0.0.2 |
||||
|
||||
keymaster: |
||||
config: # Contents of JSON Config File |
||||
|
||||
imagePullSecrets: [] |
||||
nameOverride: "" |
||||
fullnameOverride: "" |
||||
|
||||
podAnnotations: |
||||
prometheus.io/scrape: 'true' |
||||
prometheus.io/port: '9090' |
||||
|
||||
resources: {} |
||||
# We usually recommend not to specify default resources and to leave this as a conscious |
||||
# choice for the user. This also increases chances charts run on environments with little |
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following |
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'. |
||||
# limits: |
||||
# cpu: 100m |
||||
# memory: 128Mi |
||||
# requests: |
||||
# cpu: 100m |
||||
# memory: 128Mi |
||||
|
@ -1,42 +0,0 @@ |
||||
{ |
||||
"networks": { |
||||
"alfajores": { |
||||
"endpoint": "https://alfajores-forno.celo-testnet.org", |
||||
"bank": { |
||||
"signer": "<hexKey>", |
||||
"address": "<address>" |
||||
}, |
||||
"threshold": 500000000000000000 |
||||
}, |
||||
"kovan": { |
||||
"endpoint": "<RPCEndpoint>", |
||||
"bank": { |
||||
"signer": "<hexKey>", |
||||
"address": "<address>" |
||||
}, |
||||
"threshold": 500000000000000000 |
||||
} |
||||
}, |
||||
"homes": { |
||||
"alfajores": { |
||||
"replicas": ["kovan"], |
||||
"addresses": { |
||||
"kathy": "<address>", |
||||
"watcher": "<address>", |
||||
"updater": "<address>", |
||||
"relayer": "<address>", |
||||
"processor": "<address>" |
||||
} |
||||
}, |
||||
"kovan": { |
||||
"replicas": ["alfajores"], |
||||
"addresses": { |
||||
"kathy": "<address>", |
||||
"watcher": "<address>", |
||||
"updater": "<address>", |
||||
"relayer": "<address>", |
||||
"processor": "<address>" |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,206 +0,0 @@ |
||||
#!/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, get_block_height |
||||
|
||||
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: |
||||
# top-up if we see a low balance |
||||
should_top_up = False |
||||
threshold = 150000000000000000 |
||||
# for each rpc |
||||
for name, network in config["networks"].items(): |
||||
endpoint = network["endpoint"] |
||||
|
||||
# Fetch block height |
||||
try: |
||||
block_height = get_block_height(endpoint) |
||||
metrics["block_number"].labels(network=name).set(block_height) |
||||
except ValueError: |
||||
continue |
||||
|
||||
# fetch bank balance |
||||
account = network["bank"]["address"] |
||||
logging.info(f"Fetching metrics for {account} via {endpoint}") |
||||
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="bank", home=name, address=account, network=name).set(wallet_wei) |
||||
metrics["transaction_count"].labels(role="bank", home=name, address=account, network=name).set(tx_count) |
||||
|
||||
|
||||
# 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}") |
||||
if wallet_wei < threshold: |
||||
logging.warn(f"BALANCE IS LOW, MARKING FOR TOP-UP {wallet_wei} < {threshold}") |
||||
should_top_up = True |
||||
# 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) |
||||
|
||||
if should_top_up: |
||||
_top_up(ctx, auto_approve=True) |
||||
|
||||
logging.info(f"Sleeping for {pause_duration} seconds.") |
||||
time.sleep(pause_duration) |
||||
|
||||
|
||||
@cli.command() |
||||
@click.pass_context |
||||
def top_up(ctx): |
||||
_top_up(ctx) |
||||
|
||||
def _top_up(ctx, auto_approve=False): |
||||
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 = 150000000000000000 |
||||
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 = 150000000000000000 |
||||
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") |
||||
|
||||
if not auto_approve: |
||||
click.confirm("Would you like to proceed with dispatching these transactions?", abort=True) |
||||
else: |
||||
# Send it!! |
||||
click.echo("Auto-Approved. Dispatching.") |
||||
|
||||
# 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={}) |
@ -1 +0,0 @@ |
||||
docker push gcr.io/clabs-optics/keymaster:$1 |
@ -1,6 +0,0 @@ |
||||
click==8.0.1 |
||||
python-json-config==1.2.3 |
||||
web3==5.22.0 |
||||
prometheus-client==0.11.0 |
||||
backoff==1.11.1 |
||||
websockets>=10.0 # not directly required, pinned by Snyk to avoid a vulnerability |
@ -1,81 +0,0 @@ |
||||
from web3 import Web3 |
||||
import backoff |
||||
|
||||
# Checks if an address is below the threshold |
||||
# returns difference in wei if true |
||||
# returns False if not |
||||
def is_wallet_below_threshold(address:str, lower_bound:int, upper_bound:int, endpoint:str): |
||||
w3 = Web3(Web3.HTTPProvider(endpoint)) |
||||
address = Web3.toChecksumAddress(address) |
||||
# get balance |
||||
wallet_wei = get_balance(address, endpoint) |
||||
# if balance below lower bound |
||||
if wallet_wei < lower_bound: |
||||
# return the amount we have to top up |
||||
# to reach upper bound |
||||
return upper_bound - wallet_wei |
||||
else: |
||||
return False |
||||
|
||||
# creates a transaction for a sender and recipient |
||||
# given a network RPC endpoint |
||||
# returns tuple (tx_params, signed_tx) for debugging |
||||
def create_transaction(sender_key:str, recipient_address:int, amount:int, nonce:int, endpoint:str): |
||||
# Set up w3 provider with network endpoint |
||||
w3 = Web3(Web3.HTTPProvider(endpoint)) |
||||
recipient_address = Web3.toChecksumAddress(recipient_address) |
||||
chain_id = w3.eth.chain_id |
||||
gas = 100000 * 100 if "arb-rinkeby" in endpoint else 100000 |
||||
# sign transaction |
||||
tx_params = dict( |
||||
nonce=nonce, |
||||
gasPrice= 500 * 10 ** 9, |
||||
gas=gas, |
||||
to=recipient_address, |
||||
value=amount, |
||||
data=b'', |
||||
chainId=chain_id, |
||||
) |
||||
signed_txn = w3.eth.account.sign_transaction(tx_params,sender_key) |
||||
return (tx_params, signed_txn) |
||||
|
||||
|
||||
# gets the current nonce for an address |
||||
@backoff.on_exception(backoff.expo, |
||||
ValueError, |
||||
max_tries=18) |
||||
def get_nonce(address:str, endpoint:str): |
||||
w3 = Web3(Web3.HTTPProvider(endpoint)) |
||||
address = Web3.toChecksumAddress(address) |
||||
nonce = w3.eth.get_transaction_count(address) |
||||
return nonce |
||||
|
||||
# gets the current nonce for an address |
||||
@backoff.on_exception(backoff.expo, |
||||
ValueError, |
||||
max_tries=8) |
||||
def get_block_height(endpoint:str): |
||||
w3 = Web3(Web3.HTTPProvider(endpoint)) |
||||
block_height = w3.eth.get_block_number() |
||||
return block_height |
||||
|
||||
@backoff.on_exception(backoff.expo, |
||||
ValueError, |
||||
max_tries=8) |
||||
def get_balance(address:str, endpoint:str): |
||||
w3 = Web3(Web3.HTTPProvider(endpoint)) |
||||
address = Web3.toChecksumAddress(address) |
||||
wallet_wei = w3.eth.get_balance(address) |
||||
return wallet_wei |
||||
|
||||
# dispatches a signed transaction from create_transaction |
||||
@backoff.on_exception(backoff.expo, |
||||
ValueError, |
||||
max_tries=8) |
||||
def dispatch_signed_transaction(signed_transaction, endpoint:str): |
||||
# Set up w3 provider with network endpoint |
||||
w3 = Web3(Web3.HTTPProvider(endpoint)) |
||||
hash = w3.eth.send_raw_transaction(signed_transaction.rawTransaction) |
||||
return hash |
||||
|
||||
|
Loading…
Reference in new issue