fastweb3-objects 0.1.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.
- fastweb3_objects-0.1.0.dist-info/METADATA +233 -0
- fastweb3_objects-0.1.0.dist-info/RECORD +22 -0
- fastweb3_objects-0.1.0.dist-info/WHEEL +5 -0
- fastweb3_objects-0.1.0.dist-info/licenses/LICENSE +21 -0
- fastweb3_objects-0.1.0.dist-info/top_level.txt +1 -0
- fw3_objects/__init__.py +12 -0
- fw3_objects/abi.py +511 -0
- fw3_objects/account.py +407 -0
- fw3_objects/cache/__init__.py +1 -0
- fw3_objects/cache/db.py +73 -0
- fw3_objects/cache/metadata.py +89 -0
- fw3_objects/cache/rpc.py +205 -0
- fw3_objects/chain.py +317 -0
- fw3_objects/contract.py +647 -0
- fw3_objects/errors.py +92 -0
- fw3_objects/events.py +379 -0
- fw3_objects/explorers/__init__.py +1 -0
- fw3_objects/explorers/blockscout.py +83 -0
- fw3_objects/explorers/etherscan.py +96 -0
- fw3_objects/explorers/lookup.py +267 -0
- fw3_objects/monitor.py +125 -0
- fw3_objects/transaction.py +348 -0
fw3_objects/cache/rpc.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Internal RPC response cache middleware used by Chain Web3 instances."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .db import CacheDB, get_cache_db
|
|
11
|
+
|
|
12
|
+
RPC_CACHE_SCHEMA = """
|
|
13
|
+
CREATE TABLE IF NOT EXISTS rpc_cache (
|
|
14
|
+
chain_id INTEGER NOT NULL,
|
|
15
|
+
method TEXT NOT NULL,
|
|
16
|
+
params_json TEXT NOT NULL,
|
|
17
|
+
result_json TEXT NOT NULL,
|
|
18
|
+
created_at INTEGER NOT NULL,
|
|
19
|
+
updated_at INTEGER NOT NULL,
|
|
20
|
+
PRIMARY KEY (chain_id, method, params_json)
|
|
21
|
+
);
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
_READ = object()
|
|
25
|
+
CacheRule = Callable[[Any, Any], Any | None]
|
|
26
|
+
|
|
27
|
+
ERC20_METADATA_SELECTORS = {
|
|
28
|
+
"0x06fdde03", # name()
|
|
29
|
+
"0x95d89b41", # symbol()
|
|
30
|
+
"0x313ce567", # decimals()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _json_dumps(value: Any) -> str:
|
|
35
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _eth_get_code_cache_params(call, result: Any = _READ) -> Any | None:
|
|
39
|
+
params = getattr(call, "params", None)
|
|
40
|
+
if not isinstance(params, (list, tuple)) or len(params) < 2:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
address, block = params[:2]
|
|
44
|
+
if block != "latest":
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
if result is not _READ and result == "0x":
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
return [address.lower(), "latest"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _eth_call_cache_params(call, result: Any = _READ) -> Any | None:
|
|
54
|
+
params = getattr(call, "params", None)
|
|
55
|
+
if not isinstance(params, (list, tuple)) or len(params) < 2:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
tx, block = params[:2]
|
|
59
|
+
if block != "latest":
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
if not isinstance(tx, dict):
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
to = tx.get("to")
|
|
66
|
+
data = tx.get("data") or tx.get("input")
|
|
67
|
+
|
|
68
|
+
if not isinstance(to, str) or not isinstance(data, str):
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
data = data.lower()
|
|
72
|
+
if data not in ERC20_METADATA_SELECTORS:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
if isinstance(result, Exception):
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
return [to.lower(), data, "latest"]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
CACHE_RULES: dict[str, CacheRule] = {
|
|
82
|
+
"eth_getCode": _eth_get_code_cache_params,
|
|
83
|
+
"eth_call": _eth_call_cache_params,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def cache_params(call, result: Any = _READ) -> Any | None:
|
|
88
|
+
"""Return normalized cache params for a call, if it is cacheable."""
|
|
89
|
+
rule = CACHE_RULES.get(call.method)
|
|
90
|
+
if rule is None:
|
|
91
|
+
return None
|
|
92
|
+
return rule(call, result)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class RpcCache:
|
|
96
|
+
"""Read and write raw RPC cache entries."""
|
|
97
|
+
|
|
98
|
+
def __init__(self, db: CacheDB | None = None) -> None:
|
|
99
|
+
"""Initialize the RPC cache.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
db: Cache database. If omitted, the process-global cache is used.
|
|
103
|
+
"""
|
|
104
|
+
if db is None:
|
|
105
|
+
db = get_cache_db()
|
|
106
|
+
self.db = db
|
|
107
|
+
|
|
108
|
+
def get(self, chain_id: int, method: str, params: Any) -> Any | None:
|
|
109
|
+
"""Return a cached RPC result, if present."""
|
|
110
|
+
params_json = _json_dumps(params)
|
|
111
|
+
row = self.db.execute(
|
|
112
|
+
"""
|
|
113
|
+
SELECT result_json FROM rpc_cache
|
|
114
|
+
WHERE chain_id = ? AND method = ? AND params_json = ?
|
|
115
|
+
""",
|
|
116
|
+
(int(chain_id), method, params_json),
|
|
117
|
+
).fetchone()
|
|
118
|
+
|
|
119
|
+
if row is None:
|
|
120
|
+
return None
|
|
121
|
+
return json.loads(row["result_json"])
|
|
122
|
+
|
|
123
|
+
def set(self, chain_id: int, method: str, params: Any, result: Any) -> None:
|
|
124
|
+
"""Store an RPC result."""
|
|
125
|
+
now = int(time.time())
|
|
126
|
+
params_json = _json_dumps(params)
|
|
127
|
+
result_json = _json_dumps(result)
|
|
128
|
+
self.db.execute(
|
|
129
|
+
"""
|
|
130
|
+
INSERT INTO rpc_cache (
|
|
131
|
+
chain_id, method, params_json, result_json, created_at, updated_at
|
|
132
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
133
|
+
ON CONFLICT(chain_id, method, params_json) DO UPDATE SET
|
|
134
|
+
result_json = excluded.result_json,
|
|
135
|
+
updated_at = excluded.updated_at
|
|
136
|
+
""",
|
|
137
|
+
(
|
|
138
|
+
int(chain_id),
|
|
139
|
+
method,
|
|
140
|
+
params_json,
|
|
141
|
+
result_json,
|
|
142
|
+
now,
|
|
143
|
+
now,
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
self.db.commit()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class RpcCacheMiddleware:
|
|
150
|
+
"""fastweb3 middleware that caches selected RPC responses."""
|
|
151
|
+
|
|
152
|
+
def __init__(self, chain_id: int, db: CacheDB | None = None) -> None:
|
|
153
|
+
"""Initialize the middleware.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
chain_id: Chain ID to use in cache keys.
|
|
157
|
+
db: Cache database. If omitted, the process-global cache is used.
|
|
158
|
+
"""
|
|
159
|
+
self.chain_id = int(chain_id)
|
|
160
|
+
self.cache = RpcCache(db)
|
|
161
|
+
|
|
162
|
+
def before_request(self, ctx, calls):
|
|
163
|
+
"""Remove cached calls from the outbound request list."""
|
|
164
|
+
cached_results = {}
|
|
165
|
+
miss_indexes = []
|
|
166
|
+
miss_calls = []
|
|
167
|
+
|
|
168
|
+
for idx, call in enumerate(calls):
|
|
169
|
+
params = cache_params(call)
|
|
170
|
+
|
|
171
|
+
if params is not None:
|
|
172
|
+
result = self.cache.get(self.chain_id, call.method, params)
|
|
173
|
+
if result is not None:
|
|
174
|
+
cached_results[idx] = result
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
miss_indexes.append(idx)
|
|
178
|
+
miss_calls.append(call)
|
|
179
|
+
|
|
180
|
+
ctx.state["rpc_cache_cached_results"] = cached_results
|
|
181
|
+
ctx.state["rpc_cache_miss_indexes"] = miss_indexes
|
|
182
|
+
ctx.state["rpc_cache_miss_calls"] = miss_calls
|
|
183
|
+
ctx.state["rpc_cache_original_count"] = len(calls)
|
|
184
|
+
|
|
185
|
+
return miss_calls
|
|
186
|
+
|
|
187
|
+
def after_request(self, ctx, calls, results):
|
|
188
|
+
"""Store fresh results and merge them with cached hits."""
|
|
189
|
+
cached_results = ctx.state.get("rpc_cache_cached_results", {})
|
|
190
|
+
miss_indexes = ctx.state.get("rpc_cache_miss_indexes", [])
|
|
191
|
+
miss_calls = ctx.state.get("rpc_cache_miss_calls", [])
|
|
192
|
+
original_count = ctx.state.get("rpc_cache_original_count", len(results))
|
|
193
|
+
|
|
194
|
+
merged = [None] * original_count
|
|
195
|
+
|
|
196
|
+
for idx, result in cached_results.items():
|
|
197
|
+
merged[idx] = result
|
|
198
|
+
|
|
199
|
+
for idx, call, result in zip(miss_indexes, miss_calls, results, strict=True):
|
|
200
|
+
params = cache_params(call, result)
|
|
201
|
+
if params is not None:
|
|
202
|
+
self.cache.set(self.chain_id, call.method, params, result)
|
|
203
|
+
merged[idx] = result
|
|
204
|
+
|
|
205
|
+
return merged
|
fw3_objects/chain.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from contextlib import AbstractContextManager
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from fw3 import Web3
|
|
10
|
+
from fw3.deferred import deferred_response
|
|
11
|
+
from fw3.validation import block_ref, hash32
|
|
12
|
+
|
|
13
|
+
from .cache.rpc import RpcCacheMiddleware
|
|
14
|
+
from .errors import ChainMismatch
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _DefaultChainContext(AbstractContextManager["Chain"]):
|
|
18
|
+
"""Context manager for temporarily setting the thread-local default chain."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, chain: "Chain", *, strict: bool = False) -> None:
|
|
21
|
+
self._chain = chain
|
|
22
|
+
self._strict = strict
|
|
23
|
+
self._previous: tuple[Chain | None, bool] = (None, False)
|
|
24
|
+
|
|
25
|
+
def __enter__(self) -> "Chain":
|
|
26
|
+
self._previous = Chain._get_default_chain()
|
|
27
|
+
previous_chain, previous_strict = self._previous
|
|
28
|
+
|
|
29
|
+
if previous_strict:
|
|
30
|
+
raise ChainMismatch(
|
|
31
|
+
previous_chain, self._chain, "cannot nest default Chain contexts inside strict mode"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if self._strict and previous_chain is not None and previous_chain is not self._chain:
|
|
35
|
+
raise ChainMismatch(
|
|
36
|
+
previous_chain, self._chain, "default Chain context manager in strict mode"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
Chain._set_default_chain(self._chain, self._strict)
|
|
40
|
+
return self._chain
|
|
41
|
+
|
|
42
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
43
|
+
Chain._set_default_chain(*self._previous)
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Chain:
|
|
48
|
+
"""Canonical chain context for block, transaction, and contract access."""
|
|
49
|
+
|
|
50
|
+
_instances: dict[int, "Chain"] = {}
|
|
51
|
+
_instances_lock = threading.RLock()
|
|
52
|
+
|
|
53
|
+
_thread_local = threading.local()
|
|
54
|
+
|
|
55
|
+
def __new__(cls, chain_id: int) -> "Chain":
|
|
56
|
+
"""Return the canonical instance for a chain ID.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
chain_id: Chain ID to instantiate.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Canonical Chain instance for the given chain ID.
|
|
63
|
+
"""
|
|
64
|
+
chain_id = int(chain_id)
|
|
65
|
+
|
|
66
|
+
with cls._instances_lock:
|
|
67
|
+
if chain_id not in cls._instances:
|
|
68
|
+
cls._instances[chain_id] = super().__new__(cls)
|
|
69
|
+
|
|
70
|
+
return cls._instances[chain_id]
|
|
71
|
+
|
|
72
|
+
def __init__(self, chain_id: int) -> None:
|
|
73
|
+
"""Initialize chain state.
|
|
74
|
+
|
|
75
|
+
Web3 configuration is stored immediately, but the Web3 client itself is created
|
|
76
|
+
lazily on first access.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
chain_id: Chain ID to initialize.
|
|
80
|
+
"""
|
|
81
|
+
if getattr(self, "_initialized", False):
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
self._initialized = True
|
|
85
|
+
self._chain_id = int(chain_id)
|
|
86
|
+
self._w3_params: dict[str, Any] = {}
|
|
87
|
+
self._w3: Web3 | None = None
|
|
88
|
+
self.__transaction_monitor = None
|
|
89
|
+
|
|
90
|
+
def __repr__(self) -> str:
|
|
91
|
+
"""Return a developer-friendly representation."""
|
|
92
|
+
return f"Chain({self.id})"
|
|
93
|
+
|
|
94
|
+
def __int__(self) -> int:
|
|
95
|
+
"""Return the numeric chain ID."""
|
|
96
|
+
return self.id
|
|
97
|
+
|
|
98
|
+
def __len__(self) -> int:
|
|
99
|
+
"""Return the number of addressable block indices.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Deferred value resolving to latest block height plus one.
|
|
103
|
+
"""
|
|
104
|
+
height = self.height()
|
|
105
|
+
return deferred_response(None, ref_func=lambda h: h.set_value(height + 1))
|
|
106
|
+
|
|
107
|
+
def __getitem__(self, block_number: int):
|
|
108
|
+
"""Return a block by number.
|
|
109
|
+
|
|
110
|
+
Negative indices are resolved relative to the latest block.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
block_number: Absolute or negative-relative block number.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Requested block object.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
TypeError: If ``block_number`` is not an integer.
|
|
120
|
+
IndexError: If a negative index resolves before genesis.
|
|
121
|
+
|
|
122
|
+
Notes:
|
|
123
|
+
Negative indices (``-2`` and below) require an additional RPC call to
|
|
124
|
+
resolve the latest block height. As a result, accessing the returned
|
|
125
|
+
value will perform I/O on first use.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
if isinstance(block_number, slice):
|
|
129
|
+
raise TypeError("Slicing is not supported")
|
|
130
|
+
|
|
131
|
+
if not isinstance(block_number, int):
|
|
132
|
+
raise TypeError("block_number must be int")
|
|
133
|
+
|
|
134
|
+
if block_number > -2:
|
|
135
|
+
if block_number == -1:
|
|
136
|
+
block_number = "latest"
|
|
137
|
+
return self.get_block(block_number)
|
|
138
|
+
|
|
139
|
+
height = self.height()
|
|
140
|
+
|
|
141
|
+
def ref_func(handle):
|
|
142
|
+
blk = height + 1 + block_number
|
|
143
|
+
if blk < 0:
|
|
144
|
+
raise IndexError("block index out of range")
|
|
145
|
+
handle.set_value(self.get_block(blk))
|
|
146
|
+
|
|
147
|
+
return deferred_response(None, ref_func=ref_func)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def id(self) -> int:
|
|
151
|
+
"""Return the chain ID."""
|
|
152
|
+
return self._chain_id
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def w3(self) -> Web3:
|
|
156
|
+
"""Return the configured Web3 instance for this chain.
|
|
157
|
+
|
|
158
|
+
The instance is created lazily on first access.
|
|
159
|
+
|
|
160
|
+
When a default chain context is active in strict mode, access is only
|
|
161
|
+
permitted on that chain. Attempting to access ``w3`` on any other chain
|
|
162
|
+
will raise.
|
|
163
|
+
|
|
164
|
+
Raises:
|
|
165
|
+
ChainMismatch: If a strict default chain context is active and this chain is
|
|
166
|
+
not the default chain.
|
|
167
|
+
"""
|
|
168
|
+
default_chain, strict = self._get_default_chain()
|
|
169
|
+
if strict and default_chain is not None and default_chain is not self:
|
|
170
|
+
raise ChainMismatch(default_chain, self, "strict default chain")
|
|
171
|
+
if self._w3 is None:
|
|
172
|
+
self._create_w3(**self._w3_params)
|
|
173
|
+
return self._w3
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def _transaction_monitor(self):
|
|
177
|
+
if self.__transaction_monitor is None:
|
|
178
|
+
from .monitor import TransactionMonitor
|
|
179
|
+
|
|
180
|
+
self.__transaction_monitor = TransactionMonitor(self)
|
|
181
|
+
return self.__transaction_monitor
|
|
182
|
+
|
|
183
|
+
def height(self) -> int:
|
|
184
|
+
"""Return the latest block number."""
|
|
185
|
+
return self.w3.eth.block_number()
|
|
186
|
+
|
|
187
|
+
def block_gas_limit(self) -> int:
|
|
188
|
+
"""Return the gas limit of the latest block."""
|
|
189
|
+
block = self.get_block("latest")
|
|
190
|
+
|
|
191
|
+
return deferred_response(None, ref_func=lambda h: h.set_value(block["gasLimit"]))
|
|
192
|
+
|
|
193
|
+
def base_fee(self) -> int:
|
|
194
|
+
"""Return the base fee per gas of the latest block."""
|
|
195
|
+
fee_history = self.w3.eth.fee_history(1, "latest", [])
|
|
196
|
+
return deferred_response(
|
|
197
|
+
None, ref_func=lambda h: h.set_value(fee_history["baseFeePerGas"][0])
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def priority_fee(self) -> int:
|
|
201
|
+
"""Return the suggested max priority fee per gas."""
|
|
202
|
+
return self.w3.eth.max_priority_fee_per_gas()
|
|
203
|
+
|
|
204
|
+
def get_transaction(self, hash: str | bytes):
|
|
205
|
+
"""Return a transaction by hash.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
hash: Transaction hash as a hex string or bytes.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Transaction object.
|
|
212
|
+
"""
|
|
213
|
+
from .transaction import Transaction
|
|
214
|
+
|
|
215
|
+
return Transaction(hash, chain=self)
|
|
216
|
+
|
|
217
|
+
def get_block(self, block_identifier: int | str | bytes):
|
|
218
|
+
"""Return a block by number, tag, or hash.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
block_identifier: Block number, block tag, or block hash.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
RPC block object.
|
|
225
|
+
"""
|
|
226
|
+
if isinstance(block_identifier, bytes):
|
|
227
|
+
return self.w3.eth.get_block_by_hash(
|
|
228
|
+
hash32(block_identifier, name="block", strict=True)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
normalized = block_ref(block_identifier, strict=True)
|
|
232
|
+
|
|
233
|
+
if (
|
|
234
|
+
isinstance(block_identifier, str)
|
|
235
|
+
and normalized.startswith("0x")
|
|
236
|
+
and len(normalized) == 66
|
|
237
|
+
):
|
|
238
|
+
return self.w3.eth.get_block_by_hash(normalized)
|
|
239
|
+
|
|
240
|
+
return self.w3.eth.get_block_by_number(normalized)
|
|
241
|
+
|
|
242
|
+
def new_blocks(
|
|
243
|
+
self,
|
|
244
|
+
height_buffer: int = 0,
|
|
245
|
+
poll_interval: float = 5.0,
|
|
246
|
+
) -> Iterator:
|
|
247
|
+
"""Yield new blocks as they become available.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
height_buffer: Number of blocks behind the tip to follow.
|
|
251
|
+
poll_interval: Target polling interval in seconds.
|
|
252
|
+
|
|
253
|
+
Yields:
|
|
254
|
+
New buffered blocks in ascending height order.
|
|
255
|
+
|
|
256
|
+
Raises:
|
|
257
|
+
ValueError: If ``height_buffer`` is negative or ``poll_interval`` is not
|
|
258
|
+
positive.
|
|
259
|
+
"""
|
|
260
|
+
if height_buffer < 0:
|
|
261
|
+
raise ValueError("height_buffer must be >= 0")
|
|
262
|
+
|
|
263
|
+
if poll_interval <= 0:
|
|
264
|
+
raise ValueError("poll_interval must be > 0")
|
|
265
|
+
|
|
266
|
+
last_height = max(0, self.height() - height_buffer)
|
|
267
|
+
|
|
268
|
+
while True:
|
|
269
|
+
started_at = time.monotonic()
|
|
270
|
+
current_height = max(0, self.height() - height_buffer)
|
|
271
|
+
|
|
272
|
+
while current_height > last_height:
|
|
273
|
+
last_height += 1
|
|
274
|
+
yield self.get_block(last_height)
|
|
275
|
+
|
|
276
|
+
elapsed = time.monotonic() - started_at
|
|
277
|
+
time.sleep(max(0, poll_interval - elapsed))
|
|
278
|
+
|
|
279
|
+
def as_default(self, *, strict: bool = False) -> AbstractContextManager["Chain"]:
|
|
280
|
+
"""Return a context manager that sets this thread's default chain.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
strict: Whether to reject access through any other chain while the context
|
|
284
|
+
is active.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Context manager that restores the previous default chain on exit.
|
|
288
|
+
"""
|
|
289
|
+
return _DefaultChainContext(self, strict=strict)
|
|
290
|
+
|
|
291
|
+
def _create_w3(self, **w3_params: Any) -> None:
|
|
292
|
+
"""Create and assign a new ``Web3`` instance for this chain."""
|
|
293
|
+
self._w3_params = dict(w3_params)
|
|
294
|
+
self._w3 = Web3(chain_id=self.id, **self._w3_params)
|
|
295
|
+
self._w3.provider.add_middleware(RpcCacheMiddleware(self.id))
|
|
296
|
+
|
|
297
|
+
@classmethod
|
|
298
|
+
def _get_default_chain(cls) -> tuple["Chain | None", bool]:
|
|
299
|
+
return (
|
|
300
|
+
getattr(cls._thread_local, "default_chain", None),
|
|
301
|
+
getattr(cls._thread_local, "default_chain_strict", False),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
@classmethod
|
|
305
|
+
def _set_default_chain(cls, chain: "Chain | None", strict: bool) -> None:
|
|
306
|
+
cls._thread_local.default_chain = chain
|
|
307
|
+
cls._thread_local.default_chain_strict = strict
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def configure_chain(chain: Chain | int, **w3_params: Any) -> None:
|
|
311
|
+
"""Configure the canonical Chain instance for a chain ID.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
chain: Chain instance or chain ID to configure.
|
|
315
|
+
**w3_params: Keyword arguments forwarded to ``Web3``.
|
|
316
|
+
"""
|
|
317
|
+
Chain(chain)._create_w3(**w3_params)
|