Skip to content

Conversation

@nesitor
Copy link
Member

@nesitor nesitor commented Sep 10, 2025

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.

Self proofreading checklist

  • The new code clear, easy to read and well commented.
  • New code does not duplicate the functions of builtin or popular libraries.
  • An LLM was used to review the new code and look for simplifications.
  • New classes and functions contain docstrings explaining what they provide.
  • All new code is covered by relevant tests.

@nesitor nesitor self-assigned this Sep 10, 2025
@1yam 1yam force-pushed the andres-feature-implement_ledger branch from dfd8a03 to b994c79 Compare October 31, 2025 09:50
@1yam 1yam assigned 1yam and unassigned nesitor Oct 31, 2025
@1yam 1yam marked this pull request as ready for review October 31, 2025 10:27
@1yam 1yam requested a review from odesenfans October 31, 2025 10:27
@codecov
Copy link

codecov bot commented Oct 31, 2025

Codecov Report

❌ Patch coverage is 75.47170% with 39 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.56%. Comparing base (e64d162) to head (def2144).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/aleph_client/commands/account.py 77.38% 11 Missing and 8 partials ⚠️
src/aleph_client/utils.py 55.55% 9 Missing and 3 partials ⚠️
src/aleph_client/commands/domain.py 33.33% 4 Missing ⚠️
src/aleph_client/commands/files.py 66.66% 2 Missing ⚠️
src/aleph_client/commands/message.py 66.66% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #402      +/-   ##
==========================================
+ Coverage   61.07%   61.56%   +0.49%     
==========================================
  Files          20       20              
  Lines        3712     3791      +79     
  Branches      533      550      +17     
==========================================
+ Hits         2267     2334      +67     
- Misses       1168     1181      +13     
+ Partials      277      276       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions
Copy link

Failed to retrieve llama text: POST 500: {"error": "500: Unhandled error during initialisation"}

@odesenfans
Copy link
Contributor

@1yam how do you use this? I tried aleph account config --account-type external with a Ledger Nano S plugged in via USB but I get the following error:

$ aleph account config --account-type external
No config file found.
Loading External keys. Do you want to import from Ledger? [y/n] (y): y
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Traceback (most recent call last) ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ /home/olivier/git/aleph/aleph-client/src/aleph_client/utils.py:94 in runner                                                                                                                                                                                                                             │
│                                                                                                                                                                                                                                                                                                         │
│    91 │   │   │                                                                                ╭──────────────────────────────────────────────────────── locals ─────────────────────────────────────────────────────────╮                                                                              │
│    92 │   │   │   @wraps(f)                                                                    │   args = ()                                                                                                             │                                                                              │
│    93 │   │   │   def runner(*args, **kwargs):                                                 │ kwargs = {'private_key_file': None, 'chain': None, 'address': None, 'account_type': <AccountType.EXTERNAL: 'external'>} │                                                                              │
│ ❱  94 │   │   │   │   return asyncio.run(f(*args, **kwargs))                                   ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯                                                                              │
│    95 │   │   │                                                                                                                                                                                                                                                                                         │
│    96 │   │   │   decorator(runner)                                                                                                                                                                                                                                                                     │
│    97 │   │   else:                                                                                                                                                                                                                                                                                     │
│                                                                                                                                                                                                                                                                                                         │
│ /usr/lib/python3.13/asyncio/runners.py:195 in run                                                                                                                                                                                                                                                       │
│                                                                                                                                                                                                                                                                                                         │
│   192 │   │   │   "asyncio.run() cannot be called from a running event loop")                  ╭───────────────────────────── locals ─────────────────────────────╮                                                                                                                                     │
│   193 │                                                                                        │        debug = None                                              │                                                                                                                                     │
│   194 │   with Runner(debug=debug, loop_factory=loop_factory) as runner:                       │ loop_factory = None                                              │                                                                                                                                     │
│ ❱ 195 │   │   return runner.run(main)                                                          │         main = <coroutine object configure at 0x74340e754b40>    │                                                                                                                                     │
│   196                                                                                          │       runner = <asyncio.runners.Runner object at 0x74340ebefe00> │                                                                                                                                     │
│   197                                                                                          ╰──────────────────────────────────────────────────────────────────╯                                                                                                                                     │
│   198 def _cancel_all_tasks(loop):                                                                                                                                                                                                                                                                      │
│                                                                                                                                                                                                                                                                                                         │
│ /usr/lib/python3.13/asyncio/runners.py:118 in run                                                                                                                                                                                                                                                       │
│                                                                                                                                                                                                                                                                                                         │
│   115 │   │                                                                                                                                                                                                                                                                                             │
│   116 │   │   self._interrupt_count = 0                                                                                                                                                                                                                                                                 │
│   117 │   │   try:                                                                                                                                                                                                                                                                                      │
│ ❱ 118 │   │   │   return self._loop.run_until_complete(task)                                                                                                                                                                                                                                            │
│   119 │   │   except exceptions.CancelledError:                                                                                                                                                                                                                                                         │
│   120 │   │   │   if self._interrupt_count > 0:                                                                                                                                                                                                                                                         │
│   121 │   │   │   │   uncancel = getattr(task, "uncancel", None)                                                                                                                                                                                                                                        │
│                                                                                                                                                                                                                                                                                                         │
│ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── locals ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │
│ │        context = <_contextvars.Context object at 0x74340e391d80>                                                                                                                                                                                                                                    │ │
│ │           coro = <coroutine object configure at 0x74340e754b40>                                                                                                                                                                                                                                     │ │
│ │           self = <asyncio.runners.Runner object at 0x74340ebefe00>                                                                                                                                                                                                                                  │ │
│ │ sigint_handler = functools.partial(<bound method Runner._on_sigint of <asyncio.runners.Runner object at 0x74340ebefe00>>, main_task=<Task finished name='Task-1' coro=<configure() done, defined at /home/olivier/git/aleph/aleph-client/src/aleph_client/commands/account.py:533>                  │ │
│ │                  exception=OSError('open failed')>)                                                                                                                                                                                                                                                 │ │
│ │           task = <Task finished name='Task-1' coro=<configure() done, defined at /home/olivier/git/aleph/aleph-client/src/aleph_client/commands/account.py:533> exception=OSError('open failed')>                                                                                                   │ │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                                                                                                                                                                                                                                         │
│ /usr/lib/python3.13/asyncio/base_events.py:719 in run_until_complete                                                                                                                                                                                                                                    │
│                                                                                                                                                                                                                                                                                                         │
│    716 │   │   if not future.done():                                                            ╭────────────────────────────────────────────────────────────────────────────────────────── locals ───────────────────────────────────────────────────────────────────────────────────────────╮         │
│    717 │   │   │   raise RuntimeError('Event loop stopped before Future completed.')            │   future = <Task finished name='Task-1' coro=<configure() done, defined at /home/olivier/git/aleph/aleph-client/src/aleph_client/commands/account.py:533> exception=OSError('open failed')> │         │
│    718 │   │                                                                                    │ new_task = False                                                                                                                                                                            │         │
│ ❱  719 │   │   return future.result()                                                           │     self = <_UnixSelectorEventLoop running=False closed=True debug=False>                                                                                                                   │         │
│    720 │                                                                                        ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯         │
│    721 │   def stop(self):                                                                                                                                                                                                                                                                              │
│    722 │   │   """Stop running the event loop.                                                                                                                                                                                                                                                          │
│                                                                                                                                                                                                                                                                                                         │
│ /home/olivier/git/aleph/aleph-client/src/aleph_client/commands/account.py:592 in configure                                                                                                                                                                                                              │
│                                                                                                                                                                                                                                                                                                         │
│   589 │   │   │   "[bright_cyan]Loading External keys.[/bright_cyan] [yellow]Do you want to im ╭────────────────────────────────────────────────────────────────────── locals ──────────────────────────────────────────────────────────────────────╮                                                   │
│   590 │   │   │   default="y",                                                                 │     account_type = <AccountType.EXTERNAL: 'external'>                                                                                              │                                                   │
│   591 │   │   ):                                                                               │          address = None                                                                                                                            │                                                   │
│ ❱ 592 │   │   │   accounts = LedgerETHAccount.get_accounts()                                   │            chain = None                                                                                                                            │                                                   │
│   593 │   │   │   account_addresses = [acc.address for acc in accounts]                        │           config = None                                                                                                                            │                                                   │
│   594 │   │   │                                                                                │ private_key_file = None                                                                                                                            │                                                   │
│   595 │   │   │   console.print("[bold cyan]Available addresses on Ledger:[/bold cyan]")       │    unlinked_keys = [PosixPath('/home/olivier/.aleph-im/private-keys/ethereum.key'), PosixPath('/home/olivier/.aleph-im/private-keys/default.key')] │                                                   │
│                                                                                                ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯                                                   │
│                                                                                                                                                                                                                                                                                                         │
│ /home/olivier/git/aleph/aleph-client/venv/lib/python3.13/site-packages/aleph/sdk/wallets/ledger/ethereum.py:46 in get_accounts                                                                                                                                                                          │
│                                                                                                                                                                                                                                                                                                         │
│    43 │   │   """Initialize an aleph.im account from a LedgerHQ device from                    ╭─── locals ────╮                                                                                                                                                                                        │
│    44 │   │   a known wallet address.                                                          │  count = 5    │                                                                                                                                                                                        │
│    45 │   │   """                                                                              │ device = None │                                                                                                                                                                                        │
│ ❱  46 │   │   device = device or init_dongle()                                                 ╰───────────────╯                                                                                                                                                                                        │
│    47 │   │   accounts: List[LedgerAccount] = get_accounts(dongle=device, count=count)                                                                                                                                                                                                                  │
│    48 │   │   return accounts                                                                                                                                                                                                                                                                           │
│    49                                                                                                                                                                                                                                                                                                   │
│                                                                                                                                                                                                                                                                                                         │
│ /home/olivier/git/aleph/aleph-client/venv/lib/python3.13/site-packages/ledgereth/comms.py:179 in init_dongle                                                                                                                                                                                            │
│                                                                                                                                                                                                                                                                                                         │
│   176 │   # If not given, use cache if available                                               ╭──── locals ────╮                                                                                                                                                                                       │
│   177 │   if dongle is None and DONGLE_CACHE is None:                                          │  debug = False │                                                                                                                                                                                       │
│   178 │   │   try:                                                                             │ dongle = None  │                                                                                                                                                                                       │
│ ❱ 179 │   │   │   DONGLE_CACHE = getDongle(debug)  # type: ignore                              ╰────────────────╯                                                                                                                                                                                       │
│   180 │   │   except CommException as err:                                                                                                                                                                                                                                                              │
│   181 │   │   │   raise LedgerError.transalate_comm_exception(err) from err                                                                                                                                                                                                                             │
│   182                                                                                                                                                                                                                                                                                                   │
│                                                                                                                                                                                                                                                                                                         │
│ /home/olivier/git/aleph/aleph-client/venv/lib/python3.13/site-packages/ledgerblue/comm.py:361 in getDongle                                                                                                                                                                                              │
│                                                                                                                                                                                                                                                                                                         │
│   358 │   │   │   │   hidDevicePath = hidDevice['path']                                                                                                                                                                                                                                                 │
│   359 │   if hidDevicePath is not None:                                                                                                                                                                                                                                                                 │
│   360 │   │   dev = hid.device()                                                                                                                                                                                                                                                                        │
│ ❱ 361 │   │   dev.open_path(hidDevicePath)                                                                                                                                                                                                                                                              │
│   362 │   │   dev.set_nonblocking(True)                                                                                                                                                                                                                                                                 │
│   363 │   │   return HIDDongleHIDAPI(dev, ledger, debug)                                                                                                                                                                                                                                                │
│   364 │   if PCSC:                                                                                                                                                                                                                                                                                      │
│                                                                                                                                                                                                                                                                                                         │
│ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────── locals ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮                                                               │
│ │         debug = False                                                                                                                                                                                                                 │                                                               │
│ │           dev = <hid.device object at 0x74340fbb5000>                                                                                                                                                                                 │                                                               │
│ │     hidDevice = {'path': b'1-10:1.1', 'vendor_id': 6940, 'product_id': 6985, 'serial_number': '', 'release_number': 804, 'manufacturer_string': '', 'product_string': '', 'usage_page': 0, 'usage': 0, 'interface_number': 1, ... +1} │                                                               │
│ │ hidDevicePath = b'1-2.2.1:1.0'                                                                                                                                                                                                        │                                                               │
│ │        ledger = True                                                                                                                                                                                                                  │                                                               │
│ │ selectCommand = None                                                                                                                                                                                                                  │                                                               │
│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯                                                               │
│                                                                                                                                                                                                                                                                                                         │
│ in hid.device.open_path:158                                                                                                                                                                                                                                                                             │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
OSError: open failed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants