From 3fbc2f489733cba399e329ddc518cecd595704de Mon Sep 17 00:00:00 2001 From: antazoey Date: Sat, 15 Feb 2025 18:02:48 -0600 Subject: [PATCH 1/3] fix: prep txns from contracts --- src/ape/api/accounts.py | 38 ----------------------------- src/ape/api/address.py | 54 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 42 deletions(-) diff --git a/src/ape/api/accounts.py b/src/ape/api/accounts.py index 7be1d39fd2..9bbd5f4969 100644 --- a/src/ape/api/accounts.py +++ b/src/ape/api/accounts.py @@ -401,44 +401,6 @@ def check_signature( else: raise AccountsError(f"Unsupported message type: {type(data)}.") - def prepare_transaction(self, txn: TransactionAPI) -> TransactionAPI: - """ - Set default values on a transaction. - - Raises: - :class:`~ape.exceptions.AccountsError`: When the account cannot afford the transaction - or the nonce is invalid. - :class:`~ape.exceptions.TransactionError`: When given negative required confirmations. - - Args: - txn (:class:`~ape.api.transactions.TransactionAPI`): The transaction to prepare. - - Returns: - :class:`~ape.api.transactions.TransactionAPI` - """ - - # NOTE: Allow overriding nonce, assume user understands what this does - if txn.nonce is None: - txn.nonce = self.nonce - elif txn.nonce < self.nonce: - raise AccountsError("Invalid nonce, will not publish.") - - txn = self.provider.prepare_transaction(txn) - - if ( - txn.sender not in self.account_manager.test_accounts._impersonated_accounts - and txn.total_transfer_value > self.balance - ): - raise AccountsError( - f"Transfer value meets or exceeds account balance " - f"for account '{self.address}' on chain '{self.provider.chain_id}' " - f"using provider '{self.provider.name}'.\n" - "Are you using the correct account / chain / provider combination?\n" - f"(transfer_value={txn.total_transfer_value}, balance={self.balance})." - ) - - return txn - def get_deployment_address(self, nonce: Optional[int] = None) -> AddressType: """ Get a contract address before it is deployed. This is useful diff --git a/src/ape/api/address.py b/src/ape/api/address.py index 5aa9432e49..78b8872dcd 100644 --- a/src/ape/api/address.py +++ b/src/ape/api/address.py @@ -4,7 +4,7 @@ from eth_pydantic_types import HexBytes -from ape.exceptions import ConversionError +from ape.exceptions import ConversionError, AccountsError from ape.types.address import AddressType from ape.types.units import CurrencyValue from ape.utils.basemodel import BaseInterface @@ -103,9 +103,17 @@ def __call__(self, **kwargs) -> "ReceiptAPI": """ txn = self.as_transaction(**kwargs) - if "sender" in kwargs and hasattr(kwargs["sender"], "call"): - sender = kwargs["sender"] - return sender.call(txn, **kwargs) + if "sender" in kwargs: + if hasattr(kwargs["sender"], "call"): + # AccountAPI + sender = kwargs["sender"] + return sender.call(txn, **kwargs) + + elif hasattr(kwargs["sender"], "prepare_transaction"): + # BaseAddress (likely, a ContractInstance) + prepare_transaction = kwargs["sender"].prepare_transaction(txn) + return self.provider.send_transaction(prepare_transaction) + elif "sender" not in kwargs and self.account_manager.default_sender is not None: return self.account_manager.default_sender.call(txn, **kwargs) @@ -186,6 +194,44 @@ def estimate_gas_cost(self, **kwargs) -> int: txn = self.as_transaction(**kwargs) return self.provider.estimate_gas_cost(txn) + def prepare_transaction(self, txn: "TransactionAPI") -> "TransactionAPI": + """ + Set default values on a transaction. + + Raises: + :class:`~ape.exceptions.AccountsError`: When the account cannot afford the transaction + or the nonce is invalid. + :class:`~ape.exceptions.TransactionError`: When given negative required confirmations. + + Args: + txn (:class:`~ape.api.transactions.TransactionAPI`): The transaction to prepare. + + Returns: + :class:`~ape.api.transactions.TransactionAPI` + """ + + # NOTE: Allow overriding nonce, assume user understands what this does + if txn.nonce is None: + txn.nonce = self.nonce + elif txn.nonce < self.nonce: + raise AccountsError("Invalid nonce, will not publish.") + + txn = self.provider.prepare_transaction(txn) + + if ( + txn.sender not in self.account_manager.test_accounts._impersonated_accounts + and txn.total_transfer_value > self.balance + ): + raise AccountsError( + f"Transfer value meets or exceeds account balance " + f"for account '{self.address}' on chain '{self.provider.chain_id}' " + f"using provider '{self.provider.name}'.\n" + "Are you using the correct account / chain / provider combination?\n" + f"(transfer_value={txn.total_transfer_value}, balance={self.balance})." + ) + + return txn + class Address(BaseAddress): """ From eae7665f52e4b90ebcd5a2b27df918b24230fb34 Mon Sep 17 00:00:00 2001 From: antazoey Date: Sat, 15 Feb 2025 18:18:44 -0600 Subject: [PATCH 2/3] feat: allow default calls to still get prepared --- src/ape/api/accounts.py | 2 +- src/ape/api/address.py | 2 +- src/ape/api/networks.py | 4 ++-- src/ape/exceptions.py | 4 ++++ src/ape_ethereum/transactions.py | 5 +++-- src/ape_test/accounts.py | 2 +- tests/functional/test_contract_instance.py | 17 +++++++++++++++++ 7 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/ape/api/accounts.py b/src/ape/api/accounts.py index 9bbd5f4969..3266eaaf36 100644 --- a/src/ape/api/accounts.py +++ b/src/ape/api/accounts.py @@ -188,7 +188,7 @@ def call( if sign: prepared_txn = self.sign_transaction(txn, **signer_options) if not prepared_txn: - raise SignatureError("The transaction was not signed.") + raise SignatureError("The transaction was not signed.", transaction=txn) else: prepared_txn = txn diff --git a/src/ape/api/address.py b/src/ape/api/address.py index 78b8872dcd..3533114c20 100644 --- a/src/ape/api/address.py +++ b/src/ape/api/address.py @@ -4,7 +4,7 @@ from eth_pydantic_types import HexBytes -from ape.exceptions import ConversionError, AccountsError +from ape.exceptions import AccountsError, ConversionError from ape.types.address import AddressType from ape.types.units import CurrencyValue from ape.utils.basemodel import BaseInterface diff --git a/src/ape/api/networks.py b/src/ape/api/networks.py index 86b5457247..0a14ace9e9 100644 --- a/src/ape/api/networks.py +++ b/src/ape/api/networks.py @@ -226,7 +226,7 @@ def serialize_transaction(self) -> bytes: bytes """ if not self.signature: - raise SignatureError("The transaction is not signed.") + raise SignatureError("The transaction is not signed.", transaction=self) txn_data = self.model_dump(exclude={"sender"}) unsigned_txn = serializable_unsigned_transaction_from_dict(txn_data) @@ -239,7 +239,7 @@ def serialize_transaction(self) -> bytes: signed_txn = encode_transaction(unsigned_txn, signature) if self.sender and EthAccount.recover_transaction(signed_txn) != self.sender: - raise SignatureError("Recovered signer doesn't match sender!") + raise SignatureError("Recovered signer doesn't match sender!", transaction=signed_txn) return signed_txn diff --git a/src/ape/exceptions.py b/src/ape/exceptions.py index 262a214d3f..0c207add6a 100644 --- a/src/ape/exceptions.py +++ b/src/ape/exceptions.py @@ -74,6 +74,10 @@ class SignatureError(AccountsError): Raised when there are issues with signing. """ + def __init__(self, message: str, transaction: Optional["TransactionAPI"] = None): + self.transaction = transaction + super().__init__(message) + class ContractDataError(ApeException): """ diff --git a/src/ape_ethereum/transactions.py b/src/ape_ethereum/transactions.py index 8e4522a34a..25b70927ca 100644 --- a/src/ape_ethereum/transactions.py +++ b/src/ape_ethereum/transactions.py @@ -72,7 +72,7 @@ def serialize_transaction(self) -> bytes: "Did you forget to add the `sender=` kwarg to the transaction function call?" ) - raise SignatureError(message) + raise SignatureError(message, transaction=self) txn_data = self.model_dump(by_alias=True, exclude={"sender", "type"}) @@ -107,7 +107,8 @@ def serialize_transaction(self) -> bytes: recovered_signer = EthAccount.recover_transaction(signed_txn) if recovered_signer != self.sender: raise SignatureError( - f"Recovered signer '{recovered_signer}' doesn't match sender {self.sender}!" + f"Recovered signer '{recovered_signer}' doesn't match sender {self.sender}!", + transaction=self, ) return signed_txn diff --git a/src/ape_test/accounts.py b/src/ape_test/accounts.py index 867b5f73ea..38101e7c1f 100644 --- a/src/ape_test/accounts.py +++ b/src/ape_test/accounts.py @@ -161,7 +161,7 @@ def sign_transaction( ) = sign_transaction_dict(private_key, tx_data) except TypeError as err: # Occurs when missing properties on the txn that are needed to sign. - raise SignatureError(str(err)) from err + raise SignatureError(str(err), transaction=txn) from err # NOTE: Using `to_bytes(hexstr=to_hex(sig_r))` instead of `to_bytes(sig_r)` as # a performance optimization. diff --git a/tests/functional/test_contract_instance.py b/tests/functional/test_contract_instance.py index 8a502cb057..231446857b 100644 --- a/tests/functional/test_contract_instance.py +++ b/tests/functional/test_contract_instance.py @@ -1027,3 +1027,20 @@ def test_calldata_arg(calldata, expected, contract_instance, owner): tx = contract_instance.functionWithCalldata(calldata, sender=owner) assert not tx.failed assert HexBytes(expected) in tx.data + + +def test_call(fallback_contract, owner): + """ + Fallback contract call test. + """ + tx = fallback_contract(sender=owner, value="1 wei") + assert not tx.failed + + +def test_call_contract_as_sender(fallback_contract, owner, vyper_contract_instance): + owner.transfer(fallback_contract, "1 ETH") + with pytest.raises(SignatureError) as info: + fallback_contract(sender=fallback_contract, value="1 wei") + + transaction = info.value.transaction + assert transaction.nonce is not None # Proves it was prepared. From f8bb7b6421f50c487e2750e34ce26e1c82f4f0d5 Mon Sep 17 00:00:00 2001 From: antazoey Date: Sat, 15 Feb 2025 18:36:49 -0600 Subject: [PATCH 3/3] fix: type check --- src/ape/api/networks.py | 4 ++-- tests/functional/test_contract_instance.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ape/api/networks.py b/src/ape/api/networks.py index 0a14ace9e9..86b5457247 100644 --- a/src/ape/api/networks.py +++ b/src/ape/api/networks.py @@ -226,7 +226,7 @@ def serialize_transaction(self) -> bytes: bytes """ if not self.signature: - raise SignatureError("The transaction is not signed.", transaction=self) + raise SignatureError("The transaction is not signed.") txn_data = self.model_dump(exclude={"sender"}) unsigned_txn = serializable_unsigned_transaction_from_dict(txn_data) @@ -239,7 +239,7 @@ def serialize_transaction(self) -> bytes: signed_txn = encode_transaction(unsigned_txn, signature) if self.sender and EthAccount.recover_transaction(signed_txn) != self.sender: - raise SignatureError("Recovered signer doesn't match sender!", transaction=signed_txn) + raise SignatureError("Recovered signer doesn't match sender!") return signed_txn diff --git a/tests/functional/test_contract_instance.py b/tests/functional/test_contract_instance.py index 231446857b..1c786187d8 100644 --- a/tests/functional/test_contract_instance.py +++ b/tests/functional/test_contract_instance.py @@ -1043,4 +1043,5 @@ def test_call_contract_as_sender(fallback_contract, owner, vyper_contract_instan fallback_contract(sender=fallback_contract, value="1 wei") transaction = info.value.transaction + assert transaction is not None assert transaction.nonce is not None # Proves it was prepared.