-
Notifications
You must be signed in to change notification settings - Fork 574
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #293 from iamdefinitelyahuman/cli-accounts
Add brownie accounts to CLI
- Loading branch information
Showing
6 changed files
with
239 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
#!/usr/bin/python3 | ||
|
||
import json | ||
import shutil | ||
import sys | ||
from pathlib import Path | ||
|
||
from docopt import docopt | ||
|
||
from brownie import accounts | ||
from brownie._config import _get_data_folder | ||
from brownie.convert import to_address | ||
from brownie.utils import color, notify | ||
|
||
__doc__ = """Usage: brownie accounts <command> [<arguments> ...] [options] | ||
Commands: | ||
list List available accounts | ||
new <id> Add a new account by entering a private key | ||
generate <id> Add a new account with a random private key | ||
import <id> <path> Import a new account via a keystore file | ||
export <id> <path> Export an existing account keystore file | ||
password <id> Change the password of an existing account | ||
delete <id> Delete an existing account | ||
Options: | ||
--help -h Display this message | ||
Command-line helper for managing local accounts. You can unlock local accounts from | ||
scripts or the console using the Accounts.load method. | ||
""" | ||
|
||
|
||
def main(): | ||
args = docopt(__doc__) | ||
try: | ||
fn = getattr(sys.modules[__name__], f"_{args['<command>']}") | ||
except AttributeError: | ||
print("Invalid command. Try brownie accounts --help") | ||
return | ||
try: | ||
fn(*args["<arguments>"]) | ||
except TypeError: | ||
print(f"Invalid arguments for command '{args['<command>']}'. Try brownie ethpm --help") | ||
return | ||
|
||
|
||
def _list(): | ||
account_paths = sorted(_get_data_folder().glob("accounts/*.json")) | ||
print(f"Found {len(account_paths)} account{'s' if len(account_paths)!=1 else ''}:") | ||
for path in account_paths: | ||
u = "\u2514" if path == account_paths[-1] else "\u251c" | ||
with path.open() as fp: | ||
data = json.load(fp) | ||
print( | ||
f" {color('bright black')}{u}\u2500{color('bright blue')}{path.stem}{color}" | ||
f": {color('bright magenta')}{to_address(data['address'])}{color}" | ||
) | ||
|
||
|
||
def _new(id_): | ||
pk = input("Enter the private key you wish to add: ") | ||
a = accounts.add(pk) | ||
a.save(id_) | ||
notify( | ||
"SUCCESS", | ||
f"A new account '{color('bright magenta')}{a.address}{color}'" | ||
f" has been generated with the id '{color('bright blue')}{id_}{color}'", | ||
) | ||
|
||
|
||
def _generate(id_): | ||
print("Generating a new private key...") | ||
a = accounts.add() | ||
a.save(id_) | ||
notify( | ||
"SUCCESS", | ||
f"A new account '{color('bright magenta')}{a.address}{color}'" | ||
f" has been generated with the id '{color('bright blue')}{id_}{color}'", | ||
) | ||
|
||
|
||
def _import(id_, path): | ||
source_path = Path(path).absolute() | ||
if not source_path.suffix: | ||
source_path = source_path.with_suffix(".json") | ||
dest_path = _get_data_folder().joinpath(f"accounts/{id_}.json") | ||
if dest_path.exists(): | ||
raise FileExistsError(f"A keystore file already exists with the id '{id_}'") | ||
accounts.load(source_path) | ||
shutil.copy(source_path, dest_path) | ||
notify( | ||
"SUCCESS", | ||
f"Keystore '{color('bright magenta')}{source_path}{color}'" | ||
f" has been imported with the id '{color('bright blue')}{id_}{color}'", | ||
) | ||
|
||
|
||
def _export(id_, path): | ||
source_path = _get_data_folder().joinpath(f"accounts/{id_}.json") | ||
if not source_path.exists(): | ||
raise FileNotFoundError(f"No keystore exists with the id '{id_}'") | ||
dest_path = Path(path).absolute() | ||
if not dest_path.suffix: | ||
dest_path = dest_path.with_suffix(".json") | ||
if dest_path.exists(): | ||
raise FileExistsError(f"Export path {dest_path} already exists") | ||
shutil.copy(source_path, dest_path) | ||
notify( | ||
"SUCCESS", | ||
f"Account with id '{color('bright blue')}{id_}{color}' has been" | ||
f" exported to keystore '{color('bright magenta')}{dest_path}{color}'", | ||
) | ||
|
||
|
||
def _password(id_): | ||
a = accounts.load(id_) | ||
a.save(id_, overwrite=True) | ||
notify("SUCCESS", f"Password has been changed for account '{color('bright blue')}{id_}{color}'") | ||
|
||
|
||
def _delete(id_): | ||
path = _get_data_folder().joinpath(f"accounts/{id_}.json") | ||
path.unlink() | ||
notify("SUCCESS", f"Account '{color('bright blue')}{id_}{color}' has been deleted") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
#!/usr/bin/python3 | ||
|
||
import json | ||
|
||
import pytest | ||
|
||
from brownie._cli import accounts as cli_accounts | ||
from brownie._config import _get_data_folder | ||
|
||
|
||
@pytest.fixture(autouse=True) | ||
def no_pass(monkeypatch): | ||
monkeypatch.setattr("brownie.network.account.getpass", lambda x: "") | ||
|
||
|
||
def test_new_account(capfd, monkeypatch): | ||
assert not _get_data_folder().joinpath("accounts/new.json").exists() | ||
cli_accounts._list() | ||
assert "0x3b9b63C67838C9fef6c50e3a06f574Be12b3D50f" not in capfd.readouterr()[0] | ||
monkeypatch.setattr( | ||
"builtins.input", | ||
lambda x: "0x60285766d7296d0744450696603d9593513958fdbb494cc6d234a5a797a2108f", | ||
) | ||
cli_accounts._new("new") | ||
capfd.readouterr() | ||
cli_accounts._list() | ||
assert "0x3b9b63C67838C9fef6c50e3a06f574Be12b3D50f" in capfd.readouterr()[0] | ||
assert _get_data_folder().joinpath("accounts/new.json").exists() | ||
|
||
|
||
def test_generate(capfd): | ||
path = _get_data_folder().joinpath("accounts/generate.json") | ||
assert not path.exists() | ||
cli_accounts._generate("generate") | ||
assert path.exists() | ||
with path.open() as fp: | ||
data = json.load(fp) | ||
capfd.readouterr() | ||
cli_accounts._list() | ||
assert data["address"] in capfd.readouterr()[0].lower() | ||
|
||
|
||
def test_import(): | ||
path = _get_data_folder().joinpath("accounts/import-test.json") | ||
cli_accounts._generate("import-test") | ||
new_path = _get_data_folder().joinpath("x.json") | ||
path.rename(new_path) | ||
cli_accounts._import("import-new", str(new_path.absolute())) | ||
assert _get_data_folder().joinpath("accounts/import-new.json").exists() | ||
|
||
|
||
def test_import_already_exists(): | ||
cli_accounts._generate("import-exists") | ||
with pytest.raises(FileExistsError): | ||
cli_accounts._import("import-exists", "") | ||
|
||
|
||
def test_export(): | ||
cli_accounts._generate("export-test") | ||
target_path = _get_data_folder().joinpath("exported.json") | ||
assert not target_path.exists() | ||
cli_accounts._export("export-test", str(target_path.absolute())) | ||
assert target_path.exists() | ||
|
||
|
||
def test_export_not_exists(): | ||
with pytest.raises(FileNotFoundError): | ||
cli_accounts._export("unknown", "") | ||
|
||
|
||
def test_export_overwrite(): | ||
cli_accounts._generate("export-exists") | ||
path = str(_get_data_folder().joinpath("accounts/export-exists.json").absolute()) | ||
with pytest.raises(FileExistsError): | ||
cli_accounts._export("export-exists", path) | ||
|
||
|
||
def test_password(monkeypatch, accounts): | ||
cli_accounts._generate("pw-test") | ||
passwords = ["xxx", "xxx", ""] | ||
monkeypatch.setattr("brownie.network.account.getpass", lambda x: passwords.pop()) | ||
cli_accounts._password("pw-test") | ||
accounts.load("pw-test") | ||
|
||
|
||
def test_delete(): | ||
cli_accounts._generate("del-test") | ||
path = _get_data_folder().joinpath("accounts/del-test.json") | ||
assert path.exists() | ||
cli_accounts._delete("del-test") | ||
assert not path.exists() |