nexaroa 0.0.111__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.
Files changed (78) hide show
  1. neuroshard/__init__.py +93 -0
  2. neuroshard/__main__.py +4 -0
  3. neuroshard/cli.py +466 -0
  4. neuroshard/core/__init__.py +92 -0
  5. neuroshard/core/consensus/verifier.py +252 -0
  6. neuroshard/core/crypto/__init__.py +20 -0
  7. neuroshard/core/crypto/ecdsa.py +392 -0
  8. neuroshard/core/economics/__init__.py +52 -0
  9. neuroshard/core/economics/constants.py +387 -0
  10. neuroshard/core/economics/ledger.py +2111 -0
  11. neuroshard/core/economics/market.py +975 -0
  12. neuroshard/core/economics/wallet.py +168 -0
  13. neuroshard/core/governance/__init__.py +74 -0
  14. neuroshard/core/governance/proposal.py +561 -0
  15. neuroshard/core/governance/registry.py +545 -0
  16. neuroshard/core/governance/versioning.py +332 -0
  17. neuroshard/core/governance/voting.py +453 -0
  18. neuroshard/core/model/__init__.py +30 -0
  19. neuroshard/core/model/dynamic.py +4186 -0
  20. neuroshard/core/model/llm.py +905 -0
  21. neuroshard/core/model/registry.py +164 -0
  22. neuroshard/core/model/scaler.py +387 -0
  23. neuroshard/core/model/tokenizer.py +568 -0
  24. neuroshard/core/network/__init__.py +56 -0
  25. neuroshard/core/network/connection_pool.py +72 -0
  26. neuroshard/core/network/dht.py +130 -0
  27. neuroshard/core/network/dht_plan.py +55 -0
  28. neuroshard/core/network/dht_proof_store.py +516 -0
  29. neuroshard/core/network/dht_protocol.py +261 -0
  30. neuroshard/core/network/dht_service.py +506 -0
  31. neuroshard/core/network/encrypted_channel.py +141 -0
  32. neuroshard/core/network/nat.py +201 -0
  33. neuroshard/core/network/nat_traversal.py +695 -0
  34. neuroshard/core/network/p2p.py +929 -0
  35. neuroshard/core/network/p2p_data.py +150 -0
  36. neuroshard/core/swarm/__init__.py +106 -0
  37. neuroshard/core/swarm/aggregation.py +729 -0
  38. neuroshard/core/swarm/buffers.py +643 -0
  39. neuroshard/core/swarm/checkpoint.py +709 -0
  40. neuroshard/core/swarm/compute.py +624 -0
  41. neuroshard/core/swarm/diloco.py +844 -0
  42. neuroshard/core/swarm/factory.py +1288 -0
  43. neuroshard/core/swarm/heartbeat.py +669 -0
  44. neuroshard/core/swarm/logger.py +487 -0
  45. neuroshard/core/swarm/router.py +658 -0
  46. neuroshard/core/swarm/service.py +640 -0
  47. neuroshard/core/training/__init__.py +29 -0
  48. neuroshard/core/training/checkpoint.py +600 -0
  49. neuroshard/core/training/distributed.py +1602 -0
  50. neuroshard/core/training/global_tracker.py +617 -0
  51. neuroshard/core/training/production.py +276 -0
  52. neuroshard/governance_cli.py +729 -0
  53. neuroshard/grpc_server.py +895 -0
  54. neuroshard/runner.py +3223 -0
  55. neuroshard/sdk/__init__.py +92 -0
  56. neuroshard/sdk/client.py +990 -0
  57. neuroshard/sdk/errors.py +101 -0
  58. neuroshard/sdk/types.py +282 -0
  59. neuroshard/tracker/__init__.py +0 -0
  60. neuroshard/tracker/server.py +864 -0
  61. neuroshard/ui/__init__.py +0 -0
  62. neuroshard/ui/app.py +102 -0
  63. neuroshard/ui/templates/index.html +1052 -0
  64. neuroshard/utils/__init__.py +0 -0
  65. neuroshard/utils/autostart.py +81 -0
  66. neuroshard/utils/hardware.py +121 -0
  67. neuroshard/utils/serialization.py +90 -0
  68. neuroshard/version.py +1 -0
  69. nexaroa-0.0.111.dist-info/METADATA +283 -0
  70. nexaroa-0.0.111.dist-info/RECORD +78 -0
  71. nexaroa-0.0.111.dist-info/WHEEL +5 -0
  72. nexaroa-0.0.111.dist-info/entry_points.txt +4 -0
  73. nexaroa-0.0.111.dist-info/licenses/LICENSE +190 -0
  74. nexaroa-0.0.111.dist-info/top_level.txt +2 -0
  75. protos/__init__.py +0 -0
  76. protos/neuroshard.proto +651 -0
  77. protos/neuroshard_pb2.py +160 -0
  78. protos/neuroshard_pb2_grpc.py +1298 -0
@@ -0,0 +1,545 @@
1
+ """
2
+ NEP Registry - Decentralized Proposal Storage
3
+
4
+ The registry stores all NEPs and their status. It's replicated across
5
+ nodes via the gossip protocol, similar to how proofs and stakes are shared.
6
+
7
+ Storage:
8
+ - SQLite locally (fast queries)
9
+ - Gossip sync (network consistency)
10
+ - DHT backup (persistence)
11
+ """
12
+
13
+ import sqlite3
14
+ import time
15
+ import json
16
+ import logging
17
+ import threading
18
+ from typing import Dict, List, Optional, Tuple
19
+ from pathlib import Path
20
+
21
+ from .proposal import NEP, NEPType, NEPStatus, EconomicImpact, UpgradePath
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class NEPRegistry:
27
+ """
28
+ Registry of all NeuroShard Enhancement Proposals.
29
+
30
+ Responsibilities:
31
+ - Store and retrieve NEPs
32
+ - Track proposal lifecycle
33
+ - Enforce submission rules (e.g., stake requirement to propose)
34
+ - Sync with network via gossip
35
+
36
+ Economic Incentives (Stake-Proportional):
37
+ - Proposers pay PROPOSAL_FEE upfront (burned if spam/no-quorum)
38
+ - Rewards scale with total stake that participated in voting
39
+ - This aligns incentives: more impactful proposals = more votes = more reward
40
+
41
+ Reward Formula:
42
+ proposer_reward = PROPOSER_REWARD_RATE * total_stake_voted
43
+
44
+ Example:
45
+ - 1000 NEURO stake voted → 1000 * 0.001 = 1 NEURO reward
46
+ - 100,000 NEURO stake voted → 100,000 * 0.001 = 100 NEURO reward
47
+ - 1M NEURO stake voted → 1,000,000 * 0.001 = 1000 NEURO reward
48
+ """
49
+
50
+ # Minimum stake to create a proposal (prevents spam)
51
+ MIN_STAKE_TO_PROPOSE = 100.0 # 100 NEURO
52
+
53
+ # Proposal submission fee (held in escrow, burned if spam/no-quorum)
54
+ PROPOSAL_FEE = 10.0 # 10 NEURO
55
+
56
+ # Stake-proportional rewards (% of total stake that voted)
57
+ PROPOSER_REWARD_RATE_APPROVED = 0.001 # 0.1% of total stake voted (if approved)
58
+ PROPOSER_REWARD_RATE_QUORUM = 0.0001 # 0.01% of total stake voted (if rejected but quorum)
59
+
60
+ # Minimum/maximum caps to prevent extremes
61
+ PROPOSER_REWARD_MIN = 1.0 # At least 1 NEURO (if approved)
62
+ PROPOSER_REWARD_MAX = 1000.0 # At most 1000 NEURO (prevents gaming)
63
+
64
+ # Voter rewards (incentivizes participation)
65
+ # Reward = VOTER_REWARD_RATE * voter's_stake
66
+ VOTER_REWARD_RATE = 0.0001 # 0.01% of voter's stake per vote
67
+
68
+ def __init__(
69
+ self,
70
+ db_path: str = "nep_registry.db",
71
+ ledger=None, # NEUROLedger for stake checks
72
+ ):
73
+ self.db_path = db_path
74
+ self.ledger = ledger
75
+ self.lock = threading.Lock()
76
+ self._next_nep_number = 1
77
+
78
+ self._init_db()
79
+ self._load_next_number()
80
+
81
+ def _init_db(self):
82
+ """Initialize SQLite database."""
83
+ with self.lock:
84
+ with sqlite3.connect(self.db_path, timeout=60.0) as conn:
85
+ conn.execute("""
86
+ CREATE TABLE IF NOT EXISTS neps (
87
+ nep_id TEXT PRIMARY KEY,
88
+ nep_type TEXT NOT NULL,
89
+ status TEXT NOT NULL,
90
+ title TEXT NOT NULL,
91
+ author_node_id TEXT NOT NULL,
92
+ content_hash TEXT NOT NULL,
93
+ created_at REAL NOT NULL,
94
+ updated_at REAL NOT NULL,
95
+ voting_start REAL,
96
+ voting_end REAL,
97
+ activation_block INTEGER,
98
+ full_data TEXT NOT NULL
99
+ )
100
+ """)
101
+
102
+ conn.execute("""
103
+ CREATE INDEX IF NOT EXISTS idx_nep_status
104
+ ON neps(status)
105
+ """)
106
+
107
+ conn.execute("""
108
+ CREATE INDEX IF NOT EXISTS idx_nep_type
109
+ ON neps(nep_type)
110
+ """)
111
+
112
+ # Track highest NEP number
113
+ conn.execute("""
114
+ CREATE TABLE IF NOT EXISTS registry_meta (
115
+ key TEXT PRIMARY KEY,
116
+ value TEXT
117
+ )
118
+ """)
119
+
120
+ def _load_next_number(self):
121
+ """Load the next NEP number from database."""
122
+ with sqlite3.connect(self.db_path, timeout=60.0) as conn:
123
+ row = conn.execute(
124
+ "SELECT value FROM registry_meta WHERE key = 'next_nep_number'"
125
+ ).fetchone()
126
+
127
+ if row:
128
+ self._next_nep_number = int(row[0])
129
+ else:
130
+ # Count existing NEPs and set next
131
+ count = conn.execute("SELECT COUNT(*) FROM neps").fetchone()[0]
132
+ self._next_nep_number = count + 1
133
+
134
+ def _allocate_nep_id(self) -> str:
135
+ """Allocate the next NEP ID."""
136
+ nep_id = f"NEP-{self._next_nep_number:03d}"
137
+ self._next_nep_number += 1
138
+
139
+ with sqlite3.connect(self.db_path, timeout=60.0) as conn:
140
+ conn.execute(
141
+ "INSERT OR REPLACE INTO registry_meta (key, value) VALUES (?, ?)",
142
+ ("next_nep_number", str(self._next_nep_number))
143
+ )
144
+
145
+ return nep_id
146
+
147
+ def submit_proposal(
148
+ self,
149
+ nep: NEP,
150
+ node_id: str,
151
+ signature: str,
152
+ ) -> Tuple[bool, str, Optional[str]]:
153
+ """
154
+ Submit a new proposal to the registry.
155
+
156
+ Requirements:
157
+ 1. Author must have MIN_STAKE_TO_PROPOSE staked
158
+ 2. Proposal must be properly signed
159
+ 3. Proposal fee is burned
160
+
161
+ Returns: (success, message, nep_id)
162
+ """
163
+ # Check stake requirement
164
+ if self.ledger:
165
+ stake = self.ledger.get_local_stake(node_id)
166
+ if stake < self.MIN_STAKE_TO_PROPOSE:
167
+ return False, f"Insufficient stake to propose: {stake:.2f} < {self.MIN_STAKE_TO_PROPOSE}", None
168
+
169
+ # Verify signature
170
+ # (In production, verify with ECDSA)
171
+ if not signature:
172
+ return False, "Proposal must be signed", None
173
+
174
+ # Allocate official NEP ID
175
+ nep_id = self._allocate_nep_id()
176
+ nep.nep_id = nep_id
177
+ nep.status = NEPStatus.DRAFT
178
+ nep.signature = signature
179
+ nep.updated_at = time.time()
180
+
181
+ # Store in database
182
+ with self.lock:
183
+ with sqlite3.connect(self.db_path, timeout=60.0) as conn:
184
+ conn.execute("""
185
+ INSERT INTO neps
186
+ (nep_id, nep_type, status, title, author_node_id,
187
+ content_hash, created_at, updated_at, full_data)
188
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
189
+ """, (
190
+ nep.nep_id,
191
+ nep.nep_type.value,
192
+ nep.status.value,
193
+ nep.title,
194
+ nep.author_node_id,
195
+ nep.content_hash,
196
+ nep.created_at,
197
+ nep.updated_at,
198
+ json.dumps(nep.to_dict()),
199
+ ))
200
+
201
+ logger.info(f"Submitted proposal: {nep.summary()}")
202
+
203
+ # Burn proposal fee
204
+ if self.ledger:
205
+ # TODO: Implement fee burn
206
+ pass
207
+
208
+ return True, f"Proposal submitted as {nep_id}", nep_id
209
+
210
+ def get_proposal(self, nep_id: str) -> Optional[NEP]:
211
+ """Retrieve a proposal by ID."""
212
+ with sqlite3.connect(self.db_path, timeout=60.0) as conn:
213
+ row = conn.execute(
214
+ "SELECT full_data FROM neps WHERE nep_id = ?",
215
+ (nep_id,)
216
+ ).fetchone()
217
+
218
+ if not row:
219
+ return None
220
+
221
+ return NEP.from_dict(json.loads(row[0]))
222
+
223
+ def update_status(
224
+ self,
225
+ nep_id: str,
226
+ new_status: NEPStatus,
227
+ voting_start: float = None,
228
+ voting_end: float = None,
229
+ activation_block: int = None,
230
+ ) -> bool:
231
+ """Update a proposal's status."""
232
+ with self.lock:
233
+ with sqlite3.connect(self.db_path, timeout=60.0) as conn:
234
+ # Get current NEP
235
+ row = conn.execute(
236
+ "SELECT full_data FROM neps WHERE nep_id = ?",
237
+ (nep_id,)
238
+ ).fetchone()
239
+
240
+ if not row:
241
+ return False
242
+
243
+ nep = NEP.from_dict(json.loads(row[0]))
244
+ nep.status = new_status
245
+ nep.updated_at = time.time()
246
+
247
+ if voting_start:
248
+ nep.voting_start = voting_start
249
+ if voting_end:
250
+ nep.voting_end = voting_end
251
+ if activation_block:
252
+ nep.activation_block = activation_block
253
+
254
+ conn.execute("""
255
+ UPDATE neps SET
256
+ status = ?,
257
+ voting_start = ?,
258
+ voting_end = ?,
259
+ activation_block = ?,
260
+ updated_at = ?,
261
+ full_data = ?
262
+ WHERE nep_id = ?
263
+ """, (
264
+ new_status.value,
265
+ nep.voting_start,
266
+ nep.voting_end,
267
+ nep.activation_block,
268
+ nep.updated_at,
269
+ json.dumps(nep.to_dict()),
270
+ nep_id,
271
+ ))
272
+
273
+ logger.info(f"Updated {nep_id} status to {new_status.value}")
274
+ return True
275
+
276
+ def start_voting(
277
+ self,
278
+ nep_id: str,
279
+ voting_duration_days: int = 7,
280
+ ) -> Tuple[bool, str]:
281
+ """Start the voting period for a proposal."""
282
+ nep = self.get_proposal(nep_id)
283
+ if not nep:
284
+ return False, "Proposal not found"
285
+
286
+ if nep.status not in [NEPStatus.DRAFT, NEPStatus.REVIEW]:
287
+ return False, f"Cannot start voting from status: {nep.status.value}"
288
+
289
+ voting_start = time.time()
290
+ voting_end = voting_start + (voting_duration_days * 24 * 3600)
291
+
292
+ self.update_status(
293
+ nep_id,
294
+ NEPStatus.VOTING,
295
+ voting_start=voting_start,
296
+ voting_end=voting_end,
297
+ )
298
+
299
+ return True, f"Voting started, ends in {voting_duration_days} days"
300
+
301
+ def list_proposals(
302
+ self,
303
+ status: NEPStatus = None,
304
+ nep_type: NEPType = None,
305
+ limit: int = 100,
306
+ ) -> List[NEP]:
307
+ """List proposals with optional filtering."""
308
+ with sqlite3.connect(self.db_path, timeout=60.0) as conn:
309
+ query = "SELECT full_data FROM neps WHERE 1=1"
310
+ params = []
311
+
312
+ if status:
313
+ query += " AND status = ?"
314
+ params.append(status.value)
315
+
316
+ if nep_type:
317
+ query += " AND nep_type = ?"
318
+ params.append(nep_type.value)
319
+
320
+ query += " ORDER BY created_at DESC LIMIT ?"
321
+ params.append(limit)
322
+
323
+ rows = conn.execute(query, params).fetchall()
324
+
325
+ return [NEP.from_dict(json.loads(row[0])) for row in rows]
326
+
327
+ def get_active_config(self) -> Dict[str, any]:
328
+ """
329
+ Get the current active configuration based on all ACTIVE NEPs.
330
+
331
+ This is what nodes use to determine current protocol parameters.
332
+ """
333
+ config = {}
334
+
335
+ active_neps = self.list_proposals(status=NEPStatus.ACTIVE)
336
+
337
+ # Sort by activation order (older first)
338
+ active_neps.sort(key=lambda n: n.activation_block or 0)
339
+
340
+ # Apply parameter changes in order
341
+ for nep in active_neps:
342
+ for change in nep.parameter_changes:
343
+ key = f"{change.module}.{change.parameter}"
344
+ config[key] = change.new_value
345
+
346
+ return config
347
+
348
+ def to_gossip_dict(self, nep_id: str) -> Optional[Dict]:
349
+ """Serialize a NEP for gossip protocol."""
350
+ nep = self.get_proposal(nep_id)
351
+ if not nep:
352
+ return None
353
+ return nep.to_dict()
354
+
355
+ def from_gossip_dict(self, data: Dict) -> bool:
356
+ """
357
+ Receive a NEP from gossip and store it.
358
+
359
+ Returns True if the NEP was new/updated.
360
+ """
361
+ try:
362
+ incoming_nep = NEP.from_dict(data)
363
+
364
+ existing = self.get_proposal(incoming_nep.nep_id)
365
+
366
+ if existing:
367
+ # Only update if incoming is newer
368
+ if incoming_nep.updated_at > existing.updated_at:
369
+ with self.lock:
370
+ with sqlite3.connect(self.db_path, timeout=60.0) as conn:
371
+ conn.execute("""
372
+ UPDATE neps SET
373
+ status = ?,
374
+ voting_start = ?,
375
+ voting_end = ?,
376
+ activation_block = ?,
377
+ updated_at = ?,
378
+ full_data = ?
379
+ WHERE nep_id = ?
380
+ """, (
381
+ incoming_nep.status.value,
382
+ incoming_nep.voting_start,
383
+ incoming_nep.voting_end,
384
+ incoming_nep.activation_block,
385
+ incoming_nep.updated_at,
386
+ json.dumps(incoming_nep.to_dict()),
387
+ incoming_nep.nep_id,
388
+ ))
389
+ return True
390
+ return False
391
+ else:
392
+ # New NEP
393
+ with self.lock:
394
+ with sqlite3.connect(self.db_path, timeout=60.0) as conn:
395
+ conn.execute("""
396
+ INSERT INTO neps
397
+ (nep_id, nep_type, status, title, author_node_id,
398
+ content_hash, created_at, updated_at,
399
+ voting_start, voting_end, activation_block, full_data)
400
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
401
+ """, (
402
+ incoming_nep.nep_id,
403
+ incoming_nep.nep_type.value,
404
+ incoming_nep.status.value,
405
+ incoming_nep.title,
406
+ incoming_nep.author_node_id,
407
+ incoming_nep.content_hash,
408
+ incoming_nep.created_at,
409
+ incoming_nep.updated_at,
410
+ incoming_nep.voting_start,
411
+ incoming_nep.voting_end,
412
+ incoming_nep.activation_block,
413
+ json.dumps(incoming_nep.to_dict()),
414
+ ))
415
+
416
+ logger.info(f"Received new NEP via gossip: {incoming_nep.summary()}")
417
+ return True
418
+
419
+ except Exception as e:
420
+ logger.error(f"Failed to process gossip NEP: {e}")
421
+ return False
422
+
423
+
424
+ def finalize_proposal(
425
+ self,
426
+ nep_id: str,
427
+ approved: bool,
428
+ quorum_reached: bool,
429
+ total_stake_voted: float = 0.0,
430
+ ) -> Tuple[bool, str, float]:
431
+ """
432
+ Finalize a proposal after voting ends.
433
+
434
+ Rewards are STAKE-PROPORTIONAL:
435
+ - Approved: reward = 0.1% of total_stake_voted + fee refund
436
+ - Rejected w/ quorum: reward = 0.01% of total_stake_voted (community engaged)
437
+ - Rejected w/o quorum: Fee burned (nobody cared = spam)
438
+
439
+ This means:
440
+ - A proposal that engaged 100K NEURO of stake → ~100 NEURO reward
441
+ - A proposal that engaged 1M NEURO of stake → ~1000 NEURO reward (capped)
442
+ - More engagement = more reward = incentive for quality proposals
443
+
444
+ Returns: (success, message, reward_amount)
445
+ """
446
+ nep = self.get_proposal(nep_id)
447
+ if not nep:
448
+ return False, "Proposal not found", 0.0
449
+
450
+ if nep.status != NEPStatus.VOTING:
451
+ return False, f"Cannot finalize from status: {nep.status.value}", 0.0
452
+
453
+ reward = 0.0
454
+
455
+ if approved:
456
+ # Proposal passed! Reward proportional to engagement
457
+ new_status = NEPStatus.APPROVED
458
+
459
+ # Calculate stake-proportional reward
460
+ base_reward = total_stake_voted * self.PROPOSER_REWARD_RATE_APPROVED
461
+ base_reward = max(base_reward, self.PROPOSER_REWARD_MIN) # At least minimum
462
+ base_reward = min(base_reward, self.PROPOSER_REWARD_MAX) # Cap at maximum
463
+
464
+ reward = base_reward + self.PROPOSAL_FEE # Plus fee refund
465
+
466
+ message = (
467
+ f"Approved! Proposer earns {reward:.2f} NEURO "
468
+ f"({base_reward:.2f} reward + {self.PROPOSAL_FEE} fee refund, "
469
+ f"based on {total_stake_voted:.0f} NEURO stake participation)"
470
+ )
471
+
472
+ # Credit reward to author
473
+ if self.ledger:
474
+ try:
475
+ self.ledger._credit_local_balance(
476
+ nep.author_node_id,
477
+ reward,
478
+ f"NEP approved: {nep_id} (stake participation: {total_stake_voted:.0f})"
479
+ )
480
+ except Exception as e:
481
+ logger.error(f"Failed to credit proposer reward: {e}")
482
+
483
+ elif quorum_reached:
484
+ # Rejected but community engaged - smaller proportional reward
485
+ new_status = NEPStatus.REJECTED
486
+
487
+ # Smaller rate for rejected proposals, but still rewards engagement
488
+ reward = total_stake_voted * self.PROPOSER_REWARD_RATE_QUORUM
489
+ reward = min(reward, self.PROPOSER_REWARD_MAX * 0.1) # Cap at 10% of max
490
+
491
+ message = (
492
+ f"Rejected (quorum reached). Proposer receives {reward:.2f} NEURO "
493
+ f"(based on {total_stake_voted:.0f} NEURO stake participation)"
494
+ )
495
+
496
+ if self.ledger and reward > 0:
497
+ try:
498
+ self.ledger._credit_local_balance(
499
+ nep.author_node_id,
500
+ reward,
501
+ f"NEP rejected (quorum): {nep_id}"
502
+ )
503
+ except Exception as e:
504
+ logger.error(f"Failed to credit partial refund: {e}")
505
+ else:
506
+ # No quorum - fee burned (anti-spam)
507
+ new_status = NEPStatus.REJECTED
508
+ reward = 0.0
509
+ message = f"Rejected (no quorum). Fee of {self.PROPOSAL_FEE} NEURO burned."
510
+ logger.info(f"Burned {self.PROPOSAL_FEE} NEURO for rejected proposal {nep_id}")
511
+
512
+ # Update status
513
+ self.update_status(nep_id, new_status)
514
+
515
+ logger.info(f"Finalized {nep_id}: {message}")
516
+ return True, message, reward
517
+
518
+ @staticmethod
519
+ def calculate_expected_reward(total_stake_voted: float, approved: bool = True) -> float:
520
+ """
521
+ Calculate expected reward for a proposal based on stake participation.
522
+
523
+ Useful for UI to show "potential reward" before voting ends.
524
+ """
525
+ if approved:
526
+ reward = total_stake_voted * NEPRegistry.PROPOSER_REWARD_RATE_APPROVED
527
+ reward = max(reward, NEPRegistry.PROPOSER_REWARD_MIN)
528
+ reward = min(reward, NEPRegistry.PROPOSER_REWARD_MAX)
529
+ return reward + NEPRegistry.PROPOSAL_FEE
530
+ else:
531
+ reward = total_stake_voted * NEPRegistry.PROPOSER_REWARD_RATE_QUORUM
532
+ return min(reward, NEPRegistry.PROPOSER_REWARD_MAX * 0.1)
533
+
534
+
535
+ # Convenience functions
536
+ def get_active_neps(registry: NEPRegistry) -> List[NEP]:
537
+ """Get all currently active NEPs."""
538
+ return registry.list_proposals(status=NEPStatus.ACTIVE)
539
+
540
+
541
+ def get_pending_neps(registry: NEPRegistry) -> List[NEP]:
542
+ """Get all NEPs pending vote or activation."""
543
+ voting = registry.list_proposals(status=NEPStatus.VOTING)
544
+ scheduled = registry.list_proposals(status=NEPStatus.SCHEDULED)
545
+ return voting + scheduled