diff --git a/.mdformat.toml b/.mdformat.toml deleted file mode 100644 index 01b2fb0..0000000 --- a/.mdformat.toml +++ /dev/null @@ -1 +0,0 @@ -number = true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9cc7305..e15dfcf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-yaml @@ -10,7 +10,7 @@ repos: - id: isort - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.12.0 hooks: - id: black name: black @@ -21,7 +21,7 @@ repos: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 + rev: v1.7.1 hooks: - id: mypy additional_dependencies: [types-setuptools, pydantic] diff --git a/ape_ledger/accounts.py b/ape_ledger/accounts.py index 265bab4..2f02ecb 100644 --- a/ape_ledger/accounts.py +++ b/ape_ledger/accounts.py @@ -1,21 +1,23 @@ import json from pathlib import Path -from typing import Dict, Iterator, Optional, Union +from typing import Any, Dict, Iterator, Optional -import click +import rich from ape.api import AccountAPI, AccountContainerAPI, TransactionAPI from ape.types import AddressType, MessageSignature, TransactionSignature from ape_ethereum.transactions import DynamicFeeTransaction, StaticFeeTransaction -from eth_account.messages import SignableMessage +from dataclassy import asdict +from eip712 import EIP712Message, EIP712Type +from eth_account.messages import SignableMessage, encode_defunct +from eth_pydantic_types import HexBytes from eth_utils import is_0x_prefixed, to_bytes -from hexbytes import HexBytes from ape_ledger.client import LedgerDeviceClient, get_device from ape_ledger.exceptions import LedgerSigningError from ape_ledger.hdpath import HDAccountPath -def _to_bytes(val): +def _to_bytes(val) -> bytes: if val is None: return b"" elif isinstance(val, str) and is_0x_prefixed(val): @@ -65,8 +67,28 @@ def delete_account(self, alias: str): path.unlink(missing_ok=True) -def _echo_object_to_sign(obj: Union[TransactionAPI, SignableMessage]): - click.echo(f"{obj}\nPlease follow the prompts on your device.") +def _echo_object_to_sign(obj: Any): + suffix = "Please follow the prompts on your device." + if isinstance(obj, EIP712Message): + + def make_str(val) -> str: + if isinstance(val, dict): + return ", ".join([f"{k}={make_str(v)}" for k, v in val.items()]) + elif isinstance(val, EIP712Type): + subfields_str = make_str(asdict(val)) + return f"{repr(val)}({subfields_str})" + elif isinstance(val, (tuple, list, set)): + inner = ", ".join([make_str(x) for x in val]) + return f"[{inner}]" + else: + return f"{val}" + + fields_str = make_str(obj._body_["message"]) + message_str = f"{repr(obj)}({fields_str})" + else: + message_str = f"{obj}" + + rich.print(f"{message_str}\n{suffix}") class LedgerAccount(AccountAPI): @@ -94,20 +116,56 @@ def hdpath(self) -> HDAccountPath: def account_file(self) -> dict: return json.loads(self.account_file_path.read_text()) - def sign_message(self, msg: SignableMessage) -> Optional[MessageSignature]: - version = msg.version - if version == b"E": - _echo_object_to_sign(msg) - signed_msg = self._client.sign_message(msg.body) - elif version == b"\x01": - _echo_object_to_sign(msg) - header = _to_bytes(msg.header) - body = _to_bytes(msg.body) + def sign_message(self, msg: Any, **signer_options) -> Optional[MessageSignature]: + use_eip712_package = isinstance(msg, EIP712Message) + use_eip712 = use_eip712_package + if isinstance(msg, str): + msg_to_sign = encode_defunct(text=msg) + elif isinstance(msg, int): + msg_to_sign = encode_defunct(hexstr=HexBytes(msg).hex()) + elif isinstance(msg, bytes): + msg_to_sign = encode_defunct(primitive=msg) + elif use_eip712_package: + # Using eip712 package. + msg_to_sign = msg.signable_message + elif isinstance(msg, SignableMessage): + if msg.version == b"\x01": + # Using EIP-712 without eip712 package. + use_eip712 = True + elif msg.version != b"E": + try: + version_str = msg.version.decode("utf8") + except Exception: + try: + version_str = HexBytes(msg.version).hex() + except Exception: + version_str = "" + + raise LedgerSigningError( + f"Unsupported message-signing specification, (version={version_str})." + ) + + msg_to_sign = msg + else: + type_name = getattr(type(msg), "__name__", None) + if not type_name: + try: + type_name = str(type(msg)) + except Exception: + type_name = "" + + raise LedgerSigningError(f"Cannot sign messages of type '{type_name}'.") + + # Echo original message. + _echo_object_to_sign(msg) + + if use_eip712: + header = HexBytes(msg_to_sign.header) + body = HexBytes(msg_to_sign.body) signed_msg = self._client.sign_typed_data(header, body) + else: - raise LedgerSigningError( - f"Unsupported message-signing specification, (version={version!r})." - ) + signed_msg = self._client.sign_message(msg_to_sign.body) v, r, s = signed_msg return MessageSignature(v=v, r=HexBytes(r), s=HexBytes(s)) diff --git a/pyproject.toml b/pyproject.toml index 44ef44b..7423885 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,3 +37,6 @@ force_grid_wrap = 0 include_trailing_comma = true multi_line_output = 3 use_parentheses = true + +[tool.mdformat] +number = true diff --git a/setup.py b/setup.py index 6c7b262..1c85dd2 100644 --- a/setup.py +++ b/setup.py @@ -8,19 +8,20 @@ "pytest-xdist", # multi-process runner "pytest-cov", # Coverage analyzer plugin "pytest-mock", # For creating mocks - "eip712", "hypothesis>=6.2.0,<7.0", # Strategy-based fuzzer ], "lint": [ - "black>=23.9.1,<24", # auto-formatter and linter - "mypy>=1.5.1,<2", # Static type analyzer - "types-setuptools", # Needed due to mypy typeshed + "black>=23.12.0,<24", # Auto-formatter and linter + "mypy>=1.7.1,<2", # Static type analyzer + "types-setuptools", # Needed for mypy type shed "flake8>=6.1.0,<7", # Style linter + "flake8-breakpoint>=1.1.0,<2", # Detect breakpoints left in code + "flake8-print>=5.0.0,<6", # Detect print statements left in code "isort>=5.10.1,<6", # Import sorting linter "mdformat>=0.7.17", # Auto-formatter for markdown "mdformat-gfm>=0.3.5", # Needed for formatting GitHub-flavored markdown "mdformat-frontmatter>=0.4.1", # Needed for frontmatters-style headers in issue templates - "pydantic<2.0", # Needed for successful type check. TODO: Remove after full v2 support. + "mdformat-pyproject>=0.0.1", # Allows configuring in pyproject.toml ], "release": [ # `release` GitHub Action job uses this "setuptools", # Installation tool @@ -60,15 +61,16 @@ url="https://github.com/ApeWorX/ape-ledger", include_package_data=True, install_requires=[ + "eth-ape>=0.7.0,<0.8", + "ledgereth>=0.9.1,<0.10", "click", # Use same version as eth-ape - "eth-ape>=0.6.0,<0.7", - "ledgereth>=0.8.1,<0.9", - "importlib-metadata", + "rich", # Use same version as eth-ape + # ApeWorX-owned + "ethpm-types", # Use same version as eth-ape + "eip712", # Use same version as eth-ape # EF Dependencies "eth-account", # Use same version as eth-ape - "eth-typing", # Influenced by eth-ape "eth-utils", # Use same version as eth-ape - "hexbytes", # Use same version as eth-ape ], entry_points={ "ape_cli_subcommands": [ diff --git a/tests/conftest.py b/tests/conftest.py index 24f82b9..277d83e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,12 +2,9 @@ import pytest from ape import accounts, networks -from ape.api.accounts import AccountContainerAPI from click.testing import CliRunner from eth_account.messages import encode_defunct -from ethpm_types import HexBytes - -from ape_ledger.client import LedgerDeviceClient +from eth_pydantic_types import HexBytes TEST_ALIAS = "TestAlias" TEST_HD_PATH = "m/44'/60'/{x}'/0/0" @@ -81,7 +78,7 @@ def tx_signature(receipt): @pytest.fixture(autouse=True) def mock_device(mocker, hd_path, account_addresses, msg_signature, tx_signature): - device = mocker.MagicMock(spec=LedgerDeviceClient) + device = mocker.MagicMock() device._account = hd_path device.get_address.side_effect = ( lambda *args, **kwargs: account_addresses[args[0]] if args else account_addresses[0] @@ -94,7 +91,7 @@ def mock_device(mocker, hd_path, account_addresses, msg_signature, tx_signature) @pytest.fixture def mock_container(mocker): - return mocker.MagicMock(spec=AccountContainerAPI) + return mocker.MagicMock() @pytest.fixture diff --git a/tests/test_accounts.py b/tests/test_accounts.py index f6746fe..b36090d 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -10,6 +10,7 @@ from ape_ethereum.ecosystem import DynamicFeeTransaction, StaticFeeTransaction from eip712.messages import EIP712Message, EIP712Type from eth_account.messages import SignableMessage +from eth_pydantic_types import HexBytes from ape_ledger.accounts import AccountContainer, LedgerAccount from ape_ledger.exceptions import LedgerSigningError @@ -50,7 +51,7 @@ def build_transaction( txn.nonce = 0 txn.gas_limit = 2 txn.value = 10000000000 - txn.data = TEST_TXN_DATA + txn.data = HexBytes(TEST_TXN_DATA) if receiver: txn.receiver = receiver @@ -144,7 +145,7 @@ def test_sign_message_typed(self, account, capsys, msg_signature): 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 repr(message).replace("\n", "") in output.out.replace("\n", "") assert "Please follow the prompts on your device." in output.out def test_sign_message_unsupported(self, account, capsys): @@ -154,7 +155,8 @@ def test_sign_message_unsupported(self, account, capsys): header=b"thereum Signed Message:\n6", body=b"I\xe2\x99\xa5SF", ) - expected = rf"Unsupported message-signing specification, \(version={unsupported_version}\)." + version_str = unsupported_version.decode("utf8") + expected = rf"Unsupported message-signing specification, \(version={version_str}\)\." with pytest.raises(LedgerSigningError, match=expected): account.sign_message(message)