csm-dashboard 0.2.1__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.
- {csm_dashboard-0.2.1.dist-info → csm_dashboard-0.3.0.dist-info}/METADATA +90 -25
- {csm_dashboard-0.2.1.dist-info → csm_dashboard-0.3.0.dist-info}/RECORD +15 -15
- src/abis/CSAccounting.json +22 -0
- src/abis/stETH.json +10 -0
- src/cli/commands.py +252 -33
- src/core/config.py +3 -0
- src/core/types.py +74 -3
- src/data/etherscan.py +60 -0
- src/data/ipfs_logs.py +42 -2
- src/data/lido_api.py +105 -0
- src/data/onchain.py +190 -0
- src/services/operator_service.py +339 -29
- src/web/routes.py +60 -2
- {csm_dashboard-0.2.1.dist-info → csm_dashboard-0.3.0.dist-info}/WHEEL +0 -0
- {csm_dashboard-0.2.1.dist-info → csm_dashboard-0.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: csm-dashboard
|
|
3
|
-
Version: 0.
|
|
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.
|
|
244
|
+
"current_bond_eth": 651.55,
|
|
231
245
|
"required_bond_eth": 650.2,
|
|
232
|
-
"excess_bond_eth": 1.
|
|
233
|
-
"
|
|
234
|
-
"
|
|
235
|
-
"
|
|
236
|
-
"
|
|
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":
|
|
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
|
-
"
|
|
273
|
-
"
|
|
274
|
-
"
|
|
275
|
-
"
|
|
276
|
-
"
|
|
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`
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
355
|
+
The dashboard detects your operator type from the CSAccounting bond curve:
|
|
307
356
|
|
|
308
|
-
|
|
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
|
|
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=
|
|
7
|
+
src/abis/stETH.json,sha256=ldxbIRrtt8ePVFewJ9Tnz4qUGFmuOXa41GN1t3tnWEg,1106
|
|
8
8
|
src/cli/__init__.py,sha256=mgHAwomqzAhOHJnlWrWtCguhGzhDWlkCvkzKsfoFsds,35
|
|
9
|
-
src/cli/commands.py,sha256=
|
|
9
|
+
src/cli/commands.py,sha256=hEbTsj5dTF_K88yUlMWKjRCOuUDYCpugKjQI9vPpMGw,34858
|
|
10
10
|
src/core/__init__.py,sha256=ZDHZojANK1ZFpn5lQROERTo97MYQFAxqA9APvs7ruEQ,57
|
|
11
|
-
src/core/config.py,sha256=
|
|
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=
|
|
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=
|
|
18
|
-
src/data/ipfs_logs.py,sha256=
|
|
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=
|
|
21
|
-
src/data/onchain.py,sha256=
|
|
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=
|
|
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=
|
|
29
|
-
csm_dashboard-0.
|
|
30
|
-
csm_dashboard-0.
|
|
31
|
-
csm_dashboard-0.
|
|
32
|
-
csm_dashboard-0.
|
|
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,,
|
src/abis/CSAccounting.json
CHANGED
|
@@ -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
|
]
|