def select_stake(staker: Staker, emitter: StdoutEmitter, stakes_status: Stake.Status = Stake.Status.EDITABLE, filter_function: Callable[[Stake], bool] = None) -> Stake: """Interactively select a stake or abort if there are no eligible stakes.""" if stakes_status.is_child(Stake.Status.DIVISIBLE): emitter.echo(ONLY_DISPLAYING_DIVISIBLE_STAKES_NOTE, color='yellow') # Filter stakes by status stakes = staker.sorted_stakes(parent_status=stakes_status, filter_function=filter_function) if not stakes: emitter.echo(NO_STAKES_FOUND, color='red') raise click.Abort # Interactive Selection paint_unlocked = stakes_status.is_child(Stake.Status.UNLOCKED) paint_stakes(staker=staker, emitter=emitter, stakes=stakes, paint_unlocked=paint_unlocked) indexed_stakes = {stake.index: stake for stake in stakes} indices = [str(index) for index in indexed_stakes.keys()] choice = click.prompt(SELECT_STAKE, type=click.Choice(indices)) chosen_stake = indexed_stakes[int(choice)] return chosen_stake
def perform_startup_ip_check(emitter: StdoutEmitter, ursula: Ursula, force: bool = False) -> None: """ Used on ursula startup to determine if the external IP address is consistent with the configuration's values. """ try: external_ip = determine_external_ip_address(network=ursula.domain, known_nodes=ursula.known_nodes) except UnknownIPAddress: message = 'Cannot automatically determine external IP address' emitter.message(message) return # TODO: crash, or not to crash... that is the question rest_host = ursula.rest_interface.host try: validate_operator_ip(ip=rest_host) except InvalidOperatorIP: message = f'{rest_host} is not a valid or permitted operator IP address. Set the correct external IP then try again\n' \ f'automatic configuration -> nucypher ursula config ip-address\n' \ f'manual configuration -> nucypher ursula config --rest-host <IP ADDRESS>' emitter.message(message) return ip_mismatch = external_ip != rest_host if ip_mismatch and not force: error = f'\nx External IP address ({external_ip}) does not match configuration ({ursula.rest_interface.host}).\n' hint = f"Run 'nucypher ursula config ip-address' to reconfigure the IP address then try " \ f"again or use --no-ip-checkup to bypass this check (not recommended).\n" emitter.message(error, color='red') emitter.message(hint, color='yellow') raise click.Abort() else: emitter.message('✓ External IP matches configuration', 'green')
def confirm_enable_winding_down(emitter: StdoutEmitter, staking_address: str) -> bool: """Interactively confirm enabling of winding down with user agreements.""" emitter.message(WINDING_DOWN_AGREEMENT) click.confirm( CONFIRM_ENABLE_WINDING_DOWN.format(staking_address=staking_address), abort=True) return True
def delete(force, card_id): """Delete an existing character card.""" emitter = StdoutEmitter() card = select_card(emitter=emitter, card_identifier=card_id) if not force: click.confirm(f'Are you sure you want to delete {card}?', abort=True) card.delete() emitter.message(f'Deleted card for {card.id.hex()}.', color='red')
def confirm_disable_snapshots(emitter: StdoutEmitter, staking_address: str) -> bool: """Interactively confirm disabling of taking snapshots with user agreements.""" emitter.message(SNAPSHOTS_DISABLING_AGREEMENT) click.confirm( CONFIRM_DISABLE_SNAPSHOTS.format(staking_address=staking_address), abort=True) return True
def destroy_configuration(emitter: StdoutEmitter, character_config: CharacterConfiguration, force: bool = False) -> None: """Destroy a character configuration and report rhe result with an emitter.""" if not force: confirm_destroy_configuration(config=character_config) character_config.destroy() emitter.message(SUCCESSFUL_DESTRUCTION, color='green') character_config.log.debug(SUCCESSFUL_DESTRUCTION)
def confirm_enable_restaking(emitter: StdoutEmitter, staking_address: str) -> bool: """Interactively confirm enabling of the restaking with user agreements.""" emitter.message( RESTAKING_AGREEMENT.format(staking_address=staking_address)) click.confirm( CONFIRM_ENABLE_RESTAKING.format(staking_address=staking_address), abort=True) return True
def _shutdown_ursula(self, halt_reactor=False): emitter = StdoutEmitter() emitter.message( f'x [Operator {self._ursula.operator_address} is no longer bonded to any ' f'staking provider] - Commencing auto-shutdown sequence...', color="red") try: raise self.OperatorNoLongerBonded() finally: self._ursula.stop(halt_reactor=halt_reactor)
def unlock_nucypher_keystore(emitter: StdoutEmitter, password: str, character_configuration: CharacterConfiguration) -> bool: """Unlocks a nucypher keystore and attaches it to the supplied configuration if successful.""" emitter.message(DECRYPTING_CHARACTER_KEYSTORE.format(name=character_configuration.NAME.capitalize()), color='yellow') # precondition if character_configuration.dev_mode: return True # Dev accounts are always unlocked # unlock character_configuration.keystore.unlock(password=password) # Takes ~3 seconds, ~1GB Ram return True
def show(query, qrcode): """ Lookup and view existing character card QUERY can be either the card id or nickname. """ emitter = StdoutEmitter() try: card = select_card(emitter=emitter, card_identifier=query) except Card.UnknownCard as e: return emitter.error(str(e)) paint_single_card(emitter=emitter, card=card, qrcode=qrcode)
def select_network(emitter: StdoutEmitter) -> str: """Interactively select a network from nucypher networks inventory list""" headers = ["Network"] rows = [[n] for n in NetworksInventory.NETWORKS] emitter.echo(tabulate(rows, headers=headers, showindex='always')) choice = click.prompt(SELECT_NETWORK, default=0, type=click.IntRange( 0, len(NetworksInventory.NETWORKS) - 1)) network = NetworksInventory.NETWORKS[choice] return network
def __cache_addresses(self) -> None: """ Derives trezor ethereum addresses up to ADDRESS_CACHE_SIZE relative to the calculated base path and internally caches them for later use. """ emitter = StdoutEmitter() for index in range(self.ADDRESS_CACHE_SIZE): hd_path = self.__get_address_path(index=index) address = self.__derive_account(hd_path=hd_path) self.__addresses[address] = hd_path message = f"Derived {address} ({self.derivation_root}/{index})" emitter.message(message)
def select_network(emitter: StdoutEmitter, message: Optional[str] = None) -> str: """Interactively select a network from nucypher networks inventory list""" emitter.message(message=message or str(), color='yellow') rows = [[n] for n in NetworksInventory.NETWORKS] emitter.echo(tabulate(rows, showindex='always')) choice = click.prompt(SELECT_NETWORK, default=0, type=click.IntRange( 0, len(NetworksInventory.NETWORKS) - 1)) network = NetworksInventory.NETWORKS[choice] return network
def retrieve_events(emitter: StdoutEmitter, agent: EthereumContractAgent, event_name: str, from_block: BlockIdentifier, to_block: BlockIdentifier, argument_filters: Dict, csv_output_file: Optional[Path] = None) -> None: if csv_output_file: if csv_output_file.exists(): click.confirm(CONFIRM_OVERWRITE_EVENTS_CSV_FILE.format(csv_file=csv_output_file), abort=True) available_events = write_events_to_csv_file(csv_file=csv_output_file, agent=agent, event_name=event_name, from_block=from_block, to_block=to_block, argument_filters=argument_filters) if available_events: emitter.echo(f"{agent.contract_name}::{event_name} events written to {csv_output_file}", bold=True, color='green') else: emitter.echo(f'No {agent.contract_name}::{event_name} events found', color='yellow') else: event = agent.contract.events[event_name] emitter.echo(f"{event_name}:", bold=True, color='yellow') entries = event.getLogs(fromBlock=from_block, toBlock=to_block, argument_filters=argument_filters) for event_record in entries: emitter.echo(f" - {EventRecord(event_record)}")
def _list(): """Show all character cards""" emitter = StdoutEmitter() if not Card.CARD_DIR.is_dir(): Card.CARD_DIR.mkdir() card_filepaths = list(Card.CARD_DIR.iterdir()) if not card_filepaths: emitter.error( f'No cards found at {Card.CARD_DIR}. ' f"To create one run 'nucypher {contacts.name} {create.name}'.") cards = list() for filename in card_filepaths: card = Card.load(filepath=Card.CARD_DIR / filename) cards.append(card) paint_cards(emitter=emitter, cards=cards, as_table=True)
def test_ursula_command_protocol_creation(ursula): emitter = StdoutEmitter() protocol = UrsulaCommandProtocol(ursula=ursula, emitter=emitter) assert protocol.ursula == ursula assert b'Ursula' in protocol.prompt
def confirm_deployment( emitter: StdoutEmitter, deployer_interface: BlockchainDeployerInterface) -> bool: """ Interactively confirm deployment by asking the user to type the ALL CAPS name of the network they are deploying to or 'DEPLOY' if the network if not a known public chain. Aborts if the confirmation word is incorrect. """ if deployer_interface.client.chain_name == UNKNOWN_DEVELOPMENT_CHAIN_ID or deployer_interface.client.is_local: expected_chain_name = 'DEPLOY' else: expected_chain_name = deployer_interface.client.chain_name if click.prompt(f"Type '{expected_chain_name.upper()}' to continue" ) != expected_chain_name.upper(): emitter.echo(ABORT_DEPLOYMENT, color='red', bold=True) raise click.Abort(ABORT_DEPLOYMENT) return True
def unbond(registry_filepath, eth_provider_uri, signer_uri, staking_provider, network, force): """Unbonds an operator from an authorized staking provider.""" # # Setup # emitter = StdoutEmitter() if not signer_uri: emitter.message('--signer is required', color='red') raise click.Abort() if not network: network = select_network(emitter=emitter, network_type=NetworksInventory.ETH) connect_to_blockchain(eth_provider_uri=eth_provider_uri, emitter=emitter) registry = get_registry(network=network, registry_filepath=registry_filepath) agent = ContractAgency.get_agent(PREApplicationAgent, registry=registry) signer = Signer.from_signer_uri(signer_uri) transacting_power = TransactingPower(account=staking_provider, signer=signer) # # Check # bonded, onchain_operator_address = is_bonded( agent=agent, staking_provider=staking_provider, return_address=True) if not bonded: emitter.message(NOT_BONDED.format(provider=staking_provider), color='red') raise click.Abort() check_bonding_requirements(emitter=emitter, agent=agent, staking_provider=staking_provider) # # Unbond # if not force: click.confirm(CONFIRM_UNBONDING.format( provider=staking_provider, operator=onchain_operator_address), abort=True) transacting_power.unlock(password=get_client_password( checksum_address=staking_provider, envvar=NUCYPHER_ENVVAR_STAKING_PROVIDER_ETH_PASSWORD)) emitter.echo(UNBONDING.format(operator=onchain_operator_address)) receipt = agent.bond_operator(operator=NULL_ADDRESS, transacting_power=transacting_power, staking_provider=staking_provider) paint_receipt_summary(receipt=receipt, emitter=emitter)
def connect_to_blockchain(emitter: StdoutEmitter, provider_uri: str, debug: bool = False, light: bool = False ) -> BlockchainInterface: try: # Note: Conditional for test compatibility. if not BlockchainInterfaceFactory.is_interface_initialized(provider_uri=provider_uri): BlockchainInterfaceFactory.initialize_interface(provider_uri=provider_uri, light=light, emitter=emitter) emitter.echo(message=CONNECTING_TO_BLOCKCHAIN) blockchain = BlockchainInterfaceFactory.get_interface(provider_uri=provider_uri) return blockchain except Exception as e: if debug: raise emitter.echo(str(e), bold=True, color='red') raise click.Abort
def collect_keys_from_card(emitter: StdoutEmitter, card_identifier: str, force: bool): emitter.message(f"Searching contacts for {card_identifier}\n", color='yellow') card = Card.load(identifier=card_identifier) if card.character is not Bob: emitter.error('Grantee card is not a Bob.') raise click.Abort paint_single_card(emitter=emitter, card=card) if not force: click.confirm('Is this the correct grantee (Bob)?', abort=True) bob_encrypting_key = bytes(card.encrypting_key).hex() bob_verifying_key = bytes(card.verifying_key).hex() public_keys = PublicKeys(encrypting_key=bob_encrypting_key, verifying_key=bob_verifying_key) return public_keys
def test_get_nucypher_password(mock_stdin, mock_account, confirm, capsys): mock_stdin.password(INSECURE_DEVELOPMENT_PASSWORD, confirm=confirm) result = get_nucypher_password(emitter=StdoutEmitter(), confirm=confirm) assert result == INSECURE_DEVELOPMENT_PASSWORD assert mock_stdin.empty() captured = capsys.readouterr() assert COLLECT_NUCYPHER_PASSWORD in captured.out if confirm: prompt = COLLECT_NUCYPHER_PASSWORD + f" ({Keystore._MINIMUM_PASSWORD_LENGTH} character minimum)" assert prompt in captured.out
def collect_operator_ip_address(emitter: StdoutEmitter, network: str, force: bool = False) -> str: # From node swarm try: message = f'Detecting external IP address automatically' emitter.message(message, verbosity=2) ip = determine_external_ip_address(network=network) except UnknownIPAddress: if force: raise emitter.message('Cannot automatically determine external IP address - input required') ip = click.prompt(COLLECT_URSULA_IPV4_ADDRESS, type=OPERATOR_IP) # Confirmation if not force: if not click.confirm(CONFIRM_URSULA_IPV4_ADDRESS.format(rest_host=ip)): ip = click.prompt(COLLECT_URSULA_IPV4_ADDRESS, type=OPERATOR_IP) validate_operator_ip(ip=ip) return ip
def select_network(emitter: StdoutEmitter, network_type: str, message: Optional[str] = None) -> str: """Interactively select a network from nucypher networks inventory list""" emitter.message(message=message or str(), color="yellow") if network_type == NetworksInventory.ETH: network_list = NetworksInventory.ETH_NETWORKS elif network_type == NetworksInventory.POLYGON: network_list = NetworksInventory.POLY_NETWORKS else: raise (ValueError("Network type must be either 'eth' or 'polygon'")) rows = [[n] for n in network_list] emitter.echo(tabulate(rows, showindex="always")) choice = click.prompt( SELECT_NETWORK, default=0, type=click.IntRange(0, len(rows) - 1), ) network = network_list[choice] return network
def deployer_pre_launch_warnings(emitter: StdoutEmitter, etherscan: bool, hw_wallet: bool) -> None: if not hw_wallet: emitter.echo(NO_HARDWARE_WALLET_WARNING, color='yellow') if etherscan: emitter.echo(ETHERSCAN_FLAG_ENABLED_WARNING, color='yellow') else: emitter.echo(ETHERSCAN_FLAG_DISABLED_WARNING, color='yellow')
def get_or_update_configuration(emitter: StdoutEmitter, filepath: Path, config_class: Type[CharacterConfiguration], updates: Optional[dict] = None) -> None: """ Utility for writing updates to an existing configuration file then displaying the result. If the config file is invalid, try very hard to display the problem. If there are no updates, the config file will be displayed without changes. """ try: config = config_class.from_configuration_file(filepath=filepath) except FileNotFoundError: return handle_missing_configuration_file( character_config_class=config_class, config_file=filepath) except config_class.ConfigurationError: return handle_invalid_configuration_file(emitter=emitter, config_class=config_class, filepath=filepath) emitter.echo( f"{config_class.NAME.capitalize()} Configuration {filepath} \n {'='*55}" ) if updates: pretty_fields = ', '.join(updates) emitter.message(SUCCESSFUL_UPDATE_CONFIGURATION_VALUES.format( fields=pretty_fields), color='yellow') config.update(**updates) emitter.echo(config.serialize())
def paint_all_stakes(emitter: StdoutEmitter, stakeholder: 'StakeHolder', paint_unlocked: bool = False) -> None: stakers = stakeholder.get_stakers() if not stakers: emitter.echo("No staking accounts found.") total_stakers = 0 for staker in stakers: if not staker.stakes: # This staker has no active stakes. # TODO: Something with non-staking accounts? continue paint_stakes(emitter=emitter, staker=staker, paint_unlocked=paint_unlocked, stakeholder=stakeholder) total_stakers += 1 if not total_stakers: emitter.echo("No Stakes found", color='red')
def import_secure(cls, key_material: bytes, password: str, keystore_dir: Optional[Path] = None) -> 'Keystore': """ Generate a Keystore using a a custom pre-secured entropy blob. This method of keystore creation does not generate a mnemonic phrase - it is assumed that the provided blob is recoverable and secure. """ emitter = StdoutEmitter() emitter.message( f'WARNING: Key importing assumes that you have already secured your secret ' f'and can recover it. No mnemonic will be generated.\n', color='yellow') if len(key_material) != SecretKey.serialized_size(): raise ValueError( f'Entropy bytes bust be exactly {SecretKey.serialized_size()}.' ) path = Keystore.__save(secret=key_material, password=password, keystore_dir=keystore_dir) keystore = cls(keystore_path=path) return keystore
def validate_grant_command(emitter: StdoutEmitter, alice: Alice, force: bool, bob: str, bob_verifying_key: str, bob_encrypting_key: str, label: str, expiration: maya.MayaDT, rate: int, value: int): # Force mode validation if force: required = (Precondition( options='--bob or --bob-encrypting-key and --bob-verifying-key.', condition=bob or all((bob_verifying_key, bob_encrypting_key))), Precondition(options='--label', condition=bool(label)), Precondition(options='--expiration', condition=bool(expiration))) triggered = False for condition in required: # see what condition my condition was in. if not condition.condition: triggered = True emitter.error( f'Missing options in force mode: {condition.options}', color="red") if triggered: raise click.Abort() # Handle federated if alice.federated_only: if any((value, rate)): message = "Can't use --value or --rate with a federated Alice." raise click.BadOptionUsage(option_name="--value, --rate", message=click.style(message, fg="red")) elif bool(value) and bool(rate): raise click.BadOptionUsage(option_name="--rate", message=click.style( "Can't use --value if using --rate", fg="red")) # From Bob card if bob: if any((bob_encrypting_key, bob_verifying_key)): message = '--bob cannot be used with --bob-encrypting-key or --bob-verifying key' raise click.BadOptionUsage(option_name='--bob', message=click.style(message, fg="red")) # From hex public keys else: if not all((bob_encrypting_key, bob_verifying_key)): if force: emitter.message( 'Missing options in force mode: --bob or --bob-encrypting-key and --bob-verifying-key.', color="red") click.Abort() emitter.message("*Caution: Only enter public keys*")
def create(character_flag, verifying_key, encrypting_key, nickname, force): """Store a new character card""" emitter = StdoutEmitter() # Validate if not all((character_flag, verifying_key, encrypting_key)) and force: emitter.error( f'--verifying-key, --encrypting-key, and --type are required with --force enabled.' ) # Card type from constant_sorrow.constants import ALICE, BOB flags = {'a': ALICE, 'b': BOB} if not character_flag: choice = click.prompt('Enter Card Type - (A)lice or (B)ob', type=click.Choice(['a', 'b'], case_sensitive=False)) character_flag = flags[choice] else: character_flag = flags[character_flag] # Verifying Key if not verifying_key: verifying_key = click.prompt('Enter Verifying Key', type=UMBRAL_PUBLIC_KEY_HEX) verifying_key = bytes.fromhex(verifying_key) # TODO: Move / Validate # Encrypting Key if character_flag is BOB: if not encrypting_key: encrypting_key = click.prompt('Enter Encrypting Key', type=UMBRAL_PUBLIC_KEY_HEX) encrypting_key = bytes.fromhex(encrypting_key) # TODO: Move / Validate # Init new_card = Card(character_flag=character_flag, verifying_key=verifying_key, encrypting_key=encrypting_key, nickname=nickname) # Nickname if not force and not nickname: card_id_hex = new_card.id.hex() nickname = click.prompt('Enter nickname for card', default=card_id_hex) if nickname != card_id_hex: # not the default nickname = nickname.strip() new_card.nickname = nickname # Save new_card.save() emitter.message(f'Saved new card {new_card}', color='green') paint_single_card(emitter=emitter, card=new_card)
def _confirm_generate(__words: str) -> None: """ Inform the caller of new keystore seed words generation the console and optionally perform interactive confirmation. """ # notification emitter = StdoutEmitter() emitter.message( f'Backup your seed words, you will not be able to view them again.\n' ) emitter.message(f'{__words}\n', color='cyan') if not click.confirm("Have you backed up your seed phrase?"): emitter.message('Keystore generation aborted.', color='red') raise click.Abort() click.clear() # confirmation __response = click.prompt("Confirm seed words") if __response != __words: raise ValueError( 'Incorrect seed word confirmation. No keystore has been created, try again.' ) click.clear()