diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index aa36e33..af337dc 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -65,7 +65,7 @@ jobs: # TODO: Replace with macos-latest when works again. # https://github.com/actions/setup-python/issues/808 os: [ubuntu-latest, macos-12] # eventually add `windows-latest` - python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] + python-version: [3.9, "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3919289..a35de4c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: rev: 0.7.17 hooks: - id: mdformat - additional_dependencies: [mdformat-gfm, mdformat-frontmatter] + additional_dependencies: [mdformat-gfm, mdformat-frontmatter, mdformat-pyproject] default_language_version: python: python3 diff --git a/README.md b/README.md index 3d54d1c..0ef4397 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ to load and create accounts, sign messages, and sign transactions. ## Dependencies -- [python3](https://www.python.org/downloads) version 3.8 up to 3.12. +- [python3](https://www.python.org/downloads) version 3.9 up to 3.12. ## Installation diff --git a/ape_ledger/_cli.py b/ape_ledger/_cli.py index 77fe26f..48cf736 100644 --- a/ape_ledger/_cli.py +++ b/ape_ledger/_cli.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Union +from typing import Union import click from ape import accounts @@ -18,7 +18,7 @@ from ape_ledger.hdpath import HDAccountPath, HDBasePath -def _select_account(hd_path: Union[HDBasePath, str]) -> Tuple[str, HDAccountPath]: +def _select_account(hd_path: Union[HDBasePath, str]) -> tuple[str, HDAccountPath]: choices = AddressPromptChoice(hd_path) return choices.get_user_selected_account() @@ -52,7 +52,7 @@ def _list(cli_ctx): click.echo(f" {account.address}{alias_display}{hd_path_display}") -def _get_ledger_accounts() -> List[LedgerAccount]: +def _get_ledger_accounts() -> list[LedgerAccount]: return [a for a in accounts if isinstance(a, LedgerAccount)] diff --git a/ape_ledger/accounts.py b/ape_ledger/accounts.py index 2f02ecb..1f8bff7 100644 --- a/ape_ledger/accounts.py +++ b/ape_ledger/accounts.py @@ -1,6 +1,7 @@ import json +from collections.abc import Iterator from pathlib import Path -from typing import Any, Dict, Iterator, Optional +from typing import Any, Optional import rich from ape.api import AccountAPI, AccountContainerAPI, TransactionAPI @@ -31,6 +32,8 @@ def _to_bytes(val) -> bytes: class AccountContainer(AccountContainerAPI): + name: str = "ledger" + @property def accounts(self) -> Iterator[AccountAPI]: for account_file in self._account_files: @@ -172,7 +175,7 @@ def sign_message(self, msg: Any, **signer_options) -> Optional[MessageSignature] def sign_transaction(self, txn: TransactionAPI, **kwargs) -> Optional[TransactionAPI]: txn.chain_id = 1 - txn_dict: Dict = { + txn_dict: dict = { "nonce": txn.nonce, "gas": txn.gas_limit, "amount": txn.value, diff --git a/ape_ledger/choices.py b/ape_ledger/choices.py index ff0076f..245873a 100644 --- a/ape_ledger/choices.py +++ b/ape_ledger/choices.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, Tuple, Union +from typing import Any, Optional, Union import click from ape.cli import PromptChoice @@ -58,7 +58,7 @@ def convert( self._choice_index = self._choice_index if address_index is None else address_index return address - def get_user_selected_account(self) -> Tuple[str, HDAccountPath]: + def get_user_selected_account(self) -> tuple[str, HDAccountPath]: """Returns the selected address from the user along with the HD path. The user is able to page using special characters ``n`` and ``p``. """ diff --git a/ape_ledger/client.py b/ape_ledger/client.py index 5c8e9ed..8ce5f31 100644 --- a/ape_ledger/client.py +++ b/ape_ledger/client.py @@ -1,6 +1,5 @@ import atexit from functools import cached_property -from typing import Dict, Tuple import hid # type: ignore from ape.logging import LogLevel, logger @@ -13,7 +12,7 @@ class DeviceFactory: - device_map: Dict[str, "LedgerDeviceClient"] = {} + device_map: dict[str, "LedgerDeviceClient"] = {} def create_device(self, account: HDAccountPath): if account.path in self.device_map: @@ -56,23 +55,24 @@ def close(): 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]: + def sign_message(self, text: bytes) -> tuple[int, int, int]: 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]: + 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]: + def sign_transaction(self, txn: dict) -> tuple[int, int, int]: kwargs = {**txn, "sender_path": self._account, "dongle": self.dongle} signed_tx = create_transaction(**kwargs) - if isinstance(signed_tx, SignedType2Transaction): - return signed_tx.y_parity, signed_tx.sender_r, signed_tx.sender_s - else: - return signed_tx.v, signed_tx.r, signed_tx.s + return ( + (signed_tx.y_parity, signed_tx.sender_r, signed_tx.sender_s) + if isinstance(signed_tx, SignedType2Transaction) + else (signed_tx.v, signed_tx.r, signed_tx.s) + ) _device_factory = DeviceFactory() diff --git a/pyproject.toml b/pyproject.toml index 695e478..8604d06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ write_to = "ape_ledger/version.py" [tool.black] line-length = 100 -target-version = ['py38', 'py39', 'py310', 'py311', 'py312'] +target-version = ['py39', 'py310', 'py311', 'py312'] include = '\.pyi?$' [tool.pytest.ini_options] diff --git a/setup.py b/setup.py index f8c2ada..12cbe61 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- from setuptools import find_packages, setup extras_require = { @@ -61,7 +60,7 @@ url="https://github.com/ApeWorX/ape-ledger", include_package_data=True, install_requires=[ - "eth-ape>=0.7.0,<0.8", + "eth-ape>=0.8.1,<0.9", "ledgereth>=0.9.1,<0.10", "click", # Use same version as eth-ape "rich", # Use same version as eth-ape @@ -77,7 +76,7 @@ "ape_ledger=ape_ledger._cli:cli", ], }, - python_requires=">=3.8,<4", + python_requires=">=3.9,<4", extras_require=extras_require, py_modules=["ape_ledger"], license="Apache-2.0", @@ -93,7 +92,6 @@ "Operating System :: MacOS", "Operating System :: POSIX", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", diff --git a/tests/conftest.py b/tests/conftest.py index 92f4a21..7551c4f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,10 @@ import json +import shutil +from pathlib import Path +from tempfile import mkdtemp +import ape import pytest -from ape import accounts, networks from click.testing import CliRunner from eth_account.messages import encode_defunct from eth_pydantic_types import HexBytes @@ -10,6 +13,17 @@ TEST_HD_PATH = "m/44'/60'/{x}'/0/0" +# NOTE: Ensure that we don't use local paths for these +DATA_FOLDER = Path(mkdtemp()).resolve() +ape.config.DATA_FOLDER = DATA_FOLDER + + +@pytest.fixture(autouse=True, scope="session") +def clean_data_folder(): + yield # Run all tests + shutil.rmtree(DATA_FOLDER, ignore_errors=True) + + @pytest.fixture def hd_path(): return TEST_HD_PATH @@ -22,7 +36,7 @@ def alias(): @pytest.fixture def test_accounts(): - return accounts.test_accounts + return ape.accounts.test_accounts @pytest.fixture @@ -47,7 +61,7 @@ def address(account_addresses): @pytest.fixture(autouse=True) def connection(): - with networks.ethereum.local.use_provider("test") as provider: + with ape.networks.ethereum.local.use_provider("test") as provider: yield provider @@ -101,12 +115,12 @@ def runner(): @pytest.fixture def assert_account(address): - def fn(account_path, expected_address=None, expected_hdpath="m/44'/60'/0'/0/0"): + def fn(account_path: Path, expected_address=None, expected_hdpath="m/44'/60'/0'/0/0"): expected_address = expected_address or address - with open(account_path) as account_file: - account_data = json.load(account_file) - assert account_data["address"] == expected_address - assert account_data["hdpath"] == expected_hdpath + assert account_path.is_file(), "Account file missing." + account_data = json.loads(account_path.read_text()) + assert account_data["address"] == expected_address + assert account_data["hdpath"] == expected_hdpath return fn diff --git a/tests/test_accounts.py b/tests/test_accounts.py index b36090d..98d512f 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -1,12 +1,11 @@ import json -import tempfile -from pathlib import Path from typing import Optional, cast import pytest from ape import networks from ape.api import TransactionAPI from ape.types import AddressType +from ape.utils import create_tempdir from ape_ethereum.ecosystem import DynamicFeeTransaction, StaticFeeTransaction from eip712.messages import EIP712Message, EIP712Type from eth_account.messages import SignableMessage @@ -105,21 +104,20 @@ def isolated_file_system(runner): @pytest.fixture def account(mock_container, create_account, hd_path): - with tempfile.TemporaryDirectory() as temp_dir: - path = Path(temp_dir) / "account.json" + with create_tempdir() as temp_dir: + path = temp_dir / "account.json" create_account(path, hd_path) with networks.ethereum.local.use_provider("test"): - yield LedgerAccount(container=mock_container, account_file_path=path) + yield LedgerAccount(name=mock_container, account_file_path=path) class TestAccountContainer: def test_save_account(self, mock_container, alias, address, hd_path, assert_account): - with tempfile.TemporaryDirectory() as temp_dir: - data_path = Path(temp_dir) - container = AccountContainer(data_folder=data_path, account_type=LedgerAccount) - container.save_account(alias, address, hd_path) - account_path = data_path / f"{alias}.json" - assert_account(account_path, expected_hdpath=hd_path) + container = AccountContainer(account_type=LedgerAccount) + container.save_account(alias, address, hd_path) + temp_dir = container.config_manager.DATA_FOLDER + account_path = temp_dir / "ledger" / f"{alias}.json" + assert_account(account_path, expected_hdpath=hd_path) class TestLedgerAccount: