feat(admin): operations console, fees wired into payouts
Fees now flow through settlement. The payout and the deduction are posted as separate ledger transactions rather than netted, so a player's history shows the full win and the charge as itemised lines instead of a quietly smaller win. The admin console shows treasury, liability, revenue, every posting, every round, and risk flags. Auth is a constant-time token compare and the surface is not mounted at all unless ARCADE_ADMIN_TOKEN is set, so a default deployment has no admin endpoint to attack. The token lives in browser memory only. It is read-only over game outcomes by design: seeds show only after settlement and nothing can alter a crash point. A control that could would make the fairness proof a lie. The console immediately found a real bug: 343 unresolved rounds, because the reconciler only considered rounds with bets and abandoned empty ones accumulated forever, burying the signal. Now cleared automatically. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,8 @@ type Result struct {
|
||||
RoundsRefunded int
|
||||
BetsRefunded int
|
||||
MsatRefunded int64
|
||||
// EmptyRoundsClosed counts abandoned rounds that nobody had joined.
|
||||
EmptyRoundsClosed int
|
||||
}
|
||||
|
||||
// Run refunds every abandoned round it finds.
|
||||
@@ -81,6 +83,22 @@ func (rc *Reconciler) Run(ctx context.Context) (Result, error) {
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Rounds that nobody joined also need closing. They hold no money, so
|
||||
// there is nothing to refund, but leaving them open forever makes the
|
||||
// operator's "unresolved rounds" signal useless — it climbs steadily with
|
||||
// noise and stops meaning anything when a real one appears.
|
||||
empty, err := rc.pool.Exec(ctx, `
|
||||
UPDATE rounds SET voided_at = now()
|
||||
WHERE settled_at IS NULL
|
||||
AND voided_at IS NULL
|
||||
AND opened_at < now() - make_interval(secs => $1)
|
||||
AND NOT EXISTS (SELECT 1 FROM bets WHERE bets.round_id = rounds.id)`,
|
||||
rc.Stale.Seconds())
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("closing empty rounds: %w", err)
|
||||
}
|
||||
res.EmptyRoundsClosed = int(empty.RowsAffected())
|
||||
|
||||
for _, roundID := range roundIDs {
|
||||
refunded, msat, err := rc.refundRound(ctx, roundID)
|
||||
if err != nil {
|
||||
@@ -183,9 +201,11 @@ func (rc *Reconciler) RunPeriodically(ctx context.Context, every time.Duration)
|
||||
fmt.Printf("reconcile: %v\n", err)
|
||||
continue
|
||||
}
|
||||
if res.RoundsRefunded > 0 {
|
||||
fmt.Printf("reconcile: refunded %d bets across %d abandoned rounds (%d msat)\n",
|
||||
res.BetsRefunded, res.RoundsRefunded, res.MsatRefunded)
|
||||
if res.RoundsRefunded > 0 || res.EmptyRoundsClosed > 0 {
|
||||
fmt.Printf("reconcile: refunded %d bets across %d abandoned rounds "+
|
||||
"(%d msat); closed %d empty rounds\n",
|
||||
res.BetsRefunded, res.RoundsRefunded, res.MsatRefunded,
|
||||
res.EmptyRoundsClosed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user