csm-dashboard 0.2.2__py3-none-any.whl → 0.3.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csm-dashboard
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Lido CSM Operator Dashboard for tracking validator earnings
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: fastapi>=0.104
@@ -31,8 +31,11 @@ Track your Lido Community Staking Module (CSM) validator earnings, excess bond,
31
31
  - Look up operator by Ethereum address (manager or rewards address) or operator ID
32
32
  - View current bond vs required bond (excess is claimable)
33
33
  - Track cumulative rewards and unclaimed amounts
34
+ - Operator type detection (Permissionless, ICS/Legacy EA, etc.)
34
35
  - Detailed validator status from beacon chain (with `--detailed` flag)
35
36
  - APY metrics: reward APY, bond APY (stETH rebase), and net APY
37
+ - Full distribution history with per-frame breakdown (with `--history` flag)
38
+ - Withdrawal/claim history tracking (with `--withdrawals` flag)
36
39
  - JSON output for scripting and automation
37
40
  - CLI for quick terminal lookups
38
41
  - Web interface for browser-based monitoring
@@ -85,6 +88,7 @@ Available settings:
85
88
  - `BEACON_API_URL`: Beacon chain API (default: https://beaconcha.in/api/v1)
86
89
  - `BEACON_API_KEY`: Optional API key for beaconcha.in (higher rate limits)
87
90
  - `ETHERSCAN_API_KEY`: Optional API key for Etherscan (recommended for accurate historical data)
91
+ - `THEGRAPH_API_KEY`: Optional API key for The Graph (enables historical Bond APY per distribution frame)
88
92
  - `CACHE_TTL_SECONDS`: Cache duration in seconds (default: 300)
89
93
 
90
94
  ## Usage
@@ -128,6 +132,8 @@ csm rewards [ADDRESS] [OPTIONS]
128
132
  | `ADDRESS` | | Ethereum address (required unless `--id` is provided) |
129
133
  | `--id` | `-i` | Operator ID (skips address lookup, faster) |
130
134
  | `--detailed` | `-d` | Include validator status from beacon chain and APY metrics |
135
+ | `--history` | `-H` | Show all historical distribution frames with per-frame APY |
136
+ | `--withdrawals` | `-w` | Include withdrawal/claim history |
131
137
  | `--json` | `-j` | Output as JSON (same format as API) |
132
138
  | `--rpc` | `-r` | Custom RPC URL |
133
139
 
@@ -143,6 +149,12 @@ csm rewards --id 42
143
149
  # Get detailed validator info and APY
144
150
  csm rewards --id 42 --detailed
145
151
 
152
+ # Show full distribution history with Previous/Current/Lifetime columns
153
+ csm rewards --id 42 --history
154
+
155
+ # Include withdrawal history
156
+ csm rewards --id 42 --withdrawals
157
+
146
158
  # JSON output for scripting
147
159
  csm rewards --id 42 --json
148
160
 
@@ -226,17 +238,16 @@ csm rewards --id 333 --json
226
238
  "operator_id": 333,
227
239
  "manager_address": "0x6ac683C503CF210CCF88193ec7ebDe2c993f63a4",
228
240
  "reward_address": "0x55915Cf2115c4D6e9085e94c8dAD710cabefef31",
241
+ "curve_id": 2,
242
+ "operator_type": "Permissionless",
229
243
  "rewards": {
230
- "current_bond_eth": 651.5523536856277,
244
+ "current_bond_eth": 651.55,
231
245
  "required_bond_eth": 650.2,
232
- "excess_bond_eth": 1.3523536856277778,
233
- "cumulative_rewards_shares": 8973877501313655495,
234
- "cumulative_rewards_eth": 10.9642938931415,
235
- "distributed_shares": 7867435720490255061,
236
- "distributed_eth": 9.61244204773546,
237
- "unclaimed_shares": 1106441780823400434,
238
- "unclaimed_eth": 1.3518518454060409,
239
- "total_claimable_eth": 2.7042055310338187
246
+ "excess_bond_eth": 1.35,
247
+ "cumulative_rewards_eth": 10.96,
248
+ "distributed_eth": 9.61,
249
+ "unclaimed_eth": 1.35,
250
+ "total_claimable_eth": 2.70
240
251
  },
241
252
  "validators": {
242
253
  "total": 500,
@@ -251,44 +262,68 @@ With `--detailed`, additional fields are included:
251
262
  ```json
252
263
  {
253
264
  "operator_id": 333,
254
- "...": "...",
265
+ "curve_id": 2,
266
+ "operator_type": "Permissionless",
255
267
  "validators": {
256
268
  "total": 500,
257
269
  "active": 500,
258
270
  "exited": 0,
259
271
  "by_status": {
260
- "active": 100,
272
+ "active": 500,
261
273
  "pending": 0,
262
274
  "exiting": 0,
263
275
  "exited": 0,
264
- "slashed": 0,
265
- "unknown": 0
276
+ "slashed": 0
266
277
  }
267
278
  },
268
279
  "performance": {
269
280
  "avg_effectiveness": 98.5
270
281
  },
271
282
  "apy": {
272
- "historical_reward_apy_28d": 2.21,
273
- "historical_reward_apy_ltd": 2.03,
274
- "bond_apy": 2.54,
275
- "net_apy_28d": 4.75,
276
- "net_apy_ltd": 4.57
283
+ "current_distribution_apy": 2.77,
284
+ "current_bond_apr": 2.56,
285
+ "net_apy_28d": 5.33,
286
+ "lifetime_reward_apy": 2.80,
287
+ "lifetime_bond_apy": 2.60,
288
+ "lifetime_net_apy": 5.40
277
289
  },
278
290
  "active_since": "2025-02-16T12:00:00"
279
291
  }
280
292
  ```
281
293
 
294
+ With `--history`, you also get the full distribution frame history:
295
+
296
+ ```json
297
+ {
298
+ "apy": {
299
+ "frames": [
300
+ {
301
+ "frame_number": 1,
302
+ "start_date": "2025-03-14T00:00:00",
303
+ "end_date": "2025-04-11T00:00:00",
304
+ "rewards_eth": 1.2345,
305
+ "validator_count": 500,
306
+ "duration_days": 28.0,
307
+ "apy": 2.85
308
+ }
309
+ ]
310
+ }
311
+ }
312
+ ```
313
+
282
314
  ## API Endpoints
283
315
 
284
316
  - `GET /api/operator/{address_or_id}` - Get operator rewards data
285
317
  - Query param: `?detailed=true` for validator status and APY
318
+ - Query param: `?history=true` for all historical distribution frames
319
+ - Query param: `?withdrawals=true` for withdrawal/claim history
320
+ - `GET /api/operator/{address_or_id}/strikes` - Get detailed validator strikes
286
321
  - `GET /api/operators` - List all operators with rewards
287
322
  - `GET /api/health` - Health check
288
323
 
289
324
  ## Understanding APY Metrics
290
325
 
291
- The dashboard shows three APY metrics when using the `--detailed` flag:
326
+ The dashboard shows three APY metrics when using the `--detailed` or `--history` flags:
292
327
 
293
328
  | Metric | What It Means |
294
329
  |--------|---------------|
@@ -296,16 +331,34 @@ The dashboard shows three APY metrics when using the `--detailed` flag:
296
331
  | **Bond APY** | Automatic growth of your stETH bond from protocol rebasing (same for all operators) |
297
332
  | **NET APY** | Total return = Reward APY + Bond APY |
298
333
 
334
+ ### Display Modes
335
+
336
+ - **`--detailed`**: Shows only the Current frame column (simpler view)
337
+ - **`--history`**: Shows Previous, Current, and Lifetime columns with full distribution history
338
+
299
339
  ### How APY is Calculated
300
340
 
301
- **Reward APY** is calculated from actual reward distribution data published by Lido. Every ~28 days, Lido calculates how much each operator earned and publishes a "distribution frame" to IPFS (a decentralized file storage network). The dashboard fetches all these historical frames to calculate both 28-day and lifetime APY.
341
+ **Reward APY** is calculated from actual reward distribution data published by Lido. Every ~28 days, Lido calculates how much each operator earned and publishes a "distribution frame" to IPFS (a decentralized file storage network). The dashboard fetches all these historical frames to calculate APY.
342
+
343
+ - **Current APY**: Based on the most recent distribution frame (~28 days)
344
+ - **Previous APY**: Based on the second-to-last distribution frame
345
+ - **Lifetime APY**: Duration-weighted average of all frames, using **per-frame bond requirements** for accuracy
346
+
347
+ The **Lifetime APY** calculation is particularly sophisticated: it uses each frame's actual validator count to determine the bond requirement for that period, then calculates a duration-weighted average. This produces accurate lifetime APY even for operators who have grown significantly over time.
348
+
349
+ **Bond APY** represents the stETH rebase rate—the automatic growth of your bond due to Ethereum staking rewards. This rate is set by the Lido protocol and applies equally to all operators.
350
+
351
+ > **Note**: With a Graph API key configured (`THEGRAPH_API_KEY`), Bond APY shows the actual historical stETH rate for each distribution frame. Without the API key, it falls back to the current rate (marked with an asterisk).
302
352
 
303
- - **28-Day APY**: Based on the most recent ~28 days of reward distributions
304
- - **Lifetime APY**: Based on all periods where you earned rewards (excludes ramp-up periods with no rewards to avoid misleadingly low numbers)
353
+ ### Operator Types
305
354
 
306
- **Bond APY** represents the stETH rebase rate—the automatic growth of your bond due to Ethereum staking rewards. This rate is set by the Lido protocol and applies equally to all operators. The dashboard shows the current 7-day average rate from Lido's API.
355
+ The dashboard detects your operator type from the CSAccounting bond curve:
307
356
 
308
- > **Note**: Bond APY shows the current stETH rate for both 28-Day and Lifetime columns, as historical rates aren't readily available.
357
+ | Type | Description |
358
+ |------|-------------|
359
+ | **Permissionless** | Standard operators (Curve 2, current default) |
360
+ | **Permissionless (Legacy)** | Early permissionless operators (Curve 0, deprecated) |
361
+ | **ICS/Legacy EA** | Incentivized Community Stakers / Early Adopters (Curve 1) |
309
362
 
310
363
  ### Why You Might Want an Etherscan API Key
311
364
 
@@ -324,6 +377,18 @@ The actual reward data lives on IPFS and is always accessible. However, to *disc
324
377
 
325
378
  The free tier allows 5 calls/second, which is plenty for this dashboard.
326
379
 
380
+ ### Why You Might Want a Graph API Key
381
+
382
+ The Graph provides historical stETH APR data from the Lido subgraph. Without this API key, Bond APY calculations use the current rate for all periods, which is less accurate.
383
+
384
+ **How to get one (free):**
385
+ 1. Go to [thegraph.com/studio](https://thegraph.com/studio/)
386
+ 2. Connect your wallet and create an account
387
+ 3. Go to "API Keys" and create a new key
388
+ 4. Add to your `.env` file: `THEGRAPH_API_KEY=your_key_here`
389
+
390
+ The free tier includes 100,000 queries/month, which is plenty for this dashboard.
391
+
327
392
  ## Data Sources
328
393
 
329
394
  - **On-chain contracts**: CSModule, CSAccounting, CSFeeDistributor, stETH
@@ -1,32 +1,32 @@
1
1
  src/__init__.py,sha256=Bfqpjo9Q1XaV8DNqkf1sADzWg2ACpfZWSGLtahZE3iU,35
2
2
  src/main.py,sha256=tj7C09FVBGBVyjKYwmElpX5M92xfydDm8RJ6-MSFdMk,951
3
- src/abis/CSAccounting.json,sha256=FSZ4SyT9XMxPLBezcs77LCfJBKkrO9r_rCavD-uKm8A,815
3
+ src/abis/CSAccounting.json,sha256=-eBMqw3XqgMDzlVrG8mOrd7IYLf8nfsBIpjYqaKPYno,1281
4
4
  src/abis/CSFeeDistributor.json,sha256=unLBacJcCHq4xsmB4xOPlVXcOrxGWNf6KDmC3Ld5u-c,1517
5
5
  src/abis/CSModule.json,sha256=T6D6aInBoqVH3ZD6U6p3lrPa3t_ucA9V83IwE80kOuU,1687
6
6
  src/abis/__init__.py,sha256=9HV2hKMGSoNAi8evsjzymTr4V4kowITNsX1-LPu6l98,20
7
- src/abis/stETH.json,sha256=wZobN-RDcYbLs4byBs1e5A8ML6CgfihuXhEPTByB-H0,827
7
+ src/abis/stETH.json,sha256=ldxbIRrtt8ePVFewJ9Tnz4qUGFmuOXa41GN1t3tnWEg,1106
8
8
  src/cli/__init__.py,sha256=mgHAwomqzAhOHJnlWrWtCguhGzhDWlkCvkzKsfoFsds,35
9
- src/cli/commands.py,sha256=kSioEDzBf85MKEEWb_wq9DbRUyEtQYnHPJ9dxi-nPvk,23929
9
+ src/cli/commands.py,sha256=hEbTsj5dTF_K88yUlMWKjRCOuUDYCpugKjQI9vPpMGw,34858
10
10
  src/core/__init__.py,sha256=ZDHZojANK1ZFpn5lQROERTo97MYQFAxqA9APvs7ruEQ,57
11
- src/core/config.py,sha256=UD4-LVHdeQeT6n9BtLXh2zZcXfX0-ZhrMVZx93E2BGk,1429
11
+ src/core/config.py,sha256=NqkS2zsWyDMDz-q0eE9akHmTE9kZ-snGdTxLn-Dks4E,1532
12
12
  src/core/contracts.py,sha256=8Y72h5uUTaIMLWYdtu5O2Bjw1hTyIHOegaDs0W8r74g,525
13
- src/core/types.py,sha256=dzTdZiOOzxwj4oh0ZLEWqb4jbVsA0TVm6Rqkod7yUMg,4551
13
+ src/core/types.py,sha256=BrwLnZ1_yYKh-mVlw9j5UmV5ci-zWmg_Whsn3-xSZKc,7211
14
14
  src/data/__init__.py,sha256=DItA8aEp8Lbr0uFlJVppMaTtVEEznoA1hkRpH-vfHhk,57
15
15
  src/data/beacon.py,sha256=9oaR7TO2WcP_D3jVTzcd9WE6NbF2WnqQDW_y5M8GsZ0,13511
16
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
17
+ src/data/etherscan.py,sha256=UeCucGPd4m39yl13uogY7hCBm46Ge7nZQ96V3eoOmu4,5064
18
+ src/data/ipfs_logs.py,sha256=gXUTP9dmZ_e7gW6ouotJ1r_72ixq2q_32y2evYQf0DM,10709
19
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
20
+ src/data/lido_api.py,sha256=477-1vqlMAwLaisaa61T9AxJuSUV6W7NLg1cxvdmheY,4884
21
+ src/data/onchain.py,sha256=A8KiNldL_-KStFNDzvzROFqYGgZOBaDj8O0chWfOl8w,16657
22
22
  src/data/rewards_tree.py,sha256=a-MO14b4COjOvy59FPd0jaf1dkgidlqCQKAFDinwRJU,1894
23
23
  src/data/strikes.py,sha256=9iSW7Xm2W0rqySAJn5IwqCCKf-ef2XazC7tYHgz9REk,7480
24
24
  src/services/__init__.py,sha256=MC7blFLAMazErCWuyYXvS6sO3uZm1z_RUOtnlIK0kSo,38
25
- src/services/operator_service.py,sha256=Y6rXYdJzv7V3Nix99iiyC7NcDRfShdvDTcRtXbp_oTQ,13015
25
+ src/services/operator_service.py,sha256=Yau_pDylc-tSJmlVypEJVzKRhfXGMCud5Cov2rtk990,29173
26
26
  src/web/__init__.py,sha256=iI2c5xxXmzsNxIetm0P2qE3uVsT-ClsMfzn620r5YTU,40
27
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,,
28
+ src/web/routes.py,sha256=sBO7pQBJwbfyyQ61pbQRa6eCugTki6UC6yyZpQW-ne8,9488
29
+ csm_dashboard-0.3.0.dist-info/METADATA,sha256=AIHgFHGGEEInmieAAHvJ6DB1vsXTn0Nq0OI1U02cc-w,12552
30
+ csm_dashboard-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
31
+ csm_dashboard-0.3.0.dist-info/entry_points.txt,sha256=P1Ul8ALIPBwDlVlXqTPuzJ64xxRpIJsYW8U73Tyjgtg,37
32
+ csm_dashboard-0.3.0.dist-info/RECORD,,
@@ -33,5 +33,27 @@
33
33
  "outputs": [
34
34
  {"name": "", "type": "uint256"}
35
35
  ]
36
+ },
37
+ {
38
+ "name": "getBondCurve",
39
+ "type": "function",
40
+ "stateMutability": "view",
41
+ "inputs": [
42
+ {"name": "nodeOperatorId", "type": "uint256"}
43
+ ],
44
+ "outputs": [
45
+ {"name": "points", "type": "uint256[]"}
46
+ ]
47
+ },
48
+ {
49
+ "name": "getBondCurveId",
50
+ "type": "function",
51
+ "stateMutability": "view",
52
+ "inputs": [
53
+ {"name": "nodeOperatorId", "type": "uint256"}
54
+ ],
55
+ "outputs": [
56
+ {"name": "", "type": "uint256"}
57
+ ]
36
58
  }
37
59
  ]
src/abis/stETH.json CHANGED
@@ -38,5 +38,15 @@
38
38
  "outputs": [
39
39
  {"name": "", "type": "uint256"}
40
40
  ]
41
+ },
42
+ {
43
+ "name": "Transfer",
44
+ "type": "event",
45
+ "anonymous": false,
46
+ "inputs": [
47
+ {"indexed": true, "name": "from", "type": "address"},
48
+ {"indexed": true, "name": "to", "type": "address"},
49
+ {"indexed": false, "name": "value", "type": "uint256"}
50
+ ]
41
51
  }
42
52
  ]