From 1b33231d95c3f66ba017eb93359c91ccf2557230 Mon Sep 17 00:00:00 2001
From: moisses89 <7888669+moisses89@users.noreply.github.com>
Date: Thu, 30 Nov 2023 12:00:17 +0100
Subject: [PATCH] Add support for Trezor
---
safe_cli/operators/hw_accounts/hw_account.py | 16 ++-
.../hw_accounts/hw_account_manager.py | 19 +--
.../{exceptions.py => ledger_exceptions.py} | 8 +-
.../operators/hw_accounts/ledger_manager.py | 24 ++--
.../hw_accounts/trezor_exceptions.py | 37 +++++
.../operators/hw_accounts/trezor_manager.py | 39 +++++-
safe_cli/operators/safe_operator.py | 22 ++-
safe_cli/prompt_parser.py | 19 +++
safe_cli/safe_completer_constants.py | 4 +
tests/test_hw_account_manager.py | 29 +---
tests/test_ledger_manager.py | 8 +-
tests/test_trezor_manager.py | 129 ++++++++++++++++++
12 files changed, 275 insertions(+), 79 deletions(-)
rename safe_cli/operators/hw_accounts/{exceptions.py => ledger_exceptions.py} (86%)
create mode 100644 safe_cli/operators/hw_accounts/trezor_exceptions.py
create mode 100644 tests/test_trezor_manager.py
diff --git a/safe_cli/operators/hw_accounts/hw_account.py b/safe_cli/operators/hw_accounts/hw_account.py
index bf113772..f532bd1e 100644
--- a/safe_cli/operators/hw_accounts/hw_account.py
+++ b/safe_cli/operators/hw_accounts/hw_account.py
@@ -1,6 +1,5 @@
import re
from abc import ABC, abstractmethod
-from typing import Tuple
from eth_typing import ChecksumAddress
@@ -8,6 +7,10 @@
BIP32_LEGACY_LEDGER_PATTERN = r"^(m/)?44'/60'/[0-9]+'/[0-9]+$"
+class InvalidDerivationPath(Exception):
+ message = "The provided derivation path is not valid"
+
+
class HwAccount(ABC):
def __init__(self, derivation_path: str, address: ChecksumAddress):
self.derivation_path = derivation_path
@@ -26,10 +29,13 @@ def is_valid_derivation_path(derivation_path: str):
"""
Detect if a string is a valid derivation path
"""
- return (
+ if not (
re.match(BIP32_ETH_PATTERN, derivation_path) is not None
or re.match(BIP32_LEGACY_LEDGER_PATTERN, derivation_path) is not None
- )
+ ):
+ raise InvalidDerivationPath
+
+ return True
@staticmethod
@abstractmethod
@@ -41,12 +47,12 @@ def get_address_by_derivation_path(derivation_path: str) -> ChecksumAddress:
"""
@abstractmethod
- def sign_typed_hash(self, domain_hash, message_hash) -> Tuple[bytes, bytes, bytes]:
+ def sign_typed_hash(self, domain_hash, message_hash) -> bytes:
"""
:param domain_hash:
:param message_hash:
- :return: tuple os signature v, r, s
+ :return: signature
"""
def __eq__(self, other):
diff --git a/safe_cli/operators/hw_accounts/hw_account_manager.py b/safe_cli/operators/hw_accounts/hw_account_manager.py
index 4728a6e0..19a0818f 100644
--- a/safe_cli/operators/hw_accounts/hw_account_manager.py
+++ b/safe_cli/operators/hw_accounts/hw_account_manager.py
@@ -6,9 +6,7 @@
from gnosis.eth.eip712 import eip712_encode
from gnosis.safe import SafeTx
-from gnosis.safe.signatures import signature_to_bytes
-from safe_cli.operators.exceptions import HardwareWalletException
from safe_cli.operators.hw_accounts.hw_account import HwAccount
@@ -31,14 +29,11 @@ def __init__(self):
self.supported_hw_wallets[HwWalletType.LEDGER] = LedgerManager
except (ModuleNotFoundError, IOError):
- print("Exception")
pass
- print("Continue")
try:
from safe_cli.operators.hw_accounts.trezor_manager import TrezorManager
- print("Continue")
self.supported_hw_wallets[HwWalletType.TREZOR] = TrezorManager
except (ModuleNotFoundError, IOError):
pass
@@ -88,12 +83,9 @@ def add_account(
hw_wallet = self.get_hw_wallet(hw_wallet_type)
- if hw_wallet.is_valid_derivation_path(derivation_path):
- address = hw_wallet.get_address_by_derivation_path(derivation_path)
- self.accounts.add(hw_wallet(derivation_path, address))
- return address
- else:
- raise HardwareWalletException("Invalid derivation path")
+ address = hw_wallet.get_address_by_derivation_path(derivation_path)
+ self.accounts.add(hw_wallet(derivation_path, address))
+ return address
def delete_accounts(self, addresses: List[ChecksumAddress]) -> Set:
"""
@@ -130,11 +122,8 @@ def sign_eip712(self, safe_tx: SafeTx, accounts: List[HwAccount]) -> SafeTx:
)
print_formatted_text(HTML(f"Domain_hash: {domain_hash.hex()}"))
print_formatted_text(HTML(f"Message_hash: {message_hash.hex()}"))
- signed_v, signed_r, signed_s = account.sign_typed_hash(
- domain_hash, message_hash
- )
+ signature = account.sign_typed_hash(domain_hash, message_hash)
- signature = signature_to_bytes(signed_v, signed_r, signed_s)
# TODO should be refactored on safe_eth_py function insert_signature_sorted
# Insert signature sorted
if account.address not in safe_tx.signers:
diff --git a/safe_cli/operators/hw_accounts/exceptions.py b/safe_cli/operators/hw_accounts/ledger_exceptions.py
similarity index 86%
rename from safe_cli/operators/hw_accounts/exceptions.py
rename to safe_cli/operators/hw_accounts/ledger_exceptions.py
index 46a814b5..670aaf25 100644
--- a/safe_cli/operators/hw_accounts/exceptions.py
+++ b/safe_cli/operators/hw_accounts/ledger_exceptions.py
@@ -3,23 +3,19 @@
from ledgereth.exceptions import (
LedgerAppNotOpened,
LedgerCancel,
- LedgerError,
LedgerLocked,
LedgerNotFound,
)
from safe_cli.operators.exceptions import HardwareWalletException
-
-
-class InvalidDerivationPath(LedgerError):
- message = "The provided derivation path is not valid"
+from safe_cli.operators.hw_accounts.hw_account import InvalidDerivationPath
class UnsupportedHwWalletException(Exception):
pass
-def raise_as_hw_account_exception(function):
+def raise_ledger_exception_as_hw_account_exception(function):
@functools.wraps(function)
def wrapper(*args, **kwargs):
try:
diff --git a/safe_cli/operators/hw_accounts/ledger_manager.py b/safe_cli/operators/hw_accounts/ledger_manager.py
index 12f0e275..f4a4b229 100644
--- a/safe_cli/operators/hw_accounts/ledger_manager.py
+++ b/safe_cli/operators/hw_accounts/ledger_manager.py
@@ -1,13 +1,15 @@
-from typing import Tuple
-
from eth_typing import ChecksumAddress
from ledgereth import sign_typed_data_draft
from ledgereth.accounts import get_account_by_path
from ledgereth.comms import init_dongle
from ledgereth.exceptions import LedgerNotFound
-from safe_cli.operators.hw_accounts.exceptions import raise_as_hw_account_exception
+from gnosis.safe.signatures import signature_to_bytes
+
from safe_cli.operators.hw_accounts.hw_account import HwAccount
+from safe_cli.operators.hw_accounts.ledger_exceptions import (
+ raise_ledger_exception_as_hw_account_exception,
+)
class LedgerManager(HwAccount):
@@ -28,14 +30,14 @@ def connect(self) -> bool:
return False
@property
- @raise_as_hw_account_exception
+ @raise_ledger_exception_as_hw_account_exception
def connected(self) -> bool:
"""
:return: True if ledger is connected or False in other case
"""
return self.connect()
- @raise_as_hw_account_exception
+ @raise_ledger_exception_as_hw_account_exception
def get_address_by_derivation_path(derivation_path: str) -> ChecksumAddress:
"""
@@ -44,12 +46,14 @@ def get_address_by_derivation_path(derivation_path: str) -> ChecksumAddress:
"""
if derivation_path[0:2] == "m/":
derivation_path = derivation_path.replace("m/", "")
- account = get_account_by_path(derivation_path)
- return account.address
+ if LedgerManager.is_valid_derivation_path(derivation_path):
+ account = get_account_by_path(derivation_path)
+ return account.address
- @raise_as_hw_account_exception
- def sign_typed_hash(self, domain_hash, message_hash) -> Tuple[bytes, bytes, bytes]:
+ @raise_ledger_exception_as_hw_account_exception
+ def sign_typed_hash(self, domain_hash, message_hash) -> bytes:
signed = sign_typed_data_draft(
domain_hash, message_hash, self.derivation_path, self.dongle
)
- return (signed.v, signed.r, signed.s)
+
+ return signature_to_bytes(signed.v, signed.r, signed.s)
diff --git a/safe_cli/operators/hw_accounts/trezor_exceptions.py b/safe_cli/operators/hw_accounts/trezor_exceptions.py
new file mode 100644
index 00000000..ec96baa2
--- /dev/null
+++ b/safe_cli/operators/hw_accounts/trezor_exceptions.py
@@ -0,0 +1,37 @@
+import functools
+
+from trezorlib.exceptions import (
+ Cancelled,
+ OutdatedFirmwareError,
+ PinException,
+ TrezorFailure,
+)
+from trezorlib.transport import TransportException
+
+from safe_cli.operators.exceptions import HardwareWalletException
+from safe_cli.operators.hw_accounts.hw_account import InvalidDerivationPath
+
+
+class UnsupportedHwWalletException(Exception):
+ pass
+
+
+def raise_trezor_exception_as_hw_account_exception(function):
+ @functools.wraps(function)
+ def wrapper(*args, **kwargs):
+ try:
+ return function(*args, **kwargs)
+ except TrezorFailure as e:
+ raise HardwareWalletException(e.message)
+ except OutdatedFirmwareError:
+ raise HardwareWalletException("Trezor firmware version is not supported")
+ except PinException:
+ raise HardwareWalletException("Wrong PIN")
+ except Cancelled:
+ raise HardwareWalletException("Trezor operation was cancelled")
+ except TransportException:
+ raise HardwareWalletException("Trezor device is not connected")
+ except InvalidDerivationPath as e:
+ raise HardwareWalletException(e.message)
+
+ return wrapper
diff --git a/safe_cli/operators/hw_accounts/trezor_manager.py b/safe_cli/operators/hw_accounts/trezor_manager.py
index e8471240..16ec4d2b 100644
--- a/safe_cli/operators/hw_accounts/trezor_manager.py
+++ b/safe_cli/operators/hw_accounts/trezor_manager.py
@@ -1,22 +1,51 @@
-from typing import Tuple
+from functools import lru_cache
from eth_typing import ChecksumAddress
+from trezorlib import tools
+from trezorlib.client import TrezorClient, get_default_client
+from trezorlib.ethereum import get_address, sign_typed_data_hash
+from trezorlib.ui import ClickUI
from safe_cli.operators.hw_accounts.hw_account import HwAccount
+from safe_cli.operators.hw_accounts.trezor_exceptions import (
+ raise_trezor_exception_as_hw_account_exception,
+)
+
+
+@lru_cache(maxsize=None)
+@raise_trezor_exception_as_hw_account_exception
+def get_trezor_client() -> TrezorClient:
+ """
+ Return default trezor configuration that store passphrase on host.
+ This method is cached to share the same configuration between trezor calls while the class is not instantiated.
+ :return:
+ """
+ ui = ClickUI(passphrase_on_host=True)
+ client = get_default_client(ui=ui)
+ return client
class TrezorManager(HwAccount):
def __init__(self, derivation_path: str, address: ChecksumAddress):
- self.client = None
+ self.client = get_trezor_client()
super().__init__(derivation_path, address)
+ @raise_trezor_exception_as_hw_account_exception
def get_address_by_derivation_path(derivation_path: str) -> ChecksumAddress:
"""
:param derivation_path:
:return: public address for provided derivation_path
"""
- raise NotImplementedError
+ if TrezorManager.is_valid_derivation_path(derivation_path):
+ client = get_trezor_client()
+ address_n = tools.parse_path(derivation_path)
+ return get_address(client=client, n=address_n)
- def sign_typed_hash(self, domain_hash, message_hash) -> Tuple[bytes, bytes, bytes]:
- raise NotImplementedError
+ @raise_trezor_exception_as_hw_account_exception
+ def sign_typed_hash(self, domain_hash, message_hash) -> bytes:
+ address_n = tools.parse_path(self.derivation_path)
+ signed = sign_typed_data_hash(
+ self.client, n=address_n, domain_hash=domain_hash, message_hash=message_hash
+ )
+ return signed.signature
diff --git a/safe_cli/operators/safe_operator.py b/safe_cli/operators/safe_operator.py
index 4b29b125..c0ea22ad 100644
--- a/safe_cli/operators/safe_operator.py
+++ b/safe_cli/operators/safe_operator.py
@@ -270,14 +270,14 @@ def load_cli_owners(self, keys: List[str]):
except ValueError:
print_formatted_text(HTML(f"Cannot load key={key}"))
- def load_ledger_cli_owners(
- self, derivation_path: str = None, legacy_account: bool = False
+ def load_hw_wallet(
+ self, hw_wallet_type: HwWalletType, derivation_path: str, legacy_account: bool
):
- if not self.hw_account_manager.is_supported_hw_wallet(HwWalletType.LEDGER):
+ if not self.hw_account_manager.is_supported_hw_wallet(hw_wallet_type):
return None
if derivation_path is None:
ledger_accounts = self.hw_account_manager.get_accounts(
- HwWalletType.LEDGER, legacy_account=legacy_account
+ hw_wallet_type, legacy_account=legacy_account
)
if len(ledger_accounts) == 0:
return None
@@ -289,9 +289,7 @@ def load_ledger_cli_owners(
return None
_, derivation_path = ledger_accounts[option]
- address = self.hw_account_manager.add_account(
- HwWalletType.LEDGER, derivation_path
- )
+ address = self.hw_account_manager.add_account(hw_wallet_type, derivation_path)
balance = self.ethereum_client.get_balance(address)
print_formatted_text(
HTML(
@@ -301,6 +299,16 @@ def load_ledger_cli_owners(
)
)
+ def load_ledger_cli_owners(
+ self, derivation_path: str = None, legacy_account: bool = False
+ ):
+ self.load_hw_wallet(HwWalletType.LEDGER, derivation_path, legacy_account)
+
+ def load_trezor_cli_owners(
+ self, derivation_path: str = None, legacy_account: bool = False
+ ):
+ self.load_hw_wallet(HwWalletType.TREZOR, derivation_path, legacy_account)
+
def unload_cli_owners(self, owners: List[str]):
accounts_to_remove: Set[Account] = set()
for owner in owners:
diff --git a/safe_cli/prompt_parser.py b/safe_cli/prompt_parser.py
index 5673b8df..b6d6e70d 100644
--- a/safe_cli/prompt_parser.py
+++ b/safe_cli/prompt_parser.py
@@ -175,6 +175,12 @@ def load_ledger_cli_owners(args):
derivation_path=args.derivation_path, legacy_account=args.legacy_accounts
)
+ @safe_exception
+ def load_trezor_cli_owners(args):
+ safe_operator.load_trezor_cli_owners(
+ derivation_path=args.derivation_path, legacy_account=args.legacy_accounts
+ )
+
@safe_exception
def unload_cli_owners(args):
safe_operator.unload_cli_owners(args.addresses)
@@ -332,6 +338,19 @@ def remove_delegate(args):
)
parser_load_ledger_cli_owners.set_defaults(func=load_ledger_cli_owners)
+ parser_load_trezor_cli_owners = subparsers.add_parser("load_trezor_cli_owners")
+ parser_load_trezor_cli_owners.add_argument(
+ "--derivation-path",
+ type=str,
+ help="Load address for the provided derivation path",
+ )
+ parser_load_trezor_cli_owners.add_argument(
+ "--legacy-accounts",
+ action="store_true",
+ help="Enable search legacy accounts",
+ )
+ parser_load_trezor_cli_owners.set_defaults(func=load_trezor_cli_owners)
+
parser_unload_cli_owners = subparsers.add_parser("unload_cli_owners")
parser_unload_cli_owners.add_argument(
"addresses", type=check_ethereum_address, nargs="+"
diff --git a/safe_cli/safe_completer_constants.py b/safe_cli/safe_completer_constants.py
index 6ade9989..78314a65 100644
--- a/safe_cli/safe_completer_constants.py
+++ b/safe_cli/safe_completer_constants.py
@@ -25,6 +25,7 @@
"info": "(read-only)",
"load_cli_owners": " [...]",
"load_ledger_cli_owners": "[--legacy-accounts] [--derivation-path ]",
+ "load_trezor_cli_owners": "[--legacy-accounts] [--derivation-path ]",
"load_cli_owners_from_words": " ... ",
"refresh": "",
"remove_delegate": " ",
@@ -160,6 +161,9 @@
"load_ledger_cli_owners": HTML(
"Command load_ledger_cli_owners show a list of ledger addresses to choose between them "
),
+ "load_trezor_cli_owners": HTML(
+ "Command load_trezor_cli_owners show a list of trezor addresses to choose between them "
+ ),
"load_cli_owners_from_words": HTML(
"Command load_cli_owners_from_words will try to load owners via"
"seed_words. Only relevant accounts(owners) will be loaded"
diff --git a/tests/test_hw_account_manager.py b/tests/test_hw_account_manager.py
index c27acc07..6208d39b 100644
--- a/tests/test_hw_account_manager.py
+++ b/tests/test_hw_account_manager.py
@@ -1,4 +1,3 @@
-import sys
import unittest
from unittest import mock
from unittest.mock import MagicMock
@@ -16,25 +15,6 @@
class TestLedgerManager(SafeTestCaseMixin, unittest.TestCase):
def test_setup_hw_account_manager(self):
- # Shouldn't be supported any wallets
- with mock.patch(sys.modules, {"ledgereth": None, "trezorlib": None}):
- hw_acccount_manager = HwAccountManager()
- self.assertFalse(
- hw_acccount_manager.is_supported_hw_wallet(HwWalletType.LEDGER)
- )
- self.assertFalse(
- hw_acccount_manager.is_supported_hw_wallet(HwWalletType.TREZOR)
- )
- # Should support only Trezor
- with mock.patch.dict(sys.modules, {"ledgereth": None}):
- hw_acccount_manager = HwAccountManager()
- self.assertTrue(
- hw_acccount_manager.is_supported_hw_wallet(HwWalletType.TREZOR)
- )
- self.assertFalse(
- hw_acccount_manager.is_supported_hw_wallet(HwWalletType.LEDGER)
- )
-
# Should support Treezor and Ledger
hw_acccount_manager = HwAccountManager()
self.assertTrue(hw_acccount_manager.is_supported_hw_wallet(HwWalletType.TREZOR))
@@ -45,13 +25,12 @@ def test_setup_hw_account_manager(self):
"safe_cli.operators.hw_accounts.ledger_manager.LedgerManager.get_address_by_derivation_path",
autospec=True,
)
- def test_get_accounts_from_ledger(
- self, mock_get_address_by_derivation_path: MagicMock
- ):
+ def test_get_accounts(self, mock_get_address_by_derivation_path: MagicMock):
hw_account_manager = HwAccountManager()
addresses = [Account.create().address, Account.create().address]
derivation_paths = ["44'/60'/0'/0/0", "44'/60'/1'/0/0"]
mock_get_address_by_derivation_path.side_effect = addresses
+ # Choosing LEDGER because function is mocked for LEDGER
hw_accounts = hw_account_manager.get_accounts(
HwWalletType.LEDGER, number_accounts=2
)
@@ -67,14 +46,14 @@ def test_get_accounts_from_ledger(
"safe_cli.operators.hw_accounts.ledger_manager.LedgerManager.get_address_by_derivation_path",
autospec=True,
)
- def test_add_ledger_account(self, mock_get_address_by_derivation_path: MagicMock):
+ def test_add_account(self, mock_get_address_by_derivation_path: MagicMock):
hw_account_manager = HwAccountManager()
derivation_path = "44'/60'/0'/0"
account_address = Account.create().address
mock_get_address_by_derivation_path.return_value = account_address
self.assertEqual(len(hw_account_manager.accounts), 0)
-
+ # Choosing LEDGER because function is mocked for LEDGER
self.assertEqual(
hw_account_manager.add_account(HwWalletType.LEDGER, derivation_path),
account_address,
diff --git a/tests/test_ledger_manager.py b/tests/test_ledger_manager.py
index 9d4f5b7c..4a6de191 100644
--- a/tests/test_ledger_manager.py
+++ b/tests/test_ledger_manager.py
@@ -127,12 +127,8 @@ def test_sign_typed_hash(self, mock_init_dongle: MagicMock):
mock_init_dongle.return_value.exchange = MagicMock(
return_value=ledger_return_signature
)
- signature_v, signature_r, signature_s = ledger_manager.sign_typed_hash(
- encode_hash[1], encode_hash[2]
- )
- self.assertEqual(signature_v, v)
- self.assertEqual(signature_r, r)
- self.assertEqual(signature_s, s)
+ signature = ledger_manager.sign_typed_hash(encode_hash[1], encode_hash[2])
+ self.assertEqual(expected_signature, signature)
# Check that dongle exchange is called with the expected payload
# https://github.com/LedgerHQ/app-ethereum/blob/master/doc/ethapp.adoc#sign-eth-eip-712
diff --git a/tests/test_trezor_manager.py b/tests/test_trezor_manager.py
new file mode 100644
index 00000000..789ef449
--- /dev/null
+++ b/tests/test_trezor_manager.py
@@ -0,0 +1,129 @@
+import os
+import unittest
+from unittest import mock
+from unittest.mock import MagicMock
+
+from eth_account import Account
+from trezorlib.client import TrezorClient
+from trezorlib.exceptions import Cancelled, OutdatedFirmwareError, PinException
+from trezorlib.messages import EthereumTypedDataSignature
+from trezorlib.transport import TransportException
+from trezorlib.ui import ClickUI
+
+from gnosis.eth.eip712 import eip712_encode
+from gnosis.safe import SafeTx
+from gnosis.safe.tests.safe_test_case import SafeTestCaseMixin
+
+from safe_cli.operators.exceptions import HardwareWalletException
+from safe_cli.operators.hw_accounts.trezor_manager import TrezorManager
+
+
+class TestTrezorManager(SafeTestCaseMixin, unittest.TestCase):
+ @mock.patch(
+ "safe_cli.operators.hw_accounts.trezor_manager.get_trezor_client",
+ return_value=None,
+ )
+ def test_setup_trezor_manager(self, mock_trezor_client: MagicMock):
+ trezor_manager = TrezorManager("44'/60'/0'/0", Account.create().address)
+ self.assertIsNone(trezor_manager.client)
+
+ @mock.patch(
+ "safe_cli.operators.hw_accounts.trezor_manager.sign_typed_data_hash",
+ autospec=True,
+ )
+ @mock.patch(
+ "safe_cli.operators.hw_accounts.trezor_manager.get_address",
+ autospec=True,
+ )
+ @mock.patch(
+ "safe_cli.operators.hw_accounts.trezor_manager.get_trezor_client",
+ autospec=True,
+ )
+ def test_hw_device_exception(
+ self,
+ mock_trezor_client: MagicMock,
+ mock_trezor_get_address: MagicMock,
+ mock_trezor_sign: MagicMock,
+ ):
+ derivation_path = "44'/60'/0'/0"
+ transport_mock = MagicMock(auto_spec=True)
+ mock_trezor_client.return_value = TrezorClient(
+ transport_mock, ui=ClickUI(), _init_device=False
+ )
+ mock_trezor_client.return_value.is_outdated = MagicMock(return_value=False)
+ trezor_manager = TrezorManager(derivation_path, Account.create().address)
+
+ random_domain_bytes = os.urandom(32)
+ random_message_bytes = os.urandom(32)
+ mock_trezor_get_address.side_effect = TransportException
+ mock_trezor_sign.side_effect = TransportException
+ with self.assertRaises(HardwareWalletException):
+ TrezorManager.get_address_by_derivation_path(derivation_path)
+ with self.assertRaises(HardwareWalletException):
+ trezor_manager.sign_typed_hash(random_domain_bytes, random_message_bytes)
+
+ mock_trezor_get_address.side_effect = PinException
+ mock_trezor_sign.side_effect = PinException
+ with self.assertRaises(HardwareWalletException):
+ TrezorManager.get_address_by_derivation_path(derivation_path)
+ with self.assertRaises(HardwareWalletException):
+ trezor_manager.sign_typed_hash(random_domain_bytes, random_message_bytes)
+
+ mock_trezor_get_address.side_effect = Cancelled
+ mock_trezor_sign.side_effect = Cancelled
+ with self.assertRaises(HardwareWalletException):
+ TrezorManager.get_address_by_derivation_path(derivation_path)
+ with self.assertRaises(HardwareWalletException):
+ trezor_manager.sign_typed_hash(random_domain_bytes, random_message_bytes)
+
+ mock_trezor_get_address.side_effect = OutdatedFirmwareError
+ mock_trezor_sign.side_effect = OutdatedFirmwareError
+ with self.assertRaises(HardwareWalletException):
+ TrezorManager.get_address_by_derivation_path(derivation_path)
+ with self.assertRaises(HardwareWalletException):
+ trezor_manager.sign_typed_hash(random_domain_bytes, random_message_bytes)
+
+ @mock.patch(
+ "safe_cli.operators.hw_accounts.trezor_manager.get_trezor_client",
+ autospec=True,
+ )
+ def test_sign_typed_hash(self, mock_trezor_client):
+ owner = Account.create()
+ to = Account.create()
+ transport_mock = MagicMock(auto_spec=True)
+ mock_trezor_client.return_value = TrezorClient(
+ transport_mock, ui=ClickUI(), _init_device=False
+ )
+ mock_trezor_client.return_value.is_outdated = MagicMock(return_value=False)
+ trezor_manager = TrezorManager("44'/60'/0'/0", owner.address)
+
+ safe = self.deploy_test_safe(
+ owners=[owner.address],
+ threshold=1,
+ initial_funding_wei=self.w3.to_wei(0.1, "ether"),
+ )
+ safe_tx = SafeTx(
+ self.ethereum_client,
+ safe.address,
+ to.address,
+ 10,
+ b"",
+ 0,
+ 200000,
+ 200000,
+ self.gas_price,
+ None,
+ None,
+ safe_nonce=0,
+ )
+ encode_hash = eip712_encode(safe_tx.eip712_structured_data)
+ expected_signature = safe_tx.sign(owner.key)
+
+ trezor_return_signature = EthereumTypedDataSignature(
+ signature=expected_signature
+ )
+ mock_trezor_client.return_value.call = MagicMock(
+ return_value=trezor_return_signature
+ )
+ signature = trezor_manager.sign_typed_hash(encode_hash[1], encode_hash[2])
+ self.assertEqual(expected_signature, signature)