def paint_staged_stake(emitter, blockchain, stakeholder, staking_address, stake_value, lock_periods, start_period, unlock_period, division_message: str = None): economics = stakeholder.staker.economics start_datetime = datetime_at_period( period=start_period, seconds_per_period=economics.seconds_per_period, start_of_period=True) unlock_datetime = datetime_at_period( period=unlock_period, seconds_per_period=economics.seconds_per_period, start_of_period=True) locked_days = (lock_periods * economics.hours_per_period) // 24 start_datetime_pretty = start_datetime.local_datetime().strftime( "%b %d %Y %H:%M %Z") unlock_datetime_pretty = unlock_datetime.local_datetime().strftime( "%b %d %Y %H:%M %Z") if division_message: emitter.echo(f"\n{'═' * 30} ORIGINAL STAKE {'═' * 28}", bold=True) emitter.echo(division_message) emitter.echo(f"\n{'═' * 30} STAGED STAKE {'═' * 30}", bold=True) emitter.echo(f""" Staking address: {staking_address} ~ Chain -> ID # {blockchain.client.chain_id} | {blockchain.client.chain_name} ~ Value -> {stake_value} ({int(stake_value)} NuNits) ~ Duration -> {locked_days} Days ({lock_periods} Periods) ~ Enactment -> {start_datetime_pretty} (period #{start_period}) ~ Expiration -> {unlock_datetime_pretty} (period #{unlock_period}) """) # TODO: periods != Days - Do we inform the user here? emitter.echo('═' * 73, bold=True)
def __init__(self, staking_agent: StakingEscrowAgent, checksum_address: str, value: NU, first_locked_period: int, final_locked_period: int, index: int, economics): self.log = Logger(f'stake-{checksum_address}-{index}') # Ownership self.staker_address = checksum_address # Stake Metadata self.index = index self.value = value # Periods self.first_locked_period = first_locked_period # TODO: #1502 - Move Me Brightly - Docs # After this period has passes, workers can go offline, if this is the only stake. # This is the last period that can be committed for this stake. # Meaning, It must be committed in the previous period, # and no commitment can be made in this period for this stake. self.final_locked_period = final_locked_period # Blockchain self.staking_agent = staking_agent # Economics self.economics = economics self.minimum_nu = NU(int(self.economics.minimum_allowed_locked), 'NuNit') self.maximum_nu = NU(int(self.economics.maximum_allowed_locked), 'NuNit') # Time self.start_datetime = datetime_at_period(period=first_locked_period, seconds_per_period=self.economics.seconds_per_period, start_of_period=True) self.unlock_datetime = datetime_at_period(period=final_locked_period + 1, seconds_per_period=self.economics.seconds_per_period, start_of_period=True) self._status = None
def events(general_config, registry_options, contract_name, from_block, to_block, event_name): """ Show events associated to NuCypher contracts """ emitter = _setup_emitter(general_config) registry = registry_options.get_registry(emitter, general_config.debug) blockchain = BlockchainInterfaceFactory.get_interface( provider_uri=registry_options.provider_uri) if not contract_name: if event_name: raise click.BadOptionUsage( option_name='--event-name', message='--event-name requires --contract-name') contract_names = [ STAKING_ESCROW_CONTRACT_NAME, POLICY_MANAGER_CONTRACT_NAME ] else: contract_names = [contract_name] if from_block is None: # Sketch of logic for getting the approximate block height of current period start, # so by default, this command only shows events of the current period last_block = blockchain.client.w3.eth.blockNumber staking_agent = ContractAgency.get_agent(StakingEscrowAgent, registry=registry) current_period = staking_agent.get_current_period() current_period_start = datetime_at_period( period=current_period, seconds_per_period=staking_agent.staking_parameters()[0], start_of_period=True) seconds_from_midnight = int( (maya.now() - current_period_start).total_seconds()) blocks_from_midnight = seconds_from_midnight // AVERAGE_BLOCK_TIME_IN_SECONDS from_block = last_block - blocks_from_midnight if to_block is None: to_block = 'latest' # TODO: additional input validation for block numbers emitter.echo(f"Showing events from block {from_block} to {to_block}") for contract_name in contract_names: title = f" {contract_name} Events ".center(40, "-") emitter.echo(f"\n{title}\n", bold=True, color='green') agent = ContractAgency.get_agent_by_contract_name( contract_name, registry) names = agent.events.names if not event_name else [event_name] for name in names: emitter.echo(f"{name}:", bold=True, color='yellow') event_method = agent.events[name] for event_record in event_method(from_block=from_block, to_block=to_block): emitter.echo(f" - {event_record}")
def __init__(self, owner_address: str, index: int, value: NU, start_period: int, end_period: int): # Stake Info self.owner_address = owner_address self.index = index self.value = value self.start_period = start_period self.end_period = end_period self.duration = (self.end_period-self.start_period) + 1 # Internals self.start_datetime = datetime_at_period(period=start_period) self.end_datetime = datetime_at_period(period=end_period) self.duration_delta = self.end_datetime - self.start_datetime
def __init__(self, checksum_address: str, value: NU, start_period: int, end_period: int, index: int, economics=None, validate_now: bool = True): self.log = Logger(f'stake-{checksum_address}-{index}') # Stake Metadata self.owner_address = checksum_address self.worker_address = UNKNOWN_WORKER_STATUS self.index = index self.value = value self.start_period = start_period self.end_period = end_period # Time self.start_datetime = datetime_at_period(period=start_period) self.end_datetime = datetime_at_period(period=end_period) self.duration_delta = self.end_datetime - self.start_datetime # Agency self.staking_agent = None self.token_agent = NucypherTokenAgent() # TODO: Use Agency self.blockchain = self.token_agent.blockchain # Economics from nucypher.blockchain.economics import TokenEconomics self.economics = economics or TokenEconomics() self.minimum_nu = NU(int(self.economics.minimum_allowed_locked), 'NuNit') self.maximum_nu = NU(int(self.economics.maximum_allowed_locked), 'NuNit') if validate_now: self.validate_duration() self.transactions = NO_STAKING_RECEIPT self.receipt = NO_STAKING_RECEIPT
def __init__(self, miner, value: NU, start_period: int, end_period: int, index: int, validate_now: bool = True): self.miner = miner owner_address = miner.checksum_public_address self.log = Logger(f'stake-{owner_address}-{index}') # Stake Metadata self.owner_address = owner_address self.index = index self.value = value self.start_period = start_period self.end_period = end_period # Time self.start_datetime = datetime_at_period(period=start_period) self.end_datetime = datetime_at_period(period=end_period) self.duration_delta = self.end_datetime - self.start_datetime self.blockchain = miner.blockchain # Agency self.miner_agent = miner.miner_agent self.token_agent = miner.token_agent # Economics self.economics = miner.economics self.minimum_nu = NU(int(self.economics.minimum_allowed_locked), 'NuNit') self.maximum_nu = NU(int(self.economics.maximum_allowed_locked), 'NuNit') if validate_now: self.validate_duration() self.transactions = NO_STAKING_RECEIPT self.receipt = NO_STAKING_RECEIPT
def _measure_start_of_next_period(self) -> str: """Returns iso8601 datetime of next period""" current_period = datetime_to_period( datetime=maya.now(), seconds_per_period=self.economics.seconds_per_period) next_period = datetime_at_period( period=current_period + 1, seconds_per_period=self.economics.seconds_per_period, start_of_period=True) return next_period.iso8601()
def test_staker_prolongs_stake(staker, token_economics): stake_index = 0 origin_stake = staker.stakes[stake_index] # Can't use additional periods and expiration together new_expiration = datetime_at_period(period=origin_stake.final_locked_period + 3, seconds_per_period=token_economics.seconds_per_period, start_of_period=True) with pytest.raises(ValueError): staker.prolong_stake(stake=origin_stake, additional_periods=3, expiration=new_expiration) staker.prolong_stake(stake=origin_stake, additional_periods=3) stake = staker.stakes[stake_index] assert stake.first_locked_period == origin_stake.first_locked_period assert stake.final_locked_period == origin_stake.final_locked_period + 3 assert stake.value == origin_stake.value # Provided stake must be part of current stakes with pytest.raises(ValueError): staker.prolong_stake(stake=origin_stake, additional_periods=2) stake.index = len(staker.stakes) with pytest.raises(ValueError): staker.prolong_stake(stake=stake, additional_periods=2) stake.index = stake_index # New expiration date must extend stake duration origin_stake = stake new_expiration = datetime_at_period(period=origin_stake.final_locked_period, seconds_per_period=token_economics.seconds_per_period, start_of_period=True) with pytest.raises(ValueError): staker.prolong_stake(stake=origin_stake, expiration=new_expiration) new_expiration = origin_stake.unlock_datetime staker.prolong_stake(stake=origin_stake, expiration=new_expiration) stake = staker.stakes[stake_index] assert stake.first_locked_period == origin_stake.first_locked_period assert stake.final_locked_period == origin_stake.final_locked_period + 1 assert stake.value == origin_stake.value
def generate_policy_parameters( self, number_of_ursulas: int = None, duration_periods: int = None, expiration: maya.MayaDT = None, value: int = None, rate: int = None, first_period_reward: int = None, ) -> dict: """ Construct policy creation from parameters or overrides. """ if not duration_periods and not expiration: raise ValueError( "Policy end time must be specified as 'expiration' or 'duration_periods', got neither." ) # Merge injected and default params. first_period_reward = first_period_reward or self.first_period_reward rate = rate or self.rate duration_periods = duration_periods or self.duration_periods # Calculate duration in periods and expiration datetime if expiration: duration_periods = calculate_period_duration( future_time=expiration, seconds_per_period=self.economics.seconds_per_period) else: duration_periods = duration_periods or self.duration_periods expiration = datetime_at_period( self.staking_agent.get_current_period() + duration_periods, seconds_per_period=self.economics.seconds_per_period) from nucypher.policy.policies import BlockchainPolicy blockchain_payload = BlockchainPolicy.generate_policy_parameters( n=number_of_ursulas, duration_periods=duration_periods, first_period_reward=first_period_reward, value=value, rate=rate) # These values may have been recalculated in this block. policy_end_time = dict(duration_periods=duration_periods, expiration=expiration) payload = {**blockchain_payload, **policy_end_time} return payload
def unbond_worker(general_config, transacting_staker_options, config_file, force): """ Unbond worker currently bonded to a staker. """ emitter = setup_emitter(general_config) STAKEHOLDER = transacting_staker_options.create_character( emitter, config_file) blockchain = transacting_staker_options.get_blockchain() economics = STAKEHOLDER.economics client_account, staking_address = select_client_account_for_staking( emitter=emitter, stakeholder=STAKEHOLDER, staking_address=transacting_staker_options.staker_options. staking_address, individual_allocation=STAKEHOLDER.individual_allocation, force=force) # TODO: Check preconditions (e.g., minWorkerPeriods) worker_address = STAKEHOLDER.staking_agent.get_worker_from_staker( staking_address) password = transacting_staker_options.get_password(blockchain, client_account) STAKEHOLDER.assimilate(password=password) receipt = STAKEHOLDER.unbond_worker() # 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) message = SUCCESSFUL_DETACH_WORKER.format(worker_address=worker_address, staking_address=staking_address) emitter.echo(message, color='green') paint_receipt_summary(emitter=emitter, receipt=receipt, chain_name=blockchain.client.chain_name, transaction_type='unbond_worker') emitter.echo(DETACH_DETAILS.format(current_period=current_period, bonded_date=bonded_date), color='green')
def commit_to_next_period(general_config, character_options, config_file): """Manually make a commitment to the next period.""" # Setup emitter = setup_emitter(general_config, character_options.config_options.worker_address) _pre_launch_warnings(emitter, dev=character_options.config_options.dev, force=None) _, URSULA = character_options.create_character(emitter, config_file, general_config.json_ipc, load_seednodes=False) committed_period = URSULA.staking_agent.get_current_period() + 1 click.echo(CONFIRMING_ACTIVITY_NOW.format(committed_period=committed_period), color='blue') receipt = URSULA.commit_to_next_period(fire_and_forget=False) economics = EconomicsFactory.get_economics(registry=URSULA.registry) date = datetime_at_period(period=committed_period, seconds_per_period=economics.seconds_per_period) # TODO: Double-check dates here message = SUCCESSFUL_CONFIRM_ACTIVITY.format(committed_period=committed_period, date=date) emitter.echo(message, bold=True, color='blue') paint_receipt_summary(emitter=emitter, receipt=receipt, chain_name=URSULA.staking_agent.blockchain.client.chain_name)
def confirm_activity(general_config, character_options, config_file): """ Manually confirm-activity for the current period. """ emitter = _setup_emitter(general_config, character_options.config_options.worker_address) _pre_launch_warnings(emitter, dev=character_options.config_options.dev, force=None) _, URSULA = character_options.create_character(emitter, config_file, general_config.json_ipc, load_seednodes=False) confirmed_period = URSULA.staking_agent.get_current_period() + 1 click.echo(f"Confirming activity for period {confirmed_period}", color='blue') receipt = URSULA.confirm_activity() economics = TokenEconomicsFactory.get_economics(registry=URSULA.registry) date = datetime_at_period(period=confirmed_period, seconds_per_period=economics.seconds_per_period) # TODO: Double-check dates here emitter.echo(f'\nActivity confirmed for period #{confirmed_period} ' f'(starting at {date})', bold=True, color='blue') painting.paint_receipt_summary(emitter=emitter, receipt=receipt, chain_name=URSULA.staking_agent.blockchain.client.chain_name)
def bond_worker(general_config, transacting_staker_options, config_file, force, worker_address): """Bond a worker to a staker.""" emitter = setup_emitter(general_config) STAKEHOLDER = transacting_staker_options.create_character( emitter, config_file) blockchain = transacting_staker_options.get_blockchain() economics = STAKEHOLDER.economics client_account, staking_address = select_client_account_for_staking( emitter=emitter, stakeholder=STAKEHOLDER, staking_address=transacting_staker_options.staker_options. staking_address, individual_allocation=STAKEHOLDER.individual_allocation, force=force) if not worker_address: worker_address = click.prompt(PROMPT_WORKER_ADDRESS, type=EIP55_CHECKSUM_ADDRESS) if (worker_address == staking_address) and not force: click.confirm(CONFIRM_WORKER_AND_STAKER_ADDRESSES_ARE_EQUAL.format( address=worker_address), abort=True) # TODO: Check preconditions (e.g., minWorkerPeriods, already in use, etc) password = transacting_staker_options.get_password(blockchain, client_account) # TODO: Double-check dates # Calculate release datetime 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.economics.minimum_worker_periods release_period = current_period + min_worker_periods release_date = datetime_at_period( period=release_period, seconds_per_period=economics.seconds_per_period, start_of_period=True) if not force: click.confirm( f"Commit to bonding " f"worker {worker_address} to staker {staking_address} " f"for a minimum of {STAKEHOLDER.economics.minimum_worker_periods} periods?", abort=True) STAKEHOLDER.assimilate(checksum_address=client_account, password=password) receipt = STAKEHOLDER.bond_worker(worker_address=worker_address) # Report Success message = SUCCESSFUL_WORKER_BONDING.format(worker_address=worker_address, staking_address=staking_address) emitter.echo(message, color='green') paint_receipt_summary(emitter=emitter, receipt=receipt, chain_name=blockchain.client.chain_name, transaction_type='bond_worker') emitter.echo(BONDING_DETAILS.format(current_period=current_period, bonded_date=bonded_date), color='green') emitter.echo(BONDING_RELEASE_INFO.format(release_period=release_period, release_date=release_date), color='green')
def _learn_about_nodes(self, threaded: bool = True): if threaded: if self.__collecting_nodes: self.log.debug( "Skipping Round - Nodes collection thread is already running" ) return return reactor.callInThread(self._learn_about_nodes, threaded=False) self.__collecting_nodes = True agent = self.staking_agent known_nodes = list(self.known_nodes) block_time = agent.blockchain.client.w3.eth.getBlock( 'latest').timestamp # precision in seconds current_period = agent.get_current_period() log = f'Processing {len(known_nodes)} nodes at {MayaDT(epoch=block_time)} | Period {current_period}' self.log.info(log) data = list() for node in known_nodes: staker_address = node.checksum_address worker = agent.get_worker_from_staker(staker_address) stake = agent.owned_tokens(staker_address) staked_nu_tokens = float(NU.from_nunits(stake).to_tokens()) locked_nu_tokens = float( NU.from_nunits( agent.get_locked_tokens( staker_address=staker_address)).to_tokens()) economics = EconomicsFactory.get_economics(registry=self.registry) stakes = StakeList(checksum_address=staker_address, registry=self.registry) stakes.refresh() if stakes.initial_period is NOT_STAKING: continue # TODO: Skip this measurement for now start_date = datetime_at_period( stakes.initial_period, seconds_per_period=economics.seconds_per_period) start_date = start_date.datetime().timestamp() end_date = datetime_at_period( stakes.terminal_period, seconds_per_period=economics.seconds_per_period) end_date = end_date.datetime().timestamp() last_confirmed_period = agent.get_last_active_period( staker_address) num_work_orders = 0 # len(node.work_orders()) # TODO: Only works for is_me with datastore attached # TODO: do we need to worry about how much information is in memory if number of nodes is # large i.e. should I check for size of data and write within loop if too big data.append( self.NODE_LINE_PROTOCOL.format( measurement=self.NODE_MEASUREMENT, staker_address=staker_address, worker_address=worker, start_date=start_date, end_date=end_date, stake=staked_nu_tokens, locked_stake=locked_nu_tokens, current_period=current_period, last_confirmed_period=last_confirmed_period, timestamp=block_time, work_orders=num_work_orders)) success = self._influx_client.write_points( data, database=self.INFLUX_DB_NAME, time_precision='s', batch_size=10000, protocol='line') self.__collecting_nodes = False if not success: # TODO: What do we do here - Event hook for alerting? self.log.warn( f'Unable to write node information to database {self.INFLUX_DB_NAME} at ' f'{MayaDT(epoch=block_time)} | Period {current_period}')
def set_worker( click_config, # Worker Options poa, light, registry_filepath, config_file, provider_uri, staking_address, hw_wallet, beneficiary_address, allocation_filepath, worker_address, # Other options force): """ Bond a worker to a staker. """ ### 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) ############# economics = STAKEHOLDER.economics client_account, staking_address = handle_client_account_for_staking( emitter=emitter, stakeholder=STAKEHOLDER, staking_address=staking_address, individual_allocation=STAKEHOLDER.individual_allocation, force=force) 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=client_account) STAKEHOLDER.assimilate(checksum_address=client_account, 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, start_of_period=True) 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')
def detach_worker( click_config, # Worker Options poa, light, registry_filepath, config_file, provider_uri, staking_address, hw_wallet, beneficiary_address, allocation_filepath, worker_address, # Other options force): """ Detach worker currently bonded to a staker. """ ### 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) ############# economics = STAKEHOLDER.economics client_account, staking_address = handle_client_account_for_staking( emitter=emitter, stakeholder=STAKEHOLDER, staking_address=staking_address, individual_allocation=STAKEHOLDER.individual_allocation, force=force) 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=client_account) STAKEHOLDER.assimilate(checksum_address=client_account, password=password) receipt = STAKEHOLDER.detach_worker() # 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')
def ursula( click_config, action, dev, dry_run, force, lonely, network, teacher_uri, min_stake, rest_host, rest_port, db_filepath, staker_address, worker_address, federated_only, poa, config_root, config_file, provider_uri, geth, registry_filepath, interactive, sync, ) -> None: """ "Ursula the Untrusted" PRE Re-encryption node management commands. \b Actions ------------------------------------------------- \b init Create a new Ursula node configuration. view View the Ursula node's configuration. run Run an "Ursula" node. save-metadata Manually write node metadata to disk without running forget Forget all known nodes. destroy Delete Ursula node configuration. confirm-activity Manually confirm-activity for the current period. """ emitter = click_config.emitter # # Validate # if federated_only: if geth: raise click.BadOptionUsage( option_name="--geth", message="Federated only cannot be used with the --geth flag") if staker_address: raise click.BadOptionUsage( option_name='--federated-only', message="Staking address cannot be used in federated mode.") # Banner emitter.banner(URSULA_BANNER.format(worker_address or '')) # # Pre-Launch Warnings # if dev: emitter.echo("WARNING: Running in Development mode", color='yellow', verbosity=1) if force: emitter.echo("WARNING: Force is enabled", color='yellow', verbosity=1) # # Internal Ethereum Client # ETH_NODE = NO_BLOCKCHAIN_CONNECTION if geth: ETH_NODE = actions.get_provider_process() provider_uri = ETH_NODE.provider_uri(scheme='file') # # Eager Actions # if action == "init": """Create a brand-new persistent Ursula""" if dev: raise click.BadArgumentUsage( "Cannot create a persistent development character") if (not staker_address or not worker_address) and not federated_only: # Connect to Blockchain fetch_registry = registry_filepath is None and not click_config.no_registry registry = None if registry_filepath: registry = EthereumContractRegistry( registry_filepath=registry_filepath) blockchain = BlockchainInterface(provider_uri=provider_uri, registry=registry, poa=poa) blockchain.connect(fetch_registry=fetch_registry, sync_now=sync, emitter=emitter) if not staker_address: prompt = "Select staker account" staker_address = select_client_account(emitter=emitter, blockchain=blockchain, prompt=prompt) if not worker_address: prompt = "Select worker account" worker_address = select_client_account(emitter=emitter, blockchain=blockchain, prompt=prompt) if not config_root: # Flag config_root = click_config.config_file # Envvar if not rest_host: rest_host = actions.determine_external_ip_address(emitter, force=force) download_registry = not federated_only and not click_config.no_registry ursula_config = UrsulaConfiguration.generate( password=get_nucypher_password(confirm=True), config_root=config_root, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath, domains={network} if network else None, federated_only=federated_only, checksum_address=staker_address, worker_address=worker_address, download_registry=download_registry, registry_filepath=registry_filepath, provider_process=ETH_NODE, provider_uri=provider_uri, poa=poa) painting.paint_new_installation_help(emitter, new_configuration=ursula_config) return # # Make Ursula # if dev: ursula_config = UrsulaConfiguration( dev_mode=True, domains={TEMPORARY_DOMAIN}, poa=poa, download_registry=False, registry_filepath=registry_filepath, provider_process=ETH_NODE, provider_uri=provider_uri, checksum_address=staker_address, worker_address=worker_address, federated_only=federated_only, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath) else: try: ursula_config = UrsulaConfiguration.from_configuration_file( filepath=config_file, domains={network} if network else None, registry_filepath=registry_filepath, provider_process=ETH_NODE, provider_uri=provider_uri, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath, poa=poa, federated_only=federated_only) except FileNotFoundError: return actions.handle_missing_configuration_file( character_config_class=UrsulaConfiguration, config_file=config_file) except NucypherKeyring.AuthenticationFailed as e: emitter.echo(str(e), color='red', bold=True) click.get_current_context().exit(1) # TODO: Exit codes (not only for this, but for other exceptions) # # Configured Pre-Authentication Actions # # Handle destruction and forget *before* network bootstrap and character initialization below if action == "destroy": """Delete all configuration files from the disk""" if dev: message = "'nucypher ursula destroy' cannot be used in --dev mode - There is nothing to destroy." raise click.BadOptionUsage(option_name='--dev', message=message) actions.destroy_configuration(emitter, character_config=ursula_config, force=force) return elif action == "forget": actions.forget(emitter, configuration=ursula_config) return # # Make Ursula # client_password = None if not ursula_config.federated_only: if not dev and not click_config.json_ipc: client_password = get_client_password( checksum_address=ursula_config.worker_address, envvar="NUCYPHER_WORKER_ETH_PASSWORD") try: URSULA = actions.make_cli_character(character_config=ursula_config, click_config=click_config, min_stake=min_stake, teacher_uri=teacher_uri, dev=dev, lonely=lonely, client_password=client_password) except NucypherKeyring.AuthenticationFailed as e: emitter.echo(str(e), color='red', bold=True) click.get_current_context().exit(1) # TODO: Exit codes (not only for this, but for other exceptions) # # Authenticated Action Switch # if action == 'run': """Seed, Produce, Run!""" # GO! try: # Ursula Deploy Warnings emitter.message(f"Starting Ursula on {URSULA.rest_interface}", color='green', bold=True) emitter.message(f"Connecting to {','.join(ursula_config.domains)}", color='green', bold=True) emitter.message("Working ~ Keep Ursula Online!", color='blue', bold=True) if interactive: stdio.StandardIO( UrsulaCommandProtocol(ursula=URSULA, emitter=emitter)) if dry_run: return # <-- ABORT - (Last Chance) # Run - Step 3 node_deployer = URSULA.get_deployer() node_deployer.addServices() node_deployer.catalogServers(node_deployer.hendrix) node_deployer.run() # <--- Blocking Call (Reactor) # Handle Crash except Exception as e: ursula_config.log.critical(str(e)) emitter.message(f"{e.__class__.__name__} {e}", color='red', bold=True) raise # Crash :-( # Graceful Exit finally: emitter.message("Stopping Ursula", color='green') ursula_config.cleanup() emitter.message("Ursula Stopped", color='red') return elif action == "save-metadata": """Manually save a node self-metadata file""" metadata_path = ursula.write_node_metadata(node=URSULA) emitter.message( f"Successfully saved node metadata to {metadata_path}.", color='green') return elif action == "view": """Paint an existing configuration to the console""" if not URSULA.federated_only: emitter.echo("BLOCKCHAIN ----------\n") painting.paint_contract_status(emitter=emitter, blockchain=URSULA.blockchain) current_block = URSULA.blockchain.w3.eth.blockNumber emitter.echo(f'Block # {current_block}') # TODO: 1231 emitter.echo(f'NU Balance (staker): {URSULA.token_balance}') emitter.echo( f'ETH Balance (worker): {URSULA.blockchain.client.get_balance(URSULA.worker_address)}' ) emitter.echo( f'Current Gas Price {URSULA.blockchain.client.gas_price}') emitter.echo("CONFIGURATION --------") response = UrsulaConfiguration._read_configuration_file( filepath=config_file or ursula_config.config_file_location) return emitter.ipc( response=response, request_id=0, duration=0 ) # FIXME: #1216 - what are request_id and duration here? elif action == 'confirm-activity': receipt = URSULA.confirm_activity() confirmed_period = URSULA.staking_agent.get_current_period() + 1 date = datetime_at_period(period=confirmed_period) # TODO: Double-check dates here emitter.echo( f'\nActivity confirmed for period #{confirmed_period} ' f'(starting at {date})', bold=True, color='blue') painting.paint_receipt_summary( emitter=emitter, receipt=receipt, chain_name=URSULA.blockchain.client.chain_name) # TODO: Check ActivityConfirmation event (see #1193) return else: raise click.BadArgumentUsage("No such argument {}".format(action))
def test_staker_divides_stake(staker, token_economics): stake_value = NU(token_economics.minimum_allowed_locked * 5, 'NuNit') new_stake_value = NU(token_economics.minimum_allowed_locked * 2, 'NuNit') stake_index = 0 staker.initialize_stake(amount=stake_value, lock_periods=int(token_economics.minimum_locked_periods)) stake = staker.stakes[stake_index + 1] # Can't use additional periods and expiration together with pytest.raises(ValueError): staker.divide_stake(target_value=new_stake_value, stake=stake, additional_periods=2, expiration=maya.now()) staker.divide_stake(target_value=new_stake_value, stake=stake, additional_periods=2) current_period = staker.staking_agent.get_current_period() expected_old_stake = (current_period + 1, current_period + 30, stake_value - new_stake_value) expected_new_stake = (current_period + 1, current_period + 32, new_stake_value) assert 3 == len(staker.stakes), 'A new stake was not added to this stakers stakes' assert expected_old_stake == staker.stakes[stake_index + 1].to_stake_info(), 'Old stake values are invalid' assert expected_new_stake == staker.stakes[stake_index + 2].to_stake_info(), 'New stake values are invalid' # Provided stake must be part of current stakes new_stake_value = NU.from_nunits(token_economics.minimum_allowed_locked) with pytest.raises(ValueError): staker.divide_stake(target_value=new_stake_value, stake=stake, additional_periods=2) stake = staker.stakes[stake_index + 1] stake.index = len(staker.stakes) with pytest.raises(ValueError): staker.divide_stake(target_value=new_stake_value, stake=stake, additional_periods=2) yet_another_stake_value = NU(token_economics.minimum_allowed_locked, 'NuNit') stake = staker.stakes[stake_index + 2] # New expiration date must extend stake duration origin_stake = stake new_expiration = datetime_at_period(period=origin_stake.final_locked_period, seconds_per_period=token_economics.seconds_per_period, start_of_period=True) with pytest.raises(ValueError): staker.divide_stake(target_value=yet_another_stake_value, stake=stake, expiration=new_expiration) new_expiration = datetime_at_period(period=origin_stake.final_locked_period + 2, seconds_per_period=token_economics.seconds_per_period, start_of_period=True) staker.divide_stake(target_value=yet_another_stake_value, stake=stake, expiration=new_expiration) expected_new_stake = (current_period + 1, current_period + 32, new_stake_value) expected_yet_another_stake = Stake(first_locked_period=current_period + 1, final_locked_period=current_period + 34, value=yet_another_stake_value, checksum_address=staker.checksum_address, index=3, staking_agent=staker.staking_agent, economics=token_economics) assert 4 == len(staker.stakes), 'A new stake was not added after two stake divisions' assert expected_old_stake == staker.stakes[ stake_index + 1].to_stake_info(), 'Old stake values are invalid after two stake divisions' assert expected_new_stake == staker.stakes[ stake_index + 2].to_stake_info(), 'New stake values are invalid after two stake divisions' assert expected_yet_another_stake.value == staker.stakes[stake_index + 3].value, 'Third stake values are invalid'
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 set_worker(general_config, transacting_staker_options, config_file, force, worker_address): """ Bond a worker to a staker. """ emitter = _setup_emitter(general_config) STAKEHOLDER = transacting_staker_options.create_character( emitter, config_file) blockchain = transacting_staker_options.get_blockchain() economics = STAKEHOLDER.economics 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) if not worker_address: worker_address = click.prompt("Enter worker address", type=EIP55_CHECKSUM_ADDRESS) if (worker_address == staking_address) and not force: click.confirm( "The worker address provided is the same as the staking account. " "It is highly recommended to use a different accounts for staker and worker roles.", abort=True) # TODO: Check preconditions (e.g., minWorkerPeriods, already in use, etc) password = transacting_staker_options.get_password(blockchain, client_account) # TODO: Double-check dates # Calculate release datetime 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.economics.minimum_worker_periods release_period = current_period + min_worker_periods release_date = datetime_at_period( period=release_period, seconds_per_period=economics.seconds_per_period, start_of_period=True) if not force: click.confirm( f"Commit to bonding " f"worker {worker_address} to staker {staking_address} " f"for a minimum of {STAKEHOLDER.economics.minimum_worker_periods} periods?", abort=True) STAKEHOLDER.assimilate(checksum_address=client_account, password=password) receipt = STAKEHOLDER.set_worker(worker_address=worker_address) # Report Success 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')
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 Bond a worker to a staker detach-worker Detach worker currently bonded 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.") fetch_registry = registry_filepath is None and not click_config.no_registry registry = None if registry_filepath: registry = EthereumContractRegistry(registry_filepath=registry_filepath) blockchain = BlockchainInterface(provider_uri=provider_uri, registry=registry, poa=poa) blockchain.connect(fetch_registry=fetch_registry, sync_now=sync, emitter=emitter) 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 Stakeholder # 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 == 'set-worker': if not staking_address: staking_address = select_stake(stakeholder=STAKEHOLDER, emitter=emitter).owner_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 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) # TODO: Double-check dates current_period = STAKEHOLDER.staking_agent.get_current_period() bonded_date = datetime_at_period(period=current_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) emitter.echo(f"\nWorker {worker_address} successfully bonded to staker {staking_address}", color='green') paint_receipt_summary(emitter=emitter, receipt=receipt, chain_name=STAKEHOLDER.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).owner_address if worker_address: raise click.BadOptionUsage(message="detach-worker cannot be used together with --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 STAKEHOLDER.blockchain.client.is_local: password = get_client_password(checksum_address=staking_address) # TODO: Create Stakeholder.detach_worker() and use it here receipt = STAKEHOLDER.set_worker(staker_address=staking_address, password=password, 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) emitter.echo(f"Successfully detached worker {worker_address} from staker {staking_address}", color='green') paint_receipt_summary(emitter=emitter, receipt=receipt, chain_name=STAKEHOLDER.blockchain.client.chain_name, transaction_type='detach_worker') emitter.echo(f"Detached at period #{current_period} ({bonded_date})", 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) paint_receipt_summary(emitter=emitter, receipt=new_stake.receipt, chain_name=STAKEHOLDER.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 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