From 5172f54cd06963137be6d67f4eb63cad93ed1807 Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Wed, 2 Aug 2023 09:59:51 -0500 Subject: [PATCH] test: fix tests --- ape_ledger/_cli.py | 12 ++-- ape_ledger/accounts.py | 1 + ape_ledger/choices.py | 9 ++- ape_ledger/client.py | 10 ++- ape_ledger/hdpath.py | 28 ++++++--- tests/conftest.py | 31 ++++++--- tests/test_accounts.py | 128 ++++++++++++++------------------------ tests/test_choices.py | 15 +++-- tests/test_integration.py | 55 ++++++++++------ 9 files changed, 160 insertions(+), 129 deletions(-) diff --git a/ape_ledger/_cli.py b/ape_ledger/_cli.py index b7f896b..78f4e63 100644 --- a/ape_ledger/_cli.py +++ b/ape_ledger/_cli.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Tuple, Union import click from ape import accounts @@ -15,7 +15,12 @@ from ape_ledger.accounts import LedgerAccount from ape_ledger.choices import AddressPromptChoice from ape_ledger.exceptions import LedgerSigningError -from ape_ledger.hdpath import HDBasePath +from ape_ledger.hdpath import HDAccountPath, HDBasePath + + +def _select_account(hd_path: Union[HDBasePath, str]) -> Tuple[str, HDAccountPath]: + choices = AddressPromptChoice(hd_path) + return choices.get_user_selected_account() @click.group(short_help="Manage Ledger accounts") @@ -66,8 +71,7 @@ def _get_ledger_accounts() -> List[LedgerAccount]: def add(cli_ctx, alias, hd_path): """Add an account from your Ledger hardware wallet""" - choices = AddressPromptChoice(hd_path) - address, account_hd_path = choices.get_user_selected_account() + address, account_hd_path = _select_account(hd_path) container = accounts.containers.get("ledger") container.save_account(alias, address, str(account_hd_path)) cli_ctx.logger.success(f"Account '{address}' successfully added with alias '{alias}'.") diff --git a/ape_ledger/accounts.py b/ape_ledger/accounts.py index cd57b6c..03eec79 100644 --- a/ape_ledger/accounts.py +++ b/ape_ledger/accounts.py @@ -134,6 +134,7 @@ def sign_transaction(self, txn: TransactionAPI, **kwargs) -> Optional[Transactio else: raise TypeError(type(txn)) + _echo_object_to_sign(txn) v, r, s = self._client.sign_transaction(txn_dict) txn.signature = TransactionSignature( v=v, diff --git a/ape_ledger/choices.py b/ape_ledger/choices.py index 2e6c0dc..32d5adb 100644 --- a/ape_ledger/choices.py +++ b/ape_ledger/choices.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, Tuple +from typing import Any, Optional, Tuple, Union import click from ape.cli import PromptChoice @@ -17,10 +17,13 @@ class AddressPromptChoice(PromptChoice): def __init__( self, - hd_path: HDBasePath, + hd_path: Union[HDBasePath, str], index_offset: int = 0, page_size: int = DEFAULT_PAGE_SIZE, ): + if isinstance(hd_path, str): + hd_path = HDBasePath(base_path=hd_path) + self._hd_root_path = hd_path self._index_offset = index_offset self._page_size = page_size @@ -99,4 +102,4 @@ def _get_address(self, account_id: int) -> str: return device.get_address() -__all__ = ["AddressPromptChoice", "PromptChoice"] +__all__ = ["AddressPromptChoice"] diff --git a/ape_ledger/client.py b/ape_ledger/client.py index cc65b04..5c8e9ed 100644 --- a/ape_ledger/client.py +++ b/ape_ledger/client.py @@ -6,7 +6,7 @@ from ape.logging import LogLevel, logger from ledgerblue.comm import HIDDongleHIDAPI, getDongle # type: ignore from ledgereth.accounts import get_account_by_path -from ledgereth.messages import sign_message +from ledgereth.messages import sign_message, sign_typed_data_draft from ledgereth.transactions import SignedType2Transaction, create_transaction from ape_ledger.hdpath import HDAccountPath @@ -57,7 +57,13 @@ def get_address(self) -> str: return get_account_by_path(self._account, dongle=self.dongle).address def sign_message(self, text: bytes) -> Tuple[int, int, int]: - signed_msg = sign_message(text, sender_path=self._account) + signed_msg = sign_message(text, sender_path=self._account, dongle=self.dongle) + return signed_msg.v, signed_msg.r, signed_msg.s + + def sign_typed_data(self, domain_hash: bytes, message_hash: bytes) -> Tuple[int, int, int]: + signed_msg = sign_typed_data_draft( + domain_hash, message_hash, sender_path=self._account, dongle=self.dongle + ) return signed_msg.v, signed_msg.r, signed_msg.s def sign_transaction(self, txn: Dict) -> Tuple[int, int, int]: diff --git a/ape_ledger/hdpath.py b/ape_ledger/hdpath.py index 95027fa..409afdc 100644 --- a/ape_ledger/hdpath.py +++ b/ape_ledger/hdpath.py @@ -1,4 +1,5 @@ import struct +from typing import Optional, Union class HDPath: @@ -8,12 +9,18 @@ class HDPath: as well as the derivation HD path class :class:`~ape_ledger.hdpath.HDBasePath`. """ - def __init__(self, path: str): - path = path.rstrip("/") - if not path.startswith("m/"): + def __init__(self, path: Union[str, "HDBasePath"]): + if not isinstance(path, str) and hasattr(path, "path"): + # NOTE: Using getattr for mypy + path_str = getattr(path, "path") + else: + path_str = path + + path_str = path_str.rstrip("/") + if not path_str.startswith("m/"): raise ValueError("HD path must begin with m/") - self.path = path + self.path = path_str def __str__(self): return self.path @@ -64,11 +71,16 @@ class HDBasePath(HDPath): :class:`~ape_ledger.hdpath.HDAccountPath`. """ - def __init__(self, base_path=None): + def __init__(self, base_path: Optional[Union[str, "HDBasePath"]] = None): base_path = base_path or "m/44'/60'/{x}'/0/0" - base_path = base_path.rstrip("/") - base_path = base_path if "{x}" in base_path else f"{base_path}/{{x}}" - super().__init__(base_path) + if not isinstance(base_path, str) and hasattr(base_path, "path"): + base_path_str = base_path.path + else: + base_path_str = base_path + + base_path_str = base_path_str.rstrip("/") + base_path_str = base_path_str if "{x}" in base_path_str else f"{base_path_str}/{{x}}" + super().__init__(base_path_str) def get_account_path(self, account_id) -> HDAccountPath: return HDAccountPath(self.path.format(x=str(account_id))) diff --git a/tests/conftest.py b/tests/conftest.py index 2340c73..1cc4977 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ from ape_ledger.client import LedgerDeviceClient TEST_ALIAS = "TestAlias" -TEST_HD_PATH = "m/44'/60'/0'/0/0" +TEST_HD_PATH = "m/44'/60'/{x}'/0/0" @pytest.fixture @@ -66,22 +66,26 @@ def msg_signature(account_0): @pytest.fixture -def tx_signature(account_0, account_1): - txn = account_0.transfer(account_1, "1 gwei") +def receipt(account_0, account_1): + return account_0.transfer(account_1, "1 gwei") + + +@pytest.fixture +def tx_signature(receipt): return ( - txn.signature.v, - int(HexBytes(txn.signature.r).hex(), 16), - int(HexBytes(txn.signature.s).hex(), 16), + receipt.signature.v, + int(HexBytes(receipt.signature.r).hex(), 16), + int(HexBytes(receipt.signature.s).hex(), 16), ) @pytest.fixture(autouse=True) def mock_device(mocker, hd_path, account_addresses, msg_signature, tx_signature): device = mocker.MagicMock(spec=LedgerDeviceClient) - patch = mocker.patch("ape_ledger.client.get_device") - patch.return_value = device device._account = hd_path - device.get_address.side_effect = lambda a: account_addresses[a] + device.get_address.side_effect = ( + lambda *args, **kwargs: account_addresses[args[0]] if args else account_addresses[0] + ) device.sign_message.side_effect = lambda *args, **kwargs: msg_signature device.sign_transaction.side_effect = lambda *args, **kwargs: tx_signature return device @@ -107,3 +111,12 @@ def fn(account_path, expected_address=None, expected_hdpath="m/44'/60'/0'/0/0"): assert account_data["hdpath"] == expected_hdpath return fn + + +@pytest.fixture +def device_factory(mocker, mock_device): + def fn(module): + patch = mocker.patch(f"ape_ledger.{module}.get_device") + patch.return_value = mock_device + + return fn diff --git a/tests/test_accounts.py b/tests/test_accounts.py index 269d7dc..f6746fe 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -12,6 +12,12 @@ from eth_account.messages import SignableMessage from ape_ledger.accounts import AccountContainer, LedgerAccount +from ape_ledger.exceptions import LedgerSigningError + + +@pytest.fixture(autouse=True) +def patch_device(device_factory): + return device_factory("accounts") class Person(EIP712Type): @@ -105,13 +111,6 @@ def account(mock_container, create_account, hd_path): yield LedgerAccount(container=mock_container, account_file_path=path) -@pytest.fixture -def sign_txn_spy(mocker): - spy = mocker.spy(LedgerAccount, "_client") - spy.sign_transaction.return_value = (0, b"r", b"s") - return spy - - class TestAccountContainer: def test_save_account(self, mock_container, alias, address, hd_path, assert_account): with tempfile.TemporaryDirectory() as temp_dir: @@ -129,84 +128,53 @@ def test_address_returns_address_from_file(self, account, address): def test_hdpath_returns_address_from_file(self, account, hd_path): assert account.hdpath.path == hd_path - def test_sign_message_personal(self, account, capsys, mock_device): + def test_sign_message_personal(self, account, capsys, mock_device, msg_signature): message = SignableMessage( version=b"E", header=b"thereum Signed Message:\n6", body=b"I\xe2\x99\xa5SF" ) - actual_v, actual_r, actual_s = account.sign_message(message) - - assert actual_v == 27 - assert actual_r == b"r" - assert actual_s == b"s" - mock_device.assert_called_once_with(message.body) + v, r, s = account.sign_message(message) + assert (v, int(r.hex(), 16), int(s.hex(), 16)) == msg_signature + mock_device.sign_message.assert_called_once_with(message.body) + output = capsys.readouterr() + assert str(message) in output.out + assert "Please follow the prompts on your device." in output.out + def test_sign_message_typed(self, account, capsys, msg_signature): + message = TEST_TYPED_MESSAGE.signable_message + v, r, s = account.sign_message(message) + assert (v, int(r.hex(), 16), int(s.hex(), 16)) == msg_signature output = capsys.readouterr() assert str(message) in output.out assert "Please follow the prompts on your device." in output.out - # - # def test_sign_message_typed(self, mocker, account, account_connection, capsys): - # spy = mocker.spy(LedgerAccount, "_client") - # spy.sign_typed_data.return_value = (27, b"r", b"s") - # - # message = TEST_TYPED_MESSAGE.signable_message - # actual_v, actual_r, actual_s = account.sign_message(message) - # - # assert actual_v == 27 - # assert actual_r == b"r" - # assert actual_s == b"s" - # spy.sign_typed_data.assert_called_once_with(message.header, message.body) - # - # output = capsys.readouterr() - # assert str(message) in output.out - # assert "Please follow the prompts on your device." in output.out - # - # def test_sign_message_unsupported(self, account, account_connection, capsys): - # unsupported_version = b"X" - # message = SignableMessage( - # version=unsupported_version, - # header=b"thereum Signed Message:\n6", - # body=b"I\xe2\x99\xa5SF", - # ) - # with pytest.raises(LedgerSigningError) as err: - # account.sign_message(message) - # - # actual = str(err.value) - # expected = f"Unsupported message-signing specification, (version={unsupported_version})." - # assert actual == expected - # - # output = capsys.readouterr() - # assert str(message) not in output.out - # assert "Please follow the prompts on your device." not in output.out - # - # @pytest.mark.parametrize( - # "txn,expected", - # ( - # ( - # TEST_STATIC_FEE_TXN, - # "f904fa800102808502540be400b904e7608060405234801561001057600080fd5b50600080546001600160a01b031916331790556003805460ff191660011790556104a88061003f6000396000f3fe6080604052600436106100705760003560e01c80633e47d6f31161004e5780633e47d6f3146100d45780638da5cb5b14610119578063b60d42881461014a578063dc0d3dff1461015257610070565b80631229dc9e14610075578063238dafe0146100a35780633ccfd60b146100cc575b600080fd5b34801561008157600080fd5b506100a16004803603602081101561009857600080fd5b5035151561017c565b005b3480156100af57600080fd5b506100b86101dc565b604080519115158252519081900360200190f35b6100a16101e5565b3480156100e057600080fd5b50610107600480360360208110156100f757600080fd5b50356001600160a01b03166102dd565b60408051918252519081900360200190f35b34801561012557600080fd5b5061012e6102ef565b604080516001600160a01b039092168252519081900360200190f35b6100a16102fe565b34801561015e57600080fd5b5061012e6004803603602081101561017557600080fd5b50356103a4565b6000546001600160a01b031633146101c9576040805162461bcd60e51b815260206004820152600b60248201526a08585d5d1a1bdc9a5e995960aa1b604482015290519081900360640190fd5b6003805460ff1916911515919091179055565b60035460ff1681565b6000546001600160a01b03163314610232576040805162461bcd60e51b815260206004820152600b60248201526a08585d5d1a1bdc9a5e995960aa1b604482015290519081900360640190fd5b60035460ff1661024157600080fd5b60405133904780156108fc02916000818181858888f1935050505015801561026d573d6000803e3d6000fd5b5060005b6002548110156102bc5760006002828154811061028a57fe5b60009182526020808320909101546001600160a01b031682526001908190526040822091909155919091019050610271565b5060408051600081526020810191829052516102da916002916103cb565b50565b60016020526000908152604090205481565b6000546001600160a01b031681565b60035460ff1661030d57600080fd5b6000341161034c5760405162461bcd60e51b81526004018080602001828103825260238152602001806104506023913960400191505060405180910390fd5b33600081815260016020819052604082208054340190556002805491820181559091527f405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace0180546001600160a01b0319169091179055565b600281815481106103b157fe5b6000918252602090912001546001600160a01b0316905081565b828054828255906000526020600020908101928215610420579160200282015b8281111561042057825182546001600160a01b0319166001600160a01b039091161782556020909201916001909101906103eb565b5061042c929150610430565b5090565b5b8082111561042c5780546001600160a01b031916815560010161043156fe46756e6420616d6f756e74206d7573742062652067726561746572207468616e20302ea26469706673582212205c2ee1b9bdde0b2e60696f29f2b6f1d5ce1dd75f8bfdf6fb5414231a12cfa7ff64736f6c634300060c00338308d9238080", # noqa: E501 - # ), - # ( - # TEST_STATIC_FEE_TXN_WITH_RECEIVER, - # "f9050e80010294b0b0b0b0b0b0b0000000000000000000000000008502540be400b904e7608060405234801561001057600080fd5b50600080546001600160a01b031916331790556003805460ff191660011790556104a88061003f6000396000f3fe6080604052600436106100705760003560e01c80633e47d6f31161004e5780633e47d6f3146100d45780638da5cb5b14610119578063b60d42881461014a578063dc0d3dff1461015257610070565b80631229dc9e14610075578063238dafe0146100a35780633ccfd60b146100cc575b600080fd5b34801561008157600080fd5b506100a16004803603602081101561009857600080fd5b5035151561017c565b005b3480156100af57600080fd5b506100b86101dc565b604080519115158252519081900360200190f35b6100a16101e5565b3480156100e057600080fd5b50610107600480360360208110156100f757600080fd5b50356001600160a01b03166102dd565b60408051918252519081900360200190f35b34801561012557600080fd5b5061012e6102ef565b604080516001600160a01b039092168252519081900360200190f35b6100a16102fe565b34801561015e57600080fd5b5061012e6004803603602081101561017557600080fd5b50356103a4565b6000546001600160a01b031633146101c9576040805162461bcd60e51b815260206004820152600b60248201526a08585d5d1a1bdc9a5e995960aa1b604482015290519081900360640190fd5b6003805460ff1916911515919091179055565b60035460ff1681565b6000546001600160a01b03163314610232576040805162461bcd60e51b815260206004820152600b60248201526a08585d5d1a1bdc9a5e995960aa1b604482015290519081900360640190fd5b60035460ff1661024157600080fd5b60405133904780156108fc02916000818181858888f1935050505015801561026d573d6000803e3d6000fd5b5060005b6002548110156102bc5760006002828154811061028a57fe5b60009182526020808320909101546001600160a01b031682526001908190526040822091909155919091019050610271565b5060408051600081526020810191829052516102da916002916103cb565b50565b60016020526000908152604090205481565b6000546001600160a01b031681565b60035460ff1661030d57600080fd5b6000341161034c5760405162461bcd60e51b81526004018080602001828103825260238152602001806104506023913960400191505060405180910390fd5b33600081815260016020819052604082208054340190556002805491820181559091527f405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace0180546001600160a01b0319169091179055565b600281815481106103b157fe5b6000918252602090912001546001600160a01b0316905081565b828054828255906000526020600020908101928215610420579160200282015b8281111561042057825182546001600160a01b0319166001600160a01b039091161782556020909201916001909101906103eb565b5061042c929150610430565b5090565b5b8082111561042c5780546001600160a01b031916815560010161043156fe46756e6420616d6f756e74206d7573742062652067726561746572207468616e20302ea26469706673582212205c2ee1b9bdde0b2e60696f29f2b6f1d5ce1dd75f8bfdf6fb5414231a12cfa7ff64736f6c634300060c00338308d9238080", # noqa: E501 - # ), - # ( - # TEST_DYNAMIC_FEE_TXN, - # "02f905018308d92380839896808411e1a30002808502540be400b904e7608060405234801561001057600080fd5b50600080546001600160a01b031916331790556003805460ff191660011790556104a88061003f6000396000f3fe6080604052600436106100705760003560e01c80633e47d6f31161004e5780633e47d6f3146100d45780638da5cb5b14610119578063b60d42881461014a578063dc0d3dff1461015257610070565b80631229dc9e14610075578063238dafe0146100a35780633ccfd60b146100cc575b600080fd5b34801561008157600080fd5b506100a16004803603602081101561009857600080fd5b5035151561017c565b005b3480156100af57600080fd5b506100b86101dc565b604080519115158252519081900360200190f35b6100a16101e5565b3480156100e057600080fd5b50610107600480360360208110156100f757600080fd5b50356001600160a01b03166102dd565b60408051918252519081900360200190f35b34801561012557600080fd5b5061012e6102ef565b604080516001600160a01b039092168252519081900360200190f35b6100a16102fe565b34801561015e57600080fd5b5061012e6004803603602081101561017557600080fd5b50356103a4565b6000546001600160a01b031633146101c9576040805162461bcd60e51b815260206004820152600b60248201526a08585d5d1a1bdc9a5e995960aa1b604482015290519081900360640190fd5b6003805460ff1916911515919091179055565b60035460ff1681565b6000546001600160a01b03163314610232576040805162461bcd60e51b815260206004820152600b60248201526a08585d5d1a1bdc9a5e995960aa1b604482015290519081900360640190fd5b60035460ff1661024157600080fd5b60405133904780156108fc02916000818181858888f1935050505015801561026d573d6000803e3d6000fd5b5060005b6002548110156102bc5760006002828154811061028a57fe5b60009182526020808320909101546001600160a01b031682526001908190526040822091909155919091019050610271565b5060408051600081526020810191829052516102da916002916103cb565b50565b60016020526000908152604090205481565b6000546001600160a01b031681565b60035460ff1661030d57600080fd5b6000341161034c5760405162461bcd60e51b81526004018080602001828103825260238152602001806104506023913960400191505060405180910390fd5b33600081815260016020819052604082208054340190556002805491820181559091527f405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace0180546001600160a01b0319169091179055565b600281815481106103b157fe5b6000918252602090912001546001600160a01b0316905081565b828054828255906000526020600020908101928215610420579160200282015b8281111561042057825182546001600160a01b0319166001600160a01b039091161782556020909201916001909101906103eb565b5061042c929150610430565b5090565b5b8082111561042c5780546001600160a01b031916815560010161043156fe46756e6420616d6f756e74206d7573742062652067726561746572207468616e20302ea26469706673582212205c2ee1b9bdde0b2e60696f29f2b6f1d5ce1dd75f8bfdf6fb5414231a12cfa7ff64736f6c634300060c0033c0", # noqa: E501 - # ), - # ( - # TEST_DYNAMIC_FEE_TXN_WITH_RECEIVER, - # "02f905158308d92380839896808411e1a3000294b0b0b0b0b0b0b0000000000000000000000000008502540be400b904e7608060405234801561001057600080fd5b50600080546001600160a01b031916331790556003805460ff191660011790556104a88061003f6000396000f3fe6080604052600436106100705760003560e01c80633e47d6f31161004e5780633e47d6f3146100d45780638da5cb5b14610119578063b60d42881461014a578063dc0d3dff1461015257610070565b80631229dc9e14610075578063238dafe0146100a35780633ccfd60b146100cc575b600080fd5b34801561008157600080fd5b506100a16004803603602081101561009857600080fd5b5035151561017c565b005b3480156100af57600080fd5b506100b86101dc565b604080519115158252519081900360200190f35b6100a16101e5565b3480156100e057600080fd5b50610107600480360360208110156100f757600080fd5b50356001600160a01b03166102dd565b60408051918252519081900360200190f35b34801561012557600080fd5b5061012e6102ef565b604080516001600160a01b039092168252519081900360200190f35b6100a16102fe565b34801561015e57600080fd5b5061012e6004803603602081101561017557600080fd5b50356103a4565b6000546001600160a01b031633146101c9576040805162461bcd60e51b815260206004820152600b60248201526a08585d5d1a1bdc9a5e995960aa1b604482015290519081900360640190fd5b6003805460ff1916911515919091179055565b60035460ff1681565b6000546001600160a01b03163314610232576040805162461bcd60e51b815260206004820152600b60248201526a08585d5d1a1bdc9a5e995960aa1b604482015290519081900360640190fd5b60035460ff1661024157600080fd5b60405133904780156108fc02916000818181858888f1935050505015801561026d573d6000803e3d6000fd5b5060005b6002548110156102bc5760006002828154811061028a57fe5b60009182526020808320909101546001600160a01b031682526001908190526040822091909155919091019050610271565b5060408051600081526020810191829052516102da916002916103cb565b50565b60016020526000908152604090205481565b6000546001600160a01b031681565b60035460ff1661030d57600080fd5b6000341161034c5760405162461bcd60e51b81526004018080602001828103825260238152602001806104506023913960400191505060405180910390fd5b33600081815260016020819052604082208054340190556002805491820181559091527f405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace0180546001600160a01b0319169091179055565b600281815481106103b157fe5b6000918252602090912001546001600160a01b0316905081565b828054828255906000526020600020908101928215610420579160200282015b8281111561042057825182546001600160a01b0319166001600160a01b039091161782556020909201916001909101906103eb565b5061042c929150610430565b5090565b5b8082111561042c5780546001600160a01b031916815560010161043156fe46756e6420616d6f756e74206d7573742062652067726561746572207468616e20302ea26469706673582212205c2ee1b9bdde0b2e60696f29f2b6f1d5ce1dd75f8bfdf6fb5414231a12cfa7ff64736f6c634300060c0033c0", # noqa: E501 - # ), - # ), - # ) - # def test_sign_transaction( - # self, txn, expected, sign_txn_spy, account, account_connection, capsys - # ): - # account.sign_transaction(txn) - # actual = sign_txn_spy.sign_transaction.call_args[0][0].hex() - # assert actual == expected - # - # output = capsys.readouterr() - # assert str(txn) in output.out - # assert "Please follow the prompts on your device." in output.out + def test_sign_message_unsupported(self, account, capsys): + unsupported_version = b"X" + message = SignableMessage( + version=unsupported_version, + header=b"thereum Signed Message:\n6", + body=b"I\xe2\x99\xa5SF", + ) + expected = rf"Unsupported message-signing specification, \(version={unsupported_version}\)." + with pytest.raises(LedgerSigningError, match=expected): + account.sign_message(message) + + output = capsys.readouterr() + assert str(message) not in output.out + assert "Please follow the prompts on your device." not in output.out + + @pytest.mark.parametrize( + "txn", + ( + TEST_STATIC_FEE_TXN, + TEST_STATIC_FEE_TXN_WITH_RECEIVER, + TEST_DYNAMIC_FEE_TXN, + TEST_DYNAMIC_FEE_TXN_WITH_RECEIVER, + ), + ) + def test_sign_transaction(self, txn, mock_device, account, capsys, tx_signature): + actual = account.sign_transaction(txn) + v, r, s = actual.signature + assert (v, int(r.hex(), 16), int(s.hex(), 16)) == tx_signature + output = capsys.readouterr() + assert str(txn) in output.out + assert "Please follow the prompts on your device." in output.out diff --git a/tests/test_choices.py b/tests/test_choices.py index f7f3d6b..ba02a9f 100644 --- a/tests/test_choices.py +++ b/tests/test_choices.py @@ -1,23 +1,28 @@ +import pytest + from ape_ledger.choices import AddressPromptChoice -from .conftest import TEST_ADDRESS + +@pytest.fixture(autouse=True) +def patch_device(device_factory): + return device_factory("choices") class TestAddressPromptChoice: - def test_get_user_selected_account(self, mocker, mock_ethereum_app): + def test_get_user_selected_account(self, mocker, hd_path, address): mock_prompt = mocker.patch("ape_ledger.choices.click.prompt") - choices = AddressPromptChoice(mock_ethereum_app) + choices = AddressPromptChoice(hd_path) choices._choice_index = 3 choices._index_offset = 2 # `None` means the user hasn't selected yeet # And is entering other keys, possible the paging keys. - mock_prompt_return_values = iter((None, None, None, None, TEST_ADDRESS, None)) + mock_prompt_return_values = iter((None, None, None, None, address, None)) def _side_effect(*args, **kwargs): return next(mock_prompt_return_values) mock_prompt.side_effect = _side_effect address, hdpath = choices.get_user_selected_account() - assert address == TEST_ADDRESS + assert address == address assert str(hdpath) == f"m/44'/60'/{choices._choice_index + choices._index_offset}'/0/0" diff --git a/tests/test_integration.py b/tests/test_integration.py index 88c9e6b..ff5f968 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -11,7 +11,12 @@ def _get_container(): @pytest.fixture def alias(): - return "__integration_test_alias__" + val = "__integration_test_alias__" + container = _get_container() + if val in [a.alias for a in container.accounts]: + container.delete_account(val) + + return val @pytest.fixture @@ -37,6 +42,19 @@ def clean_after(runner): _clean_up(runner) +@pytest.fixture +def choices(mocker): + def fn(addr, account_id): + patch = mocker.patch("ape_ledger._cli._select_account") + + def se(hd_path): + return addr, HDBasePath(hd_path).get_account_path(account_id) + + patch.side_effect = se + + return fn + + def _clean_up(runner): runner.invoke(cli, ["ledger", "delete", alias], input="y") @@ -47,47 +65,48 @@ def _get_account_path(alias=alias): @pytest.mark.parametrize("cmd", (["ledger", "list"], ["accounts", "list", "--all"])) -def test_list(runner, existing_account, cmd, address): +def test_list(runner, existing_account, cmd, address, alias): result = runner.invoke(cli, cmd) assert result.exit_code == 0, result.output assert alias in result.output assert address.lower() in result.output.lower() -def test_add(runner, mock_device_connection, assert_account, address, alias): - selected_account_id = 0 - result = runner.invoke(cli, ["ledger", "add", alias], input=str(selected_account_id)) +def test_add(runner, assert_account, address, alias, choices, hd_path): + container = _get_container() + choices(address, 2) + result = runner.invoke(cli, ["ledger", "add", alias]) assert result.exit_code == 0, result.output assert f"SUCCESS: Account '{address}' successfully added with alias '{alias}'." in result.output - container = _get_container() expected_path = container.data_folder.joinpath(f"{alias}.json") - expected_hd_path = f"m/44'/60'/{selected_account_id}'/0/0" + expected_hd_path = "m/44'/60'/2'/0/0" assert_account(expected_path, expected_hdpath=expected_hd_path) -def test_add_when_hd_path_specified( - runner, mock_ethereum_app, mock_device_connection, alias, address, hd_path, assert_account -): +def test_add_when_hd_path_specified(runner, alias, address, hd_path, assert_account, choices): test_hd_path = "m/44'/60'/0'" - mock_ethereum_app.hd_root_path = HDBasePath(test_hd_path) - - selected_account_id = 0 + container = _get_container() + choices(address, 2) result = runner.invoke( cli, ["ledger", "add", alias, "--hd-path", test_hd_path], - input=str(selected_account_id), ) assert result.exit_code == 0, result.output assert f"SUCCESS: Account '{address}' successfully added with alias '{alias}'." in result.output - expected_path = hd_path - expected_hd_path = f"m/44'/60'/0'/{selected_account_id}" + expected_path = container.data_folder.joinpath(f"{alias}.json") + expected_hd_path = "m/44'/60'/0'/2" assert_account(expected_path, expected_hdpath=expected_hd_path) -def test_add_alias_already_exists(runner, mock_device_connection, existing_account): - result = runner.invoke(cli, ["ledger", "add", alias], input="0") +def test_add_alias_already_exists(runner, existing_account, choices, address, alias): + choices(address, 2) + + # Ensure exists + runner.invoke(cli, ["ledger", "add", alias]) + + result = runner.invoke(cli, ["ledger", "add", alias]) assert result.exit_code == 1, result.output assert ( f"ERROR: (AliasAlreadyInUseError) Account with alias '{alias}' already in use."