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
5+ import functools
6+ import inspect
57import logging
68import requests
79
8- logger = logging .getLogger (__name__ )
9-
1010
1111class L1Client :
1212 L1_MAINNET_URL = "https://eth-mainnet.g.alchemy.com/v2/{api_key}"
@@ -19,7 +19,6 @@ class L1Client:
1919 )
2020 # Taken from ethereum_base_layer_contracts.rs
2121 STARKNET_L1_CONTRACT_ADDRESS = "0xc662c410C0ECf747543f5bA90660f6ABeBD9C8c4"
22- RETRIES_COUNT = 2
2322
2423 @dataclass (frozen = True )
2524 class Log :
@@ -38,52 +37,83 @@ class Log:
3837 removed : bool
3938 block_timestamp : int
4039
41- @staticmethod
42- def get_logs (from_block : int , to_block : int , api_key : str ) -> List ["L1Client.Log" ]:
40+ def __init__ (
41+ self ,
42+ api_key : str ,
43+ timeout : int = 10 ,
44+ retries_count : int = 2 ,
45+ ):
46+ self .api_key = api_key
47+ self .logger = logging .Logger ("L1Client" )
48+ self .timeout = timeout
49+ self .retries_count = retries_count
50+ self .rpc_url = self .L1_MAINNET_URL .format (api_key = api_key )
51+ self .data_api_url = self .DATA_BLOCKS_BY_TIMESTAMP_URL_FMT .format (api_key = api_key )
52+
53+ def _run_request_with_retry (
54+ self ,
55+ request_func : Callable ,
56+ additional_log_context : Dict [str , Any ],
57+ ) -> Optional [Dict ]:
58+ caller_name = inspect .currentframe ().f_back .f_code .co_name
59+
60+ for attempt in range (self .retries_count ):
61+ try :
62+ response = request_func (timeout = self .timeout )
63+ response .raise_for_status ()
64+ result = response .json ()
65+ self .logger .debug (
66+ f"{ caller_name } succeeded on attempt { attempt + 1 } " ,
67+ extra = additional_log_context ,
68+ )
69+ return result
70+ except (requests .RequestException , ValueError ):
71+ self .logger .debug (
72+ f"{ caller_name } attempt { attempt + 1 } /{ self .retries_count } failed" ,
73+ extra = additional_log_context ,
74+ exc_info = True ,
75+ )
76+
77+ self .logger .error (
78+ f"{ caller_name } failed after { self .retries_count } attempts, returning None" ,
79+ extra = additional_log_context ,
80+ )
81+
82+ return None
83+
84+ def get_logs (self , from_block : int , to_block : int ) -> List ["L1Client.Log" ]:
4385 """
4486 Get logs from Ethereum using eth_getLogs RPC method.
45- Tries up to RETRIES_COUNT times. On failure, logs an error and returns [].
87+ Tries up to retries_count times. On failure, logs an error and returns [].
4688 """
4789 if from_block > to_block :
4890 raise ValueError ("from_block must be less than or equal to to_block" )
4991
50- rpc_url = L1Client .L1_MAINNET_URL .format (api_key = api_key )
51-
5292 payload = {
5393 "jsonrpc" : "2.0" ,
5494 "method" : "eth_getLogs" ,
5595 "params" : [
5696 {
5797 "fromBlock" : hex (from_block ),
5898 "toBlock" : hex (to_block ),
59- "address" : L1Client .STARKNET_L1_CONTRACT_ADDRESS ,
60- "topics" : [L1Client .LOG_MESSAGE_TO_L2_EVENT_SIGNATURE ],
99+ "address" : self .STARKNET_L1_CONTRACT_ADDRESS ,
100+ "topics" : [self .LOG_MESSAGE_TO_L2_EVENT_SIGNATURE ],
61101 }
62102 ],
63103 "id" : 1 ,
64104 }
65105
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- )
106+ request_func = functools .partial (requests .post , self .rpc_url , json = payload )
107+ data = self ._run_request_with_retry (
108+ request_func = request_func ,
109+ additional_log_context = {
110+ "url" : self .rpc_url ,
111+ "from_block" : from_block ,
112+ "to_block" : to_block ,
113+ },
114+ )
115+
116+ if data is None :
87117 return []
88118
89119 results = data .get ("result" , [])
@@ -104,43 +134,25 @@ def get_logs(from_block: int, to_block: int, api_key: str) -> List["L1Client.Log
104134 for result in results
105135 ]
106136
107- @staticmethod
108- def get_timestamp_of_block (block_number : int , api_key : str ) -> Optional [int ]:
137+ def get_timestamp_of_block (self , block_number : int ) -> Optional [int ]:
109138 """
110139 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.
140+ Tries up to retries_count times. On failure, logs an error and returns None.
112141 """
113- rpc_url = L1Client .L1_MAINNET_URL .format (api_key = api_key )
114-
115142 payload = {
116143 "jsonrpc" : "2.0" ,
117144 "method" : "eth_getBlockByNumber" ,
118145 "params" : [hex (block_number ), False ],
119146 "id" : 1 ,
120147 }
121148
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- )
149+ request_func = functools .partial (requests .post , self .rpc_url , json = payload )
150+ result = self ._run_request_with_retry (
151+ request_func = request_func ,
152+ additional_log_context = {"url" : self .rpc_url , "block_number" : block_number },
153+ )
138154
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- )
155+ if result is None :
144156 return None
145157
146158 block = result .get ("result" )
@@ -151,14 +163,11 @@ def get_timestamp_of_block(block_number: int, api_key: str) -> Optional[int]:
151163 # Timestamp is hex string, convert to int.
152164 return int (block ["timestamp" ], 16 )
153165
154- @staticmethod
155- def get_block_number_by_timestamp (timestamp : int , api_key : str ) -> Optional [int ]:
166+ def get_block_number_by_timestamp (self , timestamp : int ) -> Optional [int ]:
156167 """
157168 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.
169+ Tries up to retries_count times. On failure, logs an error and returns None.
159170 """
160- rpc_url = L1Client .DATA_BLOCKS_BY_TIMESTAMP_URL_FMT .format (api_key = api_key )
161-
162171 timestamp_iso = (
163172 datetime .fromtimestamp (timestamp , tz = timezone .utc ).isoformat ().replace ("+00:00" , "Z" )
164173 )
@@ -169,27 +178,13 @@ def get_block_number_by_timestamp(timestamp: int, api_key: str) -> Optional[int]
169178 "direction" : "AFTER" ,
170179 }
171180
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- )
181+ request_func = functools .partial (requests .get , self .data_api_url , params = params )
182+ data = self ._run_request_with_retry (
183+ request_func = request_func ,
184+ additional_log_context = {"url" : self .data_api_url , "timestamp" : timestamp },
185+ )
186+
187+ if data is None :
193188 return None
194189
195190 items = data .get ("data" , [])
0 commit comments