feat: USDT reward system — billing ledger, devnet treasury, settlement loop, forfeiture PoW, tracker dashboard (US-030…US-035, ADR-0015)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dobromir Popov
2026-07-02 22:31:02 +02:00
parent 4d4ab607ca
commit 1e0aa6ea8f
24 changed files with 3168 additions and 994 deletions

View File

@@ -45,7 +45,12 @@ class BillingLedger:
self._lock = threading.Lock()
self._client_balances: dict[str, float] = {}
self._node_pending: dict[str, float] = {}
self._wallet_bindings: dict[str, str] = {} # client wallet pubkey -> api_key
self._protocol_cut: float = 0.0
self._pending_since: dict[str, float] = {} # wallet -> ts pending became > 0
self._last_payout_ts: dict[str, float] = {}
# settlement id -> {"payouts": [(wallet, amount)], "signature": str|None, "ts": float}
self._settlements: dict[str, dict] = {}
self._seen_event_ids: set[str] = set()
self._event_log: list[dict] = [] # full log, order of local application
self._dirty = False
@@ -144,6 +149,117 @@ class BillingLedger:
self._apply_locked(event)
return event
def bind_wallet(self, api_key: str, wallet: str) -> dict:
"""Bind a client wallet pubkey to an API key (US-032 deposits)."""
with self._lock:
event = {
"id": f"bind-{uuid.uuid4().hex}",
"type": "bind",
"api_key": api_key,
"wallet": wallet,
"ts": time.time(),
}
self._apply_locked(event)
return event
def api_key_for_wallet(self, wallet: str) -> str | None:
with self._lock:
return self._wallet_bindings.get(wallet)
def credit_deposit(self, api_key: str, amount: float, signature: str) -> dict | None:
"""Credit an on-chain deposit exactly once.
The event id embeds the transaction signature, so replayed or
re-observed transfers — locally or via hive gossip — are no-ops.
Returns None when the signature was already credited.
"""
if amount <= 0:
raise ValueError("deposit amount must be positive")
event_id = f"deposit-{signature}"
with self._lock:
if event_id in self._seen_event_ids:
return None
event = {
"id": event_id,
"type": "credit",
"api_key": api_key,
"amount": amount,
"signature": signature,
"ts": time.time(),
"note": "onchain-deposit",
}
self._apply_locked(event)
return event
def has_event(self, event_id: str) -> bool:
with self._lock:
return event_id in self._seen_event_ids
# ---- settlement (US-033) ----
def payables(
self,
*,
threshold: float,
max_period: float,
dust_floor: float,
now: float | None = None,
exclude: set[str] | None = None,
) -> list[tuple[str, float]]:
"""Wallets due a payout: pending ≥ threshold OR pending age ≥ max_period,
never below the dust floor (mining-pool standard, ADR-0015)."""
now = now if now is not None else time.time()
exclude = exclude or set()
due: list[tuple[str, float]] = []
with self._lock:
for wallet, pending in self._node_pending.items():
if wallet in exclude or pending < max(dust_floor, 1e-9):
continue
if pending >= threshold:
due.append((wallet, pending))
continue
since = self._pending_since.get(wallet)
if since is not None and now - since >= max_period:
due.append((wallet, pending))
return due
def confirm_settlement(self, settlement_id: str, signature: str) -> dict:
"""Record the on-chain transaction signature for a settlement batch."""
with self._lock:
event = {
"id": f"settlement-{settlement_id}",
"type": "settlement",
"settlement_id": settlement_id,
"signature": signature,
"ts": time.time(),
}
self._apply_locked(event)
return event
def unconfirmed_settlements(self) -> dict[str, list[tuple[str, float]]]:
"""Settlement batches whose pending was debited but whose transaction
has not been confirmed — resend these (idempotent by settlement id)."""
with self._lock:
return {
sid: list(s["payouts"])
for sid, s in self._settlements.items()
if s["signature"] is None and s["payouts"]
}
def settlement_history(self) -> list[dict]:
with self._lock:
return [
{
"settlement_id": sid,
"signature": s["signature"],
"payouts": [
{"wallet": w, "amount": a} for w, a in s["payouts"]
],
"ts": s["ts"],
}
for sid, s in sorted(self._settlements.items(), key=lambda kv: kv[1]["ts"])
]
def settle_node_payout(self, wallet: str, amount: float, *, reference: str = "") -> dict:
"""Deduct a paid-out amount from a node's pending balance (US-033 hook)."""
if amount <= 0:
@@ -203,15 +319,35 @@ class BillingLedger:
self._client_balances[key] = self._client_balances.get(key, 0.0) - float(event["cost"])
for wallet, amount in (event.get("shares") or {}).items():
self._node_pending[wallet] = self._node_pending.get(wallet, 0.0) + float(amount)
if amount > 0:
self._pending_since.setdefault(wallet, float(event.get("ts", time.time())))
self._protocol_cut += float(event.get("protocol_amount", 0.0))
elif etype == "payout":
wallet = event["wallet"]
self._node_pending[wallet] = self._node_pending.get(wallet, 0.0) - float(event["amount"])
amount = float(event["amount"])
ts = float(event.get("ts", time.time()))
self._node_pending[wallet] = self._node_pending.get(wallet, 0.0) - amount
self._pending_since.pop(wallet, None)
self._last_payout_ts[wallet] = ts
reference = event.get("reference") or ""
if reference:
settlement = self._settlements.setdefault(
reference, {"payouts": [], "signature": None, "ts": ts}
)
settlement["payouts"].append((wallet, amount))
elif etype == "settlement":
settlement = self._settlements.setdefault(
event["settlement_id"],
{"payouts": [], "signature": None, "ts": float(event.get("ts", 0.0))},
)
settlement["signature"] = event.get("signature")
elif etype == "forfeit":
wallet = event["wallet"]
amount = float(event.get("amount", 0.0))
self._node_pending[wallet] = self._node_pending.get(wallet, 0.0) - amount
self._protocol_cut += amount
elif etype == "bind":
self._wallet_bindings[event["wallet"]] = event["api_key"]
else:
return
self._seen_event_ids.add(event["id"])
@@ -233,11 +369,21 @@ class BillingLedger:
return {
"clients": dict(self._client_balances),
"node_pending": dict(self._node_pending),
"wallet_bindings": dict(self._wallet_bindings),
"protocol_cut": self._protocol_cut,
"prices": dict(self._prices),
"default_price_per_1k_tokens": self._default_price_per_1k,
"node_share": self._node_share,
"event_count": len(self._event_log),
"forfeits": [
{
"wallet": e["wallet"],
"amount": e.get("amount", 0.0),
"reason": e.get("reason", ""),
"ts": e.get("ts", 0.0),
}
for e in self._event_log if e.get("type") == "forfeit"
][-20:],
}
# ---- persistence ----