csm-dashboard 0.2.2__py3-none-any.whl → 0.3.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.
src/web/routes.py CHANGED
@@ -11,6 +11,8 @@ router = APIRouter()
11
11
  async def get_operator(
12
12
  identifier: str,
13
13
  detailed: bool = Query(False, description="Include validator status from beacon chain"),
14
+ history: bool = Query(False, description="Include all historical distribution frames"),
15
+ withdrawals: bool = Query(False, description="Include withdrawal/claim history"),
14
16
  ):
15
17
  """
16
18
  Get operator data by address or ID.
@@ -18,6 +20,8 @@ async def get_operator(
18
20
  - If identifier is numeric, treat as operator ID
19
21
  - If identifier starts with 0x, treat as Ethereum address
20
22
  - Add ?detailed=true to include validator status breakdown
23
+ - Add ?history=true to include all historical distribution frames
24
+ - Add ?withdrawals=true to include withdrawal/claim history
21
25
  """
22
26
  service = OperatorService()
23
27
 
@@ -26,9 +30,9 @@ async def get_operator(
26
30
  operator_id = int(identifier)
27
31
  if operator_id < 0 or operator_id > 1_000_000:
28
32
  raise HTTPException(status_code=400, detail="Invalid operator ID")
29
- rewards = await service.get_operator_by_id(operator_id, detailed)
33
+ rewards = await service.get_operator_by_id(operator_id, detailed or history, history, withdrawals)
30
34
  elif identifier.startswith("0x"):
31
- rewards = await service.get_operator_by_address(identifier, detailed)
35
+ rewards = await service.get_operator_by_address(identifier, detailed or history, history, withdrawals)
32
36
  else:
33
37
  raise HTTPException(status_code=400, detail="Invalid identifier format")
34
38
 
@@ -39,17 +43,19 @@ async def get_operator(
39
43
  "operator_id": rewards.node_operator_id,
40
44
  "manager_address": rewards.manager_address,
41
45
  "reward_address": rewards.reward_address,
46
+ "curve_id": rewards.curve_id,
47
+ "operator_type": rewards.operator_type,
42
48
  "rewards": {
43
- "current_bond_eth": float(rewards.current_bond_eth),
44
- "required_bond_eth": float(rewards.required_bond_eth),
45
- "excess_bond_eth": float(rewards.excess_bond_eth),
49
+ "current_bond_eth": str(rewards.current_bond_eth),
50
+ "required_bond_eth": str(rewards.required_bond_eth),
51
+ "excess_bond_eth": str(rewards.excess_bond_eth),
46
52
  "cumulative_rewards_shares": rewards.cumulative_rewards_shares,
47
- "cumulative_rewards_eth": float(rewards.cumulative_rewards_eth),
53
+ "cumulative_rewards_eth": str(rewards.cumulative_rewards_eth),
48
54
  "distributed_shares": rewards.distributed_shares,
49
- "distributed_eth": float(rewards.distributed_eth),
55
+ "distributed_eth": str(rewards.distributed_eth),
50
56
  "unclaimed_shares": rewards.unclaimed_shares,
51
- "unclaimed_eth": float(rewards.unclaimed_eth),
52
- "total_claimable_eth": float(rewards.total_claimable_eth),
57
+ "unclaimed_eth": str(rewards.unclaimed_eth),
58
+ "total_claimable_eth": str(rewards.total_claimable_eth),
53
59
  },
54
60
  "validators": {
55
61
  "total": rewards.total_validators,
@@ -79,13 +85,65 @@ async def get_operator(
79
85
 
80
86
  # Add APY metrics if available
81
87
  if rewards.apy:
88
+ # Use actual excess bond for lifetime values (estimates for previous/current)
89
+ lifetime_bond = float(rewards.excess_bond_eth)
90
+ lifetime_net_total = (rewards.apy.lifetime_distribution_eth or 0) + lifetime_bond
82
91
  result["apy"] = {
92
+ "previous_distribution_eth": rewards.apy.previous_distribution_eth,
93
+ "previous_distribution_apy": rewards.apy.previous_distribution_apy,
94
+ "previous_net_apy": rewards.apy.previous_net_apy,
95
+ "previous_bond_eth": rewards.apy.previous_bond_eth,
96
+ "previous_bond_apr": rewards.apy.previous_bond_apr,
97
+ "previous_net_total_eth": rewards.apy.previous_net_total_eth,
98
+ "current_distribution_eth": rewards.apy.current_distribution_eth,
99
+ "current_distribution_apy": rewards.apy.current_distribution_apy,
100
+ "current_bond_eth": rewards.apy.current_bond_eth,
101
+ "current_bond_apr": rewards.apy.current_bond_apr,
102
+ "current_net_total_eth": rewards.apy.current_net_total_eth,
103
+ "lifetime_distribution_eth": rewards.apy.lifetime_distribution_eth,
104
+ "lifetime_bond_eth": lifetime_bond, # Actual excess bond, not estimate
105
+ "lifetime_net_total_eth": lifetime_net_total, # Matches Total Claimable
106
+ # Accurate lifetime APY (per-frame bond calculation when available)
107
+ "lifetime_reward_apy": rewards.apy.lifetime_reward_apy,
108
+ "lifetime_bond_apy": rewards.apy.lifetime_bond_apy,
109
+ "lifetime_net_apy": rewards.apy.lifetime_net_apy,
110
+ "next_distribution_date": rewards.apy.next_distribution_date,
111
+ "next_distribution_est_eth": rewards.apy.next_distribution_est_eth,
83
112
  "historical_reward_apy_28d": rewards.apy.historical_reward_apy_28d,
84
113
  "historical_reward_apy_ltd": rewards.apy.historical_reward_apy_ltd,
85
114
  "bond_apy": rewards.apy.bond_apy,
86
115
  "net_apy_28d": rewards.apy.net_apy_28d,
87
116
  "net_apy_ltd": rewards.apy.net_apy_ltd,
117
+ "uses_historical_apr": rewards.apy.uses_historical_apr,
88
118
  }
119
+ # Add frames if available (from history=true)
120
+ if rewards.apy.frames:
121
+ result["apy"]["frames"] = [
122
+ {
123
+ "frame_number": f.frame_number,
124
+ "start_date": f.start_date,
125
+ "end_date": f.end_date,
126
+ "rewards_eth": f.rewards_eth,
127
+ "rewards_shares": f.rewards_shares,
128
+ "duration_days": f.duration_days,
129
+ "validator_count": f.validator_count,
130
+ "apy": f.apy,
131
+ }
132
+ for f in rewards.apy.frames
133
+ ]
134
+
135
+ # Add withdrawal history if withdrawals=true (already fetched during data gathering)
136
+ if withdrawals and rewards.withdrawals:
137
+ result["withdrawals"] = [
138
+ {
139
+ "block_number": w.block_number,
140
+ "timestamp": w.timestamp,
141
+ "shares": w.shares,
142
+ "eth_value": w.eth_value,
143
+ "tx_hash": w.tx_hash,
144
+ }
145
+ for w in rewards.withdrawals
146
+ ]
89
147
 
90
148
  # Add active_since if available
91
149
  if rewards.active_since:
@@ -95,7 +153,7 @@ async def get_operator(
95
153
  if rewards.health:
96
154
  result["health"] = {
97
155
  "bond_healthy": rewards.health.bond_healthy,
98
- "bond_deficit_eth": float(rewards.health.bond_deficit_eth),
156
+ "bond_deficit_eth": str(rewards.health.bond_deficit_eth),
99
157
  "stuck_validators_count": rewards.health.stuck_validators_count,
100
158
  "slashed_validators_count": rewards.health.slashed_validators_count,
101
159
  "validators_at_risk_count": rewards.health.validators_at_risk_count,
@@ -105,6 +163,7 @@ async def get_operator(
105
163
  "validators_near_ejection": rewards.health.strikes.validators_near_ejection,
106
164
  "total_strikes": rewards.health.strikes.total_strikes,
107
165
  "max_strikes": rewards.health.strikes.max_strikes,
166
+ "strike_threshold": rewards.health.strikes.strike_threshold,
108
167
  },
109
168
  "has_issues": rewards.health.has_issues,
110
169
  }
@@ -135,18 +194,25 @@ async def get_operator_strikes(identifier: str):
135
194
  else:
136
195
  raise HTTPException(status_code=400, detail="Invalid identifier format")
137
196
 
138
- strikes = await service.get_operator_strikes(operator_id)
197
+ # Get curve_id to determine strike threshold
198
+ curve_id = await service.onchain.get_bond_curve_id(operator_id)
199
+ strikes = await service.get_operator_strikes(operator_id, curve_id)
139
200
 
140
201
  # Fetch frame dates for tooltip display
141
202
  frame_dates = await service.get_recent_frame_dates(6)
142
203
 
204
+ # Get the threshold for this operator type
205
+ strike_threshold = strikes[0].strike_threshold if strikes else 3
206
+
143
207
  return {
144
208
  "operator_id": operator_id,
209
+ "strike_threshold": strike_threshold,
145
210
  "frame_dates": frame_dates,
146
211
  "validators": [
147
212
  {
148
213
  "pubkey": s.pubkey,
149
214
  "strike_count": s.strike_count,
215
+ "strike_threshold": s.strike_threshold,
150
216
  "strikes": s.strikes,
151
217
  "at_ejection_risk": s.at_ejection_risk,
152
218
  }
@@ -1,32 +0,0 @@
1
- src/__init__.py,sha256=Bfqpjo9Q1XaV8DNqkf1sADzWg2ACpfZWSGLtahZE3iU,35
2
- src/main.py,sha256=tj7C09FVBGBVyjKYwmElpX5M92xfydDm8RJ6-MSFdMk,951
3
- src/abis/CSAccounting.json,sha256=FSZ4SyT9XMxPLBezcs77LCfJBKkrO9r_rCavD-uKm8A,815
4
- src/abis/CSFeeDistributor.json,sha256=unLBacJcCHq4xsmB4xOPlVXcOrxGWNf6KDmC3Ld5u-c,1517
5
- src/abis/CSModule.json,sha256=T6D6aInBoqVH3ZD6U6p3lrPa3t_ucA9V83IwE80kOuU,1687
6
- src/abis/__init__.py,sha256=9HV2hKMGSoNAi8evsjzymTr4V4kowITNsX1-LPu6l98,20
7
- src/abis/stETH.json,sha256=wZobN-RDcYbLs4byBs1e5A8ML6CgfihuXhEPTByB-H0,827
8
- src/cli/__init__.py,sha256=mgHAwomqzAhOHJnlWrWtCguhGzhDWlkCvkzKsfoFsds,35
9
- src/cli/commands.py,sha256=kSioEDzBf85MKEEWb_wq9DbRUyEtQYnHPJ9dxi-nPvk,23929
10
- src/core/__init__.py,sha256=ZDHZojANK1ZFpn5lQROERTo97MYQFAxqA9APvs7ruEQ,57
11
- src/core/config.py,sha256=UD4-LVHdeQeT6n9BtLXh2zZcXfX0-ZhrMVZx93E2BGk,1429
12
- src/core/contracts.py,sha256=8Y72h5uUTaIMLWYdtu5O2Bjw1hTyIHOegaDs0W8r74g,525
13
- src/core/types.py,sha256=dzTdZiOOzxwj4oh0ZLEWqb4jbVsA0TVm6Rqkod7yUMg,4551
14
- src/data/__init__.py,sha256=DItA8aEp8Lbr0uFlJVppMaTtVEEznoA1hkRpH-vfHhk,57
15
- src/data/beacon.py,sha256=9oaR7TO2WcP_D3jVTzcd9WE6NbF2WnqQDW_y5M8GsZ0,13511
16
- src/data/cache.py,sha256=w1iv4rM-FVgFlaSNYdQA3CfqyhZo9-gqbZwKmzKY0Ps,2134
17
- src/data/etherscan.py,sha256=mkd4KB-4JUvY_aVVqUH119GLS--_qivmeRKR70ivQkk,2832
18
- src/data/ipfs_logs.py,sha256=IV_eTyktO3DaiTsXgU7huzo51I60OVo8ne9G_yvRdIk,9192
19
- src/data/known_cids.py,sha256=onwTTNvAv4h0-3LZgLhqMlKzuNH2VhBqQGZ9fm8GyoE,1705
20
- src/data/lido_api.py,sha256=WuVrqVOT7trxGgZX4Kavg7k9DyKyXIa_aSc_7XVLU2Y,1026
21
- src/data/onchain.py,sha256=l9xQxKoXhmr0TKguba1L423_o9mdTLoeDhegMpa3ZgY,9710
22
- src/data/rewards_tree.py,sha256=a-MO14b4COjOvy59FPd0jaf1dkgidlqCQKAFDinwRJU,1894
23
- src/data/strikes.py,sha256=9iSW7Xm2W0rqySAJn5IwqCCKf-ef2XazC7tYHgz9REk,7480
24
- src/services/__init__.py,sha256=MC7blFLAMazErCWuyYXvS6sO3uZm1z_RUOtnlIK0kSo,38
25
- src/services/operator_service.py,sha256=Y6rXYdJzv7V3Nix99iiyC7NcDRfShdvDTcRtXbp_oTQ,13015
26
- src/web/__init__.py,sha256=iI2c5xxXmzsNxIetm0P2qE3uVsT-ClsMfzn620r5YTU,40
27
- src/web/app.py,sha256=qEIB05J0sKEeZkfHkJwotltsL-d2j1KTDS56cQl2_IU,32129
28
- src/web/routes.py,sha256=z0cmH4mA3IxlHYQ-8FFYQrXCRVsi3PvUG_8ZWLixfwg,6204
29
- csm_dashboard-0.2.2.dist-info/METADATA,sha256=jb13-vR-VJD327jLVqi9qi0mbdIvEz1ipQt1yS3h07U,10003
30
- csm_dashboard-0.2.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
31
- csm_dashboard-0.2.2.dist-info/entry_points.txt,sha256=P1Ul8ALIPBwDlVlXqTPuzJ64xxRpIJsYW8U73Tyjgtg,37
32
- csm_dashboard-0.2.2.dist-info/RECORD,,