Skip to content

Commit d9ffd19

Browse files
echonet: add get_timestamp_of_block_by_number func (#10263)
1 parent 1ae9cb9 commit d9ffd19

File tree

2 files changed

+82
-1
lines changed

2 files changed

+82
-1
lines changed

echonet/l1_client.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from dataclasses import dataclass
2-
from typing import List
2+
from typing import List, Optional
33

44
import logging
55
import requests
@@ -96,3 +96,50 @@ def get_logs(from_block: int, to_block: int, api_key: str) -> List["L1Client.Log
9696
)
9797
for result in results
9898
]
99+
100+
@staticmethod
101+
def get_timestamp_of_block(block_number: int, api_key: str) -> Optional[int]:
102+
"""
103+
Get block timestamp by block number using eth_getBlockByNumber RPC method.
104+
Tries up to RETRIES_COUNT times. On failure, logs an error and returns None.
105+
"""
106+
rpc_url = L1Client.L1_MAINNET_URL.format(api_key=api_key)
107+
108+
payload = {
109+
"jsonrpc": "2.0",
110+
"method": "eth_getBlockByNumber",
111+
"params": [hex(block_number), False],
112+
"id": 1,
113+
}
114+
115+
for attempt in range(L1Client.RETRIES_COUNT):
116+
try:
117+
response = requests.post(rpc_url, json=payload, timeout=10)
118+
response.raise_for_status()
119+
result = response.json()
120+
logger.debug(
121+
f"get_timestamp_of_block succeeded on attempt {attempt + 1}",
122+
extra={"url": rpc_url, "block_number": block_number},
123+
)
124+
break # success -> exit loop
125+
except (requests.RequestException, ValueError) as exc:
126+
logger.debug(
127+
f"get_timestamp_of_block attempt {attempt + 1}/{L1Client.RETRIES_COUNT} failed",
128+
extra={"url": rpc_url, "block_number": block_number},
129+
exc_info=True,
130+
)
131+
132+
else:
133+
logger.error(
134+
f"get_timestamp_of_block failed after {L1Client.RETRIES_COUNT} attempts, returning None",
135+
extra={"url": rpc_url, "block_number": block_number},
136+
)
137+
return None
138+
139+
block = result.get("result")
140+
if block is None:
141+
# Block not found
142+
return None
143+
144+
# Timestamp is hex string, convert to int.
145+
return int(block["timestamp"], 16)

echonet/tests/test_l1_client.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,40 @@ def test_get_logs_when_rpc_result_is_empty(self, mock_post):
170170
self.assertEqual(mock_post.call_count, 1)
171171
self.assertEqual(logs, [])
172172

173+
@patch("l1_client.requests.post")
174+
def test_get_timestamp_of_block_retries_after_failure_and_succeeds(self, mock_post):
175+
request_exception = requests.RequestException("some error")
176+
177+
successful_response = Mock()
178+
successful_response.raise_for_status.return_value = None
179+
successful_response.json.return_value = {"result": {"timestamp": "0x20"}} # 32
180+
181+
mock_post.side_effect = [request_exception, successful_response]
182+
183+
result = L1Client.get_timestamp_of_block(
184+
block_number=123,
185+
api_key="api_key",
186+
)
187+
188+
self.assertEqual(mock_post.call_count, 2)
189+
self.assertEqual(result, 32)
190+
191+
@patch("l1_client.requests.post")
192+
def test_get_timestamp_of_block_returns_none_when_rpc_result_is_empty(self, mock_post):
193+
response_ok = Mock()
194+
response_ok.raise_for_status.return_value = None
195+
response_ok.json.return_value = {"result": None}
196+
197+
mock_post.return_value = response_ok
198+
199+
result = L1Client.get_timestamp_of_block(
200+
block_number=123,
201+
api_key="api_key",
202+
)
203+
204+
self.assertEqual(mock_post.call_count, 1)
205+
self.assertIsNone(result)
206+
173207

174208
if __name__ == "__main__":
175209
unittest.main()

0 commit comments

Comments
 (0)