csm-dashboard 0.3.5__py3-none-any.whl → 0.3.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {csm_dashboard-0.3.5.dist-info → csm_dashboard-0.3.6.1.dist-info}/METADATA +1 -1
- {csm_dashboard-0.3.5.dist-info → csm_dashboard-0.3.6.1.dist-info}/RECORD +7 -7
- src/data/beacon.py +60 -22
- src/services/operator_service.py +11 -9
- src/web/app.py +1 -1
- {csm_dashboard-0.3.5.dist-info → csm_dashboard-0.3.6.1.dist-info}/WHEEL +0 -0
- {csm_dashboard-0.3.5.dist-info → csm_dashboard-0.3.6.1.dist-info}/entry_points.txt +0 -0
|
@@ -13,7 +13,7 @@ src/core/config.py,sha256=KYYlJv383RtHo_CpaCt2DM7yaTr3C267cY4T8_ZRiEc,1613
|
|
|
13
13
|
src/core/contracts.py,sha256=u1KW0z-9V21Zpf7qxGBvRy2Ffdo4YfGusUdT4K_ggP4,582
|
|
14
14
|
src/core/types.py,sha256=pirEDIR6csbGWWUH2s3RDCG2ce-oqgEvdsamAI-rurM,7807
|
|
15
15
|
src/data/__init__.py,sha256=DItA8aEp8Lbr0uFlJVppMaTtVEEznoA1hkRpH-vfHhk,57
|
|
16
|
-
src/data/beacon.py,sha256=
|
|
16
|
+
src/data/beacon.py,sha256=jICauXdee4efYj8mj1GIoGsY5_uWmgHB57XhqkXvwS4,15439
|
|
17
17
|
src/data/cache.py,sha256=w1iv4rM-FVgFlaSNYdQA3CfqyhZo9-gqbZwKmzKY0Ps,2134
|
|
18
18
|
src/data/etherscan.py,sha256=JcO6H2dFFZFOV-qCaNhoJvyza2ymd2GhgmJo7vxvTPM,10938
|
|
19
19
|
src/data/ipfs_logs.py,sha256=gXUTP9dmZ_e7gW6ouotJ1r_72ixq2q_32y2evYQf0DM,10709
|
|
@@ -23,11 +23,11 @@ src/data/onchain.py,sha256=2eLGAI0URz1JuNZi2JmLWN-h_UDfkDlbT30rXpoqbts,25252
|
|
|
23
23
|
src/data/rewards_tree.py,sha256=a-MO14b4COjOvy59FPd0jaf1dkgidlqCQKAFDinwRJU,1894
|
|
24
24
|
src/data/strikes.py,sha256=b68exKntZpzjiEBftiI3AcvEclHp7vg5lsOiG2a7Kzo,9046
|
|
25
25
|
src/services/__init__.py,sha256=MC7blFLAMazErCWuyYXvS6sO3uZm1z_RUOtnlIK0kSo,38
|
|
26
|
-
src/services/operator_service.py,sha256=
|
|
26
|
+
src/services/operator_service.py,sha256=KkuWihW8sRuDf7rvSnFLeqWysg4YoMlHI1ZVSOBBHO0,30234
|
|
27
27
|
src/web/__init__.py,sha256=iI2c5xxXmzsNxIetm0P2qE3uVsT-ClsMfzn620r5YTU,40
|
|
28
|
-
src/web/app.py,sha256=
|
|
28
|
+
src/web/app.py,sha256=rXXoqNZVBAtLu5QW3QebeQdS7zj0DmY2m_hub-Rvkh4,47946
|
|
29
29
|
src/web/routes.py,sha256=UydCD7DMWBYv_lbQQjeHI82l8gLZkZ2lEy4OayPxNiU,10716
|
|
30
|
-
csm_dashboard-0.3.
|
|
31
|
-
csm_dashboard-0.3.
|
|
32
|
-
csm_dashboard-0.3.
|
|
33
|
-
csm_dashboard-0.3.
|
|
30
|
+
csm_dashboard-0.3.6.1.dist-info/METADATA,sha256=ieTgZaq6G73b9xHLpEYmpmp9DQlN7hUDk7dy_hugzug,12554
|
|
31
|
+
csm_dashboard-0.3.6.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
32
|
+
csm_dashboard-0.3.6.1.dist-info/entry_points.txt,sha256=P1Ul8ALIPBwDlVlXqTPuzJ64xxRpIJsYW8U73Tyjgtg,37
|
|
33
|
+
csm_dashboard-0.3.6.1.dist-info/RECORD,,
|
src/data/beacon.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Beacon chain data fetching via beaconcha.in API."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
from datetime import datetime, timedelta, timezone
|
|
4
5
|
from decimal import Decimal
|
|
5
6
|
from enum import Enum
|
|
@@ -172,47 +173,84 @@ class BeaconDataProvider:
|
|
|
172
173
|
Fetch validator info for multiple pubkeys.
|
|
173
174
|
|
|
174
175
|
beaconcha.in supports comma-separated pubkeys (up to 100).
|
|
176
|
+
Includes retry logic for rate limiting and proper error handling.
|
|
175
177
|
"""
|
|
176
178
|
if not pubkeys:
|
|
177
179
|
return []
|
|
178
180
|
|
|
179
181
|
validators = []
|
|
180
182
|
batch_size = 100 # beaconcha.in limit
|
|
183
|
+
max_retries = 3
|
|
181
184
|
|
|
182
185
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
183
186
|
for i in range(0, len(pubkeys), batch_size):
|
|
184
187
|
batch = pubkeys[i : i + batch_size]
|
|
185
188
|
pubkeys_param = ",".join(batch)
|
|
186
189
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
headers=self._get_headers(),
|
|
191
|
-
)
|
|
190
|
+
# Add delay between batches to avoid rate limiting
|
|
191
|
+
if i > 0:
|
|
192
|
+
await asyncio.sleep(0.5)
|
|
192
193
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
194
|
+
for attempt in range(max_retries):
|
|
195
|
+
try:
|
|
196
|
+
response = await client.get(
|
|
197
|
+
f"{self.base_url}/validator/{pubkeys_param}",
|
|
198
|
+
headers=self._get_headers(),
|
|
199
|
+
)
|
|
198
200
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
if response.status_code == 200:
|
|
202
|
+
data = response.json().get("data", [])
|
|
203
|
+
# API returns single object if only one validator
|
|
204
|
+
if isinstance(data, dict):
|
|
205
|
+
data = [data]
|
|
206
|
+
|
|
207
|
+
for v in data:
|
|
208
|
+
validators.append(self._parse_validator(v))
|
|
209
|
+
break # Success, exit retry loop
|
|
210
|
+
elif response.status_code == 404:
|
|
211
|
+
# Validators not found - create placeholder entries
|
|
212
|
+
for pubkey in batch:
|
|
213
|
+
validators.append(
|
|
214
|
+
ValidatorInfo(
|
|
215
|
+
pubkey=pubkey,
|
|
216
|
+
status=ValidatorStatus.PENDING_INITIALIZED,
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
break # Success, exit retry loop
|
|
220
|
+
elif response.status_code == 429:
|
|
221
|
+
# Rate limited - wait and retry
|
|
222
|
+
if attempt < max_retries - 1:
|
|
223
|
+
await asyncio.sleep(2**attempt) # 1s, 2s, 4s
|
|
224
|
+
continue
|
|
225
|
+
# Max retries reached, add as unknown
|
|
226
|
+
for pubkey in batch:
|
|
227
|
+
validators.append(
|
|
228
|
+
ValidatorInfo(
|
|
229
|
+
pubkey=pubkey, status=ValidatorStatus.UNKNOWN
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
break
|
|
233
|
+
else:
|
|
234
|
+
# Other error status - add as unknown
|
|
235
|
+
for pubkey in batch:
|
|
236
|
+
validators.append(
|
|
237
|
+
ValidatorInfo(
|
|
238
|
+
pubkey=pubkey, status=ValidatorStatus.UNKNOWN
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
break
|
|
242
|
+
except Exception:
|
|
243
|
+
if attempt < max_retries - 1:
|
|
244
|
+
await asyncio.sleep(1)
|
|
245
|
+
continue
|
|
246
|
+
# On final failure, add unknown status for this batch
|
|
203
247
|
for pubkey in batch:
|
|
204
248
|
validators.append(
|
|
205
249
|
ValidatorInfo(
|
|
206
|
-
pubkey=pubkey,
|
|
207
|
-
status=ValidatorStatus.PENDING_INITIALIZED,
|
|
250
|
+
pubkey=pubkey, status=ValidatorStatus.UNKNOWN
|
|
208
251
|
)
|
|
209
252
|
)
|
|
210
|
-
|
|
211
|
-
# On error, add unknown status for this batch
|
|
212
|
-
for pubkey in batch:
|
|
213
|
-
validators.append(
|
|
214
|
-
ValidatorInfo(pubkey=pubkey, status=ValidatorStatus.UNKNOWN)
|
|
215
|
-
)
|
|
253
|
+
break
|
|
216
254
|
|
|
217
255
|
return validators
|
|
218
256
|
|
src/services/operator_service.py
CHANGED
|
@@ -279,22 +279,16 @@ class OperatorService:
|
|
|
279
279
|
steth_data = await self.lido_api.get_steth_apr()
|
|
280
280
|
bond_apy = steth_data.get("apr")
|
|
281
281
|
|
|
282
|
-
# 3. Net APY calculations
|
|
282
|
+
# 3. Net APY calculations (initialized here, calculated after historical APR section)
|
|
283
283
|
net_apy_28d = None
|
|
284
284
|
net_apy_ltd = None
|
|
285
285
|
|
|
286
|
-
# Current frame net APY (historical_reward_apy_28d is basically current frame APY)
|
|
287
|
-
if historical_reward_apy_28d is not None and bond_apy is not None:
|
|
288
|
-
net_apy_28d = round(historical_reward_apy_28d + bond_apy, 2)
|
|
289
|
-
elif bond_apy is not None:
|
|
290
|
-
net_apy_28d = round(bond_apy, 2)
|
|
291
|
-
|
|
292
286
|
# Lifetime net APY - intentionally NOT calculated
|
|
293
287
|
# (same reason as historical_reward_apy_ltd - can't accurately calculate without historical bond)
|
|
294
288
|
# net_apy_ltd remains None
|
|
295
289
|
|
|
296
|
-
#
|
|
297
|
-
#
|
|
290
|
+
# Current frame net APY and Previous frame net APY are calculated in section 4b/4c
|
|
291
|
+
# after we have historical APR values (current_bond_apr, previous_bond_apy)
|
|
298
292
|
|
|
299
293
|
# 4. Calculate bond stETH earnings (from stETH rebasing)
|
|
300
294
|
# Formula: bond_eth * (apr / 100) * (duration_days / 365)
|
|
@@ -431,6 +425,14 @@ class OperatorService:
|
|
|
431
425
|
if prev_bond_apy_to_use is not None:
|
|
432
426
|
previous_net_apy = round(previous_distribution_apy + prev_bond_apy_to_use, 2)
|
|
433
427
|
|
|
428
|
+
# 4c. Current frame net APY (using historical APR when available, like previous frame)
|
|
429
|
+
if historical_reward_apy_28d is not None:
|
|
430
|
+
curr_bond_apy_to_use = current_bond_apr if current_bond_apr is not None else bond_apy
|
|
431
|
+
if curr_bond_apy_to_use is not None:
|
|
432
|
+
net_apy_28d = round(historical_reward_apy_28d + curr_bond_apy_to_use, 2)
|
|
433
|
+
elif bond_apy is not None:
|
|
434
|
+
net_apy_28d = round(bond_apy, 2)
|
|
435
|
+
|
|
434
436
|
# 5. Calculate net totals (Rewards + Bond)
|
|
435
437
|
if previous_distribution_eth is not None or previous_bond_eth is not None:
|
|
436
438
|
previous_net_total_eth = round(
|
src/web/app.py
CHANGED
|
File without changes
|
|
File without changes
|