11from dataclasses import dataclass
22from datetime import datetime , timezone
3- from typing import List , Optional
3+ from typing import Any , Callable , Dict , List , Optional
44
55import logging
66import requests
77
8- logger = logging .getLogger (__name__ )
9-
108
119class L1Client :
1210 L1_MAINNET_URL = "https://eth-mainnet.g.alchemy.com/v2/{api_key}"
@@ -19,7 +17,6 @@ class L1Client:
1917 )
2018 # Taken from ethereum_base_layer_contracts.rs
2119 STARKNET_L1_CONTRACT_ADDRESS = "0xc662c410C0ECf747543f5bA90660f6ABeBD9C8c4"
22- RETRIES_COUNT = 2
2320
2421 @dataclass (frozen = True )
2522 class Log :
@@ -38,52 +35,82 @@ class Log:
3835 removed : bool
3936 block_timestamp : int
4037
41- @staticmethod
42- def get_logs (from_block : int , to_block : int , api_key : str ) -> List ["L1Client.Log" ]:
38+ def __init__ (
39+ self ,
40+ api_key : str ,
41+ logger : logging .Logger ,
42+ timeout : int = 10 ,
43+ retries_count : int = 2 ,
44+ ):
45+ self .api_key = api_key
46+ self .logger = logger
47+ self .timeout = timeout
48+ self .retries_count = retries_count
49+ self .rpc_url = self .L1_MAINNET_URL .format (api_key = api_key )
50+ self .data_api_url = self .DATA_BLOCKS_BY_TIMESTAMP_URL_FMT .format (api_key = api_key )
51+
52+ def _run_request_with_retry (
53+ self ,
54+ request_func : Callable [[], Dict ],
55+ method_name_for_logs : str ,
56+ additional_log_context : Dict [str , Any ],
57+ ) -> Optional [Dict ]:
58+ for attempt in range (self .retries_count ):
59+ try :
60+ result = request_func ()
61+ self .logger .debug (
62+ f"{ method_name_for_logs } succeeded on attempt { attempt + 1 } " ,
63+ extra = additional_log_context ,
64+ )
65+ return result
66+ except (requests .RequestException , ValueError ):
67+ self .logger .debug (
68+ f"{ method_name_for_logs } attempt { attempt + 1 } /{ self .retries_count } failed" ,
69+ extra = additional_log_context ,
70+ exc_info = True ,
71+ )
72+
73+ self .logger .error (
74+ f"{ method_name_for_logs } failed after { self .retries_count } attempts, returning None" ,
75+ extra = additional_log_context ,
76+ )
77+
78+ return None
79+
80+ def get_logs (self , from_block : int , to_block : int ) -> List ["L1Client.Log" ]:
4381 """
4482 Get logs from Ethereum using eth_getLogs RPC method.
45- Tries up to RETRIES_COUNT times. On failure, logs an error and returns [].
83+ Tries up to retries_count times. On failure, logs an error and returns [].
4684 """
4785 if from_block > to_block :
4886 raise ValueError ("from_block must be less than or equal to to_block" )
4987
50- rpc_url = L1Client .L1_MAINNET_URL .format (api_key = api_key )
51-
5288 payload = {
5389 "jsonrpc" : "2.0" ,
5490 "method" : "eth_getLogs" ,
5591 "params" : [
5692 {
5793 "fromBlock" : hex (from_block ),
5894 "toBlock" : hex (to_block ),
59- "address" : L1Client .STARKNET_L1_CONTRACT_ADDRESS ,
60- "topics" : [L1Client .LOG_MESSAGE_TO_L2_EVENT_SIGNATURE ],
95+ "address" : self .STARKNET_L1_CONTRACT_ADDRESS ,
96+ "topics" : [self .LOG_MESSAGE_TO_L2_EVENT_SIGNATURE ],
6197 }
6298 ],
6399 "id" : 1 ,
64100 }
65101
66- for attempt in range (L1Client .RETRIES_COUNT ):
67- try :
68- response = requests .post (rpc_url , json = payload , timeout = 10 )
69- response .raise_for_status ()
70- data = response .json ()
71- logger .debug (
72- f"get_logs succeeded on attempt { attempt + 1 } " ,
73- extra = {"url" : rpc_url , "from_block" : from_block , "to_block" : to_block },
74- )
75- break
76- except (requests .RequestException , ValueError ):
77- logger .debug (
78- f"get_logs attempt { attempt + 1 } /{ L1Client .RETRIES_COUNT } failed" ,
79- extra = {"url" : rpc_url , "from_block" : from_block , "to_block" : to_block },
80- exc_info = True ,
81- )
82- else :
83- logger .error (
84- f"get_logs failed after { L1Client .RETRIES_COUNT } attempts, returning []" ,
85- extra = {"url" : rpc_url , "from_block" : from_block , "to_block" : to_block },
86- )
102+ def request_func ():
103+ response = requests .post (self .rpc_url , json = payload , timeout = self .timeout )
104+ response .raise_for_status ()
105+ return response .json ()
106+
107+ data = self ._run_request_with_retry (
108+ request_func = request_func ,
109+ method_name_for_logs = "get_logs" ,
110+ additional_log_context = {"url" : self .rpc_url , "from_block" : from_block , "to_block" : to_block },
111+ )
112+
113+ if data is None :
87114 return []
88115
89116 results = data .get ("result" , [])
@@ -104,43 +131,30 @@ def get_logs(from_block: int, to_block: int, api_key: str) -> List["L1Client.Log
104131 for result in results
105132 ]
106133
107- @staticmethod
108- def get_timestamp_of_block (block_number : int , api_key : str ) -> Optional [int ]:
134+ def get_timestamp_of_block (self , block_number : int ) -> Optional [int ]:
109135 """
110136 Get block timestamp by block number using eth_getBlockByNumber RPC method.
111- Tries up to RETRIES_COUNT times. On failure, logs an error and returns None.
137+ Tries up to retries_count times. On failure, logs an error and returns None.
112138 """
113- rpc_url = L1Client .L1_MAINNET_URL .format (api_key = api_key )
114-
115139 payload = {
116140 "jsonrpc" : "2.0" ,
117141 "method" : "eth_getBlockByNumber" ,
118142 "params" : [hex (block_number ), False ],
119143 "id" : 1 ,
120144 }
121145
122- for attempt in range (L1Client .RETRIES_COUNT ):
123- try :
124- response = requests .post (rpc_url , json = payload , timeout = 10 )
125- response .raise_for_status ()
126- result = response .json ()
127- logger .debug (
128- f"get_timestamp_of_block succeeded on attempt { attempt + 1 } " ,
129- extra = {"url" : rpc_url , "block_number" : block_number },
130- )
131- break # success -> exit loop
132- except (requests .RequestException , ValueError ) as exc :
133- logger .debug (
134- f"get_timestamp_of_block attempt { attempt + 1 } /{ L1Client .RETRIES_COUNT } failed" ,
135- extra = {"url" : rpc_url , "block_number" : block_number },
136- exc_info = True ,
137- )
146+ def request_func ():
147+ response = requests .post (self .rpc_url , json = payload , timeout = self .timeout )
148+ response .raise_for_status ()
149+ return response .json ()
138150
139- else :
140- logger .error (
141- f"get_timestamp_of_block failed after { L1Client .RETRIES_COUNT } attempts, returning None" ,
142- extra = {"url" : rpc_url , "block_number" : block_number },
143- )
151+ result = self ._run_request_with_retry (
152+ request_func = request_func ,
153+ method_name_for_logs = "get_timestamp_of_block" ,
154+ additional_log_context = {"url" : self .rpc_url , "block_number" : block_number },
155+ )
156+
157+ if result is None :
144158 return None
145159
146160 block = result .get ("result" )
@@ -151,14 +165,11 @@ def get_timestamp_of_block(block_number: int, api_key: str) -> Optional[int]:
151165 # Timestamp is hex string, convert to int.
152166 return int (block ["timestamp" ], 16 )
153167
154- @staticmethod
155- def get_block_number_by_timestamp (timestamp : int , api_key : str ) -> Optional [int ]:
168+ def get_block_number_by_timestamp (self , timestamp : int ) -> Optional [int ]:
156169 """
157170 Get the block number at/after a given timestamp using blocks-by-timestamp API.
158- Tries up to RETRIES_COUNT times. On failure, logs an error and returns None.
171+ Tries up to retries_count times. On failure, logs an error and returns None.
159172 """
160- rpc_url = L1Client .DATA_BLOCKS_BY_TIMESTAMP_URL_FMT .format (api_key = api_key )
161-
162173 timestamp_iso = (
163174 datetime .fromtimestamp (timestamp , tz = timezone .utc ).isoformat ().replace ("+00:00" , "Z" )
164175 )
@@ -169,27 +180,18 @@ def get_block_number_by_timestamp(timestamp: int, api_key: str) -> Optional[int]
169180 "direction" : "AFTER" ,
170181 }
171182
172- for attempt in range (L1Client .RETRIES_COUNT ):
173- try :
174- response = requests .get (rpc_url , params = params , timeout = 10 )
175- response .raise_for_status ()
176- data = response .json ()
177- logger .debug (
178- f"get_block_number_by_timestamp succeeded on attempt { attempt + 1 } " ,
179- extra = {"url" : rpc_url , "timestamp" : timestamp },
180- )
181- break # success -> exit loop
182- except (requests .RequestException , ValueError ) as exc :
183- logger .debug (
184- f"get_block_number_by_timestamp attempt { attempt + 1 } /{ L1Client .RETRIES_COUNT } failed" ,
185- extra = {"url" : rpc_url , "timestamp" : timestamp },
186- exc_info = True ,
187- )
188- else :
189- logger .error (
190- f"get_block_number_by_timestamp failed after { L1Client .RETRIES_COUNT } attempts, returning None" ,
191- extra = {"url" : rpc_url , "timestamp" : timestamp },
192- )
183+ def request_func ():
184+ response = requests .get (self .data_api_url , params = params , timeout = self .timeout )
185+ response .raise_for_status ()
186+ return response .json ()
187+
188+ data = self ._run_request_with_retry (
189+ request_func = request_func ,
190+ method_name_for_logs = "get_block_number_by_timestamp" ,
191+ additional_log_context = {"url" : self .data_api_url , "timestamp" : timestamp },
192+ )
193+
194+ if data is None :
193195 return None
194196
195197 items = data .get ("data" , [])
0 commit comments