mirror of https://github.com/nucypher/nucypher.git
Test coverage and bug fixes for interactive client account selection with balances.
parent
78ed8fd145
commit
cc66d9310a
|
@ -80,6 +80,7 @@ def select_stake(stakeholder,
|
|||
|
||||
def select_client_account(emitter,
|
||||
provider_uri: str = None,
|
||||
signer: Signer = None,
|
||||
signer_uri: str = None,
|
||||
wallet: Wallet = None,
|
||||
prompt: str = None,
|
||||
|
@ -97,19 +98,31 @@ def select_client_account(emitter,
|
|||
|
||||
# We use Wallet internally as an account management abstraction
|
||||
if not wallet:
|
||||
|
||||
if signer and signer_uri:
|
||||
raise ValueError('Pass either signer or signer_uri but not both.')
|
||||
|
||||
if not provider_uri and not signer_uri:
|
||||
raise ValueError("At least a provider URI or signer URI is necessary to select an account")
|
||||
# Lazy connect the blockchain interface
|
||||
if not BlockchainInterfaceFactory.is_interface_initialized(provider_uri=provider_uri):
|
||||
BlockchainInterfaceFactory.initialize_interface(provider_uri=provider_uri, poa=poa, emitter=emitter)
|
||||
signer = Signer.from_signer_uri(signer_uri) if signer_uri else None
|
||||
|
||||
if provider_uri:
|
||||
# Lazy connect the blockchain interface
|
||||
if not BlockchainInterfaceFactory.is_interface_initialized(provider_uri=provider_uri):
|
||||
BlockchainInterfaceFactory.initialize_interface(provider_uri=provider_uri, poa=poa, emitter=emitter)
|
||||
|
||||
if signer_uri:
|
||||
signer = Signer.from_signer_uri(signer_uri) if signer_uri else None
|
||||
|
||||
wallet = Wallet(provider_uri=provider_uri, signer=signer)
|
||||
|
||||
elif provider_uri or signer_uri:
|
||||
raise ValueError("If you input a wallet, don't pass a provider URI or signer URI too")
|
||||
|
||||
# Display accounts info
|
||||
if show_nu_balance or show_staking: # Lazy registry fetching
|
||||
if not registry:
|
||||
if not network:
|
||||
raise ValueError("Pass network name or registry; Got neither.")
|
||||
registry = InMemoryContractRegistry.from_latest_publication(network=network)
|
||||
|
||||
wallet_accounts = wallet.accounts
|
||||
|
|
|
@ -19,21 +19,19 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
|||
import tabulate
|
||||
from web3.main import Web3
|
||||
|
||||
from nucypher.blockchain.eth.actors import StakeHolder
|
||||
from nucypher.blockchain.eth.constants import STAKING_ESCROW_CONTRACT_NAME
|
||||
from nucypher.blockchain.eth.token import NU
|
||||
from nucypher.blockchain.eth.utils import datetime_at_period, prettify_eth_amount
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.cli.literature import NO_ACTIVE_STAKES, NO_STAKES_AT_ALL, NO_STAKING_ACCOUNTS, POST_STAKING_ADVICE
|
||||
from nucypher.cli.literature import POST_STAKING_ADVICE
|
||||
from nucypher.cli.painting.transactions import paint_receipt_summary
|
||||
|
||||
|
||||
STAKE_TABLE_COLUMNS = ('Idx', 'Value', 'Remaining', 'Enactment', 'Termination')
|
||||
STAKER_TABLE_COLUMNS = ('Status', 'Restaking', 'Winding Down', 'Unclaimed Fees', 'Min fee rate')
|
||||
|
||||
|
||||
def paint_stakes(emitter: StdoutEmitter,
|
||||
stakeholder: StakeHolder,
|
||||
stakeholder: 'StakeHolder',
|
||||
paint_inactive: bool = False,
|
||||
staker_address: str = None) -> None:
|
||||
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from eth_utils import is_checksum_address
|
||||
from web3 import Web3
|
||||
|
||||
from nucypher.blockchain.eth.actors import Wallet
|
||||
from nucypher.blockchain.eth.token import NU
|
||||
from nucypher.cli.actions.select import select_client_account
|
||||
from nucypher.config.constants import TEMPORARY_DOMAIN
|
||||
from tests.constants import MOCK_PROVIDER_URI, MOCK_SIGNER_URI, NUMBER_OF_ETH_TEST_ACCOUNTS
|
||||
|
||||
|
||||
def test_select_client_account(mock_click_prompt, test_emitter, mock_testerchain):
|
||||
"""Fine-grained assertions about the return value of interactive client account selection"""
|
||||
selection = 0
|
||||
mock_click_prompt.return_value = selection
|
||||
selected_account = select_client_account(emitter=test_emitter, provider_uri=MOCK_PROVIDER_URI)
|
||||
assert selected_account, "Account selection returned Falsy instead of an address"
|
||||
assert isinstance(selected_account, str), "Selection is not a str"
|
||||
assert is_checksum_address(selected_account), "Selection is not a valid checksum address"
|
||||
assert selected_account == mock_testerchain.etherbase_account, "Selection returned the wrong address"
|
||||
|
||||
|
||||
def test_select_client_account_invalid_input(mock_click_prompt, test_emitter, mock_testerchain):
|
||||
|
||||
# Provider URI Problems
|
||||
error_message = "At least a provider URI or signer URI is necessary to select an account"
|
||||
with pytest.raises(ValueError, match=error_message):
|
||||
select_client_account(emitter=test_emitter)
|
||||
|
||||
# Signer Problems
|
||||
error_message = "Pass either signer or signer_uri but not both."
|
||||
with pytest.raises(ValueError, match=error_message):
|
||||
select_client_account(emitter=test_emitter, signer=Mock(), signer_uri=MOCK_SIGNER_URI)
|
||||
|
||||
|
||||
def test_select_client_account_valid_inputs(mock_click_prompt,
|
||||
test_emitter,
|
||||
mock_testerchain,
|
||||
patch_keystore,
|
||||
mock_accounts):
|
||||
selection = 0
|
||||
mock_click_prompt.return_value = selection
|
||||
|
||||
# From Provider
|
||||
selected_account = select_client_account(emitter=test_emitter, provider_uri=MOCK_PROVIDER_URI)
|
||||
assert selected_account == mock_testerchain.etherbase_account
|
||||
|
||||
# From Wallet
|
||||
wallet = Wallet(provider_uri=MOCK_PROVIDER_URI)
|
||||
selected_account = select_client_account(emitter=test_emitter, wallet=wallet)
|
||||
assert selected_account == mock_testerchain.etherbase_account
|
||||
|
||||
# From External Signer
|
||||
selected_account = select_client_account(emitter=test_emitter, signer_uri=MOCK_SIGNER_URI)
|
||||
signer_etherbase_keystore = list(mock_accounts.items())[0]
|
||||
_filename, signer_etherbase_account = signer_etherbase_keystore
|
||||
assert selected_account == signer_etherbase_account.address
|
||||
|
||||
|
||||
@pytest.mark.parametrize('selection,show_staking,show_eth,show_tokens,mock_stakes',(
|
||||
(0, True, True, True, []),
|
||||
(1, True, True, True, []),
|
||||
(5, True, True, True, []),
|
||||
(NUMBER_OF_ETH_TEST_ACCOUNTS-1, True, True, True, []),
|
||||
(4, True, True, True, [(1, 2, 3)]),
|
||||
(0, False, True, True, []),
|
||||
(0, False, False, True, []),
|
||||
(0, False, False, False, []),
|
||||
))
|
||||
def test_select_client_account_with_balance_display(mock_click_prompt,
|
||||
test_emitter,
|
||||
mock_testerchain,
|
||||
stdout_trap,
|
||||
test_registry_source_manager,
|
||||
mock_staking_agent,
|
||||
mock_token_agent,
|
||||
selection,
|
||||
show_staking,
|
||||
show_eth,
|
||||
show_tokens,
|
||||
mock_stakes):
|
||||
|
||||
mock_click_prompt.return_value = selection
|
||||
|
||||
# Missing network kwarg with balance display active
|
||||
blockchain_read_required = any((show_staking, show_eth, show_tokens))
|
||||
if blockchain_read_required:
|
||||
with pytest.raises(ValueError, match='Pass network name or registry; Got neither.'):
|
||||
select_client_account(emitter=test_emitter,
|
||||
show_eth_balance=show_eth,
|
||||
show_nu_balance=show_tokens,
|
||||
show_staking=show_staking,
|
||||
provider_uri=MOCK_PROVIDER_URI)
|
||||
|
||||
mock_staking_agent.get_all_stakes.return_value = mock_stakes
|
||||
selected_account = select_client_account(emitter=test_emitter,
|
||||
network=TEMPORARY_DOMAIN,
|
||||
show_eth_balance=show_eth,
|
||||
show_nu_balance=show_tokens,
|
||||
show_staking=show_staking,
|
||||
provider_uri=MOCK_PROVIDER_URI)
|
||||
|
||||
# check for accurate selection consistency with client index
|
||||
assert selected_account == mock_testerchain.client.accounts[selection]
|
||||
|
||||
# Display account info
|
||||
headers = ['Account']
|
||||
if show_staking:
|
||||
headers.append('Staking')
|
||||
if show_eth:
|
||||
headers.append('ETH')
|
||||
if show_tokens:
|
||||
headers.append('NU')
|
||||
|
||||
raw_output = stdout_trap.getvalue()
|
||||
for column_name in headers:
|
||||
assert column_name in raw_output, f'"{column_name}" column was not displayed'
|
||||
|
||||
output = raw_output.strip().split('\n')
|
||||
body = output[2:-1]
|
||||
assert len(body) == len(mock_testerchain.client.accounts), "Some accounts are not displayed"
|
||||
|
||||
accounts = dict()
|
||||
for row in body:
|
||||
account_display = row.split()
|
||||
account_data = dict(zip(headers, account_display[1::]))
|
||||
account = account_data['Account']
|
||||
accounts[account] = account_data
|
||||
assert is_checksum_address(account)
|
||||
|
||||
if show_tokens:
|
||||
balance = mock_token_agent.get_balance(address=account)
|
||||
assert str(NU.from_nunits(balance)) in row
|
||||
|
||||
if show_eth:
|
||||
balance = mock_testerchain.client.get_balance(account=account)
|
||||
assert str(Web3.fromWei(balance, 'ether')) in row
|
||||
|
||||
if show_staking:
|
||||
if len(mock_stakes) == 0:
|
||||
assert "No" in row
|
||||
else:
|
||||
assert 'Yes' in row
|
|
@ -23,14 +23,15 @@ from eth_account.account import Account
|
|||
from io import StringIO
|
||||
|
||||
from nucypher.blockchain.economics import EconomicsFactory
|
||||
from nucypher.blockchain.eth import KeystoreSigner
|
||||
from nucypher.blockchain.eth.agents import ContractAgency
|
||||
from nucypher.blockchain.eth.interfaces import BlockchainInterface, BlockchainInterfaceFactory
|
||||
from nucypher.blockchain.eth.registry import InMemoryContractRegistry
|
||||
from nucypher.characters.control.emitters import StdoutEmitter
|
||||
from nucypher.config.characters import UrsulaConfiguration
|
||||
from tests.constants import KEYFILE_NAME_TEMPLATE, NUMBER_OF_ETH_TEST_ACCOUNTS
|
||||
from tests.constants import KEYFILE_NAME_TEMPLATE, MOCK_KEYSTORE_PATH, NUMBER_OF_ETH_TEST_ACCOUNTS
|
||||
from tests.fixtures import _make_testerchain, make_token_economics
|
||||
from tests.mock.agents import FAKE_RECEIPT, MockContractAgency, MockStakingAgent, MockWorkLockAgent
|
||||
from tests.mock.agents import FAKE_RECEIPT, MockContractAgency, MockNucypherToken, MockStakingAgent, MockWorkLockAgent
|
||||
from tests.mock.interfaces import MockBlockchain, make_mock_registry_source_manager
|
||||
|
||||
|
||||
|
@ -50,6 +51,13 @@ def mock_contract_agency(monkeymodule, module_mocker, token_economics):
|
|||
monkeymodule.delattr(ContractAgency, 'get_agent')
|
||||
|
||||
|
||||
@pytest.fixture(scope='module', autouse=True)
|
||||
def mock_token_agent(mock_testerchain, token_economics, mock_contract_agency):
|
||||
mock_agent = mock_contract_agency.get_agent(MockNucypherToken)
|
||||
yield mock_agent
|
||||
mock_agent.reset()
|
||||
|
||||
|
||||
@pytest.fixture(scope='module', autouse=True)
|
||||
def mock_worklock_agent(mock_testerchain, token_economics, mock_contract_agency):
|
||||
mock_agent = mock_contract_agency.get_agent(MockWorkLockAgent)
|
||||
|
@ -74,10 +82,11 @@ def mock_click_confirm(mocker):
|
|||
return mocker.patch.object(click, 'confirm')
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@pytest.fixture(scope='function')
|
||||
def stdout_trap():
|
||||
trap = StringIO()
|
||||
return trap
|
||||
yield trap
|
||||
trap.truncate(0)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
@ -165,3 +174,26 @@ def worker_address(worker_account):
|
|||
def custom_config_filepath(custom_filepath):
|
||||
filepath = os.path.join(custom_filepath, UrsulaConfiguration.generate_filename())
|
||||
return filepath
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def patch_keystore(mock_accounts, monkeypatch, mocker):
|
||||
|
||||
def successful_mock_keyfile_reader(_keystore, path):
|
||||
|
||||
# Ensure the absolute path is passed to the keyfile reader
|
||||
assert MOCK_KEYSTORE_PATH in path
|
||||
full_path = path
|
||||
del path
|
||||
|
||||
for filename, account in mock_accounts.items(): # Walk the mock filesystem
|
||||
if filename in full_path:
|
||||
break
|
||||
else:
|
||||
raise FileNotFoundError(f"No such file {full_path}")
|
||||
return account.address, dict(version=3, address=account.address)
|
||||
|
||||
mocker.patch('os.listdir', return_value=list(mock_accounts.keys()))
|
||||
monkeypatch.setattr(KeystoreSigner, '_KeystoreSigner__read_keyfile', successful_mock_keyfile_reader)
|
||||
yield
|
||||
monkeypatch.delattr(KeystoreSigner, '_KeystoreSigner__read_keyfile')
|
||||
|
|
|
@ -150,6 +150,9 @@ class MockContractAgent:
|
|||
class MockNucypherToken(MockContractAgent, NucypherTokenAgent):
|
||||
"""Look at me im a token!"""
|
||||
|
||||
CALLS = ('get_balance',
|
||||
)
|
||||
|
||||
|
||||
class MockStakingAgent(MockContractAgent, StakingEscrowAgent):
|
||||
"""dont forget the eggs!"""
|
||||
|
|
Loading…
Reference in New Issue