def create_config(self, emitter, config_file): if self.dev: return UrsulaConfiguration( emitter=emitter, dev_mode=True, domain=TEMPORARY_DOMAIN, poa=self.poa, light=self.light, registry_filepath=self.registry_filepath, policy_registry_filepath=self.policy_registry_filepath, eth_provider_uri=self.eth_provider_uri, signer_uri=self.signer_uri, gas_strategy=self.gas_strategy, max_gas_price=self.max_gas_price, checksum_address=self.operator_address, federated_only=self.federated_only, rest_host=self.rest_host, rest_port=self.rest_port, db_filepath=self.db_filepath, availability_check=self.availability_check, payment_method=self.payment_method, payment_provider=self.payment_provider, payment_network=self.payment_network) else: if not config_file: config_file = select_config_file( emitter=emitter, checksum_address=self.operator_address, config_class=UrsulaConfiguration) try: return UrsulaConfiguration.from_configuration_file( emitter=emitter, filepath=config_file, domain=self.domain, registry_filepath=self.registry_filepath, policy_registry_filepath=self.policy_registry_filepath, eth_provider_uri=self.eth_provider_uri, signer_uri=self.signer_uri, gas_strategy=self.gas_strategy, max_gas_price=self.max_gas_price, rest_host=self.rest_host, rest_port=self.rest_port, db_filepath=self.db_filepath, poa=self.poa, light=self.light, federated_only=self.federated_only, availability_check=self.availability_check, payment_method=self.payment_method, payment_provider=self.payment_provider, payment_network=self.payment_network) except FileNotFoundError: return handle_missing_configuration_file( character_config_class=UrsulaConfiguration, config_file=config_file) except Keystore.AuthenticationFailed as e: emitter.echo(str(e), color='red', bold=True) # TODO: Exit codes (not only for this, but for other exceptions) return click.get_current_context().exit(1)
def run_ursula(rest_port, rest_host, db_name, checksum_address, federated_only, metadata_dir, config_file, dev ) -> None: """ The following procedure is required to "spin-up" an Ursula node. 1. Initialize UrsulaConfiguration 2. Initialize Ursula 3. Run TLS deployment 4. Start the staking daemon Configurable values are first read from the configuration file, but can be overridden (mostly for testing purposes) with inline cli options. """ if not dev: click.echo("WARNING: Development mode is disabled") temp = False else: click.echo("Running in development mode") temp = True if config_file: ursula_config = UrsulaConfiguration.from_configuration_file(filepath=config_file) else: ursula_config = UrsulaConfiguration(temp=temp, auto_initialize=temp, rest_host=rest_host, rest_port=rest_port, db_name=db_name, is_me=True, federated_only=federated_only, checksum_address=checksum_address, # save_metadata=False, # TODO load_metadata=True, known_metadata_dir=metadata_dir, start_learning_now=True, abort_on_learning_error=temp) try: URSULA = ursula_config.produce() URSULA.get_deployer().run() # Run TLS Deploy (Reactor) if not URSULA.federated_only: # TODO: Resume / Init URSULA.stake() # Start Staking Daemon finally: click.echo("Cleaning up temporary runtime files and directories") ursula_config.cleanup() # TODO: Integrate with other "graceful" shutdown functionality click.echo("Exited gracefully")
def _get_ursula_config(emitter, geth, provider_uri, network, registry_filepath, dev, config_file, staker_address, worker_address, federated_only, rest_host, rest_port, db_filepath, poa): ETH_NODE = NO_BLOCKCHAIN_CONNECTION if geth: ETH_NODE = actions.get_provider_process() provider_uri = ETH_NODE.provider_uri(scheme='file') if dev: ursula_config = UrsulaConfiguration( dev_mode=True, domains={TEMPORARY_DOMAIN}, poa=poa, registry_filepath=registry_filepath, provider_process=ETH_NODE, provider_uri=provider_uri, checksum_address=staker_address, worker_address=worker_address, federated_only=federated_only, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath) else: try: ursula_config = UrsulaConfiguration.from_configuration_file( filepath=config_file, domains={network} if network else None, registry_filepath=registry_filepath, provider_process=ETH_NODE, provider_uri=provider_uri, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath, poa=poa, federated_only=federated_only) except FileNotFoundError: return actions.handle_missing_configuration_file( character_config_class=UrsulaConfiguration, config_file=config_file) except NucypherKeyring.AuthenticationFailed as e: emitter.echo(str(e), color='red', bold=True) # TODO: Exit codes (not only for this, but for other exceptions) return click.get_current_context().exit(1) return ursula_config, provider_uri
def create_config(self, emitter, config_file): if self.dev: return UrsulaConfiguration( emitter=emitter, dev_mode=True, domains={TEMPORARY_DOMAIN}, poa=self.poa, light=self.light, registry_filepath=self.registry_filepath, provider_process=self.eth_node, provider_uri=self.provider_uri, signer_uri=self.signer_uri, gas_strategy=self.gas_strategy, checksum_address=self.worker_address, federated_only=self.federated_only, rest_host=self.rest_host, rest_port=self.rest_port, db_filepath=self.db_filepath, availability_check=self.availability_check) else: try: return UrsulaConfiguration.from_configuration_file( emitter=emitter, filepath=config_file, domains=self.domains, registry_filepath=self.registry_filepath, provider_process=self.eth_node, provider_uri=self.provider_uri, signer_uri=self.signer_uri, gas_strategy=self.gas_strategy, rest_host=self.rest_host, rest_port=self.rest_port, db_filepath=self.db_filepath, poa=self.poa, light=self.light, federated_only=self.federated_only, availability_check=self.availability_check) except FileNotFoundError: return handle_missing_configuration_file( character_config_class=UrsulaConfiguration, config_file=config_file) except NucypherKeyring.AuthenticationFailed as e: emitter.echo(str(e), color='red', bold=True) # TODO: Exit codes (not only for this, but for other exceptions) return click.get_current_context().exit(1)
def status(click_config, config_file): """ Echo a snapshot of live network metadata. """ # # Initialize # ursula_config = UrsulaConfiguration.from_configuration_file( filepath=config_file) if not ursula_config.federated_only: ursula_config.connect_to_blockchain( provider_uri=ursula_config.provider_uri) ursula_config.connect_to_contracts() # Contracts paint_contract_status(ursula_config=ursula_config, click_config=click_config) # Known Nodes paint_known_nodes(ursula=ursula_config)
def status(click_config, config_file): """ Echo a snapshot of live network metadata. """ # # Initialize # ursula_config = UrsulaConfiguration.from_configuration_file( filepath=config_file) if not ursula_config.federated_only: ursula_config.get_blockchain_interface( provider_uri=ursula_config.provider_uri) ursula_config.acquire_agency() # Contracts paint_contract_status(click_config.emitter, ursula_config=ursula_config, click_config=click_config) # Known Nodes paint_known_nodes(emitter=click_config.emitter, ursula=ursula_config)
def test_ursula_init_with_local_keystore_signer( click_runner, temp_dir_path, mocker, mock_testerchain, mock_account_password_keystore): custom_filepath = temp_dir_path custom_config_filepath = temp_dir_path / UrsulaConfiguration.generate_filename( ) worker_account, password, mock_keystore_path = mock_account_password_keystore mock_signer_uri = f'keystore://{mock_keystore_path}' # Good signer... pre_config_signer = KeystoreSigner.from_signer_uri(uri=mock_signer_uri, testnet=True) deploy_port = select_test_port() init_args = ( 'ursula', 'init', # Layer 1 '--network', TEMPORARY_DOMAIN, '--eth-provider', mock_testerchain.eth_provider_uri, # Layer 2 '--payment-network', TEMPORARY_DOMAIN, '--payment-provider', mock_testerchain.eth_provider_uri, '--rest-host', MOCK_IP_ADDRESS, '--rest-port', deploy_port, '--operator-address', worker_account.address, '--config-root', str(custom_filepath.absolute()), # The bit we are testing here '--signer', mock_signer_uri) cli_env = { NUCYPHER_ENVVAR_KEYSTORE_PASSWORD: password, NUCYPHER_ENVVAR_OPERATOR_ETH_PASSWORD: password, } result = click_runner.invoke(nucypher_cli, init_args, catch_exceptions=False, env=cli_env) assert result.exit_code == 0, result.stdout # Inspect the configuration file for the signer URI with open(custom_config_filepath, 'r') as config_file: raw_config_data = config_file.read() config_data = json.loads(raw_config_data) assert config_data['signer_uri'] == mock_signer_uri,\ "Keystore URI was not correctly included in configuration file" # Recreate a configuration with the signer URI preserved ursula_config = UrsulaConfiguration.from_configuration_file( custom_config_filepath, config_root=custom_filepath) assert ursula_config.signer_uri == mock_signer_uri # Mock decryption of web3 client keystore mocker.patch.object(Account, 'decrypt', return_value=worker_account.key) ursula_config.keystore.unlock(password=password) # Produce an ursula with a Keystore signer correctly derived from the signer URI, and don't do anything else! ursula = ursula_config.produce() ursula.signer.unlock_account(account=worker_account.address, password=password) # Verify the keystore path is still preserved assert isinstance(ursula.signer, KeystoreSigner) assert isinstance(ursula.signer.path, Path), "Use pathlib.Path" assert ursula.signer.path.absolute() == mock_keystore_path.absolute() # Show that we can produce the exact same signer as pre-config... assert pre_config_signer.path == ursula.signer.path ursula.stop()
def ursula(config, action, rest_port, rest_host, db_name, checksum_address, debug, teacher_uri, min_stake) -> None: """ Manage and run an Ursula node Here is the procedure to "spin-up" an Ursula node. \b 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) """ log = Logger("ursula/launch") password = os.environ.get(config._KEYRING_PASSPHRASE_ENVVAR, None) if not password: password = click.prompt("Password to unlock Ursula's keyring", hide_input=True) 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( message="No account specified. pass --checksum-address, --dev, " "or use a configuration file with --config-file <path>") 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, start_learning_now=True, learn_on_same_thread=False, abort_on_learning_error=config.dev) # # Configure # overrides = dict() if config.dev: ursula_config = __make_ursula() else: try: 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') # # Seed # teacher_nodes = list() if teacher_uri: if '@' in teacher_uri: checksum_address, teacher_uri = teacher_uri.split("@") if not is_checksum_address(checksum_address): raise click.BadParameter( "{} is not a valid checksum address.".format( checksum_address)) else: checksum_address = None # federated # HTTPS Explicit Required parsed_teacher_uri = urlparse(teacher_uri) if not parsed_teacher_uri.scheme == "https": raise click.BadParameter( "Invalid teacher URI. Is the hostname prefixed with 'https://' ?" ) port = parsed_teacher_uri.port or UrsulaConfiguration.DEFAULT_REST_PORT while not teacher_nodes: try: teacher = Ursula.from_seed_and_stake_info( host=parsed_teacher_uri.hostname, port=port, federated_only=ursula_config.federated_only, checksum_address=checksum_address, minimum_stake=min_stake, certificates_directory=ursula_config.known_certificates_dir ) teacher_nodes.append(teacher) except (socket.gaierror, requests.exceptions.ConnectionError, ConnectionRefusedError): log.warn("Can't connect to seed node. Will retry.") time.sleep(5) # # Produce # try: URSULA = ursula_config.produce(passphrase=password, known_nodes=teacher_nodes, **overrides) # 2 except CryptoError: click.secho("Invalid keyring passphrase") return click.secho("Initialized Ursula {}".format(URSULA), fg='green') # # Run # if action == 'run': try: # GO! click.secho("Running Ursula on {}".format(URSULA.rest_interface), fg='green', bold=True) stdio.StandardIO(UrsulaCommandProtocol(ursula=URSULA)) URSULA.get_deployer().run() 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
def ursula( click_config, action, dev, dry_run, force, lonely, network, teacher_uri, min_stake, rest_host, rest_port, db_filepath, staker_address, worker_address, federated_only, poa, config_root, config_file, provider_uri, geth, registry_filepath, interactive, sync, ) -> None: """ "Ursula the Untrusted" PRE Re-encryption node management commands. \b Actions ------------------------------------------------- \b init Create a new Ursula node configuration. view View the Ursula node's configuration. run Run an "Ursula" node. save-metadata Manually write node metadata to disk without running forget Forget all known nodes. destroy Delete Ursula node configuration. confirm-activity Manually confirm-activity for the current period. """ emitter = click_config.emitter # # Validate # if federated_only: if geth: raise click.BadOptionUsage( option_name="--geth", message="Federated only cannot be used with the --geth flag") if staker_address: raise click.BadOptionUsage( option_name='--federated-only', message="Staking address cannot be used in federated mode.") # Banner emitter.banner(URSULA_BANNER.format(worker_address or '')) # # Pre-Launch Warnings # if dev: emitter.echo("WARNING: Running in Development mode", color='yellow', verbosity=1) if force: emitter.echo("WARNING: Force is enabled", color='yellow', verbosity=1) # # Internal Ethereum Client # ETH_NODE = NO_BLOCKCHAIN_CONNECTION if geth: ETH_NODE = actions.get_provider_process() provider_uri = ETH_NODE.provider_uri(scheme='file') # # Eager Actions # if action == "init": """Create a brand-new persistent Ursula""" if dev: raise click.BadArgumentUsage( "Cannot create a persistent development character") if (not staker_address or not worker_address) and not federated_only: # Connect to Blockchain fetch_registry = registry_filepath is None and not click_config.no_registry registry = None if registry_filepath: registry = EthereumContractRegistry( registry_filepath=registry_filepath) blockchain = BlockchainInterface(provider_uri=provider_uri, registry=registry, poa=poa) blockchain.connect(fetch_registry=fetch_registry, sync_now=sync, emitter=emitter) if not staker_address: prompt = "Select staker account" staker_address = select_client_account(emitter=emitter, blockchain=blockchain, prompt=prompt) if not worker_address: prompt = "Select worker account" worker_address = select_client_account(emitter=emitter, blockchain=blockchain, prompt=prompt) if not config_root: # Flag config_root = click_config.config_file # Envvar if not rest_host: rest_host = actions.determine_external_ip_address(emitter, force=force) download_registry = not federated_only and not click_config.no_registry ursula_config = UrsulaConfiguration.generate( password=get_nucypher_password(confirm=True), config_root=config_root, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath, domains={network} if network else None, federated_only=federated_only, checksum_address=staker_address, worker_address=worker_address, download_registry=download_registry, registry_filepath=registry_filepath, provider_process=ETH_NODE, provider_uri=provider_uri, poa=poa) painting.paint_new_installation_help(emitter, new_configuration=ursula_config) return # # Make Ursula # if dev: ursula_config = UrsulaConfiguration( dev_mode=True, domains={TEMPORARY_DOMAIN}, poa=poa, download_registry=False, registry_filepath=registry_filepath, provider_process=ETH_NODE, provider_uri=provider_uri, checksum_address=staker_address, worker_address=worker_address, federated_only=federated_only, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath) else: try: ursula_config = UrsulaConfiguration.from_configuration_file( filepath=config_file, domains={network} if network else None, registry_filepath=registry_filepath, provider_process=ETH_NODE, provider_uri=provider_uri, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath, poa=poa, federated_only=federated_only) except FileNotFoundError: return actions.handle_missing_configuration_file( character_config_class=UrsulaConfiguration, config_file=config_file) except NucypherKeyring.AuthenticationFailed as e: emitter.echo(str(e), color='red', bold=True) click.get_current_context().exit(1) # TODO: Exit codes (not only for this, but for other exceptions) # # Configured Pre-Authentication Actions # # Handle destruction and forget *before* network bootstrap and character initialization below if action == "destroy": """Delete all configuration files from the disk""" if dev: message = "'nucypher ursula destroy' cannot be used in --dev mode - There is nothing to destroy." raise click.BadOptionUsage(option_name='--dev', message=message) actions.destroy_configuration(emitter, character_config=ursula_config, force=force) return elif action == "forget": actions.forget(emitter, configuration=ursula_config) return # # Make Ursula # client_password = None if not ursula_config.federated_only: if not dev and not click_config.json_ipc: client_password = get_client_password( checksum_address=ursula_config.worker_address, envvar="NUCYPHER_WORKER_ETH_PASSWORD") try: URSULA = actions.make_cli_character(character_config=ursula_config, click_config=click_config, min_stake=min_stake, teacher_uri=teacher_uri, dev=dev, lonely=lonely, client_password=client_password) except NucypherKeyring.AuthenticationFailed as e: emitter.echo(str(e), color='red', bold=True) click.get_current_context().exit(1) # TODO: Exit codes (not only for this, but for other exceptions) # # Authenticated Action Switch # if action == 'run': """Seed, Produce, Run!""" # GO! try: # Ursula Deploy Warnings emitter.message(f"Starting Ursula on {URSULA.rest_interface}", color='green', bold=True) emitter.message(f"Connecting to {','.join(ursula_config.domains)}", color='green', bold=True) emitter.message("Working ~ Keep Ursula Online!", color='blue', bold=True) if interactive: stdio.StandardIO( UrsulaCommandProtocol(ursula=URSULA, emitter=emitter)) if dry_run: return # <-- ABORT - (Last Chance) # Run - Step 3 node_deployer = URSULA.get_deployer() node_deployer.addServices() node_deployer.catalogServers(node_deployer.hendrix) node_deployer.run() # <--- Blocking Call (Reactor) # Handle Crash except Exception as e: ursula_config.log.critical(str(e)) emitter.message(f"{e.__class__.__name__} {e}", color='red', bold=True) raise # Crash :-( # Graceful Exit finally: emitter.message("Stopping Ursula", color='green') ursula_config.cleanup() emitter.message("Ursula Stopped", color='red') return elif action == "save-metadata": """Manually save a node self-metadata file""" metadata_path = ursula.write_node_metadata(node=URSULA) emitter.message( f"Successfully saved node metadata to {metadata_path}.", color='green') return elif action == "view": """Paint an existing configuration to the console""" if not URSULA.federated_only: emitter.echo("BLOCKCHAIN ----------\n") painting.paint_contract_status(emitter=emitter, blockchain=URSULA.blockchain) current_block = URSULA.blockchain.w3.eth.blockNumber emitter.echo(f'Block # {current_block}') # TODO: 1231 emitter.echo(f'NU Balance (staker): {URSULA.token_balance}') emitter.echo( f'ETH Balance (worker): {URSULA.blockchain.client.get_balance(URSULA.worker_address)}' ) emitter.echo( f'Current Gas Price {URSULA.blockchain.client.gas_price}') emitter.echo("CONFIGURATION --------") response = UrsulaConfiguration._read_configuration_file( filepath=config_file or ursula_config.config_file_location) return emitter.ipc( response=response, request_id=0, duration=0 ) # FIXME: #1216 - what are request_id and duration here? elif action == 'confirm-activity': receipt = URSULA.confirm_activity() confirmed_period = URSULA.staking_agent.get_current_period() + 1 date = datetime_at_period(period=confirmed_period) # TODO: Double-check dates here emitter.echo( f'\nActivity confirmed for period #{confirmed_period} ' f'(starting at {date})', bold=True, color='blue') painting.paint_receipt_summary( emitter=emitter, receipt=receipt, chain_name=URSULA.blockchain.client.chain_name) # TODO: Check ActivityConfirmation event (see #1193) return else: raise click.BadArgumentUsage("No such argument {}".format(action))
def test_ursula_and_local_keystore_signer_integration( click_runner, tmp_path, staking_providers, application_economics, mocker, mock_funded_account_password_keystore, testerchain): config_root_path = tmp_path ursula_config_path = config_root_path / UrsulaConfiguration.generate_filename( ) worker_account, password, mock_keystore_path = mock_funded_account_password_keystore # # Operator Steps # # Good signer... mock_signer_uri = f'keystore:{mock_keystore_path}' pre_config_signer = KeystoreSigner.from_signer_uri(uri=mock_signer_uri, testnet=True) assert worker_account.address in pre_config_signer.accounts deploy_port = select_test_port() init_args = ( 'ursula', 'init', '--network', TEMPORARY_DOMAIN, '--payment-network', TEMPORARY_DOMAIN, '--operator-address', worker_account.address, '--config-root', str(config_root_path.absolute()), '--eth-provider', TEST_ETH_PROVIDER_URI, '--payment-provider', TEST_POLYGON_PROVIDER_URI, '--rest-host', MOCK_IP_ADDRESS, '--rest-port', deploy_port, # The bit we are testing for here '--signer', mock_signer_uri) cli_env = { NUCYPHER_ENVVAR_KEYSTORE_PASSWORD: password, NUCYPHER_ENVVAR_OPERATOR_ETH_PASSWORD: password, } result = click_runner.invoke(nucypher_cli, init_args, catch_exceptions=False, env=cli_env) assert result.exit_code == 0, result.stdout # Inspect the configuration file for the signer URI with open(ursula_config_path, 'r') as config_file: raw_config_data = config_file.read() config_data = json.loads(raw_config_data) assert config_data['signer_uri'] == mock_signer_uri,\ "Keystore URI was not correctly included in configuration file" # Recreate a configuration with the signer URI preserved ursula_config = UrsulaConfiguration.from_configuration_file( ursula_config_path) assert ursula_config.signer_uri == mock_signer_uri # Mock decryption of web3 client keystore mocker.patch.object(Account, 'decrypt', return_value=worker_account.key) ursula_config.keystore.unlock(password=password) # Produce an Ursula with a Keystore signer correctly derived from the signer URI, and don't do anything else! ursula = ursula_config.produce() ursula.signer.unlock_account(account=worker_account.address, password=password) try: # Verify the keystore path is still preserved assert isinstance(ursula.signer, KeystoreSigner) assert isinstance(ursula.signer.path, Path), "Use Path" assert ursula.signer.path.absolute() == mock_keystore_path.absolute() # Show that we can produce the exact same signer as pre-config... assert pre_config_signer.path == ursula.signer.path # ...and that transactions are signed by the keystore signer txhash = ursula.confirm_address() receipt = testerchain.wait_for_receipt(txhash) transaction_data = testerchain.client.w3.eth.getTransaction( receipt['transactionHash']) assert transaction_data['from'] == worker_account.address finally: ursula.stop()
def test_ursula_init_with_local_keystore_signer(click_runner, custom_filepath, custom_config_filepath, agency_local_registry, worker_account, mocker, testerchain): # Good signer... pre_config_signer = KeystoreSigner.from_signer_uri(uri=MOCK_SIGNER_URI) assert worker_account.address in pre_config_signer.accounts init_args = ( 'ursula', 'init', '--network', TEMPORARY_DOMAIN, '--worker-address', worker_account.address, '--config-root', custom_filepath, '--provider', TEST_PROVIDER_URI, '--registry-filepath', agency_local_registry.filepath, '--rest-host', MOCK_IP_ADDRESS, '--rest-port', MOCK_URSULA_STARTING_PORT, # The bit were' testing here '--signer', MOCK_SIGNER_URI) result = click_runner.invoke(nucypher_cli, init_args, catch_exceptions=False, env=CLI_ENV) assert result.exit_code == 0, result.stdout # Inspect the configuration file for the signer URI with open(custom_config_filepath, 'r') as config_file: raw_config_data = config_file.read() config_data = json.loads(raw_config_data) assert config_data['signer_uri'] == MOCK_SIGNER_URI,\ "Keystore URI was not correctly included in configuration file" # Recreate a configuration with the signer URI preserved ursula_config = UrsulaConfiguration.from_configuration_file( custom_config_filepath) assert ursula_config.signer_uri == MOCK_SIGNER_URI # Mock decryption of web3 client keyring mocker.patch.object(Account, 'decrypt', return_value=worker_account.privateKey) ursula_config.attach_keyring(checksum_address=worker_account.address) ursula_config.keyring.unlock(password=INSECURE_DEVELOPMENT_PASSWORD) # Produce an ursula with a Keystore signer correctly derived from the signer URI, and dont do anything else! mocker.patch.object(StakeList, 'refresh', autospec=True) ursula = ursula_config.produce( client_password=INSECURE_DEVELOPMENT_PASSWORD, block_until_ready=False) # Verify the keystore path is still preserved assert isinstance(ursula.signer, KeystoreSigner) assert ursula.signer.path == MOCK_KEYSTORE_PATH # Show that we can produce the exact same signer as pre-config... assert pre_config_signer.path == ursula.signer.path # ...and that transactions are signed by the keytore signer receipt = ursula.confirm_activity() transaction_data = testerchain.client.w3.eth.getTransaction( receipt['transactionHash']) assert transaction_data['from'] == worker_account.address
def ursula(click_config, action, dev, quiet, dry_run, force, lonely, network, teacher_uri, min_stake, rest_host, rest_port, db_filepath, checksum_address, withdraw_address, federated_only, poa, config_root, config_file, provider_uri, recompile_solidity, no_registry, registry_filepath, value, duration, index, list_, divide ) -> None: """ Manage and run an "Ursula" PRE node. \b Actions ------------------------------------------------- \b init Create a new Ursula node configuration. view View the Ursula node's configuration. run Run an "Ursula" node. save-metadata Manually write node metadata to disk without running forget Forget all known nodes. destroy Delete Ursula node configuration. stake Manage stakes for this node. confirm-activity Manually confirm-activity for the current period. collect-reward Withdraw staking reward. """ # # Boring Setup Stuff # if not quiet: log = Logger('ursula.cli') if click_config.debug and quiet: raise click.BadOptionUsage(option_name="quiet", message="--debug and --quiet cannot be used at the same time.") if not click_config.json_ipc and not click_config.quiet: click.secho(URSULA_BANNER.format(checksum_address or '')) # # Pre-Launch Warnings # if not click_config.quiet: if dev: click.secho("WARNING: Running in Development mode", fg='yellow') if force: click.secho("WARNING: Force is enabled", fg='yellow') # # Unauthenticated Configurations & Un-configured Ursula Control # if action == "init": """Create a brand-new persistent Ursula""" if not network: raise click.BadArgumentUsage('--network is required to initialize a new configuration.') if dev: click_config.emitter(message="WARNING: Using temporary storage area", color='yellow') if not config_root: # Flag config_root = click_config.config_file # Envvar if not rest_host: rest_host = click.prompt("Enter Ursula's public-facing IPv4 address") # TODO: Remove this step ursula_config = UrsulaConfiguration.generate(password=click_config._get_password(confirm=True), config_root=config_root, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath, domains={network} if network else None, federated_only=federated_only, checksum_public_address=checksum_address, no_registry=federated_only or no_registry, registry_filepath=registry_filepath, provider_uri=provider_uri, poa=poa) painting.paint_new_installation_help(new_configuration=ursula_config, config_root=config_root, config_file=config_file, federated_only=federated_only) return # # Configured Ursulas # # Development Configuration if dev: ursula_config = UrsulaConfiguration(dev_mode=True, domains={TEMPORARY_DOMAIN}, poa=poa, registry_filepath=registry_filepath, provider_uri=provider_uri, checksum_public_address=checksum_address, federated_only=federated_only, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath) # Authenticated Configurations else: # Domains -> bytes | or default domains = [bytes(network, encoding='utf-8')] if network else None # Load Ursula from Configuration File ursula_config = UrsulaConfiguration.from_configuration_file(filepath=config_file, domains=domains, registry_filepath=registry_filepath, provider_uri=provider_uri, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath, poa=poa) click_config.unlock_keyring(character_configuration=ursula_config) # # Connect to Blockchain (Non-Federated) # if not ursula_config.federated_only: click_config.connect_to_blockchain(character_configuration=ursula_config, recompile_contracts=recompile_solidity) click_config.ursula_config = ursula_config # Pass Ursula's config onto staking sub-command # # Launch Warnings # if ursula_config.federated_only: click_config.emitter(message="WARNING: Running in Federated mode", color='yellow') # Seed - Step 1 teacher_uris = [teacher_uri] if teacher_uri else list() teacher_nodes = actions.load_seednodes(teacher_uris=teacher_uris, min_stake=min_stake, federated_only=federated_only, network_middleware=click_config.middleware) # Produce - Step 2 URSULA = ursula_config(known_nodes=teacher_nodes, lonely=lonely) # # Action Switch # if action == 'run': """Seed, Produce, Run!""" # GO! try: click_config.emitter( message="Starting Ursula on {}".format(URSULA.rest_interface), color='green', bold=True) # Ursula Deploy Warnings click_config.emitter( message="Connecting to {}".format(','.join(str(d, encoding='utf-8') for d in ursula_config.domains)), color='green', bold=True) if not URSULA.federated_only and URSULA.stakes: click_config.emitter( message=f"Staking {str(URSULA.total_staked)} ~ Keep Ursula Online!", color='blue', bold=True) if not click_config.debug: stdio.StandardIO(UrsulaCommandProtocol(ursula=URSULA)) if dry_run: return # <-- ABORT -X (Last Chance) # Run - Step 3 node_deployer = URSULA.get_deployer() node_deployer.addServices() node_deployer.catalogServers(node_deployer.hendrix) node_deployer.run() # <--- Blocking Call (Reactor) # Handle Crash except Exception as e: ursula_config.log.critical(str(e)) click_config.emitter( message="{} {}".format(e.__class__.__name__, str(e)), color='red', bold=True) raise # Crash :-( # Graceful Exit / Crash finally: click_config.emitter(message="Stopping Ursula", color='green') ursula_config.cleanup() click_config.emitter(message="Ursula Stopped", color='red') return elif action == "save-metadata": """Manually save a node self-metadata file""" metadata_path = ursula.write_node_metadata(node=URSULA) return click_config.emitter(message="Successfully saved node metadata to {}.".format(metadata_path), color='green') elif action == "view": """Paint an existing configuration to the console""" response = UrsulaConfiguration._read_configuration_file(filepath=config_file or ursula_config.config_file_location) return click_config.emitter(response=response) elif action == "forget": actions.forget(configuration=ursula_config) return elif action == "destroy": """Delete all configuration files from the disk""" if dev: message = "'nucypher ursula destroy' cannot be used in --dev mode" raise click.BadOptionUsage(option_name='--dev', message=message) destroyed_filepath = destroy_system_configuration(config_class=UrsulaConfiguration, config_file=config_file, network=network, config_root=ursula_config.config_file_location, force=force) return click_config.emitter(message=f"Destroyed {destroyed_filepath}", color='green') elif action == 'stake': # List Only if list_: if not URSULA.stakes: click.echo(f"There are no existing stakes for {URSULA.checksum_public_address}") painting.paint_stakes(stakes=URSULA.stakes) return # Divide Only if divide: """Divide an existing stake by specifying the new target value and end period""" # Validate if len(URSULA.stakes) == 0: click.secho("There are no active stakes for {}".format(URSULA.checksum_public_address)) return # Selection if index is None: painting.paint_stakes(stakes=URSULA.stakes) index = click.prompt("Select a stake to divide", type=click.IntRange(min=0, max=len(URSULA.stakes)-1)) # Lookup the stake current_stake = URSULA.stakes[index] # Value if not value: value = click.prompt(f"Enter target value (must be less than {str(current_stake.value)})", type=STAKE_VALUE) value = NU(value, 'NU') # Duration if not duration: extension = click.prompt("Enter number of periods to extend", type=STAKE_EXTENSION) else: extension = duration if not force: painting.paint_staged_stake_division(ursula=URSULA, original_index=index, original_stake=current_stake, target_value=value, extension=extension) click.confirm("Is this correct?", abort=True) txhash_bytes = URSULA.divide_stake(stake_index=index, target_value=value, additional_periods=extension) if not quiet: click.secho('Successfully divided stake', fg='green') click.secho(f'Transaction Hash ........... {txhash_bytes.hex()}') # Show the resulting stake list painting.paint_stakes(stakes=URSULA.stakes) return # Confirm new stake init if not force: click.confirm("Stage a new stake?", abort=True) # Validate balance balance = URSULA.token_balance if balance == 0: click.secho(f"{ursula.checksum_public_address} has 0 NU.") raise click.Abort if not quiet: click.echo(f"Current balance: {balance}") # Gather stake value if not value: value = click.prompt(f"Enter stake value", type=STAKE_VALUE, default=NU(MIN_ALLOWED_LOCKED, 'NuNit')) else: value = NU(int(value), 'NU') # Duration if not quiet: message = "Minimum duration: {} | Maximum Duration: {}".format(MIN_LOCKED_PERIODS, MAX_MINTING_PERIODS) click.echo(message) if not duration: duration = click.prompt("Enter stake duration in periods (1 Period = 24 Hours)", type=STAKE_DURATION) start_period = URSULA.miner_agent.get_current_period() end_period = start_period + duration # Review if not force: painting.paint_staged_stake(ursula=URSULA, stake_value=value, duration=duration, start_period=start_period, end_period=end_period) if not dev: actions.confirm_staged_stake(ursula=URSULA, value=value, duration=duration) # Last chance to bail if not force: click.confirm("Publish staged stake to the blockchain?", abort=True) staking_transactions = URSULA.initialize_stake(amount=int(value), lock_periods=duration) painting.paint_staking_confirmation(ursula=URSULA, transactions=staking_transactions) return elif action == 'confirm-activity': if not URSULA.stakes: click.secho("There are no active stakes for {}".format(URSULA.checksum_public_address)) return URSULA.miner_agent.confirm_activity(node_address=URSULA.checksum_public_address) return elif action == 'collect-reward': """Withdraw staking reward to the specified wallet address""" if not force: click.confirm(f"Send {URSULA.calculate_reward()} to {URSULA.checksum_public_address}?") URSULA.collect_policy_reward(collector_address=withdraw_address or checksum_address) URSULA.collect_staking_reward() else: raise click.BadArgumentUsage("No such argument {}".format(action))
def ursula( click_config, action, debug, dev, quiet, dry_run, force, lonely, network, teacher_uri, min_stake, rest_host, rest_port, db_filepath, checksum_address, federated_only, poa, config_root, config_file, metadata_dir, # TODO: Start nodes from an additional existing metadata dir provider_uri, no_registry, registry_filepath) -> None: """ Manage and run an "Ursula" PRE node. \b Actions ------------------------------------------------- \b run Run an "Ursula" node. init Create a new Ursula node configuration. view View the Ursula node's configuration. forget Forget all known nodes. save-metadata Manually write node metadata to disk without running destroy Delete Ursula node configuration. """ # # Boring Setup Stuff # if not quiet: click.secho(URSULA_BANNER) log = Logger('ursula.cli') if debug and quiet: raise click.BadOptionUsage( option_name="quiet", message="--debug and --quiet cannot be used at the same time.") if debug: click_config.log_to_sentry = False click_config.log_to_file = True globalLogPublisher.removeObserver(logToSentry) # Sentry GlobalConsoleLogger.set_log_level("debug") elif quiet: globalLogPublisher.removeObserver(logToSentry) globalLogPublisher.removeObserver(SimpleObserver) globalLogPublisher.removeObserver(getJsonFileObserver()) # # Pre-Launch Warnings # if not quiet: if dev: click.secho("WARNING: Running in development mode", fg='yellow') if force: click.secho("WARNING: Force is enabled", fg='yellow') # # Unauthenticated Configurations # if action == "init": """Create a brand-new persistent Ursula""" if dev and not quiet: click.secho("WARNING: Using temporary storage area", fg='yellow') if not config_root: # Flag config_root = click_config.config_file # Envvar if not rest_host: rest_host = click.prompt( "Enter Ursula's public-facing IPv4 address") ursula_config = UrsulaConfiguration.generate( password=click_config.get_password(confirm=True), config_root=config_root, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath, domains={network} if network else None, federated_only=federated_only, checksum_public_address=checksum_address, no_registry=federated_only or no_registry, registry_filepath=registry_filepath, provider_uri=provider_uri, poa=poa) if not quiet: click.secho("Generated keyring {}".format( ursula_config.keyring_dir), fg='green') click.secho("Saved configuration file {}".format( ursula_config.config_file_location), fg='green') # Give the use a suggestion as to what to do next... how_to_run_message = "\nTo run an Ursula node from the default configuration filepath run: \n\n'{}'\n" suggested_command = 'nucypher ursula run' if config_root is not None: config_file_location = os.path.join( config_root, config_file or UrsulaConfiguration.CONFIG_FILENAME) suggested_command += ' --config-file {}'.format( config_file_location) click.secho(how_to_run_message.format(suggested_command), fg='green') return # FIN else: click.secho("OK") elif action == "destroy": """Delete all configuration files from the disk""" if dev: message = "'nucypher ursula destroy' cannot be used in --dev mode" raise click.BadOptionUsage(option_name='--dev', message=message) destroy_system_configuration(config_class=UrsulaConfiguration, config_file=config_file, network=network, config_root=config_root, force=force, log=log) if not quiet: click.secho("Destroyed {}".format(config_root)) return # Development Configuration if dev: ursula_config = UrsulaConfiguration( dev_mode=True, domains={TEMPORARY_DOMAIN}, poa=poa, registry_filepath=registry_filepath, provider_uri=provider_uri, checksum_public_address=checksum_address, federated_only=federated_only, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath) # Authenticated Configurations else: # Deserialize network domain name if override passed if network: domain_constant = getattr(constants, network.upper()) domains = {domain_constant} else: domains = None ursula_config = UrsulaConfiguration.from_configuration_file( filepath=config_file, domains=domains, registry_filepath=registry_filepath, provider_uri=provider_uri, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath, # TODO: Handle Boolean overrides # poa=poa, # federated_only=federated_only, ) try: # Unlock Keyring if not quiet: click.secho('Decrypting keyring...', fg='blue') ursula_config.keyring.unlock(password=click_config.get_password() ) # Takes ~3 seconds, ~1GB Ram except CryptoError: raise ursula_config.keyring.AuthenticationFailed if not ursula_config.federated_only: try: ursula_config.connect_to_blockchain(recompile_contracts=False) ursula_config.connect_to_contracts() except EthereumContractRegistry.NoRegistry: message = "Cannot configure blockchain character: No contract registry found; " \ "Did you mean to pass --federated-only?" raise EthereumContractRegistry.NoRegistry(message) click_config.ursula_config = ursula_config # Pass Ursula's config onto staking sub-command # # Launch Warnings # if not quiet: if ursula_config.federated_only: click.secho("WARNING: Running in Federated mode", fg='yellow') # # Action Switch # if action == 'run': """Seed, Produce, Run!""" # # Seed - Step 1 # teacher_nodes = list() if teacher_uri: node = Ursula.from_teacher_uri( teacher_uri=teacher_uri, min_stake=min_stake, federated_only=ursula_config.federated_only) teacher_nodes.append(node) # # Produce - Step 2 # ursula = ursula_config(known_nodes=teacher_nodes, lonely=lonely) # GO! try: # # Run - Step 3 # click.secho("Connecting to {}".format(','.join( str(d) for d in ursula_config.domains)), fg='blue', bold=True) click.secho("Running Ursula {} on {}".format( ursula, ursula.rest_interface), fg='green', bold=True) if not debug: stdio.StandardIO(UrsulaCommandProtocol(ursula=ursula)) if dry_run: # That's all folks! return ursula.get_deployer().run() # <--- Blocking Call (Reactor) except Exception as e: ursula_config.log.critical(str(e)) click.secho("{} {}".format(e.__class__.__name__, str(e)), fg='red') raise # Crash :-( finally: if not quiet: click.secho("Stopping Ursula") ursula_config.cleanup() if not quiet: click.secho("Ursula Stopped", fg='red') return elif action == "save-metadata": """Manually save a node self-metadata file""" ursula = ursula_config.produce(ursula_config=ursula_config) metadata_path = ursula.write_node_metadata(node=ursula) if not quiet: click.secho("Successfully saved node metadata to {}.".format( metadata_path), fg='green') return elif action == "view": """Paint an existing configuration to the console""" paint_configuration( config_filepath=config_file or ursula_config.config_file_location) return elif action == "forget": """Forget all known nodes via storages""" click.confirm("Permanently delete all known node data?", abort=True) ursula_config.forget_nodes() message = "Removed all stored node node metadata and certificates" click.secho(message=message, fg='red') return else: raise click.BadArgumentUsage("No such argument {}".format(action))
def test_ursula_and_local_keystore_signer_integration( click_runner, tmp_path, manual_staker, stake_value, token_economics, mocker, mock_funded_account_password_keystore, testerchain): config_root_path = tmp_path ursula_config_path = config_root_path / UrsulaConfiguration.generate_filename( ) stakeholder_config_path = config_root_path / StakeHolderConfiguration.generate_filename( ) worker_account, password, mock_keystore_path = mock_funded_account_password_keystore # # Stakeholder Steps # init_args = ('stake', 'init-stakeholder', '--config-root', config_root_path, '--provider', TEST_PROVIDER_URI, '--network', TEMPORARY_DOMAIN) click_runner.invoke(nucypher_cli, init_args, catch_exceptions=False) stake_args = ('stake', 'create', '--config-file', stakeholder_config_path, '--staking-address', manual_staker, '--value', stake_value.to_tokens(), '--lock-periods', token_economics.minimum_locked_periods, '--force') # TODO: Is This test is writing to the default system directory and ignoring updates to the passed filepath? user_input = f'0\n{password}\nY\n' click_runner.invoke(nucypher_cli, stake_args, input=user_input, catch_exceptions=False) init_args = ('stake', 'bond-worker', '--config-file', stakeholder_config_path, '--staking-address', manual_staker, '--worker-address', worker_account.address, '--force') user_input = password click_runner.invoke(nucypher_cli, init_args, input=user_input, catch_exceptions=False) # # Worker Steps # # Good signer... mock_signer_uri = f'keystore:{mock_keystore_path}' pre_config_signer = KeystoreSigner.from_signer_uri(uri=mock_signer_uri, testnet=True) assert worker_account.address in pre_config_signer.accounts deploy_port = select_test_port() init_args = ( 'ursula', 'init', '--network', TEMPORARY_DOMAIN, '--worker-address', worker_account.address, '--config-root', config_root_path, '--provider', TEST_PROVIDER_URI, '--rest-host', MOCK_IP_ADDRESS, '--rest-port', deploy_port, # The bit we are testing for here '--signer', mock_signer_uri) cli_env = { NUCYPHER_ENVVAR_KEYRING_PASSWORD: password, NUCYPHER_ENVVAR_WORKER_ETH_PASSWORD: password, } result = click_runner.invoke(nucypher_cli, init_args, catch_exceptions=False, env=cli_env) assert result.exit_code == 0, result.stdout # Inspect the configuration file for the signer URI with open(ursula_config_path, 'r') as config_file: raw_config_data = config_file.read() config_data = json.loads(raw_config_data) assert config_data['signer_uri'] == mock_signer_uri,\ "Keystore URI was not correctly included in configuration file" # Recreate a configuration with the signer URI preserved ursula_config = UrsulaConfiguration.from_configuration_file( ursula_config_path) assert ursula_config.signer_uri == mock_signer_uri # Mock decryption of web3 client keyring mocker.patch.object(Account, 'decrypt', return_value=worker_account.privateKey) ursula_config.attach_keyring(checksum_address=worker_account.address) ursula_config.keyring.unlock(password=password) # Produce an Ursula with a Keystore signer correctly derived from the signer URI, and don't do anything else! mocker.patch.object(StakeList, 'refresh', autospec=True) ursula = ursula_config.produce(client_password=password, commit_now=False, block_until_ready=False) try: # Verify the keystore path is still preserved assert isinstance(ursula.signer, KeystoreSigner) assert isinstance(ursula.signer.path, str), "Use str" assert ursula.signer.path == str(mock_keystore_path) # Show that we can produce the exact same signer as pre-config... assert pre_config_signer.path == ursula.signer.path # ...and that transactions are signed by the keystore signer txhash = ursula.commit_to_next_period() receipt = testerchain.wait_for_receipt(txhash) transaction_data = testerchain.client.w3.eth.getTransaction( receipt['transactionHash']) assert transaction_data['from'] == worker_account.address finally: ursula.stop()
def test_ursula_init_with_local_keystore_signer(click_runner, tmp_path, mocker, mock_testerchain, mock_account_password_keystore, test_registry_source_manager): custom_filepath = tmp_path custom_config_filepath = tmp_path / UrsulaConfiguration.generate_filename() worker_account, password, mock_keystore_path = mock_account_password_keystore mock_signer_uri = f'keystore:{mock_keystore_path}' # Good signer... pre_config_signer = KeystoreSigner.from_signer_uri(uri=mock_signer_uri) init_args = ( 'ursula', 'init', '--network', TEMPORARY_DOMAIN, '--worker-address', worker_account.address, '--config-root', custom_filepath, '--provider', TEST_PROVIDER_URI, '--rest-host', MOCK_IP_ADDRESS, '--rest-port', MOCK_URSULA_STARTING_PORT, # The bit we are testing here '--signer', mock_signer_uri) cli_env = { NUCYPHER_ENVVAR_KEYRING_PASSWORD: password, NUCYPHER_ENVVAR_WORKER_ETH_PASSWORD: password, } result = click_runner.invoke(nucypher_cli, init_args, catch_exceptions=False, env=cli_env) assert result.exit_code == 0, result.stdout # Inspect the configuration file for the signer URI with open(custom_config_filepath, 'r') as config_file: raw_config_data = config_file.read() config_data = json.loads(raw_config_data) assert config_data['signer_uri'] == mock_signer_uri,\ "Keystore URI was not correctly included in configuration file" # Recreate a configuration with the signer URI preserved ursula_config = UrsulaConfiguration.from_configuration_file( custom_config_filepath) assert ursula_config.signer_uri == mock_signer_uri # Mock decryption of web3 client keyring mocker.patch.object(Account, 'decrypt', return_value=worker_account.privateKey) ursula_config.attach_keyring(checksum_address=worker_account.address) ursula_config.keyring.unlock(password=password) # Produce an ursula with a Keystore signer correctly derived from the signer URI, and dont do anything else! mocker.patch.object(StakeList, 'refresh', autospec=True) ursula = ursula_config.produce(client_password=password, block_until_ready=False) # Verify the keystore path is still preserved assert isinstance(ursula.signer, KeystoreSigner) assert isinstance(ursula.signer.path, str), "Use str" assert ursula.signer.path == str(mock_keystore_path) # Show that we can produce the exact same signer as pre-config... assert pre_config_signer.path == ursula.signer.path
def ursula( click_config, action, dev, quiet, dry_run, force, lonely, network, teacher_uri, min_stake, rest_host, rest_port, db_filepath, checksum_address, withdraw_address, federated_only, poa, config_root, config_file, provider_uri, geth, no_registry, registry_filepath, value, duration, index, list_, divide, sync, device, interactive, ) -> None: """ Manage and run an "Ursula" PRE node. \b Actions ------------------------------------------------- \b init Create a new Ursula node configuration. view View the Ursula node's configuration. run Run an "Ursula" node. save-metadata Manually write node metadata to disk without running forget Forget all known nodes. destroy Delete Ursula node configuration. stake Manage stakes for this node. confirm-activity Manually confirm-activity for the current period. collect-reward Withdraw staking reward. """ # # Validate # if federated_only and geth: raise click.BadOptionUsage( option_name="--geth", message="Federated only cannot be used with the --geth flag") if click_config.debug and quiet: raise click.BadOptionUsage( option_name="quiet", message="--debug and --quiet cannot be used at the same time.") # Banner if not click_config.json_ipc and not click_config.quiet: click.secho(URSULA_BANNER.format(checksum_address or '')) # # Pre-Launch Warnings # if not click_config.quiet: if dev: click.secho("WARNING: Running in Development mode", fg='yellow') if force: click.secho("WARNING: Force is enabled", fg='yellow') # # Internal Ethereum Client # ETH_NODE = NO_BLOCKCHAIN_CONNECTION if geth: ETH_NODE = actions.get_provider_process() provider_uri = ETH_NODE.provider_uri(scheme='file') # # Eager Actions # if action == "init": """Create a brand-new persistent Ursula""" if dev: raise click.BadArgumentUsage( "Cannot create a persistent development character") if not config_root: # Flag config_root = click_config.config_file # Envvar if not rest_host: rest_host = actions.determine_external_ip_address(force=force) ursula_config = UrsulaConfiguration.generate( password=get_password(confirm=True), config_root=config_root, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath, domains={network} if network else None, federated_only=federated_only, checksum_address=checksum_address, download_registry=federated_only or no_registry, registry_filepath=registry_filepath, provider_process=ETH_NODE, provider_uri=provider_uri, poa=poa) painting.paint_new_installation_help(new_configuration=ursula_config) return # # Make Ursula # if dev: ursula_config = UrsulaConfiguration( dev_mode=True, domains={TEMPORARY_DOMAIN}, poa=poa, download_registry=False, registry_filepath=registry_filepath, provider_process=ETH_NODE, provider_uri=provider_uri, checksum_address=checksum_address, federated_only=federated_only, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath) else: try: ursula_config = UrsulaConfiguration.from_configuration_file( filepath=config_file, domains={network} if network else None, registry_filepath=registry_filepath, provider_process=ETH_NODE, provider_uri=provider_uri, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath, poa=poa, federated_only=federated_only) except FileNotFoundError: return actions.handle_missing_configuration_file( character_config_class=UrsulaConfiguration, config_file=config_file) except Exception as e: if click_config.debug: raise else: click.secho(str(e), fg='red', bold=True) raise click.Abort # # Configured Pre-Authentication Actions # # Handle destruction *before* network bootstrap and character initialization below if action == "destroy": """Delete all configuration files from the disk""" if dev: message = "'nucypher ursula destroy' cannot be used in --dev mode - There is nothing to destroy." raise click.BadOptionUsage(option_name='--dev', message=message) return actions.destroy_configuration(character_config=ursula_config, force=force) # # Make Ursula # URSULA = actions.make_cli_character(character_config=ursula_config, click_config=click_config, min_stake=min_stake, teacher_uri=teacher_uri, dev=dev, lonely=lonely) # # Authenticated Action Switch # if action == 'run': """Seed, Produce, Run!""" # GO! try: # Ursula Deploy Warnings click_config.emit(message="Starting Ursula on {}".format( URSULA.rest_interface), color='green', bold=True) click_config.emit(message="Connecting to {}".format(','.join( ursula_config.domains)), color='green', bold=True) if not URSULA.federated_only and URSULA.stakes: click_config.emit( message= f"Staking {str(URSULA.current_stake)} ~ Keep Ursula Online!", color='blue', bold=True) if interactive: stdio.StandardIO(UrsulaCommandProtocol(ursula=URSULA)) if dry_run: return # <-- ABORT - (Last Chance) # Run - Step 3 node_deployer = URSULA.get_deployer() node_deployer.addServices() node_deployer.catalogServers(node_deployer.hendrix) node_deployer.run() # <--- Blocking Call (Reactor) # Handle Crash except Exception as e: ursula_config.log.critical(str(e)) click_config.emit(message="{} {}".format(e.__class__.__name__, str(e)), color='red', bold=True) raise # Crash :-( # Graceful Exit finally: click_config.emit(message="Stopping Ursula", color='green') ursula_config.cleanup() click_config.emit(message="Ursula Stopped", color='red') return elif action == "save-metadata": """Manually save a node self-metadata file""" metadata_path = ursula.write_node_metadata(node=URSULA) return click_config.emit( message="Successfully saved node metadata to {}.".format( metadata_path), color='green') elif action == "view": """Paint an existing configuration to the console""" if not URSULA.federated_only: click.secho("BLOCKCHAIN ----------\n") painting.paint_contract_status(click_config=click_config, ursula_config=ursula_config) current_block = URSULA.blockchain.w3.eth.blockNumber click.secho(f'Block # {current_block}') click.secho(f'NU Balance: {URSULA.token_balance}') click.secho(f'ETH Balance: {URSULA.eth_balance}') click.secho( f'Current Gas Price {URSULA.blockchain.client.gasPrice}') click.secho("CONFIGURATION --------") response = UrsulaConfiguration._read_configuration_file( filepath=config_file or ursula_config.config_file_location) return click_config.emit(response=response) elif action == "forget": actions.forget(configuration=ursula_config) return elif action == 'stake': # List Only if list_: if not URSULA.stakes: click.echo( f"There are no active stakes for {URSULA.checksum_address}" ) else: painting.paint_stakes(stakes=URSULA.stakes) return # Divide Only if divide: """Divide an existing stake by specifying the new target value and end period""" # Validate if not URSULA.stakes: click.echo( f"There are no active stakes for {URSULA.checksum_address}" ) return # Selection if index is None: painting.paint_stakes(stakes=URSULA.stakes) index = click.prompt("Select a stake to divide", type=click.IntRange( min=0, max=len(URSULA.stakes) - 1)) # Lookup the stake current_stake = URSULA.stakes[index] # Value if not value: value = click.prompt( f"Enter target value (must be less than {str(current_stake.value)})", type=STAKE_VALUE) value = NU(value, 'NU') # Duration if not duration: extension = click.prompt("Enter number of periods to extend", type=STAKE_EXTENSION) else: extension = duration if not force: painting.paint_staged_stake_division( ursula=URSULA, original_index=index, original_stake=current_stake, target_value=value, extension=extension) click.confirm("Is this correct?", abort=True) modified_stake, new_stake = URSULA.divide_stake( stake_index=index, target_value=value, additional_periods=extension) if not quiet: click.secho('Successfully divided stake', fg='green') click.secho( f'Transaction Hash ........... {new_stake.receipt}') # Show the resulting stake list painting.paint_stakes(stakes=URSULA.stakes) return # Confirm new stake init if not force: click.confirm("Stage a new stake?", abort=True) # Validate balance balance = URSULA.token_balance if balance == 0: click.secho(f"{URSULA.checksum_address} has 0 NU.") raise click.Abort if not quiet: click.echo(f"Current balance: {balance}") # Gather stake value if not value: min_locked = NU(URSULA.economics.minimum_allowed_locked, 'NuNit') value = click.prompt(f"Enter stake value", type=STAKE_VALUE, default=min_locked) else: value = NU(int(value), 'NU') # Duration if not quiet: message = f"Minimum duration: {URSULA.economics.minimum_allowed_locked} | " \ f"Maximum Duration: {URSULA.economics.maximum_allowed_locked}" click.echo(message) if not duration: duration = click.prompt( "Enter stake duration in periods (1 Period = 24 Hours)", type=STAKE_DURATION) start_period = URSULA.staking_agent.get_current_period() end_period = start_period + duration # Review if not force: painting.paint_staged_stake(ursula=URSULA, stake_value=value, duration=duration, start_period=start_period, end_period=end_period) if not dev: actions.confirm_staged_stake(ursula=URSULA, value=value, duration=duration) # Last chance to bail if not force: click.confirm("Publish staged stake to the blockchain?", abort=True) stake = URSULA.initialize_stake(amount=int(value), lock_periods=duration) # TODO temporary fix to not break backward compatibility URSULA.set_worker(worker_address=URSULA.checksum_address) painting.paint_staking_confirmation(ursula=URSULA, transactions=stake.transactions) return elif action == 'confirm-activity': if not URSULA.stakes: click.secho("There are no active stakes for {}".format( URSULA.checksum_address)) return URSULA.staking_agent.confirm_activity( node_address=URSULA.checksum_address) return elif action == 'collect-reward': """Withdraw staking reward to the specified wallet address""" if not force: click.confirm( f"Send {URSULA.calculate_reward()} to {URSULA.checksum_address}?" ) inflation_reward = URSULA.calculate_reward() if inflation_reward: URSULA.collect_staking_reward() URSULA.collect_policy_reward( collector_address=withdraw_address or checksum_address) else: raise click.BadArgumentUsage("No such argument {}".format(action))
def ursula( click_config, action, dev, quiet, dry_run, force, lonely, network, teacher_uri, min_stake, rest_host, rest_port, db_filepath, checksum_address, federated_only, poa, config_root, config_file, metadata_dir, # TODO: Start nodes from an additional existing metadata dir provider_uri, recompile_solidity, no_registry, registry_filepath) -> None: """ Manage and run an "Ursula" PRE node. \b Actions ------------------------------------------------- \b run Run an "Ursula" node. init Create a new Ursula node configuration. view View the Ursula node's configuration. forget Forget all known nodes. save-metadata Manually write node metadata to disk without running destroy Delete Ursula node configuration. """ # # Boring Setup Stuff # if not quiet: log = Logger('ursula.cli') if click_config.debug and quiet: raise click.BadOptionUsage( option_name="quiet", message="--debug and --quiet cannot be used at the same time.") if click_config.debug: click_config.log_to_sentry = False click_config.log_to_file = True globalLogPublisher.removeObserver(logToSentry) # Sentry GlobalConsoleLogger.set_log_level("debug") elif quiet: globalLogPublisher.removeObserver(logToSentry) globalLogPublisher.removeObserver(SimpleObserver) globalLogPublisher.removeObserver(getJsonFileObserver()) if not click_config.json_ipc and not click_config.quiet: click.secho(URSULA_BANNER) # # Pre-Launch Warnings # if not quiet: if dev: click.secho("WARNING: Running in development mode", fg='yellow') if force: click.secho("WARNING: Force is enabled", fg='yellow') # # Unauthenticated Configurations # if action == "init": """Create a brand-new persistent Ursula""" if not network: raise click.BadArgumentUsage( '--network is required to initialize a new configuration.') if dev: actions.handle_control_output( message="WARNING: Using temporary storage area", color='yellow', quiet=quiet, json=click_config.json) if not config_root: # Flag config_root = click_config.config_file # Envvar if not rest_host: rest_host = click.prompt( "Enter Ursula's public-facing IPv4 address" ) # TODO: Remove this step ursula_config = UrsulaConfiguration.generate( password=click_config.get_password(confirm=True), config_root=config_root, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath, domains={network} if network else None, federated_only=federated_only, checksum_public_address=checksum_address, no_registry=federated_only or no_registry, registry_filepath=registry_filepath, provider_uri=provider_uri, poa=poa) click_config.emitter(message="Generated keyring {}".format( ursula_config.keyring_dir), color='green') click_config.emitter(message="Saved configuration file {}".format( ursula_config.config_file_location), color='green') # Give the use a suggestion as to what to do next... how_to_run_message = "\nTo run an Ursula node from the default configuration filepath run: \n\n'{}'\n" suggested_command = 'nucypher ursula run' if config_root is not None: config_file_location = os.path.join( config_root, config_file or UrsulaConfiguration.CONFIG_FILENAME) suggested_command += ' --config-file {}'.format( config_file_location) return click_config.emitter( message=how_to_run_message.format(suggested_command), color='green') # Development Configuration if dev: ursula_config = UrsulaConfiguration( dev_mode=True, domains={TEMPORARY_DOMAIN}, poa=poa, registry_filepath=registry_filepath, provider_uri=provider_uri, checksum_public_address=checksum_address, federated_only=federated_only, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath) # Authenticated Configurations else: # Deserialize network domain name if override passed if network: domain_constant = getattr(constants, network.upper()) domains = {domain_constant} else: domains = None ursula_config = UrsulaConfiguration.from_configuration_file( filepath=config_file, domains=domains, registry_filepath=registry_filepath, provider_uri=provider_uri, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath, # TODO: Handle Boolean overrides # poa=poa, # federated_only=federated_only, ) actions.unlock_keyring(configuration=ursula_config, password=click_config.get_password()) if not ursula_config.federated_only: actions.connect_to_blockchain(configuration=ursula_config, recompile_contracts=recompile_solidity) click_config.ursula_config = ursula_config # Pass Ursula's config onto staking sub-command # # Launch Warnings # if ursula_config.federated_only: click_config.emitter(message="WARNING: Running in Federated mode", color='yellow') # # Action Switch # if action == 'run': """Seed, Produce, Run!""" # # Seed - Step 1 # teacher_uris = [teacher_uri] if teacher_uri else list() teacher_nodes = actions.load_seednodes( teacher_uris=teacher_uris, min_stake=min_stake, federated_only=federated_only, network_middleware=click_config.middleware) # # Produce - Step 2 # URSULA = ursula_config(known_nodes=teacher_nodes, lonely=lonely) # GO! try: # # Run - Step 3 # click_config.emitter(message="Connecting to {}".format(','.join( str(d) for d in ursula_config.domains)), color='green', bold=True) click_config.emitter(message="Running Ursula {} on {}".format( URSULA, URSULA.rest_interface), color='green', bold=True) if not click_config.debug: stdio.StandardIO(UrsulaCommandProtocol(ursula=URSULA)) if dry_run: # That's all folks! return URSULA.get_deployer().run() # <--- Blocking Call (Reactor) except Exception as e: ursula_config.log.critical(str(e)) click_config.emitter(message="{} {}".format( e.__class__.__name__, str(e)), color='red', bold=True) raise # Crash :-( finally: click_config.emitter(message="Stopping Ursula", color='green') ursula_config.cleanup() click_config.emitter(message="Ursula Stopped", color='red') return elif action == "save-metadata": """Manually save a node self-metadata file""" URSULA = ursula_config.produce(ursula_config=ursula_config) metadata_path = ursula.write_node_metadata(node=URSULA) return click_config.emitter( message="Successfully saved node metadata to {}.".format( metadata_path), color='green') elif action == "view": """Paint an existing configuration to the console""" response = UrsulaConfiguration._read_configuration_file( filepath=config_file or ursula_config.config_file_location) return click_config.emitter(response=response) elif action == "forget": # TODO: Move to character control actions.forget(configuration=ursula_config) return elif action == "destroy": """Delete all configuration files from the disk""" if dev: message = "'nucypher ursula destroy' cannot be used in --dev mode" raise click.BadOptionUsage(option_name='--dev', message=message) destroyed_filepath = destroy_system_configuration( config_class=UrsulaConfiguration, config_file=config_file, network=network, config_root=ursula_config.config_file_location, force=force) return click_config.emitter(message=f"Destroyed {destroyed_filepath}", color='green') else: raise click.BadArgumentUsage("No such argument {}".format(action))