Create a console emitter only once (in NucypherClickConfig) and propagate it everywhere

pull/1124/head
Bogdan Opanchuk 2019-07-22 22:54:24 -07:00 committed by Kieran Prasch
parent 52dbca6519
commit 0ce9458bf0
No known key found for this signature in database
GPG Key ID: 199AB839D4125A62
17 changed files with 307 additions and 313 deletions

View File

@ -318,7 +318,7 @@ class JSONRPCController(CharacterControlServer):
if not control_requests: if not control_requests:
e = self.emitter.InvalidRequest() e = self.emitter.InvalidRequest()
return self.emitter(e=e) return self.emitter.error(e)
batch_size = 0 batch_size = 0
for request in control_requests: # TODO: parallelism for request in control_requests: # TODO: parallelism
@ -332,7 +332,7 @@ class JSONRPCController(CharacterControlServer):
control_request = json.loads(control_request) control_request = json.loads(control_request)
except JSONDecodeError: except JSONDecodeError:
e = self.emitter.ParseError() e = self.emitter.ParseError()
return self.emitter(e=e) return self.emitter.error(e)
# Handle batch of messages # Handle batch of messages
if isinstance(control_request, list): if isinstance(control_request, list):
@ -343,12 +343,12 @@ class JSONRPCController(CharacterControlServer):
return self.handle_message(message=control_request, *args, **kwargs) return self.handle_message(message=control_request, *args, **kwargs)
except self.emitter.JSONRPCError as e: except self.emitter.JSONRPCError as e:
return self.emitter(e=e) return self.emitter.error(e)
except Exception as e: except Exception as e:
if self.crash_on_error: if self.crash_on_error:
raise raise
return self.emitter(e=e) return self.emitter.error(e)
class WebController(CharacterControlServer): class WebController(CharacterControlServer):

View File

@ -13,81 +13,63 @@ class StdoutEmitter:
transport_serializer = str transport_serializer = str
default_color = 'white' default_color = 'white'
default_sink_callable = sys.stdout.write
__stdout_trap = list() # sys.stdout.write() doesn't work well with click_runner's output capture
default_sink_callable = print
def __init__(self, def __init__(self,
sink: Callable = None, sink: Callable = None,
capture_stdout: bool = False,
quiet: bool = False): quiet: bool = False):
self.name = self.__class__.__name__.lower() self.name = self.__class__.__name__.lower()
self.sink = sink or self.default_sink_callable self.sink = sink or self.default_sink_callable
self.capture_stdout = capture_stdout
self.quiet = quiet self.quiet = quiet
self.log = Logger(self.name) self.log = Logger(self.name)
super().__init__() def clear(self):
if not self.quiet:
click.clear()
def __call__(self, *args, **kwargs): def message(self,
try: message: str = None,
return self._emit(*args, **kwargs) color: str = None,
except Exception: bold: bool = False):
self.log.debug("Error while emitting nucypher controller output") if not self.quiet:
raise self.echo(message=message, color=color or self.default_color, bold=bold)
def trap_output(self, output) -> int:
self.__stdout_trap.append(output)
return len(bytes(output)) # number of bytes written
def _emit(self,
response: dict = None,
message: str = None,
color: str = None,
bold: bool = False,
) -> None:
"""
Write pretty messages to stdout. For Human consumption only.
"""
if response and message:
raise ValueError(f'{self.__class__.__name__} received both a response and a message.')
if self.quiet:
# reduces the number of CLI conditionals by
# wrapping console output functions
return
if self.capture_stdout:
self.trap_output(response or message)
elif response:
# WARNING: Do not log in this block
for k, v in response.items():
click.secho(message=f'{k} ...... {v}',
fg=color or self.default_color,
bold=bold)
elif message:
# Most likely a message emitted without a character control instance
click.secho(message=message, fg=color or self.default_color, bold=bold)
self.log.debug(message) self.log.debug(message)
else: def echo(self,
raise ValueError('Either "response" dict or "message" str is required, but got neither.') message: str = None,
color: str = None,
bold: bool = False,
nl: bool = True):
if not self.quiet:
click.secho(message=message, fg=color or self.default_color, bold=bold, nl=nl)
def banner(self, banner):
if not self.quiet:
click.echo(banner)
def ipc(self, response: dict, request_id: int, duration):
# WARNING: Do not log in this block
if not self.quiet:
for k, v in response.items():
click.secho(message=f'{k} ...... {v}', fg=self.default_color)
def error(self, e):
if not self.quiet:
e_str = str(e)
click.echo(message=e_str)
self.log.info(e_str)
class JSONRPCStdoutEmitter(StdoutEmitter): class JSONRPCStdoutEmitter(StdoutEmitter):
transport_serializer = json.dumps transport_serializer = json.dumps
default_sink_callable = print
delimiter = '\n' delimiter = '\n'
def __init__(self, sink: Callable = None, *args, **kwargs): def __init__(self, *args, **kwargs):
self.sink = sink or self.default_sink_callable
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.log = Logger("JSON-RPC-Emitter") self.log = Logger("JSON-RPC-Emitter")
class JSONRPCError(RuntimeError): class JSONRPCError(RuntimeError):
@ -114,17 +96,6 @@ class JSONRPCStdoutEmitter(StdoutEmitter):
code = -32603 code = -32603
message = "Internal JSON-RPC error." message = "Internal JSON-RPC error."
def __call__(self, *args, **kwargs):
if 'response' in kwargs:
return self.__emit_rpc_response(*args, **kwargs)
elif 'message' in kwargs:
if not self.quiet:
self.log.info(*args, **kwargs)
elif 'e' in kwargs:
return self.__emit_rpc_error(*args, **kwargs)
else:
raise self.JSONRPCError("Internal Error")
@staticmethod @staticmethod
def assemble_response(response: dict, message_id: int) -> dict: def assemble_response(response: dict, message_id: int) -> dict:
response_data = {'jsonrpc': '2.0', response_data = {'jsonrpc': '2.0',
@ -162,16 +133,39 @@ class JSONRPCStdoutEmitter(StdoutEmitter):
serialized_response = self.__serialize(data=data) serialized_response = self.__serialize(data=data)
# Capture Message Output
if self.capture_stdout:
return self.trap_output(serialized_response)
# Write to stdout file descriptor # Write to stdout file descriptor
else: number_of_written_bytes = self.sink(serialized_response) # < ------ OUTLET
number_of_written_bytes = self.sink(serialized_response) # < ------ OUTLET return number_of_written_bytes
return number_of_written_bytes
def __emit_rpc_error(self, e): def clear(self):
pass
def message(self,
message: str = None,
color: str = None,
bold: bool = False):
if not self.quiet:
self.log.info(message)
def echo(self, *args, **kwds):
pass
def banner(self, banner):
pass
def ipc(self, response: dict, request_id: int, duration) -> int:
"""
Write RPC response object to stdout and return the number of bytes written.
"""
# Serialize JSON RPC Message
assembled_response = self.assemble_response(response=response, message_id=request_id)
size = self.__write(data=assembled_response)
if not self.quiet:
self.log.info(f"OK | Responded to IPC request #{request_id} with {size} bytes, took {duration}")
return size
def error(self, e):
""" """
Write RPC error object to stdout and return the number of bytes written. Write RPC error object to stdout and return the number of bytes written.
""" """
@ -185,20 +179,8 @@ class JSONRPCStdoutEmitter(StdoutEmitter):
raise self.JSONRPCError raise self.JSONRPCError
size = self.__write(data=assembled_error) size = self.__write(data=assembled_error)
# if not self.quiet: #if not self.quiet:
# self.log.info(f"Error {e.code} | {e.message}") # TODO: Restore this log message # self.log.info(f"Error {e.code} | {e.message}") # TODO: Restore this log message
return size
def __emit_rpc_response(self, response: dict, request_id: int, duration) -> int:
"""
Write RPC response object to stdout and return the number of bytes written.
"""
# Serialize JSON RPC Message
assembled_response = self.assemble_response(response=response, message_id=request_id)
size = self.__write(data=assembled_response)
if not self.quiet:
self.log.info(f"OK | Responded to IPC request #{request_id} with {size} bytes, took {duration}")
return size return size

View File

@ -49,7 +49,7 @@ def character_control_interface(func):
duration = responding - received duration = responding - received
# Emit # Emit
return instance.emitter(response=response, request_id=request_id, duration=duration) return instance.emitter.ipc(response=response, request_id=request_id, duration=duration)
return wrapped return wrapped

View File

@ -22,16 +22,14 @@ from typing import List
import click import click
import requests import requests
from constant_sorrow.constants import NO_BLOCKCHAIN_CONNECTION, NO_PASSWORD from constant_sorrow.constants import NO_BLOCKCHAIN_CONNECTION, NO_PASSWORD, NO_CONTROL_PROTOCOL
from nacl.exceptions import CryptoError from nacl.exceptions import CryptoError
from twisted.logger import Logger from twisted.logger import Logger
from nucypher.blockchain.eth.clients import NuCypherGethGoerliProcess from nucypher.blockchain.eth.clients import NuCypherGethGoerliProcess
from nucypher.blockchain.eth.token import Stake from nucypher.blockchain.eth.token import Stake
from nucypher.characters.control.emitters import JSONRPCStdoutEmitter
from nucypher.characters.lawful import Ursula from nucypher.characters.lawful import Ursula
from nucypher.cli import painting from nucypher.cli import painting
from nucypher.cli.config import NucypherClickConfig
from nucypher.cli.types import IPV4_ADDRESS from nucypher.cli.types import IPV4_ADDRESS
from nucypher.config.node import CharacterConfiguration from nucypher.config.node import CharacterConfiguration
from nucypher.network.middleware import RestMiddleware from nucypher.network.middleware import RestMiddleware
@ -62,8 +60,6 @@ SUCCESSFUL_DESTRUCTION = "Successfully destroyed NuCypher configuration"
LOG = Logger('cli.actions') LOG = Logger('cli.actions')
console_emitter = NucypherClickConfig.emit
class UnknownIPAddress(RuntimeError): class UnknownIPAddress(RuntimeError):
pass pass
@ -77,8 +73,8 @@ def get_password(confirm: bool = False) -> str:
return keyring_password return keyring_password
def unlock_nucypher_keyring(password: str, character_configuration: CharacterConfiguration): def unlock_nucypher_keyring(emitter, password: str, character_configuration: CharacterConfiguration):
console_emitter(message='Decrypting NuCypher keyring...', color='yellow') emitter.message('Decrypting NuCypher keyring...', color='yellow')
if character_configuration.dev_mode: if character_configuration.dev_mode:
return True # Dev accounts are always unlocked return True # Dev accounts are always unlocked
@ -90,7 +86,8 @@ def unlock_nucypher_keyring(password: str, character_configuration: CharacterCon
raise character_configuration.keyring.AuthenticationFailed raise character_configuration.keyring.AuthenticationFailed
def load_seednodes(min_stake: int, def load_seednodes(emitter,
min_stake: int,
federated_only: bool, federated_only: bool,
network_domains: set, network_domains: set,
network_middleware: RestMiddleware = None, network_middleware: RestMiddleware = None,
@ -112,7 +109,7 @@ def load_seednodes(min_stake: int,
except KeyError: except KeyError:
# TODO: If this is a unknown domain, require the caller to pass a teacher URI explicitly? # TODO: If this is a unknown domain, require the caller to pass a teacher URI explicitly?
if not teacher_uris: if not teacher_uris:
console_emitter(message=f"No default teacher nodes exist for the specified network: {domain}") emitter.message(f"No default teacher nodes exist for the specified network: {domain}")
for uri in teacher_uris: for uri in teacher_uris:
teacher_node = Ursula.from_teacher_uri(teacher_uri=uri, teacher_node = Ursula.from_teacher_uri(teacher_uri=uri,
@ -122,7 +119,7 @@ def load_seednodes(min_stake: int,
teacher_nodes.append(teacher_node) teacher_nodes.append(teacher_node)
if not teacher_nodes: if not teacher_nodes:
console_emitter(message=f'WARNING - No Bootnodes Available') emitter.message(f'WARNING - No Bootnodes Available')
return teacher_nodes return teacher_nodes
@ -135,7 +132,7 @@ def get_external_ip_from_centralized_source() -> str:
f"(status code {ip_request.status_code})") f"(status code {ip_request.status_code})")
def determine_external_ip_address(force: bool = False) -> str: def determine_external_ip_address(emitter, force: bool = False) -> str:
""" """
Attempts to automatically get the external IP from ifconfig.me Attempts to automatically get the external IP from ifconfig.me
If the request fails, it falls back to the standard process. If the request fails, it falls back to the standard process.
@ -151,12 +148,12 @@ def determine_external_ip_address(force: bool = False) -> str:
if not click.confirm(f"Is this the public-facing IPv4 address ({rest_host}) you want to use for Ursula?"): if not click.confirm(f"Is this the public-facing IPv4 address ({rest_host}) you want to use for Ursula?"):
rest_host = click.prompt("Please enter Ursula's public-facing IPv4 address here:", type=IPV4_ADDRESS) rest_host = click.prompt("Please enter Ursula's public-facing IPv4 address here:", type=IPV4_ADDRESS)
else: else:
console_emitter(message=f"WARNING: --force is set, using auto-detected IP '{rest_host}'", color='yellow') emitter.message(f"WARNING: --force is set, using auto-detected IP '{rest_host}'", color='yellow')
return rest_host return rest_host
def destroy_configuration(character_config, force: bool = False) -> None: def destroy_configuration(emitter, character_config, force: bool = False) -> None:
if not force: if not force:
click.confirm(CHARACTER_DESTRUCTION.format(name=character_config._NAME, click.confirm(CHARACTER_DESTRUCTION.format(name=character_config._NAME,
root=character_config.config_root, root=character_config.config_root,
@ -165,17 +162,16 @@ def destroy_configuration(character_config, force: bool = False) -> None:
config=character_config.filepath), abort=True) config=character_config.filepath), abort=True)
character_config.destroy() character_config.destroy()
SUCCESSFUL_DESTRUCTION = "Successfully destroyed NuCypher configuration" SUCCESSFUL_DESTRUCTION = "Successfully destroyed NuCypher configuration"
console_emitter(message=SUCCESSFUL_DESTRUCTION, color='green') emitter.message(SUCCESSFUL_DESTRUCTION, color='green')
character_config.log.debug(SUCCESSFUL_DESTRUCTION) character_config.log.debug(SUCCESSFUL_DESTRUCTION)
def forget(configuration): def forget(emitter, configuration):
"""Forget all known nodes via storage""" """Forget all known nodes via storage"""
click.confirm("Permanently delete all known node data?", abort=True) click.confirm("Permanently delete all known node data?", abort=True)
configuration.forget_nodes() configuration.forget_nodes()
message = "Removed all stored node node metadata and certificates" message = "Removed all stored node node metadata and certificates"
console_emitter(message=message, color='red') emitter.message(message, color='red')
click.secho(message=message, fg='red')
def confirm_staged_stake(stakeholder, value, duration): def confirm_staged_stake(stakeholder, value, duration):
@ -231,6 +227,8 @@ def make_cli_character(character_config,
min_stake: int = 0, min_stake: int = 0,
**config_args): **config_args):
emitter = click_config.emitter
# #
# Pre-Init # Pre-Init
# #
@ -242,13 +240,15 @@ def make_cli_character(character_config,
# Handle Keyring # Handle Keyring
if not dev: if not dev:
character_config.attach_keyring() character_config.attach_keyring()
unlock_nucypher_keyring(character_configuration=character_config, unlock_nucypher_keyring(emitter,
character_configuration=character_config,
password=get_password(confirm=False)) password=get_password(confirm=False))
# Handle Teachers # Handle Teachers
teacher_nodes = None teacher_nodes = None
if teacher_uri: if teacher_uri:
teacher_nodes = load_seednodes(teacher_uris=[teacher_uri] if teacher_uri else None, teacher_nodes = load_seednodes(emitter,
teacher_uris=[teacher_uri] if teacher_uri else None,
min_stake=min_stake, min_stake=min_stake,
federated_only=character_config.federated_only, federated_only=character_config.federated_only,
network_domains=character_config.domains, network_domains=character_config.domains,
@ -267,14 +267,12 @@ def make_cli_character(character_config,
# Post-Init # Post-Init
# #
# TODO: Move to character configuration if CHARACTER.controller is not NO_CONTROL_PROTOCOL:
# Switch to character control emitter CHARACTER.controller.emitter = emitter # TODO: set it on object creation? Or not set at all?
if click_config.json_ipc:
CHARACTER.controller.emitter = JSONRPCStdoutEmitter(quiet=click_config.quiet)
# Federated # Federated
if character_config.federated_only: if character_config.federated_only:
console_emitter(message="WARNING: Running in Federated mode", color='yellow') emitter.message("WARNING: Running in Federated mode", color='yellow')
return CHARACTER return CHARACTER
@ -288,26 +286,26 @@ def select_stake(stakeholder) -> Stake:
return chosen_stake return chosen_stake
def select_client_account(blockchain, prompt: str = None, default=0) -> str: def select_client_account(emitter, blockchain, prompt: str = None, default=0) -> str:
enumerated_accounts = dict(enumerate(blockchain.client.accounts)) enumerated_accounts = dict(enumerate(blockchain.client.accounts))
for index, account in enumerated_accounts.items(): for index, account in enumerated_accounts.items():
click.secho(f"{index} | {account}") emitter.echo(f"{index} | {account}")
prompt = prompt or "Select Account" prompt = prompt or "Select Account"
choice = click.prompt(prompt, type=click.IntRange(min=0, max=len(enumerated_accounts)-1), default=default) choice = click.prompt(prompt, type=click.IntRange(min=0, max=len(enumerated_accounts)-1), default=default)
chosen_account = enumerated_accounts[choice] chosen_account = enumerated_accounts[choice]
return chosen_account return chosen_account
def confirm_deployment(deployer) -> bool: def confirm_deployment(emitter, deployer) -> bool:
if deployer.blockchain.client.chain_id == 'UNKNOWN' or deployer.blockchain.client.is_local: if deployer.blockchain.client.chain_id == 'UNKNOWN' or deployer.blockchain.client.is_local:
if click.prompt("Type 'DEPLOY' to continue") != 'DEPLOY': if click.prompt("Type 'DEPLOY' to continue") != 'DEPLOY':
click.secho("Aborting Deployment", fg='red', bold=True) emitter.echo("Aborting Deployment", fg='red', bold=True)
raise click.Abort() raise click.Abort()
else: else:
confirmed_chain_id = int(click.prompt("Enter the Chain ID to confirm deployment", type=click.INT)) confirmed_chain_id = int(click.prompt("Enter the Chain ID to confirm deployment", type=click.INT))
expected_chain_id = int(deployer.blockchain.client.chain_id) expected_chain_id = int(deployer.blockchain.client.chain_id)
if confirmed_chain_id != expected_chain_id: if confirmed_chain_id != expected_chain_id:
click.secho(f"Chain ID not a match ({confirmed_chain_id} != {expected_chain_id}) Aborting Deployment", emitter.echo(f"Chain ID not a match ({confirmed_chain_id} != {expected_chain_id}) Aborting Deployment",
fg='red', fg='red',
bold=True) bold=True)
raise click.Abort() raise click.Abort()

View File

@ -94,9 +94,9 @@ def alice(click_config,
raise click.BadOptionUsage(option_name="--geth", message="Federated only cannot be used with the --geth flag") raise click.BadOptionUsage(option_name="--geth", message="Federated only cannot be used with the --geth flag")
# Banner # Banner
click.clear() emitter = click_config.emitter
if not click_config.json_ipc and not click_config.quiet: emitter.clear()
click.secho(ALICE_BANNER) emitter.banner(ALICE_BANNER)
# #
# Managed Ethereum Client # Managed Ethereum Client
@ -135,15 +135,14 @@ def alice(click_config,
duration=duration, duration=duration,
rate=rate) rate=rate)
painting.paint_new_installation_help(new_configuration=new_alice_config) painting.paint_new_installation_help(emitter, new_configuration=new_alice_config)
return # Exit return # Exit
elif action == "view": elif action == "view":
"""Paint an existing configuration to the console""" """Paint an existing configuration to the console"""
configuration_file_location = config_file or AliceConfiguration.default_filepath() configuration_file_location = config_file or AliceConfiguration.default_filepath()
response = AliceConfiguration._read_configuration_file(filepath=configuration_file_location) response = AliceConfiguration._read_configuration_file(filepath=configuration_file_location)
click_config.emit(response) return emitter.ipc(response=response, request_id=0, duration=0) # FIXME: what are request_id and duration here?
return # Exit
# #
# Make Alice # Make Alice
@ -194,7 +193,7 @@ def alice(click_config,
# HTTP # HTTP
else: else:
ALICE.controller.emitter(message=f"Alice Verifying Key {bytes(ALICE.stamp).hex()}", color="green", bold=True) emitter.message(f"Alice Verifying Key {bytes(ALICE.stamp).hex()}", color="green", bold=True)
controller = ALICE.make_web_controller(crash_on_error=click_config.debug) controller = ALICE.make_web_controller(crash_on_error=click_config.debug)
ALICE.log.info('Starting HTTP Character Web Controller') ALICE.log.info('Starting HTTP Character Web Controller')
return controller.start(http_port=controller_port, dry_run=dry_run) return controller.start(http_port=controller_port, dry_run=dry_run)
@ -204,7 +203,7 @@ def alice(click_config,
if dev: if dev:
message = "'nucypher alice destroy' cannot be used in --dev mode" message = "'nucypher alice destroy' cannot be used in --dev mode"
raise click.BadOptionUsage(option_name='--dev', message=message) raise click.BadOptionUsage(option_name='--dev', message=message)
return actions.destroy_configuration(character_config=alice_config, force=force) return actions.destroy_configuration(emitter, character_config=alice_config, force=force)
# #
# Alice API # Alice API

View File

@ -60,9 +60,9 @@ def bob(click_config,
# #
# Banner # Banner
click.clear() emitter = click_config.emitter
if not click_config.json_ipc and not click_config.quiet: emitter.clear()
click.secho(BOB_BANNER) emitter.banner(BOB_BANNER)
# #
# Eager Actions # Eager Actions
@ -86,13 +86,13 @@ def bob(click_config,
registry_filepath=registry_filepath, registry_filepath=registry_filepath,
provider_uri=provider_uri) provider_uri=provider_uri)
return painting.paint_new_installation_help(new_configuration=new_bob_config) return painting.paint_new_installation_help(emitter, new_configuration=new_bob_config)
# TODO # TODO
# elif action == "view": # elif action == "view":
# """Paint an existing configuration to the console""" # """Paint an existing configuration to the console"""
# response = BobConfiguration._read_configuration_file(filepath=config_file or bob_config.config_file_location) # response = BobConfiguration._read_configuration_file(filepath=config_file or bob_config.config_file_location)
# return BOB.controller.emitter(response=response) # return BOB.controller.emitter.ipc(response)
# #
# Make Bob # Make Bob
@ -131,9 +131,6 @@ def bob(click_config,
if action == "run": if action == "run":
# Echo Public Keys
click_config.emit(message=f"Bob Verifying Key {bytes(BOB.stamp).hex()}", color='green', bold=True)
# RPC # RPC
if click_config.json_ipc: if click_config.json_ipc:
rpc_controller = BOB.make_rpc_controller() rpc_controller = BOB.make_rpc_controller()
@ -141,9 +138,10 @@ def bob(click_config,
rpc_controller.start() rpc_controller.start()
return return
click_config.emit(message=f"Bob Verifying Key {bytes(BOB.stamp).hex()}", color='green', bold=True) # Echo Public Keys
emitter.message(f"Bob Verifying Key {bytes(BOB.stamp).hex()}", color='green', bold=True)
bob_encrypting_key = bytes(BOB.public_keys(DecryptingPower)).hex() bob_encrypting_key = bytes(BOB.public_keys(DecryptingPower)).hex()
click_config.emit(message=f"Bob Encrypting Key {bob_encrypting_key}", color="blue", bold=True) emitter.message(f"Bob Encrypting Key {bob_encrypting_key}", color="blue", bold=True)
# Start Controller # Start Controller
controller = BOB.make_web_controller(crash_on_error=click_config.debug) controller = BOB.make_web_controller(crash_on_error=click_config.debug)
@ -159,7 +157,7 @@ def bob(click_config,
raise click.BadOptionUsage(option_name='--dev', message=message) raise click.BadOptionUsage(option_name='--dev', message=message)
# Request # Request
return actions.destroy_configuration(character_config=bob_config) return actions.destroy_configuration(emitter, character_config=bob_config)
# #
# Bob API Actions # Bob API Actions

View File

@ -28,9 +28,9 @@ def enrico(click_config, action, policy_encrypting_key, dry_run, http_port, mess
raise click.BadArgumentUsage('--policy-encrypting-key is required to start Enrico.') raise click.BadArgumentUsage('--policy-encrypting-key is required to start Enrico.')
# Banner # Banner
click.clear() emitter = click_config.emitter
if not click_config.json_ipc and not click_config.quiet: emitter.clear()
click.secho(ENRICO_BANNER) emitter.banner(ENRICO_BANNER)
# #
# Make Enrico # Make Enrico
@ -38,8 +38,7 @@ def enrico(click_config, action, policy_encrypting_key, dry_run, http_port, mess
policy_encrypting_key = UmbralPublicKey.from_bytes(bytes.fromhex(policy_encrypting_key)) policy_encrypting_key = UmbralPublicKey.from_bytes(bytes.fromhex(policy_encrypting_key))
ENRICO = Enrico(policy_encrypting_key=policy_encrypting_key) ENRICO = Enrico(policy_encrypting_key=policy_encrypting_key)
if click_config.json_ipc: ENRICO.controller.emitter = emitter # TODO: set it on object creation? Or not set at all?
ENRICO.controller.emitter = JSONRPCStdoutEmitter(quiet=click_config.quiet)
# #
# Actions # Actions

View File

@ -54,10 +54,11 @@ def felix(click_config,
dev, dev,
force): force):
emitter = click_config.emitter
# Intro # Intro
click.clear() emitter.clear()
if not click_config.quiet: emitter.banner(FELIX_BANNER.format(checksum_address or ''))
click.secho(FELIX_BANNER.format(checksum_address or ''))
ETH_NODE = NO_BLOCKCHAIN_CONNECTION ETH_NODE = NO_BLOCKCHAIN_CONNECTION
if geth: if geth:
@ -87,11 +88,11 @@ def felix(click_config,
if click_config.debug: if click_config.debug:
raise raise
else: else:
click.secho(str(e), fg='red', bold=True) emitter.echo(str(e), color='red', bold=True)
raise click.Abort raise click.Abort
# Paint Help # Paint Help
painting.paint_new_installation_help(new_configuration=new_felix_config) painting.paint_new_installation_help(emitter, new_configuration=new_felix_config)
return # <-- do not remove (conditional flow control) return # <-- do not remove (conditional flow control)
@ -111,8 +112,8 @@ def felix(click_config,
poa=poa) poa=poa)
except FileNotFoundError: except FileNotFoundError:
click.secho(f"No Felix configuration file found at {config_file}. " emitter.echo(f"No Felix configuration file found at {config_file}. "
f"Check the filepath or run 'nucypher felix init' to create a new system configuration.") f"Check the filepath or run 'nucypher felix init' to create a new system configuration.")
raise click.Abort raise click.Abort
try: try:
@ -121,10 +122,13 @@ def felix(click_config,
felix_config.get_blockchain_interface() felix_config.get_blockchain_interface()
# Authenticate # Authenticate
unlock_nucypher_keyring(character_configuration=felix_config, password=get_password(confirm=False)) unlock_nucypher_keyring(emitter,
character_configuration=felix_config,
password=get_password(confirm=False))
# Produce Teacher Ursulas # Produce Teacher Ursulas
teacher_nodes = actions.load_seednodes(teacher_uris=[teacher_uri] if teacher_uri else None, teacher_nodes = actions.load_seednodes(emitter,
teacher_uris=[teacher_uri] if teacher_uri else None,
min_stake=min_stake, min_stake=min_stake,
federated_only=felix_config.federated_only, federated_only=felix_config.federated_only,
network_domains=felix_config.domains, network_domains=felix_config.domains,
@ -138,7 +142,7 @@ def felix(click_config,
if click_config.debug: if click_config.debug:
raise raise
else: else:
click.secho(str(e), fg='red', bold=True) emitter.echo(str(e), color='red', bold=True)
raise click.Abort raise click.Abort
if action == "createdb": # Initialize Database if action == "createdb": # Initialize Database
@ -146,15 +150,15 @@ def felix(click_config,
if not force: if not force:
click.confirm("Overwrite existing database?", abort=True) click.confirm("Overwrite existing database?", abort=True)
os.remove(FELIX.db_filepath) os.remove(FELIX.db_filepath)
click.secho(f"Destroyed existing database {FELIX.db_filepath}") emitter.echo(f"Destroyed existing database {FELIX.db_filepath}")
FELIX.create_tables() FELIX.create_tables()
click.secho(f"\nCreated new database at {FELIX.db_filepath}", fg='green') emitter.echo(f"\nCreated new database at {FELIX.db_filepath}", color='green')
elif action == 'view': elif action == 'view':
token_balance = FELIX.token_balance token_balance = FELIX.token_balance
eth_balance = FELIX.eth_balance eth_balance = FELIX.eth_balance
click.secho(f""" emitter.echo(f"""
Address .... {FELIX.checksum_address} Address .... {FELIX.checksum_address}
NU ......... {str(token_balance)} NU ......... {str(token_balance)}
ETH ........ {str(eth_balance)} ETH ........ {str(eth_balance)}
@ -163,17 +167,17 @@ ETH ........ {str(eth_balance)}
elif action == "accounts": elif action == "accounts":
accounts = FELIX.blockchain.client.accounts accounts = FELIX.blockchain.client.accounts
for account in accounts: for account in accounts:
click.secho(account) emitter.echo(account)
elif action == "destroy": elif action == "destroy":
"""Delete all configuration files from the disk""" """Delete all configuration files from the disk"""
actions.destroy_configuration(character_config=felix_config, force=force) actions.destroy_configuration(emitter, character_config=felix_config, force=force)
elif action == 'run': # Start web services elif action == 'run': # Start web services
try: try:
click.secho("Waiting for blockchain sync...", fg='yellow') emitter.echo("Waiting for blockchain sync...", color='yellow')
click_config.emit(message=f"Running Felix on {host}:{port}") emitter.message(f"Running Felix on {host}:{port}")
FELIX.start(host=host, FELIX.start(host=host,
port=port, port=port,
web_services=not dry_run, web_services=not dry_run,

View File

@ -23,14 +23,16 @@ def moe(click_config, teacher_uri, min_stake, network, ws_port, dry_run, http_po
"Moe" NuCypher node monitor CLI. "Moe" NuCypher node monitor CLI.
""" """
emitter = click_config.emitter
# Banner # Banner
click.clear() emitter.clear()
if not click_config.json_ipc and not click_config.quiet: emitter.banner(MOE_BANNER)
click.secho(MOE_BANNER)
# Teacher Ursula # Teacher Ursula
teacher_uris = [teacher_uri] if teacher_uri else None teacher_uris = [teacher_uri] if teacher_uri else None
teacher_nodes = actions.load_seednodes(teacher_uris=teacher_uris, teacher_nodes = actions.load_seednodes(emitter,
teacher_uris=teacher_uris,
min_stake=min_stake, min_stake=min_stake,
federated_only=True, # TODO: hardcoded for now. Is Moe a Character? federated_only=True, # TODO: hardcoded for now. Is Moe a Character?
network_domains={network} if network else None, network_domains={network} if network else None,
@ -45,6 +47,6 @@ def moe(click_config, teacher_uri, min_stake, network, ws_port, dry_run, http_po
# Run # Run
MOE.start_learning_loop(now=learn_on_launch) MOE.start_learning_loop(now=learn_on_launch)
click_config.emit(message=f"Running Moe on 127.0.0.1:{http_port}") emitter.message(f"Running Moe on 127.0.0.1:{http_port}")
MOE.start(http_port=http_port, ws_port=ws_port, dry_run=dry_run) MOE.start(http_port=http_port, ws_port=ws_port, dry_run=dry_run)

View File

@ -109,6 +109,8 @@ def ursula(click_config,
""" """
emitter = click_config.emitter
# #
# Validate # Validate
# #
@ -123,8 +125,7 @@ def ursula(click_config,
message="Staking address canot be used in federated mode.") message="Staking address canot be used in federated mode.")
# Banner # Banner
if not click_config.json_ipc and not click_config.quiet: emitter.banner(URSULA_BANNER.format(worker_address or ''))
click.secho(URSULA_BANNER.format(worker_address or ''))
# #
# Pre-Launch Warnings # Pre-Launch Warnings
@ -132,9 +133,9 @@ def ursula(click_config,
if not click_config.quiet: if not click_config.quiet:
if dev: if dev:
click.secho("WARNING: Running in Development mode", fg='yellow') emitter.echo("WARNING: Running in Development mode", color='yellow')
if force: if force:
click.secho("WARNING: Force is enabled", fg='yellow') emitter.echo("WARNING: Force is enabled", color='yellow')
# #
# Internal Ethereum Client # Internal Ethereum Client
@ -166,16 +167,16 @@ def ursula(click_config,
blockchain.connect(fetch_registry=False) blockchain.connect(fetch_registry=False)
if not staker_address: if not staker_address:
staker_address = select_client_account(blockchain=blockchain) staker_address = select_client_account(emitter=emitter, blockchain=blockchain)
if not worker_address: if not worker_address:
worker_address = select_client_account(blockchain=blockchain) worker_address = select_client_account(emitter=emitter, blockchain=blockchain)
if not config_root: # Flag if not config_root: # Flag
config_root = click_config.config_file # Envvar config_root = click_config.config_file # Envvar
if not rest_host: if not rest_host:
rest_host = actions.determine_external_ip_address(force=force) rest_host = actions.determine_external_ip_address(emitter, force=force)
ursula_config = UrsulaConfiguration.generate(password=get_password(confirm=True), ursula_config = UrsulaConfiguration.generate(password=get_password(confirm=True),
config_root=config_root, config_root=config_root,
@ -192,7 +193,7 @@ def ursula(click_config,
provider_uri=provider_uri, provider_uri=provider_uri,
poa=poa) poa=poa)
painting.paint_new_installation_help(new_configuration=ursula_config) painting.paint_new_installation_help(emitter, new_configuration=ursula_config)
return return
# #
@ -232,7 +233,7 @@ def ursula(click_config,
if click_config.debug: if click_config.debug:
raise raise
else: else:
click.secho(str(e), fg='red', bold=True) emitter.echo(str(e), color='red', bold=True)
raise click.Abort raise click.Abort
# #
@ -245,7 +246,7 @@ def ursula(click_config,
if dev: if dev:
message = "'nucypher ursula destroy' cannot be used in --dev mode - There is nothing to destroy." message = "'nucypher ursula destroy' cannot be used in --dev mode - There is nothing to destroy."
raise click.BadOptionUsage(option_name='--dev', message=message) raise click.BadOptionUsage(option_name='--dev', message=message)
return actions.destroy_configuration(character_config=ursula_config, force=force) return actions.destroy_configuration(emitter, character_config=ursula_config, force=force)
# #
# Make Ursula # Make Ursula
@ -269,23 +270,23 @@ def ursula(click_config,
try: try:
# Ursula Deploy Warnings # Ursula Deploy Warnings
click_config.emit( emitter.message(
message="Starting Ursula on {}".format(URSULA.rest_interface), f"Starting Ursula on {URSULA.rest_interface}",
color='green', color='green',
bold=True) bold=True)
click_config.emit( emitter.message(
message="Connecting to {}".format(','.join(ursula_config.domains)), f"Connecting to {','.join(ursula_config.domains)}",
color='green', color='green',
bold=True) bold=True)
click_config.emit( emitter.message(
message=f"Working ~ Keep Ursula Online!", "Working ~ Keep Ursula Online!",
color='blue', color='blue',
bold=True) bold=True)
if interactive: if interactive:
stdio.StandardIO(UrsulaCommandProtocol(ursula=URSULA)) stdio.StandardIO(UrsulaCommandProtocol(ursula=URSULA, emitter=emitter))
if dry_run: if dry_run:
return # <-- ABORT - (Last Chance) return # <-- ABORT - (Last Chance)
@ -299,47 +300,48 @@ def ursula(click_config,
# Handle Crash # Handle Crash
except Exception as e: except Exception as e:
ursula_config.log.critical(str(e)) ursula_config.log.critical(str(e))
click_config.emit( emitter.message(
message="{} {}".format(e.__class__.__name__, str(e)), f"{e.__class__.__name__} {e}",
color='red', color='red',
bold=True) bold=True)
raise # Crash :-( raise # Crash :-(
# Graceful Exit # Graceful Exit
finally: finally:
click_config.emit(message="Stopping Ursula", color='green') emitter.message("Stopping Ursula", color='green')
ursula_config.cleanup() ursula_config.cleanup()
click_config.emit(message="Ursula Stopped", color='red') emitter.message("Ursula Stopped", color='red')
return return
elif action == "save-metadata": elif action == "save-metadata":
"""Manually save a node self-metadata file""" """Manually save a node self-metadata file"""
metadata_path = ursula.write_node_metadata(node=URSULA) metadata_path = ursula.write_node_metadata(node=URSULA)
return click_config.emit(message="Successfully saved node metadata to {}.".format(metadata_path), color='green') emitter.message(f"Successfully saved node metadata to {metadata_path}.", color='green')
return
elif action == "view": elif action == "view":
"""Paint an existing configuration to the console""" """Paint an existing configuration to the console"""
if not URSULA.federated_only: if not URSULA.federated_only:
click.secho("BLOCKCHAIN ----------\n") emitter.echo("BLOCKCHAIN ----------\n")
painting.paint_contract_status(click_config=click_config, ursula_config=ursula_config) painting.paint_contract_status(emitter, ursula_config=ursula_config)
current_block = URSULA.blockchain.w3.eth.blockNumber current_block = URSULA.blockchain.w3.eth.blockNumber
click.secho(f'Block # {current_block}') emitter.echo(f'Block # {current_block}')
click.secho(f'NU Balance: {URSULA.token_balance}') emitter.echo(f'NU Balance: {URSULA.token_balance}')
click.secho(f'ETH Balance: {URSULA.eth_balance}') emitter.echo(f'ETH Balance: {URSULA.eth_balance}')
click.secho(f'Current Gas Price {URSULA.blockchain.client.gasPrice}') emitter.echo(f'Current Gas Price {URSULA.blockchain.client.gasPrice}')
click.secho("CONFIGURATION --------") emitter.echo("CONFIGURATION --------")
response = UrsulaConfiguration._read_configuration_file(filepath=config_file or ursula_config.config_file_location) response = UrsulaConfiguration._read_configuration_file(filepath=config_file or ursula_config.config_file_location)
return click_config.emit(response=response) return emitter.ipc(response=response, request_id=0, duration=0) # FIXME: what are request_id and duration here?
elif action == "forget": elif action == "forget":
actions.forget(configuration=ursula_config) actions.forget(emitter, configuration=ursula_config)
return return
elif action == 'confirm-activity': elif action == 'confirm-activity':
if not URSULA.stakes: if not URSULA.stakes:
click.secho("There are no active stakes for {}".format(URSULA.checksum_address)) emitter.echo(f"There are no active stakes for {URSULA.checksum_address}")
return return
URSULA.staking_agent.confirm_activity(node_address=URSULA.checksum_address) URSULA.staking_agent.confirm_activity(node_address=URSULA.checksum_address)
return return

View File

@ -33,7 +33,6 @@ from nucypher.utilities.sandbox.middleware import MockRestMiddleware
class NucypherClickConfig: class NucypherClickConfig:
# Output Sinks # Output Sinks
capture_stdout = False
__emitter = None __emitter = None
# Environment Variables # Environment Variables
@ -60,9 +59,9 @@ class NucypherClickConfig:
# Session Emitter for pre and post character control engagement. # Session Emitter for pre and post character control engagement.
if json_ipc: if json_ipc:
emitter = JSONRPCStdoutEmitter(quiet=quiet, capture_stdout=NucypherClickConfig.capture_stdout) emitter = JSONRPCStdoutEmitter(quiet=quiet)
else: else:
emitter = StdoutEmitter(quiet=quiet, capture_stdout=NucypherClickConfig.capture_stdout) emitter = StdoutEmitter(quiet=quiet)
self.attach_emitter(emitter) self.attach_emitter(emitter)
@ -117,22 +116,22 @@ class NucypherClickConfig:
# Only used for testing outputs; # Only used for testing outputs;
# Redirects outputs to in-memory python containers. # Redirects outputs to in-memory python containers.
if mock_networking: if mock_networking:
self.emit(message="WARNING: Mock networking is enabled") self.emitter.message("WARNING: Mock networking is enabled")
self.middleware = MockRestMiddleware() self.middleware = MockRestMiddleware()
else: else:
self.middleware = RestMiddleware() self.middleware = RestMiddleware()
# Global Warnings # Global Warnings
if self.verbose: if self.verbose:
self.emit(message="Verbose mode is enabled", color='blue') self.emitter.message("Verbose mode is enabled", color='blue')
@classmethod @classmethod
def attach_emitter(cls, emitter) -> None: def attach_emitter(cls, emitter) -> None:
cls.__emitter = emitter cls.__emitter = emitter
@classmethod @property
def emit(cls, *args, **kwargs): def emitter(cls):
cls.__emitter(*args, **kwargs) return cls.__emitter
# Register the above click configuration classes as a decorators # Register the above click configuration classes as a decorators

View File

@ -30,6 +30,7 @@ from nucypher.blockchain.eth.registry import EthereumContractRegistry
from nucypher.blockchain.eth.sol.compile import SolidityCompiler from nucypher.blockchain.eth.sol.compile import SolidityCompiler
from nucypher.characters.banners import NU_BANNER from nucypher.characters.banners import NU_BANNER
from nucypher.cli import actions from nucypher.cli import actions
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.cli.actions import get_password, select_client_account from nucypher.cli.actions import get_password, select_client_account
from nucypher.cli.painting import paint_contract_deployment from nucypher.cli.painting import paint_contract_deployment
from nucypher.cli.types import EIP55_CHECKSUM_ADDRESS, EXISTING_READABLE_FILE from nucypher.cli.types import EIP55_CHECKSUM_ADDRESS, EXISTING_READABLE_FILE
@ -79,6 +80,8 @@ def deploy(action,
ETH_NODE = None ETH_NODE = None
emitter = StdoutEmitter()
# #
# Validate # Validate
# #
@ -115,7 +118,7 @@ def deploy(action,
try: try:
blockchain.connect(fetch_registry=False, sync_now=sync) blockchain.connect(fetch_registry=False, sync_now=sync)
except BlockchainDeployerInterface.ConnectionFailed as e: except BlockchainDeployerInterface.ConnectionFailed as e:
click.secho(str(e), fg='red', bold=True) emitter.echo(str(e), color='red', bold=True)
raise click.Abort() raise click.Abort()
# #
@ -123,7 +126,7 @@ def deploy(action,
# #
if not deployer_address: if not deployer_address:
deployer_address = select_client_account(blockchain=blockchain) deployer_address = select_client_account(emitter=emitter, blockchain=blockchain)
# Verify Address # Verify Address
if not force: if not force:
@ -138,9 +141,9 @@ def deploy(action,
deployer_address=deployer_address) deployer_address=deployer_address)
# Verify ETH Balance # Verify ETH Balance
click.secho(f"\n\nDeployer ETH balance: {deployer.eth_balance}") emitter.echo(f"\n\nDeployer ETH balance: {deployer.eth_balance}")
if deployer.eth_balance == 0: if deployer.eth_balance == 0:
click.secho("Deployer address has no ETH.", fg='red', bold=True) emitter.echo("Deployer address has no ETH.", color='red', bold=True)
raise click.Abort() raise click.Abort()
# Add ETH Bootnode or Peer # Add ETH Bootnode or Peer
@ -179,7 +182,7 @@ def deploy(action,
contract_deployer = deployer.deployers[contract_name] contract_deployer = deployer.deployers[contract_name]
except KeyError: except KeyError:
message = f"No such contract {contract_name}. Available contracts are {deployer.deployers.keys()}" message = f"No such contract {contract_name}. Available contracts are {deployer.deployers.keys()}"
click.secho(message, fg='red', bold=True) emitter.echo(message, color='red', bold=True)
raise click.Abort() raise click.Abort()
else: else:
click.secho(f"Deploying {contract_name}") click.secho(f"Deploying {contract_name}")
@ -208,34 +211,33 @@ def deploy(action,
# #
secrets = deployer.collect_deployment_secrets() secrets = deployer.collect_deployment_secrets()
click.clear() emitter.clear()
click.secho(NU_BANNER) emitter.banner(NU_BANNER)
click.secho(f"Current Time ........ {maya.now().iso8601()}") emitter.echo(f"Current Time ........ {maya.now().iso8601()}")
click.secho(f"Web3 Provider ....... {deployer.blockchain.provider_uri}") emitter.echo(f"Web3 Provider ....... {deployer.blockchain.provider_uri}")
click.secho(f"Block ............... {deployer.blockchain.client.block_number}") emitter.echo(f"Block ............... {deployer.blockchain.client.block_number}")
click.secho(f"Gas Price ........... {deployer.blockchain.client.gas_price}") emitter.echo(f"Gas Price ........... {deployer.blockchain.client.gas_price}")
click.secho(f"Deployer Address .... {deployer.checksum_address}") emitter.echo(f"Deployer Address .... {deployer.checksum_address}")
click.secho(f"ETH ................. {deployer.eth_balance}") emitter.echo(f"ETH ................. {deployer.eth_balance}")
click.secho(f"Chain ID ............ {deployer.blockchain.client.chain_id}") emitter.echo(f"Chain ID ............ {deployer.blockchain.client.chain_id}")
click.secho(f"Chain Name .......... {deployer.blockchain.client.chain_name}") emitter.echo(f"Chain Name .......... {deployer.blockchain.client.chain_name}")
# Ask - Last chance to gracefully abort. This step cannot be forced. # Ask - Last chance to gracefully abort. This step cannot be forced.
click.secho("\nDeployment successfully staged. Take a deep breath. \n", fg='green') emitter.echo("\nDeployment successfully staged. Take a deep breath. \n", color='green')
# Trigger Deployment # Trigger Deployment
if not actions.confirm_deployment(deployer=deployer): if not actions.confirm_deployment(emitter=emitter, deployer=deployer):
raise click.Abort() raise click.Abort()
# Delay - Last chance to crash and abort # Delay - Last chance to crash and abort
click.secho(f"Starting deployment in 3 seconds...", fg='red') emitter.echo(f"Starting deployment in 3 seconds...", color='red')
time.sleep(1) time.sleep(1)
click.secho(f"2...", fg='yellow') emitter.echo(f"2...", color='yellow')
time.sleep(1) time.sleep(1)
click.secho(f"1...", fg='green') emitter.echo(f"1...", color='green')
time.sleep(1) time.sleep(1)
click.secho(f"Deploying...", bold=True) emitter.echo(f"Deploying...", bold=True)
# #
# DEPLOY # DEPLOY
@ -248,13 +250,14 @@ def deploy(action,
# Paint outfile paths # Paint outfile paths
# TODO: Echo total gas used. # TODO: Echo total gas used.
# click.secho("Cumulative Gas Consumption: {} gas".format(total_gas_used), bold=True, fg='blue') # emitter.echo(f"Cumulative Gas Consumption: {total_gas_used} gas", bold=True, color='blue')
registry_outfile = deployer.blockchain.registry.filepath registry_outfile = deployer.blockchain.registry.filepath
click.secho('Generated registry {}'.format(registry_outfile), bold=True, fg='blue') emitter.echo('Generated registry {}'.format(registry_outfile), bold=True, color='blue')
# Save transaction metadata # Save transaction metadata
receipts_filepath = deployer.save_deployment_receipts(receipts=deployment_receipts) receipts_filepath = deployer.save_deployment_receipts(receipts=deployment_receipts)
click.secho(f"Saved deployment receipts to {receipts_filepath}", fg='blue', bold=True) emitter.echo(f"Saved deployment receipts to {receipts_filepath}", color='blue', bold=True)
elif action == "allocations": elif action == "allocations":
if not allocation_infile: if not allocation_infile:
@ -267,7 +270,7 @@ def deploy(action,
token_agent = NucypherTokenAgent(blockchain=blockchain) token_agent = NucypherTokenAgent(blockchain=blockchain)
click.confirm(f"Transfer {amount} from {token_agent.contract_address} to {recipient_address}?", abort=True) click.confirm(f"Transfer {amount} from {token_agent.contract_address} to {recipient_address}?", abort=True)
txhash = token_agent.transfer(amount=amount, sender_address=token_agent.contract_address, target_address=recipient_address) txhash = token_agent.transfer(amount=amount, sender_address=token_agent.contract_address, target_address=recipient_address)
click.secho(f"OK | {txhash}") emitter.echo(f"OK | {txhash}")
else: else:
raise click.BadArgumentUsage(message=f"Unknown action '{action}'") raise click.BadArgumentUsage(message=f"Unknown action '{action}'")

View File

@ -24,11 +24,8 @@ from constant_sorrow.constants import NO_KNOWN_NODES
from nucypher.blockchain.eth.interfaces import BlockchainInterface from nucypher.blockchain.eth.interfaces import BlockchainInterface
from nucypher.blockchain.eth.utils import datetime_at_period from nucypher.blockchain.eth.utils import datetime_at_period
from nucypher.characters.banners import NUCYPHER_BANNER from nucypher.characters.banners import NUCYPHER_BANNER
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.config.constants import SEEDNODES from nucypher.config.constants import SEEDNODES
emitter = StdoutEmitter()
def echo_version(ctx, param, value): def echo_version(ctx, param, value):
if not value or ctx.resilient_parsing: if not value or ctx.resilient_parsing:
@ -37,25 +34,25 @@ def echo_version(ctx, param, value):
ctx.exit() ctx.exit()
def paint_new_installation_help(new_configuration): def paint_new_installation_help(emitter, new_configuration):
character_config_class = new_configuration.__class__ character_config_class = new_configuration.__class__
character_name = character_config_class._NAME.lower() character_name = character_config_class._NAME.lower()
emitter(message="Generated keyring {}".format(new_configuration.keyring_root), color='green') emitter.message("Generated keyring {}".format(new_configuration.keyring_root), color='green')
emitter(message="Saved configuration file {}".format(new_configuration.config_file_location), color='green') emitter.message("Saved configuration file {}".format(new_configuration.config_file_location), color='green')
# Felix # Felix
if character_name == 'felix': if character_name == 'felix':
suggested_db_command = 'nucypher felix createdb' suggested_db_command = 'nucypher felix createdb'
how_to_proceed_message = f'\nTo initialize a new faucet database run:' how_to_proceed_message = f'\nTo initialize a new faucet database run:'
emitter(message=how_to_proceed_message, color='green') emitter.message(how_to_proceed_message, color='green')
emitter(message=f'\n\'{suggested_db_command}\'', color='green') emitter.message(f'\n\'{suggested_db_command}\'', color='green')
# Ursula # Ursula
elif character_name == 'ursula' and not new_configuration.federated_only: elif character_name == 'ursula' and not new_configuration.federated_only:
suggested_staking_command = f'nucypher ursula stake' suggested_staking_command = f'nucypher ursula stake'
how_to_stake_message = f"\nTo initialize a NU stake, run '{suggested_staking_command}' or" how_to_stake_message = f"\nTo initialize a NU stake, run '{suggested_staking_command}' or"
emitter(message=how_to_stake_message, color='green') emitter.message(how_to_stake_message, color='green')
# Everyone: Give the use a suggestion as to what to do next # Everyone: Give the use a suggestion as to what to do next
vowels = ('a', 'e', 'i', 'o', 'u') vowels = ('a', 'e', 'i', 'o', 'u')
@ -64,7 +61,7 @@ def paint_new_installation_help(new_configuration):
suggested_command = f'nucypher {character_name} run' suggested_command = f'nucypher {character_name} run'
how_to_run_message = f"\nTo run {adjective} {character_name.capitalize()} node from the default configuration filepath run: \n\n'{suggested_command}'\n" how_to_run_message = f"\nTo run {adjective} {character_name.capitalize()} node from the default configuration filepath run: \n\n'{suggested_command}'\n"
return emitter(message=how_to_run_message.format(suggested_command), color='green') emitter.message(how_to_run_message.format(suggested_command), color='green')
def build_fleet_state_status(ursula) -> str: def build_fleet_state_status(ursula) -> str:
@ -84,7 +81,7 @@ def build_fleet_state_status(ursula) -> str:
return fleet_state return fleet_state
def paint_node_status(ursula, start_time): def paint_node_status(emitter, ursula, start_time):
# Build Learning status line # Build Learning status line
learning_status = "Unknown" learning_status = "Unknown"
@ -119,10 +116,10 @@ def paint_node_status(ursula, start_time):
current_period = f'Current Period ...... {ursula.staking_agent.get_current_period()}' current_period = f'Current Period ...... {ursula.staking_agent.get_current_period()}'
stats.extend([current_period, staking_address]) stats.extend([current_period, staking_address])
click.echo('\n' + '\n'.join(stats) + '\n') emitter.echo('\n' + '\n'.join(stats) + '\n')
def paint_known_nodes(ursula) -> None: def paint_known_nodes(emitter, ursula) -> None:
# Gather Data # Gather Data
known_nodes = ursula.known_nodes known_nodes = ursula.known_nodes
number_of_known_nodes = len(ursula.node_storage.all(federated_only=ursula.federated_only)) number_of_known_nodes = len(ursula.node_storage.all(federated_only=ursula.federated_only))
@ -131,17 +128,17 @@ def paint_known_nodes(ursula) -> None:
# Operating Mode # Operating Mode
federated_only = ursula.federated_only federated_only = ursula.federated_only
if federated_only: if federated_only:
click.secho("Configured in Federated Only mode", fg='green') emitter.echo("Configured in Federated Only mode", color='green')
# Heading # Heading
label = "Known Nodes (connected {} / seen {})".format(number_of_known_nodes, seen_nodes) label = "Known Nodes (connected {} / seen {})".format(number_of_known_nodes, seen_nodes)
heading = '\n' + label + " " * (45 - len(label)) heading = '\n' + label + " " * (45 - len(label))
click.secho(heading, bold=True, nl=True) emitter.echo(heading, bold=True)
# Build FleetState status line # Build FleetState status line
fleet_state = build_fleet_state_status(ursula=ursula) fleet_state = build_fleet_state_status(ursula=ursula)
fleet_status_line = 'Fleet State {}'.format(fleet_state) fleet_status_line = 'Fleet State {}'.format(fleet_state)
click.secho(fleet_status_line, fg='blue', bold=True, nl=True) emitter.echo(fleet_status_line, color='blue', bold=True)
# Legend # Legend
color_index = { color_index = {
@ -152,8 +149,8 @@ def paint_known_nodes(ursula) -> None:
# Ledgend # Ledgend
# for node_type, color in color_index.items(): # for node_type, color in color_index.items():
# click.secho('{0:<6} | '.format(node_type), fg=color, nl=False) # emitter.echo('{0:<6} | '.format(node_type), color=color, nl=False)
# click.echo('\n') # emitter.echo('\n')
seednode_addresses = list(bn.checksum_address for bn in SEEDNODES) seednode_addresses = list(bn.checksum_address for bn in SEEDNODES)
@ -166,10 +163,10 @@ def paint_known_nodes(ursula) -> None:
elif node.checksum_address in seednode_addresses: elif node.checksum_address in seednode_addresses:
node_type = 'seednode' node_type = 'seednode'
row_template += ' ({})'.format(node_type) row_template += ' ({})'.format(node_type)
click.secho(row_template.format(node.rest_url().ljust(20), node), fg=color_index[node_type]) emitter.echo(row_template.format(node.rest_url().ljust(20), node), color=color_index[node_type])
def paint_contract_status(ursula_config, click_config): def paint_contract_status(emitter, ursula_config):
contract_payload = """ contract_payload = """
| NuCypher ETH Contracts | | NuCypher ETH Contracts |
@ -187,7 +184,7 @@ PolicyManager ............ {manager}
escrow=ursula_config.staking_agent.contract_address, escrow=ursula_config.staking_agent.contract_address,
manager=ursula_config.policy_agent.contract_address, manager=ursula_config.policy_agent.contract_address,
period=ursula_config.staking_agent.get_current_period()) period=ursula_config.staking_agent.get_current_period())
click.secho(contract_payload) emitter.echo(contract_payload)
network_payload = """ network_payload = """
| Blockchain Network | | Blockchain Network |
@ -199,10 +196,11 @@ Active Staking Ursulas ... {ursulas}
""".format(period=ursula_config.staking_agent.get_current_period(), """.format(period=ursula_config.staking_agent.get_current_period(),
gas_price=ursula_config.blockchain.client.gasPrice, gas_price=ursula_config.blockchain.client.gasPrice,
ursulas=ursula_config.staking_agent.get_staker_population()) ursulas=ursula_config.staking_agent.get_staker_population())
click.secho(network_payload) emitter.echo(network_payload)
def paint_staged_stake(ursula, def paint_staged_stake(emitter,
ursula,
stake_value, stake_value,
duration, duration,
start_period, start_period,
@ -210,12 +208,12 @@ def paint_staged_stake(ursula,
division_message: str = None): division_message: str = None):
if division_message: if division_message:
click.secho(f"\n{'=' * 30} ORIGINAL STAKE {'=' * 28}", bold=True) emitter.echo(f"\n{'=' * 30} ORIGINAL STAKE {'=' * 28}", bold=True)
click.secho(division_message) emitter.echo(division_message)
click.secho(f"\n{'=' * 30} STAGED STAKE {'=' * 30}", bold=True) emitter.echo(f"\n{'=' * 30} STAGED STAKE {'=' * 30}", bold=True)
click.echo(f""" emitter.echo(f"""
{ursula} {ursula}
~ Chain -> ID # {ursula.blockchain.client.chain_id} | {ursula.blockchain.client.chain_name} ~ Chain -> ID # {ursula.blockchain.client.chain_id} | {ursula.blockchain.client.chain_name}
~ Value -> {stake_value} ({Decimal(int(stake_value)):.2E} NuNits) ~ Value -> {stake_value} ({Decimal(int(stake_value)):.2E} NuNits)
@ -224,20 +222,20 @@ def paint_staged_stake(ursula,
~ Expiration -> {datetime_at_period(period=end_period)} (period #{end_period}) ~ Expiration -> {datetime_at_period(period=end_period)} (period #{end_period})
""") """)
click.secho('=========================================================================', bold=True) emitter.echo('=========================================================================', bold=True)
def paint_staking_confirmation(ursula, transactions): def paint_staking_confirmation(emitter, ursula, transactions):
click.secho(f'\nEscrow Address ... {ursula.staking_agent.contract_address}', fg='blue') emitter.echo(f'\nEscrow Address ... {ursula.staking_agent.contract_address}', color='blue')
for tx_name, receipt in transactions.items(): for tx_name, receipt in transactions.items():
click.secho(f'{tx_name.capitalize()} .......... {receipt["transactionHash"].hex()}', fg='green') emitter.echo(f'{tx_name.capitalize()} .......... {receipt["transactionHash"].hex()}', color='green')
click.secho(f''' emitter.echo(f'''
Successfully transmitted stake initialization transactions. Successfully transmitted stake initialization transactions.
View your stakes by running 'nucypher stake list' View your stakes by running 'nucypher stake list'
or set your Ursula worker node address by running 'nucypher stake set-worker'. or set your Ursula worker node address by running 'nucypher stake set-worker'.
''', fg='green') ''', color='green')
def prettify_stake(stake, index: int = None) -> str: def prettify_stake(stake, index: int = None) -> str:
@ -259,25 +257,25 @@ def prettify_stake(stake, index: int = None) -> str:
return pretty return pretty
def paint_stakes(stakes): def paint_stakes(emitter, stakes):
title = "=========================== Active Stakes ==============================\n" title = "=========================== Active Stakes ==============================\n"
header = f'| ~ | Staker | Worker | # | Value | Duration | Enactment ' header = f'| ~ | Staker | Worker | # | Value | Duration | Enactment '
breaky = f'| | ------ | ------ | - | -------- | ------------ | ------------------ ' breaky = f'| | ------ | ------ | - | -------- | ------------ | ------------------ '
click.secho(title) emitter.echo(title)
click.secho(header, bold=True) emitter.echo(header, bold=True)
click.secho(breaky, bold=True) emitter.echo(breaky, bold=True)
for index, stake in enumerate(stakes): for index, stake in enumerate(stakes):
row = prettify_stake(stake=stake, index=index) row = prettify_stake(stake=stake, index=index)
row_color = 'yellow' if stake.worker_address == BlockchainInterface.NULL_ADDRESS else 'white' row_color = 'yellow' if stake.worker_address == BlockchainInterface.NULL_ADDRESS else 'white'
click.secho(row, fg=row_color) emitter.echo(row, color=row_color)
click.secho('') # newline emitter.echo('') # newline
return
def paint_staged_stake_division(ursula, def paint_staged_stake_division(emitter,
ursula,
original_stake, original_stake,
target_value, target_value,
extension): extension):
@ -290,7 +288,8 @@ def paint_staged_stake_division(ursula,
~ Original Stake: {prettify_stake(stake=original_stake, index=None)} ~ Original Stake: {prettify_stake(stake=original_stake, index=None)}
""" """
paint_staged_stake(ursula=ursula, paint_staged_stake(emitter=emitter,
ursula=ursula,
stake_value=target_value, stake_value=target_value,
duration=new_duration, duration=new_duration,
start_period=original_stake.start_period, start_period=original_stake.start_period,
@ -300,6 +299,8 @@ def paint_staged_stake_division(ursula,
def paint_contract_deployment(contract_name: str, contract_address: str, receipts: dict): def paint_contract_deployment(contract_name: str, contract_address: str, receipts: dict):
# TODO: switch to using an explicit emitter
# Paint heading # Paint heading
heading = '\n{} ({})'.format(contract_name, contract_address) heading = '\n{} ({})'.format(contract_name, contract_address)
click.secho(heading, bold=True) click.secho(heading, bold=True)

View File

@ -35,10 +35,11 @@ class UrsulaCommandProtocol(LineReceiver):
encoding = 'utf-8' encoding = 'utf-8'
delimiter = os.linesep.encode(encoding=encoding) delimiter = os.linesep.encode(encoding=encoding)
def __init__(self, ursula): def __init__(self, ursula, emitter):
super().__init__() super().__init__()
self.ursula = ursula self.ursula = ursula
self.emitter = emitter
self.start_time = maya.now() self.start_time = maya.now()
self.__history = deque(maxlen=10) self.__history = deque(maxlen=10)
@ -78,11 +79,11 @@ class UrsulaCommandProtocol(LineReceiver):
""" """
Display this help message. Display this help message.
""" """
click.secho("\nUrsula Command Help\n===================\n") self.emitter.echo("\nUrsula Command Help\n===================\n")
for command, func in self.__commands.items(): for command, func in self.__commands.items():
if '?' not in command: if '?' not in command:
try: try:
click.secho(f'{command}\n{"-"*len(command)}\n{func.__doc__.lstrip()}') self.emitter.echo(f'{command}\n{"-"*len(command)}\n{func.__doc__.lstrip()}')
except AttributeError: except AttributeError:
raise AttributeError("Ursula Command method is missing a docstring," raise AttributeError("Ursula Command method is missing a docstring,"
" which is required for generating help text.") " which is required for generating help text.")
@ -92,7 +93,7 @@ class UrsulaCommandProtocol(LineReceiver):
Display a list of all known nucypher peers. Display a list of all known nucypher peers.
""" """
from nucypher.cli.painting import paint_known_nodes from nucypher.cli.painting import paint_known_nodes
paint_known_nodes(ursula=self.ursula) paint_known_nodes(emitter=self.emitter, ursula=self.ursula)
def paintStakes(self): def paintStakes(self):
""" """
@ -100,23 +101,23 @@ class UrsulaCommandProtocol(LineReceiver):
""" """
from nucypher.cli.painting import paint_stakes from nucypher.cli.painting import paint_stakes
if self.ursula.stakes: if self.ursula.stakes:
paint_stakes(stakes=self.ursula.stakes) paint_stakes(self.emitter, stakes=self.ursula.stakes)
else: else:
click.secho("No active stakes.") self.emitter.echo("No active stakes.")
def paintStatus(self): def paintStatus(self):
""" """
Display the current status of the attached Ursula node. Display the current status of the attached Ursula node.
""" """
from nucypher.cli.painting import paint_node_status from nucypher.cli.painting import paint_node_status
paint_node_status(ursula=self.ursula, start_time=self.start_time) paint_node_status(emitter=self.emitter, ursula=self.ursula, start_time=self.start_time)
def paintFleetState(self): def paintFleetState(self):
""" """
Display information about the network-wide fleet state as the attached Ursula node sees it. Display information about the network-wide fleet state as the attached Ursula node sees it.
""" """
line = '{}'.format(build_fleet_state_status(ursula=self.ursula)) line = '{}'.format(build_fleet_state_status(ursula=self.ursula))
click.secho(line) self.emitter.echo(line)
def connectionMade(self): def connectionMade(self):
@ -124,10 +125,10 @@ class UrsulaCommandProtocol(LineReceiver):
self.ursula.checksum_address, self.ursula.checksum_address,
self.ursula.rest_url()) self.ursula.rest_url())
click.secho(message, fg='green') self.emitter.echo(message, color='green')
click.secho('{} | {}'.format(self.ursula.nickname_icon, self.ursula.nickname), fg='blue', bold=True) self.emitter.echo('{} | {}'.format(self.ursula.nickname_icon, self.ursula.nickname), color='blue', bold=True)
click.secho("\nType 'help' or '?' for help") self.emitter.echo("\nType 'help' or '?' for help")
self.transport.write(self.prompt) self.transport.write(self.prompt)
def connectionLost(self, reason=connectionDone) -> None: def connectionLost(self, reason=connectionDone) -> None:
@ -149,7 +150,7 @@ class UrsulaCommandProtocol(LineReceiver):
# Print # Print
except KeyError: except KeyError:
if line: # allow for empty string if line: # allow for empty string
click.secho("Invalid input. Options are {}".format(', '.join(self.__commands.keys()))) self.emitter.echo("Invalid input. Options are {}".format(', '.join(self.__commands.keys())))
else: else:
self.__history.append(raw_line) self.__history.append(raw_line)

View File

@ -1,6 +1,5 @@
import click import click
from constant_sorrow.constants import NO_STAKING_DEVICE
from web3 import Web3 from web3 import Web3
from nucypher.blockchain.eth.actors import StakeHolder from nucypher.blockchain.eth.actors import StakeHolder
@ -69,9 +68,9 @@ def stake(click_config,
) -> None: ) -> None:
# Banner # Banner
if not click_config.quiet: emitter = click_config.emitter
click.clear() emitter.clear()
click.secho(NU_BANNER) emitter.banner(NU_BANNER)
if action == 'new-stakeholder': if action == 'new-stakeholder':
@ -93,7 +92,7 @@ def stake(click_config,
blockchain=blockchain) blockchain=blockchain)
filepath = new_stakeholder.to_configuration_file(override=force) filepath = new_stakeholder.to_configuration_file(override=force)
click.secho(f"Wrote new stakeholder configuration to {filepath}", fg='green') emitter.echo(f"Wrote new stakeholder configuration to {filepath}", color='green')
return # Exit return # Exit
# #
@ -110,21 +109,21 @@ def stake(click_config,
if action == 'list': if action == 'list':
if not STAKEHOLDER.stakes: if not STAKEHOLDER.stakes:
click.echo(f"There are no active stakes") emitter.echo(f"There are no active stakes")
else: else:
painting.paint_stakes(stakes=STAKEHOLDER.stakes) painting.paint_stakes(emitter=emitter, stakes=STAKEHOLDER.stakes)
return return
elif action == 'accounts': elif action == 'accounts':
for address, balances in STAKEHOLDER.account_balances.items(): for address, balances in STAKEHOLDER.account_balances.items():
click.secho(f"{address} | {Web3.fromWei(balances['ETH'], 'ether')} ETH | {NU.from_nunits(balances['NU'])}") emitter.echo(f"{address} | {Web3.fromWei(balances['ETH'], 'ether')} ETH | {NU.from_nunits(balances['NU'])}")
return # Exit return # Exit
elif action == 'sync': elif action == 'sync':
click.secho("Reading on-chain stake data...") emitter.echo("Reading on-chain stake data...")
STAKEHOLDER.read_onchain_stakes() STAKEHOLDER.read_onchain_stakes()
STAKEHOLDER.to_configuration_file(override=True) STAKEHOLDER.to_configuration_file(override=True)
click.secho("OK!", fg='green') emitter.echo("OK!", color='green')
return # Exit return # Exit
elif action == 'set-worker': elif action == 'set-worker':
@ -142,7 +141,7 @@ def stake(click_config,
password=password, password=password,
worker_address=worker_address) worker_address=worker_address)
click.secho("OK!", fg='green') emitter.echo("OK!", color='green')
return # Exit return # Exit
elif action == 'init': elif action == 'init':
@ -181,7 +180,8 @@ def stake(click_config,
# #
if not force: if not force:
painting.paint_staged_stake(ursula=STAKEHOLDER, painting.paint_staged_stake(emitter=emitter,
ursula=STAKEHOLDER,
stake_value=value, stake_value=value,
duration=duration, duration=duration,
start_period=start_period, start_period=start_period,
@ -198,7 +198,9 @@ def stake(click_config,
checksum_address=staking_address, checksum_address=staking_address,
password=password) password=password)
painting.paint_staking_confirmation(ursula=STAKEHOLDER, transactions=new_stake.transactions) painting.paint_staking_confirmation(emitter=emitter,
ursula=STAKEHOLDER,
transactions=new_stake.transactions)
return # Exit return # Exit
elif action == 'divide': elif action == 'divide':
@ -227,7 +229,8 @@ def stake(click_config,
extension = duration extension = duration
if not force: if not force:
painting.paint_staged_stake_division(ursula=STAKEHOLDER, painting.paint_staged_stake_division(emitter=emitter,
ursula=STAKEHOLDER,
original_stake=current_stake, original_stake=current_stake,
target_value=value, target_value=value,
extension=extension) extension=extension)
@ -243,11 +246,11 @@ def stake(click_config,
duration=extension, duration=extension,
password=password) password=password)
if not click_config.quiet: if not click_config.quiet:
click.secho('Successfully divided stake', fg='green') emitter.echo('Successfully divided stake', color='green')
click.secho(f'Receipt ........... {new_stake.receipt}') emitter.echo(f'Receipt ........... {new_stake.receipt}')
# Show the resulting stake list # Show the resulting stake list
painting.paint_stakes(stakes=STAKEHOLDER.stakes) painting.paint_stakes(emitter=emitter, stakes=STAKEHOLDER.stakes)
return # Exit return # Exit
elif action == 'collect-reward': elif action == 'collect-reward':

View File

@ -42,7 +42,7 @@ def status(click_config, config_file):
ursula_config.acquire_agency() ursula_config.acquire_agency()
# Contracts # Contracts
paint_contract_status(ursula_config=ursula_config, click_config=click_config) paint_contract_status(click_config.emitter, ursula_config=ursula_config, click_config=click_config)
# Known Nodes # Known Nodes
paint_known_nodes(ursula=ursula_config) paint_known_nodes(emitter=click_config.emitter, ursula=ursula_config)

View File

@ -4,6 +4,7 @@ from contextlib import contextmanager
import pytest import pytest
from io import StringIO from io import StringIO
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.cli.config import NucypherClickConfig from nucypher.cli.config import NucypherClickConfig
from nucypher.cli.processes import UrsulaCommandProtocol from nucypher.cli.processes import UrsulaCommandProtocol
@ -31,13 +32,15 @@ def ursula(federated_ursulas):
@pytest.fixture(scope='module') @pytest.fixture(scope='module')
def protocol(ursula): def protocol(ursula):
protocol = UrsulaCommandProtocol(ursula=ursula) emitter = StdoutEmitter()
protocol = UrsulaCommandProtocol(ursula=ursula, emitter=emitter)
return protocol return protocol
def test_ursula_command_protocol_creation(ursula): def test_ursula_command_protocol_creation(ursula):
protocol = UrsulaCommandProtocol(ursula=ursula) emitter = StdoutEmitter()
protocol = UrsulaCommandProtocol(ursula=ursula, emitter=emitter)
assert protocol.ursula == ursula assert protocol.ursula == ursula
assert b'Ursula' in protocol.prompt assert b'Ursula' in protocol.prompt