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)