From 0168ba6012d35faf0ed5e4ede4660e28ecd83217 Mon Sep 17 00:00:00 2001 From: Aiman Baharna Date: Fri, 25 Jul 2025 13:36:00 +0300 Subject: [PATCH 1/5] Always show keyfile names in `account list` Drop the `--with-files` option because it's unnecessary. --- autonity_cli/commands/account.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/autonity_cli/commands/account.py b/autonity_cli/commands/account.py index 78d52eb..5a2c35a 100644 --- a/autonity_cli/commands/account.py +++ b/autonity_cli/commands/account.py @@ -54,9 +54,8 @@ def account_group() -> None: @account_group.command(name="list") -@option("--with-files", is_flag=True, help="also show keyfile names.") @keystore_option() -def list_cmd(keystore: Optional[str], with_files: bool) -> None: +def list_cmd(keystore: Optional[str]) -> None: """ List the accounts for files in the keystore directory. """ @@ -64,10 +63,7 @@ def list_cmd(keystore: Optional[str], with_files: bool) -> None: keystore = config.get_keystore_directory(keystore) keyfiles = address_keyfile_dict(keystore) for addr, keyfile in keyfiles.items(): - if with_files: - print(addr + " " + keyfile) - else: - print(addr) + print(addr + " " + keyfile) @account_group.command() From 2b512f5f6c62aaddbecafc294aa34fb94c924b87 Mon Sep 17 00:00:00 2001 From: Aiman Baharna Date: Fri, 25 Jul 2025 14:41:41 +0300 Subject: [PATCH 2/5] Refactor keystore_option for custom decorator class --- autonity_cli/options.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/autonity_cli/options.py b/autonity_cli/options.py index 14152e4..a0ebf10 100644 --- a/autonity_cli/options.py +++ b/autonity_cli/options.py @@ -50,6 +50,19 @@ def make_option( help="encrypted private key file (falls back to 'keyfile' in config file).", ) +keystore_option_info = OptionInfo( + args=( + "--keystore", + "-s", + ), + type=Path(exists=True), + help=( + "keystore directory (falls back to 'keystore' in config file, " + "defaults to ~/.autonity/keystore)." + ), +) + + trezor_option_info = OptionInfo( args=["--trezor"], metavar="ACCOUNT", @@ -72,21 +85,13 @@ def make_option( ) -def keystore_option() -> Decorator[Func]: +def keystore_option(cls: Decorator[Any] = click.option) -> Decorator[Func]: """ Option: --keystore . """ def decorator(fn: Func) -> Func: - return click.option( - "--keystore", - "-s", - type=Path(exists=True), - help=( - "keystore directory (falls back to 'keystore' in config file, " - "defaults to ~/.autonity/keystore)." - ), - )(fn) + return make_option(keystore_option_info, cls=cls)(fn) return decorator From 8908bc9f596da33d64006ba9b726f83dda58beea Mon Sep 17 00:00:00 2001 From: Aiman Baharna Date: Fri, 25 Jul 2025 14:52:53 +0300 Subject: [PATCH 3/5] Extract Trezor common code into new module Extract common hardware wallet code into a new autonity_cli.device module, which may not be authenticator-specific. Currently getting a Trezor device object is extracted. --- autonity_cli/auth.py | 17 +++-------------- autonity_cli/device.py | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 14 deletions(-) create mode 100644 autonity_cli/device.py diff --git a/autonity_cli/auth.py b/autonity_cli/auth.py index fddb94f..08263ef 100644 --- a/autonity_cli/auth.py +++ b/autonity_cli/auth.py @@ -14,19 +14,15 @@ from eth_utils.conversions import to_int from eth_utils.crypto import keccak from hexbytes import HexBytes -from trezorlib.client import get_default_client from trezorlib.exceptions import Cancelled from trezorlib.messages import Features from trezorlib.tools import parse_path -from trezorlib.transport import DeviceIsBusy from web3.types import TxParams -from . import config +from . import config, device from .logging import log from .utils import to_checksum_address -TREZOR_DEFAULT_PREFIX = "m/44h/60h/0h/0" - class Authenticator(Protocol): address: ChecksumAddress @@ -67,7 +63,7 @@ def sign_message(self, message: str) -> bytes: class TrezorAuthenticator: def __init__(self, path_or_index: str): if path_or_index.isdigit(): - path_str = f"{TREZOR_DEFAULT_PREFIX}/{int(path_or_index)}" + path_str = f"{device.TREZOR_DEFAULT_PREFIX}/{int(path_or_index)}" else: path_str = path_or_index try: @@ -76,14 +72,7 @@ def __init__(self, path_or_index: str): raise click.ClickException( f"Invalid Trezor BIP32 derivation path '{path_str}'." ) from exc - try: - self.client = get_default_client() - except DeviceIsBusy as exc: - raise click.ClickException("Device in use by another process.") from exc - except Exception as exc: - raise click.ClickException( - "No Trezor device found. Check device is connected, unlocked, and detected by OS." - ) from exc + self.client = device.get_client() device_info = self.device_info(self.client.features) log(f"Connected to Trezor: {device_info}") diff --git a/autonity_cli/device.py b/autonity_cli/device.py new file mode 100644 index 0000000..cad5027 --- /dev/null +++ b/autonity_cli/device.py @@ -0,0 +1,21 @@ +"""Hardware wallet common functions. + +Currently only Trezor devices are supported.""" + +import click +from trezorlib.client import TrezorClient, get_default_client +from trezorlib.transport import DeviceIsBusy + + +TREZOR_DEFAULT_PREFIX = "m/44h/60h/0h/0" + + +def get_client() -> TrezorClient: + try: + return get_default_client() + except DeviceIsBusy as exc: + raise click.ClickException("Device in use by another process.") from exc + except Exception as exc: + raise click.ClickException( + "No Trezor device found. Check device is connected, unlocked, and detected by OS." + ) from exc From 648de14dea0e171d37d24e67a1792b7c8ad1e624 Mon Sep 17 00:00:00 2001 From: Aiman Baharna Date: Fri, 25 Jul 2025 14:57:52 +0300 Subject: [PATCH 4/5] Add comment clarifying what exception is handling --- autonity_cli/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autonity_cli/auth.py b/autonity_cli/auth.py index 08263ef..038d18f 100644 --- a/autonity_cli/auth.py +++ b/autonity_cli/auth.py @@ -78,7 +78,7 @@ def __init__(self, path_or_index: str): try: address_str = trezor_eth.get_address(self.client, self.path) - except Cancelled as exc: + except Cancelled as exc: # user cancelled optional passphrase prompt raise click.Abort() from exc self.address = to_checksum_address(address_str) From bc8a9be3c1b4d4305ed2f2c9d8d50ce123173d86 Mon Sep 17 00:00:00 2001 From: Aiman Baharna Date: Fri, 25 Jul 2025 14:55:14 +0300 Subject: [PATCH 5/5] Extend `account list` to enumerate Trezor accounts --- autonity_cli/commands/account.py | 57 +++++++++++++++++++++++++++----- autonity_cli/device.py | 26 +++++++++++++++ 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/autonity_cli/commands/account.py b/autonity_cli/commands/account.py index 5a2c35a..1f4fed0 100644 --- a/autonity_cli/commands/account.py +++ b/autonity_cli/commands/account.py @@ -2,6 +2,7 @@ import json from typing import List, Optional +import click import eth_account from autonity import Autonity from click import ClickException, Path, argument, group, option @@ -11,8 +12,11 @@ from web3 import Web3 from web3.types import BlockIdentifier -from .. import config -from ..auth import validate_authenticator, validate_authenticator_account +from .. import config, device +from ..auth import ( + validate_authenticator, + validate_authenticator_account, +) from ..denominations import ( format_auton_quantity, format_newton_quantity, @@ -31,6 +35,7 @@ keyfile_option, keystore_option, newton_or_token_option, + optgroup, rpc_endpoint_option, ) from ..user import get_account_stats @@ -54,16 +59,50 @@ def account_group() -> None: @account_group.command(name="list") -@keystore_option() -def list_cmd(keystore: Optional[str]) -> None: +@optgroup.group("Keyfile accounts") +@keystore_option(cls=optgroup.option) +@optgroup.group("Trezor accounts") +@optgroup.option("--trezor", is_flag=True, help="Enumerate Trezor accounts") +@optgroup.option( + "--prefix", + metavar="PREFIX", + default=device.TREZOR_DEFAULT_PREFIX, + show_default=True, + help="Custom BIP32 derivation prefix", +) +@optgroup.option( + "--start", + type=int, + default=0, + show_default=True, + help="Start index at BIP32 derivation prefix", +) +@optgroup.option( + "-n", + type=int, + default=20, + show_default=True, + help="Number of Trezor accounts to list", +) +def list_cmd( + keystore: Optional[str], trezor: bool, prefix: str, start: int, n: int +) -> None: """ - List the accounts for files in the keystore directory. + List accounts in keyfiles or in a Trezor device. """ - keystore = config.get_keystore_directory(keystore) - keyfiles = address_keyfile_dict(keystore) - for addr, keyfile in keyfiles.items(): - print(addr + " " + keyfile) + if trezor and keystore: + raise click.ClickException( + "Options --trezor and --keystore are mutually exclusive." + ) + + if trezor: + accounts = device.enumerate_accounts(prefix, start, n) + else: + keystore = config.get_keystore_directory(keystore) + accounts = address_keyfile_dict(keystore).items() + for addr, path in accounts: + print(addr + " " + path) @account_group.command() diff --git a/autonity_cli/device.py b/autonity_cli/device.py index cad5027..82c8321 100644 --- a/autonity_cli/device.py +++ b/autonity_cli/device.py @@ -3,9 +3,14 @@ Currently only Trezor devices are supported.""" import click +import trezorlib.ethereum as trezor_eth +from eth_typing import ChecksumAddress from trezorlib.client import TrezorClient, get_default_client +from trezorlib.exceptions import Cancelled +from trezorlib.tools import parse_path from trezorlib.transport import DeviceIsBusy +from .utils import to_checksum_address TREZOR_DEFAULT_PREFIX = "m/44h/60h/0h/0" @@ -19,3 +24,24 @@ def get_client() -> TrezorClient: raise click.ClickException( "No Trezor device found. Check device is connected, unlocked, and detected by OS." ) from exc + + +def enumerate_accounts( + prefix: str, start: int, n: int +) -> list[tuple[ChecksumAddress, str]]: + accounts: list[tuple[ChecksumAddress, str]] = [] + client = get_client() + try: + for index in range(start, start + n): + path_str = prefix + f"/{index}" + try: + path = parse_path(path_str) + except ValueError as exc: + raise click.ClickException( + f"Invalid Trezor BIP32 derivation path '{path_str}'." + ) from exc + address_str = trezor_eth.get_address(client, path) + accounts.append((to_checksum_address(address_str), path_str)) + except Cancelled as exc: # user cancelled optional passphrase prompt + raise click.Abort() from exc + return accounts