def list_stakes(general_config, staker_options, config_file, all): """ List active stakes for current stakeholder. """ emitter = _setup_emitter(general_config) STAKEHOLDER = staker_options.create_character(emitter, config_file) painting.paint_stakes(emitter=emitter, stakeholder=STAKEHOLDER, paint_inactive=all)
def select_stake(stakeholder, emitter) -> Stake: stakes = stakeholder.all_stakes enumerated_stakes = dict(enumerate(stakes)) painting.paint_stakes(stakes=stakes, emitter=emitter) choice = click.prompt("Select Stake", type=click.IntRange(min=0, max=len(enumerated_stakes)-1)) chosen_stake = enumerated_stakes[choice] return chosen_stake
def select_stake(stakeholder, emitter, divisible: bool = False, staker_address: str = None) -> Stake: if staker_address: staker = stakeholder.get_staker(checksum_address=staker_address) stakes = staker.stakes else: stakes = stakeholder.all_stakes if not stakes: emitter.echo(f"No stakes found.", color='red') raise click.Abort stakes = sorted((stake for stake in stakes if stake.is_active), key=lambda s: s.address_index_ordering_key) if divisible: emitter.echo("NOTE: Showing divisible stakes only", color='yellow') stakes = list( filter( lambda s: bool(s.value >= stakeholder.economics. minimum_allowed_locked * 2), stakes)) # TODO: Move to method on Stake if not stakes: emitter.echo(f"No divisible stakes found.", color='red') raise click.Abort enumerated_stakes = dict(enumerate(stakes)) painting.paint_stakes(stakeholder=stakeholder, emitter=emitter, staker_address=staker_address) choice = click.prompt("Select Stake", type=click.IntRange(min=0, max=len(enumerated_stakes) - 1)) chosen_stake = enumerated_stakes[choice] return chosen_stake
def list( click_config, # API Options poa, light, registry_filepath, config_file, provider_uri, staking_address, # Other options all): """ List active stakes for current stakeholder. """ ### Setup ### emitter = _setup_emitter(click_config) STAKEHOLDER, blockchain = _create_stakeholder(config_file, provider_uri, poa, light, registry_filepath, staking_address, beneficiary_address=None, allocation_filepath=None) ############# painting.paint_stakes(emitter=emitter, stakes=STAKEHOLDER.all_stakes, paint_inactive=all)
def prolong(general_config, transacting_staker_options, config_file, force, lock_periods, index): """Prolong an existing stake's duration.""" # Setup emitter = _setup_emitter(general_config) STAKEHOLDER = transacting_staker_options.create_character(emitter, config_file) action_period = STAKEHOLDER.staking_agent.get_current_period() blockchain = transacting_staker_options.get_blockchain() economics = STAKEHOLDER.economics # Handle account selection client_account, staking_address = handle_client_account_for_staking( emitter=emitter, stakeholder=STAKEHOLDER, staking_address=transacting_staker_options.staker_options.staking_address, individual_allocation=STAKEHOLDER.individual_allocation, force=force) # Handle stake update and selection if transacting_staker_options.staker_options.staking_address and index is not None: # 0 is valid. STAKEHOLDER.stakes = StakeList(registry=STAKEHOLDER.registry, checksum_address=transacting_staker_options.staker_options.staking_address) STAKEHOLDER.stakes.refresh() current_stake = STAKEHOLDER.stakes[index] else: current_stake = select_stake(stakeholder=STAKEHOLDER, emitter=emitter) # # Prolong # # Interactive if not lock_periods: max_extension = MAX_UINT16 - current_stake.final_locked_period # +1 because current period excluded min_extension = economics.minimum_locked_periods - current_stake.periods_remaining + 1 if min_extension < 1: min_extension = 1 duration_extension_range = click.IntRange(min=min_extension, max=max_extension, clamp=False) lock_periods = click.prompt(f"Enter number of periods to extend ({min_extension}-{max_extension})", type=duration_extension_range) if not force: click.confirm(f"Publish stake extension of {lock_periods} period(s) to the blockchain?", abort=True) password = transacting_staker_options.get_password(blockchain, client_account) # Non-interactive: Consistency check to prevent the above agreement from going stale. last_second_current_period = STAKEHOLDER.staking_agent.get_current_period() if action_period != last_second_current_period: emitter.echo("Current period advanced before transaction was broadcasted. Please try again.", red='red') raise click.Abort # Authenticate and Execute STAKEHOLDER.assimilate(checksum_address=current_stake.staker_address, password=password) receipt = STAKEHOLDER.prolong_stake(stake_index=current_stake.index, additional_periods=lock_periods) # Report emitter.echo('Successfully Prolonged Stake', color='green', verbosity=1) paint_receipt_summary(emitter=emitter, receipt=receipt, chain_name=blockchain.client.chain_name) painting.paint_stakes(emitter=emitter, stakeholder=STAKEHOLDER) return # Exit
def paintStakes(self): """ Display a list of all active stakes. """ from nucypher.cli.painting import paint_stakes if self.ursula.stakes: paint_stakes(self.emitter, stakes=self.ursula.stakes) else: self.emitter.echo("No active stakes.")
def select_stake(stakeholder, emitter) -> Stake: stakes = stakeholder.all_stakes active_stakes = sorted( (stake for stake in stakes if stake.is_active), key=lambda some_stake: some_stake.address_index_ordering_key) enumerated_stakes = dict(enumerate(active_stakes)) painting.paint_stakes(stakes=active_stakes, emitter=emitter) choice = click.prompt("Select Stake", type=click.IntRange(min=0, max=len(enumerated_stakes) - 1)) chosen_stake = enumerated_stakes[choice] return chosen_stake
def divide(general_config, transacting_staker_options, config_file, force, value, lock_periods, index): """ Create a new stake from part of an existing one. """ # Setup emitter = _setup_emitter(general_config) STAKEHOLDER = transacting_staker_options.create_character( emitter, config_file) blockchain = transacting_staker_options.get_blockchain() economics = STAKEHOLDER.economics action_period = STAKEHOLDER.staking_agent.get_current_period() client_account, staking_address = handle_client_account_for_staking( emitter=emitter, stakeholder=STAKEHOLDER, staking_address=transacting_staker_options.staker_options. staking_address, individual_allocation=STAKEHOLDER.individual_allocation, force=force) # Dynamic click types (Economics) min_locked = economics.minimum_allowed_locked stake_value_range = click.FloatRange( min=NU.from_nunits(min_locked).to_tokens(), clamp=False) if transacting_staker_options.staker_options.staking_address and index is not None: # 0 is valid. STAKEHOLDER.stakes = StakeList( registry=STAKEHOLDER.registry, checksum_address=transacting_staker_options.staker_options. staking_address) STAKEHOLDER.stakes.refresh() current_stake = STAKEHOLDER.stakes[index] else: current_stake = select_stake(stakeholder=STAKEHOLDER, emitter=emitter, divisible=True, staker_address=client_account) # # Stage Stake # # Value if not value: min_allowed_locked = NU.from_nunits( STAKEHOLDER.economics.minimum_allowed_locked) max_divide_value = max(min_allowed_locked, current_stake.value - min_allowed_locked) value = click.prompt( f"Enter target value ({min_allowed_locked} - {str(max_divide_value)})", type=stake_value_range) value = NU(value, 'NU') # Duration if not lock_periods: max_extension = MAX_UINT16 - current_stake.final_locked_period divide_extension_range = click.IntRange(min=1, max=max_extension, clamp=False) extension = click.prompt(f"Enter number of periods to extend", type=divide_extension_range) else: extension = lock_periods if not force: issue_stake_suggestions(lock_periods=extension, value=value) painting.paint_staged_stake_division(emitter=emitter, stakeholder=STAKEHOLDER, original_stake=current_stake, target_value=value, extension=extension) click.confirm("Publish stake division to the blockchain?", abort=True) # Authenticate password = transacting_staker_options.get_password(blockchain, client_account) # Consistency check to prevent the above agreement from going stale. last_second_current_period = STAKEHOLDER.staking_agent.get_current_period() if action_period != last_second_current_period: emitter.echo( "Current period advanced before stake division was broadcasted. Please try again.", red='red') raise click.Abort # Execute STAKEHOLDER.assimilate(checksum_address=current_stake.staker_address, password=password) modified_stake, new_stake = STAKEHOLDER.divide_stake( stake_index=current_stake.index, target_value=value, additional_periods=extension) emitter.echo('Successfully divided stake', color='green', verbosity=1) paint_receipt_summary(emitter=emitter, receipt=new_stake.receipt, chain_name=blockchain.client.chain_name) # Show the resulting stake list painting.paint_stakes(emitter=emitter, stakeholder=STAKEHOLDER)
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 stake(click_config, action, config_root, config_file, # Mode force, offline, hw_wallet, # Blockchain poa, registry_filepath, provider_uri, sync, # Stake staking_address, worker_address, withdraw_address, value, lock_periods, index, policy_reward, staking_reward, enable, lock_until, ) -> None: """ Manage stakes and other staker-related operations. \b Actions ------------------------------------------------- init-stakeholder Create a new stakeholder configuration list List active stakes for current stakeholder accounts Show ETH and NU balances for stakeholder's accounts sync Synchronize stake data with on-chain information set-worker Bond a worker to a staker detach-worker Detach worker currently bonded to a staker init Create a new stake restake Manage re-staking with --enable or --disable divide Create a new stake from part of an existing one collect-reward Withdraw staking reward """ # Banner emitter = click_config.emitter emitter.clear() emitter.banner(StakeHolder.banner) if action == 'init-stakeholder': if not provider_uri: raise click.BadOptionUsage(option_name='--provider', message="--provider is required to create a new stakeholder.") new_stakeholder = StakeHolderConfiguration.generate(config_root=config_root, provider_uri=provider_uri, poa=poa, sync=False, registry_filepath=registry_filepath) filepath = new_stakeholder.to_configuration_file(override=force) emitter.echo(f"Wrote new stakeholder configuration to {filepath}", color='green') return # Exit try: stakeholder_config = StakeHolderConfiguration.from_configuration_file(filepath=config_file, provider_uri=provider_uri, poa=poa, sync=False, registry_filepath=registry_filepath) except FileNotFoundError: return actions.handle_missing_configuration_file(character_config_class=StakeHolderConfiguration, config_file=config_file) # # Make Stakeholder # STAKEHOLDER = stakeholder_config.produce(initial_address=staking_address) blockchain = BlockchainInterfaceFactory.get_interface(provider_uri=provider_uri) # Eager connection economics = STAKEHOLDER.economics # Dynamic click types (Economics) min_locked = economics.minimum_allowed_locked stake_value_range = click.FloatRange(min=NU.from_nunits(min_locked).to_tokens(), clamp=False) stake_duration_range = click.IntRange(min=economics.minimum_locked_periods, clamp=False) stake_extension_range = click.IntRange(min=1, max=economics.maximum_allowed_locked, clamp=False) # # Eager Actions # if action == 'list': stakes = STAKEHOLDER.all_stakes if not stakes: emitter.echo(f"There are no active stakes") else: painting.paint_stakes(emitter=emitter, stakes=stakes) return # Exit elif action == 'accounts': for address, balances in STAKEHOLDER.wallet.balances.items(): emitter.echo(f"{address} | {Web3.fromWei(balances['ETH'], 'ether')} ETH | {NU.from_nunits(balances['NU'])}") return # Exit elif action == 'set-worker': if not staking_address: staking_address = select_stake(stakeholder=STAKEHOLDER, emitter=emitter).staker_address if not worker_address: worker_address = click.prompt("Enter worker address", type=EIP55_CHECKSUM_ADDRESS) # TODO: Check preconditions (e.g., minWorkerPeriods, already in use, etc) password = None if not hw_wallet and not blockchain.client.is_local: password = get_client_password(checksum_address=staking_address) STAKEHOLDER.assimilate(checksum_address=staking_address, password=password) receipt = STAKEHOLDER.set_worker(worker_address=worker_address) # TODO: Double-check dates current_period = STAKEHOLDER.staking_agent.get_current_period() bonded_date = datetime_at_period(period=current_period, seconds_per_period=economics.seconds_per_period) min_worker_periods = STAKEHOLDER.staking_agent.staking_parameters()[7] release_period = current_period + min_worker_periods release_date = datetime_at_period(period=release_period, seconds_per_period=economics.seconds_per_period) emitter.echo(f"\nWorker {worker_address} successfully bonded to staker {staking_address}", color='green') paint_receipt_summary(emitter=emitter, receipt=receipt, chain_name=blockchain.client.chain_name, transaction_type='set_worker') emitter.echo(f"Bonded at period #{current_period} ({bonded_date})", color='green') emitter.echo(f"This worker can be replaced or detached after period " f"#{release_period} ({release_date})", color='green') return # Exit elif action == 'detach-worker': if not staking_address: staking_address = select_stake(stakeholder=STAKEHOLDER, emitter=emitter).staker_address if worker_address: raise click.BadOptionUsage(message="detach-worker cannot be used together with --worker-address", option_name='--worker-address') # TODO: Check preconditions (e.g., minWorkerPeriods) worker_address = STAKEHOLDER.staking_agent.get_worker_from_staker(staking_address) password = None if not hw_wallet and not blockchain.client.is_local: password = get_client_password(checksum_address=staking_address) # TODO: Create Stakeholder.detach_worker() and use it here STAKEHOLDER.assimilate(checksum_address=staking_address, password=password) receipt = STAKEHOLDER.set_worker(worker_address=BlockchainInterface.NULL_ADDRESS) # TODO: Double-check dates current_period = STAKEHOLDER.staking_agent.get_current_period() bonded_date = datetime_at_period(period=current_period, seconds_per_period=economics.seconds_per_period) emitter.echo(f"Successfully detached worker {worker_address} from staker {staking_address}", color='green') paint_receipt_summary(emitter=emitter, receipt=receipt, chain_name=blockchain.client.chain_name, transaction_type='detach_worker') emitter.echo(f"Detached at period #{current_period} ({bonded_date})", color='green') return # Exit elif action == 'create': """Initialize a new stake""" # # Get Staking Account # password = None if not staking_address: staking_address = select_client_account(prompt="Select staking account", emitter=emitter, provider_uri=STAKEHOLDER.wallet.blockchain.provider_uri) if not hw_wallet and not blockchain.client.is_local: password = click.prompt(f"Enter password to unlock {staking_address}", hide_input=True, confirmation_prompt=False) # # Stage Stake # if not value: value = click.prompt(f"Enter stake value in NU", type=stake_value_range, default=NU.from_nunits(min_locked).to_tokens()) value = NU.from_tokens(value) if not lock_periods: prompt = f"Enter stake duration ({STAKEHOLDER.economics.minimum_locked_periods} periods minimum)" lock_periods = click.prompt(prompt, type=stake_duration_range) start_period = STAKEHOLDER.staking_agent.get_current_period() end_period = start_period + lock_periods # # Review # if not force: painting.paint_staged_stake(emitter=emitter, stakeholder=STAKEHOLDER, staking_address=staking_address, stake_value=value, lock_periods=lock_periods, start_period=start_period, end_period=end_period) confirm_staged_stake(staker_address=staking_address, value=value, lock_periods=lock_periods) # Last chance to bail click.confirm("Publish staged stake to the blockchain?", abort=True) # Execute STAKEHOLDER.assimilate(checksum_address=staking_address, password=password) new_stake = STAKEHOLDER.initialize_stake(amount=value, lock_periods=lock_periods) painting.paint_staking_confirmation(emitter=emitter, ursula=STAKEHOLDER, transactions=new_stake.transactions) return # Exit elif action == "restake": # Authenticate if not staking_address: staking_address = select_stake(stakeholder=STAKEHOLDER, emitter=emitter).staker_address password = None if not hw_wallet and not blockchain.client.is_local: password = get_client_password(checksum_address=staking_address) STAKEHOLDER.assimilate(checksum_address=staking_address, password=password) # Inner Exclusive Switch if lock_until: if not force: confirm_enable_restaking_lock(emitter, staking_address=staking_address, release_period=lock_until) receipt = STAKEHOLDER.enable_restaking_lock(release_period=lock_until) emitter.echo(f'Successfully enabled re-staking lock for {staking_address} until {lock_until}', color='green', verbosity=1) elif enable: if not force: confirm_enable_restaking(emitter, staking_address=staking_address) receipt = STAKEHOLDER.enable_restaking() emitter.echo(f'Successfully enabled re-staking for {staking_address}', color='green', verbosity=1) else: if not force: click.confirm(f"Confirm disable re-staking for staker {staking_address}?", abort=True) receipt = STAKEHOLDER.disable_restaking() emitter.echo(f'Successfully disabled re-staking for {staking_address}', color='green', verbosity=1) paint_receipt_summary(receipt=receipt, emitter=emitter, chain_name=blockchain.client.chain_name) return # Exit elif action == 'divide': """Divide an existing stake by specifying the new target value and end period""" if staking_address and index is not None: # 0 is valid. current_stake = STAKEHOLDER.stakes[index] else: current_stake = select_stake(stakeholder=STAKEHOLDER, emitter=emitter) # # Stage Stake # # Value if not value: value = click.prompt(f"Enter target value (must be less than or equal to {str(current_stake.value)})", type=stake_value_range) value = NU(value, 'NU') # Duration if not lock_periods: extension = click.prompt("Enter number of periods to extend", type=stake_extension_range) else: extension = lock_periods if not force: painting.paint_staged_stake_division(emitter=emitter, stakeholder=STAKEHOLDER, original_stake=current_stake, target_value=value, extension=extension) click.confirm("Is this correct?", abort=True) # Execute password = None if not hw_wallet and not blockchain.client.is_local: password = get_client_password(checksum_address=current_stake.staker_address) STAKEHOLDER.assimilate(checksum_address=current_stake.staker_address, password=password) modified_stake, new_stake = STAKEHOLDER.divide_stake(stake_index=current_stake.index, target_value=value, additional_periods=extension) emitter.echo('Successfully divided stake', color='green', verbosity=1) paint_receipt_summary(emitter=emitter, receipt=new_stake.receipt, chain_name=blockchain.client.chain_name) # Show the resulting stake list painting.paint_stakes(emitter=emitter, stakes=STAKEHOLDER.stakes) return # Exit elif action == 'collect-reward': """Withdraw staking reward to the specified wallet address""" password = None if not hw_wallet and not blockchain.client.is_local: password = get_client_password(checksum_address=staking_address) if not staking_reward and not policy_reward: raise click.BadArgumentUsage(f"Either --staking-reward or --policy-reward must be True to collect rewards.") STAKEHOLDER.assimilate(checksum_address=staking_address, password=password) if staking_reward: # Note: Sending staking / inflation rewards to another account is not allowed. staking_receipt = STAKEHOLDER.collect_staking_reward() paint_receipt_summary(receipt=staking_receipt, chain_name=STAKEHOLDER.wallet.blockchain.client.chain_name, emitter=emitter) if policy_reward: policy_receipt = STAKEHOLDER.collect_policy_reward(collector_address=withdraw_address) paint_receipt_summary(receipt=policy_receipt, chain_name=STAKEHOLDER.wallet.blockchain.client.chain_name, emitter=emitter) return # Exit # Catch-All for unknown actions else: ctx = click.get_current_context() raise click.UsageError(message=f"Unknown action '{action}'.", ctx=ctx)
def stake( click_config, action, config_root, config_file, # Mode force, offline, hw_wallet, # Blockchain poa, registry_filepath, provider_uri, sync, # Stake staking_address, worker_address, withdraw_address, value, duration, index, policy_reward, staking_reward, ) -> None: """ Manage stakes and other staker-related operations. \b Actions ------------------------------------------------- new-stakeholder Create a new stakeholder configuration list List active stakes for current stakeholder accounts Show ETH and NU balances for stakeholder's accounts sync Synchronize stake data with on-chain information set-worker Bound a worker to a staker detach-worker Detach worker currently bound to a staker init Create a new stake divide Create a new stake from part of an existing one collect-reward Withdraw staking reward """ # Banner emitter = click_config.emitter emitter.clear() emitter.banner(NU_BANNER) if action == 'new-stakeholder': if not provider_uri: raise click.BadOptionUsage( option_name='--provider', message="--provider is required to create a new stakeholder.") registry = None fetch_registry = True if registry_filepath: registry = EthereumContractRegistry( registry_filepath=registry_filepath) fetch_registry = False blockchain = BlockchainInterface(provider_uri=provider_uri, registry=registry, poa=poa) blockchain.connect(sync_now=sync, fetch_registry=fetch_registry) new_stakeholder = StakeHolder(config_root=config_root, offline_mode=offline, blockchain=blockchain) filepath = new_stakeholder.to_configuration_file(override=force) emitter.echo(f"Wrote new stakeholder configuration to {filepath}", color='green') return # Exit # # Make Staker # STAKEHOLDER = StakeHolder.from_configuration_file( filepath=config_file, provider_uri=provider_uri, registry_filepath=registry_filepath, offline=offline, sync_now=sync) # # Eager Actions # if action == 'list': if not STAKEHOLDER.stakes: emitter.echo(f"There are no active stakes") else: painting.paint_stakes(emitter=emitter, stakes=STAKEHOLDER.stakes) return elif action == 'accounts': for address, balances in STAKEHOLDER.account_balances.items(): emitter.echo( f"{address} | {Web3.fromWei(balances['ETH'], 'ether')} ETH | {NU.from_nunits(balances['NU'])}" ) return # Exit elif action == 'sync': emitter.echo("Reading on-chain stake data...") STAKEHOLDER.read_onchain_stakes() STAKEHOLDER.to_configuration_file(override=True) emitter.echo("OK!", color='green') return # Exit elif action in ('set-worker', 'detach-worker'): if not staking_address: staking_address = select_stake(stakeholder=STAKEHOLDER, emitter=emitter).owner_address if action == 'set-worker': if not worker_address: worker_address = click.prompt("Enter worker address", type=EIP55_CHECKSUM_ADDRESS) elif action == 'detach-worker': if worker_address: raise click.BadOptionUsage( message= "detach-worker cannot be used together with --worker-address option" ) worker_address = BlockchainInterface.NULL_ADDRESS password = None if not hw_wallet and not STAKEHOLDER.blockchain.client.is_local: password = get_client_password(checksum_address=staking_address) receipt = STAKEHOLDER.set_worker(staker_address=staking_address, password=password, worker_address=worker_address) emitter.echo(f"OK | Receipt: {receipt['transactionHash'].hex()}", color='green') return # Exit elif action == 'init': """Initialize a new stake""" # # Get Staking Account # password = None if not staking_address: staking_address = select_client_account( blockchain=STAKEHOLDER.blockchain, prompt="Select staking account", emitter=emitter) if not hw_wallet and not STAKEHOLDER.blockchain.client.is_local: password = click.prompt( f"Enter password to unlock {staking_address}", hide_input=True, confirmation_prompt=False) # # Stage Stake # if not value: min_locked = STAKEHOLDER.economics.minimum_allowed_locked value = click.prompt( f"Enter stake value in NU", type=STAKE_VALUE, default=NU.from_nunits(min_locked).to_tokens()) value = NU.from_tokens(value) if not duration: prompt = f"Enter stake duration ({STAKEHOLDER.economics.minimum_locked_periods} periods minimum)" duration = click.prompt(prompt, type=STAKE_DURATION) start_period = STAKEHOLDER.staking_agent.get_current_period() end_period = start_period + duration # # Review # if not force: painting.paint_staged_stake(emitter=emitter, stakeholder=STAKEHOLDER, staking_address=staking_address, stake_value=value, duration=duration, start_period=start_period, end_period=end_period) confirm_staged_stake(staker_address=staking_address, value=value, duration=duration) # Last chance to bail click.confirm("Publish staged stake to the blockchain?", abort=True) # Execute new_stake = STAKEHOLDER.initialize_stake( amount=value, duration=duration, checksum_address=staking_address, password=password) painting.paint_staking_confirmation( emitter=emitter, ursula=STAKEHOLDER, transactions=new_stake.transactions) return # Exit elif action == 'divide': """Divide an existing stake by specifying the new target value and end period""" if staking_address and index is not None: staker = STAKEHOLDER.get_active_staker(address=staking_address) current_stake = staker.stakes[index] else: current_stake = select_stake(stakeholder=STAKEHOLDER, emitter=emitter) # # Stage Stake # # Value if not value: value = click.prompt( f"Enter target value (must be less than or equal to {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(emitter=emitter, stakeholder=STAKEHOLDER, original_stake=current_stake, target_value=value, extension=extension) click.confirm("Is this correct?", abort=True) # Execute password = None if not hw_wallet and not STAKEHOLDER.blockchain.client.is_local: password = get_client_password( checksum_address=current_stake.owner_address) modified_stake, new_stake = STAKEHOLDER.divide_stake( address=current_stake.owner_address, index=current_stake.index, value=value, duration=extension, password=password) emitter.echo('Successfully divided stake', color='green', verbosity=1) emitter.echo(f'Receipt ........... {new_stake.receipt}', verbosity=1) # Show the resulting stake list painting.paint_stakes(emitter=emitter, stakes=STAKEHOLDER.stakes) return # Exit elif action == 'collect-reward': """Withdraw staking reward to the specified wallet address""" password = None if not hw_wallet and not STAKEHOLDER.blockchain.client.is_local: password = get_client_password(checksum_address=staking_address) STAKEHOLDER.collect_rewards(staker_address=staking_address, withdraw_address=withdraw_address, password=password, staking=staking_reward, policy=policy_reward) else: ctx = click.get_current_context() click.UsageError(message=f"Unknown action '{action}'.", ctx=ctx).show() return # Exit
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 divide( click_config, # Stake Options poa, light, registry_filepath, config_file, provider_uri, staking_address, hw_wallet, beneficiary_address, allocation_filepath, # Other force, value, lock_periods, index): """ Create a new stake from part of an existing one. """ ### Setup ### emitter = _setup_emitter(click_config) STAKEHOLDER, blockchain = _create_stakeholder( config_file, provider_uri, poa, light, registry_filepath, staking_address, beneficiary_address=beneficiary_address, allocation_filepath=allocation_filepath) ############# client_account, staking_address = handle_client_account_for_staking( emitter=emitter, stakeholder=STAKEHOLDER, staking_address=staking_address, individual_allocation=STAKEHOLDER.individual_allocation, force=force) economics = STAKEHOLDER.economics # Dynamic click types (Economics) min_locked = economics.minimum_allowed_locked stake_value_range = click.FloatRange( min=NU.from_nunits(min_locked).to_tokens(), clamp=False) stake_extension_range = click.IntRange( min=1, max=economics.maximum_allowed_locked, clamp=False) if staking_address and index is not None: # 0 is valid. STAKEHOLDER.stakes = StakeList(registry=STAKEHOLDER.registry, checksum_address=staking_address) STAKEHOLDER.stakes.refresh() current_stake = STAKEHOLDER.stakes[index] else: current_stake = select_stake(stakeholder=STAKEHOLDER, emitter=emitter) # # Stage Stake # # Value if not value: value = click.prompt( f"Enter target value (must be less than or equal to {str(current_stake.value)})", type=stake_value_range) value = NU(value, 'NU') # Duration if not lock_periods: extension = click.prompt("Enter number of periods to extend", type=stake_extension_range) else: extension = lock_periods if not force: painting.paint_staged_stake_division(emitter=emitter, stakeholder=STAKEHOLDER, original_stake=current_stake, target_value=value, extension=extension) click.confirm("Is this correct?", abort=True) # Execute password = None if not hw_wallet and not blockchain.client.is_local: password = get_client_password( checksum_address=current_stake.staker_address) STAKEHOLDER.assimilate(checksum_address=current_stake.staker_address, password=password) modified_stake, new_stake = STAKEHOLDER.divide_stake( stake_index=current_stake.index, target_value=value, additional_periods=extension) emitter.echo('Successfully divided stake', color='green', verbosity=1) paint_receipt_summary(emitter=emitter, receipt=new_stake.receipt, chain_name=blockchain.client.chain_name) # Show the resulting stake list painting.paint_stakes(emitter=emitter, stakes=STAKEHOLDER.stakes)