From 951cf2403aa58a9b58c3c1a793b51cd5c58cb56e Mon Sep 17 00:00:00 2001 From: Andrew Fleming Date: Sat, 27 Aug 2022 02:54:19 -0400 Subject: [PATCH] Add get-accounts command (#119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add get-accounts * add get-account tests * add new test key * update readme * remove comment * fix docstring * Update README.md Co-authored-by: Martín Triay * add nre msg for get-accounts * fix logging * update test logs * test nre get-accounts msgs * add link to docs in print msg * add link to docs, move internal func * update test logs * formatting * change log to print * update msgs in get-accounts tests Co-authored-by: Martín Triay --- README.md | 42 ++++++++++ src/nile/accounts.py | 4 +- src/nile/cli.py | 8 ++ src/nile/core/account.py | 5 +- src/nile/nre.py | 5 ++ src/nile/utils/get_accounts.py | 42 ++++++++++ tests/commands/test_account.py | 2 +- tests/commands/test_get_accounts.py | 126 ++++++++++++++++++++++++++++ tests/test_nre.py | 1 - tox.ini | 1 + 10 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 src/nile/utils/get_accounts.py create mode 100644 tests/commands/test_get_accounts.py diff --git a/README.md b/README.md index 222b7e66..22077fcf 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,48 @@ CONTRACT_ADDRESS2:PATH_TO_COMPILED_CONTRACT2.json ... ``` +### `get-accounts` + +Retrieves a list of ready-to-use accounts which allows for easy scripting integration. Before using `get-accounts`: + +1. store private keys in a `.env` + + ``` + PRIVATE_KEY_ALIAS_1=286426666527820764590699050992975838532 + PRIVATE_KEY_ALIAS_2=263637040172279991633704324379452721903 + PRIVATE_KEY_ALIAS_3=325047780196174231475632140485641889884 + ``` + +2. deploy accounts with the keys therefrom like this: + + ```bash + nile setup PRIVATE_KEY_ALIAS_1 + ... + nile setup PRIVATE_KEY_ALIAS_2 + ... + nile setup PRIVATE_KEY_ALIAS_3 + ... + ``` + +Next, write a script and call `get-accounts` to retrieve and use the deployed accounts. + +```python +def run(nre): + + # fetch the list of deployed accounts + accounts = nre.get_accounts() + + # then + accounts[0].send(...) + + # or + alice, bob, *_ = accounts + alice.send(...) + bob.send(...) +``` + +> Please note that the list of accounts include only those that exist in the local `.accounts.json` file. + ## Extending Nile with plugins Nile has the possibility of extending its CLI and `NileRuntimeEnvironment` functionalities through plugins. For developing plugins for Nile fork [this plugin example](https://github.com/franalgaba/nile-plugin-example) boilerplate and implement your desired functionality with the provided instructions. diff --git a/src/nile/accounts.py b/src/nile/accounts.py index e5316492..76994a6e 100644 --- a/src/nile/accounts.py +++ b/src/nile/accounts.py @@ -5,7 +5,7 @@ from nile.common import ACCOUNTS_FILENAME -def register(pubkey, address, index, network): +def register(pubkey, address, index, alias, network): """Register a new account.""" file = f"{network}.{ACCOUNTS_FILENAME}" @@ -14,7 +14,7 @@ def register(pubkey, address, index, network): with open(file, "r") as fp: accounts = json.load(fp) - accounts[pubkey] = {"address": address, "index": index} + accounts[pubkey] = {"address": address, "index": index, "alias": alias} with open(file, "w") as file: json.dump(accounts, file) diff --git a/src/nile/cli.py b/src/nile/cli.py index f9046e81..0853b3fb 100755 --- a/src/nile/cli.py +++ b/src/nile/cli.py @@ -18,6 +18,7 @@ from nile.core.test import test as test_command from nile.core.version import version as version_command from nile.utils.debug import debug as debug_command +from nile.utils.get_accounts import get_accounts as get_accounts_command logging.basicConfig(level=logging.DEBUG, format="%(message)s") @@ -231,6 +232,13 @@ def debug(tx_hash, network, contracts_file): debug_command(tx_hash, network, contracts_file) +@cli.command() +@network_option +def get_accounts(network): + """Retrieve and manage deployed accounts.""" + return get_accounts_command(network) + + cli = load_plugins(cli) diff --git a/src/nile/core/account.py b/src/nile/core/account.py index 5b4794f8..89fcd49f 100644 --- a/src/nile/core/account.py +++ b/src/nile/core/account.py @@ -23,6 +23,7 @@ def __init__(self, signer, network): """Get or deploy an Account contract for the given private key.""" try: self.signer = Signer(int(os.environ[signer])) + self.alias = signer self.network = network except KeyError: logging.error( @@ -55,7 +56,9 @@ def deploy(self): overriding_path, ) - accounts.register(self.signer.public_key, address, index, self.network) + accounts.register( + self.signer.public_key, address, index, self.alias, self.network + ) return address, index diff --git a/src/nile/nre.py b/src/nile/nre.py index 1029fcb6..e9fc638b 100644 --- a/src/nile/nre.py +++ b/src/nile/nre.py @@ -6,6 +6,7 @@ from nile.core.declare import declare from nile.core.deploy import deploy from nile.core.plugins import get_installed_plugins, skip_click_exit +from nile.utils.get_accounts import get_accounts class NileRuntimeEnvironment: @@ -50,3 +51,7 @@ def get_declaration(self, identifier): def get_or_deploy_account(self, signer): """Get or deploy an Account contract.""" return Account(signer, self.network) + + def get_accounts(self): + """Retrieve and manage deployed accounts.""" + return get_accounts(self.network) diff --git a/src/nile/utils/get_accounts.py b/src/nile/utils/get_accounts.py new file mode 100644 index 00000000..adb4c2b5 --- /dev/null +++ b/src/nile/utils/get_accounts.py @@ -0,0 +1,42 @@ +"""Retrieve and manage deployed accounts.""" +import json +import logging + +from nile.accounts import current_index +from nile.core.account import Account + + +def get_accounts(network): + """Retrieve deployed accounts.""" + try: + total_accounts = current_index(network) + logging.info(f"\nTotal registered accounts: {total_accounts}\n") + except FileNotFoundError: + print(f"\n❌ No registered accounts detected in {network}.accounts.json") + print("For more info, see https://github.com/OpenZeppelin/nile#get-accounts\n") + return + + with open(f"{network}.accounts.json", "r") as f: + account_data = json.load(f) + + accounts = [] + pubkeys = list(account_data.keys()) + addresses = [i["address"] for i in account_data.values()] + signers = [i["alias"] for i in account_data.values()] + + for i in range(total_accounts): + logging.info(f"{i}: {addresses[i]}") + + _account = _check_and_return_account(signers[i], pubkeys[i], network) + accounts.append(_account) + + logging.info("\n🚀 Successfully retrieved deployed accounts") + return accounts + + +def _check_and_return_account(signer, pubkey, network): + account = Account(signer, network) + assert str(pubkey) == str( + account.signer.public_key + ), "Signer pubkey does not match deployed pubkey" + return account diff --git a/tests/commands/test_account.py b/tests/commands/test_account.py index 7cf058b2..a7f41606 100644 --- a/tests/commands/test_account.py +++ b/tests/commands/test_account.py @@ -75,7 +75,7 @@ def test_deploy_accounts_register(mock_register, mock_deploy): account = Account(KEY, NETWORK) mock_register.assert_called_once_with( - account.signer.public_key, MOCK_ADDRESS, MOCK_INDEX, NETWORK + account.signer.public_key, MOCK_ADDRESS, MOCK_INDEX, KEY, NETWORK ) diff --git a/tests/commands/test_get_accounts.py b/tests/commands/test_get_accounts.py new file mode 100644 index 00000000..0205f844 --- /dev/null +++ b/tests/commands/test_get_accounts.py @@ -0,0 +1,126 @@ +"""Tests for get-accounts command.""" +import logging +from unittest.mock import MagicMock, patch + +import pytest + +from nile.core.account import Account +from nile.utils.get_accounts import _check_and_return_account, get_accounts + +NETWORK = "goerli" +PUBKEYS = [ + "883045738439352841478194533192765345509759306772397516907181243450667673002", + "661519931401775515888740911132355225260405929679788917190706536765421826262", +] +ADDRESSES = ["333", "444"] +INDEXES = [0, 1] +ALIASES = ["TEST_KEY", "TEST_KEY_2"] + +MOCK_ACCOUNTS = { + PUBKEYS[0]: { + "address": ADDRESSES[0], + "index": INDEXES[0], + "alias": ALIASES[0], + }, + PUBKEYS[1]: { + "address": ADDRESSES[1], + "index": INDEXES[1], + "alias": ALIASES[1], + }, +} + + +@pytest.fixture(autouse=True) +def tmp_working_dir(monkeypatch, tmp_path): + monkeypatch.chdir(tmp_path) + return tmp_path + + +@pytest.fixture(autouse=True) +def mock_subprocess(): + with patch("nile.core.compile.subprocess") as mock_subprocess: + yield mock_subprocess + + +@pytest.mark.parametrize( + "private_keys, public_keys", + [ + ([ALIASES[0], PUBKEYS[0]]), + ([ALIASES[1], PUBKEYS[1]]), + ], +) +def test__check_and_return_account_with_matching_keys(private_keys, public_keys): + # Check matching public/private keys + account = _check_and_return_account(private_keys, public_keys, NETWORK) + + assert type(account) is Account + + +@pytest.mark.parametrize( + "private_keys, public_keys", + [ + ([ALIASES[0], PUBKEYS[1]]), + ([ALIASES[1], PUBKEYS[0]]), + ], +) +def test__check_and_return_account_with_mismatching_keys(private_keys, public_keys): + # Check mismatched public/private keys + with pytest.raises(AssertionError) as err: + _check_and_return_account(private_keys, public_keys, NETWORK) + + assert "Signer pubkey does not match deployed pubkey" in str(err.value) + + +def test_get_accounts_no_activated_accounts_feedback(capsys): + get_accounts(NETWORK) + # This test uses capsys in order to test the print statements (instead of logging) + captured = capsys.readouterr() + + assert ( + f"❌ No registered accounts detected in {NETWORK}.accounts.json" in captured.out + ) + assert ( + "For more info, see https://github.com/OpenZeppelin/nile#get-accounts" + in captured.out + ) + + +@patch("nile.utils.get_accounts.current_index", MagicMock(return_value=len(PUBKEYS))) +@patch("nile.utils.get_accounts.open", MagicMock()) +@patch("nile.utils.get_accounts.json.load", MagicMock(return_value=MOCK_ACCOUNTS)) +def test_get_accounts_activated_accounts_feedback(caplog): + logging.getLogger().setLevel(logging.INFO) + + # Default argument + get_accounts(NETWORK) + + # Check total accounts log + assert f"\nTotal registered accounts: {len(PUBKEYS)}\n" in caplog.text + + # Check index/address log + for i in range(len(PUBKEYS)): + assert f"{INDEXES[i]}: {ADDRESSES[i]}" in caplog.text + + # Check final success log + assert "\n🚀 Successfully retrieved deployed accounts" in caplog.text + + +@patch("nile.utils.get_accounts.current_index", MagicMock(return_value=len(PUBKEYS))) +@patch("nile.utils.get_accounts.open", MagicMock()) +@patch("nile.utils.get_accounts.json.load", MagicMock(return_value=MOCK_ACCOUNTS)) +def test_get_accounts_with_keys(): + + with patch( + "nile.utils.get_accounts._check_and_return_account" + ) as mock_return_account: + result = get_accounts(NETWORK) + + # Check correct args are passed to `_check_and_receive_account` + for i in range(len(PUBKEYS)): + mock_return_account.assert_any_call(ALIASES[i], PUBKEYS[i], NETWORK) + + # Assert call count equals correct number of accounts + assert mock_return_account.call_count == len(PUBKEYS) + + # assert returned accounts array equals correct number of accounts + assert len(result) == len(PUBKEYS) diff --git a/tests/test_nre.py b/tests/test_nre.py index e9f21bdf..5f010f87 100644 --- a/tests/test_nre.py +++ b/tests/test_nre.py @@ -3,7 +3,6 @@ Only unit tests for now. """ - from unittest.mock import patch import click diff --git a/tox.ini b/tox.ini index 6e10f73e..b6f4f7d3 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ description = Invoke pytest to run automated tests setenv = TOXINIDIR = {toxinidir} TEST_KEY = 1234 + TEST_KEY_2 = 4321 passenv = HOME extras =