Skip to content

Commit df7d7a1

Browse files
antazoeyevan-quest
andauthored
feat: support web3.py v7 (in addition to v6) (#2394)
Co-authored-by: Evan <163005762+evan-quest@users.noreply.github.com>
1 parent 5b3ad09 commit df7d7a1

File tree

8 files changed

+101
-64
lines changed

8 files changed

+101
-64
lines changed

setup.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,14 @@
118118
"urllib3>=2.0.0,<3",
119119
"watchdog>=3.0,<4",
120120
# ** Dependencies maintained by Ethereum Foundation **
121-
# All version pins dependent on web3[tester]
122-
"eth-abi",
123-
"eth-account",
124-
"eth-typing>=3.5.2,<4",
125-
"eth-utils",
126-
"hexbytes",
127-
"py-geth>=5.1.0,<6",
121+
"eth-abi>=5.1.0,<6",
122+
"eth-account>=0.11.3,<0.14",
123+
"eth-typing>=3.5.2,<6",
124+
"eth-utils>=2.1.0,<6",
125+
"hexbytes>=0.3.1,<2",
126+
"py-geth>=3.14.0,<6",
128127
"trie>=3.0.1,<4", # Peer: stricter pin needed for uv support.
129-
"web3[tester]>=6.17.2,<7",
128+
"web3[tester]>=6.20.1,<8",
130129
# ** Dependencies maintained by ApeWorX **
131130
"eip712>=0.2.10,<0.3",
132131
"ethpm-types>=0.6.19,<0.7",

src/ape/utils/_web3_compat.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from eth_account import Account as EthAccount
2+
3+
try:
4+
# Web3 v7
5+
from web3.middleware import ExtraDataToPOAMiddleware # type: ignore
6+
except ImportError:
7+
from web3.middleware import geth_poa_middleware as ExtraDataToPOAMiddleware # type: ignore
8+
9+
try:
10+
from web3.providers import WebsocketProviderV2 as WebsocketProvider # type: ignore
11+
except ImportError:
12+
from web3.providers import WebSocketProvider as WebsocketProvider # type: ignore
13+
14+
15+
def sign_hash(msghash, private_key):
16+
try:
17+
# Web3 v7
18+
return EthAccount.unsafe_sign_hash(msghash, private_key) # type: ignore
19+
except AttributeError:
20+
# Web3 v6
21+
return EthAccount.signHash(msghash, private_key) # type: ignore
22+
23+
24+
__all__ = [
25+
"ExtraDataToPOAMiddleware",
26+
"sign_hash",
27+
"WebsocketProvider",
28+
]

src/ape_accounts/accounts.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from ape.exceptions import AccountsError
1919
from ape.logging import logger
2020
from ape.types.signatures import MessageSignature, SignableMessage, TransactionSignature
21+
from ape.utils._web3_compat import sign_hash
2122
from ape.utils.basemodel import ManagerAccessMixin
2223
from ape.utils.misc import log_instead_of_fail
2324
from ape.utils.validators import _validate_account_alias, _validate_account_passphrase
@@ -255,7 +256,7 @@ def sign_raw_msghash(self, msghash: HexBytes) -> Optional[MessageSignature]:
255256
# Also, we have already warned the user about the safety.
256257
with warnings.catch_warnings():
257258
warnings.simplefilter("ignore")
258-
signed_msg = EthAccount.signHash(msghash, self.__key)
259+
signed_msg = sign_hash(msghash, self.__key)
259260

260261
return MessageSignature(
261262
v=signed_msg.v,

src/ape_ethereum/provider.py

Lines changed: 58 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,21 @@
2020
from pydantic.dataclasses import dataclass
2121
from requests import HTTPError
2222
from web3 import HTTPProvider, IPCProvider, Web3
23-
from web3 import WebsocketProvider as WebSocketProvider
24-
from web3._utils.http import construct_user_agent
23+
from web3 import __version__ as web3_version
2524
from web3.exceptions import ContractLogicError as Web3ContractLogicError
2625
from web3.exceptions import (
2726
ExtraDataLengthError,
2827
MethodUnavailable,
2928
TimeExhausted,
3029
TransactionNotFound,
3130
)
31+
32+
try:
33+
from web3.exceptions import Web3RPCError
34+
except ImportError:
35+
Web3RPCError = ValueError # type: ignore
36+
3237
from web3.gas_strategies.rpc import rpc_gas_price_strategy
33-
from web3.middleware import geth_poa_middleware as ExtraDataToPOAMiddleware
3438
from web3.middleware.validation import MAX_EXTRADATA_LENGTH
3539
from web3.providers import AutoProvider
3640
from web3.providers.auto import load_provider_from_environment
@@ -59,6 +63,7 @@
5963
from ape.types.events import ContractLog, LogFilter
6064
from ape.types.gas import AutoGasLimit
6165
from ape.types.trace import SourceTraceback
66+
from ape.utils._web3_compat import ExtraDataToPOAMiddleware, WebsocketProvider
6267
from ape.utils.basemodel import ManagerAccessMixin
6368
from ape.utils.misc import DEFAULT_MAX_RETRIES_TX, gas_estimation_error_message, to_int
6469
from ape_ethereum._print import CONSOLE_ADDRESS, console_contract
@@ -124,6 +129,24 @@ def assert_web3_provider_uri_env_var_not_set():
124129
)
125130

126131

132+
def _post_send_transaction(tx: TransactionAPI, receipt: ReceiptAPI):
133+
"""Execute post-transaction ops"""
134+
135+
# TODO: Optional configuration?
136+
if tx.receiver and Address(tx.receiver).is_contract:
137+
# Look for and print any contract logging
138+
try:
139+
receipt.show_debug_logs()
140+
except TransactionNotFound:
141+
# Receipt never published. Likely failed.
142+
pass
143+
except Exception as err:
144+
# Avoid letting debug logs causes program crashes.
145+
logger.debug(f"Unable to show debug logs: {err}")
146+
147+
logger.info(f"Confirmed {receipt.txn_hash} (total fees paid = {receipt.total_fees_paid})")
148+
149+
127150
class Web3Provider(ProviderAPI, ABC):
128151
"""
129152
A base provider mixin class that uses the
@@ -160,7 +183,7 @@ def post_tx_hook(send_tx):
160183
@wraps(send_tx)
161184
def send_tx_wrapper(self, txn: TransactionAPI) -> ReceiptAPI:
162185
receipt = send_tx(self, txn)
163-
self._post_send_transaction(txn, receipt)
186+
_post_send_transaction(txn, receipt)
164187
return receipt
165188

166189
return send_tx_wrapper
@@ -1006,35 +1029,9 @@ def prepare_transaction(self, txn: TransactionAPI) -> TransactionAPI:
10061029
def send_transaction(self, txn: TransactionAPI) -> ReceiptAPI:
10071030
vm_err = None
10081031
txn_data = None
1009-
txn_hash = None
10101032
try:
1011-
if txn.sender is not None and txn.signature is None:
1012-
# Missing signature, user likely trying to use an unlocked account.
1013-
attempt_send = True
1014-
if (
1015-
self.network.is_dev
1016-
and txn.sender not in self.account_manager.test_accounts._impersonated_accounts
1017-
):
1018-
try:
1019-
self.account_manager.test_accounts.impersonate_account(txn.sender)
1020-
except NotImplementedError:
1021-
# Unable to impersonate. Try sending as raw-tx.
1022-
attempt_send = False
1023-
1024-
if attempt_send:
1025-
# For some reason, some nodes have issues with integer-types.
1026-
txn_data = {
1027-
k: to_hex(v) if isinstance(v, int) else v
1028-
for k, v in txn.model_dump(by_alias=True, mode="json").items()
1029-
}
1030-
tx_params = cast(TxParams, txn_data)
1031-
txn_hash = to_hex(self.web3.eth.send_transaction(tx_params))
1032-
# else: attempt raw tx
1033-
1034-
if txn_hash is None:
1035-
txn_hash = to_hex(self.web3.eth.send_raw_transaction(txn.serialize_transaction()))
1036-
1037-
except (ValueError, Web3ContractLogicError) as err:
1033+
txn_hash = self._send_transaction(txn)
1034+
except (Web3RPCError, Web3ContractLogicError) as err:
10381035
vm_err = self.get_virtual_machine_error(
10391036
err, txn=txn, set_ape_traceback=txn.raise_on_revert
10401037
)
@@ -1102,22 +1099,35 @@ def send_transaction(self, txn: TransactionAPI) -> ReceiptAPI:
11021099

11031100
return receipt
11041101

1105-
def _post_send_transaction(self, tx: TransactionAPI, receipt: ReceiptAPI):
1106-
"""Execute post-transaction ops"""
1102+
def _send_transaction(self, txn: TransactionAPI) -> str:
1103+
txn_hash = None
1104+
if txn.sender is not None and txn.signature is None:
1105+
# Missing signature, user likely trying to use an unlocked account.
1106+
attempt_send = True
1107+
if (
1108+
self.network.is_dev
1109+
and txn.sender not in self.account_manager.test_accounts._impersonated_accounts
1110+
):
1111+
try:
1112+
self.account_manager.test_accounts.impersonate_account(txn.sender)
1113+
except NotImplementedError:
1114+
# Unable to impersonate. Try sending as raw-tx.
1115+
attempt_send = False
1116+
1117+
if attempt_send:
1118+
# For some reason, some nodes have issues with integer-types.
1119+
txn_data = {
1120+
k: to_hex(v) if isinstance(v, int) else v
1121+
for k, v in txn.model_dump(by_alias=True, mode="json").items()
1122+
}
1123+
tx_params = cast(TxParams, txn_data)
1124+
txn_hash = to_hex(self.web3.eth.send_transaction(tx_params))
1125+
# else: attempt raw tx
11071126

1108-
# TODO: Optional configuration?
1109-
if tx.receiver and Address(tx.receiver).is_contract:
1110-
# Look for and print any contract logging
1111-
try:
1112-
receipt.show_debug_logs()
1113-
except TransactionNotFound:
1114-
# Receipt never published. Likely failed.
1115-
pass
1116-
except Exception as err:
1117-
# Avoid letting debug logs causes program crashes.
1118-
logger.debug(f"Unable to show debug logs: {err}")
1127+
if txn_hash is None:
1128+
txn_hash = to_hex(self.web3.eth.send_raw_transaction(txn.serialize_transaction()))
11191129

1120-
logger.info(f"Confirmed {receipt.txn_hash} (total fees paid = {receipt.total_fees_paid})")
1130+
return txn_hash
11211131

11221132
def _post_connect(self):
11231133
# Register the console contract for trace enrichment
@@ -1326,9 +1336,7 @@ class EthereumNodeProvider(Web3Provider, ABC):
13261336
name: str = "node"
13271337

13281338
# NOTE: Appends user-agent to base User-Agent string.
1329-
request_header: dict = {
1330-
"User-Agent": construct_user_agent(str(HTTPProvider)),
1331-
}
1339+
request_header: dict = {"User-Agent": f"EthereumNodeProvider/web3.py/{web3_version}"}
13321340

13331341
@property
13341342
def uri(self) -> str:
@@ -1619,7 +1627,7 @@ def _create_web3(
16191627

16201628
providers.append(lambda: HTTPProvider(endpoint_uri=http, request_kwargs=request_kwargs))
16211629
if ws := ws_uri:
1622-
providers.append(lambda: WebSocketProvider(endpoint_uri=ws))
1630+
providers.append(lambda: WebsocketProvider(endpoint_uri=ws))
16231631

16241632
provider = AutoProvider(potential_providers=providers)
16251633
return Web3(provider)

src/ape_node/provider.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
from pydantic import field_validator
1414
from pydantic_settings import SettingsConfigDict
1515
from requests.exceptions import ConnectionError
16-
from web3.middleware import geth_poa_middleware as ExtraDataToPOAMiddleware
1716

1817
from ape.api.config import PluginConfig
1918
from ape.api.providers import SubprocessProvider, TestProviderAPI
2019
from ape.logging import LogLevel, logger
20+
from ape.utils._web3_compat import ExtraDataToPOAMiddleware
2121
from ape.utils.misc import ZERO_ADDRESS, log_instead_of_fail, raises_not_implemented
2222
from ape.utils.process import JoinableQueue, spawn
2323
from ape.utils.testing import (

src/ape_test/accounts.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from ape.api.accounts import TestAccountAPI, TestAccountContainerAPI
1414
from ape.exceptions import ProviderNotConnectedError, SignatureError
1515
from ape.types.signatures import MessageSignature, TransactionSignature
16+
from ape.utils._web3_compat import sign_hash
1617
from ape.utils.testing import (
1718
DEFAULT_NUMBER_OF_TEST_ACCOUNTS,
1819
DEFAULT_TEST_HD_PATH,
@@ -166,7 +167,7 @@ def sign_transaction(
166167
def sign_raw_msghash(self, msghash: HexBytes) -> MessageSignature:
167168
with warnings.catch_warnings():
168169
warnings.simplefilter("ignore")
169-
signed_msg = EthAccount.signHash(msghash, self.private_key)
170+
signed_msg = sign_hash(msghash, self.private_key)
170171

171172
return MessageSignature(
172173
v=signed_msg.v,

tests/functional/geth/test_provider.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from web3 import AutoProvider, Web3
1212
from web3.exceptions import ContractLogicError as Web3ContractLogicError
1313
from web3.exceptions import ExtraDataLengthError
14-
from web3.middleware import geth_poa_middleware as ExtraDataToPOAMiddleware
1514
from web3.providers import HTTPProvider
1615

1716
from ape.exceptions import (
@@ -26,6 +25,7 @@
2625
VirtualMachineError,
2726
)
2827
from ape.utils import to_int
28+
from ape.utils._web3_compat import ExtraDataToPOAMiddleware
2929
from ape_ethereum.ecosystem import Block
3030
from ape_ethereum.provider import DEFAULT_SETTINGS, EthereumNodeProvider
3131
from ape_ethereum.trace import TraceApproach

tests/functional/test_ecosystem.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,11 +1188,11 @@ def get_calltree(self) -> CallTreeNode:
11881188
{
11891189
"name": "NumberChange",
11901190
"calldata": {
1191-
"b": "0x3e..404b",
1191+
"b": "0x3ee0..404b",
11921192
"prevNum": 0,
11931193
"dynData": '"Dynamic"',
11941194
"newNum": 123,
1195-
"dynIndexed": "0x9f..a94d",
1195+
"dynIndexed": "0x9f3d..a94d",
11961196
},
11971197
}
11981198
]

0 commit comments

Comments
 (0)