web3 7.6.1__py3-none-any.whl → 7.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. ens/async_ens.py +1 -1
  2. ens/ens.py +1 -1
  3. web3/_utils/caching/caching_utils.py +64 -0
  4. web3/_utils/caching/request_caching_validation.py +1 -0
  5. web3/_utils/events.py +1 -1
  6. web3/_utils/http_session_manager.py +32 -3
  7. web3/_utils/module_testing/eth_module.py +5 -18
  8. web3/_utils/module_testing/module_testing_utils.py +1 -43
  9. web3/_utils/module_testing/persistent_connection_provider.py +696 -207
  10. web3/_utils/module_testing/utils.py +99 -33
  11. web3/beacon/api_endpoints.py +10 -0
  12. web3/beacon/async_beacon.py +47 -0
  13. web3/beacon/beacon.py +45 -0
  14. web3/contract/async_contract.py +2 -206
  15. web3/contract/base_contract.py +217 -13
  16. web3/contract/contract.py +2 -205
  17. web3/datastructures.py +15 -16
  18. web3/eth/async_eth.py +23 -5
  19. web3/exceptions.py +7 -0
  20. web3/main.py +24 -3
  21. web3/manager.py +140 -48
  22. web3/method.py +1 -1
  23. web3/middleware/attrdict.py +12 -22
  24. web3/middleware/base.py +14 -6
  25. web3/module.py +17 -21
  26. web3/providers/async_base.py +23 -14
  27. web3/providers/base.py +6 -8
  28. web3/providers/ipc.py +7 -6
  29. web3/providers/legacy_websocket.py +1 -1
  30. web3/providers/persistent/async_ipc.py +5 -3
  31. web3/providers/persistent/persistent.py +121 -17
  32. web3/providers/persistent/persistent_connection.py +11 -4
  33. web3/providers/persistent/request_processor.py +49 -41
  34. web3/providers/persistent/subscription_container.py +56 -0
  35. web3/providers/persistent/subscription_manager.py +298 -0
  36. web3/providers/persistent/websocket.py +4 -4
  37. web3/providers/rpc/async_rpc.py +16 -3
  38. web3/providers/rpc/rpc.py +9 -5
  39. web3/types.py +28 -14
  40. web3/utils/__init__.py +4 -0
  41. web3/utils/subscriptions.py +289 -0
  42. {web3-7.6.1.dist-info → web3-7.8.0.dist-info}/LICENSE +1 -1
  43. {web3-7.6.1.dist-info → web3-7.8.0.dist-info}/METADATA +68 -56
  44. {web3-7.6.1.dist-info → web3-7.8.0.dist-info}/RECORD +46 -43
  45. {web3-7.6.1.dist-info → web3-7.8.0.dist-info}/WHEEL +1 -1
  46. {web3-7.6.1.dist-info → web3-7.8.0.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,23 @@
1
1
  import asyncio
2
+ from dataclasses import (
3
+ dataclass,
4
+ )
2
5
  import pytest
3
6
  from typing import (
4
7
  TYPE_CHECKING,
5
8
  Any,
6
9
  Dict,
10
+ Generator,
11
+ List,
7
12
  Tuple,
13
+ Union,
8
14
  cast,
9
15
  )
10
16
 
17
+ from eth_typing import (
18
+ ChecksumAddress,
19
+ HexStr,
20
+ )
11
21
  from eth_utils import (
12
22
  is_hexstr,
13
23
  )
@@ -15,6 +25,13 @@ from hexbytes import (
15
25
  HexBytes,
16
26
  )
17
27
 
28
+ from web3 import (
29
+ AsyncWeb3,
30
+ PersistentConnectionProvider,
31
+ )
32
+ from web3.beacon import (
33
+ AsyncBeacon,
34
+ )
18
35
  from web3.datastructures import (
19
36
  AttributeDict,
20
37
  )
@@ -22,16 +39,43 @@ from web3.middleware import (
22
39
  ExtraDataToPOAMiddleware,
23
40
  )
24
41
  from web3.types import (
42
+ BlockData,
25
43
  FormattedEthSubscriptionResponse,
44
+ LogReceipt,
45
+ Nonce,
26
46
  RPCEndpoint,
47
+ TxData,
48
+ Wei,
49
+ )
50
+ from web3.utils import (
51
+ EthSubscription,
52
+ )
53
+ from web3.utils.subscriptions import (
54
+ LogsSubscription,
55
+ LogsSubscriptionContext,
56
+ NewHeadsSubscription,
57
+ NewHeadsSubscriptionContext,
58
+ PendingTxSubscription,
59
+ PendingTxSubscriptionContext,
27
60
  )
28
61
 
29
62
  if TYPE_CHECKING:
30
- from web3.main import (
31
- AsyncWeb3,
63
+ from web3.contract.async_contract import (
64
+ AsyncContract,
65
+ AsyncContractFunction,
66
+ )
67
+ from web3.providers.persistent.subscription_manager import (
68
+ SubscriptionContainer,
32
69
  )
33
70
 
34
71
 
72
+ # LogIndexedAndNotIndexed event args
73
+ INDEXED_ADDR = "0xdEad000000000000000000000000000000000000"
74
+ INDEXED_UINT256 = 1337
75
+ NON_INDEXED_ADDR = "0xbeeF000000000000000000000000000000000000"
76
+ NON_INDEXED_UINT256 = 1999
77
+ NON_INDEXED_STRING = "test logs subscriptions"
78
+
35
79
  SOME_BLOCK_KEYS = [
36
80
  "number",
37
81
  "hash",
@@ -39,214 +83,182 @@ SOME_BLOCK_KEYS = [
39
83
  "transactionsRoot",
40
84
  "stateRoot",
41
85
  "receiptsRoot",
42
- "size",
43
86
  "gasLimit",
44
87
  "gasUsed",
45
88
  "timestamp",
46
- "transactions",
47
89
  "baseFeePerGas",
90
+ "withdrawalsRoot",
48
91
  ]
49
92
 
50
93
 
94
+ @dataclass
95
+ class SubscriptionHandlerTest:
96
+ passed: bool = False
97
+
98
+
99
+ async def new_heads_handler(
100
+ handler_context: NewHeadsSubscriptionContext,
101
+ ) -> None:
102
+ w3 = handler_context.async_w3
103
+ sub = handler_context.subscription
104
+ assert isinstance(w3, AsyncWeb3)
105
+ provider = cast(PersistentConnectionProvider, w3.provider)
106
+ assert isinstance(provider.get_endpoint_uri_or_ipc_path(), str)
107
+
108
+ assert isinstance(sub, EthSubscription)
109
+
110
+ block = handler_context.result
111
+ assert block is not None
112
+ assert all(k in block.keys() for k in SOME_BLOCK_KEYS)
113
+
114
+ assert handler_context.new_heads_handler_test.passed is False
115
+ handler_context.new_heads_handler_test.passed = True
116
+ assert await sub.unsubscribe()
117
+
118
+
119
+ async def pending_tx_handler(
120
+ handler_context: PendingTxSubscriptionContext,
121
+ ) -> None:
122
+ w3 = handler_context.async_w3
123
+ sub = handler_context.subscription
124
+ tx = handler_context.result
125
+
126
+ assert w3 is not None
127
+ provider = cast(PersistentConnectionProvider, w3.provider)
128
+ assert isinstance(provider.get_endpoint_uri_or_ipc_path(), str)
129
+
130
+ assert isinstance(sub, PendingTxSubscription)
131
+
132
+ assert tx is not None
133
+ tx = cast(TxData, tx)
134
+ accts = await w3.eth.accounts
135
+ assert tx["from"] == accts[0]
136
+ await w3.eth.wait_for_transaction_receipt(tx["hash"])
137
+
138
+ assert handler_context.pending_tx_handler_test.passed is False
139
+ handler_context.pending_tx_handler_test.passed = True
140
+ assert await sub.unsubscribe()
141
+
142
+
143
+ async def logs_handler(
144
+ handler_context: LogsSubscriptionContext,
145
+ ) -> None:
146
+ w3 = handler_context.async_w3
147
+ sub = handler_context.subscription
148
+ log_receipt = handler_context.result
149
+
150
+ assert w3 is not None
151
+ provider = cast(PersistentConnectionProvider, w3.provider)
152
+ assert isinstance(provider.get_endpoint_uri_or_ipc_path(), str)
153
+
154
+ assert isinstance(sub, LogsSubscription)
155
+ event_data = handler_context.event.process_log(log_receipt)
156
+ assert event_data.args.indexedAddress == INDEXED_ADDR
157
+ assert event_data.args.indexedUint256 == INDEXED_UINT256
158
+ assert event_data.args.nonIndexedAddress == NON_INDEXED_ADDR
159
+ assert event_data.args.nonIndexedUint256 == NON_INDEXED_UINT256
160
+ assert event_data.args.nonIndexedString == NON_INDEXED_STRING
161
+
162
+ assert handler_context.logs_handler_test.passed is False
163
+ handler_context.logs_handler_test.passed = True
164
+ assert await sub.unsubscribe()
165
+
166
+
167
+ async def idle_handler(
168
+ _handler_context: Any,
169
+ ) -> None:
170
+ pass
171
+
172
+
173
+ async def emit_contract_event(
174
+ async_w3: AsyncWeb3,
175
+ acct: ChecksumAddress,
176
+ contract_function: "AsyncContractFunction",
177
+ args: Any = (),
178
+ delay: float = 0.25,
179
+ ) -> None:
180
+ await asyncio.sleep(delay)
181
+ tx_hash = await contract_function(*args).transact({"from": acct})
182
+ receipt = await async_w3.eth.wait_for_transaction_receipt(tx_hash)
183
+ assert receipt["status"] == 1
184
+
185
+
186
+ async def log_indexed_and_non_indexed_args_task(
187
+ async_w3: AsyncWeb3,
188
+ async_emitter_contract: "AsyncContract",
189
+ acct: ChecksumAddress,
190
+ delay: float = 0.1,
191
+ ) -> "asyncio.Task[None]":
192
+ return asyncio.create_task(
193
+ emit_contract_event(
194
+ async_w3,
195
+ acct,
196
+ async_emitter_contract.functions.logIndexedAndNotIndexedArgs,
197
+ args=(
198
+ INDEXED_ADDR,
199
+ INDEXED_UINT256,
200
+ NON_INDEXED_ADDR,
201
+ NON_INDEXED_UINT256,
202
+ NON_INDEXED_STRING,
203
+ ),
204
+ delay=delay,
205
+ )
206
+ )
207
+
208
+
209
+ def assert_no_subscriptions_left(sub_container: "SubscriptionContainer") -> None:
210
+ assert len(sub_container) == 0
211
+ assert len(sub_container.subscriptions) == 0
212
+ assert len(sub_container.subscriptions_by_id) == 0
213
+ assert len(sub_container.subscriptions_by_label) == 0
214
+ assert len(sub_container.handler_subscriptions) == 0
215
+
216
+
217
+ async def clean_up_task(task: "asyncio.Task[Any]") -> None:
218
+ task.cancel()
219
+ try:
220
+ await task
221
+ except asyncio.CancelledError:
222
+ pass
223
+
224
+
51
225
  class PersistentConnectionProviderTest:
226
+ @pytest.fixture(autouse=True)
227
+ def clear_caches(self, async_w3: AsyncWeb3) -> Generator[None, None, None]:
228
+ yield
229
+ async_w3.provider._request_processor.clear_caches()
230
+ async_w3.subscription_manager.total_handler_calls = 0
231
+
232
+ @staticmethod
233
+ async def seed_transactions_to_geth(
234
+ async_w3: AsyncWeb3,
235
+ acct: ChecksumAddress,
236
+ num_txs: int = 1,
237
+ delay: float = 0.1,
238
+ ) -> None:
239
+ nonce = int(await async_w3.eth.get_transaction_count(acct))
240
+
241
+ async def send_tx() -> None:
242
+ nonlocal nonce
243
+ await async_w3.eth.send_transaction(
244
+ {
245
+ "from": acct,
246
+ "to": acct,
247
+ "value": Wei(nonce),
248
+ "gas": 21000,
249
+ "nonce": Nonce(nonce),
250
+ }
251
+ )
252
+ nonce += 1
253
+
254
+ for _ in range(num_txs):
255
+ await asyncio.sleep(delay)
256
+ await send_tx()
257
+
52
258
  @pytest.mark.asyncio
53
259
  @pytest.mark.parametrize(
54
260
  "subscription_params,ws_subscription_response,expected_formatted_result",
55
261
  (
56
- (
57
- ("newHeads",),
58
- {
59
- "jsonrpc": "2.0",
60
- "method": "eth_subscription",
61
- "params": {
62
- "subscription": "THIS_WILL_BE_REPLACED_IN_THE_TEST",
63
- "result": {
64
- "number": "0x539",
65
- "hash": "0xb46b85928f2c2264c2bf7ad5c6d6985664f1527e744193ef990cc0d3da5afc5e", # noqa: E501
66
- "parentHash": "0xb46b85928f2c2264c2bf7ad5c6d6985664f1527e744193ef990cc0d3da5afc5e", # noqa: E501
67
- "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", # noqa: E501
68
- "logsBloom": "0x00",
69
- "transactionsRoot": "0x56260fe8298aff6d360e3a68fa855693f25dcb2708d8a7e509e8519b265d3988", # noqa: E501
70
- "stateRoot": "0x56260fe8298aff6d360e3a68fa855693f25dcb2708d8a7e509e8519b265d3988", # noqa: E501
71
- "receiptsRoot": "0x56260fe8298aff6d360e3a68fa855693f25dcb2708d8a7e509e8519b265d3988", # noqa: E501
72
- "miner": "0x0000000000000000000000000000000000000000",
73
- "difficulty": "0x0",
74
- "extraData": "0x496c6c756d696e61746520446d6f63726174697a6520447374726962757465", # noqa: E501
75
- "gasLimit": "0x1c9c380",
76
- "gasUsed": "0xd1ce44",
77
- "timestamp": "0x539",
78
- "baseFeePerGas": "0x26f93fef9",
79
- "withdrawalsRoot": "0x56260fe8298aff6d360e3a68fa855693f25dcb2708d8a7e509e8519b265d3988", # noqa: E501
80
- "nonce": "0x0000000000000000",
81
- "mixHash": "0x73e9e036ec894047f29954571d4b6d9e8717de7304269c263cbf150caa4e0768", # noqa: E501
82
- },
83
- },
84
- },
85
- AttributeDict(
86
- {
87
- "number": 1337,
88
- "hash": HexBytes(
89
- "0xb46b85928f2c2264c2bf7ad5c6d6985664f1527e744193ef990cc0d3da5afc5e" # noqa: E501
90
- ),
91
- "parentHash": HexBytes(
92
- "0xb46b85928f2c2264c2bf7ad5c6d6985664f1527e744193ef990cc0d3da5afc5e" # noqa: E501
93
- ),
94
- "sha3Uncles": HexBytes(
95
- "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" # noqa: E501
96
- ),
97
- "logsBloom": HexBytes("0x00"),
98
- "transactionsRoot": HexBytes(
99
- "0x56260fe8298aff6d360e3a68fa855693f25dcb2708d8a7e509e8519b265d3988" # noqa: E501
100
- ),
101
- "stateRoot": HexBytes(
102
- "0x56260fe8298aff6d360e3a68fa855693f25dcb2708d8a7e509e8519b265d3988" # noqa: E501
103
- ),
104
- "receiptsRoot": HexBytes(
105
- "0x56260fe8298aff6d360e3a68fa855693f25dcb2708d8a7e509e8519b265d3988" # noqa: E501
106
- ),
107
- "miner": "0x0000000000000000000000000000000000000000",
108
- "difficulty": 0,
109
- "extraData": HexBytes(
110
- "0x496c6c756d696e61746520446d6f63726174697a6520447374726962757465" # noqa: E501
111
- ),
112
- "gasLimit": 30000000,
113
- "gasUsed": 13749828,
114
- "timestamp": 1337,
115
- "baseFeePerGas": 10461904633,
116
- "withdrawalsRoot": HexBytes(
117
- "0x56260fe8298aff6d360e3a68fa855693f25dcb2708d8a7e509e8519b265d3988" # noqa: E501
118
- ),
119
- "nonce": HexBytes("0x0000000000000000"),
120
- "mixHash": HexBytes(
121
- "0x73e9e036ec894047f29954571d4b6d9e8717de7304269c263cbf150caa4e0768" # noqa: E501
122
- ),
123
- }
124
- ),
125
- ),
126
- (
127
- ("newPendingTransactions", True),
128
- {
129
- "jsonrpc": "2.0",
130
- "method": "eth_subscription",
131
- "params": {
132
- "subscription": "THIS_WILL_BE_REPLACED_IN_THE_TEST",
133
- "result": {
134
- "blockHash": None,
135
- "blockNumber": None,
136
- "from": "0x0000000000000000000000000000000000000000",
137
- "gas": "0xf2f4",
138
- "gasPrice": "0x29035f36f",
139
- "maxFeePerGas": "0x29035f36f",
140
- "maxPriorityFeePerGas": "0x3b9aca00",
141
- "hash": "0xb46b85928f2c2264c2bf7ad5c6d6985664f1527e744193ef990cc0d3da5afc5e", # noqa: E501
142
- "input": "0x00",
143
- "nonce": "0x2013",
144
- "to": "0x0000000000000000000000000000000000000000",
145
- "transactionIndex": None,
146
- "value": "0x0",
147
- "type": "0x2",
148
- "accessList": [],
149
- "chainId": "0x1",
150
- "v": "0x1",
151
- "r": "0x3c144a7c00ed3118d55445cd5be2ae4620ca377f7c685e9c5f3687671d4dece1", # noqa: E501
152
- "s": "0x284de67cbf75fec8a9edb368dee3a37cf6faba87f0af4413b2f869ebfa87d002", # noqa: E501
153
- "yParity": "0x1",
154
- },
155
- },
156
- },
157
- AttributeDict(
158
- {
159
- "blockHash": None,
160
- "blockNumber": None,
161
- "from": "0x0000000000000000000000000000000000000000",
162
- "gas": 62196,
163
- "gasPrice": 11009389423,
164
- "maxFeePerGas": 11009389423,
165
- "maxPriorityFeePerGas": 1000000000,
166
- "hash": HexBytes(
167
- "0xb46b85928f2c2264c2bf7ad5c6d6985664f1527e744193ef990cc0d3da5afc5e" # noqa: E501
168
- ),
169
- "input": HexBytes("0x00"),
170
- "nonce": 8211,
171
- "to": "0x0000000000000000000000000000000000000000",
172
- "transactionIndex": None,
173
- "value": 0,
174
- "type": 2,
175
- "accessList": [],
176
- "chainId": 1,
177
- "v": 1,
178
- "r": HexBytes(
179
- "0x3c144a7c00ed3118d55445cd5be2ae4620ca377f7c685e9c5f3687671d4dece1" # noqa: E501
180
- ),
181
- "s": HexBytes(
182
- "0x284de67cbf75fec8a9edb368dee3a37cf6faba87f0af4413b2f869ebfa87d002" # noqa: E501
183
- ),
184
- "yParity": 1,
185
- }
186
- ),
187
- ),
188
- (
189
- ("newPendingTransactions", False),
190
- {
191
- "jsonrpc": "2.0",
192
- "method": "eth_subscription",
193
- "params": {
194
- "subscription": "THIS_WILL_BE_REPLACED_IN_THE_TEST",
195
- "result": "0xb46b85928f2c2264c2bf7ad5c6d6985664f1527e744193ef990cc0d3da5afc5e", # noqa: E501
196
- },
197
- },
198
- HexBytes(
199
- "0xb46b85928f2c2264c2bf7ad5c6d6985664f1527e744193ef990cc0d3da5afc5e"
200
- ),
201
- ),
202
- (
203
- ("logs", {"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"}),
204
- {
205
- "jsonrpc": "2.0",
206
- "method": "eth_subscription",
207
- "params": {
208
- "subscription": "THIS_WILL_BE_REPLACED_IN_THE_TEST",
209
- "result": {
210
- "removed": False,
211
- "logIndex": "0x0",
212
- "transactionIndex": "0x0",
213
- "transactionHash": "0x56260fe8298aff6d360e3a68fa855693f25dcb2708d8a7e509e8519b265d3988", # noqa: E501
214
- "blockHash": "0xb46b85928f2c2264c2bf7ad5c6d6985664f1527e744193ef990cc0d3da5afc5e", # noqa: E501
215
- "blockNumber": "0x539",
216
- "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
217
- "data": "0x00",
218
- "topics": [
219
- "0xe1fffdd4923d04f559f4d29e8bfc6cda04eb5b0d3c460751c2402c5c5cc9105c", # noqa: E501
220
- "0x00000000000000000000000016250d5630b4cf539739df2c5dacb4c659f2482d", # noqa: E501
221
- ],
222
- },
223
- },
224
- },
225
- AttributeDict(
226
- {
227
- "removed": False,
228
- "logIndex": 0,
229
- "transactionIndex": 0,
230
- "transactionHash": HexBytes(
231
- "0x56260fe8298aff6d360e3a68fa855693f25dcb2708d8a7e509e8519b265d3988" # noqa: E501
232
- ),
233
- "blockHash": HexBytes(
234
- "0xb46b85928f2c2264c2bf7ad5c6d6985664f1527e744193ef990cc0d3da5afc5e" # noqa: E501
235
- ),
236
- "blockNumber": 1337,
237
- "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
238
- "data": HexBytes("0x00"),
239
- "topics": [
240
- HexBytes(
241
- "0xe1fffdd4923d04f559f4d29e8bfc6cda04eb5b0d3c460751c2402c5c5cc9105c" # noqa: E501
242
- ),
243
- HexBytes(
244
- "0x00000000000000000000000016250d5630b4cf539739df2c5dacb4c659f2482d" # noqa: E501
245
- ),
246
- ],
247
- }
248
- ),
249
- ),
250
262
  (
251
263
  ("syncing",),
252
264
  {
@@ -285,17 +297,13 @@ class PersistentConnectionProviderTest:
285
297
  ),
286
298
  ),
287
299
  ids=[
288
- "newHeads",
289
- "newPendingTransactions-FullTxs",
290
- "newPendingTransactions-TxHashes",
291
- "logs",
292
300
  "syncing-False",
293
301
  "syncing-True",
294
302
  ],
295
303
  )
296
- async def test_async_eth_subscribe_mocked(
304
+ async def test_async_eth_subscribe_syncing_mocked(
297
305
  self,
298
- async_w3: "AsyncWeb3",
306
+ async_w3: AsyncWeb3,
299
307
  subscription_params: Tuple[Any, ...],
300
308
  ws_subscription_response: Dict[str, Any],
301
309
  expected_formatted_result: Any,
@@ -321,10 +329,261 @@ class PersistentConnectionProviderTest:
321
329
  await async_w3.eth.unsubscribe(sub_id)
322
330
  break
323
331
 
332
+ assert_no_subscriptions_left(
333
+ async_w3.subscription_manager._subscription_container
334
+ )
335
+
336
+ @pytest.mark.asyncio
337
+ async def test_async_eth_subscribe_new_heads(self, async_w3: AsyncWeb3) -> None:
338
+ sub_id = await async_w3.eth.subscribe("newHeads")
339
+ assert is_hexstr(sub_id)
340
+
341
+ async for msg in async_w3.socket.process_subscriptions():
342
+ response = cast(FormattedEthSubscriptionResponse, msg)
343
+ assert response["subscription"] == sub_id
344
+ result = cast(BlockData, response["result"])
345
+ assert all(k in result.keys() for k in SOME_BLOCK_KEYS)
346
+ break
347
+
348
+ assert await async_w3.eth.unsubscribe(sub_id)
349
+ assert_no_subscriptions_left(
350
+ async_w3.subscription_manager._subscription_container
351
+ )
352
+
353
+ @pytest.mark.asyncio
354
+ async def test_async_eth_subscribe_creates_and_handles_new_heads_subscription_type(
355
+ self,
356
+ async_w3: AsyncWeb3,
357
+ ) -> None:
358
+ sub_manager = async_w3.subscription_manager
359
+ new_heads_handler_test = SubscriptionHandlerTest()
360
+
361
+ sub_id = await async_w3.eth.subscribe(
362
+ "newHeads",
363
+ handler=new_heads_handler,
364
+ handler_context={"new_heads_handler_test": new_heads_handler_test},
365
+ )
366
+ assert is_hexstr(sub_id)
367
+
368
+ assert len(sub_manager.subscriptions) == 1
369
+ sub = sub_manager.subscriptions[0]
370
+ assert isinstance(sub, NewHeadsSubscription)
371
+
372
+ await sub_manager.handle_subscriptions()
373
+
374
+ assert new_heads_handler_test.passed
375
+ assert len(sub_manager.subscriptions) == 0
376
+
377
+ assert sub_manager.total_handler_calls == 1
378
+ assert sub.handler_call_count == 1
379
+
380
+ @pytest.mark.asyncio
381
+ async def test_async_eth_subscribe_process_pending_tx_true(
382
+ self,
383
+ async_w3: AsyncWeb3,
384
+ ) -> None:
385
+ sub_id = await async_w3.eth.subscribe("newPendingTransactions", True)
386
+ assert is_hexstr(sub_id)
387
+
388
+ accts = await async_w3.eth.accounts
389
+ acct = accts[0]
390
+
391
+ num_txs = 2
392
+ original_nonce = await async_w3.eth.get_transaction_count(acct)
393
+ tx_seeder_task = asyncio.create_task(
394
+ self.seed_transactions_to_geth(async_w3, acct, num_txs=num_txs)
395
+ )
396
+
397
+ nonce = int(original_nonce)
398
+ tx_hash = None
399
+ async for msg in async_w3.socket.process_subscriptions():
400
+ response = cast(FormattedEthSubscriptionResponse, msg)
401
+ assert response["subscription"] == sub_id
402
+ result = cast(TxData, response["result"])
403
+ assert result["gas"] == 21000
404
+ assert result["from"] == acct
405
+ assert result["to"] == acct
406
+ assert int(result["value"]) == int(nonce)
407
+ tx_hash = result["hash"]
408
+ assert tx_hash is not None
409
+
410
+ nonce += 1
411
+ if nonce == int(original_nonce) + num_txs:
412
+ break
413
+
414
+ # cleanup
415
+ assert await async_w3.eth.unsubscribe(sub_id)
416
+ assert_no_subscriptions_left(
417
+ async_w3.subscription_manager._subscription_container
418
+ )
419
+ async_w3.provider._request_processor.clear_caches()
420
+ await async_w3.eth.wait_for_transaction_receipt(tx_hash)
421
+ await clean_up_task(tx_seeder_task)
422
+
423
+ @pytest.mark.asyncio
424
+ async def test_async_eth_subscribe_and_process_pending_tx_false(
425
+ self,
426
+ async_w3: AsyncWeb3,
427
+ ) -> None:
428
+ sub_id = await async_w3.eth.subscribe("newPendingTransactions")
429
+ assert is_hexstr(sub_id)
430
+
431
+ accts = await async_w3.eth.accounts
432
+ acct = accts[0]
433
+ await async_w3.eth.get_transaction_count(acct)
434
+
435
+ num_txs = 2
436
+ tx_seeder_task = asyncio.create_task(
437
+ self.seed_transactions_to_geth(async_w3, acct, num_txs=num_txs)
438
+ )
439
+
440
+ tx_hash = None
441
+ i = 0
442
+ async for msg in async_w3.socket.process_subscriptions():
443
+ response = cast(FormattedEthSubscriptionResponse, msg)
444
+ assert response["subscription"] == sub_id
445
+ assert isinstance(response["result"], HexBytes)
446
+ tx_hash = response["result"]
447
+
448
+ i += 1
449
+ if i == num_txs:
450
+ break
451
+
452
+ # cleanup
453
+ await async_w3.eth.unsubscribe(sub_id)
454
+ assert_no_subscriptions_left(
455
+ async_w3.subscription_manager._subscription_container
456
+ )
457
+ await async_w3.eth.wait_for_transaction_receipt(tx_hash)
458
+ await clean_up_task(tx_seeder_task)
459
+
460
+ @pytest.mark.asyncio
461
+ async def test_async_eth_subscribe_creates_and_handles_pending_tx_subscription_type(
462
+ self,
463
+ async_w3: AsyncWeb3,
464
+ ) -> None:
465
+ sub_manager = async_w3.subscription_manager
466
+ pending_tx_handler_test = SubscriptionHandlerTest()
467
+
468
+ sub_id = await async_w3.eth.subscribe(
469
+ "newPendingTransactions",
470
+ True,
471
+ handler=pending_tx_handler,
472
+ handler_context={"pending_tx_handler_test": pending_tx_handler_test},
473
+ )
474
+ assert is_hexstr(sub_id)
475
+
476
+ assert len(sub_manager.subscriptions) == 1
477
+ sub = sub_manager.subscriptions[0]
478
+ assert isinstance(sub, PendingTxSubscription)
479
+
480
+ # seed transactions to geth
481
+ accts = await async_w3.eth.accounts
482
+ acct = accts[0]
483
+ tx_seeder_task = asyncio.create_task(
484
+ self.seed_transactions_to_geth(async_w3, acct)
485
+ )
486
+ await sub_manager.handle_subscriptions()
487
+
488
+ assert pending_tx_handler_test.passed
489
+ assert len(sub_manager.subscriptions) == 0
490
+
491
+ assert sub_manager.total_handler_calls == 1
492
+ assert sub.handler_call_count == 1
493
+
494
+ # cleanup
495
+ sub_manager.total_handler_calls = 0
496
+ await clean_up_task(tx_seeder_task)
497
+
498
+ @pytest.mark.asyncio
499
+ async def test_async_eth_subscribe_and_process_logs(
500
+ self, async_w3: AsyncWeb3, async_emitter_contract: "AsyncContract"
501
+ ) -> None:
502
+ event = async_emitter_contract.events.LogIndexedAndNotIndexed
503
+ event_topic = async_w3.keccak(text=event.abi_element_identifier).to_0x_hex()
504
+
505
+ sub_id = await async_w3.eth.subscribe(
506
+ "logs",
507
+ {
508
+ "address": async_emitter_contract.address,
509
+ "topics": [HexStr(event_topic)],
510
+ },
511
+ )
512
+ assert is_hexstr(sub_id)
513
+
514
+ accts = await async_w3.eth.accounts
515
+ acct = accts[0]
516
+ emit_event_task = await log_indexed_and_non_indexed_args_task(
517
+ async_w3, async_emitter_contract, acct
518
+ )
519
+
520
+ async for msg in async_w3.socket.process_subscriptions():
521
+ response = cast(FormattedEthSubscriptionResponse, msg)
522
+ assert response["subscription"] == sub_id
523
+ log_receipt = cast(LogReceipt, response["result"])
524
+ event_data = event.process_log(log_receipt)
525
+ assert event_data.args.indexedAddress == INDEXED_ADDR
526
+ assert event_data.args.indexedUint256 == INDEXED_UINT256
527
+ assert event_data.args.nonIndexedAddress == NON_INDEXED_ADDR
528
+ assert event_data.args.nonIndexedUint256 == NON_INDEXED_UINT256
529
+ assert event_data.args.nonIndexedString == NON_INDEXED_STRING
530
+ break
531
+
532
+ assert await async_w3.eth.unsubscribe(sub_id)
533
+ assert len(async_w3.subscription_manager.subscriptions) == 0
534
+ await clean_up_task(emit_event_task)
535
+
536
+ @pytest.mark.asyncio
537
+ async def test_async_eth_subscribe_creates_and_handles_logs_subscription_type(
538
+ self,
539
+ async_w3: AsyncWeb3,
540
+ async_emitter_contract: "AsyncContract",
541
+ ) -> None:
542
+ sub_manager = async_w3.subscription_manager
543
+
544
+ event = async_emitter_contract.events.LogIndexedAndNotIndexed
545
+
546
+ logs_handler_test = SubscriptionHandlerTest()
547
+ sub_id = await async_w3.eth.subscribe(
548
+ "logs",
549
+ {
550
+ "address": async_emitter_contract.address,
551
+ "topics": [event.topic],
552
+ },
553
+ handler=logs_handler,
554
+ handler_context={
555
+ "logs_handler_test": logs_handler_test,
556
+ "event": event,
557
+ },
558
+ )
559
+ assert is_hexstr(sub_id)
560
+
561
+ assert len(sub_manager.subscriptions) == 1
562
+ sub = sub_manager.subscriptions[0]
563
+ assert isinstance(sub, LogsSubscription)
564
+
565
+ accts = await async_w3.eth.accounts
566
+ acct = accts[0]
567
+ emit_event_task = await log_indexed_and_non_indexed_args_task(
568
+ async_w3, async_emitter_contract, acct
569
+ )
570
+
571
+ await sub_manager.handle_subscriptions()
572
+
573
+ assert logs_handler_test.passed
574
+ assert len(sub_manager.subscriptions) == 0
575
+
576
+ assert sub_manager.total_handler_calls == 1
577
+ assert sub.handler_call_count == 1
578
+
579
+ # cleanup
580
+ sub_manager.total_handler_calls = 0
581
+ await clean_up_task(emit_event_task)
582
+
324
583
  @pytest.mark.asyncio
325
584
  async def test_async_extradata_poa_middleware_on_eth_subscription(
326
585
  self,
327
- async_w3: "AsyncWeb3",
586
+ async_w3: AsyncWeb3,
328
587
  ) -> None:
329
588
  async_w3.middleware_onion.inject(
330
589
  ExtraDataToPOAMiddleware, "poa_middleware", layer=0
@@ -361,12 +620,13 @@ class PersistentConnectionProviderTest:
361
620
  break
362
621
 
363
622
  # clean up
623
+ assert await async_w3.eth.unsubscribe(sub_id)
364
624
  async_w3.middleware_onion.remove("poa_middleware")
365
625
 
366
626
  @pytest.mark.asyncio
367
627
  async def test_asyncio_gather_for_multiple_requests_matches_the_responses(
368
628
  self,
369
- async_w3: "AsyncWeb3",
629
+ async_w3: AsyncWeb3,
370
630
  ) -> None:
371
631
  (
372
632
  latest,
@@ -402,7 +662,10 @@ class PersistentConnectionProviderTest:
402
662
  assert isinstance(chain_id3, int)
403
663
 
404
664
  @pytest.mark.asyncio
405
- async def test_public_socket_api(self, async_w3: "AsyncWeb3") -> None:
665
+ async def test_async_public_socket_api(self, async_w3: AsyncWeb3) -> None:
666
+ # clear all caches and queues
667
+ async_w3.provider._request_processor.clear_caches()
668
+
406
669
  # send a request over the socket
407
670
  await async_w3.socket.send(
408
671
  RPCEndpoint("eth_getBlockByNumber"), ["latest", True]
@@ -425,3 +688,229 @@ class PersistentConnectionProviderTest:
425
688
  assert "result" in response, "Expected 'result' key in response."
426
689
  assert all(k in response["result"].keys() for k in SOME_BLOCK_KEYS)
427
690
  assert not isinstance(response["result"]["number"], int) # assert not processed
691
+
692
+ @pytest.mark.asyncio
693
+ async def test_async_subscription_manager_subscribes_to_many_subscriptions(
694
+ self, async_w3: AsyncWeb3, async_emitter_contract: "AsyncContract"
695
+ ) -> None:
696
+ sub_manager = async_w3.subscription_manager
697
+
698
+ event = async_emitter_contract.events.LogIndexedAndNotIndexed
699
+ event_topic = async_w3.keccak(text=event.abi_element_identifier).to_0x_hex()
700
+
701
+ new_heads_handler_test = SubscriptionHandlerTest()
702
+ pending_tx_handler_test = SubscriptionHandlerTest()
703
+ logs_handler_test = SubscriptionHandlerTest()
704
+
705
+ await sub_manager.subscribe(
706
+ [
707
+ NewHeadsSubscription(
708
+ handler=new_heads_handler,
709
+ handler_context={"new_heads_handler_test": new_heads_handler_test},
710
+ ),
711
+ PendingTxSubscription(
712
+ full_transactions=True,
713
+ handler=pending_tx_handler,
714
+ handler_context={
715
+ "pending_tx_handler_test": pending_tx_handler_test
716
+ },
717
+ ),
718
+ LogsSubscription(
719
+ address=async_emitter_contract.address,
720
+ topics=[HexStr(event_topic)],
721
+ handler=logs_handler,
722
+ handler_context={
723
+ "logs_handler_test": logs_handler_test,
724
+ "event": event,
725
+ },
726
+ ),
727
+ ]
728
+ )
729
+
730
+ # emit contract event for `logs` subscription
731
+ accts = await async_w3.eth.accounts
732
+ acct = accts[0]
733
+ emit_event_task = await log_indexed_and_non_indexed_args_task(
734
+ async_w3, async_emitter_contract, acct
735
+ )
736
+
737
+ # get subscriptions before they are unsubscribed and removed
738
+ subs = sub_manager.subscriptions
739
+
740
+ await sub_manager.handle_subscriptions()
741
+
742
+ # assert unsubscribed and removed subscriptions
743
+ assert len(sub_manager.subscriptions) == 0
744
+
745
+ assert sub_manager.total_handler_calls == 3
746
+ assert all(sub.handler_call_count == 1 for sub in subs)
747
+
748
+ assert new_heads_handler_test.passed
749
+ assert pending_tx_handler_test.passed
750
+ assert logs_handler_test.passed
751
+
752
+ # cleanup
753
+ sub_manager.total_handler_calls = 0
754
+ await clean_up_task(emit_event_task)
755
+
756
+ @pytest.mark.asyncio
757
+ async def test_subscription_handler_context(self, async_w3: AsyncWeb3) -> None:
758
+ base_url = "http://localhost:1337"
759
+ async_beacon = AsyncBeacon(base_url)
760
+ handler_test = SubscriptionHandlerTest()
761
+
762
+ async def test_sub_handler(
763
+ handler_context: NewHeadsSubscriptionContext,
764
+ ) -> None:
765
+ beacon = handler_context.beacon
766
+ assert isinstance(beacon, AsyncBeacon)
767
+ assert beacon.base_url == base_url
768
+ assert handler_context.int1 == 1337
769
+ assert handler_context.str1 == "foo"
770
+ assert handler_context.int2 == 1999
771
+ assert handler_context.str2 == "bar"
772
+
773
+ handler_context.handler_test.passed = True
774
+ unsubscribed = await handler_context.subscription.unsubscribe()
775
+ assert unsubscribed
776
+
777
+ subscribed = await async_w3.eth.subscribe(
778
+ "newHeads",
779
+ label="foo",
780
+ handler=test_sub_handler,
781
+ handler_context={
782
+ "beacon": async_beacon,
783
+ "int1": 1337,
784
+ "str1": "foo",
785
+ "int2": 1999,
786
+ "str2": "bar",
787
+ "handler_test": handler_test,
788
+ },
789
+ )
790
+ assert is_hexstr(subscribed)
791
+
792
+ sub_manager = async_w3.subscription_manager
793
+
794
+ await sub_manager.handle_subscriptions()
795
+
796
+ assert len(sub_manager.subscriptions) == 0
797
+ assert sub_manager.total_handler_calls == 1
798
+ assert handler_test.passed
799
+
800
+ # cleanup
801
+ sub_manager.total_handler_calls = 0
802
+
803
+ @pytest.mark.asyncio
804
+ async def test_subscriptions_with_handler_and_without(
805
+ self, async_w3: AsyncWeb3
806
+ ) -> None:
807
+ handler_test = SubscriptionHandlerTest()
808
+ stream_passed = False
809
+
810
+ async def test_sub_handler(
811
+ handler_context: NewHeadsSubscriptionContext,
812
+ ) -> None:
813
+ handler_context.handler_test.passed = True
814
+ await handler_context.subscription.unsubscribe()
815
+
816
+ async def handle_subscription_stream() -> None:
817
+ nonlocal stream_passed
818
+ async for msg in async_w3.socket.process_subscriptions():
819
+ response = cast(FormattedEthSubscriptionResponse, msg)
820
+ assert sub_manager.get_by_id(response["subscription"]) is not None
821
+ assert response["result"] is not None
822
+ # wait for the handler to unsubscribe:
823
+ stream_passed = True
824
+ await async_w3.eth.unsubscribe(response["subscription"])
825
+ break
826
+
827
+ await async_w3.eth.subscribe(
828
+ "newHeads",
829
+ handler=test_sub_handler,
830
+ label="managed",
831
+ handler_context={"handler_test": handler_test},
832
+ )
833
+ await async_w3.eth.subscribe("newHeads", label="streamed")
834
+
835
+ sub_manager = async_w3.subscription_manager
836
+ assert len(sub_manager.subscriptions) == 2
837
+
838
+ await asyncio.gather(
839
+ sub_manager.handle_subscriptions(),
840
+ handle_subscription_stream(),
841
+ )
842
+
843
+ assert len(sub_manager.subscriptions) == 0
844
+ assert sub_manager.total_handler_calls == 1
845
+ assert handler_test.passed
846
+ assert stream_passed
847
+
848
+ # cleanup
849
+ sub_manager.total_handler_calls = 0
850
+
851
+ @pytest.mark.asyncio
852
+ async def test_handle_subscriptions_breaks_on_unsubscribe(
853
+ self,
854
+ async_w3: AsyncWeb3,
855
+ ) -> None:
856
+ async def unsubscribe_subs(
857
+ subs: List[Union[NewHeadsSubscription, LogsSubscription]]
858
+ ) -> None:
859
+ for sub in subs:
860
+ await sub.unsubscribe()
861
+
862
+ sub_manager = async_w3.subscription_manager
863
+ sub1 = NewHeadsSubscription(label="foo", handler=idle_handler)
864
+ sub2 = LogsSubscription(label="bar", handler=idle_handler)
865
+
866
+ await sub_manager.subscribe([sub1, sub2])
867
+
868
+ assert sub_manager.subscriptions == [sub1, sub2]
869
+
870
+ unsubscribe_task = asyncio.create_task(unsubscribe_subs([sub1, sub2]))
871
+ # With no subscriptions in the queue, ``handle_subscriptions`` should hang
872
+ # indefinitely. Test that when the last subscription is unsubscribed from,
873
+ # the method breaks out of the loop. This is done via a raised
874
+ # ``SubscriptionProcessingFinished`` within the ``TaskReliantQueue``.
875
+ await sub_manager.handle_subscriptions()
876
+
877
+ assert_no_subscriptions_left(sub_manager._subscription_container)
878
+ await clean_up_task(unsubscribe_task)
879
+
880
+ @pytest.mark.asyncio
881
+ async def test_run_forever_starts_with_0_subs_and_runs_until_task_cancelled(
882
+ self, async_w3: AsyncWeb3
883
+ ) -> None:
884
+ sub_manager = async_w3.subscription_manager
885
+ assert_no_subscriptions_left(sub_manager._subscription_container)
886
+
887
+ run_forever_task = asyncio.create_task(
888
+ sub_manager.handle_subscriptions(run_forever=True)
889
+ )
890
+
891
+ await asyncio.sleep(0.1)
892
+ assert run_forever_task.done() is False
893
+ assert sub_manager.subscriptions == []
894
+
895
+ # subscribe to newHeads and validate it
896
+ new_heads_handler_test = SubscriptionHandlerTest()
897
+ sub1 = NewHeadsSubscription(
898
+ label="foo",
899
+ handler=new_heads_handler,
900
+ handler_context={"new_heads_handler_test": new_heads_handler_test},
901
+ )
902
+ sub_id = await sub_manager.subscribe(sub1)
903
+ assert is_hexstr(sub_id)
904
+ assert len(sub_manager.subscriptions) == 1
905
+ assert sub_manager.subscriptions[0] == sub1
906
+
907
+ # wait for the handler to unsubscribe
908
+ while sub_manager.subscriptions:
909
+ await asyncio.sleep(0.1)
910
+
911
+ assert new_heads_handler_test.passed
912
+ assert run_forever_task.done() is False
913
+ assert run_forever_task.cancelled() is False
914
+
915
+ # cleanup
916
+ await clean_up_task(run_forever_task)