Skip to content

Commit

Permalink
Add ledger manager
Browse files Browse the repository at this point in the history
  • Loading branch information
moisses89 committed Oct 24, 2023
1 parent 0dc3df9 commit 723daba
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 128 deletions.
66 changes: 6 additions & 60 deletions safe_cli/operators/hw_accounts/ledger_account.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import warnings

from eth_account.datastructures import SignedTransaction
from eth_account.signers.base import BaseAccount
from hexbytes import HexBytes
from ledgerblue import Dongle
from ledgereth import create_transaction, sign_typed_data_draft
from web3 import Web3
from web3.types import TxParams


class LedgerAccount(BaseAccount):
def __init__(self, path, address, dongle: Dongle):
class LedgerAccount:
def __init__(self, path, address):
"""
Initialize a new ledger account (no private key)
Expand All @@ -19,64 +16,16 @@ def __init__(self, path, address, dongle: Dongle):
"""
self._address = address
self.path = path
self.dongle = dongle

@property
def address(self):
return self._address

@property
def privateKey(self):
"""
.. CAUTION:: Deprecated for :meth:`~eth_account.signers.local.LocalAccount.key`.
This attribute will be removed in v0.5
"""
warnings.warn(
"privateKey is deprecated in favor of key",
category=DeprecationWarning,
)
return None

@property
def key(self):
"""
Get the private key.
"""
return None

def encrypt(self, password, kdf=None, iterations=None):
"""
Generate a string with the encrypted key.
This uses the same structure as in
:meth:`~eth_account.account.Account.encrypt`, but without a private key argument.
"""
# return self._publicapi.encrypt(self.key, password, kdf=kdf, iterations=iterations)
# TODO with ledger
pass

def signHash(self, domain_hash: bytes, message_hash: bytes):
signed = sign_typed_data_draft(domain_hash, message_hash, dongle=self.dongle)
def signMessage(self, domain_hash: bytes, message_hash: bytes, dongle: Dongle):
signed = sign_typed_data_draft(domain_hash, message_hash, self.path, dongle)
return (signed.v, signed.r, signed.s)

def sign_message(self, signable_message):
"""
Generate a string with the encrypted key.
This uses the same structure as in
:meth:`~eth_account.account.Account.sign_message`, but without a private key argument.
"""
# TODO with ledger
pass

def signTransaction(self, transaction_dict):
warnings.warn(
"signTransaction is deprecated in favor of sign_transaction",
category=DeprecationWarning,
)
pass

def sign_transaction(self, tx: TxParams) -> SignedTransaction:
def sign_transaction(self, tx: TxParams, dongle: Dongle) -> SignedTransaction:
signed = create_transaction(
destination=tx["to"],
amount=tx["value"],
Expand All @@ -87,7 +36,7 @@ def sign_transaction(self, tx: TxParams) -> SignedTransaction:
nonce=tx["nonce"],
chain_id=tx["chainId"],
sender_path=self.path,
dongle=self.dongle,
dongle=dongle,
)
raw_transaction = signed.raw_transaction()
return SignedTransaction(
Expand All @@ -97,6 +46,3 @@ def sign_transaction(self, tx: TxParams) -> SignedTransaction:
signed.sender_s,
signed.y_parity,
)

def __bytes__(self):
return self.key
112 changes: 112 additions & 0 deletions safe_cli/operators/hw_accounts/ledger_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from enum import Enum
from typing import List, Optional, Set, Tuple

from eth_typing import ChecksumAddress
from ledgereth.accounts import get_account_by_path
from ledgereth.comms import init_dongle
from ledgereth.constants import DEFAULT_PATH_STRING
from ledgereth.exceptions import LedgerAppNotOpened, LedgerLocked, LedgerNotFound
from prompt_toolkit import HTML, print_formatted_text

from gnosis.safe.signatures import signature_to_bytes

from safe_cli.operators.hw_accounts.ledger_account import LedgerAccount


class LedgerStatus(Enum):
DISCONNECTED = 0
LOCKED = 1 # Connected but locked
APP_CLOSED = 2 # Connected, unlocked but app is closed
READY = 3 # Ready to communicate


class LedgerManager:

LEDGER_SEARCH_DEEP = 10

def __init__(self):
self.dongle = None
self.accounts: Set[LedgerAccount] = set()
self.connected: bool

def _print_error_message(self, message: str):
print_formatted_text(HTML(f"<ansired>{message}</ansired>"))

def check_status(self, print_message: bool = False) -> LedgerStatus:
try:
self.dongle = init_dongle(self.dongle)
# Get default derivation to check following status
get_account_by_path(DEFAULT_PATH_STRING)
except LedgerNotFound:
if print_message:
self._print_error_message("Ledger is disconnected")
return LedgerStatus.DISCONNECTED
except LedgerLocked:
if print_message:
self._print_error_message("Ledger is locked")
return LedgerStatus.LOCKED
except LedgerAppNotOpened:
if print_message:
self._print_error_message("Ledger is disconnected")
return LedgerStatus.APP_CLOSED

return LedgerStatus.READY

@property
def connected(self) -> bool:
if self.check_status() != LedgerStatus.DISCONNECTED:
return True
return False

def get_accounts(
self, legacy_account: Optional[bool] = False
) -> List[Tuple[ChecksumAddress, str]] | None:
"""
:param legacy_account:
:return: a list of tuples with address and derivation path
"""
accounts = []
if self.check_status(True) != LedgerStatus.READY:
return None
for i in range(self.LEDGER_SEARCH_DEEP):
if legacy_account:
path_string = f"44'/60'/0'/{i}"
else:
path_string = f"44'/60'/{i}'/0/0"
try:
account = get_account_by_path(path_string, self.dongle)
except LedgerLocked as ledger_error:
print(f"Ledger exception: {ledger_error}")
accounts.append((account.address, account.path))
return accounts

def add_account(self, derivation_path: str) -> bool:
"""
Add account to ledger manager list
:param derivation_path:
:return:
"""
if self.check_status(True) != LedgerStatus.READY:
return False
account = get_account_by_path(derivation_path, self.dongle)
self.accounts.add(LedgerAccount(account.path, account.address))
return True

def sign_eip712(
self, domain_hash: bytes, message_hash: bytes, account: LedgerAccount
) -> bytes | None:
"""
Sign eip712 hashes
:param domain_hash:
:param message_hash:
:param account: ledger account
:return: bytes signature
"""
if self.check_status(True) != LedgerStatus.READY:
return None

v, r, s = account.signMessage(domain_hash, message_hash, self.dongle)

return signature_to_bytes(v, r, s)
Loading

0 comments on commit 723daba

Please sign in to comment.