mirror of https://github.com/nucypher/nucypher.git
1214 lines
53 KiB
Python
Executable File
1214 lines
53 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import collections
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import random
|
|
import shutil
|
|
import ssl
|
|
import sys
|
|
from typing import Tuple, ClassVar
|
|
|
|
import click
|
|
from constant_sorrow import constants
|
|
from eth_utils import is_checksum_address
|
|
from twisted.internet import reactor
|
|
from web3.middleware import geth_poa_middleware
|
|
|
|
from nucypher.blockchain.eth.agents import MinerAgent, PolicyAgent, NucypherTokenAgent, EthereumContractAgent
|
|
from nucypher.blockchain.eth.chains import Blockchain
|
|
from nucypher.blockchain.eth.constants import (DISPATCHER_SECRET_LENGTH,
|
|
MIN_ALLOWED_LOCKED,
|
|
MIN_LOCKED_PERIODS,
|
|
MAX_MINTING_PERIODS, MAX_ALLOWED_LOCKED)
|
|
from nucypher.blockchain.eth.deployers import NucypherTokenDeployer, MinerEscrowDeployer, PolicyManagerDeployer
|
|
from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface
|
|
from nucypher.blockchain.eth.registry import TemporaryEthereumContractRegistry
|
|
from nucypher.blockchain.eth.sol.compile import SolidityCompiler
|
|
from nucypher.config.characters import UrsulaConfiguration
|
|
from nucypher.config.constants import BASE_DIR
|
|
from nucypher.config.keyring import NucypherKeyring
|
|
from nucypher.config.node import NodeConfiguration
|
|
from nucypher.utilities.sandbox.blockchain import TesterBlockchain, token_airdrop
|
|
from nucypher.utilities.sandbox.constants import (DEVELOPMENT_TOKEN_AIRDROP_AMOUNT,
|
|
DEVELOPMENT_ETH_AIRDROP_AMOUNT,
|
|
DEFAULT_SIMULATION_REGISTRY_FILEPATH)
|
|
from nucypher.utilities.sandbox.ursula import UrsulaProcessProtocol
|
|
|
|
|
|
__version__ = '0.1.0-alpha.0'
|
|
BANNER = """
|
|
_
|
|
| |
|
|
_ __ _ _ ___ _ _ _ __ | |__ ___ _ __
|
|
| '_ \| | | |/ __| | | | '_ \| '_ \ / _ \ '__|
|
|
| | | | |_| | (__| |_| | |_) | | | | __/ |
|
|
|_| |_|\__,_|\___|\__, | .__/|_| |_|\___|_|
|
|
__/ | |
|
|
|___/|_|
|
|
|
|
version {}
|
|
|
|
""".format(__version__)
|
|
|
|
|
|
def echo_version(ctx, param, value):
|
|
if not value or ctx.resilient_parsing:
|
|
return
|
|
click.secho(BANNER, bold=True)
|
|
ctx.exit()
|
|
|
|
|
|
# Setup Logging
|
|
root = logging.getLogger()
|
|
root.setLevel(logging.DEBUG)
|
|
|
|
ch = logging.StreamHandler(sys.stdout)
|
|
ch.setLevel(logging.DEBUG) # TODO: set to INFO
|
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
ch.setFormatter(formatter)
|
|
root.addHandler(ch)
|
|
|
|
|
|
#
|
|
# CLI Configuration
|
|
#
|
|
|
|
# CLI Constants
|
|
DEBUG = True
|
|
KEYRING_PASSPHRASE_ENVVAR = 'NUCYPHER_KEYRING_PASSPHRASE'
|
|
|
|
|
|
# Pending Configuration Named Tuple
|
|
fields = 'passphrase wallet signing tls skip_keys save_file'.split()
|
|
PendingConfigurationDetails = collections.namedtuple('PendingConfigurationDetails', fields)
|
|
|
|
|
|
class NucypherClickConfig:
|
|
|
|
def __init__(self):
|
|
self.log = logging.getLogger(self.__class__.__name__)
|
|
|
|
# Node Configuration
|
|
self.node_configuration = constants.NO_NODE_CONFIGURATION
|
|
self.dev = constants.NO_NODE_CONFIGURATION
|
|
self.federated_only = constants.NO_NODE_CONFIGURATION
|
|
self.config_root = constants.NO_NODE_CONFIGURATION
|
|
self.config_file = constants.NO_NODE_CONFIGURATION
|
|
|
|
# Blockchain
|
|
self.deployer = constants.NO_BLOCKCHAIN_CONNECTION
|
|
self.compile = constants.NO_BLOCKCHAIN_CONNECTION
|
|
self.poa = constants.NO_BLOCKCHAIN_CONNECTION
|
|
self.blockchain = constants.NO_BLOCKCHAIN_CONNECTION
|
|
self.provider_uri = constants.NO_BLOCKCHAIN_CONNECTION
|
|
self.registry_filepath = constants.NO_BLOCKCHAIN_CONNECTION
|
|
self.accounts = constants.NO_BLOCKCHAIN_CONNECTION
|
|
|
|
# Agency
|
|
self.token_agent = constants.NO_BLOCKCHAIN_CONNECTION
|
|
self.miner_agent = constants.NO_BLOCKCHAIN_CONNECTION
|
|
self.policy_agent = constants.NO_BLOCKCHAIN_CONNECTION
|
|
|
|
# Simulation
|
|
self.sim_processes = constants.NO_SIMULATION_RUNNING
|
|
|
|
def get_node_configuration(self, configuration_class=NodeConfiguration):
|
|
if self.config_root:
|
|
node_configuration = configuration_class(temp=False,
|
|
config_root=self.config_root,
|
|
auto_initialize=False)
|
|
elif self.dev:
|
|
node_configuration = configuration_class(temp=self.dev, auto_initialize=False, federated_only=self.federated_only)
|
|
elif self.config_file:
|
|
click.echo("Using configuration file {}".format(self.config_file))
|
|
node_configuration = configuration_class.from_configuration_file(filepath=self.config_file)
|
|
else:
|
|
node_configuration = configuration_class(federated_only=self.federated_only,
|
|
auto_initialize=False)
|
|
|
|
self.node_configuration = node_configuration
|
|
|
|
def connect_to_blockchain(self):
|
|
if self.federated_only:
|
|
raise NodeConfiguration.ConfigurationError("Cannot connect to blockchain in federated mode")
|
|
if self.deployer:
|
|
self.registry_filepath = NodeConfiguration.REGISTRY_SOURCE
|
|
if self.compile:
|
|
click.confirm("Compile solidity source?", abort=True)
|
|
self.blockchain = Blockchain.connect(provider_uri=self.provider_uri,
|
|
registry_filepath=self.registry_filepath or self.node_configuration.registry_filepath,
|
|
deployer=self.deployer,
|
|
compile=self.compile)
|
|
if self.poa:
|
|
w3 = self.blockchain.interface.w3
|
|
w3.middleware_stack.inject(geth_poa_middleware, layer=0)
|
|
self.accounts = self.blockchain.interface.w3.eth.accounts
|
|
self.log.debug("CLI established connection to provider {}".format(self.blockchain.interface.provider_uri))
|
|
|
|
def connect_to_contracts(self) -> None:
|
|
"""Initialize contract agency and set them on config"""
|
|
self.token_agent = NucypherTokenAgent(blockchain=self.blockchain)
|
|
self.miner_agent = MinerAgent(token_agent=self.token_agent)
|
|
self.policy_agent = PolicyAgent(miner_agent=self.miner_agent)
|
|
self.log.debug("CLI established connection to nucypher contracts")
|
|
|
|
def create_account(self, passphrase: str = None) -> str:
|
|
"""Creates a new local or hosted ethereum wallet"""
|
|
choice = click.prompt("Create a new Hosted or Local account?", default='hosted', type=click.STRING).strip().lower()
|
|
if choice not in ('hosted', 'local'):
|
|
click.echo("Invalid Input")
|
|
raise click.Abort()
|
|
|
|
if not passphrase:
|
|
message = "Enter a passphrase to encrypt your wallet's private key"
|
|
passphrase = click.prompt(message, hide_input=True, confirmation_prompt=True)
|
|
|
|
if choice == 'local':
|
|
keyring = NucypherKeyring.generate(passphrase=passphrase,
|
|
keyring_root=self.node_configuration.keyring_dir,
|
|
encrypting=False,
|
|
wallet=True)
|
|
new_address = keyring.checksum_address
|
|
elif choice == 'hosted':
|
|
new_address = self.blockchain.interface.w3.personal.newAccount(passphrase)
|
|
else:
|
|
raise click.BadParameter("Invalid choice; Options are hosted or local.")
|
|
return new_address
|
|
|
|
def _collect_pending_configuration_details(self, ursula: bool=False, force: bool = False) -> PendingConfigurationDetails:
|
|
|
|
# Defaults
|
|
passphrase = None
|
|
host = UrsulaConfiguration.DEFAULT_REST_HOST
|
|
skip_all_key_generation, generate_wallet = False, False
|
|
generate_encrypting_keys, generate_tls_keys, save_node_configuration_file = force, force, force
|
|
|
|
if ursula:
|
|
if not self.federated_only: # Wallet
|
|
generate_wallet = click.confirm("Do you need to generate a new wallet to use for staking?",
|
|
default=False)
|
|
if not generate_wallet: # I'll take that as a no...
|
|
self.federated_only = True # TODO: Without a wallet, let's assume this is a "federated configuration"
|
|
if not force: # TLS
|
|
generate_tls_keys = click.confirm("Do you need to generate a new TLS certificate (Ursula)?",
|
|
default=False)
|
|
if generate_tls_keys or force:
|
|
if not force:
|
|
host = click.prompt("Enter the node's hostname", default=UrsulaConfiguration.DEFAULT_REST_HOST,
|
|
type=click.STRING)
|
|
self.node_configuration.rest_host = host
|
|
if not force: # Signing / Encrypting
|
|
generate_encrypting_keys = click.confirm("Do you need to generate a new signing keypair?", default=False)
|
|
if not any((generate_wallet, generate_tls_keys, generate_encrypting_keys)):
|
|
skip_all_key_generation = click.confirm("Skip all key generation (Provide custom configuration file)?")
|
|
if not skip_all_key_generation:
|
|
if os.environ.get(KEYRING_PASSPHRASE_ENVVAR):
|
|
passphrase = os.environ.get(KEYRING_PASSPHRASE_ENVVAR)
|
|
else:
|
|
passphrase = click.prompt("Enter a passphrase to encrypt your keyring",
|
|
hide_input=True, confirmation_prompt=True)
|
|
if not force:
|
|
save_node_configuration_file = click.confirm("Generate node configuration file?")
|
|
|
|
details = PendingConfigurationDetails(passphrase=passphrase, wallet=generate_wallet,
|
|
signing=generate_encrypting_keys, tls=generate_tls_keys,
|
|
skip_keys=skip_all_key_generation, save_file=save_node_configuration_file)
|
|
return details
|
|
|
|
def create_new_configuration(self, ursula: bool=False, force: bool = False, no_registry: bool = False):
|
|
if force:
|
|
click.secho("Force is enabled - Using defaults", fg='yellow')
|
|
if self.dev:
|
|
click.secho("Using temporary storage area", fg='blue')
|
|
if not force:
|
|
click.confirm("Initialize new nucypher {} configuration?".format('ursula' if ursula else ''), abort=True)
|
|
if not no_registry and not self.federated_only:
|
|
registry_source = self.node_configuration.REGISTRY_SOURCE
|
|
if not os.path.isfile(registry_source):
|
|
click.echo("Seed contract registry does not exist at path {}. "
|
|
"Use --no-registry to skip.".format(registry_source))
|
|
raise click.Abort()
|
|
|
|
if self.config_root: # Custom installation location
|
|
self.node_configuration.config_root = self.config_root
|
|
self.node_configuration.federated_only = self.federated_only
|
|
|
|
try:
|
|
pending_config = self._collect_pending_configuration_details(force=force, ursula=ursula)
|
|
new_installation_path = self.node_configuration.initialize(passphrase=pending_config.passphrase,
|
|
wallet=pending_config.wallet,
|
|
encrypting=pending_config.signing,
|
|
tls=pending_config.tls,
|
|
no_registry=no_registry,
|
|
no_keys=pending_config.skip_keys)
|
|
if not pending_config.skip_keys:
|
|
click.secho("Generated new keys at {}".format(self.node_configuration.keyring_dir), fg='blue')
|
|
except NodeConfiguration.ConfigurationError as e:
|
|
click.secho(str(e), fg='red')
|
|
raise click.Abort()
|
|
else:
|
|
click.secho("Created nucypher installation files at {}".format(new_installation_path), fg='green')
|
|
if pending_config.save_file is True:
|
|
configuration_filepath = self.node_configuration.to_configuration_file(filepath=self.config_file)
|
|
click.secho("Saved node configuration file {}".format(configuration_filepath), fg='green')
|
|
if ursula:
|
|
click.secho("\nTo run an Ursula node from the "
|
|
"default configuration filepath run 'nucypher-cli ursula run'\n")
|
|
|
|
def destroy_configuration(self):
|
|
if self.dev:
|
|
raise NodeConfiguration.ConfigurationError("Cannot destroy a temporary node configuration")
|
|
click.confirm('''
|
|
*Permanently and irreversibly delete all* nucypher files including:
|
|
- Private and Public Keys
|
|
- Known Nodes
|
|
- TLS certificates
|
|
- Node Configurations
|
|
Located at {}?'''.format(self.node_configuration.config_root), abort=True)
|
|
shutil.rmtree(self.node_configuration.config_root, ignore_errors=True)
|
|
click.secho("Deleted configuration files at {}".format(self.node_configuration.config_root), fg='blue')
|
|
|
|
|
|
# Register the above class as a decorator
|
|
uses_config = click.make_pass_decorator(NucypherClickConfig, ensure=True)
|
|
|
|
|
|
# Custom input type
|
|
class ChecksumAddress(click.ParamType):
|
|
name = 'checksum_address'
|
|
|
|
def convert(self, value, param, ctx):
|
|
if is_checksum_address(value):
|
|
return value
|
|
self.fail('{} is not a valid EIP-55 checksum address'.format(value, param, ctx))
|
|
|
|
|
|
CHECKSUM_ADDRESS = ChecksumAddress()
|
|
|
|
|
|
@click.group()
|
|
@click.option('--version', help="Echo the CLI version", is_flag=True, callback=echo_version, expose_value=False, is_eager=True)
|
|
@click.option('-v', '--verbose', help="Specify verbosity level", count=True)
|
|
@click.option('--dev', help="Run in development mode", is_flag=True)
|
|
@click.option('--federated-only', help="Connect only to federated nodes", is_flag=True)
|
|
@click.option('--config-root', help="Custom configuration directory", type=click.Path())
|
|
@click.option('--config-file', help="Path to configuration file", type=click.Path(exists=True, dir_okay=False, file_okay=True, readable=True))
|
|
@click.option('--metadata-dir', help="Custom known metadata directory", type=click.Path(exists=True, dir_okay=True, file_okay=False, writable=True))
|
|
@click.option('--provider-uri', help="Blockchain provider's URI", type=click.STRING)
|
|
@click.option('--compile/--no-compile', help="Compile solidity from source files", is_flag=True)
|
|
@click.option('--registry-filepath', help="Custom contract registry filepath", type=click.Path(exists=True, dir_okay=False, file_okay=True, readable=True))
|
|
@click.option('--deployer', help="Connect using a deployer's blockchain interface", is_flag=True)
|
|
@click.option('--poa', help="Inject POA middleware", is_flag=True)
|
|
@uses_config
|
|
def cli(config,
|
|
verbose,
|
|
dev,
|
|
federated_only,
|
|
config_root,
|
|
config_file,
|
|
metadata_dir,
|
|
provider_uri,
|
|
registry_filepath,
|
|
deployer,
|
|
compile,
|
|
poa):
|
|
|
|
click.echo(BANNER)
|
|
|
|
# Store config data
|
|
config.verbose = verbose
|
|
|
|
config.dev = dev
|
|
config.federated_only = federated_only
|
|
config.config_root = config_root
|
|
config.config_file = config_file
|
|
config.metadata_dir = metadata_dir
|
|
config.provider_uri = provider_uri
|
|
config.compile = compile
|
|
config.registry_filepath = registry_filepath
|
|
config.deployer = deployer
|
|
config.poa = poa
|
|
|
|
# TODO: Create NodeConfiguration from these values
|
|
# node_configuration = NodeConfiguration(temp=dev,
|
|
# federated_only=federated_only,
|
|
# config_root=config_root,
|
|
# known_metadata_dir=metadata_dir,
|
|
# registry_filepath=registry_filepath,
|
|
# )
|
|
|
|
if config.verbose:
|
|
click.secho("Verbose mode is enabled", fg='blue')
|
|
|
|
if not config.dev:
|
|
click.secho("WARNING: Development mode is disabled", fg='yellow', bold=True)
|
|
else:
|
|
click.secho("Running in development mode", fg='blue')
|
|
|
|
|
|
@cli.command()
|
|
@click.option('--ursula', help="Configure ursula", is_flag=True, default=False)
|
|
@click.option('--filesystem', is_flag=True, default=False)
|
|
@click.option('--no-registry', help="Skip importing the default contract registry", is_flag=True)
|
|
@click.option('--force', help="Ask confirm once; Do not generate wallet or certificate", is_flag=True)
|
|
@click.option('--checksum-address', type=CHECKSUM_ADDRESS)
|
|
@click.argument('action')
|
|
@uses_config
|
|
def configure(config,
|
|
action,
|
|
ursula,
|
|
filesystem,
|
|
no_registry,
|
|
checksum_address, # TODO: Clean by address
|
|
force):
|
|
"""Manage local nucypher files and directories"""
|
|
config.get_node_configuration(configuration_class=UrsulaConfiguration if ursula else NodeConfiguration)
|
|
if action == "install":
|
|
config.create_new_configuration(ursula=ursula, force=force, no_registry=no_registry)
|
|
elif action == "destroy":
|
|
config.destroy_configuration()
|
|
elif action == "reset":
|
|
config.destroy_configuration()
|
|
config.create_new_configuration(ursula=ursula, force=force, no_registry=no_registry)
|
|
elif action == "cleanup":
|
|
pass # TODO: Clean by address
|
|
elif action == "validate":
|
|
is_valid = True # Until there is a reason to believe otherwise...
|
|
try:
|
|
if filesystem: # Check runtime directory
|
|
is_valid = NodeConfiguration.validate(config_root=config.node_configuration.config_root,
|
|
no_registry=no_registry)
|
|
except NodeConfiguration.InvalidConfiguration:
|
|
is_valid = False
|
|
finally:
|
|
result = 'Valid' if is_valid else 'Invalid'
|
|
click.echo('{} is {}'.format(config.node_configuration.config_root, result))
|
|
else:
|
|
raise click.BadArgumentUsage("No such argument {}".format(action))
|
|
|
|
|
|
@cli.command()
|
|
@click.option('--checksum-address', help="The account to lock/unlock instead of the default", type=CHECKSUM_ADDRESS)
|
|
@click.argument('action', default='list', required=False)
|
|
@uses_config
|
|
def accounts(config,
|
|
action,
|
|
checksum_address):
|
|
"""Manage local and hosted node accounts"""
|
|
|
|
#
|
|
# Initialize
|
|
#
|
|
config.get_node_configuration()
|
|
if not config.federated_only:
|
|
config.connect_to_blockchain()
|
|
config.connect_to_contracts()
|
|
|
|
if not checksum_address:
|
|
checksum_address = config.blockchain.interface.w3.eth.coinbase
|
|
click.echo("WARNING: No checksum address specified - Using the node's default account.")
|
|
|
|
def __collect_transfer_details(denomination: str):
|
|
destination = click.prompt("Enter destination checksum_address")
|
|
if not is_checksum_address(destination):
|
|
click.secho("{} is not a valid checksum checksum_address".format(destination), fg='red', bold=True)
|
|
raise click.Abort()
|
|
amount = click.prompt("Enter amount of {} to transfer".format(denomination), type=click.INT)
|
|
return destination, amount
|
|
|
|
#
|
|
# Action Switch
|
|
#
|
|
if action == 'new':
|
|
new_address = config.create_account()
|
|
click.secho("Created new ETH address {}".format(new_address), fg='blue')
|
|
if click.confirm("Set new address as the node's keying default account?".format(new_address)):
|
|
config.blockchain.interface.w3.eth.defaultAccount = new_address
|
|
click.echo("{} is now the node's default account.".format(config.blockchain.interface.w3.eth.defaultAccount))
|
|
|
|
if action == 'set-default':
|
|
config.blockchain.interface.w3.eth.defaultAccount = checksum_address # TODO: is there a better way to do this?
|
|
click.echo("{} is now the node's default account.".format(config.blockchain.interface.w3.eth.defaultAccount))
|
|
|
|
elif action == 'export':
|
|
keyring = NucypherKeyring(account=checksum_address)
|
|
click.confirm("Export local private key for {} to node's keyring: {}?".format(checksum_address, config.provider_uri), abort=True)
|
|
passphrase = click.prompt("Enter passphrase to decrypt account", type=click.STRING, hide_input=True, confirmation_prompt=True)
|
|
keyring._export_wallet_to_node(blockchain=config.blockchain, passphrase=passphrase)
|
|
|
|
elif action == 'list':
|
|
for index, checksum_address in enumerate(config.accounts):
|
|
token_balance = config.token_agent.get_balance(address=checksum_address)
|
|
eth_balance = config.blockchain.interface.w3.eth.getBalance(checksum_address)
|
|
base_row_template = ' {address}\n Tokens: {tokens}\n ETH: {eth}\n '
|
|
row_template = ('\netherbase |'+base_row_template) if not index else '{index} ....... |'+base_row_template
|
|
row = row_template.format(index=index, address=checksum_address, tokens=token_balance, eth=eth_balance)
|
|
click.secho(row, fg='blue')
|
|
|
|
elif action == 'balance':
|
|
if not checksum_address:
|
|
checksum_address = config.blockchain.interface.w3.eth.etherbase
|
|
click.echo('No checksum_address supplied, Using the default {}'.format(checksum_address))
|
|
token_balance = config.token_agent.get_balance(address=checksum_address)
|
|
eth_balance = config.token_agent.blockchain.interface.w3.eth.getBalance(checksum_address)
|
|
click.secho("Balance of {} | Tokens: {} | ETH: {}".format(checksum_address, token_balance, eth_balance), fg='blue')
|
|
|
|
elif action == "transfer-tokens":
|
|
destination, amount = __collect_transfer_details(denomination='tokens')
|
|
click.confirm("Are you sure you want to send {} tokens to {}?".format(amount, destination), abort=True)
|
|
txhash = config.token_agent.transfer(amount=amount, target_address=destination, sender_address=checksum_address)
|
|
config.blockchain.wait_for_receipt(txhash)
|
|
click.echo("Sent {} tokens to {} | {}".format(amount, destination, txhash))
|
|
|
|
elif action == "transfer-eth":
|
|
destination, amount = __collect_transfer_details(denomination='ETH')
|
|
tx = {'to': destination, 'from': checksum_address, 'value': amount}
|
|
click.confirm("Are you sure you want to send {} tokens to {}?".format(tx['value'], tx['to']), abort=True)
|
|
txhash = config.blockchain.interface.w3.eth.sendTransaction(tx)
|
|
config.blockchain.wait_for_receipt(txhash)
|
|
click.echo("Sent {} ETH to {} | {}".format(amount, destination, str(txhash)))
|
|
|
|
else:
|
|
raise click.BadArgumentUsage
|
|
|
|
|
|
@cli.command()
|
|
@click.option('--checksum-address', type=CHECKSUM_ADDRESS)
|
|
@click.option('--value', help="Token value of stake", type=click.IntRange(min=MIN_ALLOWED_LOCKED, max=MIN_ALLOWED_LOCKED, clamp=False))
|
|
@click.option('--duration', help="Period duration of stake", type=click.IntRange(min=MIN_LOCKED_PERIODS, max=MAX_MINTING_PERIODS, clamp=False))
|
|
@click.option('--index', help="A specific stake index to resume", type=click.INT)
|
|
@click.argument('action', default='list', required=False)
|
|
@uses_config
|
|
def stake(config,
|
|
action,
|
|
checksum_address,
|
|
index,
|
|
value,
|
|
duration):
|
|
"""
|
|
Manage token staking.
|
|
|
|
|
|
Arguments
|
|
==========
|
|
|
|
action - Which action to perform; The choices are:
|
|
|
|
- list: List all stakes for this node
|
|
- info: Display info about a specific stake
|
|
- start: Start the staking daemon
|
|
- confirm-activity: Manually confirm-activity for the current period
|
|
- divide-stake: Divide an existing stake
|
|
|
|
value - The quantity of tokens to stake.
|
|
|
|
periods - The duration (in periods) of the stake.
|
|
|
|
Options
|
|
========
|
|
|
|
--wallet-address - A valid ethereum checksum address to use instead of the default
|
|
--stake-index - The zero-based stake index, or stake tag for this wallet-address
|
|
|
|
"""
|
|
|
|
#
|
|
# Initialize
|
|
#
|
|
config.get_node_configuration()
|
|
if not config.federated_only:
|
|
config.connect_to_blockchain()
|
|
config.connect_to_contracts()
|
|
|
|
if not checksum_address:
|
|
|
|
for index, address in enumerate(config.accounts):
|
|
if index == 0:
|
|
row = 'etherbase (0) | {}'.format(address)
|
|
else:
|
|
row = '{} .......... | {}'.format(index, address)
|
|
click.echo(row)
|
|
|
|
click.echo("Select ethereum address")
|
|
account_selection = click.prompt("Enter 0-{}".format(len(config.accounts)), type=click.INT)
|
|
address = config.accounts[account_selection]
|
|
|
|
if action == 'list':
|
|
live_stakes = config.miner_agent.get_all_stakes(miner_address=address)
|
|
for index, stake_info in enumerate(live_stakes):
|
|
row = '{} | {}'.format(index, stake_info)
|
|
click.echo(row)
|
|
|
|
elif action == 'init':
|
|
click.confirm("Stage a new stake?", abort=True)
|
|
|
|
live_stakes = config.miner_agent.get_all_stakes(miner_address=address)
|
|
if len(live_stakes) > 0:
|
|
raise RuntimeError("There is an existing stake for {}".format(address))
|
|
|
|
# Value
|
|
balance = config.token_agent.get_balance(address=address)
|
|
click.echo("Current balance: {}".format(balance))
|
|
value = click.prompt("Enter stake value", type=click.INT)
|
|
|
|
# Duration
|
|
message = "Minimum duration: {} | Maximum Duration: {}".format(constants.MIN_LOCKED_PERIODS,
|
|
constants.MAX_REWARD_PERIODS)
|
|
click.echo(message)
|
|
duration = click.prompt("Enter stake duration in days", type=click.INT)
|
|
|
|
start_period = config.miner_agent.get_current_period()
|
|
end_period = start_period + duration
|
|
|
|
# Review
|
|
click.echo("""
|
|
|
|
| Staged Stake |
|
|
|
|
Node: {address}
|
|
Value: {value}
|
|
Duration: {duration}
|
|
Start Period: {start_period}
|
|
End Period: {end_period}
|
|
|
|
""".format(address=address,
|
|
value=value,
|
|
duration=duration,
|
|
start_period=start_period,
|
|
end_period=end_period))
|
|
|
|
# TODO: Ursula Process management
|
|
# if not click.confirm("Is this correct?"):
|
|
# # field = click.prompt("Which stake field do you want to edit?")
|
|
# raise NotImplementedError
|
|
#
|
|
# # Initialize the staged stake
|
|
# config.miner_agent.deposit_tokens(amount=value, lock_periods=duration, sender_address=address)
|
|
#
|
|
# proc_params = ['run_ursula']
|
|
# processProtocol = UrsulaProcessProtocol(command=proc_params, checksum_address=checksum_address)
|
|
# ursula_proc = reactor.spawnProcess(processProtocol, "nucypher-cli", proc_params)
|
|
raise NotImplementedError
|
|
|
|
elif action == 'resume':
|
|
"""Reconnect and resume an existing live stake"""
|
|
# proc_params = ['run_ursula']
|
|
# processProtocol = UrsulaProcessProtocol(command=proc_params, checksum_address=checksum_address)
|
|
# ursula_proc = reactor.spawnProcess(processProtocol, "nucypher-cli", proc_params)
|
|
raise NotImplementedError
|
|
|
|
elif action == 'confirm-activity':
|
|
"""Manually confirm activity for the active period"""
|
|
stakes = config.miner_agent.get_all_stakes(miner_address=address)
|
|
if len(stakes) == 0:
|
|
raise RuntimeError("There are no active stakes for {}".format(address))
|
|
config.miner_agent.confirm_activity(node_address=address)
|
|
|
|
elif action == 'divide':
|
|
"""Divide an existing stake by specifying the new target value and end period"""
|
|
|
|
stakes = config.miner_agent.get_all_stakes(miner_address=address)
|
|
if len(stakes) == 0:
|
|
raise RuntimeError("There are no active stakes for {}".format(address))
|
|
|
|
if not index:
|
|
for selection_index, stake_info in enumerate(stakes):
|
|
click.echo("{} ....... {}".format(selection_index, stake_info))
|
|
index = click.prompt("Select a stake to divide", type=click.INT)
|
|
|
|
target_value = click.prompt("Enter new target value", type=click.INT)
|
|
extension = click.prompt("Enter number of periods to extend", type=click.INT)
|
|
|
|
click.echo("""
|
|
Current Stake: {}
|
|
|
|
New target value {}
|
|
New end period: {}
|
|
|
|
""".format(stakes[index],
|
|
target_value,
|
|
target_value+extension))
|
|
|
|
click.confirm("Is this correct?", abort=True)
|
|
config.miner_agent.divide_stake(miner_address=address,
|
|
stake_index=index,
|
|
value=value,
|
|
periods=extension)
|
|
|
|
elif action == 'collect-reward':
|
|
"""Withdraw staking reward to the specified wallet address"""
|
|
# click.confirm("Send {} to {}?".format)
|
|
# config.miner_agent.collect_staking_reward(collector_address=address)
|
|
raise NotImplementedError
|
|
|
|
elif action == 'abort':
|
|
click.confirm("Are you sure you want to abort the staking process?", abort=True)
|
|
# os.kill(pid=NotImplemented)
|
|
raise NotImplementedError
|
|
|
|
else:
|
|
raise click.BadArgumentUsage
|
|
|
|
|
|
@cli.command()
|
|
@click.option('--geth', help="Simulate with geth dev-mode", is_flag=True)
|
|
@click.option('--pyevm', help="Simulate with PyEVM", is_flag=True)
|
|
@click.option('--nodes', help="The number of nodes to simulate", type=click.INT, default=10)
|
|
@click.argument('action')
|
|
@uses_config
|
|
def simulate(config,
|
|
action,
|
|
nodes,
|
|
geth,
|
|
pyevm):
|
|
"""
|
|
Locally simulate the nucypher network
|
|
|
|
action - Which action to perform; The choices are:
|
|
- start: Start a multi-process nucypher network simulation
|
|
- stop: Stop a running simulation gracefully
|
|
|
|
|
|
--nodes - The quantity of nodes (processes) to execute during the simulation
|
|
--duration = The number of periods to run the simulation before termination
|
|
|
|
"""
|
|
|
|
if action == 'start':
|
|
|
|
#
|
|
# Blockchain Connection
|
|
#
|
|
if config.sim_processes is constants.NO_SIMULATION_RUNNING:
|
|
config.sim_processes = list()
|
|
elif len(config.sim_processes) != 0:
|
|
for process in config.sim_processes:
|
|
config.sim_processes.remove(process)
|
|
os.kill(process.pid, 9)
|
|
|
|
if not config.federated_only:
|
|
if geth:
|
|
config.provider_uri = "ipc:///tmp/geth.ipc"
|
|
elif pyevm:
|
|
config.provider_uri = "tester://pyevm"
|
|
sim_provider_uri = config.provider_uri
|
|
|
|
# Sanity check
|
|
supported_sim_uris = ("tester://geth", "tester://pyevm", "ipc:///tmp/geth.ipc")
|
|
if config.provider_uri not in supported_sim_uris:
|
|
message = "{} is not a supported simulation node backend. Supported URIs are {}"
|
|
click.echo(message.format(config.provider_uri, supported_sim_uris))
|
|
raise click.Abort()
|
|
|
|
simulation_registry = TemporaryEthereumContractRegistry()
|
|
simulation_interface = BlockchainDeployerInterface(provider_uri=sim_provider_uri,
|
|
registry=simulation_registry,
|
|
compiler=SolidityCompiler())
|
|
|
|
sim_blockchain = TesterBlockchain(interface=simulation_interface, test_accounts=nodes, airdrop=False)
|
|
|
|
accounts = sim_blockchain.interface.w3.eth.accounts
|
|
origin, *everyone_else = accounts
|
|
|
|
# Set the deployer address from the freshly created test account
|
|
simulation_interface.deployer_address = origin
|
|
|
|
#
|
|
# Blockchain Action
|
|
#
|
|
sim_blockchain.ether_airdrop(amount=DEVELOPMENT_ETH_AIRDROP_AMOUNT)
|
|
|
|
click.confirm("Deploy all nucypher contracts to {}?".format(config.provider_uri), abort=True)
|
|
click.echo("Bootstrapping simulated blockchain network")
|
|
|
|
# Deploy contracts
|
|
token_deployer = NucypherTokenDeployer(blockchain=sim_blockchain, deployer_address=origin)
|
|
token_deployer.arm()
|
|
token_deployer.deploy()
|
|
sim_token_agent = token_deployer.make_agent()
|
|
|
|
miners_escrow_secret = os.urandom(DISPATCHER_SECRET_LENGTH)
|
|
miner_escrow_deployer = MinerEscrowDeployer(token_agent=sim_token_agent,
|
|
deployer_address=origin,
|
|
secret_hash=miners_escrow_secret)
|
|
miner_escrow_deployer.arm()
|
|
miner_escrow_deployer.deploy()
|
|
sim_miner_agent = miner_escrow_deployer.make_agent()
|
|
|
|
policy_manager_secret = os.urandom(DISPATCHER_SECRET_LENGTH)
|
|
policy_manager_deployer = PolicyManagerDeployer(miner_agent=sim_miner_agent,
|
|
deployer_address=origin,
|
|
secret_hash=policy_manager_secret)
|
|
policy_manager_deployer.arm()
|
|
policy_manager_deployer.deploy()
|
|
policy_agent = policy_manager_deployer.make_agent()
|
|
|
|
airdrop_amount = DEVELOPMENT_TOKEN_AIRDROP_AMOUNT
|
|
click.echo("Airdropping tokens {} to {} addresses".format(airdrop_amount, len(everyone_else)))
|
|
_receipts = token_airdrop(token_agent=sim_token_agent,
|
|
origin=origin,
|
|
addresses=everyone_else,
|
|
amount=airdrop_amount)
|
|
|
|
# Commit the current state of deployment to a registry file.
|
|
click.echo("Writing filesystem registry")
|
|
_sim_registry_name = sim_blockchain.interface.registry.commit(filepath=DEFAULT_SIMULATION_REGISTRY_FILEPATH)
|
|
|
|
click.echo("Ready to run swarm.")
|
|
|
|
#
|
|
# Swarm
|
|
#
|
|
|
|
# Select a port range to use on localhost for sim servers
|
|
|
|
if not config.federated_only:
|
|
sim_addresses = everyone_else
|
|
else:
|
|
sim_addresses = NotImplemented
|
|
|
|
start_port, counter = 8787, 0
|
|
for sim_port_number, sim_address in enumerate(sim_addresses, start=start_port):
|
|
|
|
#
|
|
# Parse sim-ursula parameters
|
|
#
|
|
|
|
sim_db_name = 'sim-{}'.format(sim_port_number)
|
|
|
|
process_params = ['nucypher-cli', '--dev']
|
|
if geth is True:
|
|
process_params.append('--poa')
|
|
if config.federated_only:
|
|
process_params.append('--federated-only')
|
|
else:
|
|
process_params.extend('--registry-filepath {} --provider-uri {}'.format(simulation_registry.filepath,
|
|
sim_provider_uri).split())
|
|
ursula_params = '''ursula run --rest-port {} --db-name {}'''.format(sim_port_number, sim_db_name).split()
|
|
process_params.extend(ursula_params)
|
|
|
|
if not config.federated_only:
|
|
min_stake, balance = MIN_ALLOWED_LOCKED, DEVELOPMENT_TOKEN_AIRDROP_AMOUNT
|
|
value = random.randint(min_stake, balance) # stake a random amount...
|
|
min_locktime, max_locktime = MIN_LOCKED_PERIODS, MAX_MINTING_PERIODS # ...for a random lock duration
|
|
periods = random.randint(min_locktime, max_locktime)
|
|
process_params.extend('--checksum-address {}'.format(sim_address).split())
|
|
process_params.extend('--stake-amount {} --stake-periods {}'.format(value, periods).split())
|
|
|
|
# Spawn
|
|
click.echo("Spawning node #{}".format(counter+1))
|
|
processProtocol = UrsulaProcessProtocol(command=process_params, checksum_address=sim_address)
|
|
cli_exec = os.path.join(BASE_DIR, 'cli', 'main.py')
|
|
ursula_process = reactor.spawnProcess(processProtocol=processProtocol,
|
|
executable=cli_exec,
|
|
args=process_params,
|
|
env=os.environ)
|
|
|
|
config.sim_processes.append(ursula_process)
|
|
|
|
#
|
|
# post-spawnProcess
|
|
#
|
|
|
|
# Start with some basic status data, then build on it
|
|
|
|
rest_uri = "https://{}:{}".format('localhost', sim_port_number)
|
|
|
|
sim_data = "prepared simulated Ursula | ReST {}".format(rest_uri)
|
|
rest_uri = "{host}:{port}".format(host='localhost', port=str(sim_port_number))
|
|
sim_data.format(rest_uri)
|
|
|
|
if not config.federated_only:
|
|
sim_data += '| ETH address {}'.format(sim_address)
|
|
|
|
click.echo(sim_data)
|
|
counter += 1
|
|
|
|
click.echo("Starting the reactor")
|
|
click.confirm("Start the reactor?", abort=True)
|
|
try:
|
|
reactor.run()
|
|
finally:
|
|
if not config.federated_only:
|
|
click.echo("Removing simulation registry")
|
|
os.remove(DEFAULT_SIMULATION_REGISTRY_FILEPATH)
|
|
click.echo("Stopping simulated Ursula processes")
|
|
for process in config.sim_processes:
|
|
os.kill(process.pid, 9)
|
|
click.echo("Killed {}".format(process))
|
|
click.echo("Simulation Stopped")
|
|
|
|
elif action == 'stop':
|
|
# Kill the simulated ursulas
|
|
for process in config.ursula_processes:
|
|
process.transport.signalProcess('KILL')
|
|
|
|
elif action == 'status':
|
|
if not config.simulation_running:
|
|
status_message = "Simulation not running."
|
|
else:
|
|
ursula_processes = len(config.ursula_processes)
|
|
status_message = """
|
|
|
|
| Node Swarm Simulation Status |
|
|
|
|
Simulation processes .............. {}
|
|
|
|
""".format(ursula_processes)
|
|
click.echo(status_message)
|
|
|
|
else:
|
|
raise click.BadArgumentUsage
|
|
|
|
|
|
@cli.command()
|
|
@click.option('--contract-name', help="Deploy a single contract by name", type=click.STRING)
|
|
@click.option('--force', is_flag=True)
|
|
@click.option('--deployer-address', help="Deployer's checksum address", type=CHECKSUM_ADDRESS)
|
|
@click.option('--registry-outfile', help="Output path for new registry", type=click.Path(), default=NodeConfiguration.REGISTRY_SOURCE)
|
|
@click.argument('action')
|
|
@uses_config
|
|
def deploy(config,
|
|
action,
|
|
deployer_address,
|
|
contract_name,
|
|
registry_outfile,
|
|
force):
|
|
"""Manage contract and registry deployment"""
|
|
|
|
if not config.deployer:
|
|
click.secho("The --deployer flag must be used to issue the deploy command.", fg='red', bold=True)
|
|
raise click.Abort()
|
|
|
|
def __get_deployers():
|
|
|
|
config.registry_filepath = registry_outfile
|
|
config.connect_to_blockchain()
|
|
config.blockchain.interface.deployer_address = deployer_address or config.accounts[0]
|
|
|
|
DeployerInfo = collections.namedtuple('DeployerInfo', 'deployer_class upgradeable agent_name dependant')
|
|
deployers = collections.OrderedDict({
|
|
|
|
NucypherTokenDeployer._contract_name: DeployerInfo(deployer_class=NucypherTokenDeployer,
|
|
upgradeable=False,
|
|
agent_name='token_agent',
|
|
dependant=None),
|
|
|
|
MinerEscrowDeployer._contract_name: DeployerInfo(deployer_class=MinerEscrowDeployer,
|
|
upgradeable=True,
|
|
agent_name='miner_agent',
|
|
dependant='token_agent'),
|
|
|
|
PolicyManagerDeployer._contract_name: DeployerInfo(deployer_class=PolicyManagerDeployer,
|
|
upgradeable=True,
|
|
agent_name='policy_agent',
|
|
dependant='miner_agent'
|
|
),
|
|
|
|
# UserEscrowDeployer._contract_name: DeployerInfo(deployer_class=UserEscrowDeployer,
|
|
# upgradeable=True,
|
|
# agent_name='user_agent',
|
|
# dependant='policy_agent'), # TODO: User Escrow CLI Deployment
|
|
})
|
|
|
|
click.confirm("Continue?", abort=True)
|
|
return deployers
|
|
|
|
if action == "contracts":
|
|
deployers = __get_deployers()
|
|
__deployment_transactions = dict()
|
|
__deployment_agents = dict()
|
|
|
|
available_deployers = ", ".join(deployers)
|
|
click.echo("\n-----------------------------------------------")
|
|
click.echo("Available Deployers: {}".format(available_deployers))
|
|
click.echo("Blockchain Provider URI ... {}".format(config.blockchain.interface.provider_uri))
|
|
click.echo("Registry Output Filepath .. {}".format(config.blockchain.interface.registry.filepath))
|
|
click.echo("Deployer's Address ........ {}".format(config.blockchain.interface.deployer_address))
|
|
click.echo("-----------------------------------------------\n")
|
|
|
|
def __deploy_contract(deployer_class: ClassVar,
|
|
upgradeable: bool,
|
|
agent_name: str,
|
|
dependant: str = None
|
|
) -> Tuple[dict, EthereumContractAgent]:
|
|
|
|
__contract_name = deployer_class._contract_name
|
|
|
|
__deployer_init_args = dict(blockchain=config.blockchain,
|
|
deployer_address=config.blockchain.interface.deployer_address)
|
|
|
|
if dependant is not None:
|
|
__deployer_init_args.update({dependant: __deployment_agents[dependant]})
|
|
|
|
if upgradeable:
|
|
secret = click.prompt("Enter deployment secret for {}".format(__contract_name),
|
|
hide_input=True, confirmation_prompt=True)
|
|
secret_hash = hashlib.sha256(secret)
|
|
__deployer_init_args.update({'secret_hash': secret_hash})
|
|
|
|
__deployer = deployer_class(**__deployer_init_args)
|
|
|
|
#
|
|
# Arm
|
|
#
|
|
if not force:
|
|
click.confirm("Arm {}?".format(deployer_class.__name__), abort=True)
|
|
|
|
is_armed, disqualifications = __deployer.arm(abort=False)
|
|
if not is_armed:
|
|
disqualifications = ', '.join(disqualifications)
|
|
click.secho("Failed to arm {}. Disqualifications: {}".format(__contract_name, disqualifications),
|
|
fg='red', bold=True)
|
|
raise click.Abort()
|
|
|
|
#
|
|
# Deploy
|
|
#
|
|
if not force:
|
|
click.confirm("Deploy {}?".format(__contract_name), abort=True)
|
|
__transactions = __deployer.deploy()
|
|
__deployment_transactions[__contract_name] = __transactions
|
|
|
|
__agent = __deployer.make_agent()
|
|
__deployment_agents[agent_name] = __agent
|
|
|
|
click.secho("Deployed {} - Contract Address: {}".format(contract_name, __agent.contract_address),
|
|
fg='green', bold=True)
|
|
|
|
return __transactions, __agent
|
|
|
|
if contract_name:
|
|
#
|
|
# Deploy Single Contract
|
|
#
|
|
try:
|
|
deployer_info = deployers[contract_name]
|
|
except KeyError:
|
|
click.secho("No such contract {}. Available contracts are {}".format(contract_name, available_deployers), fg='red', bold=True)
|
|
raise click.Abort()
|
|
else:
|
|
_txs, _agent = __deploy_contract(deployer_info.deployer_class,
|
|
upgradeable=deployer_info.upgradeable,
|
|
agent_name=deployer_info.agent_name,
|
|
dependant=deployer_info.dependant)
|
|
else:
|
|
#
|
|
# Deploy All Contracts
|
|
#
|
|
for deployer_name, deployer_info in deployers.items():
|
|
_txs, _agent = __deploy_contract(deployer_info.deployer_class,
|
|
upgradeable=deployer_info.upgradeable,
|
|
agent_name=deployer_info.agent_name,
|
|
dependant=deployer_info.dependant)
|
|
|
|
if not force and click.prompt("View deployment transaction hashes?"):
|
|
for contract_name, transactions in __deployment_transactions.items():
|
|
click.echo(contract_name)
|
|
for tx_name, txhash in transactions.items():
|
|
click.echo("{}:{}".format(tx_name, txhash))
|
|
|
|
if not force and click.confirm("Save transaction hashes to JSON file?"):
|
|
file = click.prompt("Enter output filepath", type=click.File(mode='w')) # TODO
|
|
file.__write(json.dumps(__deployment_transactions))
|
|
click.secho("Successfully wrote transaction hashes file to {}".format(file.path), fg='green')
|
|
|
|
else:
|
|
raise click.BadArgumentUsage
|
|
|
|
|
|
@cli.command()
|
|
@click.option('--contracts', help="Echo nucypher smart contract info", is_flag=True)
|
|
@click.option('--network', help="Echo the network status", is_flag=True)
|
|
@uses_config
|
|
def status(config,
|
|
contracts,
|
|
network):
|
|
"""
|
|
Echo a snapshot of live network metadata.
|
|
"""
|
|
#
|
|
# Initialize
|
|
#
|
|
config.get_node_configuration()
|
|
if not config.federated_only:
|
|
config.connect_to_blockchain()
|
|
config.connect_to_contracts()
|
|
|
|
contract_payload = """
|
|
|
|
| NuCypher ETH Contracts |
|
|
|
|
Provider URI ............. {provider_uri}
|
|
Registry Path ............ {registry_filepath}
|
|
|
|
NucypherToken ............ {token}
|
|
MinerEscrow .............. {escrow}
|
|
PolicyManager ............ {manager}
|
|
|
|
""".format(provider_uri=config.blockchain.interface.provider_uri,
|
|
registry_filepath=config.blockchain.interface.registry.filepath,
|
|
token=config.token_agent.contract_address,
|
|
escrow=config.miner_agent.contract_address,
|
|
manager=config.policy_agent.contract_address,
|
|
period=config.miner_agent.get_current_period())
|
|
|
|
network_payload = """
|
|
| Blockchain Network |
|
|
|
|
Current Period ........... {period}
|
|
Gas Price ................ {gas_price}
|
|
Active Staking Ursulas ... {ursulas}
|
|
|
|
""".format(period=config.miner_agent.get_current_period(),
|
|
gas_price=config.blockchain.interface.w3.eth.gasPrice,
|
|
ursulas=config.miner_agent.get_miner_population())
|
|
|
|
subpayloads = ((contracts, contract_payload),
|
|
(network, network_payload),
|
|
)
|
|
|
|
if not any(sp[0] for sp in subpayloads):
|
|
payload = ''.join(sp[1] for sp in subpayloads)
|
|
else:
|
|
payload = str()
|
|
for requested, subpayload in subpayloads:
|
|
if requested is True:
|
|
payload += subpayload
|
|
|
|
click.echo(payload)
|
|
|
|
|
|
@cli.command()
|
|
@click.option('--debug', is_flag=True)
|
|
@click.option('--rest-host', type=click.STRING)
|
|
@click.option('--rest-port', type=click.IntRange(min=49151, max=65535, clamp=False))
|
|
@click.option('--additional-nodes', help="Custom known metadata directory", type=click.Path(exists=True, dir_okay=True, file_okay=False, writable=True))
|
|
@click.option('--db-name', type=click.STRING)
|
|
@click.option('--checksum-address', type=CHECKSUM_ADDRESS)
|
|
@click.option('--stake-amount', type=click.IntRange(min=MIN_ALLOWED_LOCKED, max=MAX_ALLOWED_LOCKED, clamp=False))
|
|
@click.option('--stake-periods', type=click.IntRange(min=MIN_LOCKED_PERIODS, max=MAX_MINTING_PERIODS, clamp=False))
|
|
@click.option('--resume', help="Resume an existing stake", is_flag=True)
|
|
@click.option('--no-reactor', help="Development feature", is_flag=True)
|
|
@click.option('--password', help="Password to unlock Ursula's keyring",
|
|
prompt=True, hide_input=True, confirmation_prompt=True, envvar="NUCYPHER_KEYRING_PASSPHRASE")
|
|
@click.argument('action')
|
|
@uses_config
|
|
def ursula(config,
|
|
action,
|
|
rest_port,
|
|
rest_host,
|
|
additional_nodes,
|
|
db_name,
|
|
checksum_address,
|
|
stake_amount,
|
|
stake_periods,
|
|
resume, # TODO Implement stake resume
|
|
no_reactor,
|
|
password,
|
|
debug
|
|
) -> None:
|
|
"""
|
|
Manage and run an Ursula node
|
|
|
|
Here is the procedure to "spin-up" an Ursula node.
|
|
|
|
0. Validate CLI Input
|
|
1. Initialize UrsulaConfiguration (from configuration file or inline)
|
|
2. Initialize Ursula with Passphrase
|
|
3. Initialize Staking Loop
|
|
4. Run TLS deployment (Learning Loop + Reactor)
|
|
|
|
"""
|
|
|
|
def __make_ursula():
|
|
if not checksum_address and not config.dev:
|
|
raise click.BadArgumentUsage("No Configuration file found, and no --checksum address <addr> was provided.")
|
|
if not checksum_address and not config.dev:
|
|
raise click.BadOptionUsage("No account specified. pass --checksum-address or --dev, "
|
|
"or use a configuration file with --config-file <path>")
|
|
if not config.federated_only:
|
|
if not all((stake_amount, stake_periods)) and not resume:
|
|
raise click.BadOptionUsage("Both the --stake-amount <amount> and --stake-periods <periods> options "
|
|
"or the --resume flag is required to run a non-federated Ursula."
|
|
"For federated run 'nucypher-cli --federated-only ursula <action>'")
|
|
|
|
return UrsulaConfiguration(temp=config.dev,
|
|
auto_initialize=config.dev,
|
|
is_me=True,
|
|
rest_host=rest_host,
|
|
rest_port=rest_port,
|
|
db_name=db_name,
|
|
federated_only=config.federated_only,
|
|
registry_filepath=config.registry_filepath,
|
|
provider_uri=config.provider_uri,
|
|
checksum_address=checksum_address,
|
|
poa=config.poa,
|
|
save_metadata=False,
|
|
load_metadata=True,
|
|
known_metadata_dir=config.metadata_dir,
|
|
start_learning_now=True,
|
|
abort_on_learning_error=config.dev)
|
|
|
|
#
|
|
# Produce
|
|
#
|
|
overrides = dict()
|
|
if config.dev:
|
|
ursula_config = __make_ursula()
|
|
else:
|
|
try: # TODO: inline overrides for file-based configurations
|
|
filepath = config.config_file or UrsulaConfiguration.DEFAULT_CONFIG_FILE_LOCATION
|
|
click.secho("Reading Ursula node configuration file {}".format(filepath), fg='blue')
|
|
ursula_config = UrsulaConfiguration.from_configuration_file(filepath=filepath)
|
|
except FileNotFoundError:
|
|
ursula_config = __make_ursula()
|
|
|
|
config.operating_mode = "federated" if ursula_config.federated_only else "decentralized"
|
|
click.secho("Running in {} mode".format(config.operating_mode), fg='blue')
|
|
|
|
# Bootnodes, Seeds, Known Nodes
|
|
ursula_config.get_bootnodes()
|
|
quantity_known_nodes = len(ursula_config.known_nodes)
|
|
if quantity_known_nodes > 0:
|
|
click.secho("Loaded {} known nodes from storages".format(quantity_known_nodes, fg='blue'))
|
|
else:
|
|
click.secho("WARNING: No seed nodes available", fg='red', bold=True)
|
|
|
|
URSULA = ursula_config.produce(passphrase=password, **overrides) # 2
|
|
click.secho("Initialized Ursula {}".format(URSULA.checksum_public_address), fg='green')
|
|
|
|
#
|
|
# Run
|
|
#
|
|
if action == 'run':
|
|
try:
|
|
if not ursula_config.federated_only: # 3
|
|
URSULA.stake(amount=stake_amount, lock_periods=stake_periods)
|
|
click.secho("Initialized Stake", fg='blue')
|
|
if not no_reactor:
|
|
click.secho("Running Ursula on {}".format(URSULA.rest_interface), fg='green', bold=True)
|
|
URSULA.get_deployer().run() # 4
|
|
except Exception as e:
|
|
config.log.critical(str(e))
|
|
click.secho("{} {}".format(e.__class__.__name__, str(e)), fg='red')
|
|
if debug: raise
|
|
raise click.Abort()
|
|
finally:
|
|
click.secho("Stopping Ursula")
|
|
ursula_config.cleanup()
|
|
click.secho("Ursula Stopped", fg='red')
|
|
|
|
elif action == "save-metadata":
|
|
metadata_path = URSULA.write_node_metadata(node=URSULA)
|
|
click.secho("Successfully saved node metadata to {}.".format(metadata_path), fg='green')
|
|
|
|
else:
|
|
raise click.BadArgumentUsage
|
|
|
|
|
|
if __name__ == "__main__":
|
|
cli()
|