From cbb066090dc14e5feb640104bc9b44ca8776406a Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 10 Sep 2025 14:20:11 +0200 Subject: [PATCH 01/19] Problem: Ledger wallet users cannot use Aleph to send transactions. Solution: Implement Ledger use on CLI to allow using them. Do it importing a specific branch of the SDK. --- pyproject.toml | 5 +- src/aleph_client/commands/account.py | 47 +++++++++++++++---- .../commands/instance/__init__.py | 1 + 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 37282041..594c8194 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,8 @@ dependencies = [ "aiodns==3.2", "aiohttp==3.11.13", "aleph-message>=1.0.5", - "aleph-sdk-python>=2.1", + #"aleph-sdk-python>=2.1", + "aleph-sdk-python @ git+https://github.com/aleph-im/aleph-sdk-python@andres-feature-implement_ledger_wallet", "base58==2.1.1", # Needed now as default with _load_account changement "click<8.2", "py-sr25519-bindings==0.2", # Needed for DOT signatures @@ -44,6 +45,8 @@ dependencies = [ "substrate-interface==1.7.11", # Needed for DOT signatures "textual==0.73", "typer==0.15.2", + "ledgerblue>=0.1.48", + "ledgereth>=0.10.0", ] optional-dependencies.cosmos = [ "cosmospy==6" ] optional-dependencies.docs = [ "sphinxcontrib-plantuml==0.30" ] diff --git a/src/aleph_client/commands/account.py b/src/aleph_client/commands/account.py index 18157ff4..df48980f 100644 --- a/src/aleph_client/commands/account.py +++ b/src/aleph_client/commands/account.py @@ -15,6 +15,7 @@ from aleph.sdk.chains.solana import parse_private_key as parse_solana_private_key from aleph.sdk.conf import ( MainConfiguration, + AccountType, load_main_configuration, save_main_configuration, settings, @@ -25,6 +26,7 @@ get_compatible_chains, ) from aleph.sdk.utils import bytes_from_hex, displayable_amount +from aleph.sdk.wallets.ledger import LedgerETHAccount from aleph_message.models import Chain from rich import box from rich.console import Console @@ -145,10 +147,10 @@ async def create( @app.command(name="address") def display_active_address( - private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = settings.PRIVATE_KEY_STRING, + private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = None, private_key_file: Annotated[ Optional[Path], typer.Option(help=help_strings.PRIVATE_KEY_FILE) - ] = settings.PRIVATE_KEY_FILE, + ] = None, ): """ Display your public address(es). @@ -476,11 +478,16 @@ async def vouchers( async def configure( private_key_file: Annotated[Optional[Path], typer.Option(help="New path to the private key file")] = None, chain: Annotated[Optional[Chain], typer.Option(help="New active chain")] = None, + address: Annotated[Optional[str], typer.Option(help="New active address")] = None, + account_type: Annotated[Optional[AccountType], typer.Option(help="Account type")] = None, ): """Configure current private key file and active chain (default selection)""" unlinked_keys, config = await list_unlinked_keys() + if not account_type: + account_type = AccountType.INTERNAL + # Fixes private key file path if private_key_file: if not private_key_file.name.endswith(".key"): @@ -494,7 +501,7 @@ async def configure( raise typer.Exit() # Configures active private key file - if not private_key_file and config and hasattr(config, "path") and Path(config.path).exists(): + if not account_type and not private_key_file and config and hasattr(config, "path") and Path(config.path).exists(): if not yes_no_input( f"Active private key file: [bright_cyan]{config.path}[/bright_cyan]\n[yellow]Keep current active private " "key?[/yellow]", @@ -521,8 +528,32 @@ async def configure( private_key_file = Path(config.path) if not private_key_file: - typer.secho("No private key file provided or found.", fg=typer.colors.RED) - raise typer.Exit() + if yes_no_input( + f"[bright_cyan]No private key file found.[/bright_cyan] [yellow]" + f"Do you want to import from Ledger?[/yellow]", + default="y", + ): + accounts = LedgerETHAccount.get_accounts() + account_addresses = [acc.address for acc in accounts] + + console.print("[bold cyan]Available addresses on Ledger:[/bold cyan]") + for idx, account_address in enumerate(account_addresses, start=1): + console.print(f"[{idx}] {account_address}") + + key_choice = Prompt.ask("Choose a address by index") + if key_choice.isdigit(): + key_index = int(key_choice) - 1 + selected_address = account_addresses[key_index] + + if not selected_address: + typer.secho("No valid address selected.", fg=typer.colors.RED) + raise typer.Exit() + + address = selected_address + account_type = AccountType.EXTERNAL + else: + typer.secho("No private key file provided or found.", fg=typer.colors.RED) + raise typer.Exit() # Configure active chain if not chain and config and hasattr(config, "chain"): @@ -545,11 +576,11 @@ async def configure( raise typer.Exit() try: - config = MainConfiguration(path=private_key_file, chain=chain) + config = MainConfiguration(path=private_key_file, chain=chain, address=address, type=account_type) save_main_configuration(settings.CONFIG_FILE, config) console.print( - f"New Default Configuration: [italic bright_cyan]{config.path}[/italic bright_cyan] with [italic " - f"bright_cyan]{config.chain}[/italic bright_cyan]", + f"New Default Configuration: [italic bright_cyan]{config.path or config.address}" + f"[/italic bright_cyan] with [italic bright_cyan]{config.chain}[/italic bright_cyan]", style=typer.colors.GREEN, ) except ValueError as e: diff --git a/src/aleph_client/commands/instance/__init__.py b/src/aleph_client/commands/instance/__init__.py index bed8b2d5..22d064ff 100644 --- a/src/aleph_client/commands/instance/__init__.py +++ b/src/aleph_client/commands/instance/__init__.py @@ -168,6 +168,7 @@ async def create( # Populates account / address account = _load_account(private_key, private_key_file, chain=payment_chain) + address = address or settings.ADDRESS_TO_USE or account.get_address() # Start the fetch in the background (async_lru_cache already returns a future) From 4e8c9b670b597d8656e1720ec0af8b615b0889a5 Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 10 Sep 2025 14:37:25 +0200 Subject: [PATCH 02/19] Fix: Solve code quality issues. --- pyproject.toml | 12 ++++++------ src/aleph_client/commands/account.py | 14 ++++++-------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 594c8194..dbf6e74d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,20 +33,20 @@ dependencies = [ "aleph-message>=1.0.5", #"aleph-sdk-python>=2.1", "aleph-sdk-python @ git+https://github.com/aleph-im/aleph-sdk-python@andres-feature-implement_ledger_wallet", - "base58==2.1.1", # Needed now as default with _load_account changement + "base58==2.1.1", # Needed now as default with _load_account changement "click<8.2", - "py-sr25519-bindings==0.2", # Needed for DOT signatures + "ledgerblue>=0.1.48", + "ledgereth>=0.10", + "py-sr25519-bindings==0.2", # Needed for DOT signatures "pydantic>=2", "pygments==2.19.1", - "pynacl==1.5", # Needed now as default with _load_account changement + "pynacl==1.5", # Needed now as default with _load_account changement "python-magic==0.4.27", "rich==13.9.*", "setuptools>=65.5", - "substrate-interface==1.7.11", # Needed for DOT signatures + "substrate-interface==1.7.11", # Needed for DOT signatures "textual==0.73", "typer==0.15.2", - "ledgerblue>=0.1.48", - "ledgereth>=0.10.0", ] optional-dependencies.cosmos = [ "cosmospy==6" ] optional-dependencies.docs = [ "sphinxcontrib-plantuml==0.30" ] diff --git a/src/aleph_client/commands/account.py b/src/aleph_client/commands/account.py index df48980f..8b9dc45e 100644 --- a/src/aleph_client/commands/account.py +++ b/src/aleph_client/commands/account.py @@ -14,8 +14,8 @@ from aleph.sdk.chains.common import generate_key from aleph.sdk.chains.solana import parse_private_key as parse_solana_private_key from aleph.sdk.conf import ( - MainConfiguration, AccountType, + MainConfiguration, load_main_configuration, save_main_configuration, settings, @@ -148,9 +148,7 @@ async def create( @app.command(name="address") def display_active_address( private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = None, - private_key_file: Annotated[ - Optional[Path], typer.Option(help=help_strings.PRIVATE_KEY_FILE) - ] = None, + private_key_file: Annotated[Optional[Path], typer.Option(help=help_strings.PRIVATE_KEY_FILE)] = None, ): """ Display your public address(es). @@ -383,7 +381,7 @@ async def list_accounts(): table.add_column("Active", no_wrap=True) active_chain = None - if config: + if config and config.path: active_chain = config.chain table.add_row(config.path.stem, str(config.path), "[bold green]*[/bold green]") else: @@ -529,9 +527,9 @@ async def configure( if not private_key_file: if yes_no_input( - f"[bright_cyan]No private key file found.[/bright_cyan] [yellow]" - f"Do you want to import from Ledger?[/yellow]", - default="y", + "[bright_cyan]No private key file found.[/bright_cyan] [yellow]" + "Do you want to import from Ledger?[/yellow]", + default="y", ): accounts = LedgerETHAccount.get_accounts() account_addresses = [acc.address for acc in accounts] From f4c0f2af261f0fc4603a34e5200013d3ca0ebb38 Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 10 Sep 2025 15:53:37 +0200 Subject: [PATCH 03/19] Fix: Solve issue loading the good configuration. --- src/aleph_client/commands/account.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/aleph_client/commands/account.py b/src/aleph_client/commands/account.py index 8b9dc45e..e835af84 100644 --- a/src/aleph_client/commands/account.py +++ b/src/aleph_client/commands/account.py @@ -160,8 +160,14 @@ def display_active_address( typer.secho("No private key available", fg=RED) raise typer.Exit(code=1) - evm_address = _load_account(private_key, private_key_file, chain=Chain.ETH).get_address() - sol_address = _load_account(private_key, private_key_file, chain=Chain.SOL).get_address() + config_file_path = Path(settings.CONFIG_FILE) + config = load_main_configuration(config_file_path) + if config and config.type and config.type == AccountType.EXTERNAL: + evm_address = _load_account(None, None, chain=Chain.ETH).get_address() + sol_address = _load_account(None, None, chain=Chain.SOL).get_address() + else: + evm_address = _load_account(private_key, private_key_file, chain=Chain.ETH).get_address() + sol_address = _load_account(private_key, private_key_file, chain=Chain.SOL).get_address() console.print( "✉ [bold italic blue]Addresses for Active Account[/bold italic blue] ✉\n\n" @@ -483,9 +489,6 @@ async def configure( unlinked_keys, config = await list_unlinked_keys() - if not account_type: - account_type = AccountType.INTERNAL - # Fixes private key file path if private_key_file: if not private_key_file.name.endswith(".key"): @@ -499,7 +502,8 @@ async def configure( raise typer.Exit() # Configures active private key file - if not account_type and not private_key_file and config and hasattr(config, "path") and Path(config.path).exists(): + if (not account_type or account_type == AccountType.INTERNAL and + not private_key_file and config and hasattr(config, "path") and Path(config.path).exists()): if not yes_no_input( f"Active private key file: [bright_cyan]{config.path}[/bright_cyan]\n[yellow]Keep current active private " "key?[/yellow]", @@ -525,9 +529,9 @@ async def configure( else: # No change private_key_file = Path(config.path) - if not private_key_file: + if not private_key_file and account_type == AccountType.EXTERNAL: if yes_no_input( - "[bright_cyan]No private key file found.[/bright_cyan] [yellow]" + "[bright_cyan]Loading External keys.[/bright_cyan] [yellow]" "Do you want to import from Ledger?[/yellow]", default="y", ): @@ -573,6 +577,9 @@ async def configure( typer.secho("No chain provided.", fg=typer.colors.RED) raise typer.Exit() + if not account_type: + account_type = AccountType.INTERNAL.value + try: config = MainConfiguration(path=private_key_file, chain=chain, address=address, type=account_type) save_main_configuration(settings.CONFIG_FILE, config) From c52f9dc9fe07169119746f61dfa41d802408c49a Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 10 Sep 2025 16:27:47 +0200 Subject: [PATCH 04/19] Fix: Solved definitively the wallet selection issue and also solved another issue fetching instances from scheduler. --- src/aleph_client/commands/account.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/aleph_client/commands/account.py b/src/aleph_client/commands/account.py index e835af84..163724ea 100644 --- a/src/aleph_client/commands/account.py +++ b/src/aleph_client/commands/account.py @@ -502,8 +502,13 @@ async def configure( raise typer.Exit() # Configures active private key file - if (not account_type or account_type == AccountType.INTERNAL and - not private_key_file and config and hasattr(config, "path") and Path(config.path).exists()): + if not account_type or ( + account_type == AccountType.INTERNAL + and not private_key_file + and config + and hasattr(config, "path") + and Path(config.path).exists() + ): if not yes_no_input( f"Active private key file: [bright_cyan]{config.path}[/bright_cyan]\n[yellow]Keep current active private " "key?[/yellow]", @@ -531,8 +536,7 @@ async def configure( if not private_key_file and account_type == AccountType.EXTERNAL: if yes_no_input( - "[bright_cyan]Loading External keys.[/bright_cyan] [yellow]" - "Do you want to import from Ledger?[/yellow]", + "[bright_cyan]Loading External keys.[/bright_cyan] [yellow]Do you want to import from Ledger?[/yellow]", default="y", ): accounts = LedgerETHAccount.get_accounts() @@ -578,7 +582,7 @@ async def configure( raise typer.Exit() if not account_type: - account_type = AccountType.INTERNAL.value + account_type = AccountType.INTERNAL try: config = MainConfiguration(path=private_key_file, chain=chain, address=address, type=account_type) From b994c79d6db660420caf742c9e2f9a42e26c9fde Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 10 Sep 2025 16:35:47 +0200 Subject: [PATCH 05/19] Fix: Solved code-quality issue. --- src/aleph_client/commands/aggregate.py | 15 +++++++-------- src/aleph_client/commands/domain.py | 17 ++++++++++------- src/aleph_client/commands/files.py | 10 +++++----- src/aleph_client/commands/message.py | 10 +++++----- src/aleph_client/commands/program.py | 6 +++--- 5 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/aleph_client/commands/aggregate.py b/src/aleph_client/commands/aggregate.py index c2848e33..c2c25c13 100644 --- a/src/aleph_client/commands/aggregate.py +++ b/src/aleph_client/commands/aggregate.py @@ -11,7 +11,6 @@ from aleph.sdk.account import _load_account from aleph.sdk.client import AuthenticatedAlephHttpClient from aleph.sdk.conf import settings -from aleph.sdk.types import AccountFromPrivateKey from aleph.sdk.utils import extended_json_encoder from aleph_message.models import Chain, MessageType from aleph_message.status import MessageStatus @@ -59,7 +58,7 @@ async def forget( setup_logging(debug) - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account = _load_account(private_key, private_key_file) address = account.get_address() if address is None else address if key == "security" and not is_same_context(): @@ -132,7 +131,7 @@ async def post( setup_logging(debug) - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account = _load_account(private_key, private_key_file) address = account.get_address() if address is None else address if key == "security" and not is_same_context(): @@ -194,7 +193,7 @@ async def get( setup_logging(debug) - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account = _load_account(private_key, private_key_file) address = account.get_address() if address is None else address async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client: @@ -230,7 +229,7 @@ async def list_aggregates( setup_logging(debug) - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account = _load_account(private_key, private_key_file) address = account.get_address() if address is None else address aggr_link = f"{sanitize_url(settings.API_HOST)}/api/v0/aggregates/{address}.json" @@ -304,7 +303,7 @@ async def authorize( setup_logging(debug) - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account = _load_account(private_key, private_key_file) data = await get( key="security", @@ -378,7 +377,7 @@ async def revoke( setup_logging(debug) - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account = _load_account(private_key, private_key_file) data = await get( key="security", @@ -433,7 +432,7 @@ async def permissions( setup_logging(debug) - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account = _load_account(private_key, private_key_file) address = account.get_address() if address is None else address data = await get( diff --git a/src/aleph_client/commands/domain.py b/src/aleph_client/commands/domain.py index 538bdcbd..295e6ce9 100644 --- a/src/aleph_client/commands/domain.py +++ b/src/aleph_client/commands/domain.py @@ -3,7 +3,7 @@ import logging from pathlib import Path from time import sleep -from typing import Annotated, Optional, cast +from typing import Annotated, Optional, Union, cast import typer from aleph.sdk.account import _load_account @@ -19,6 +19,7 @@ from aleph.sdk.exceptions import DomainConfigurationError from aleph.sdk.query.filters import MessageFilter from aleph.sdk.types import AccountFromPrivateKey +from aleph.sdk.wallets.ledger import LedgerETHAccount from aleph_message.models import AggregateMessage from aleph_message.models.base import MessageType from rich.console import Console @@ -65,7 +66,7 @@ async def check_domain_records(fqdn, target, owner): async def attach_resource( - account: AccountFromPrivateKey, + account, fqdn: Hostname, item_hash: Optional[str] = None, catch_all_path: Optional[str] = None, @@ -137,7 +138,9 @@ async def attach_resource( ) -async def detach_resource(account: AccountFromPrivateKey, fqdn: Hostname, interactive: Optional[bool] = None): +async def detach_resource( + account: Union[AccountFromPrivateKey, LedgerETHAccount], fqdn: Hostname, interactive: Optional[bool] = None +): domain_info = await get_aggregate_domain_info(account, fqdn) interactive = is_environment_interactive() if interactive is None else interactive @@ -187,7 +190,7 @@ async def add( ] = settings.PRIVATE_KEY_FILE, ): """Add and link a Custom Domain.""" - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account = _load_account(private_key, private_key_file) interactive = False if (not ask) else is_environment_interactive() console = Console() @@ -272,7 +275,7 @@ async def attach( ] = settings.PRIVATE_KEY_FILE, ): """Attach resource to a Custom Domain.""" - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account = _load_account(private_key, private_key_file) await attach_resource( account, @@ -294,7 +297,7 @@ async def detach( ] = settings.PRIVATE_KEY_FILE, ): """Unlink Custom Domain.""" - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account = _load_account(private_key, private_key_file) await detach_resource(account, Hostname(fqdn), interactive=False if (not ask) else None) raise typer.Exit() @@ -309,7 +312,7 @@ async def info( ] = settings.PRIVATE_KEY_FILE, ): """Show Custom Domain Details.""" - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account = _load_account(private_key, private_key_file) console = Console() domain_validator = DomainValidator() diff --git a/src/aleph_client/commands/files.py b/src/aleph_client/commands/files.py index bad66bcb..f63f455f 100644 --- a/src/aleph_client/commands/files.py +++ b/src/aleph_client/commands/files.py @@ -12,7 +12,7 @@ from aleph.sdk import AlephHttpClient, AuthenticatedAlephHttpClient from aleph.sdk.account import _load_account from aleph.sdk.conf import settings -from aleph.sdk.types import AccountFromPrivateKey, StorageEnum, StoredContent +from aleph.sdk.types import StorageEnum, StoredContent from aleph.sdk.utils import safe_getattr from aleph_message.models import ItemHash, StoreMessage from aleph_message.status import MessageStatus @@ -44,7 +44,7 @@ async def pin( setup_logging(debug) - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account = _load_account(private_key, private_key_file) async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client: result: StoreMessage @@ -75,7 +75,7 @@ async def upload( setup_logging(debug) - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account = _load_account(private_key, private_key_file) async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client: if not path.is_file(): @@ -181,7 +181,7 @@ async def forget( setup_logging(debug) - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account = _load_account(private_key, private_key_file) hashes = [ItemHash(item_hash) for item_hash in item_hash.split(",")] @@ -270,7 +270,7 @@ async def list_files( json: Annotated[bool, typer.Option(help="Print as json instead of rich table")] = False, ): """List all files for a given address""" - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account = _load_account(private_key, private_key_file) if account and not address: address = account.get_address() diff --git a/src/aleph_client/commands/message.py b/src/aleph_client/commands/message.py index a7a48d60..07582587 100644 --- a/src/aleph_client/commands/message.py +++ b/src/aleph_client/commands/message.py @@ -20,7 +20,7 @@ ) from aleph.sdk.query.filters import MessageFilter from aleph.sdk.query.responses import MessagesResponse -from aleph.sdk.types import AccountFromPrivateKey, StorageEnum +from aleph.sdk.types import StorageEnum from aleph.sdk.utils import extended_json_encoder from aleph_message.models import AlephMessage, ProgramMessage from aleph_message.models.base import MessageType @@ -138,7 +138,7 @@ async def post( setup_logging(debug) - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account = _load_account(private_key, private_key_file) storage_engine: StorageEnum content: dict @@ -188,7 +188,7 @@ async def amend( setup_logging(debug) - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account = _load_account(private_key, private_key_file) async with AlephHttpClient(api_server=settings.API_HOST) as client: existing_message: Optional[AlephMessage] = None @@ -253,7 +253,7 @@ async def forget( hash_list: list[ItemHash] = [ItemHash(h) for h in hashes.split(",")] - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account = _load_account(private_key, private_key_file) async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client: await client.forget(hashes=hash_list, reason=reason, channel=channel) @@ -296,7 +296,7 @@ def sign( setup_logging(debug) - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account = _load_account(private_key, private_key_file) if message is None: message = input_multiline() diff --git a/src/aleph_client/commands/program.py b/src/aleph_client/commands/program.py index 4105656f..becdc246 100644 --- a/src/aleph_client/commands/program.py +++ b/src/aleph_client/commands/program.py @@ -24,7 +24,7 @@ ) from aleph.sdk.query.filters import MessageFilter from aleph.sdk.query.responses import PriceResponse -from aleph.sdk.types import AccountFromPrivateKey, StorageEnum, TokenType +from aleph.sdk.types import StorageEnum, TokenType from aleph.sdk.utils import displayable_amount, make_program_content, safe_getattr from aleph_message.models import ( Chain, @@ -127,7 +127,7 @@ async def upload( typer.echo("No such file or directory") raise typer.Exit(code=4) from error - account: AccountFromPrivateKey = _load_account(private_key, private_key_file, chain=payment_chain) + account = _load_account(private_key, private_key_file, chain=payment_chain) address = address or settings.ADDRESS_TO_USE or account.get_address() # Loads default configuration if no chain is set @@ -339,7 +339,7 @@ async def update( typer.echo("No such file or directory") raise typer.Exit(code=4) from error - account: AccountFromPrivateKey = _load_account(private_key, private_key_file, chain=chain) + account = _load_account(private_key, private_key_file, chain=chain) async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client: try: From cb798b519392528d2dccef5d94c5aed06ad15a24 Mon Sep 17 00:00:00 2001 From: 1yam Date: Fri, 31 Oct 2025 10:51:04 +0100 Subject: [PATCH 06/19] Feature: cli load_account to handle account selections --- src/aleph_client/utils.py | 49 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/src/aleph_client/utils.py b/src/aleph_client/utils.py index cc3c5aaa..560ab3ac 100644 --- a/src/aleph_client/utils.py +++ b/src/aleph_client/utils.py @@ -18,11 +18,22 @@ import aiohttp import typer from aiohttp import ClientSession -from aleph.sdk.conf import MainConfiguration, load_main_configuration, settings -from aleph.sdk.types import GenericMessage +from aleph.sdk.account import _load_account +from aleph.sdk.conf import ( + AccountType, + MainConfiguration, + load_main_configuration, + settings, +) +from aleph.sdk.types import AccountFromPrivateKey, GenericMessage +from aleph.sdk.wallets.ledger import LedgerETHAccount +from aleph_message.models import Chain from aleph_message.models.base import MessageType from aleph_message.models.execution.base import Encoding +# Type alias for account types +AlephAccount = Union[AccountFromPrivateKey, LedgerETHAccount] + logger = logging.getLogger(__name__) try: @@ -190,3 +201,37 @@ def cached_async_function(*args, **kwargs): return ensure_future(async_function(*args, **kwargs)) return cached_async_function + + +def load_account( + private_key_str: Optional[str], private_key_file: Optional[Path], chain: Optional[Chain] = None +) -> AlephAccount: + """ + Two Case Possible + - Account from private key + - External account (ledger) + + We first try to load configurations, if no configurations we fallback to private_key_str / private_key_file. + """ + + # 1st Check for configurations + config_file_path = Path(settings.CONFIG_FILE) + config = load_main_configuration(config_file_path) + + # If no config we try to load private_key_str / private_key_file + if not config: + logger.warning("No config detected fallback to private key") + if private_key_str is not None: + private_key_file = None + + elif private_key_file and not private_key_file.exists(): + logger.error("No account could be retrieved please use `aleph account create` or `aleph account configure`") + raise typer.Exit(code=1) + + if not chain and config: + chain = config.chain + + if config and config.type and config.type == AccountType.EXTERNAL: + return _load_account(None, None, chain=chain) + else: + return _load_account(private_key_str, private_key_file, chain=chain) From 9c347b56d9335af1b9d95cface940c4bf13c7a07 Mon Sep 17 00:00:00 2001 From: 1yam Date: Fri, 31 Oct 2025 10:51:40 +0100 Subject: [PATCH 07/19] Fix: using load_account instead of _load_account --- src/aleph_client/commands/account.py | 47 ++++++++----------- src/aleph_client/commands/aggregate.py | 17 ++++--- src/aleph_client/commands/credit.py | 7 ++- src/aleph_client/commands/domain.py | 19 +++----- src/aleph_client/commands/files.py | 11 ++--- .../commands/instance/__init__.py | 21 ++++----- .../commands/instance/port_forwarder.py | 13 +++-- src/aleph_client/commands/message.py | 11 ++--- 8 files changed, 64 insertions(+), 82 deletions(-) diff --git a/src/aleph_client/commands/account.py b/src/aleph_client/commands/account.py index 163724ea..1efcdb7b 100644 --- a/src/aleph_client/commands/account.py +++ b/src/aleph_client/commands/account.py @@ -44,7 +44,12 @@ validated_prompt, yes_no_input, ) -from aleph_client.utils import AsyncTyper, list_unlinked_keys +from aleph_client.utils import ( + AlephAccount, + AsyncTyper, + list_unlinked_keys, + load_account, +) logger = logging.getLogger(__name__) app = AsyncTyper(no_args_is_help=True) @@ -154,20 +159,8 @@ def display_active_address( Display your public address(es). """ - if private_key is not None: - private_key_file = None - elif private_key_file and not private_key_file.exists(): - typer.secho("No private key available", fg=RED) - raise typer.Exit(code=1) - - config_file_path = Path(settings.CONFIG_FILE) - config = load_main_configuration(config_file_path) - if config and config.type and config.type == AccountType.EXTERNAL: - evm_address = _load_account(None, None, chain=Chain.ETH).get_address() - sol_address = _load_account(None, None, chain=Chain.SOL).get_address() - else: - evm_address = _load_account(private_key, private_key_file, chain=Chain.ETH).get_address() - sol_address = _load_account(private_key, private_key_file, chain=Chain.SOL).get_address() + evm_address = load_account(private_key, private_key_file, chain=Chain.ETH).get_address() + sol_address = load_account(private_key, private_key_file, chain=Chain.SOL).get_address() console.print( "✉ [bold italic blue]Addresses for Active Account[/bold italic blue] ✉\n\n" @@ -267,7 +260,7 @@ def sign_bytes( setup_logging(debug) - account = _load_account(private_key, private_key_file, chain=chain) + account: AlephAccount = load_account(private_key, private_key_file, chain=chain) if not message: message = input_multiline() @@ -302,7 +295,7 @@ async def balance( chain: Annotated[Optional[Chain], typer.Option(help=help_strings.ADDRESS_CHAIN)] = None, ): """Display your ALEPH balance and basic voucher information.""" - account = _load_account(private_key, private_key_file, chain=chain) + account: AlephAccount = load_account(private_key, private_key_file, chain=chain) if account and not address: address = account.get_address() @@ -431,7 +424,7 @@ async def vouchers( chain: Annotated[Optional[Chain], typer.Option(help=help_strings.ADDRESS_CHAIN)] = None, ): """Display detailed information about your vouchers.""" - account = _load_account(private_key, private_key_file, chain=chain) + account = load_account(private_key, private_key_file, chain=chain) if account and not address: address = account.get_address() @@ -501,13 +494,11 @@ async def configure( typer.secho(f"Private key file not found: {private_key_file}", fg=typer.colors.RED) raise typer.Exit() - # Configures active private key file - if not account_type or ( - account_type == AccountType.INTERNAL - and not private_key_file - and config - and hasattr(config, "path") - and Path(config.path).exists() + # If private_key_file is specified via command line, prioritize it + if private_key_file: + pass + elif not account_type or ( + account_type == AccountType.INTERNAL and config and hasattr(config, "path") and Path(config.path).exists() ): if not yes_no_input( f"Active private key file: [bright_cyan]{config.path}[/bright_cyan]\n[yellow]Keep current active private " @@ -561,8 +552,10 @@ async def configure( typer.secho("No private key file provided or found.", fg=typer.colors.RED) raise typer.Exit() - # Configure active chain - if not chain and config and hasattr(config, "chain"): + # If chain is specified via command line, prioritize it + if chain: + pass + elif config and hasattr(config, "chain"): if not yes_no_input( f"Active chain: [bright_cyan]{config.chain}[/bright_cyan]\n[yellow]Keep current active chain?[/yellow]", default="y", diff --git a/src/aleph_client/commands/aggregate.py b/src/aleph_client/commands/aggregate.py index c2c25c13..6ec0cd07 100644 --- a/src/aleph_client/commands/aggregate.py +++ b/src/aleph_client/commands/aggregate.py @@ -8,7 +8,6 @@ import typer from aiohttp import ClientResponseError, ClientSession -from aleph.sdk.account import _load_account from aleph.sdk.client import AuthenticatedAlephHttpClient from aleph.sdk.conf import settings from aleph.sdk.utils import extended_json_encoder @@ -20,7 +19,7 @@ from aleph_client.commands import help_strings from aleph_client.commands.utils import setup_logging -from aleph_client.utils import AsyncTyper, sanitize_url +from aleph_client.utils import AlephAccount, AsyncTyper, load_account, sanitize_url logger = logging.getLogger(__name__) app = AsyncTyper(no_args_is_help=True) @@ -58,7 +57,7 @@ async def forget( setup_logging(debug) - account = _load_account(private_key, private_key_file) + account: AlephAccount = load_account(private_key, private_key_file) address = account.get_address() if address is None else address if key == "security" and not is_same_context(): @@ -131,7 +130,7 @@ async def post( setup_logging(debug) - account = _load_account(private_key, private_key_file) + account: AlephAccount = load_account(private_key, private_key_file) address = account.get_address() if address is None else address if key == "security" and not is_same_context(): @@ -193,7 +192,7 @@ async def get( setup_logging(debug) - account = _load_account(private_key, private_key_file) + account: AlephAccount = load_account(private_key, private_key_file) address = account.get_address() if address is None else address async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client: @@ -229,7 +228,7 @@ async def list_aggregates( setup_logging(debug) - account = _load_account(private_key, private_key_file) + account: AlephAccount = load_account(private_key, private_key_file) address = account.get_address() if address is None else address aggr_link = f"{sanitize_url(settings.API_HOST)}/api/v0/aggregates/{address}.json" @@ -303,7 +302,7 @@ async def authorize( setup_logging(debug) - account = _load_account(private_key, private_key_file) + account: AlephAccount = load_account(private_key, private_key_file) data = await get( key="security", @@ -377,7 +376,7 @@ async def revoke( setup_logging(debug) - account = _load_account(private_key, private_key_file) + account: AlephAccount = load_account(private_key, private_key_file) data = await get( key="security", @@ -432,7 +431,7 @@ async def permissions( setup_logging(debug) - account = _load_account(private_key, private_key_file) + account: AlephAccount = load_account(private_key, private_key_file) address = account.get_address() if address is None else address data = await get( diff --git a/src/aleph_client/commands/credit.py b/src/aleph_client/commands/credit.py index 54b8dcde..70a95bf3 100644 --- a/src/aleph_client/commands/credit.py +++ b/src/aleph_client/commands/credit.py @@ -7,7 +7,6 @@ from aleph.sdk import AlephHttpClient from aleph.sdk.account import _load_account from aleph.sdk.conf import settings -from aleph.sdk.types import AccountFromPrivateKey from aleph.sdk.utils import displayable_amount from rich import box from rich.console import Console @@ -17,7 +16,7 @@ from aleph_client.commands import help_strings from aleph_client.commands.utils import setup_logging -from aleph_client.utils import AsyncTyper +from aleph_client.utils import AlephAccount, AsyncTyper logger = logging.getLogger(__name__) app = AsyncTyper(no_args_is_help=True) @@ -41,7 +40,7 @@ async def show( setup_logging(debug) - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account: AlephAccount = _load_account(private_key, private_key_file) if account and not address: address = account.get_address() @@ -87,7 +86,7 @@ async def history( ): setup_logging(debug) - account: AccountFromPrivateKey = _load_account(private_key, private_key_file) + account: AlephAccount = _load_account(private_key, private_key_file) if account and not address: address = account.get_address() diff --git a/src/aleph_client/commands/domain.py b/src/aleph_client/commands/domain.py index 295e6ce9..86c9e801 100644 --- a/src/aleph_client/commands/domain.py +++ b/src/aleph_client/commands/domain.py @@ -3,10 +3,9 @@ import logging from pathlib import Path from time import sleep -from typing import Annotated, Optional, Union, cast +from typing import Annotated, Optional, cast import typer -from aleph.sdk.account import _load_account from aleph.sdk.client import AlephHttpClient, AuthenticatedAlephHttpClient from aleph.sdk.conf import settings from aleph.sdk.domain import ( @@ -18,8 +17,6 @@ ) from aleph.sdk.exceptions import DomainConfigurationError from aleph.sdk.query.filters import MessageFilter -from aleph.sdk.types import AccountFromPrivateKey -from aleph.sdk.wallets.ledger import LedgerETHAccount from aleph_message.models import AggregateMessage from aleph_message.models.base import MessageType from rich.console import Console @@ -28,7 +25,7 @@ from aleph_client.commands import help_strings from aleph_client.commands.utils import is_environment_interactive -from aleph_client.utils import AsyncTyper +from aleph_client.utils import AlephAccount, AsyncTyper, load_account logger = logging.getLogger(__name__) @@ -138,9 +135,7 @@ async def attach_resource( ) -async def detach_resource( - account: Union[AccountFromPrivateKey, LedgerETHAccount], fqdn: Hostname, interactive: Optional[bool] = None -): +async def detach_resource(account: AlephAccount, fqdn: Hostname, interactive: Optional[bool] = None): domain_info = await get_aggregate_domain_info(account, fqdn) interactive = is_environment_interactive() if interactive is None else interactive @@ -190,7 +185,7 @@ async def add( ] = settings.PRIVATE_KEY_FILE, ): """Add and link a Custom Domain.""" - account = _load_account(private_key, private_key_file) + account: AlephAccount = load_account(private_key, private_key_file) interactive = False if (not ask) else is_environment_interactive() console = Console() @@ -275,7 +270,7 @@ async def attach( ] = settings.PRIVATE_KEY_FILE, ): """Attach resource to a Custom Domain.""" - account = _load_account(private_key, private_key_file) + account: AlephAccount = load_account(private_key, private_key_file) await attach_resource( account, @@ -297,7 +292,7 @@ async def detach( ] = settings.PRIVATE_KEY_FILE, ): """Unlink Custom Domain.""" - account = _load_account(private_key, private_key_file) + account: AlephAccount = load_account(private_key, private_key_file) await detach_resource(account, Hostname(fqdn), interactive=False if (not ask) else None) raise typer.Exit() @@ -312,7 +307,7 @@ async def info( ] = settings.PRIVATE_KEY_FILE, ): """Show Custom Domain Details.""" - account = _load_account(private_key, private_key_file) + account: AlephAccount = load_account(private_key, private_key_file) console = Console() domain_validator = DomainValidator() diff --git a/src/aleph_client/commands/files.py b/src/aleph_client/commands/files.py index f63f455f..586fff85 100644 --- a/src/aleph_client/commands/files.py +++ b/src/aleph_client/commands/files.py @@ -10,7 +10,6 @@ import typer from aiohttp import ClientResponseError from aleph.sdk import AlephHttpClient, AuthenticatedAlephHttpClient -from aleph.sdk.account import _load_account from aleph.sdk.conf import settings from aleph.sdk.types import StorageEnum, StoredContent from aleph.sdk.utils import safe_getattr @@ -23,7 +22,7 @@ from aleph_client.commands import help_strings from aleph_client.commands.utils import setup_logging -from aleph_client.utils import AsyncTyper +from aleph_client.utils import AlephAccount, AsyncTyper, load_account logger = logging.getLogger(__name__) app = AsyncTyper(no_args_is_help=True) @@ -44,7 +43,7 @@ async def pin( setup_logging(debug) - account = _load_account(private_key, private_key_file) + account: AlephAccount = load_account(private_key, private_key_file) async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client: result: StoreMessage @@ -75,7 +74,7 @@ async def upload( setup_logging(debug) - account = _load_account(private_key, private_key_file) + account: AlephAccount = load_account(private_key, private_key_file) async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client: if not path.is_file(): @@ -181,7 +180,7 @@ async def forget( setup_logging(debug) - account = _load_account(private_key, private_key_file) + account: AlephAccount = load_account(private_key, private_key_file) hashes = [ItemHash(item_hash) for item_hash in item_hash.split(",")] @@ -270,7 +269,7 @@ async def list_files( json: Annotated[bool, typer.Option(help="Print as json instead of rich table")] = False, ): """List all files for a given address""" - account = _load_account(private_key, private_key_file) + account: AlephAccount = load_account(private_key, private_key_file) if account and not address: address = account.get_address() diff --git a/src/aleph_client/commands/instance/__init__.py b/src/aleph_client/commands/instance/__init__.py index 22d064ff..0cda53eb 100644 --- a/src/aleph_client/commands/instance/__init__.py +++ b/src/aleph_client/commands/instance/__init__.py @@ -11,7 +11,6 @@ import aiohttp import typer from aleph.sdk import AlephHttpClient, AuthenticatedAlephHttpClient -from aleph.sdk.account import _load_account from aleph.sdk.chains.ethereum import ETHAccount from aleph.sdk.client.services.crn import NetworkGPUS from aleph.sdk.client.services.pricing import Price @@ -82,7 +81,7 @@ yes_no_input, ) from aleph_client.models import CRNInfo -from aleph_client.utils import AsyncTyper, sanitize_url +from aleph_client.utils import AlephAccount, AsyncTyper, load_account, sanitize_url logger = logging.getLogger(__name__) app = AsyncTyper(no_args_is_help=True) @@ -167,7 +166,7 @@ async def create( ssh_pubkey: str = ssh_pubkey_file.read_text(encoding="utf-8").strip() # Populates account / address - account = _load_account(private_key, private_key_file, chain=payment_chain) + account: AlephAccount = load_account(private_key, private_key_file, chain=payment_chain) address = address or settings.ADDRESS_TO_USE or account.get_address() @@ -831,7 +830,7 @@ async def delete( setup_logging(debug) - account = _load_account(private_key, private_key_file, chain=chain) + account: AlephAccount = load_account(private_key, private_key_file, chain=chain) async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client: try: existing_message: InstanceMessage = await client.get_message( @@ -943,7 +942,7 @@ async def list_instances( setup_logging(debug) - account = _load_account(private_key, private_key_file, chain=chain) + account: AlephAccount = load_account(private_key, private_key_file, chain=chain) address = address or settings.ADDRESS_TO_USE or account.get_address() async with AlephHttpClient(api_server=settings.API_HOST) as client: @@ -980,7 +979,7 @@ async def reboot( or Prompt.ask("URL of the CRN (Compute node) on which the VM is running") ) - account = _load_account(private_key, private_key_file, chain=chain) + account: AlephAccount = load_account(private_key, private_key_file, chain=chain) async with VmClient(account, domain) as manager: status, result = await manager.reboot_instance(vm_id=vm_id) @@ -1013,7 +1012,7 @@ async def allocate( or Prompt.ask("URL of the CRN (Compute node) on which the VM will be allocated") ) - account = _load_account(private_key, private_key_file, chain=chain) + account: AlephAccount = load_account(private_key, private_key_file, chain=chain) async with VmClient(account, domain) as manager: status, result = await manager.start_instance(vm_id=vm_id) @@ -1041,7 +1040,7 @@ async def logs( domain = (domain and sanitize_url(domain)) or await find_crn_of_vm(vm_id) or Prompt.ask(help_strings.PROMPT_CRN_URL) - account = _load_account(private_key, private_key_file, chain=chain) + account: AlephAccount = load_account(private_key, private_key_file, chain=chain) async with VmClient(account, domain) as manager: try: @@ -1072,7 +1071,7 @@ async def stop( domain = (domain and sanitize_url(domain)) or await find_crn_of_vm(vm_id) or Prompt.ask(help_strings.PROMPT_CRN_URL) - account = _load_account(private_key, private_key_file, chain=chain) + account: AlephAccount = load_account(private_key, private_key_file, chain=chain) async with VmClient(account, domain) as manager: status, result = await manager.stop_instance(vm_id=vm_id) @@ -1111,7 +1110,7 @@ async def confidential_init_session( or Prompt.ask("URL of the CRN (Compute node) on which the session will be initialized") ) - account = _load_account(private_key, private_key_file, chain=chain) + account: AlephAccount = load_account(private_key, private_key_file, chain=chain) sevctl_path = find_sevctl_or_exit() @@ -1188,7 +1187,7 @@ async def confidential_start( session_dir.mkdir(exist_ok=True, parents=True) vm_hash = ItemHash(vm_id) - account = _load_account(private_key, private_key_file, chain=chain) + account: AlephAccount = load_account(private_key, private_key_file, chain=chain) sevctl_path = find_sevctl_or_exit() domain = ( diff --git a/src/aleph_client/commands/instance/port_forwarder.py b/src/aleph_client/commands/instance/port_forwarder.py index 58421402..bbbb6518 100644 --- a/src/aleph_client/commands/instance/port_forwarder.py +++ b/src/aleph_client/commands/instance/port_forwarder.py @@ -7,7 +7,6 @@ import typer from aiohttp import ClientResponseError from aleph.sdk import AlephHttpClient, AuthenticatedAlephHttpClient -from aleph.sdk.account import _load_account from aleph.sdk.conf import settings from aleph.sdk.exceptions import MessageNotProcessed, NotAuthorize from aleph.sdk.types import InstanceManual, PortFlags, Ports @@ -21,7 +20,7 @@ from aleph_client.commands import help_strings from aleph_client.commands.utils import setup_logging -from aleph_client.utils import AsyncTyper +from aleph_client.utils import AlephAccount, AsyncTyper, load_account logger = logging.getLogger(__name__) app = AsyncTyper(no_args_is_help=True) @@ -42,7 +41,7 @@ async def list_ports( setup_logging(debug) - account = _load_account(private_key, private_key_file, chain=chain) + account: AlephAccount = load_account(private_key, private_key_file) address = address or settings.ADDRESS_TO_USE or account.get_address() async with AlephHttpClient(api_server=settings.API_HOST) as client: @@ -160,7 +159,7 @@ async def create( typer.echo("Error: Port must be between 1 and 65535") raise typer.Exit(code=1) - account = _load_account(private_key, private_key_file, chain=chain) + account: AlephAccount = load_account(private_key, private_key_file) # Create the port flags port_flags = PortFlags(tcp=tcp, udp=udp) @@ -213,7 +212,7 @@ async def update( typer.echo("Error: Port must be between 1 and 65535") raise typer.Exit(code=1) - account = _load_account(private_key, private_key_file, chain=chain) + account: AlephAccount = load_account(private_key, private_key_file, chain) # First check if the port forward exists async with AlephHttpClient(api_server=settings.API_HOST) as client: @@ -293,7 +292,7 @@ async def delete( typer.echo("Error: Port must be between 1 and 65535") raise typer.Exit(code=1) - account = _load_account(private_key, private_key_file, chain=chain) + account: AlephAccount = load_account(private_key, private_key_file, chain) # First check if the port forward exists async with AlephHttpClient(api_server=settings.API_HOST) as client: @@ -376,7 +375,7 @@ async def refresh( setup_logging(debug) - account = _load_account(private_key, private_key_file, chain=chain) + account: AlephAccount = load_account(private_key, private_key_file, chain) try: async with AuthenticatedAlephHttpClient(api_server=settings.API_HOST, account=account) as client: diff --git a/src/aleph_client/commands/message.py b/src/aleph_client/commands/message.py index 07582587..36285e00 100644 --- a/src/aleph_client/commands/message.py +++ b/src/aleph_client/commands/message.py @@ -11,7 +11,6 @@ import typer from aleph.sdk import AlephHttpClient, AuthenticatedAlephHttpClient -from aleph.sdk.account import _load_account from aleph.sdk.conf import settings from aleph.sdk.exceptions import ( ForgottenMessageError, @@ -35,7 +34,7 @@ setup_logging, str_to_datetime, ) -from aleph_client.utils import AsyncTyper +from aleph_client.utils import AlephAccount, AsyncTyper, load_account app = AsyncTyper(no_args_is_help=True) @@ -138,7 +137,7 @@ async def post( setup_logging(debug) - account = _load_account(private_key, private_key_file) + account: AlephAccount = load_account(private_key, private_key_file) storage_engine: StorageEnum content: dict @@ -188,7 +187,7 @@ async def amend( setup_logging(debug) - account = _load_account(private_key, private_key_file) + account: AlephAccount = load_account(private_key, private_key_file) async with AlephHttpClient(api_server=settings.API_HOST) as client: existing_message: Optional[AlephMessage] = None @@ -253,7 +252,7 @@ async def forget( hash_list: list[ItemHash] = [ItemHash(h) for h in hashes.split(",")] - account = _load_account(private_key, private_key_file) + account: AlephAccount = load_account(private_key, private_key_file) async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client: await client.forget(hashes=hash_list, reason=reason, channel=channel) @@ -296,7 +295,7 @@ def sign( setup_logging(debug) - account = _load_account(private_key, private_key_file) + account: AlephAccount = load_account(private_key, private_key_file) if message is None: message = input_multiline() From f770d0fa923e3125bcd9a78d4168bf2c3bbb7ba9 Mon Sep 17 00:00:00 2001 From: 1yam Date: Fri, 31 Oct 2025 10:51:57 +0100 Subject: [PATCH 08/19] fix: unit test mock --- tests/unit/test_account_transact.py | 2 +- tests/unit/test_aggregate.py | 14 +++++++------- tests/unit/test_commands.py | 1 + tests/unit/test_instance.py | 24 ++++++++++++------------ tests/unit/test_port_forwarder.py | 26 +++++++++++++------------- 5 files changed, 34 insertions(+), 33 deletions(-) diff --git a/tests/unit/test_account_transact.py b/tests/unit/test_account_transact.py index 81a59b1b..3faba2da 100644 --- a/tests/unit/test_account_transact.py +++ b/tests/unit/test_account_transact.py @@ -26,7 +26,7 @@ def test_account_can_transact_success(mock_account): assert mock_account.can_transact() is True -@patch("aleph_client.commands.account._load_account") +@patch("aleph_client.commands.account.load_account") def test_account_can_transact_error_handling(mock_load_account): """Test that error is handled properly when account.can_transact() fails.""" # Setup mock account that will raise InsufficientFundsError diff --git a/tests/unit/test_aggregate.py b/tests/unit/test_aggregate.py index dc03988f..97c75f06 100644 --- a/tests/unit/test_aggregate.py +++ b/tests/unit/test_aggregate.py @@ -67,7 +67,7 @@ async def test_forget(capsys, args): mock_list_aggregates = AsyncMock(return_value=FAKE_AGGREGATE_DATA) mock_auth_client_class, mock_auth_client = create_mock_auth_client() - @patch("aleph_client.commands.aggregate._load_account", mock_load_account) + @patch("aleph_client.commands.aggregate.load_account", mock_load_account) @patch("aleph_client.commands.aggregate.list_aggregates", mock_list_aggregates) @patch("aleph_client.commands.aggregate.AuthenticatedAlephHttpClient", mock_auth_client_class) async def run_forget(aggr_spec): @@ -101,7 +101,7 @@ async def test_post(capsys, args): mock_load_account = create_mock_load_account() mock_auth_client_class, mock_auth_client = create_mock_auth_client() - @patch("aleph_client.commands.aggregate._load_account", mock_load_account) + @patch("aleph_client.commands.aggregate.load_account", mock_load_account) @patch("aleph_client.commands.aggregate.AuthenticatedAlephHttpClient", mock_auth_client_class) async def run_post(aggr_spec): print() # For better display when pytest -v -s @@ -135,7 +135,7 @@ async def test_get(capsys, args, expected): mock_load_account = create_mock_load_account() mock_auth_client_class, mock_auth_client = create_mock_auth_client(return_fetch=FAKE_AGGREGATE_DATA["AI"]) - @patch("aleph_client.commands.aggregate._load_account", mock_load_account) + @patch("aleph_client.commands.aggregate.load_account", mock_load_account) @patch("aleph_client.commands.aggregate.AuthenticatedAlephHttpClient", mock_auth_client_class) async def run_get(aggr_spec): print() # For better display when pytest -v -s @@ -152,7 +152,7 @@ async def run_get(aggr_spec): async def test_list_aggregates(): mock_load_account = create_mock_load_account() - @patch("aleph_client.commands.aggregate._load_account", mock_load_account) + @patch("aleph_client.commands.aggregate.load_account", mock_load_account) @patch.object(aiohttp.ClientSession, "get", mock_client_session_get) async def run_list_aggregates(): print() # For better display when pytest -v -s @@ -169,7 +169,7 @@ async def test_authorize(capsys): mock_get = AsyncMock(return_value=FAKE_AGGREGATE_DATA["security"]) mock_post = AsyncMock(return_value=True) - @patch("aleph_client.commands.aggregate._load_account", mock_load_account) + @patch("aleph_client.commands.aggregate.load_account", mock_load_account) @patch("aleph_client.commands.aggregate.get", mock_get) @patch("aleph_client.commands.aggregate.post", mock_post) async def run_authorize(): @@ -190,7 +190,7 @@ async def test_revoke(capsys): mock_get = AsyncMock(return_value=FAKE_AGGREGATE_DATA["security"]) mock_post = AsyncMock(return_value=True) - @patch("aleph_client.commands.aggregate._load_account", mock_load_account) + @patch("aleph_client.commands.aggregate.load_account", mock_load_account) @patch("aleph_client.commands.aggregate.get", mock_get) @patch("aleph_client.commands.aggregate.post", mock_post) async def run_revoke(): @@ -210,7 +210,7 @@ async def test_permissions(): mock_load_account = create_mock_load_account() mock_get = AsyncMock(return_value=FAKE_AGGREGATE_DATA["security"]) - @patch("aleph_client.commands.aggregate._load_account", mock_load_account) + @patch("aleph_client.commands.aggregate.load_account", mock_load_account) @patch("aleph_client.commands.aggregate.get", mock_get) async def run_permissions(): print() # For better display when pytest -v -s diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index 6199d343..308129ef 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -373,6 +373,7 @@ def test_account_vouchers_no_vouchers(mocker, env_files): def test_account_config(env_files): settings.CONFIG_FILE = env_files[1] result = runner.invoke(app, ["account", "config", "--private-key-file", str(env_files[0]), "--chain", "ETH"]) + print(result.output) assert result.exit_code == 0 assert result.stdout.startswith("New Default Configuration: ") diff --git a/tests/unit/test_instance.py b/tests/unit/test_instance.py index a050c1fd..ae335171 100644 --- a/tests/unit/test_instance.py +++ b/tests/unit/test_instance.py @@ -531,7 +531,7 @@ async def test_create_instance(args, expected, mock_crn_list_obj, mock_pricing_i # Setup all required patches with ( patch("aleph_client.commands.instance.validate_ssh_pubkey_file", mock_validate_ssh_pubkey_file), - patch("aleph_client.commands.instance._load_account", mock_load_account), + patch("aleph_client.commands.instance.load_account", mock_load_account), patch("aleph_client.commands.instance.AlephHttpClient", mock_client_class), patch("aleph_client.commands.pricing.AlephHttpClient", mock_client_class), patch("aleph_client.commands.instance.AuthenticatedAlephHttpClient", mock_auth_client_class), @@ -620,7 +620,7 @@ async def test_list_instances(mock_crn_list_obj, mock_pricing_info_response, moc ) # Setup all patches - @patch("aleph_client.commands.instance._load_account", mock_load_account) + @patch("aleph_client.commands.instance.load_account", mock_load_account) @patch("aleph_client.commands.instance.network.fetch_latest_crn_version", mock_fetch_latest_crn_version) @patch("aleph_client.commands.files.AlephHttpClient", mock_client_class) @patch("aleph_client.commands.instance.AlephHttpClient", mock_auth_client_class) @@ -657,7 +657,7 @@ async def test_delete_instance(mock_api_response): # We need to mock that there is no CRN information to skip VM erasure mock_auth_client.instance = MagicMock(get_instances_allocations=AsyncMock(return_value=MagicMock(root={}))) - @patch("aleph_client.commands.instance._load_account", mock_load_account) + @patch("aleph_client.commands.instance.load_account", mock_load_account) @patch("aleph_client.commands.instance.AuthenticatedAlephHttpClient", mock_auth_client_class) @patch("aleph_client.commands.instance.VmClient", mock_vm_client_class) @patch("aleph_client.commands.instance.fetch_settings", mock_fetch_settings) @@ -709,7 +709,7 @@ async def test_delete_instance_with_insufficient_funds(): } ) - @patch("aleph_client.commands.instance._load_account", mock_load_account) + @patch("aleph_client.commands.instance.load_account", mock_load_account) @patch("aleph_client.commands.instance.AuthenticatedAlephHttpClient", mock_auth_client_class) @patch("aleph_client.commands.instance.VmClient", mock_vm_client_class) @patch("aleph_client.commands.instance.fetch_settings", mock_fetch_settings) @@ -753,7 +753,7 @@ async def test_delete_instance_with_detailed_insufficient_funds_error(capsys, mo } ) - @patch("aleph_client.commands.instance._load_account", mock_load_account) + @patch("aleph_client.commands.instance.load_account", mock_load_account) @patch("aleph_client.commands.instance.AuthenticatedAlephHttpClient", mock_auth_client_class) @patch("aleph_client.commands.instance.VmClient", mock_vm_client_class) @patch("aleph_client.commands.instance.fetch_settings", mock_fetch_settings) @@ -794,7 +794,7 @@ async def test_reboot_instance(): # Add the mock to the auth client mock_auth_client.instance = MagicMock(get_instances_allocations=AsyncMock(return_value=mock_allocation)) - @patch("aleph_client.commands.instance._load_account", mock_load_account) + @patch("aleph_client.commands.instance.load_account", mock_load_account) @patch("aleph_client.commands.instance.network.AlephHttpClient", mock_auth_client_class) @patch("aleph_client.commands.instance.VmClient", mock_vm_client_class) async def reboot_instance(): @@ -826,7 +826,7 @@ async def test_allocate_instance(): # Add the mock to the auth client mock_auth_client.instance = MagicMock(get_instances_allocations=AsyncMock(return_value=mock_allocation)) - @patch("aleph_client.commands.instance._load_account", mock_load_account) + @patch("aleph_client.commands.instance.load_account", mock_load_account) @patch("aleph_client.commands.instance.network.AlephHttpClient", mock_auth_client_class) @patch("aleph_client.commands.instance.VmClient", mock_vm_client_class) async def allocate_instance(): @@ -858,7 +858,7 @@ async def test_logs_instance(capsys): # Add the mock to the auth client mock_auth_client.instance = MagicMock(get_instances_allocations=AsyncMock(return_value=mock_allocation)) - @patch("aleph_client.commands.instance._load_account", mock_load_account) + @patch("aleph_client.commands.instance.load_account", mock_load_account) @patch("aleph_client.commands.instance.network.AlephHttpClient", mock_auth_client_class) @patch("aleph_client.commands.instance.VmClient", mock_vm_client_class) async def logs_instance(): @@ -892,7 +892,7 @@ async def test_stop_instance(): # Add the mock to the auth client mock_auth_client.instance = MagicMock(get_instances_allocations=AsyncMock(return_value=mock_allocation)) - @patch("aleph_client.commands.instance._load_account", mock_load_account) + @patch("aleph_client.commands.instance.load_account", mock_load_account) @patch("aleph_client.commands.instance.network.AlephHttpClient", mock_auth_client_class) @patch("aleph_client.commands.instance.VmClient", mock_vm_client_class) async def stop_instance(): @@ -925,7 +925,7 @@ async def test_confidential_init_session(): # Add the mock to the auth client mock_auth_client.instance = MagicMock(get_instances_allocations=AsyncMock(return_value=mock_allocation)) - @patch("aleph_client.commands.instance._load_account", mock_load_account) + @patch("aleph_client.commands.instance.load_account", mock_load_account) @patch("aleph_client.commands.instance.network.AlephHttpClient", mock_auth_client_class) @patch("aleph_client.commands.utils.shutil", mock_shutil) @patch("aleph_client.commands.instance.shutil", mock_shutil) @@ -967,7 +967,7 @@ async def test_confidential_start(): # Add the mock to the auth client mock_auth_client.instance = MagicMock(get_instances_allocations=AsyncMock(return_value=mock_allocation)) - @patch("aleph_client.commands.instance._load_account", mock_load_account) + @patch("aleph_client.commands.instance.load_account", mock_load_account) @patch("aleph_client.commands.utils.shutil", mock_shutil) @patch("aleph_client.commands.instance.network.AlephHttpClient", mock_auth_client_class) @patch.object(Path, "exists", MagicMock(return_value=True)) @@ -1090,7 +1090,7 @@ async def test_gpu_create_no_gpus_available(mock_crn_list_obj, mock_pricing_info mock_fetch_latest_crn_version = create_mock_fetch_latest_crn_version() mock_validated_prompt = MagicMock(return_value="1") - @patch("aleph_client.commands.instance._load_account", mock_load_account) + @patch("aleph_client.commands.instance.load_account", mock_load_account) @patch("aleph_client.commands.instance.validate_ssh_pubkey_file", mock_validate_ssh_pubkey_file) @patch("aleph_client.commands.instance.AlephHttpClient", mock_client_class) @patch("aleph_client.commands.pricing.AlephHttpClient", mock_client_class) diff --git a/tests/unit/test_port_forwarder.py b/tests/unit/test_port_forwarder.py index a7387e64..5c782313 100644 --- a/tests/unit/test_port_forwarder.py +++ b/tests/unit/test_port_forwarder.py @@ -98,7 +98,7 @@ async def test_list_ports(mock_auth_setup): mock_console = MagicMock() with ( - patch("aleph_client.commands.instance.port_forwarder._load_account", mock_load_account), + patch("aleph_client.commands.instance.port_forwarder.load_account", mock_load_account), patch("aleph_client.commands.instance.port_forwarder.AlephHttpClient", mock_client_class), patch("aleph_client.commands.instance.port_forwarder.Console", return_value=mock_console), ): @@ -118,7 +118,7 @@ async def test_list_ports(mock_auth_setup): ) with ( - patch("aleph_client.commands.instance.port_forwarder._load_account", mock_load_account), + patch("aleph_client.commands.instance.port_forwarder.load_account", mock_load_account), patch("aleph_client.commands.instance.port_forwarder.AlephHttpClient", mock_client_class), patch("aleph_client.commands.instance.port_forwarder.typer.echo") as mock_echo, patch("aleph_client.commands.instance.port_forwarder.typer.Exit", side_effect=SystemExit), @@ -142,7 +142,7 @@ async def test_create_port(mock_auth_setup): mock_client_class = mock_auth_setup["mock_client_class"] with ( - patch("aleph_client.commands.instance.port_forwarder._load_account", mock_load_account), + patch("aleph_client.commands.instance.port_forwarder.load_account", mock_load_account), patch("aleph_client.commands.instance.port_forwarder.AuthenticatedAlephHttpClient", mock_client_class), patch("aleph_client.commands.instance.port_forwarder.typer.echo") as mock_echo, ): @@ -177,7 +177,7 @@ async def test_update_port(mock_auth_setup): mock_client.port_forwarder.get_ports.return_value = mock_existing_ports with ( - patch("aleph_client.commands.instance.port_forwarder._load_account", mock_load_account), + patch("aleph_client.commands.instance.port_forwarder.load_account", mock_load_account), patch("aleph_client.commands.instance.port_forwarder.AlephHttpClient", mock_client_class), patch("aleph_client.commands.instance.port_forwarder.AuthenticatedAlephHttpClient", mock_client_class), patch("aleph_client.commands.instance.port_forwarder.typer.echo") as mock_echo, @@ -211,7 +211,7 @@ async def test_delete_port(mock_auth_setup): mock_client.port_forwarder.get_ports.return_value = mock_existing_ports with ( - patch("aleph_client.commands.instance.port_forwarder._load_account", mock_load_account), + patch("aleph_client.commands.instance.port_forwarder.load_account", mock_load_account), patch("aleph_client.commands.instance.port_forwarder.AlephHttpClient", mock_client_class), patch("aleph_client.commands.instance.port_forwarder.AuthenticatedAlephHttpClient", mock_client_class), patch("aleph_client.commands.instance.port_forwarder.typer.echo") as mock_echo, @@ -236,7 +236,7 @@ async def test_delete_port(mock_auth_setup): mock_client.port_forwarder.delete_ports.reset_mock() with ( - patch("aleph_client.commands.instance.port_forwarder._load_account", mock_load_account), + patch("aleph_client.commands.instance.port_forwarder.load_account", mock_load_account), patch("aleph_client.commands.instance.port_forwarder.AlephHttpClient", mock_client_class), patch("aleph_client.commands.instance.port_forwarder.AuthenticatedAlephHttpClient", mock_client_class), patch("aleph_client.commands.instance.port_forwarder.typer.echo") as mock_echo, @@ -268,7 +268,7 @@ async def test_delete_port_last_port(mock_auth_setup): mock_client.port_forwarder.update_ports = None with ( - patch("aleph_client.commands.instance.port_forwarder._load_account", mock_load_account), + patch("aleph_client.commands.instance.port_forwarder.load_account", mock_load_account), patch("aleph_client.commands.instance.port_forwarder.AlephHttpClient", mock_client_class), patch("aleph_client.commands.instance.port_forwarder.AuthenticatedAlephHttpClient", mock_client_class), patch("aleph_client.commands.instance.port_forwarder.typer.echo") as mock_echo, @@ -310,7 +310,7 @@ async def test_refresh_port(mock_auth_setup): mock_client.instance.get_instance_allocation_info.return_value = (None, mock_allocation) with ( - patch("aleph_client.commands.instance.port_forwarder._load_account", mock_load_account), + patch("aleph_client.commands.instance.port_forwarder.load_account", mock_load_account), patch("aleph_client.commands.instance.port_forwarder.AuthenticatedAlephHttpClient", mock_client_class), patch("aleph_client.commands.instance.port_forwarder.typer.echo") as mock_echo, ): @@ -340,7 +340,7 @@ async def test_refresh_port_no_allocation(mock_auth_setup): mock_client.instance.get_instance_allocation_info.return_value = (None, None) with ( - patch("aleph_client.commands.instance.port_forwarder._load_account", mock_load_account), + patch("aleph_client.commands.instance.port_forwarder.load_account", mock_load_account), patch("aleph_client.commands.instance.port_forwarder.AuthenticatedAlephHttpClient", mock_client_class), patch("aleph_client.commands.instance.port_forwarder.typer.echo") as mock_echo, patch("aleph_client.commands.instance.port_forwarder.typer.Exit", side_effect=SystemExit), @@ -376,7 +376,7 @@ async def test_refresh_port_scheduler_allocation(mock_auth_setup): mock_client.instance.get_instance_allocation_info.return_value = (None, mock_allocation) with ( - patch("aleph_client.commands.instance.port_forwarder._load_account", mock_load_account), + patch("aleph_client.commands.instance.port_forwarder.load_account", mock_load_account), patch("aleph_client.commands.instance.port_forwarder.AuthenticatedAlephHttpClient", mock_client_class), patch("aleph_client.commands.instance.port_forwarder.typer.echo") as mock_echo, ): @@ -415,7 +415,7 @@ async def test_non_processed_message_statuses(): mock_http_client.port_forwarder.get_ports = AsyncMock(return_value=mock_existing_ports) with ( - patch("aleph_client.commands.instance.port_forwarder._load_account", mock_load_account), + patch("aleph_client.commands.instance.port_forwarder.load_account", mock_load_account), patch("aleph_client.commands.instance.port_forwarder.AlephHttpClient", mock_http_client_class), patch("aleph_client.commands.instance.port_forwarder.AuthenticatedAlephHttpClient", mock_auth_client_class), patch("aleph_client.commands.instance.port_forwarder.typer.echo") as mock_echo, @@ -432,7 +432,7 @@ async def test_non_processed_message_statuses(): mock_echo.reset_mock() with ( - patch("aleph_client.commands.instance.port_forwarder._load_account", mock_load_account), + patch("aleph_client.commands.instance.port_forwarder.load_account", mock_load_account), patch("aleph_client.commands.instance.port_forwarder.AlephHttpClient", mock_http_client_class), patch("aleph_client.commands.instance.port_forwarder.AuthenticatedAlephHttpClient", mock_auth_client_class), patch("aleph_client.commands.instance.port_forwarder.typer.echo") as mock_echo, @@ -450,7 +450,7 @@ async def test_non_processed_message_statuses(): mock_echo.reset_mock() with ( - patch("aleph_client.commands.instance.port_forwarder._load_account", mock_load_account), + patch("aleph_client.commands.instance.port_forwarder.load_account", mock_load_account), patch("aleph_client.commands.instance.port_forwarder.AlephHttpClient", mock_http_client_class), patch("aleph_client.commands.instance.port_forwarder.AuthenticatedAlephHttpClient", mock_auth_client_class), patch("aleph_client.commands.instance.port_forwarder.typer.echo") as mock_echo, From 677799d1198c9ba544f5a3e89b1e06bca4e97ccf Mon Sep 17 00:00:00 2001 From: 1yam Date: Fri, 31 Oct 2025 11:23:36 +0100 Subject: [PATCH 09/19] Feature: aleph account init to create based config for new user using ledger --- src/aleph_client/commands/account.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/aleph_client/commands/account.py b/src/aleph_client/commands/account.py index 1efcdb7b..f10c73f0 100644 --- a/src/aleph_client/commands/account.py +++ b/src/aleph_client/commands/account.py @@ -75,6 +75,26 @@ def decode_private_key(private_key: str, encoding: KeyEncoding) -> bytes: raise ValueError(INVALID_KEY_FORMAT.format(encoding)) +@app.command() +async def init(): + """Initialize base configuration file.""" + config = MainConfiguration(path=None, chain=Chain.ETH) + + # Create the parent directory and private-keys subdirectory if they don't exist + if settings.CONFIG_HOME: + settings.CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True) + private_keys_dir = Path(settings.CONFIG_HOME, "private-keys") + private_keys_dir.mkdir(parents=True, exist_ok=True) + save_main_configuration(settings.CONFIG_FILE, config) + + typer.echo( + "Configuration initialized.\n" + "Next steps:\n" + " • Run `aleph account create` to add a private key, or\n" + " • Run `aleph account config --account-type external` to add a ledger account." + ) + + @app.command() async def create( private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = None, From 725e042f1e71bd33fe62d2c263ddd9dd9ea2fac3 Mon Sep 17 00:00:00 2001 From: 1yam Date: Fri, 31 Oct 2025 11:24:58 +0100 Subject: [PATCH 10/19] fix: aleph account list now handle ledger device --- src/aleph_client/commands/account.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/aleph_client/commands/account.py b/src/aleph_client/commands/account.py index f10c73f0..448e88f6 100644 --- a/src/aleph_client/commands/account.py +++ b/src/aleph_client/commands/account.py @@ -403,6 +403,9 @@ async def list_accounts(): if config and config.path: active_chain = config.chain table.add_row(config.path.stem, str(config.path), "[bold green]*[/bold green]") + elif config and config.address and config.type == AccountType.EXTERNAL: + active_chain = config.chain + table.add_row(f"Ledger ({config.address[:8]}...)", "External (Ledger)", "[bold green]*[/bold green]") else: console.print( "[red]No private key path selected in the config file.[/red]\nTo set it up, use: [bold " @@ -414,13 +417,27 @@ async def list_accounts(): if key_file.stem != "default": table.add_row(key_file.stem, str(key_file), "[bold red]-[/bold red]") + # Try to detect Ledger devices + try: + ledger_accounts = LedgerETHAccount.get_accounts() + if ledger_accounts: + for idx, ledger_acc in enumerate(ledger_accounts): + is_active = config and config.type == AccountType.EXTERNAL and config.address == ledger_acc.address + status = "[bold green]*[/bold green]" if is_active else "[bold red]-[/bold red]" + table.add_row(f"Ledger #{idx}", f"{ledger_acc.address}", status) + except Exception: + logger.info("No ledger detected") + hold_chains = [*get_chains_with_holding(), Chain.SOL.value] payg_chains = get_chains_with_super_token() active_address = None - if config and config.path and active_chain: - account = _load_account(private_key_path=config.path, chain=active_chain) - active_address = account.get_address() + if config and active_chain: + if config.path: + account = _load_account(private_key_path=config.path, chain=active_chain) + active_address = account.get_address() + elif config.address and config.type == AccountType.EXTERNAL: + active_address = config.address console.print( "🌐 [bold italic blue]Chain Infos[/bold italic blue] 🌐\n" From 051b0e48c46cb555438d1a0689acc2c7d8867810 Mon Sep 17 00:00:00 2001 From: 1yam Date: Fri, 31 Oct 2025 11:25:25 +0100 Subject: [PATCH 11/19] fix: aleph account address now handle ledger device --- src/aleph_client/commands/account.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/aleph_client/commands/account.py b/src/aleph_client/commands/account.py index 448e88f6..c974328d 100644 --- a/src/aleph_client/commands/account.py +++ b/src/aleph_client/commands/account.py @@ -178,12 +178,25 @@ def display_active_address( """ Display your public address(es). """ + # For regular accounts and Ledger accounts + evm_account = load_account(private_key, private_key_file, chain=Chain.ETH) + evm_address = evm_account.get_address() - evm_address = load_account(private_key, private_key_file, chain=Chain.ETH).get_address() - sol_address = load_account(private_key, private_key_file, chain=Chain.SOL).get_address() + # For Ledger accounts, the SOL address might not be available + try: + sol_address = load_account(private_key, private_key_file, chain=Chain.SOL).get_address() + except Exception: + sol_address = "Not available (using Ledger device)" + + # Detect if it's a Ledger account + config_file_path = Path(settings.CONFIG_FILE) + config = load_main_configuration(config_file_path) + account_type = config.type if config else None + + account_type_str = " (Ledger)" if account_type == AccountType.EXTERNAL else "" console.print( - "✉ [bold italic blue]Addresses for Active Account[/bold italic blue] ✉\n\n" + f"✉ [bold italic blue]Addresses for Active Account{account_type_str}[/bold italic blue] ✉\n\n" f"[italic]EVM[/italic]: [cyan]{evm_address}[/cyan]\n" f"[italic]SOL[/italic]: [magenta]{sol_address}[/magenta]\n" ) From 55308d645826fd6e550cc16157ca1583940d187e Mon Sep 17 00:00:00 2001 From: 1yam Date: Fri, 31 Oct 2025 11:26:55 +0100 Subject: [PATCH 12/19] fix: aleph account export-private-key handle ledger case (can't export private key) --- src/aleph_client/commands/account.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/aleph_client/commands/account.py b/src/aleph_client/commands/account.py index c974328d..0774ce45 100644 --- a/src/aleph_client/commands/account.py +++ b/src/aleph_client/commands/account.py @@ -261,7 +261,16 @@ def export_private_key( """ Display your private key. """ + # Check if we're using a Ledger account + config_file_path = Path(settings.CONFIG_FILE) + config = load_main_configuration(config_file_path) + + if config and config.type == AccountType.EXTERNAL: + typer.secho("Cannot export private key from a Ledger hardware wallet", fg=RED) + typer.secho("The private key remains securely stored on your Ledger device", fg=RED) + raise typer.Exit(code=1) + # Normal private key handling if private_key: private_key_file = None elif private_key_file and not private_key_file.exists(): From d433012991c17c332f29e1e1eb182bfe1260f64d Mon Sep 17 00:00:00 2001 From: 1yam Date: Mon, 3 Nov 2025 12:06:22 +0100 Subject: [PATCH 13/19] Feature: missing unit test for ledger --- tests/unit/test_commands.py | 163 +++++++++++++++++++++++++++++++++++- 1 file changed, 160 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index 308129ef..f32d5e24 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -6,7 +6,7 @@ import pytest from aleph.sdk.chains.ethereum import ETHAccount -from aleph.sdk.conf import settings +from aleph.sdk.conf import AccountType, MainConfiguration, settings from aleph.sdk.exceptions import ( ForgottenMessageError, MessageNotFoundError, @@ -14,7 +14,7 @@ ) from aleph.sdk.query.responses import MessagesResponse from aleph.sdk.types import StorageEnum, StoredContent -from aleph_message.models import PostMessage, StoreMessage +from aleph_message.models import Chain, PostMessage, StoreMessage from typer.testing import CliRunner from aleph_client.__main__ import app @@ -202,12 +202,86 @@ def test_account_import_sol(env_files): assert new_key != old_key -def test_account_address(env_files): +def test_account_init(env_files): + """Test the new account init command.""" + settings.CONFIG_FILE = env_files[1] + + # First ensure the config directory exists but is empty + config_dir = env_files[1].parent + # Removing unused variable + + with ( + patch("aleph.sdk.conf.settings.CONFIG_FILE", env_files[1]), + patch("aleph.sdk.conf.settings.CONFIG_HOME", str(config_dir)), + patch("pathlib.Path.mkdir") as mock_mkdir, + ): + + result = runner.invoke(app, ["account", "init"]) + assert result.exit_code == 0 + assert "Configuration initialized." in result.stdout + assert "Run `aleph account create`" in result.stdout + assert "Run `aleph account config --account-type external`" in result.stdout + + # Check that directories were created + mock_mkdir.assert_any_call(parents=True, exist_ok=True) + + +@patch("aleph.sdk.wallets.ledger.LedgerETHAccount.get_accounts") +def test_account_address(mock_get_accounts, env_files): settings.CONFIG_FILE = env_files[1] result = runner.invoke(app, ["account", "address", "--private-key-file", str(env_files[0])]) assert result.exit_code == 0 assert result.stdout.startswith("✉ Addresses for Active Account ✉\n\nEVM: 0x") + # Test with ledger device + mock_ledger_account = MagicMock() + mock_ledger_account.address = "0xdeadbeef1234567890123456789012345678beef" + mock_ledger_account.get_address.return_value = "0xdeadbeef1234567890123456789012345678beef" + mock_get_accounts.return_value = [mock_ledger_account] + + # Create a ledger config + ledger_config = MainConfiguration( + path=None, chain=Chain.ETH, type=AccountType.EXTERNAL, address=mock_ledger_account.address + ) + + with patch("aleph_client.commands.account.load_main_configuration", return_value=ledger_config): + with patch( + "aleph_client.commands.account.load_account", + side_effect=lambda _, __, chain: ( + mock_ledger_account if chain == Chain.ETH else Exception("Ledger doesn't support SOL") + ), + ): + result = runner.invoke(app, ["account", "address"]) + assert result.exit_code == 0 + assert result.stdout.startswith("✉ Addresses for Active Account (Ledger) ✉\n\nEVM: 0x") + + +def test_account_init_with_isolated_filesystem(): + """Test the new account init command that creates base configuration for new users.""" + # Set up a test directory for config + with runner.isolated_filesystem(): + config_dir = Path("test_config") + config_file = config_dir / "config.json" + + # Create the directory first + config_dir.mkdir(parents=True, exist_ok=True) + + with ( + patch("aleph.sdk.conf.settings.CONFIG_FILE", config_file), + patch("aleph.sdk.conf.settings.CONFIG_HOME", str(config_dir)), + ): + + result = runner.invoke(app, ["account", "init"]) + + # Verify command executed successfully + assert result.exit_code == 0 + assert "Configuration initialized." in result.stdout + assert "Run `aleph account create`" in result.stdout + assert "Run `aleph account config --account-type external`" in result.stdout + + # Verify the config file was created + assert config_file.exists() + def test_account_chain(env_files): settings.CONFIG_FILE = env_files[1] @@ -236,6 +310,22 @@ def test_account_export_private_key(env_files): assert result.stdout.startswith("⚠️ Private Keys for Active Account ⚠️\n\nEVM: 0x") +def test_account_export_private_key_ledger(): + """Test that export-private-key fails for Ledger devices.""" + # Create a ledger config + ledger_config = MainConfiguration( + path=None, chain=Chain.ETH, type=AccountType.EXTERNAL, address="0xdeadbeef1234567890123456789012345678beef" + ) + + with patch("aleph_client.commands.account.load_main_configuration", return_value=ledger_config): + result = runner.invoke(app, ["account", "export-private-key"]) + + # Command should fail with appropriate message + assert result.exit_code == 1 + assert "Cannot export private key from a Ledger hardware wallet" in result.stdout + assert "The private key remains securely stored on your Ledger device" in result.stdout + + def test_account_list(env_files): settings.CONFIG_FILE = env_files[1] result = runner.invoke(app, ["account", "list"]) @@ -243,6 +333,43 @@ def test_account_list(env_files): assert result.stdout.startswith("🌐 Chain Infos 🌐") +@patch("aleph.sdk.wallets.ledger.LedgerETHAccount.get_accounts") +def test_account_list_with_ledger(mock_get_accounts): + """Test that account list shows Ledger devices when available.""" + # Create mock Ledger accounts + mock_account1 = MagicMock() + mock_account1.address = "0xdeadbeef1234567890123456789012345678beef" + mock_account2 = MagicMock() + mock_account2.address = "0xcafebabe5678901234567890123456789012cafe" + mock_get_accounts.return_value = [mock_account1, mock_account2] + + # Test with no configuration first + with patch("aleph_client.commands.account.load_main_configuration", return_value=None): + result = runner.invoke(app, ["account", "list"]) + assert result.exit_code == 0 + + # Check that the ledger accounts are listed + assert "Ledger #0" in result.stdout + assert "Ledger #1" in result.stdout + assert mock_account1.address in result.stdout + assert mock_account2.address in result.stdout + + # Test with a ledger account that's active in configuration + ledger_config = MainConfiguration( + path=None, chain=Chain.ETH, type=AccountType.EXTERNAL, address=mock_account1.address + ) + + with patch("aleph_client.commands.account.load_main_configuration", return_value=ledger_config): + result = runner.invoke(app, ["account", "list"]) + assert result.exit_code == 0 + + # Check that the active ledger account is marked + assert "Ledger" in result.stdout + assert mock_account1.address in result.stdout + # Just check for asterisk since rich formatting tags may not be visible in test output + assert "*" in result.stdout + + def test_account_sign_bytes(env_files): settings.CONFIG_FILE = env_files[1] result = runner.invoke(app, ["account", "sign-bytes", "--message", "test", "--chain", "ETH"]) @@ -378,6 +505,36 @@ def test_account_config(env_files): assert result.stdout.startswith("New Default Configuration: ") +@patch("aleph.sdk.wallets.ledger.LedgerETHAccount.get_accounts") +def test_account_config_with_ledger(mock_get_accounts): + """Test configuring account with a Ledger device.""" + # Create mock Ledger accounts + mock_account1 = MagicMock() + mock_account1.address = "0xdeadbeef1234567890123456789012345678beef" + mock_account2 = MagicMock() + mock_account2.address = "0xcafebabe5678901234567890123456789012cafe" + mock_get_accounts.return_value = [mock_account1, mock_account2] + + # Create a temporary config file + with runner.isolated_filesystem(): + config_dir = Path("test_config") + config_dir.mkdir() + config_file = config_dir / "config.json" + + with ( + patch("aleph.sdk.conf.settings.CONFIG_FILE", config_file), + patch("aleph.sdk.conf.settings.CONFIG_HOME", str(config_dir)), + patch("aleph_client.commands.account.Prompt.ask", return_value="1"), + patch("aleph_client.commands.account.yes_no_input", return_value=True), + ): + + result = runner.invoke(app, ["account", "config", "--account-type", "external", "--chain", "ETH"]) + + assert result.exit_code == 0 + assert "New Default Configuration" in result.stdout + assert mock_account1.address in result.stdout + + def test_message_get(mocker, store_message_fixture): # Use subprocess to avoid border effects between tests caused by the initialisation # of the aiohttp client session out of an async context in the SDK. This avoids From def2144d9bd9e50d57880aeec922b3e7a19a7e23 Mon Sep 17 00:00:00 2001 From: 1yam Date: Tue, 4 Nov 2025 13:35:27 +0100 Subject: [PATCH 14/19] Fix: handle common error using ledger (OsError / LedgerError) --- src/aleph_client/commands/account.py | 45 +++++++++++++++++----------- src/aleph_client/utils.py | 8 ++++- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/aleph_client/commands/account.py b/src/aleph_client/commands/account.py index 0774ce45..7065e341 100644 --- a/src/aleph_client/commands/account.py +++ b/src/aleph_client/commands/account.py @@ -28,6 +28,7 @@ from aleph.sdk.utils import bytes_from_hex, displayable_amount from aleph.sdk.wallets.ledger import LedgerETHAccount from aleph_message.models import Chain +from ledgereth.exceptions import LedgerError from rich import box from rich.console import Console from rich.panel import Panel @@ -589,24 +590,32 @@ async def configure( "[bright_cyan]Loading External keys.[/bright_cyan] [yellow]Do you want to import from Ledger?[/yellow]", default="y", ): - accounts = LedgerETHAccount.get_accounts() - account_addresses = [acc.address for acc in accounts] - - console.print("[bold cyan]Available addresses on Ledger:[/bold cyan]") - for idx, account_address in enumerate(account_addresses, start=1): - console.print(f"[{idx}] {account_address}") - - key_choice = Prompt.ask("Choose a address by index") - if key_choice.isdigit(): - key_index = int(key_choice) - 1 - selected_address = account_addresses[key_index] - - if not selected_address: - typer.secho("No valid address selected.", fg=typer.colors.RED) - raise typer.Exit() - - address = selected_address - account_type = AccountType.EXTERNAL + try: + + accounts = LedgerETHAccount.get_accounts() + account_addresses = [acc.address for acc in accounts] + + console.print("[bold cyan]Available addresses on Ledger:[/bold cyan]") + for idx, account_address in enumerate(account_addresses, start=1): + console.print(f"[{idx}] {account_address}") + + key_choice = Prompt.ask("Choose a address by index") + if key_choice.isdigit(): + key_index = int(key_choice) - 1 + selected_address = account_addresses[key_index] + + if not selected_address: + typer.secho("No valid address selected.", fg=typer.colors.RED) + raise typer.Exit() + + address = selected_address + account_type = AccountType.EXTERNAL + except LedgerError as e: + logger.warning(f"Ledger Error : {e.message}") + raise typer.Exit(code=1) from e + except OSError as err: + logger.warning("Please ensure Udev rules are set to use Ledger") + raise typer.Exit(code=1) from err else: typer.secho("No private key file provided or found.", fg=typer.colors.RED) raise typer.Exit() diff --git a/src/aleph_client/utils.py b/src/aleph_client/utils.py index 560ab3ac..0f0d6ab0 100644 --- a/src/aleph_client/utils.py +++ b/src/aleph_client/utils.py @@ -30,6 +30,7 @@ from aleph_message.models import Chain from aleph_message.models.base import MessageType from aleph_message.models.execution.base import Encoding +from ledgereth.exceptions import LedgerError # Type alias for account types AlephAccount = Union[AccountFromPrivateKey, LedgerETHAccount] @@ -232,6 +233,11 @@ def load_account( chain = config.chain if config and config.type and config.type == AccountType.EXTERNAL: - return _load_account(None, None, chain=chain) + try: + return _load_account(None, None, chain=chain) + except LedgerError as err: + raise typer.Exit(code=1) from err + except OSError as err: + raise typer.Exit(code=1) from err else: return _load_account(private_key_str, private_key_file, chain=chain) From 46bbbdf29ecd86acef45794422892b7d141919c1 Mon Sep 17 00:00:00 2001 From: 1yam Date: Fri, 7 Nov 2025 14:41:48 +0100 Subject: [PATCH 15/19] Fix: handle change from account on sdk side --- src/aleph_client/commands/account.py | 40 +++++++++++++++------------- src/aleph_client/utils.py | 14 ++++------ tests/unit/test_commands.py | 15 +++++------ 3 files changed, 33 insertions(+), 36 deletions(-) diff --git a/src/aleph_client/commands/account.py b/src/aleph_client/commands/account.py index 7065e341..155e00d4 100644 --- a/src/aleph_client/commands/account.py +++ b/src/aleph_client/commands/account.py @@ -25,6 +25,7 @@ get_chains_with_super_token, get_compatible_chains, ) +from aleph.sdk.types import AccountFromPrivateKey from aleph.sdk.utils import bytes_from_hex, displayable_amount from aleph.sdk.wallets.ledger import LedgerETHAccount from aleph_message.models import Chain @@ -45,12 +46,7 @@ validated_prompt, yes_no_input, ) -from aleph_client.utils import ( - AlephAccount, - AsyncTyper, - list_unlinked_keys, - load_account, -) +from aleph_client.utils import AsyncTyper, list_unlinked_keys, load_account logger = logging.getLogger(__name__) app = AsyncTyper(no_args_is_help=True) @@ -194,7 +190,7 @@ def display_active_address( config = load_main_configuration(config_file_path) account_type = config.type if config else None - account_type_str = " (Ledger)" if account_type == AccountType.EXTERNAL else "" + account_type_str = " (Ledger)" if account_type == AccountType.HARDWARE else "" console.print( f"✉ [bold italic blue]Addresses for Active Account{account_type_str}[/bold italic blue] ✉\n\n" @@ -266,7 +262,7 @@ def export_private_key( config_file_path = Path(settings.CONFIG_FILE) config = load_main_configuration(config_file_path) - if config and config.type == AccountType.EXTERNAL: + if config and config.type == AccountType.HARDWARE: typer.secho("Cannot export private key from a Ledger hardware wallet", fg=RED) typer.secho("The private key remains securely stored on your Ledger device", fg=RED) raise typer.Exit(code=1) @@ -278,9 +274,15 @@ def export_private_key( typer.secho("No private key available", fg=RED) raise typer.Exit(code=1) - evm_pk = _load_account(private_key, private_key_file, chain=Chain.ETH).export_private_key() - sol_pk = _load_account(private_key, private_key_file, chain=Chain.SOL).export_private_key() + eth_account = _load_account(private_key, private_key_file, chain=Chain.ETH) + sol_account = _load_account(private_key, private_key_file, chain=Chain.SOL) + evm_pk = "Not Available" + if isinstance(eth_account, AccountFromPrivateKey): + evm_pk = eth_account.export_private_key() + sol_pk = "Not Available" + if isinstance(sol_account, AccountFromPrivateKey): + sol_pk = sol_account.export_private_key() console.print( "⚠️ [bold italic red]Private Keys for Active Account[/bold italic red] ⚠️\n\n" f"[italic]EVM[/italic]: [cyan]{evm_pk}[/cyan]\n" @@ -303,7 +305,7 @@ def sign_bytes( setup_logging(debug) - account: AlephAccount = load_account(private_key, private_key_file, chain=chain) + account = load_account(private_key, private_key_file, chain=chain) if not message: message = input_multiline() @@ -338,7 +340,7 @@ async def balance( chain: Annotated[Optional[Chain], typer.Option(help=help_strings.ADDRESS_CHAIN)] = None, ): """Display your ALEPH balance and basic voucher information.""" - account: AlephAccount = load_account(private_key, private_key_file, chain=chain) + account = load_account(private_key, private_key_file, chain=chain) if account and not address: address = account.get_address() @@ -426,7 +428,7 @@ async def list_accounts(): if config and config.path: active_chain = config.chain table.add_row(config.path.stem, str(config.path), "[bold green]*[/bold green]") - elif config and config.address and config.type == AccountType.EXTERNAL: + elif config and config.address and config.type == AccountType.HARDWARE: active_chain = config.chain table.add_row(f"Ledger ({config.address[:8]}...)", "External (Ledger)", "[bold green]*[/bold green]") else: @@ -445,7 +447,7 @@ async def list_accounts(): ledger_accounts = LedgerETHAccount.get_accounts() if ledger_accounts: for idx, ledger_acc in enumerate(ledger_accounts): - is_active = config and config.type == AccountType.EXTERNAL and config.address == ledger_acc.address + is_active = config and config.type == AccountType.HARDWARE and config.address == ledger_acc.address status = "[bold green]*[/bold green]" if is_active else "[bold red]-[/bold red]" table.add_row(f"Ledger #{idx}", f"{ledger_acc.address}", status) except Exception: @@ -459,7 +461,7 @@ async def list_accounts(): if config.path: account = _load_account(private_key_path=config.path, chain=active_chain) active_address = account.get_address() - elif config.address and config.type == AccountType.EXTERNAL: + elif config.address and config.type == AccountType.HARDWARE: active_address = config.address console.print( @@ -558,7 +560,7 @@ async def configure( if private_key_file: pass elif not account_type or ( - account_type == AccountType.INTERNAL and config and hasattr(config, "path") and Path(config.path).exists() + account_type == AccountType.IMPORTED and config and hasattr(config, "path") and Path(config.path).exists() ): if not yes_no_input( f"Active private key file: [bright_cyan]{config.path}[/bright_cyan]\n[yellow]Keep current active private " @@ -585,7 +587,7 @@ async def configure( else: # No change private_key_file = Path(config.path) - if not private_key_file and account_type == AccountType.EXTERNAL: + if not private_key_file and account_type == AccountType.HARDWARE: if yes_no_input( "[bright_cyan]Loading External keys.[/bright_cyan] [yellow]Do you want to import from Ledger?[/yellow]", default="y", @@ -609,7 +611,7 @@ async def configure( raise typer.Exit() address = selected_address - account_type = AccountType.EXTERNAL + account_type = AccountType.HARDWARE except LedgerError as e: logger.warning(f"Ledger Error : {e.message}") raise typer.Exit(code=1) from e @@ -643,7 +645,7 @@ async def configure( raise typer.Exit() if not account_type: - account_type = AccountType.INTERNAL + account_type = AccountType.IMPORTED try: config = MainConfiguration(path=private_key_file, chain=chain, address=address, type=account_type) diff --git a/src/aleph_client/utils.py b/src/aleph_client/utils.py index 0f0d6ab0..4bd0f937 100644 --- a/src/aleph_client/utils.py +++ b/src/aleph_client/utils.py @@ -18,23 +18,19 @@ import aiohttp import typer from aiohttp import ClientSession -from aleph.sdk.account import _load_account +from aleph.sdk.account import AccountLike, _load_account from aleph.sdk.conf import ( AccountType, MainConfiguration, load_main_configuration, settings, ) -from aleph.sdk.types import AccountFromPrivateKey, GenericMessage -from aleph.sdk.wallets.ledger import LedgerETHAccount +from aleph.sdk.types import GenericMessage from aleph_message.models import Chain from aleph_message.models.base import MessageType from aleph_message.models.execution.base import Encoding from ledgereth.exceptions import LedgerError -# Type alias for account types -AlephAccount = Union[AccountFromPrivateKey, LedgerETHAccount] - logger = logging.getLogger(__name__) try: @@ -206,11 +202,11 @@ def cached_async_function(*args, **kwargs): def load_account( private_key_str: Optional[str], private_key_file: Optional[Path], chain: Optional[Chain] = None -) -> AlephAccount: +) -> AccountLike: """ Two Case Possible - Account from private key - - External account (ledger) + - Hardware account (ledger) We first try to load configurations, if no configurations we fallback to private_key_str / private_key_file. """ @@ -232,7 +228,7 @@ def load_account( if not chain and config: chain = config.chain - if config and config.type and config.type == AccountType.EXTERNAL: + if config and config.type and config.type == AccountType.HARDWARE: try: return _load_account(None, None, chain=chain) except LedgerError as err: diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index f32d5e24..6b3545b4 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -226,7 +226,7 @@ def test_account_init(env_files): mock_mkdir.assert_any_call(parents=True, exist_ok=True) -@patch("aleph.sdk.wallets.ledger.LedgerETHAccount.get_accounts") +@patch("aleph.sdk.wallets.ledger.ethereum.LedgerETHAccount.get_accounts") def test_account_address(mock_get_accounts, env_files): settings.CONFIG_FILE = env_files[1] result = runner.invoke(app, ["account", "address", "--private-key-file", str(env_files[0])]) @@ -241,7 +241,7 @@ def test_account_address(mock_get_accounts, env_files): # Create a ledger config ledger_config = MainConfiguration( - path=None, chain=Chain.ETH, type=AccountType.EXTERNAL, address=mock_ledger_account.address + path=None, chain=Chain.ETH, type=AccountType.HARDWARE, address=mock_ledger_account.address ) with patch("aleph_client.commands.account.load_main_configuration", return_value=ledger_config): @@ -314,7 +314,7 @@ def test_account_export_private_key_ledger(): """Test that export-private-key fails for Ledger devices.""" # Create a ledger config ledger_config = MainConfiguration( - path=None, chain=Chain.ETH, type=AccountType.EXTERNAL, address="0xdeadbeef1234567890123456789012345678beef" + path=None, chain=Chain.ETH, type=AccountType.HARDWARE, address="0xdeadbeef1234567890123456789012345678beef" ) with patch("aleph_client.commands.account.load_main_configuration", return_value=ledger_config): @@ -333,7 +333,7 @@ def test_account_list(env_files): assert result.stdout.startswith("🌐 Chain Infos 🌐") -@patch("aleph.sdk.wallets.ledger.LedgerETHAccount.get_accounts") +@patch("aleph.sdk.wallets.ledger.ethereum.LedgerETHAccount.get_accounts") def test_account_list_with_ledger(mock_get_accounts): """Test that account list shows Ledger devices when available.""" # Create mock Ledger accounts @@ -356,7 +356,7 @@ def test_account_list_with_ledger(mock_get_accounts): # Test with a ledger account that's active in configuration ledger_config = MainConfiguration( - path=None, chain=Chain.ETH, type=AccountType.EXTERNAL, address=mock_account1.address + path=None, chain=Chain.ETH, type=AccountType.HARDWARE, address=mock_account1.address ) with patch("aleph_client.commands.account.load_main_configuration", return_value=ledger_config): @@ -500,12 +500,11 @@ def test_account_vouchers_no_vouchers(mocker, env_files): def test_account_config(env_files): settings.CONFIG_FILE = env_files[1] result = runner.invoke(app, ["account", "config", "--private-key-file", str(env_files[0]), "--chain", "ETH"]) - print(result.output) assert result.exit_code == 0 assert result.stdout.startswith("New Default Configuration: ") -@patch("aleph.sdk.wallets.ledger.LedgerETHAccount.get_accounts") +@patch("aleph.sdk.wallets.ledger.ethereum.LedgerETHAccount.get_accounts") def test_account_config_with_ledger(mock_get_accounts): """Test configuring account with a Ledger device.""" # Create mock Ledger accounts @@ -528,7 +527,7 @@ def test_account_config_with_ledger(mock_get_accounts): patch("aleph_client.commands.account.yes_no_input", return_value=True), ): - result = runner.invoke(app, ["account", "config", "--account-type", "external", "--chain", "ETH"]) + result = runner.invoke(app, ["account", "config", "--account-type", "hardware", "--chain", "ETH"]) assert result.exit_code == 0 assert "New Default Configuration" in result.stdout From 96ee9cd41962a4ace499748491b7563a57d00e89 Mon Sep 17 00:00:00 2001 From: 1yam Date: Fri, 7 Nov 2025 17:12:05 +0100 Subject: [PATCH 16/19] Fix: remove init commands and ensure that config file/folder and subfolder are created --- src/aleph_client/commands/account.py | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/aleph_client/commands/account.py b/src/aleph_client/commands/account.py index 155e00d4..b9226c68 100644 --- a/src/aleph_client/commands/account.py +++ b/src/aleph_client/commands/account.py @@ -72,26 +72,6 @@ def decode_private_key(private_key: str, encoding: KeyEncoding) -> bytes: raise ValueError(INVALID_KEY_FORMAT.format(encoding)) -@app.command() -async def init(): - """Initialize base configuration file.""" - config = MainConfiguration(path=None, chain=Chain.ETH) - - # Create the parent directory and private-keys subdirectory if they don't exist - if settings.CONFIG_HOME: - settings.CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True) - private_keys_dir = Path(settings.CONFIG_HOME, "private-keys") - private_keys_dir.mkdir(parents=True, exist_ok=True) - save_main_configuration(settings.CONFIG_FILE, config) - - typer.echo( - "Configuration initialized.\n" - "Next steps:\n" - " • Run `aleph account create` to add a private key, or\n" - " • Run `aleph account config --account-type external` to add a ledger account." - ) - - @app.command() async def create( private_key: Annotated[Optional[str], typer.Option(help=help_strings.PRIVATE_KEY)] = None, @@ -542,6 +522,11 @@ async def configure( ): """Configure current private key file and active chain (default selection)""" + if settings.CONFIG_HOME: + settings.CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True) # Ensure config file is created + private_keys_dir = Path(settings.CONFIG_HOME, "private-keys") # ensure private-keys folder created + private_keys_dir.mkdir(parents=True, exist_ok=True) + unlinked_keys, config = await list_unlinked_keys() # Fixes private key file path From edd5f07b20c5d4dd63b2b2da94dd2aca0640b02d Mon Sep 17 00:00:00 2001 From: 1yam Date: Fri, 7 Nov 2025 17:13:05 +0100 Subject: [PATCH 17/19] Fix: AccountLike renamed to AccountTypes --- src/aleph_client/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/aleph_client/utils.py b/src/aleph_client/utils.py index 4bd0f937..997c0fa2 100644 --- a/src/aleph_client/utils.py +++ b/src/aleph_client/utils.py @@ -18,7 +18,7 @@ import aiohttp import typer from aiohttp import ClientSession -from aleph.sdk.account import AccountLike, _load_account +from aleph.sdk.account import AccountTypes, _load_account from aleph.sdk.conf import ( AccountType, MainConfiguration, @@ -202,7 +202,7 @@ def cached_async_function(*args, **kwargs): def load_account( private_key_str: Optional[str], private_key_file: Optional[Path], chain: Optional[Chain] = None -) -> AccountLike: +) -> AccountTypes: """ Two Case Possible - Account from private key From ffc634e1916be33be850f8beebf2e2012736b566 Mon Sep 17 00:00:00 2001 From: 1yam Date: Fri, 7 Nov 2025 17:23:11 +0100 Subject: [PATCH 18/19] fix: AlephAccount should bez AccountTypes --- src/aleph_client/commands/aggregate.py | 16 +++++++-------- src/aleph_client/commands/credit.py | 6 +++--- src/aleph_client/commands/domain.py | 12 +++++------ src/aleph_client/commands/files.py | 10 +++++----- .../commands/instance/__init__.py | 20 +++++++++---------- .../commands/instance/port_forwarder.py | 12 +++++------ src/aleph_client/commands/message.py | 10 +++++----- 7 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/aleph_client/commands/aggregate.py b/src/aleph_client/commands/aggregate.py index 6ec0cd07..bdcadc94 100644 --- a/src/aleph_client/commands/aggregate.py +++ b/src/aleph_client/commands/aggregate.py @@ -19,7 +19,7 @@ from aleph_client.commands import help_strings from aleph_client.commands.utils import setup_logging -from aleph_client.utils import AlephAccount, AsyncTyper, load_account, sanitize_url +from aleph_client.utils import AccountTypes, AsyncTyper, load_account, sanitize_url logger = logging.getLogger(__name__) app = AsyncTyper(no_args_is_help=True) @@ -57,7 +57,7 @@ async def forget( setup_logging(debug) - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) address = account.get_address() if address is None else address if key == "security" and not is_same_context(): @@ -130,7 +130,7 @@ async def post( setup_logging(debug) - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) address = account.get_address() if address is None else address if key == "security" and not is_same_context(): @@ -192,7 +192,7 @@ async def get( setup_logging(debug) - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) address = account.get_address() if address is None else address async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client: @@ -228,7 +228,7 @@ async def list_aggregates( setup_logging(debug) - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) address = account.get_address() if address is None else address aggr_link = f"{sanitize_url(settings.API_HOST)}/api/v0/aggregates/{address}.json" @@ -302,7 +302,7 @@ async def authorize( setup_logging(debug) - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) data = await get( key="security", @@ -376,7 +376,7 @@ async def revoke( setup_logging(debug) - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) data = await get( key="security", @@ -431,7 +431,7 @@ async def permissions( setup_logging(debug) - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) address = account.get_address() if address is None else address data = await get( diff --git a/src/aleph_client/commands/credit.py b/src/aleph_client/commands/credit.py index 70a95bf3..8bf4f56b 100644 --- a/src/aleph_client/commands/credit.py +++ b/src/aleph_client/commands/credit.py @@ -16,7 +16,7 @@ from aleph_client.commands import help_strings from aleph_client.commands.utils import setup_logging -from aleph_client.utils import AlephAccount, AsyncTyper +from aleph_client.utils import AccountTypes, AsyncTyper logger = logging.getLogger(__name__) app = AsyncTyper(no_args_is_help=True) @@ -40,7 +40,7 @@ async def show( setup_logging(debug) - account: AlephAccount = _load_account(private_key, private_key_file) + account: AccountTypes = _load_account(private_key, private_key_file) if account and not address: address = account.get_address() @@ -86,7 +86,7 @@ async def history( ): setup_logging(debug) - account: AlephAccount = _load_account(private_key, private_key_file) + account: AccountTypes = _load_account(private_key, private_key_file) if account and not address: address = account.get_address() diff --git a/src/aleph_client/commands/domain.py b/src/aleph_client/commands/domain.py index 86c9e801..9161b190 100644 --- a/src/aleph_client/commands/domain.py +++ b/src/aleph_client/commands/domain.py @@ -25,7 +25,7 @@ from aleph_client.commands import help_strings from aleph_client.commands.utils import is_environment_interactive -from aleph_client.utils import AlephAccount, AsyncTyper, load_account +from aleph_client.utils import AccountTypes, AsyncTyper, load_account logger = logging.getLogger(__name__) @@ -135,7 +135,7 @@ async def attach_resource( ) -async def detach_resource(account: AlephAccount, fqdn: Hostname, interactive: Optional[bool] = None): +async def detach_resource(account: AccountTypes, fqdn: Hostname, interactive: Optional[bool] = None): domain_info = await get_aggregate_domain_info(account, fqdn) interactive = is_environment_interactive() if interactive is None else interactive @@ -185,7 +185,7 @@ async def add( ] = settings.PRIVATE_KEY_FILE, ): """Add and link a Custom Domain.""" - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) interactive = False if (not ask) else is_environment_interactive() console = Console() @@ -270,7 +270,7 @@ async def attach( ] = settings.PRIVATE_KEY_FILE, ): """Attach resource to a Custom Domain.""" - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) await attach_resource( account, @@ -292,7 +292,7 @@ async def detach( ] = settings.PRIVATE_KEY_FILE, ): """Unlink Custom Domain.""" - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) await detach_resource(account, Hostname(fqdn), interactive=False if (not ask) else None) raise typer.Exit() @@ -307,7 +307,7 @@ async def info( ] = settings.PRIVATE_KEY_FILE, ): """Show Custom Domain Details.""" - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) console = Console() domain_validator = DomainValidator() diff --git a/src/aleph_client/commands/files.py b/src/aleph_client/commands/files.py index 586fff85..5ef3a949 100644 --- a/src/aleph_client/commands/files.py +++ b/src/aleph_client/commands/files.py @@ -22,7 +22,7 @@ from aleph_client.commands import help_strings from aleph_client.commands.utils import setup_logging -from aleph_client.utils import AlephAccount, AsyncTyper, load_account +from aleph_client.utils import AccountTypes, AsyncTyper, load_account logger = logging.getLogger(__name__) app = AsyncTyper(no_args_is_help=True) @@ -43,7 +43,7 @@ async def pin( setup_logging(debug) - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client: result: StoreMessage @@ -74,7 +74,7 @@ async def upload( setup_logging(debug) - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client: if not path.is_file(): @@ -180,7 +180,7 @@ async def forget( setup_logging(debug) - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) hashes = [ItemHash(item_hash) for item_hash in item_hash.split(",")] @@ -269,7 +269,7 @@ async def list_files( json: Annotated[bool, typer.Option(help="Print as json instead of rich table")] = False, ): """List all files for a given address""" - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) if account and not address: address = account.get_address() diff --git a/src/aleph_client/commands/instance/__init__.py b/src/aleph_client/commands/instance/__init__.py index 0cda53eb..ec0705b4 100644 --- a/src/aleph_client/commands/instance/__init__.py +++ b/src/aleph_client/commands/instance/__init__.py @@ -81,7 +81,7 @@ yes_no_input, ) from aleph_client.models import CRNInfo -from aleph_client.utils import AlephAccount, AsyncTyper, load_account, sanitize_url +from aleph_client.utils import AccountTypes, AsyncTyper, load_account, sanitize_url logger = logging.getLogger(__name__) app = AsyncTyper(no_args_is_help=True) @@ -166,7 +166,7 @@ async def create( ssh_pubkey: str = ssh_pubkey_file.read_text(encoding="utf-8").strip() # Populates account / address - account: AlephAccount = load_account(private_key, private_key_file, chain=payment_chain) + account: AccountTypes = load_account(private_key, private_key_file, chain=payment_chain) address = address or settings.ADDRESS_TO_USE or account.get_address() @@ -830,7 +830,7 @@ async def delete( setup_logging(debug) - account: AlephAccount = load_account(private_key, private_key_file, chain=chain) + account: AccountTypes = load_account(private_key, private_key_file, chain=chain) async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client: try: existing_message: InstanceMessage = await client.get_message( @@ -942,7 +942,7 @@ async def list_instances( setup_logging(debug) - account: AlephAccount = load_account(private_key, private_key_file, chain=chain) + account: AccountTypes = load_account(private_key, private_key_file, chain=chain) address = address or settings.ADDRESS_TO_USE or account.get_address() async with AlephHttpClient(api_server=settings.API_HOST) as client: @@ -979,7 +979,7 @@ async def reboot( or Prompt.ask("URL of the CRN (Compute node) on which the VM is running") ) - account: AlephAccount = load_account(private_key, private_key_file, chain=chain) + account: AccountTypes = load_account(private_key, private_key_file, chain=chain) async with VmClient(account, domain) as manager: status, result = await manager.reboot_instance(vm_id=vm_id) @@ -1012,7 +1012,7 @@ async def allocate( or Prompt.ask("URL of the CRN (Compute node) on which the VM will be allocated") ) - account: AlephAccount = load_account(private_key, private_key_file, chain=chain) + account: AccountTypes = load_account(private_key, private_key_file, chain=chain) async with VmClient(account, domain) as manager: status, result = await manager.start_instance(vm_id=vm_id) @@ -1040,7 +1040,7 @@ async def logs( domain = (domain and sanitize_url(domain)) or await find_crn_of_vm(vm_id) or Prompt.ask(help_strings.PROMPT_CRN_URL) - account: AlephAccount = load_account(private_key, private_key_file, chain=chain) + account: AccountTypes = load_account(private_key, private_key_file, chain=chain) async with VmClient(account, domain) as manager: try: @@ -1071,7 +1071,7 @@ async def stop( domain = (domain and sanitize_url(domain)) or await find_crn_of_vm(vm_id) or Prompt.ask(help_strings.PROMPT_CRN_URL) - account: AlephAccount = load_account(private_key, private_key_file, chain=chain) + account: AccountTypes = load_account(private_key, private_key_file, chain=chain) async with VmClient(account, domain) as manager: status, result = await manager.stop_instance(vm_id=vm_id) @@ -1110,7 +1110,7 @@ async def confidential_init_session( or Prompt.ask("URL of the CRN (Compute node) on which the session will be initialized") ) - account: AlephAccount = load_account(private_key, private_key_file, chain=chain) + account: AccountTypes = load_account(private_key, private_key_file, chain=chain) sevctl_path = find_sevctl_or_exit() @@ -1187,7 +1187,7 @@ async def confidential_start( session_dir.mkdir(exist_ok=True, parents=True) vm_hash = ItemHash(vm_id) - account: AlephAccount = load_account(private_key, private_key_file, chain=chain) + account: AccountTypes = load_account(private_key, private_key_file, chain=chain) sevctl_path = find_sevctl_or_exit() domain = ( diff --git a/src/aleph_client/commands/instance/port_forwarder.py b/src/aleph_client/commands/instance/port_forwarder.py index bbbb6518..23c84c9f 100644 --- a/src/aleph_client/commands/instance/port_forwarder.py +++ b/src/aleph_client/commands/instance/port_forwarder.py @@ -20,7 +20,7 @@ from aleph_client.commands import help_strings from aleph_client.commands.utils import setup_logging -from aleph_client.utils import AlephAccount, AsyncTyper, load_account +from aleph_client.utils import AccountTypes, AsyncTyper, load_account logger = logging.getLogger(__name__) app = AsyncTyper(no_args_is_help=True) @@ -41,7 +41,7 @@ async def list_ports( setup_logging(debug) - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) address = address or settings.ADDRESS_TO_USE or account.get_address() async with AlephHttpClient(api_server=settings.API_HOST) as client: @@ -159,7 +159,7 @@ async def create( typer.echo("Error: Port must be between 1 and 65535") raise typer.Exit(code=1) - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) # Create the port flags port_flags = PortFlags(tcp=tcp, udp=udp) @@ -212,7 +212,7 @@ async def update( typer.echo("Error: Port must be between 1 and 65535") raise typer.Exit(code=1) - account: AlephAccount = load_account(private_key, private_key_file, chain) + account: AccountTypes = load_account(private_key, private_key_file, chain) # First check if the port forward exists async with AlephHttpClient(api_server=settings.API_HOST) as client: @@ -292,7 +292,7 @@ async def delete( typer.echo("Error: Port must be between 1 and 65535") raise typer.Exit(code=1) - account: AlephAccount = load_account(private_key, private_key_file, chain) + account: AccountTypes = load_account(private_key, private_key_file, chain) # First check if the port forward exists async with AlephHttpClient(api_server=settings.API_HOST) as client: @@ -375,7 +375,7 @@ async def refresh( setup_logging(debug) - account: AlephAccount = load_account(private_key, private_key_file, chain) + account: AccountTypes = load_account(private_key, private_key_file, chain) try: async with AuthenticatedAlephHttpClient(api_server=settings.API_HOST, account=account) as client: diff --git a/src/aleph_client/commands/message.py b/src/aleph_client/commands/message.py index 36285e00..94ccd68e 100644 --- a/src/aleph_client/commands/message.py +++ b/src/aleph_client/commands/message.py @@ -34,7 +34,7 @@ setup_logging, str_to_datetime, ) -from aleph_client.utils import AlephAccount, AsyncTyper, load_account +from aleph_client.utils import AccountTypes, AsyncTyper, load_account app = AsyncTyper(no_args_is_help=True) @@ -137,7 +137,7 @@ async def post( setup_logging(debug) - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) storage_engine: StorageEnum content: dict @@ -187,7 +187,7 @@ async def amend( setup_logging(debug) - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) async with AlephHttpClient(api_server=settings.API_HOST) as client: existing_message: Optional[AlephMessage] = None @@ -252,7 +252,7 @@ async def forget( hash_list: list[ItemHash] = [ItemHash(h) for h in hashes.split(",")] - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) async with AuthenticatedAlephHttpClient(account=account, api_server=settings.API_HOST) as client: await client.forget(hashes=hash_list, reason=reason, channel=channel) @@ -295,7 +295,7 @@ def sign( setup_logging(debug) - account: AlephAccount = load_account(private_key, private_key_file) + account: AccountTypes = load_account(private_key, private_key_file) if message is None: message = input_multiline() From 5e66ce23a91ef885a9a29e80b217d5c52d71ccda Mon Sep 17 00:00:00 2001 From: 1yam Date: Fri, 7 Nov 2025 17:23:48 +0100 Subject: [PATCH 19/19] fix: account init commands unit test should be removed since not usefull anymore --- tests/unit/test_commands.py | 51 ------------------------------------- 1 file changed, 51 deletions(-) diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index 6b3545b4..ee574935 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -202,30 +202,6 @@ def test_account_import_sol(env_files): assert new_key != old_key -def test_account_init(env_files): - """Test the new account init command.""" - settings.CONFIG_FILE = env_files[1] - - # First ensure the config directory exists but is empty - config_dir = env_files[1].parent - # Removing unused variable - - with ( - patch("aleph.sdk.conf.settings.CONFIG_FILE", env_files[1]), - patch("aleph.sdk.conf.settings.CONFIG_HOME", str(config_dir)), - patch("pathlib.Path.mkdir") as mock_mkdir, - ): - - result = runner.invoke(app, ["account", "init"]) - assert result.exit_code == 0 - assert "Configuration initialized." in result.stdout - assert "Run `aleph account create`" in result.stdout - assert "Run `aleph account config --account-type external`" in result.stdout - - # Check that directories were created - mock_mkdir.assert_any_call(parents=True, exist_ok=True) - - @patch("aleph.sdk.wallets.ledger.ethereum.LedgerETHAccount.get_accounts") def test_account_address(mock_get_accounts, env_files): settings.CONFIG_FILE = env_files[1] @@ -256,33 +232,6 @@ def test_account_address(mock_get_accounts, env_files): assert result.stdout.startswith("✉ Addresses for Active Account (Ledger) ✉\n\nEVM: 0x") -def test_account_init_with_isolated_filesystem(): - """Test the new account init command that creates base configuration for new users.""" - # Set up a test directory for config - with runner.isolated_filesystem(): - config_dir = Path("test_config") - config_file = config_dir / "config.json" - - # Create the directory first - config_dir.mkdir(parents=True, exist_ok=True) - - with ( - patch("aleph.sdk.conf.settings.CONFIG_FILE", config_file), - patch("aleph.sdk.conf.settings.CONFIG_HOME", str(config_dir)), - ): - - result = runner.invoke(app, ["account", "init"]) - - # Verify command executed successfully - assert result.exit_code == 0 - assert "Configuration initialized." in result.stdout - assert "Run `aleph account create`" in result.stdout - assert "Run `aleph account config --account-type external`" in result.stdout - - # Verify the config file was created - assert config_file.exists() - - def test_account_chain(env_files): settings.CONFIG_FILE = env_files[1] result = runner.invoke(app, ["account", "chain"])