From 73f606a31d704850476beb97fdb6ade5fdaa33cf Mon Sep 17 00:00:00 2001 From: Kieran Prasch Date: Sat, 6 Jul 2019 09:52:09 -0700 Subject: [PATCH] Restores staking CLI tests post #1029 via Stakeholder API. --- nucypher/blockchain/eth/actors.py | 25 +- nucypher/blockchain/eth/interfaces.py | 9 +- nucypher/blockchain/eth/token.py | 7 +- nucypher/cli/stake.py | 17 +- ...sula.py => test_stakeholder_and_ursula.py} | 213 ++++++++++++------ 5 files changed, 187 insertions(+), 84 deletions(-) rename tests/cli/ursula/{test_blockchain_ursula.py => test_stakeholder_and_ursula.py} (59%) diff --git a/nucypher/blockchain/eth/actors.py b/nucypher/blockchain/eth/actors.py index 2f36bfb5e..c70c27447 100644 --- a/nucypher/blockchain/eth/actors.py +++ b/nucypher/blockchain/eth/actors.py @@ -745,10 +745,6 @@ class StakeHolder(BaseConfiguration): self.funding_power.activate() self.__funding_account = funding_account - self.staking_agent = StakingEscrowAgent(blockchain=blockchain) - self.token_agent = NucypherTokenAgent(blockchain=blockchain) - self.economics = TokenEconomics() - self.__accounts = list() self.__stakers = dict() self.__transacting_powers = dict() @@ -782,7 +778,8 @@ class StakeHolder(BaseConfiguration): @classmethod def from_configuration_file(cls, filepath: str = None, - blockchain = None, + provider_uri: str = None, + registry_filepath: str = None, **overrides) -> 'StakeHolder': filepath = filepath or cls.default_filepath() @@ -790,8 +787,11 @@ class StakeHolder(BaseConfiguration): # Sub config blockchain_payload = payload.pop('blockchain') - if not blockchain: - blockchain = BlockchainInterface.from_dict(payload=blockchain_payload) + blockchain = BlockchainInterface.from_dict(payload=blockchain_payload, + provider_uri=provider_uri, + registry_filepath=registry_filepath) + + blockchain.connect() instance = cls(filepath=filepath, blockchain=blockchain, **payload, **overrides) @@ -1039,19 +1039,26 @@ class StakeHolder(BaseConfiguration): def collect_rewards(self, staker_address: str, password: str, + withdraw_address: str = None, staking: bool = True, policy: bool = True) -> Dict[str, dict]: if not staking and not policy: raise ValueError("Either staking or policy must be True in order to collect rewards") - staker = self.get_active_staker(address=staker_address) + try: + staker = self.get_active_staker(address=staker_address) + except self.NoStakes: + staker = Staker(is_me=True, checksum_address=staker_address, blockchain=self.blockchain) + self.attach_transacting_power(checksum_address=staker.checksum_address, password=password) receipts = dict() if staking: receipts['staking_reward'] = staker.collect_staking_reward() if policy: - receipts['policy_reward'] = staker.collect_policy_reward(collector_address=self.funding_account) + withdraw_address = withdraw_address or self.funding_account + receipts['policy_reward'] = staker.collect_policy_reward(collector_address=withdraw_address) + self.to_configuration_file(override=True) return receipts diff --git a/nucypher/blockchain/eth/interfaces.py b/nucypher/blockchain/eth/interfaces.py index d6584f58e..2b757a4bd 100644 --- a/nucypher/blockchain/eth/interfaces.py +++ b/nucypher/blockchain/eth/interfaces.py @@ -171,10 +171,13 @@ class BlockchainInterface: return r @classmethod - def from_dict(cls, payload: dict) -> 'BlockchainInterface': + def from_dict(cls, payload: dict, **overrides) -> 'BlockchainInterface': + + # Apply overrides + payload.update({k: v for k, v in overrides.items() if v is not None}) + registry = EthereumContractRegistry(registry_filepath=payload['registry_filepath']) - blockchain = cls(provider_uri=payload['provider_uri'], - registry=registry) + blockchain = cls(provider_uri=payload['provider_uri'], registry=registry) return blockchain def to_dict(self) -> dict: diff --git a/nucypher/blockchain/eth/token.py b/nucypher/blockchain/eth/token.py index 7c1817c6f..80e77db3a 100644 --- a/nucypher/blockchain/eth/token.py +++ b/nucypher/blockchain/eth/token.py @@ -281,12 +281,7 @@ class Stake: rulebook = ( (self.minimum_nu <= self.value, - 'Stake amount too low; ({amount}) must be at least {minimum}' - .format(minimum=self.minimum_nu, amount=self.value)), - - (self.maximum_nu >= self.value, - 'Stake amount too high; ({amount}) must be no more than {maximum}.' - .format(maximum=self.maximum_nu, amount=self.value)), + f'Stake amount too low; ({self.minimum_nu}) must be at least {self.value}'), ) if raise_on_fail is True: diff --git a/nucypher/cli/stake.py b/nucypher/cli/stake.py index 2f1e353de..0c72e816d 100644 --- a/nucypher/cli/stake.py +++ b/nucypher/cli/stake.py @@ -101,6 +101,8 @@ def stake(click_config, # STAKEHOLDER = StakeHolder.from_configuration_file(filepath=config_file, + provider_uri=provider_uri, + registry_filepath=registry_filepath, offline=offline) # # Eager Actions @@ -172,7 +174,10 @@ def stake(click_config, confirmation_prompt=False) if not pre_funded: - fund_now = click.confirm("Fund staking account with funding account?", abort=False, default=True) + if force: + fund_now = True + else: + fund_now = click.confirm("Fund staking account with funding account?", abort=False, default=True) else: # TODO: Validate the balance of self-manged funders. fund_now = False @@ -235,7 +240,11 @@ def stake(click_config, elif action == 'divide': """Divide an existing stake by specifying the new target value and end period""" - current_stake = select_stake(stakeholder=STAKEHOLDER) + 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) # # Stage Stake @@ -280,6 +289,8 @@ def stake(click_config, click.confirm(f"Send {STAKEHOLDER.calculate_reward()} to {STAKEHOLDER.funding_account}?") password = get_password(confirm=False) - STAKEHOLDER.collect_rewards(staker_address=staking_address, password=password) + STAKEHOLDER.collect_rewards(staker_address=staking_address, + withdraw_address=withdraw_address, + password=password) return # Exit diff --git a/tests/cli/ursula/test_blockchain_ursula.py b/tests/cli/ursula/test_stakeholder_and_ursula.py similarity index 59% rename from tests/cli/ursula/test_blockchain_ursula.py rename to tests/cli/ursula/test_stakeholder_and_ursula.py index 0b49ff207..fb0fb790d 100644 --- a/tests/cli/ursula/test_blockchain_ursula.py +++ b/tests/cli/ursula/test_stakeholder_and_ursula.py @@ -6,8 +6,9 @@ import datetime import maya import pytest -from nucypher.blockchain.eth.actors import Staker -from nucypher.blockchain.eth.agents import StakingEscrowAgent +from nucypher.blockchain.eth.actors import Staker, StakeHolder, Worker +from nucypher.blockchain.eth.agents import StakingEscrowAgent, Agency +from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface, BlockchainInterface from nucypher.blockchain.eth.token import NU from nucypher.characters.lawful import Enrico from nucypher.cli.main import nucypher_cli @@ -20,16 +21,22 @@ from nucypher.utilities.sandbox.constants import ( INSECURE_DEVELOPMENT_PASSWORD, MOCK_REGISTRY_FILEPATH, TEMPORARY_DOMAIN, -) + NUMBER_OF_URSULAS_IN_BLOCKCHAIN_TESTS) from nucypher.utilities.sandbox.middleware import MockRestMiddleware @pytest.fixture(scope='module') -def configuration_file_location(custom_filepath): +def worker_configuration_file_location(custom_filepath): _configuration_file_location = os.path.join(MOCK_CUSTOM_INSTALLATION_PATH, UrsulaConfiguration.generate_filename()) return _configuration_file_location +@pytest.fixture(scope='module') +def staker_configuration_file_location(custom_filepath): + _configuration_file_location = os.path.join(MOCK_CUSTOM_INSTALLATION_PATH, StakeHolder.generate_filename()) + return _configuration_file_location + + @pytest.fixture(scope="module") def charlie_blockchain_test_config(blockchain_ursulas, agency): token_agent, staking_agent, policy_agent = agency @@ -51,7 +58,7 @@ def charlie_blockchain_test_config(blockchain_ursulas, agency): @pytest.fixture(scope='module') -def mock_registry_filepath(testerchain): +def mock_registry_filepath(testerchain, agency): registry = testerchain.registry @@ -65,15 +72,48 @@ def mock_registry_filepath(testerchain): os.remove(MOCK_REGISTRY_FILEPATH) -def test_initialize_system_blockchain_configuration(click_runner, - custom_filepath, - mock_registry_filepath, - staking_participant): +def test_new_stakeholder(click_runner, + custom_filepath, + mock_registry_filepath, + staking_participant, + testerchain): + + init_args = ('stake', 'new-stakeholder', + '--poa', + '--funding-address', testerchain.etherbase_account, + '--config-root', custom_filepath, + '--provider-uri', 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 len(config_data['stakers']) == NUMBER_OF_URSULAS_IN_BLOCKCHAIN_TESTS + assert config_data['blockchain']['provider_uri'] == TEST_PROVIDER_URI + + +def test_ursula_init(click_runner, + custom_filepath, + mock_registry_filepath, + staking_participant, + testerchain): init_args = ('ursula', 'init', '--poa', '--network', TEMPORARY_DOMAIN, - '--checksum-address', staking_participant.checksum_address, + '--staker-address', testerchain.etherbase_account, + '--worker-address', staking_participant.checksum_address, '--config-root', custom_filepath, '--provider-uri', TEST_PROVIDER_URI, '--registry-filepath', mock_registry_filepath, @@ -99,45 +139,67 @@ def test_initialize_system_blockchain_configuration(click_runner, raw_config_data = config_file.read() config_data = json.loads(raw_config_data) assert config_data['provider_uri'] == TEST_PROVIDER_URI - assert config_data['checksum_address'] == staking_participant.checksum_address + assert config_data['checksum_address'] == testerchain.etherbase_account assert TEMPORARY_DOMAIN in config_data['domains'] -@pytest.mark.skip(reason="Wait for #1056") -def test_init_ursula_stake(click_runner, - configuration_file_location, - funded_blockchain, - stake_value, - token_economics): +def test_stake_init(click_runner, + staker_configuration_file_location, + funded_blockchain, + stake_value, + mock_registry_filepath, + token_economics, + testerchain, + agency): + + # Simulate "Reconnection" + cached_blockchain = BlockchainInterface.reconnect() + registry = cached_blockchain.registry + assert registry.filepath == mock_registry_filepath + + def from_dict(*args, **kwargs): + return testerchain + BlockchainInterface.from_dict = from_dict stake_args = ('stake', 'init', - '--config-file', configuration_file_location, - '--value', stake_value.to_tokens(), + '--config-file', staker_configuration_file_location, + '--registry-filepath', mock_registry_filepath, + '--value', stake_value.to_nunits(), '--duration', token_economics.minimum_locked_periods, '--force') - result = click_runner.invoke(nucypher_cli, stake_args, input=INSECURE_DEVELOPMENT_PASSWORD, catch_exceptions=False) + user_input = f'0\n' + f'{INSECURE_DEVELOPMENT_PASSWORD}\n' + f'Y\n' + result = click_runner.invoke(nucypher_cli, stake_args, input=user_input, catch_exceptions=False) assert result.exit_code == 0 - with open(configuration_file_location, 'r') as config_file: + with open(staker_configuration_file_location, 'r') as config_file: config_data = json.loads(config_file.read()) # Verify the stake is on-chain - staking_agent = StakingEscrowAgent() - stakes = list(staking_agent.get_all_stakes(staker_address=config_data['checksum_address'])) + staking_agent = Agency.get_agent(StakingEscrowAgent) + stakes = list(staking_agent.get_all_stakes(staker_address=testerchain.etherbase_account)) assert len(stakes) == 1 start_period, end_period, value = stakes[0] assert NU(int(value), 'NuNit') == stake_value -@pytest.mark.skip(reason="Wait for #1056") -def test_list_ursula_stakes(click_runner, - funded_blockchain, - configuration_file_location, - stake_value): - stake_args = ('stake', 'list', - '--config-file', configuration_file_location, - '--poa') +def test_stake_list(click_runner, + funded_blockchain, + staker_configuration_file_location, + stake_value, + mock_registry_filepath, + testerchain): + + # Simulate "Reconnection" + cached_blockchain = BlockchainInterface.reconnect() + registry = cached_blockchain.registry + assert registry.filepath == mock_registry_filepath + + def from_dict(*args, **kwargs): + return testerchain + BlockchainInterface.from_dict = from_dict + + stake_args = ('stake', 'list', '--config-file', staker_configuration_file_location) user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}' result = click_runner.invoke(nucypher_cli, stake_args, input=user_input, catch_exceptions=False) @@ -145,12 +207,15 @@ def test_list_ursula_stakes(click_runner, assert str(stake_value) in result.output -@pytest.mark.skip(reason="Wait for #1056") -def test_ursula_divide_stakes(click_runner, configuration_file_location, token_economics): +def test_ursula_divide_stakes(click_runner, + staker_configuration_file_location, + token_economics, + testerchain): divide_args = ('stake', 'divide', - '--config-file', configuration_file_location, + '--config-file', staker_configuration_file_location, '--force', + '--staking-address', testerchain.etherbase_account, '--index', 0, '--value', NU(token_economics.minimum_allowed_locked, 'NuNit').to_tokens(), '--duration', 10) @@ -162,7 +227,7 @@ def test_ursula_divide_stakes(click_runner, configuration_file_location, token_e assert result.exit_code == 0 stake_args = ('stake', 'list', - '--config-file', configuration_file_location, + '--config-file', staker_configuration_file_location, '--poa') user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}' @@ -171,14 +236,32 @@ def test_ursula_divide_stakes(click_runner, configuration_file_location, token_e assert str(NU(token_economics.minimum_allowed_locked, 'NuNit').to_tokens()) in result.output -@pytest.mark.skip(reason="Wait for #1056") -def test_run_blockchain_ursula(click_runner, - configuration_file_location, - staking_participant): - # Now start running your Ursula! - init_args = ('ursula', 'run', - '--dry-run', - '--config-file', configuration_file_location) +def test_stake_set_worker(click_runner, + testerchain, + staker_configuration_file_location, + staking_participant): + + init_args = ('stake', 'set-worker', + '--config-file', staker_configuration_file_location, + '--staking-address', testerchain.etherbase_account, + '--worker-address', testerchain.etherbase_account, # TODO: Use Manual Worker + '--force') + + user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}' + result = click_runner.invoke(nucypher_cli, + init_args, + input=user_input, + catch_exceptions=False) + assert result.exit_code == 0 + + +def test_run_blockchain_ursula(click_runner, + worker_configuration_file_location, + staking_participant): + # Now start running your Ursula! + init_args = ('ursula', 'run', + '--dry-run', + '--config-file', worker_configuration_file_location) user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}' result = click_runner.invoke(nucypher_cli, @@ -188,9 +271,9 @@ def test_run_blockchain_ursula(click_runner, assert result.exit_code == 0 -@pytest.mark.skip(reason="Wait for #1056") def test_collect_rewards_integration(click_runner, - configuration_file_location, + testerchain, + staker_configuration_file_location, blockchain_alice, blockchain_bob, random_policy_label, @@ -199,27 +282,30 @@ def test_collect_rewards_integration(click_runner, policy_value, policy_rate): - blockchain = staking_participant.blockchain - half_stake_time = token_economics.minimum_locked_periods // 2 # Test setup logger = staking_participant.log # Enter the Teacher's Logger, and current_period = 1 # State the initial period for incrementing staker = Staker(is_me=True, - checksum_address=staking_participant.checksum_address, - blockchain=blockchain) + checksum_address=testerchain.etherbase_account, + blockchain=testerchain) # The staker is staking. assert staker.stakes assert staker.is_staking pre_stake_token_balance = staker.token_balance + worker = Worker(is_me=True, + worker_address=testerchain.etherbase_account, + checksum_address=testerchain.etherbase_account, # TODO: Use another account + blockchain=testerchain) + # Confirm for half the first stake duration for _ in range(half_stake_time): current_period += 1 logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<") - blockchain.time_travel(periods=1) - staker.confirm_activity() + testerchain.time_travel(periods=1) + worker.confirm_activity() # Alice creates a policy and grants Bob access blockchain_alice.selection_buffer = 1 @@ -263,31 +349,32 @@ def test_collect_rewards_integration(click_runner, assert random_data == cleartexts[0] # Ursula Staying online and the clock advancing - blockchain.time_travel(periods=1) - staker.confirm_activity() + testerchain.time_travel(periods=1) + worker.confirm_activity() current_period += 1 # Finish the passage of time for the first Stake for _ in range(5): # plus the extended periods from stake division current_period += 1 logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<") - blockchain.time_travel(periods=1) - staker.confirm_activity() + testerchain.time_travel(periods=1) + worker.confirm_activity() # # WHERES THE MONEY URSULA?? - Collecting Rewards # # The address the client wants Ursula to send rewards to - burner_wallet = blockchain.w3.eth.account.create(INSECURE_DEVELOPMENT_PASSWORD) + burner_wallet = testerchain.w3.eth.account.create(INSECURE_DEVELOPMENT_PASSWORD) # The rewards wallet is initially empty, because it is freshly created - assert blockchain.client.get_balance(burner_wallet.address) == 0 + assert testerchain.client.get_balance(burner_wallet.address) == 0 # Snag a random teacher from the fleet collection_args = ('--mock-networking', - 'ursula', 'collect-reward', - '--config-file', configuration_file_location, + 'stake', 'collect-reward', + '--config-file', staker_configuration_file_location, + '--staking-address', testerchain.etherbase_account, '--withdraw-address', burner_wallet.address, '--force') @@ -298,7 +385,7 @@ def test_collect_rewards_integration(click_runner, assert result.exit_code == 0 # Policy Reward - collected_policy_reward = blockchain.client.get_balance(burner_wallet.address) + collected_policy_reward = testerchain.client.get_balance(burner_wallet.address) expected_collection = policy_rate * 30 assert collected_policy_reward == expected_collection @@ -307,10 +394,10 @@ def test_collect_rewards_integration(click_runner, for _ in range(9): current_period += 1 logger.debug(f">>>>>>>>>>> TEST PERIOD {current_period} <<<<<<<<<<<<<<<<") - blockchain.time_travel(periods=1) - staker.confirm_activity() + testerchain.time_travel(periods=1) + worker.confirm_activity() # Staking Reward calculated_reward = staker.staking_agent.calculate_staking_reward(checksum_address=staker.checksum_address) assert calculated_reward - assert staker.token_balance > pre_stake_token_balance \ No newline at end of file + assert staker.token_balance > pre_stake_token_balance