22
33import logging
44import os
5+ from asyncio import sleep
56from datetime import datetime
67
78import httpx
@@ -21,24 +22,36 @@ def __init__(self):
2122 )
2223 self .contract_address = os .getenv ("ETHEREUM_BRIDGE_CONTRACT" , "" )
2324 self .confirmations = int (os .getenv ("ETHEREUM_CONFIRMATIONS" , "12" ))
25+ self .max_retries = int (os .getenv ("ETHEREUM_RPC_RETRIES" , "3" ))
26+ self ._last_block_hash : str | None = None
2427
2528 async def _rpc_call (self , method : str , params : list = None ) -> dict :
2629 """Make a JSON-RPC call to the Ethereum node."""
27- async with httpx .AsyncClient () as client :
28- response = await client .post (
29- self .rpc_url ,
30- json = {
31- "jsonrpc" : "2.0" ,
32- "id" : 1 ,
33- "method" : method ,
34- "params" : params or [],
35- },
36- timeout = 30 ,
37- )
38- data = response .json ()
39- if "error" in data and data ["error" ]:
40- raise Exception (f"Ethereum RPC error: { data ['error' ]} " )
41- return data .get ("result" )
30+ last_error : Exception | None = None
31+ for attempt in range (1 , self .max_retries + 1 ):
32+ try :
33+ async with httpx .AsyncClient () as client :
34+ response = await client .post (
35+ self .rpc_url ,
36+ json = {
37+ "jsonrpc" : "2.0" ,
38+ "id" : 1 ,
39+ "method" : method ,
40+ "params" : params or [],
41+ },
42+ timeout = 30 ,
43+ )
44+ response .raise_for_status ()
45+ data = response .json ()
46+ if "error" in data and data ["error" ]:
47+ raise RuntimeError (f"Ethereum RPC error: { data ['error' ]} " )
48+ return data .get ("result" )
49+ except Exception as exc :
50+ last_error = exc
51+ if attempt == self .max_retries :
52+ break
53+ await sleep (attempt )
54+ raise RuntimeError ("Ethereum RPC call failed" ) from last_error
4255
4356 async def get_latest_block (self ) -> int :
4457 try :
@@ -49,12 +62,11 @@ async def get_latest_block(self) -> int:
4962 logger .error ("[ethereum] Failed to get block number: %s" , e )
5063 raise
5164
52- async def fetch_events (
53- self , from_block : int , to_block : int
54- ) -> list [IndexedEvent ]:
65+ async def fetch_events (self , from_block : int , to_block : int ) -> list [IndexedEvent ]:
5566 if not self .contract_address :
5667 return []
5768
69+ await self .detect_reorg (from_block )
5870 events = []
5971 try :
6072 logs = await self ._rpc_call (
@@ -71,10 +83,19 @@ async def fetch_events(
7183 for log in logs or []:
7284 try :
7385 block_num = int (log ["blockNumber" ], 16 )
86+ block_data = await self ._rpc_call (
87+ "eth_getBlockByNumber" ,
88+ [log ["blockNumber" ], False ],
89+ )
90+ self ._last_block_hash = (
91+ block_data .get ("hash" ) if block_data else None
92+ )
7493 events .append (
7594 IndexedEvent (
7695 chain = "ethereum" ,
77- event_type = log ["topics" ][0 ] if log .get ("topics" ) else "unknown" ,
96+ event_type = (
97+ log ["topics" ][0 ] if log .get ("topics" ) else "unknown"
98+ ),
7899 tx_hash = log .get ("transactionHash" , "" ),
79100 block_number = block_num ,
80101 contract_address = log .get ("address" ),
@@ -101,3 +122,23 @@ async def handle_reorg(self, reorg_block: int) -> None:
101122 reorg_block ,
102123 )
103124 # In production: delete indexed events >= reorg_block from DB
125+
126+ async def detect_reorg (self , from_block : int ) -> None :
127+ if from_block <= 0 or not self ._last_block_hash :
128+ return
129+ previous_block = await self ._rpc_call (
130+ "eth_getBlockByNumber" , [hex (from_block - 1 ), False ]
131+ )
132+ if previous_block and previous_block .get ("hash" ) != self ._last_block_hash :
133+ await self .handle_reorg (from_block - 1 )
134+
135+ async def get_mempool_transactions (self ) -> list [str ]:
136+ txpool = await self ._rpc_call ("txpool_content" )
137+ if not isinstance (txpool , dict ):
138+ return []
139+ pending = txpool .get ("pending" , {})
140+ return list (pending .keys ())
141+
142+ async def broadcast_transaction (self , raw_tx : str ) -> str :
143+ tx_hash = await self ._rpc_call ("eth_sendRawTransaction" , [raw_tx ])
144+ return str (tx_hash )
0 commit comments