1
1
import os
2
- from functools import lru_cache
3
2
from time import sleep
4
3
from typing import Optional , TypedDict , Sequence
5
4
6
5
from eth_typing import BlockNumber , Address , ChecksumAddress
6
+ from eth_typing .evm import BlockParams
7
7
from hexbytes import HexBytes
8
- from web3 .datastructures import AttributeDict
9
8
from web3 .eth import Eth
10
9
from web3 .exceptions import ContractLogicError
11
10
from web3 .types import FilterParams , LogReceipt , StateOverride , BlockIdentifier , TxParams , BlockData , _Hash32
@@ -19,10 +18,22 @@ class ForkedBlock(Exception):
19
18
20
19
21
20
class FilterParamsExtended (TypedDict , total = False ):
21
+ address : ChecksumAddress | list [ChecksumAddress ]
22
+ blockHash : HexBytes
23
+ fromBlock : BlockParams | BlockNumber | int
24
+ fromBlockParentHash : _Hash32
25
+ toBlock : BlockParams | BlockNumber | int
26
+ toBlockHash : _Hash32
27
+ topics : Sequence [_Hash32 | Sequence [_Hash32 ] | None ]
28
+
29
+
30
+ class FilterParamsSanitized (TypedDict , total = False ):
22
31
address : Address | ChecksumAddress | list [Address ] | list [ChecksumAddress ]
23
32
blockHash : HexBytes
24
- fromBlock : BlockIdentifier | BlockData
25
- toBlock : BlockIdentifier | BlockData
33
+ fromBlock : int
34
+ fromBlockParentHash : str
35
+ toBlock : int
36
+ toBlockHash : str
26
37
topics : Sequence [_Hash32 | Sequence [_Hash32 ] | None ]
27
38
28
39
@@ -139,32 +150,60 @@ def get_block(
139
150
140
151
def get_logs (
141
152
self ,
142
- filter_params : FilterParamsExtended ,
153
+ _filter_params : FilterParamsExtended ,
143
154
show_progress_bar : bool = False ,
144
155
p_bar = None ,
145
156
no_retry : bool = False ,
146
- use_subsquid : bool = os .getenv ("NO_SUBSQUID_LOGS" ) is None ,
147
- get_logs_by_block_hash : bool = False
157
+ use_subsquid : bool = os .getenv ("NO_SUBSQUID_LOGS" ) is None
148
158
) -> list [LogReceipt ]:
149
159
filter_block_range = self .w3 .filter_block_range
150
160
if filter_block_range == 0 :
151
161
raise Exception ("RPC does not support eth_getLogs" )
152
162
153
163
# getting logs for a single block defined by its block hash. No drama
154
- if "blockHash" in filter_params :
155
- assert "fromBlock" not in filter_params and "toBlock" not in filter_params
156
- return self .get_logs_inner (filter_params , no_retry = no_retry )
164
+ if "blockHash" in _filter_params :
165
+ assert "fromBlock" not in _filter_params and "toBlock" not in _filter_params
166
+ return self .get_logs_inner (_filter_params , no_retry = no_retry )
167
+
168
+ filter_params : FilterParamsSanitized = {** _filter_params }
169
+
170
+ if "fromBlockParentHash" in filter_params :
171
+ if filter_params ["fromBlockParentHash" ] is None :
172
+ del filter_params ["fromBlockParentHash" ]
173
+ elif not isinstance (filter_params ["fromBlockParentHash" ], str ):
174
+ filter_params ["fromBlockParentHash" ] = filter_params ["fromBlockParentHash" ].to_0x_hex ()
175
+
176
+ if "toBlockHash" in filter_params :
177
+ if filter_params ["toBlockHash" ] is None :
178
+ del filter_params ["toBlockHash" ]
179
+ elif not isinstance (filter_params ["toBlockHash" ], str ):
180
+ filter_params ["toBlockHash" ] = filter_params ["toBlockHash" ].to_0x_hex ()
181
+
182
+ if "fromBlock" not in filter_params or not isinstance (filter_params ["fromBlock" ], int ):
183
+ assert "fromBlockParentHash" not in filter_params , "can not specify fromBlockParentHash without fromBlock number"
184
+ filter_params ["fromBlock" ] = self .get_block (filter_params .get ("fromBlock" , "earliest" ))["number" ]
185
+
186
+ if "toBlock" not in filter_params or not isinstance (filter_params ["toBlock" ], int ):
187
+ assert "toBlockHash" not in filter_params , "can not specify toBlockHash without toBlock number"
188
+ filter_params ["toBlock" ] = self .get_block (filter_params .get ("toBlock" , "latest" ))["number" ]
157
189
158
- from_block , from_block_body = self .sanitize_block (filter_params .get ("fromBlock" , "latest" ))
159
- to_block , to_block_body = self .sanitize_block (filter_params .get ("toBlock" , "latest" ))
160
- filter_params = {** filter_params , "fromBlock" : from_block , "toBlock" : to_block }
190
+ kwargs = dict (
191
+ show_progress_bar = show_progress_bar ,
192
+ p_bar = p_bar ,
193
+ no_retry = no_retry ,
194
+ use_subsquid = use_subsquid ,
195
+ )
196
+
197
+ from_block = filter_params ["fromBlock" ]
198
+ to_block = filter_params ["toBlock" ]
199
+ from_block_parent_hash = filter_params .get ("fromBlockParentHash" )
200
+ to_block_hash = filter_params .get ("toBlockHash" )
161
201
162
- assert to_block >= from_block , f"{ from_block = } , { to_block = } "
202
+ assert to_block >= from_block , f"from block after to block, { from_block = } , { to_block = } "
163
203
164
204
# if logs for a single block are queried, and we know the block hash, query by it
165
- if from_block == to_block and (from_block_body or to_block_body ):
166
- block_body = from_block_body if from_block_body else to_block_body
167
- single_hash_filter = {** filter_params , "blockHash" : block_body ["hash" ]}
205
+ if from_block == to_block and to_block_hash is not None :
206
+ single_hash_filter = {** filter_params , "blockHash" : to_block_hash }
168
207
del single_hash_filter ["fromBlock" ]
169
208
del single_hash_filter ["toBlock" ]
170
209
return self .get_logs_inner (single_hash_filter , no_retry = no_retry )
@@ -177,78 +216,26 @@ def get_logs(
177
216
# local import as tqdm is an optional dependency of this package
178
217
from tqdm import tqdm
179
218
p_bar = tqdm (total = num_blocks )
219
+ kwargs ["p_bar" ] = p_bar
180
220
181
- kwargs = dict (
182
- show_progress_bar = show_progress_bar ,
183
- p_bar = p_bar ,
184
- no_retry = no_retry ,
185
- use_subsquid = use_subsquid ,
186
- )
187
-
188
- if use_subsquid and self .w3 .subsquid_available and from_block < self .w3 .latest_seen_block - self .w3 .unstable_blocks :
221
+ if use_subsquid and self .w3 .subsquid_available and from_block < self .w3 .latest_seen_block - 1_000 :
189
222
kwargs ["use_subsquid" ] = False # make sure we only try once with Subsquid
190
223
try :
191
224
# trying to get logs from SubSquid
192
- till_block , results = get_filter (
225
+ next_block , results = get_filter (
193
226
chain_id = self .chain_id ,
194
- filter_params = { ** filter_params , "toBlock" : min ( to_block , self . w3 . latest_seen_block - self . w3 . unstable_blocks )} ,
227
+ filter_params = filter_params ,
195
228
partial_allowed = True ,
196
229
p_bar = p_bar ,
197
230
)
198
- if till_block >= to_block :
199
- return results
200
- return results + self .get_logs ({** filter_params , "fromBlock" : till_block + 1 }, ** kwargs )
201
231
except Exception as e :
202
232
if not isinstance (e , ValueError ) or "Subsquid only has indexed till block " not in str (e ):
203
233
print (f"Getting logs from SubSquid threw exception { repr (e )} , falling back to RPC" )
204
-
205
- last_stable_block = self .w3 .latest_seen_block - self .w3 .unstable_blocks
206
- if get_logs_by_block_hash or to_block > last_stable_block :
207
- # getting logs via from and to block range sometimes drops logs.
208
- # This does not happen when getting them individually for each block by their block hash
209
- # get all block hashes and ensure they build upon each other
210
-
211
- # if only unstable blocks need to be gathered by hash, gather stable blocks as log range
212
- results : list [LogReceipt ] = []
213
- if not get_logs_by_block_hash and from_block < last_stable_block :
214
- results += self .get_logs ({** filter_params , "toBlock" : last_stable_block }, ** kwargs )
215
- from_block = last_stable_block + 1
216
- assert to_block >= from_block
217
- num_blocks = to_block - from_block + 1
218
-
219
- with self .w3 .batch_requests () as batch :
220
- batch .add_mapping ({
221
- self .w3 .eth ._get_block : list (range (from_block , to_block + 1 ))
222
- })
223
- blocks : list [BlockData ] = batch .execute ()
224
- assert len (blocks ) == num_blocks
225
-
226
- # make sure chain of blocks is consistent with each block building on the previous one
227
- for i , block_number in enumerate (range (from_block , to_block + 1 )):
228
- block = blocks [i ]
229
- if i != 0 :
230
- if block ["parentHash" ] != blocks [i - 1 ]["hash" ]:
231
- raise ForkedBlock (f"expected={ blocks [i - 1 ]['hash' ].to_0x_hex ()} , actual={ block ['parentHash' ].to_0x_hex ()} " )
232
- if from_block_body is not None and from_block_body ["number" ] == block_number :
233
- if block ["hash" ] != from_block_body ["hash" ]:
234
- raise ForkedBlock (f"expected={ from_block_body ['hash' ].to_0x_hex ()} , actual={ block ['hash' ].to_0x_hex ()} " )
235
- if to_block_body is not None and to_block_body ["number" ] == block_number :
236
- if block ["hash" ] != to_block_body ["hash" ]:
237
- raise ForkedBlock (f"expected={ to_block_body ['hash' ].to_0x_hex ()} , actual={ block ['hash' ].to_0x_hex ()} " )
238
-
239
- single_hash_filter = filter_params .copy ()
240
- del single_hash_filter ["fromBlock" ]
241
- del single_hash_filter ["toBlock" ]
242
- with self .w3 .batch_requests () as batch :
243
- batch .add_mapping ({
244
- self .w3 .eth ._get_logs : [{** single_hash_filter , "blockHash" : block ["hash" ]} for block in blocks ]
245
- })
246
- results_per_block : list [list [LogReceipt ]] = batch .execute ()
247
- assert len (results_per_block ) == num_blocks
248
- if p_bar is not None :
249
- p_bar .update (len (blocks ))
250
- results += sum (results_per_block , [])
251
- return results
234
+ else :
235
+ assert next_block <= to_block + 1 , "SubSquid returned logs for more blocks than specified"
236
+ if next_block == to_block + 1 :
237
+ return results
238
+ return results + self .get_logs ({** filter_params , "fromBlock" : next_block }, ** kwargs )
252
239
253
240
# getting logs for a single block, which is not at the chain head. No drama
254
241
if num_blocks == 1 :
@@ -258,16 +245,42 @@ def get_logs(
258
245
if num_blocks > filter_block_range :
259
246
results = []
260
247
for filter_start in range (from_block , to_block + 1 , filter_block_range ):
261
- results += self .get_logs ({
248
+ filter_end = min (filter_start + filter_block_range - 1 , to_block )
249
+ partial_filter = {
262
250
** filter_params ,
263
251
"fromBlock" : filter_start ,
264
- "toBlock" : min (filter_start + filter_block_range - 1 , to_block ),
265
- }, ** kwargs )
252
+ "toBlock" : filter_end ,
253
+ }
254
+ if to_block_hash is not None and filter_end != to_block :
255
+ del partial_filter ["toBlockHash" ]
256
+ if from_block_parent_hash is not None and filter_start != from_block :
257
+ del partial_filter ["fromBlockParentHash" ]
258
+ results += self .get_logs (partial_filter , ** kwargs )
266
259
return results
267
260
268
261
# get logs and split on exception
269
262
try :
270
- events = self ._get_logs (filter_params )
263
+ with self .w3 .batch_requests () as batch :
264
+ if from_block_parent_hash is not None :
265
+ batch .add (self ._get_block (from_block ))
266
+ batch .add (self ._get_logs (filter_params ))
267
+ batch .add (self ._get_block (to_block ))
268
+ events : list [LogReceipt ]
269
+ to_block_body : BlockData
270
+ batch_results = batch .execute ()
271
+ if from_block_parent_hash is not None :
272
+ events , to_block_body = batch_results
273
+ else :
274
+ from_block_body : BlockData
275
+ events , to_block_body , from_block_body = batch_results
276
+ assert from_block_body ["number" ] == from_block , "eth_getLogs RPC returned unexpected from block number"
277
+ if from_block_body ["parentHash" ].to_0x_hex () != from_block_parent_hash :
278
+ raise ForkedBlock (f"expected={ from_block_parent_hash } , actual={ from_block_body ['parentHash' ].to_0x_hex ()} " )
279
+
280
+ assert to_block_body ["number" ] == to_block , "eth_getLogs RPC returned unexpected to block number"
281
+ if to_block_hash is not None and to_block_body ["hash" ].to_0x_hex () != to_block_hash :
282
+ raise ForkedBlock (f"expected={ to_block_hash } , actual={ to_block_body ['hash' ].to_0x_hex ()} " )
283
+
271
284
if p_bar is not None :
272
285
p_bar .update (num_blocks )
273
286
return events
@@ -277,21 +290,12 @@ def get_logs(
277
290
mid_block = (from_block + to_block ) // 2
278
291
left_filter = {** filter_params , "toBlock" : mid_block }
279
292
right_filter = {** filter_params , "fromBlock" : mid_block + 1 }
293
+ if "toBlockHash" in left_filter :
294
+ del left_filter ["toBlockHash" ]
295
+ if "fromBlockParentHash" in right_filter :
296
+ del right_filter ["fromBlockParentHash" ]
280
297
return self .get_logs (left_filter , ** kwargs ) + self .get_logs (right_filter , ** kwargs )
281
298
282
-
283
- def sanitize_block (self , block : BlockIdentifier | BlockData ) -> tuple [int , BlockData | None ]:
284
- if isinstance (block , int ):
285
- block_body = None
286
- block_number = block
287
- elif isinstance (block , AttributeDict ) or isinstance (block , dict ):
288
- block_body = block
289
- block_number = block ["number" ]
290
- else :
291
- block_body = self .get_block (block )
292
- block_number = block_body ["number" ]
293
- return block_number , block_body
294
-
295
299
def get_logs_inner (self , filter_params : FilterParams , no_retry : bool = False ):
296
300
if not self .w3 .should_retry :
297
301
no_retry = True
0 commit comments