From 34d2fa5de7c4edf7f73cdc96e8b3ea87b1e40a05 Mon Sep 17 00:00:00 2001 From: DAMON CIARELLI Date: Thu, 26 May 2022 20:15:57 -0700 Subject: [PATCH] defund leftover ETH from nodes --- nucypher_ops/cli/nodes.py | 4 +-- nucypher_ops/cli/ursula.py | 19 ++++++++++++-- nucypher_ops/ops/fleet_ops.py | 48 ++++++++++++++++++++++++++++++++++- 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/nucypher_ops/cli/nodes.py b/nucypher_ops/cli/nodes.py index 43f500f..7fa1270 100644 --- a/nucypher_ops/cli/nodes.py +++ b/nucypher_ops/cli/nodes.py @@ -134,7 +134,7 @@ def list(network, namespace, all, as_json): @click.option('--cloudprovider', help="aws or digitalocean") @click.option('--namespace', help="Namespace for these operations. Used to address hosts and data locally and name hosts on cloud platforms.", type=click.STRING, default=DEFAULT_NAMESPACE) @click.option('--network', help="The Nucypher network name these hosts will run on.", type=click.STRING, default=DEFAULT_NETWORK) -@click.option('--include-host', 'include_hosts', help="destroy only the named hosts", multiple=True, type=click.STRING) +@click.option('--include-host', 'include_hosts', help="Peform this operation on only the named hosts", multiple=True, type=click.STRING) def destroy(cloudprovider, namespace, network, include_hosts): """Cleans up all previously created resources for the given network for the same cloud provider""" @@ -163,7 +163,7 @@ def destroy(cloudprovider, namespace, network, include_hosts): @cli.command('remove') @click.option('--namespace', help="Namespace for these operations. Used to address hosts and data locally and name hosts on cloud platforms.", type=click.STRING, default=DEFAULT_NAMESPACE) @click.option('--network', help="The Nucypher network name these hosts will run on.", type=click.STRING, default=DEFAULT_NETWORK) -@click.option('--include-host', 'include_hosts', help="destroy only the named hosts", multiple=True, type=click.STRING) +@click.option('--include-host', 'include_hosts', help="Peform this operation on only the named hosts", multiple=True, type=click.STRING) def remove(namespace, network, include_hosts): """Removes managed resources for the given network/namespace""" diff --git a/nucypher_ops/cli/ursula.py b/nucypher_ops/cli/ursula.py index 0de39ff..6fe597b 100644 --- a/nucypher_ops/cli/ursula.py +++ b/nucypher_ops/cli/ursula.py @@ -83,7 +83,7 @@ def deploy(nucypher_image, namespace, network, include_hosts, envvars, cliargs): @click.option('--fast', help="Only call blockchain and http methods, skip ssh into each node", default=None, is_flag=True) @click.option('--namespace', help="Namespace for these operations. Used to address hosts and data locally and name hosts on cloud platforms.", type=click.STRING, default=DEFAULT_NAMESPACE) @click.option('--network', help="The Nucypher network name these hosts will run on.", type=click.STRING, default=DEFAULT_NETWORK) -@click.option('--include-host', 'include_hosts', help="Query status on only the named hosts", multiple=True, type=click.STRING) +@click.option('--include-host', 'include_hosts', help="Peform this operation on only the named hosts", multiple=True, type=click.STRING) def status(fast, namespace, network, include_hosts): """Displays ursula status and updates worker data in stakeholder config""" @@ -100,7 +100,7 @@ def status(fast, namespace, network, include_hosts): @click.option('--amount', help="The amount to fund each node. Default is .003", type=click.FLOAT, default=.003) @click.option('--namespace', help="Namespace for these operations. Used to address hosts and data locally and name hosts on cloud platforms.", type=click.STRING, default=DEFAULT_NAMESPACE) @click.option('--network', help="The Nucypher network name these hosts will run on.", type=click.STRING, default=DEFAULT_NETWORK) -@click.option('--include-host', 'include_hosts', help="Query status on only the named hosts", multiple=True, type=click.STRING) +@click.option('--include-host', 'include_hosts', help="Peform this operation on only the named hosts", multiple=True, type=click.STRING) def fund(amount, namespace, network, include_hosts): """ fund remote nodes autmoatically using a locally managed burner wallet @@ -139,3 +139,18 @@ def fund(amount, namespace, network, include_hosts): deployer.fund_nodes(wallet, hostnames, amount) +@cli.command('defund') +@click.option('--amount', help="The amount to defund. Default is the entire balance of the node's wallet.", type=click.FLOAT, default=None) +@click.option('--to-address', help="To which ETH address are you sending the proceeds?", required=True) +@click.option('--namespace', help="Namespace for these operations. Used to address hosts and data locally and name hosts on cloud platforms.", type=click.STRING, default=DEFAULT_NAMESPACE) +@click.option('--network', help="The Nucypher network name these hosts will run on.", type=click.STRING, default=DEFAULT_NETWORK) +@click.option('--include-host', 'include_hosts', help="Peform this operation on only the named hosts", multiple=True, type=click.STRING) +def defund(amount, to_address, namespace, network, include_hosts): + + deployer = CloudDeployers.get_deployer('generic')(emitter, namespace=namespace, network=network) + + hostnames = deployer.config['instances'].keys() + if include_hosts: + hostnames = include_hosts + + deployer.defund_nodes(hostnames, to=to_address, amount=amount) diff --git a/nucypher_ops/ops/fleet_ops.py b/nucypher_ops/ops/fleet_ops.py index 8bb7c43..ddacbb5 100644 --- a/nucypher_ops/ops/fleet_ops.py +++ b/nucypher_ops/ops/fleet_ops.py @@ -894,6 +894,7 @@ def fund_nodes(self, web3, wallet, node_names, amount): def send_eth(self, web3, wallet, destination_address, amount_eth): transaction = { + 'chainId': self.chain_id, "nonce": web3.eth.getTransactionCount(wallet.address, 'pending'), "from": wallet.address, "to": destination_address, @@ -903,7 +904,52 @@ def send_eth(self, web3, wallet, destination_address, amount_eth): } signed_tx = wallet.sign_transaction(transaction) return web3.eth.send_raw_transaction(signed_tx.rawTransaction).hex() - + + def get_backup_path_by_nickname(self, nickname): + rootpath = os.path.join(self.config_dir, 'remote_worker_backups') + return os.path.join(rootpath,self.config['instances'][nickname]['publicaddress']) + + def get_node_config(self, nickname): + return self.config['instances'][nickname] + + @needs_provider + def defund_nodes(self, web3, hostnames, to=None, amount=None): + for hostname in hostnames: + amount_to_send = None + backuppath = self.get_backup_path_by_nickname(hostname) + nodeconfig = self.get_node_config(hostname) + for keystorepath in Path(backuppath).rglob('*UTC*'): # should only be one + ethpw = self.config['ethpassword'] + with open(keystorepath) as keyfile: + encrypted_key = keyfile.read() + private_key = web3.eth.account.decrypt(encrypted_key, ethpw) + wallet = web3.eth.account.from_key(private_key) + balance = web3.eth.get_balance(wallet.address) + if not balance: + self.emitter.echo(f'{hostname} has no ETH') + continue + if amount: + amount_to_send = web3.toWei(amount, 'ether') + else: + # we are sending all of it + needed_gas = web3.eth.gasPrice * 21000 * 2 + amount_minus_gas = balance - needed_gas + amount_to_send = amount_minus_gas + + if amount_to_send < 0: + msg = f"amount to send, including transaction gas: {web3.fromWei(max(amount_to_send, needed_gas), 'ether')} is more than total available ETH ({web3.fromWei(balance, 'ether')})" + if len(hostnames) > 1: + # keep going but notify + self.emitter.echo(msg) + continue + else: + raise AttributeError(msg) + print (amount_to_send) + self.emitter.echo(f"Attempting to send {web3.fromWei(amount_to_send, 'ether')} ETH from {hostname} to {to} in 3 seconds.") + time.sleep(3) + result = self.send_eth(wallet, to, web3.fromWei(amount_to_send, 'ether')) + self.emitter.echo(f'Broadcast transaction: {result}') + class DigitalOceanConfigurator(BaseCloudNodeConfigurator):