Skip to content

Commit 2b23390

Browse files
echonet: refactor l1 client, add init, share retry func (#10416)
1 parent 1f61e93 commit 2b23390

File tree

2 files changed

+95
-100
lines changed

2 files changed

+95
-100
lines changed

echonet/l1_client.py

Lines changed: 79 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from dataclasses import dataclass
22
from 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
57
import logging
68
import requests
79

8-
logger = logging.getLogger(__name__)
9-
1010

1111
class 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", [])

echonet/tests/test_l1_client.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -61,24 +61,24 @@ def test_get_logs_retries_after_exception_and_succeeds_on_second_attempt(self, m
6161

6262
mock_post.side_effect = [request_exception, successful_response]
6363

64-
logs = L1Client.get_logs(
64+
client = L1Client(api_key="api_key")
65+
logs = client.get_logs(
6566
from_block=self.BLOCK_NUMBER_SAMPLE,
6667
to_block=self.BLOCK_NUMBER_SAMPLE,
67-
api_key="api_key",
6868
)
6969

7070
self.assertEqual(mock_post.call_count, 2)
7171
self.assertEqual(logs, [self.EXPECTED_LOG_SAMPLE])
7272

7373
def test_get_logs_raises_on_invalid_block_range(self):
74+
client = L1Client(api_key="api_key")
7475
with self.assertRaisesRegex(
7576
ValueError,
7677
"from_block must be less than or equal to to_block",
7778
):
78-
L1Client.get_logs(
79+
client.get_logs(
7980
from_block=11,
8081
to_block=10,
81-
api_key="api_key",
8282
)
8383

8484
@patch("l1_client.requests.post")
@@ -116,10 +116,10 @@ def test_get_logs_parses_several_results(self, mock_post):
116116

117117
mock_post.return_value = response_ok
118118

119-
logs = L1Client.get_logs(
119+
client = L1Client(api_key="api_key")
120+
logs = client.get_logs(
120121
from_block=1,
121122
to_block=2,
122-
api_key="api_key",
123123
)
124124

125125
self.assertEqual(mock_post.call_count, 1)
@@ -161,10 +161,10 @@ def test_get_logs_when_rpc_result_is_empty(self, mock_post):
161161

162162
mock_post.return_value = response_ok
163163

164-
logs = L1Client.get_logs(
164+
client = L1Client(api_key="api_key")
165+
logs = client.get_logs(
165166
from_block=1,
166167
to_block=1,
167-
api_key="api_key",
168168
)
169169

170170
self.assertEqual(mock_post.call_count, 1)
@@ -180,9 +180,9 @@ def test_get_timestamp_of_block_retries_after_failure_and_succeeds(self, mock_po
180180

181181
mock_post.side_effect = [request_exception, successful_response]
182182

183-
result = L1Client.get_timestamp_of_block(
183+
client = L1Client(api_key="api_key")
184+
result = client.get_timestamp_of_block(
184185
block_number=123,
185-
api_key="api_key",
186186
)
187187

188188
self.assertEqual(mock_post.call_count, 2)
@@ -196,9 +196,9 @@ def test_get_timestamp_of_block_returns_none_when_rpc_result_is_empty(self, mock
196196

197197
mock_post.return_value = response_ok
198198

199-
result = L1Client.get_timestamp_of_block(
199+
client = L1Client(api_key="api_key")
200+
result = client.get_timestamp_of_block(
200201
block_number=123,
201-
api_key="api_key",
202202
)
203203

204204
self.assertEqual(mock_post.call_count, 1)
@@ -216,9 +216,9 @@ def test_get_block_number_by_timestamp_retries_after_failure_and_succeeds(self,
216216

217217
mock_get.side_effect = [request_exception, successful_response]
218218

219-
result = L1Client.get_block_number_by_timestamp(
219+
client = L1Client(api_key="api_key")
220+
result = client.get_block_number_by_timestamp(
220221
timestamp=1_600_000_000,
221-
api_key="api_key",
222222
)
223223

224224
self.assertEqual(mock_get.call_count, 2)
@@ -232,9 +232,9 @@ def test_get_block_number_by_timestamp_returns_none_when_rpc_result_is_empty(sel
232232

233233
mock_get.return_value = response_ok
234234

235-
result = L1Client.get_block_number_by_timestamp(
235+
client = L1Client(api_key="api_key")
236+
result = client.get_block_number_by_timestamp(
236237
timestamp=1_600_000_000,
237-
api_key="api_key",
238238
)
239239

240240
self.assertEqual(mock_get.call_count, 1)

0 commit comments

Comments
 (0)