A user could bet and then double-spend the 1-conference deposit that funded the bet with a private chain: the bet left the chain but its participant stayed `confirmed`, and the payout failed "insufficient pool UTXOs" for ever with everyone else's money stuck. Coinbase outputs were also credited at 1 confirmation (maturity is 120). Deposits - `round_config.deposit_confirmations` (default 6, 1-100, editable from /admin) is the threshold for a deposit; an output of a platform transaction (any txid of a pending_transactions row, RBF versions included) keeps 1; a coinbase output needs 120, recognised from the raw transaction, which must hash to the txid (`is_coinbase`, cached). `read_deposit_policy` / `classify_new_deposits` run between the read and the write, so no session is open across the lookup. - The quorum must also place the coin deep enough on our own tip (ELC-17). - Confirmed outputs short of their threshold are pending deposits (memory only, replaced at every refresh): shown in `pending_balance_sats` / `has_pending` (also for DEP-13's withdrawal message), never spendable. No scripthash notification announces a maturing deposit, so each new block refreshes only the users that have one, one task per user (DEP-09 now says so). Payout - `_trigger_payout` verifies the round's confirmed participants' bet outputs with the other servers (`verify_pool_outpoints`: strict majority of >= 2 answers per outpoint, a mempool-only coin or a tie gives no verdict) and builds from exactly those outpoints (RND-22), never from whatever else sits at the address. No quorum: `payout_failed`, `pool verification unavailable`, nothing changes. Bets that are gone become `reorged_out`, one `participant_reorged_out` row each, `pool_amount_sats` rewritten (only downward) in the same transaction, and the 70/30 split is computed on what is left. - Winner's own bet gone: nothing is built; `rounds.payout_blocked_at` (Alembic f2c64a8d1e93, with `deposit_confirmations`, plain add_column) is set, `payout_blocked` and the pause with `paused_reason = 'payout_blocked'` are written in one transaction; `POST /admin/resume` clears the mark and the next attempt verifies again. /admin/rounds returns it. - Rounds opened before B-80 (pool = the shared reserve address) keep the old active-server `listunspent` choice, since that address holds no single round's bets. Ambiguities resolved: the winner's row stays `confirmed` while held (AUD-45 says every participant whose bet is gone becomes `reorged_out`, but OPS-30/DAT-68 need the verification to repeat after the resume, which only a `confirmed` row allows); `/`'s deposit hint no longer says "1 confirmation" (7 languages, same keys); guides updated. Tests changed because the spec changed: listener tests that credited height-100 coins at tip 0 now set a tip past every threshold; the payout fixtures seed confirmed participants (the new source of the inputs) instead of an active-server `listunspent`, FakeNetwork answers the verification, and test_round_pool_address asserts the quorum is asked about the round's address. New: thresholds at exactly 6/5, configured value, coinbase at 119/120 and a mismatching or missing raw tx, platform outputs at 1, dust, quorum height, waiting-deposit display and disappearance, block-triggered refresh, resume, verification verdicts, gone bets excluded with the split recomputed, winner gone held and paused, held payouts untouched until the resume, re-verification after it, no quorum, a payout with 1 and with 400 participants against reserve+fee+dust, admin config bounds and migration. Mutation-checked: threshold 1, ignored coinbase, no height bound, no verification and no winner hold each fail tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
193 lines
8.3 KiB
HTML
193 lines
8.3 KiB
HTML
<!doctype html>
|
||
<html lang="it">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>PLM Lottery — Admin</title>
|
||
<link rel="icon" type="image/svg+xml" href="/logo.svg">
|
||
<link rel="stylesheet" href="/admin.css">
|
||
</head>
|
||
<body>
|
||
|
||
<section id="login-section">
|
||
<header>
|
||
<h1>PLM Lottery — Admin</h1>
|
||
<p>Accesso riservato</p>
|
||
</header>
|
||
|
||
<div class="card">
|
||
<label for="admin-token">Admin token</label>
|
||
<input id="admin-token" type="password" placeholder="valore di ADMIN_TOKEN" autofocus>
|
||
<button onclick="adminLogin()" id="login-btn">Accedi</button>
|
||
</div>
|
||
</section>
|
||
|
||
<div id="dashboard-section" class="hidden">
|
||
<nav class="navbar">
|
||
<span class="brand">PLM Lottery — Admin</span>
|
||
<span class="nav-tab active" id="nav-parametri" onclick="switchView('parametri')">Parametri</span>
|
||
<span class="nav-tab" id="nav-utenti" onclick="switchView('utenti')">Utenti</span>
|
||
<span class="nav-tab" id="nav-round" onclick="switchView('round')">Round</span>
|
||
<span class="nav-tab" id="nav-pending" onclick="switchView('pending')">Transazioni pendenti</span>
|
||
<span class="nav-tab" id="nav-audit" onclick="switchView('audit')">Audit log</span>
|
||
<span class="nav-tab" id="nav-bugreports" onclick="switchView('bugreports')">Segnalazioni bug</span>
|
||
<span class="spacer"></span>
|
||
<span class="chain-status-pill">
|
||
<span class="status-dot" id="chain-status-dot"></span>
|
||
<span id="chain-status-label">Connessione…</span>
|
||
</span>
|
||
<span class="chain-block mono" id="chain-block">Blocco —</span>
|
||
<button class="secondary" style="margin:8px 0 8px 14px" onclick="adminLogout()">Esci</button>
|
||
</nav>
|
||
|
||
<main>
|
||
|
||
<div class="view active" id="view-parametri">
|
||
<h2 class="section-title">Parametri</h2>
|
||
<p class="hint">Configurazione operativa, salvata nel database — modificabile in qualsiasi momento senza riavviare il server.</p>
|
||
|
||
<div class="card" id="maintenance-card">
|
||
<h2>Manutenzione</h2>
|
||
<p class="hint" id="maintenance-hint">
|
||
Interrompe l'apertura di nuovi round dopo quello in corso, senza troncare il round attuale — chiusura,
|
||
estrazione e pagamento del vincitore avvengono normalmente. Gli utenti vedono un avviso di manutenzione.
|
||
</p>
|
||
<div class="row-between">
|
||
<span class="chain-status-pill">
|
||
<span class="status-dot" id="maintenance-dot"></span>
|
||
<span id="maintenance-status-label">—</span>
|
||
</span>
|
||
<button id="maintenance-btn" class="secondary" style="width:auto;margin-top:0" onclick="toggleMaintenance()">…</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<!-- B-66: no fee_address means no round can open at all (the payout pays the
|
||
30% commission to it, so it cannot even be built). Shown here because
|
||
this is the one screen that can fix it. -->
|
||
<div class="warning-banner hidden" id="admin-fee-address-warning">
|
||
⚠️ Nessun fee address configurato: finché resta vuoto <strong>non si aprirà nessun round</strong>
|
||
(il payout non sarebbe costruibile). Impostalo qui sotto e salva.
|
||
</div>
|
||
<div class="grid-2">
|
||
<div>
|
||
<label for="admin-fee-address">Fee address (dove finisce il 30% di ogni round)</label>
|
||
<input id="admin-fee-address" class="mono" placeholder="plm1q...">
|
||
<label for="admin-bet-amount">Bet amount (PLM)</label>
|
||
<input id="admin-bet-amount" inputmode="decimal" placeholder="es. 10">
|
||
</div>
|
||
<div>
|
||
<label for="admin-round-duration">Durata round (secondi)</label>
|
||
<input id="admin-round-duration" inputmode="numeric" placeholder="es. 600">
|
||
<label for="admin-round-cooldown">Pausa tra un round e il successivo (secondi)</label>
|
||
<input id="admin-round-cooldown" inputmode="numeric" placeholder="es. 30">
|
||
<label for="admin-draw-animation">Durata animazione estrazione (secondi)</label>
|
||
<input id="admin-draw-animation" inputmode="numeric" placeholder="es. 20">
|
||
<label for="admin-draw-confirmations">Conferme sul blocco di estrazione (1–20, dal round successivo)</label>
|
||
<input id="admin-draw-confirmations" inputmode="numeric" placeholder="es. 3">
|
||
<label for="admin-deposit-confirmations">Conferme per accreditare un deposito (1–100; le uscite coinbase ne richiedono comunque 120)</label>
|
||
<input id="admin-deposit-confirmations" inputmode="numeric" placeholder="es. 6">
|
||
<label for="admin-fee-rate">Fee rate di rete (sat/vB)</label>
|
||
<input id="admin-fee-rate" inputmode="numeric" placeholder="es. 1">
|
||
<label for="admin-rbf-timeout">Timeout prima del fee-bump RBF (secondi)</label>
|
||
<input id="admin-rbf-timeout" inputmode="numeric" placeholder="es. 900">
|
||
</div>
|
||
</div>
|
||
<button onclick="adminSave()" id="save-btn">Salva</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="view-utenti">
|
||
<h2 class="section-title">Utenti</h2>
|
||
<p class="hint">Elenco utenti registrati, con saldo interno, accesso alla chiave privata per interventi manuali (es. restituire fondi bloccati) e reset password per chi resta bloccato fuori dall'account.</p>
|
||
|
||
<div class="warning-banner">
|
||
⚠ La chiave privata dà accesso completo ai fondi dell'utente: ogni visualizzazione viene registrata nell'audit log, non condividerla né salvarla altrove. La password esistente di un utente non è mai recuperabile (è salvata solo come hash Argon2) — "Reset" ne genera una nuova al posto della vecchia, anche questo audit-loggato.
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr><th>ID</th><th>Username</th><th>Indirizzo</th><th>Saldo (PLM)</th><th>Registrato</th><th>Chiave</th><th>Password</th></tr>
|
||
</thead>
|
||
<tbody id="users-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="view-round">
|
||
<h2 class="section-title">Round</h2>
|
||
<p class="hint">Ultimi round: stato, vincitore, importi e transazione di payout.</p>
|
||
|
||
<div class="card">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr><th>ID</th><th>Stato</th><th>Apertura</th><th>Vincitore</th><th>Pool (PLM)</th><th>Vincita (PLM)</th><th>Fee (PLM)</th><th>Payout txid</th></tr>
|
||
</thead>
|
||
<tbody id="rounds-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="view-pending">
|
||
<h2 class="section-title">Transazioni pendenti</h2>
|
||
<p class="hint">Bet, payout e prelievi non ancora confermati — candidati al fee-bump RBF se scade il timeout.</p>
|
||
|
||
<div class="card">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr><th>ID</th><th>Tipo</th><th>Stato</th><th>Txid</th><th>Fee rate</th><th>Tentativi</th><th>Trasmessa</th></tr>
|
||
</thead>
|
||
<tbody id="pending-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="view-audit">
|
||
<h2 class="section-title">Audit log</h2>
|
||
<p class="hint">Ultimi eventi registrati dal sistema (config, bet, payout, accessi a chiavi private, ecc.).</p>
|
||
|
||
<div class="card">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr><th>ID</th><th>Evento</th><th>Dettagli</th><th>Utente</th><th>Round</th><th>Quando</th></tr>
|
||
</thead>
|
||
<tbody id="audit-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="view" id="view-bugreports">
|
||
<h2 class="section-title">Segnalazioni bug</h2>
|
||
<p class="hint">Segnalazioni inviate dagli utenti tramite la pagina "Segnala un bug".</p>
|
||
|
||
<div class="card">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr><th>ID</th><th>Descrizione</th><th>Contatto</th><th>Utente</th><th>Quando</th><th>Stato</th></tr>
|
||
</thead>
|
||
<tbody id="bugreports-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</main>
|
||
</div>
|
||
|
||
<div id="toast-container" aria-live="polite"></div>
|
||
|
||
<script src="/admin.js"></script>
|
||
|
||
</body>
|
||
</html>
|