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:
@@ -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 ----
|
||||
|
||||
Reference in New Issue
Block a user