csm-dashboard 0.3.5__tar.gz → 0.3.6.1__tar.gz

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.
Files changed (52) hide show
  1. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/CHANGELOG.md +5 -0
  2. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/PKG-INFO +1 -1
  3. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/pyproject.toml +1 -1
  4. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/data/beacon.py +60 -22
  5. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/services/operator_service.py +11 -9
  6. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/web/app.py +1 -1
  7. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/.dockerignore +0 -0
  8. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/.env.example +0 -0
  9. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/.github/workflows/docker-publish.yaml +0 -0
  10. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/.github/workflows/release.yaml +0 -0
  11. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/.gitignore +0 -0
  12. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/Dockerfile +0 -0
  13. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/README.md +0 -0
  14. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/docker-compose.yml +0 -0
  15. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/img/csm-dash-cli.png +0 -0
  16. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/img/csm-dash-web.png +0 -0
  17. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/img/favicon.ico +0 -0
  18. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/img/logo.png +0 -0
  19. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/my-lido-csm-dashboard.xml +0 -0
  20. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/requirements.txt +0 -0
  21. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/__init__.py +0 -0
  22. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/abis/CSAccounting.json +0 -0
  23. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/abis/CSFeeDistributor.json +0 -0
  24. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/abis/CSModule.json +0 -0
  25. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/abis/WithdrawalQueueERC721.json +0 -0
  26. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/abis/__init__.py +0 -0
  27. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/abis/stETH.json +0 -0
  28. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/cli/__init__.py +0 -0
  29. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/cli/commands.py +0 -0
  30. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/core/__init__.py +0 -0
  31. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/core/config.py +0 -0
  32. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/core/contracts.py +0 -0
  33. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/core/types.py +0 -0
  34. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/data/__init__.py +0 -0
  35. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/data/cache.py +0 -0
  36. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/data/etherscan.py +0 -0
  37. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/data/ipfs_logs.py +0 -0
  38. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/data/known_cids.py +0 -0
  39. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/data/lido_api.py +0 -0
  40. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/data/onchain.py +0 -0
  41. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/data/rewards_tree.py +0 -0
  42. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/data/strikes.py +0 -0
  43. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/main.py +0 -0
  44. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/services/__init__.py +0 -0
  45. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/web/__init__.py +0 -0
  46. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/src/web/routes.py +0 -0
  47. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/tests/__init__.py +0 -0
  48. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/tests/conftest.py +0 -0
  49. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/tests/unit/test_cache.py +0 -0
  50. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/tests/unit/test_config.py +0 -0
  51. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/tests/unit/test_strikes.py +0 -0
  52. {csm_dashboard-0.3.5 → csm_dashboard-0.3.6.1}/tests/unit/test_types.py +0 -0
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.6] - 2026-01-22
4
+
5
+ ### Added
6
+ - Retry logic and rate limiting for validator batch fetching
7
+
3
8
  ## [0.3.5] - 2026-01-05
4
9
 
5
10
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csm-dashboard
3
- Version: 0.3.5
3
+ Version: 0.3.6.1
4
4
  Summary: Lido CSM Operator Dashboard for tracking validator earnings
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: fastapi>=0.104
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "csm-dashboard"
3
- version = "0.3.5"
3
+ version = "0.3.6.1"
4
4
  description = "Lido CSM Operator Dashboard for tracking validator earnings"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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
- try:
188
- response = await client.get(
189
- f"{self.base_url}/validator/{pubkeys_param}",
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
- if response.status_code == 200:
194
- data = response.json().get("data", [])
195
- # API returns single object if only one validator
196
- if isinstance(data, dict):
197
- data = [data]
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
- for v in data:
200
- validators.append(self._parse_validator(v))
201
- elif response.status_code == 404:
202
- # Validators not found - create placeholder entries
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
- except Exception:
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
 
@@ -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
- # Previous frame net APY calculation is moved after we know previous_bond_apy
297
- # (calculated in section 4 below)
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(
@@ -14,7 +14,7 @@ def create_app() -> FastAPI:
14
14
  app = FastAPI(
15
15
  title="CSM Operator Dashboard",
16
16
  description="Track your Lido CSM validator earnings",
17
- version="0.3.5",
17
+ version="0.3.6.1",
18
18
  )
19
19
 
20
20
  app.include_router(router, prefix="/api")
File without changes