diff --git a/ape_safe/_cli/pending.py b/ape_safe/_cli/pending.py index ac05de8..b3a354a 100644 --- a/ape_safe/_cli/pending.py +++ b/ape_safe/_cli/pending.py @@ -3,7 +3,7 @@ import click import rich from ape.api import AccountAPI -from ape.cli import NetworkBoundCommand, get_user_selected_account, network_option +from ape.cli import ConnectedProviderCommand, get_user_selected_account, network_option from ape.exceptions import SignatureError from ape.types import AddressType from click.exceptions import BadOptionUsage @@ -25,17 +25,15 @@ def pending(): """ -@pending.command("list", cls=NetworkBoundCommand) +@pending.command("list", cls=ConnectedProviderCommand) @safe_cli_ctx -@network_option() @safe_option @click.option("--verbose", is_flag=True) -def _list(cli_ctx: SafeCliContext, network, safe, verbose) -> None: +def _list(cli_ctx: SafeCliContext, safe, verbose) -> None: """ View pending transactions for a Safe """ - _ = network # Needed for NetworkBoundCommand txns = list(safe.client.get_transactions(starting_nonce=safe.next_nonce, confirmed=False)) if not txns: rich.print("There are no pending transactions.") @@ -50,6 +48,7 @@ def _list(cli_ctx: SafeCliContext, network, safe, verbose) -> None: all_items = txns_by_nonce.items() total_items = len(all_items) + max_op_len = len("rejection") for root_idx, (nonce, tx_list) in enumerate(all_items): tx_len = len(tx_list) for idx, tx in enumerate(tx_list): @@ -57,10 +56,11 @@ def _list(cli_ctx: SafeCliContext, network, safe, verbose) -> None: is_rejection = not tx.value and not tx.data and tx.to == tx.safe operation_name = tx.operation.name if tx.data else "transfer" if is_rejection: - title = f"{title} rejection" - else: - title = f"{title} {operation_name}" + operation_name = "rejection" + # Add spacing (unless verbose) so columns are aligned. + spaces = (max(0, max_op_len - len(operation_name))) * " " if verbose else "" + title = f"{title} {operation_name}{spaces}" confirmations = tx.confirmations rich.print( f"{title} " @@ -71,7 +71,7 @@ def _list(cli_ctx: SafeCliContext, network, safe, verbose) -> None: if verbose: fields = ("to", "value", "data", "base_gas", "gas_price") data = {} - for field_name, value in tx.dict().items(): + for field_name, value in tx.model_dump(by_alias=True, mode="json").items(): if field_name not in fields: continue @@ -124,9 +124,8 @@ def _handle_execute_cli_arg(ctx, param, val): ) -@pending.command(cls=NetworkBoundCommand) +@pending.command(cls=ConnectedProviderCommand) @safe_cli_ctx -@network_option() @safe_option @click.option("--data", type=HexBytes, help="Transaction data", default=HexBytes("")) @click.option("--gas-price", type=int, help="Transaction gas price") @@ -134,12 +133,10 @@ def _handle_execute_cli_arg(ctx, param, val): @click.option("--to", "receiver", type=AddressType, help="Transaction receiver") @click.option("--nonce", type=int, help="Transaction nonce") @click.option("--execute", callback=_handle_execute_cli_arg) -def propose(cli_ctx, network, safe, data, gas_price, value, receiver, nonce, execute): +def propose(cli_ctx, ecosystem, safe, data, gas_price, value, receiver, nonce, execute): """ Create a new transaction """ - _ = network # Needed for NetworkBoundCommand - ecosystem = cli_ctx.chain_manager.provider.network.ecosystem nonce = safe.new_nonce if nonce is None else nonce txn = ecosystem.create_transaction( value=value, data=data, gas_price=gas_price, nonce=nonce, receiver=receiver @@ -215,14 +212,12 @@ def _load_submitter(ctx, param, val): return None -@pending.command(cls=NetworkBoundCommand) +@pending.command(cls=ConnectedProviderCommand) @safe_cli_ctx -@network_option() @safe_option @txn_ids_argument @click.option("--execute", callback=_handle_execute_cli_arg) -def approve(cli_ctx: SafeCliContext, network, safe, txn_ids, execute): - _ = network # Needed for NetworkBoundCommand +def approve(cli_ctx: SafeCliContext, safe, txn_ids, execute): submitter: Optional[AccountAPI] = execute if isinstance(execute, AccountAPI) else None pending_transactions = list( safe.client.get_transactions(confirmed=False, starting_nonce=safe.next_nonce) @@ -235,7 +230,7 @@ def approve(cli_ctx: SafeCliContext, network, safe, txn_ids, execute): # Not a specified txn. continue - safe_tx = safe.create_safe_tx(**txn.dict(by_alias=True)) + safe_tx = safe.create_safe_tx(**txn.model_dump(by_alias=True, mode="json")) num_confirmations = len(txn.confirmations) signatures_added = {} @@ -270,14 +265,14 @@ def approve(cli_ctx: SafeCliContext, network, safe, txn_ids, execute): cli_ctx.abort_txns_not_found(txn_ids) -@pending.command(cls=NetworkBoundCommand) +@pending.command(cls=ConnectedProviderCommand) @safe_cli_ctx @network_option() @safe_option @txn_ids_argument # NOTE: Doesn't use --execute because we don't need BOOL values. @click.option("--submitter", callback=_load_submitter) -def execute(cli_ctx, network, safe, txn_ids, submitter): +def execute(cli_ctx, safe, txn_ids, submitter): """ Execute a transaction """ @@ -300,7 +295,7 @@ def execute(cli_ctx, network, safe, txn_ids, submitter): def _execute(safe: SafeAccount, txn: UnexecutedTxData, submitter: AccountAPI): - safe_tx = safe.create_safe_tx(**txn.dict(by_alias=True)) + safe_tx = safe.create_safe_tx(**txn.model_dump(mode="json", by_alias=True)) signatures = {c.owner: _rsv_to_message_signature(c.signature) for c in txn.confirmations} # NOTE: We have a hack that allows bytes in the mapping, hence type ignore @@ -309,9 +304,8 @@ def _execute(safe: SafeAccount, txn: UnexecutedTxData, submitter: AccountAPI): submitter.call(exc_tx) -@pending.command(cls=NetworkBoundCommand) +@pending.command(cls=ConnectedProviderCommand) @safe_cli_ctx -@network_option() @safe_option @txn_ids_argument def reject(cli_ctx: SafeCliContext, network, safe, txn_ids): @@ -319,7 +313,6 @@ def reject(cli_ctx: SafeCliContext, network, safe, txn_ids): Reject one or more pending transactions """ - _ = network # Needed for NetworkBoundCommand pending_transactions = safe.client.get_transactions( confirmed=False, starting_nonce=safe.next_nonce ) @@ -352,16 +345,14 @@ def reject(cli_ctx: SafeCliContext, network, safe, txn_ids): cli_ctx.abort_txns_not_found(txn_ids) -@pending.command(cls=NetworkBoundCommand) +@pending.command(cls=ConnectedProviderCommand) @safe_cli_ctx -@network_option() @safe_option @click.argument("txn_id") -def show_confs(cli_ctx, network, safe, txn_id): +def show_confs(cli_ctx, safe, txn_id): """ Show existing confirmations """ - _ = network # Needed for NetworkBoundCommand if txn_id.isnumeric(): nonce = int(txn_id) diff --git a/ape_safe/accounts.py b/ape_safe/accounts.py index 27b782d..5f11569 100644 --- a/ape_safe/accounts.py +++ b/ape_safe/accounts.py @@ -1,7 +1,7 @@ import json import os from pathlib import Path -from typing import Dict, Iterable, Iterator, List, Mapping, Optional, Tuple, Type, Union, cast +from typing import Any, Dict, Iterable, Iterator, List, Mapping, Optional, Tuple, Type, Union, cast from ape.api import AccountAPI, AccountContainerAPI, ReceiptAPI, TransactionAPI from ape.api.address import BaseAddress @@ -10,7 +10,7 @@ from ape.exceptions import ProviderNotConnectedError from ape.logging import logger from ape.managers.accounts import AccountManager, TestAccountManager -from ape.types import AddressType, HexBytes, MessageSignature, SignableMessage +from ape.types import AddressType, HexBytes, MessageSignature from ape.utils import ZERO_ADDRESS, cached_property from ape_ethereum.transactions import TransactionType from eip712.common import create_safe_tx_def @@ -206,11 +206,13 @@ def contract(self) -> ContractInstance: if fallback_signatures < contract_signatures: return safe_contract # for some reason this never gets hit - contract_type = safe_contract.contract_type.dict() - fallback_type = self.fallback_handler.contract_type.dict() + contract_type = safe_contract.contract_type.model_dump(by_alias=True, mode="json") + fallback_type = self.fallback_handler.contract_type.model_dump( + by_alias=True, mode="json" + ) contract_type["abi"].extend(fallback_type["abi"]) return self.chain_manager.contracts.instance_at( - self.address, contract_type=ContractType.parse_obj(contract_type) + self.address, contract_type=ContractType.model_validate(contract_type) ) else: @@ -219,7 +221,7 @@ def contract(self) -> ContractInstance: @cached_property def fallback_handler(self) -> Optional[ContractInstance]: slot = keccak(text="fallback_manager.handler.address") - value = self.provider.get_storage_at(self.address, slot) + value = self.provider.get_storage(self.address, slot) address = self.network_manager.ecosystem.decode_address(value[-20:]) return ( self.chain_manager.contracts.instance_at(address) if address != ZERO_ADDRESS else None @@ -304,7 +306,7 @@ def new_nonce(self): # No pending transactions. Use next on-chain nonce. return self.next_nonce - def sign_message(self, msg: SignableMessage) -> Optional[MessageSignature]: + def sign_message(self, msg: Any, **signer_options) -> Optional[MessageSignature]: raise NotImplementedError("Safe accounts do not support message signing!") @property @@ -345,7 +347,9 @@ def create_safe_tx(self, txn: Optional[TransactionAPI] = None, **safe_tx_kwargs) def pending_transactions(self) -> Iterator[Tuple[SafeTx, List[SafeTxConfirmation]]]: for executed_tx in self.client.get_transactions(confirmed=False): - yield self.create_safe_tx(**executed_tx.dict(by_alias=True)), executed_tx.confirmations + yield self.create_safe_tx( + **executed_tx.model_dump(mode="json", by_alias=True) + ), executed_tx.confirmations @property def local_signers(self) -> List[AccountAPI]: diff --git a/ape_safe/client/__init__.py b/ape_safe/client/__init__.py index e232595..511a0e5 100644 --- a/ape_safe/client/__init__.py +++ b/ape_safe/client/__init__.py @@ -71,7 +71,7 @@ def __init__( @property def safe_details(self) -> SafeDetails: response = self._get(f"safes/{self.address}") - return SafeDetails.parse_obj(response.json()) + return SafeDetails.model_validate(response.json()) def get_next_nonce(self) -> int: return self.safe_details.nonce @@ -90,10 +90,10 @@ def _all_transactions(self) -> Iterator[SafeApiTxData]: # TODO: Replace with `model_validate()` after ape 0.7. # NOTE: Using construct because of pydantic v2 back import validation error. if "isExecuted" in txn and txn["isExecuted"]: - yield ExecutedTxData.parse_obj(txn) + yield ExecutedTxData.model_validate(txn) else: - yield UnexecutedTxData.parse_obj(txn) + yield UnexecutedTxData.model_validate(txn) url = data.get("next") @@ -102,7 +102,7 @@ def get_confirmations(self, safe_tx_hash: SafeTxID) -> Iterator[SafeTxConfirmati while url: response = self._get(url) data = response.json() - yield from map(SafeTxConfirmation.parse_obj, data.get("results")) + yield from map(SafeTxConfirmation.model_validate, data.get("results")) url = data.get("next") def post_transaction( @@ -119,7 +119,7 @@ def post_transaction( ) post_dict: Dict = {"signature": signature.hex()} - for key, value in tx_data.dict(by_alias=True).items(): + for key, value in tx_data.model_dump(by_alias=True, mode="json").items(): if isinstance(value, HexBytes): post_dict[key] = value.hex() elif isinstance(value, OperationType): diff --git a/ape_safe/client/mock.py b/ape_safe/client/mock.py index ecd019c..904f062 100644 --- a/ape_safe/client/mock.py +++ b/ape_safe/client/mock.py @@ -29,7 +29,7 @@ def __init__(self, contract: ContractInstance): @property def safe_details(self) -> SafeDetails: slot = keccak(text="fallback_manager.handler.address") - value = self.provider.get_storage_at(self.contract.address, slot) + value = self.provider.get_storage(self.contract.address, slot) fallback_address = self.network_manager.ecosystem.decode_address(value[-20:]) return SafeDetails( diff --git a/ape_safe/multisend.py b/ape_safe/multisend.py index 182763b..ead9dfd 100644 --- a/ape_safe/multisend.py +++ b/ape_safe/multisend.py @@ -129,7 +129,7 @@ def contract(self) -> ContractInstance: # All versions have this ABI contract = self.chain_manager.contracts.instance_at( multisend_address, - contract_type=ContractType.parse_obj(MULTISEND_CONTRACT_TYPE), + contract_type=ContractType.model_validate(MULTISEND_CONTRACT_TYPE), ) if contract.code != MULTISEND_CODE: diff --git a/setup.py b/setup.py index f8b3b6e..6803253 100644 --- a/setup.py +++ b/setup.py @@ -59,10 +59,10 @@ include_package_data=True, install_requires=[ "eth-ape>=0.6.27,<0.7.0", - "eip712>=0.2.2,<0.3.0", "requests>=2.31.0,<3", + "eip712", # Use same version as eth-ape "click", # Use same version as eth-ape - "pydantic<2", # TODO: Rm constraint on Ape 0.7. + "pydantic", # Use same version as eth-ape "eth-utils", # Use same version as eth-ape ], entry_points={ diff --git a/tests/conftest.py b/tests/conftest.py index 8b1374f..e1a6eee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -104,13 +104,15 @@ def safe(safe_data_file): @pytest.fixture def token(deployer: SafeAccount): - contract = ContractType.parse_file(contracts_directory / "Token.json") + text = (contracts_directory / "Token.json").read_text() + contract = ContractType.model_validate_json(text) return deployer.deploy(ContractContainer(contract)) @pytest.fixture def vault(deployer: SafeAccount, token): - vault = ContractContainer(ContractType.parse_file(contracts_directory / "VyperVault.json")) + text = (contracts_directory / "VyperVault.json").read_text() + vault = ContractContainer(ContractType.model_validate_json(text)) return deployer.deploy(vault, token) diff --git a/tests/test_account.py b/tests/test_account.py index 637e4ac..f53f1d4 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -46,7 +46,7 @@ def test_swap_owner(safe, accounts, OWNERS, mode): safe_tx_hash = add_0x_prefix(f"{pending_txns[0].safe_tx_hash}") safe_tx_data = pending_txns[0] - safe_tx = safe.create_safe_tx(**safe_tx_data.dict(by_alias=True)) + safe_tx = safe.create_safe_tx(**safe_tx_data.model_dump(by_alias=True, mode="json")) # Ensure client confirmations works client_confs = list(safe.client.get_confirmations(safe_tx_hash)) @@ -98,7 +98,7 @@ def test_add_owner(safe, accounts, OWNERS, mode): # `safe_tx` is in mock client, extract it and execute it successfully this time safe_tx_data = next(safe.client.get_transactions(confirmed=False)) - safe_tx = safe.create_safe_tx(**safe_tx_data.dict(by_alias=True)) + safe_tx = safe.create_safe_tx(**safe_tx_data.model_dump(by_alias=True, mode="json")) receipt = safe.submit_safe_tx(safe_tx) assert receipt.events == [ @@ -146,7 +146,7 @@ def exec_transaction(): # `safe_tx` is in mock client, extract it and execute it successfully this time safe_tx_data = next(safe.client.get_transactions(confirmed=False)) - safe_tx = safe.create_safe_tx(**safe_tx_data.dict(by_alias=True)) + safe_tx = safe.create_safe_tx(**safe_tx_data.model_dump(by_alias=True, mode="json")) receipt = safe.submit_safe_tx(safe_tx) expected_events = [