11from dataclasses import dataclass
2+ from datetime import datetime , timezone
23from typing import List , Optional
34
45import logging
910
1011class L1Client :
1112 L1_MAINNET_URL = "https://eth-mainnet.g.alchemy.com/v2/{api_key}"
13+ DATA_BLOCKS_BY_TIMESTAMP_URL_FMT = (
14+ "https://api.g.alchemy.com/data/v1/{api_key}/utility/blocks/by-timestamp"
15+ )
16+ # Taken from apollo_l1_provider/src/lib.rs
17+ LOG_MESSAGE_TO_L2_EVENT_SIGNATURE = (
18+ "0xdb80dd488acf86d17c747445b0eabb5d57c541d3bd7b6b87af987858e5066b2b"
19+ )
20+ # Taken from ethereum_base_layer_contracts.rs
21+ STARKNET_L1_CONTRACT_ADDRESS = "0xc662c410C0ECf747543f5bA90660f6ABeBD9C8c4"
1222 RETRIES_COUNT = 2
1323
1424 @dataclass (frozen = True )
@@ -46,11 +56,8 @@ def get_logs(from_block: int, to_block: int, api_key: str) -> List["L1Client.Log
4656 {
4757 "fromBlock" : hex (from_block ),
4858 "toBlock" : hex (to_block ),
49- "address" : "0xc662c410C0ECf747543f5bA90660f6ABeBD9C8c4" , # Starknet L1 contract
50- "topics" : [
51- "0xdb80dd488acf86d17c747445b0eabb5d57c541d3bd7b6b87af987858e5066b2b"
52- # LogMessageToL2 event signature
53- ],
59+ "address" : L1Client .STARKNET_L1_CONTRACT_ADDRESS ,
60+ "topics" : [L1Client .LOG_MESSAGE_TO_L2_EVENT_SIGNATURE ],
5461 }
5562 ],
5663 "id" : 1 ,
@@ -74,7 +81,7 @@ def get_logs(from_block: int, to_block: int, api_key: str) -> List["L1Client.Log
7481 )
7582 else :
7683 logger .error (
77- f"get_logs failed after { L1Client .RETRIES_COUNT } attempts" ,
84+ f"get_logs failed after { L1Client .RETRIES_COUNT } attempts, returning [] " ,
7885 extra = {"url" : rpc_url , "from_block" : from_block , "to_block" : to_block },
7986 )
8087 return []
@@ -143,3 +150,51 @@ def get_timestamp_of_block(block_number: int, api_key: str) -> Optional[int]:
143150
144151 # Timestamp is hex string, convert to int.
145152 return int (block ["timestamp" ], 16 )
153+
154+ @staticmethod
155+ def get_block_number_by_timestamp (timestamp : int , api_key : str ) -> Optional [int ]:
156+ """
157+ 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.
159+ """
160+ rpc_url = L1Client .DATA_BLOCKS_BY_TIMESTAMP_URL_FMT .format (api_key = api_key )
161+
162+ timestamp_iso = (
163+ datetime .fromtimestamp (timestamp , tz = timezone .utc ).isoformat ().replace ("+00:00" , "Z" )
164+ )
165+
166+ params = {
167+ "networks" : "eth-mainnet" ,
168+ "timestamp" : timestamp_iso ,
169+ "direction" : "AFTER" ,
170+ }
171+
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+ )
193+ return None
194+
195+ items = data .get ("data" , [])
196+ if not items :
197+ return None
198+
199+ block = items [0 ].get ("block" , {})
200+ return block .get ("number" )
0 commit comments