def test_handle_selection_with_no_divisible_stakes( test_emitter, token_economics, mock_staking_agent, test_registry, mock_testerchain, mock_stdin, # used to assert the user hasn't been prompted capsys, non_divisible_stakes): # Setup mock_staking_agent.get_all_stakes.return_value = non_divisible_stakes stakeholder = StakeHolder(registry=test_registry) stakeholder.assimilate(checksum_address=mock_testerchain.etherbase_account, password=INSECURE_DEVELOPMENT_PASSWORD) # FAILURE: Divisible only with no divisible stakes on chain with pytest.raises(click.Abort): select_stake(emitter=test_emitter, divisible=True, stakeholder=stakeholder) # Divisible warning was displayed, but having # no divisible stakes cases an expected failure captured = capsys.readouterr() assert NO_STAKES_FOUND not in captured.out assert ONLY_DISPLAYING_DIVISIBLE_STAKES_NOTE in captured.out assert_stake_table_not_painted(output=captured.out)
def select_client_account_for_staking( emitter: StdoutEmitter, stakeholder: StakeHolder, staking_address: Optional[str], ) -> Tuple[str, str]: """ Manages client account selection for stake-related operations. It always returns a tuple of addresses: the first is the local client account and the second is the staking address. When this is not a preallocation staker (which is the normal use case), both addresses are the same. Otherwise, when the staker is a contract managed by a beneficiary account, then the local client account is the beneficiary, and the staking address is the address of the staking contract. """ if staking_address: client_account = staking_address else: client_account = select_client_account( prompt=SELECT_STAKING_ACCOUNT_INDEX, emitter=emitter, registry=stakeholder.registry, network=stakeholder.domain, signer=stakeholder.signer) staking_address = client_account stakeholder.assimilate(client_account) return client_account, staking_address
def stakeholder_with_no_divisible_stakes(mock_testerchain, token_economics, mock_staking_agent, test_registry, non_divisible_stakes): mock_staking_agent.get_all_stakes.return_value = non_divisible_stakes stakeholder = StakeHolder(registry=test_registry) account = mock_testerchain.etherbase_account stakeholder.assimilate(checksum_address=account, password=INSECURE_DEVELOPMENT_PASSWORD) return stakeholder
def test_stakeholder_configuration(test_emitter, test_registry, mock_testerchain, mock_staking_agent): stakeholder_config_options = StakeHolderConfigOptions( provider_uri=MOCK_PROVIDER_URI, poa=None, light=None, registry_filepath=None, network=TEMPORARY_DOMAIN, signer_uri=None) mock_staking_agent.get_all_stakes.return_value = [SubStakeInfo(1, 2, 3)] force = False selected_index = 0 selected_account = mock_testerchain.client.accounts[selected_index] expected_stakeholder = StakeHolder(registry=test_registry, domains={TEMPORARY_DOMAIN}, initial_address=selected_account) expected_stakeholder.refresh_stakes() staker_options = StakerOptions(config_options=stakeholder_config_options, staking_address=selected_account) transacting_staker_options = TransactingStakerOptions( staker_options=staker_options, hw_wallet=None, beneficiary_address=None, allocation_filepath=None) stakeholder_from_configuration = transacting_staker_options.create_character( emitter=test_emitter, config_file=None) client_account, staking_address = select_client_account_for_staking( emitter=test_emitter, stakeholder=stakeholder_from_configuration, staking_address=selected_account, individual_allocation=None, force=force) assert client_account == staking_address == selected_account assert stakeholder_from_configuration.stakes == expected_stakeholder.stakes assert stakeholder_from_configuration.checksum_address == client_account staker_options = StakerOptions(config_options=stakeholder_config_options, staking_address=None) transacting_staker_options = TransactingStakerOptions( staker_options=staker_options, hw_wallet=None, beneficiary_address=None, allocation_filepath=None) stakeholder_from_configuration = transacting_staker_options.create_character( emitter=None, config_file=None) client_account, staking_address = select_client_account_for_staking( emitter=test_emitter, stakeholder=stakeholder_from_configuration, staking_address=selected_account, individual_allocation=None, force=force) assert client_account == staking_address == selected_account assert stakeholder_from_configuration.stakes == expected_stakeholder.stakes assert stakeholder_from_configuration.checksum_address == client_account
def test_new_stakeholder(click_runner, custom_filepath, mock_registry_filepath, testerchain): init_args = ('stake', 'new-stakeholder', '--poa', '--config-root', custom_filepath, '--provider', TEST_PROVIDER_URI, '--registry-filepath', mock_registry_filepath) result = click_runner.invoke(nucypher_cli, init_args, catch_exceptions=False) assert result.exit_code == 0 # Files and Directories assert os.path.isdir(custom_filepath), 'Configuration file does not exist' custom_config_filepath = os.path.join(custom_filepath, StakeHolder.generate_filename()) assert os.path.isfile(custom_config_filepath), 'Configuration file does not exist' with open(custom_config_filepath, 'r') as config_file: raw_config_data = config_file.read() config_data = json.loads(raw_config_data) assert config_data['blockchain']['provider_uri'] == TEST_PROVIDER_URI
def select_stake(stakeholder: StakeHolder, emitter: StdoutEmitter, divisible: bool = False, staker_address: str = None ) -> Stake: """Interactively select a stake or abort if there are no eligible stakes.""" # Precondition: Active Stakes if staker_address: staker = stakeholder.get_staker(checksum_address=staker_address) stakes = staker.stakes else: stakes = stakeholder.all_stakes if not stakes: emitter.echo(NO_STAKES_FOUND, color='red') raise click.Abort # Precondition: Divisible Stakes stakes = stakeholder.sorted_stakes if divisible: emitter.echo(ONLY_DISPLAYING_DIVISIBLE_STAKES_NOTE, color='yellow') stakes = stakeholder.divisible_stakes if not stakes: emitter.echo(NO_DIVISIBLE_STAKES, color='red') raise click.Abort # Interactive Selection enumerated_stakes = dict(enumerate(stakes)) 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 test_select_client_account_for_staking_cli_action( test_emitter, test_registry, test_registry_source_manager, mock_stdin, mock_testerchain, capsys, mocker, mock_staking_agent): """Fine-grained assertions about the return value of interactive client account selection""" force = False mock_staking_agent.get_all_stakes.return_value = [] selected_index = 0 selected_account = mock_testerchain.client.accounts[selected_index] stakeholder = StakeHolder(registry=test_registry, domains={TEMPORARY_DOMAIN}) client_account, staking_address = select_client_account_for_staking( emitter=test_emitter, stakeholder=stakeholder, staking_address=selected_account, individual_allocation=None, force=force) assert client_account == staking_address == selected_account mock_stdin.line(str(selected_index)) client_account, staking_address = select_client_account_for_staking( emitter=test_emitter, stakeholder=stakeholder, staking_address=None, individual_allocation=None, force=force) assert client_account == staking_address == selected_account assert mock_stdin.empty() staking_contract_address = '0xFABADA' mock_individual_allocation = mocker.Mock( beneficiary_address=selected_account, contract_address=staking_contract_address) mock_stdin.line(YES) client_account, staking_address = select_client_account_for_staking( emitter=test_emitter, stakeholder=stakeholder, individual_allocation=mock_individual_allocation, staking_address=None, force=force) assert client_account == selected_account assert staking_address == staking_contract_address assert mock_stdin.empty() captured = capsys.readouterr() message = PREALLOCATION_STAKE_ADVISORY.format( client_account=selected_account, staking_address=staking_contract_address) assert message in captured.out
def software_stakeholder(testerchain, agency, stakeholder_config_file_location, test_registry): token_agent = ContractAgency.get_agent(NucypherTokenAgent, registry=test_registry) # Setup path = stakeholder_config_file_location if path.exists(): path.unlink() # 0xaAa482c790b4301bE18D75A0D1B11B2ACBEF798B stakeholder_private_key = '255f64a948eeb1595b8a2d1e76740f4683eca1c8f1433d13293db9b6e27676cc' address = testerchain.provider.ethereum_tester.add_account( private_key=stakeholder_private_key, password=INSECURE_DEVELOPMENT_PASSWORD) testerchain.provider.ethereum_tester.unlock_account( account=address, password=INSECURE_DEVELOPMENT_PASSWORD) tx = { 'to': address, 'from': testerchain.etherbase_account, 'value': Web3.toWei('1', 'ether') } txhash = testerchain.client.w3.eth.sendTransaction(tx) _receipt = testerchain.wait_for_receipt(txhash) # Mock TransactingPower consumption (Etherbase) transacting_power = TransactingPower( account=testerchain.etherbase_account, signer=Web3Signer(testerchain.client), password=INSECURE_DEVELOPMENT_PASSWORD) token_agent.transfer(amount=NU(200_000, 'NU').to_nunits(), transacting_power=transacting_power, target_address=address) # Create stakeholder from on-chain values given accounts over a web3 provider signer = Web3Signer(testerchain.client) signer.unlock_account(account=address, password=INSECURE_DEVELOPMENT_PASSWORD) stakeholder = StakeHolder(registry=test_registry, domain=TEMPORARY_DOMAIN, signer=signer, initial_address=address) # Teardown yield stakeholder if path.exists(): path.unlink()
def select_client_account_for_staking( emitter: StdoutEmitter, stakeholder: StakeHolder, staking_address: Optional[str], individual_allocation: Optional[IndividualAllocationRegistry], force: bool, ) -> Tuple[str, str]: """ Manages client account selection for stake-related operations. It always returns a tuple of addresses: the first is the local client account and the second is the staking address. When this is not a preallocation staker (which is the normal use case), both addresses are the same. Otherwise, when the staker is a contract managed by a beneficiary account, then the local client account is the beneficiary, and the staking address is the address of the staking contract. """ if individual_allocation: client_account = individual_allocation.beneficiary_address staking_address = individual_allocation.contract_address message = PREALLOCATION_STAKE_ADVISORY.format( client_account=client_account, staking_address=staking_address) emitter.echo(message, color='yellow', verbosity=1) if not force: click.confirm(IS_THIS_CORRECT, abort=True) else: if staking_address: client_account = staking_address else: client_account = select_client_account( prompt=SELECT_STAKING_ACCOUNT_INDEX, emitter=emitter, registry=stakeholder.registry, network=stakeholder.network, wallet=stakeholder.wallet) staking_address = client_account stakeholder.set_staker(client_account) return client_account, staking_address
def stakeholder(current_period, mock_staking_agent, test_registry): mock_staking_agent.get_current_period.return_value = current_period staker_info = StakerInfo(current_committed_period=current_period - 1, next_committed_period=current_period, value=0, last_committed_period=0, lock_restake_until_period=False, completed_work=0, worker_start_period=0, worker=NULL_ADDRESS, flags=bytes()) mock_staking_agent.get_staker_info.return_value = staker_info return StakeHolder(registry=test_registry)
def software_stakeholder(testerchain, agency, stakeholder_config_file_location): # Setup path = stakeholder_config_file_location if os.path.exists(path): os.remove(path) # 0xaAa482c790b4301bE18D75A0D1B11B2ACBEF798B stakeholder_private_key = '255f64a948eeb1595b8a2d1e76740f4683eca1c8f1433d13293db9b6e27676cc' address = testerchain.provider.ethereum_tester.add_account( stakeholder_private_key, password=INSECURE_DEVELOPMENT_PASSWORD) testerchain.provider.ethereum_tester.unlock_account( address, password=INSECURE_DEVELOPMENT_PASSWORD) tx = { 'to': address, 'from': testerchain.etherbase_account, 'value': Web3.toWei('1', 'ether') } txhash = testerchain.client.w3.eth.sendTransaction(tx) _receipt = testerchain.wait_for_receipt(txhash) # Mock TransactingPower consumption (Etherbase) transacting_power = TransactingPower( account=testerchain.etherbase_account, password=INSECURE_DEVELOPMENT_PASSWORD, blockchain=testerchain) transacting_power.activate() token_agent = Agency.get_agent(NucypherTokenAgent) token_agent.transfer(amount=NU(200_000, 'NU').to_nunits(), sender_address=testerchain.etherbase_account, target_address=address) # Create stakeholder from on-chain values given accounts over a web3 provider stakeholder = StakeHolder(blockchain=testerchain, funding_account=address, funding_password=INSECURE_DEVELOPMENT_PASSWORD, trezor=False) # Teardown yield stakeholder if os.path.exists(path): os.remove(path)
def stakeholder(current_period, mock_staking_agent, test_registry, mock_testerchain): mock_staking_agent.get_current_period.return_value = current_period staker_info = StakerInfo(current_committed_period=current_period-1, next_committed_period=current_period, value=0, last_committed_period=0, lock_restake_until_period=False, completed_work=0, worker_start_period=0, worker=NULL_ADDRESS, flags=bytes()) mock_staking_agent.get_staker_info.return_value = staker_info return StakeHolder(registry=test_registry, domain=TEMPORARY_DOMAIN, signer=Web3Signer(mock_testerchain.client))
def test_handle_select_stake_with_no_stakes( test_emitter, token_economics, mock_staking_agent, test_registry, mock_testerchain, mock_stdin, # used to assert user hasn't been prompted capsys): # Setup mock_stakes = [] mock_staking_agent.get_all_stakes.return_value = mock_stakes stakeholder = StakeHolder(registry=test_registry) # Test with pytest.raises(click.Abort): select_stake(emitter=test_emitter, stakeholder=stakeholder) # Examine captured = capsys.readouterr() assert NO_STAKES_FOUND in captured.out assert_stake_table_not_painted(output=captured.out)
def test_software_stakeholder_configuration(testerchain, software_stakeholder, stakeholder_config_file_location): stakeholder = software_stakeholder path = stakeholder_config_file_location # Check attributes can be successfully read assert stakeholder.total_stake == 0 assert not stakeholder.stakes assert stakeholder.accounts # Save the stakeholder JSON config stakeholder.to_configuration_file(filepath=path) with open(stakeholder.filepath, 'r') as file: # Ensure file contents are serializable contents = file.read() first_config_contents = json.loads(contents) # Destroy this stake holder, leaving only the configuration file behind del stakeholder # Restore StakeHolder instance from JSON config the_same_stakeholder = StakeHolder.from_configuration_file( filepath=path, funding_password=INSECURE_DEVELOPMENT_PASSWORD, blockchain=testerchain) # Save the JSON config again the_same_stakeholder.to_configuration_file(filepath=path, override=True) with open(the_same_stakeholder.filepath, 'r') as file: contents = file.read() second_config_contents = json.loads(contents) # Ensure the stakeholder was accurately restored from JSON config assert first_config_contents == second_config_contents
def test_select_client_account_for_staking_cli_action( test_emitter, test_registry, test_registry_source_manager, mock_stdin, mock_testerchain, capsys, mock_staking_agent): """Fine-grained assertions about the return value of interactive client account selection""" mock_staking_agent.get_all_stakes.return_value = [] selected_index = 0 selected_account = mock_testerchain.client.accounts[selected_index] stakeholder = StakeHolder(registry=test_registry, domain=TEMPORARY_DOMAIN, signer=Web3Signer(mock_testerchain.client)) client_account, staking_address = select_client_account_for_staking( emitter=test_emitter, stakeholder=stakeholder, staking_address=selected_account) assert client_account == staking_address == selected_account mock_stdin.line(str(selected_index)) client_account, staking_address = select_client_account_for_staking( emitter=test_emitter, stakeholder=stakeholder, staking_address=None) assert client_account == staking_address == selected_account assert mock_stdin.empty()
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 stakeholder_configuration_file_location(custom_filepath): _configuration_file_location = os.path.join( MOCK_CUSTOM_INSTALLATION_PATH, StakeHolder.generate_filename()) return _configuration_file_location