Only the duration and the cooldown (and, since B-73, the draw confirmations) were copied onto the round when it opened. The cost of a bet and the fee address were read live, so an edit from /admin in the middle of a round gave the players of that round different stakes for the same chance and redirected the commission of the round in progress. - `rounds.bet_amount_sats`, `rounds.fee_address` and `rounds.max_round_pool_sats` are copied from round_config in the transaction that opens the round (Alembic a7d3e9f15b48: plain add_column, existing rounds backfilled with the current configuration, i.e. what they were using; the expression indexes untouched). `place_bet` charges the round's cost, `_trigger_payout` pays the round's fee address (an empty one on the row still stops the payout, as before), `GET /rounds/current` advertises the round's cost while one is active and the configured one otherwise. The minimum withdrawal and the fee rate stay live, by design (ADR-20, ADR-10). - `round_config.max_round_pool_sats` (0 = none, otherwise >= the bet cost and <= 4e15, in /admin and PUT/GET /admin/config): a bet is refused with `round_full` (params also carry `max_round_pool_sats`) once the sum of the round's participant rows, in-flight ones included, has reached the round's cap, before anything moves; the pool can exceed it by one bet at most. A cap under the bet cost is a 422 `max_round_pool_below_bet_amount`. - PUT /admin/config's payability check (RND-40) now also judges a fee-rate change against the bet cost the round in progress opened with, which can be lower than the configured one. - `error.round_full` no longer says "maximum of N players" (it also fires for the pool cap): the seven translations are generic. Ambiguity resolved: the spec said the cap is snapshotted (RND-05, BET-20, ARC) in some places and "applies immediately, also to the round in progress" in the round_config table of modello-dati; the snapshot wins, with a column on `rounds` (modello-dati/api/guide-admin brought in line). Tests changed because the spec changed: the payout fixtures seed the round's own fee address, and the old "fee address missing" payout test now empties the round's address (clearing it in the configuration mid-round must not stop the payout). New test_round_snapshot.py: the copy at open, edits reaching only the next round, every bet of a round paying the same stake whether the cost is raised or lowered meanwhile, the payout following the round's address (and surviving it being cleared in the configuration), the cap (exactly at the cap is full, in-flight bets count, no cap, a mid-round edit of the cap does not apply, the refused player's coins untouched), plus the route, the admin bounds and the two 422s, the fee-rate check against the round's own cost, and the migration backfill. Mutation-checked: charging the live cost, paying the live address, not copying the cap and `>` instead of `>=` each fail tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
64 KiB
API HTTP
Specifica dell'interfaccia HTTP di PLM Lottery: convenzioni comuni, contratto degli errori, ogni endpoint esposto e il catalogo completo dei codici di errore. Descrive la forma delle richieste e delle risposte; il comportamento di dominio che un endpoint innesca (come si costruisce una scommessa, come si chiude un round…) è specificato nel documento del suo argomento, a cui ogni sezione rimanda:
| Argomento | Documento |
|---|---|
| Registrazione, depositi, saldo mostrato | registrazione-e-depositi.md |
| Scommesse e prelievi | scommesse-e-prelievi.md |
| Round, estrazione, pagamento | ciclo-di-vita-del-round.md |
| Ciclo di vita delle transazioni, RBF | transazioni-e-riconciliazione.md |
| Chiavi, JWT, limiti di frequenza, header di sicurezza | sicurezza.md |
| Eventi del registro di audit | audit-log.md |
| Tabelle e campi | modello-dati.md |
| Frontend, canale SSE lato architettura, i18n | architettura.md |
Le due interfacce web (/ e /admin) parlano con questa stessa API JSON: non
esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
Panoramica degli endpoint
| Metodo | Percorso | Autenticazione | Requisito |
|---|---|---|---|
POST |
/auth/register |
nessuna | API-18 |
POST |
/auth/login |
nessuna | API-19 |
GET |
/users/me |
Bearer | API-20 |
POST |
/users/me/change-password |
Bearer | API-21 |
GET |
/users/me/last-round-result |
Bearer | API-22 |
POST |
/bets |
Bearer | API-23 |
POST |
/withdrawals |
Bearer | API-24 |
GET |
/rounds/current |
facoltativa | API-25 |
GET |
/rounds/stream |
nessuna | API-26 |
GET |
/rounds/{round_id}/proof |
nessuna | API-45 |
GET |
/qr/{address} |
nessuna | API-27 |
POST |
/bug-reports |
facoltativa | API-28 |
GET |
/bug-reports/mine |
Bearer | API-29 |
GET |
/admin/config |
admin | API-30 |
PUT |
/admin/config |
admin | API-31 |
POST |
/admin/pause |
admin | API-32 |
POST |
/admin/resume |
admin | API-32 |
GET |
/admin/users |
admin | API-33 |
GET |
/admin/users/{user_id}/privkey |
admin | API-34 |
POST |
/admin/users/{user_id}/reset-password |
admin | API-35 |
GET |
/admin/rounds |
admin | API-36 |
GET |
/admin/audit-log |
admin | API-37 |
GET |
/admin/pending-transactions |
admin | API-38 |
GET |
/admin/bug-reports |
admin | API-39 |
POST |
/admin/bug-reports/{report_id}/status |
admin | API-40 |
GET |
/admin/electrum |
admin | API-47 — da implementare (B-86, B-92) |
GET |
/health |
nessuna | API-41 |
GET |
/health/ready |
nessuna | API-46 — da implementare (B-92) |
GET |
/, /admin, /guida, /report-bug |
nessuna | API-42 |
GET |
qualunque altro percorso (file statici) | nessuna | API-43 |
Convenzioni generali
-
[API-01] L'API è servita dallo stesso processo e sulla stessa origine delle interfacce web: in produzione
https://<SITE_ADDRESS>/…attraverso Caddy (che termina TLS e inoltra al containerappsulla porta interna 8123), senza prefisso di versione (/v1…) e senza CORS — non è installato alcun middleware CORS, quindi un browser può chiamarla solo dalla stessa origine. I percorsi sono quelli della tabella sopra, alla radice. Codice:app/main.py(include_router),Caddyfile,Dockerfile -
[API-02] Corpi di richiesta e di risposta sono JSON (
application/json), tranne tre eccezioni:GET /qr/{address}rispondeimage/png,GET /rounds/streamrispondetext/event-stream, e le pagine/i file statici rispondono con il loro tipo. I campi dei corpi sono validati da modelli Pydantic; i campi non previsti vengono ignorati in silenzio (comportamento predefinito di Pydantic), non rifiutati. Codice:app/api/routes/(modelli*Request/*Responsedi ogni file) -
[API-03] Tutti gli importi viaggiano come interi in satoshi (1 PLM = 100 000 000 sat), in campi il cui nome termina in
_sats. L'API non espone mai importi in PLM né valori decimali; la conversione per la visualizzazione è compito del client. Codice:app/api/routes/·app/static/app.js:errorParams -
[API-04] Ogni istante nelle risposte è una stringa ISO 8601 in UTC con offset esplicito
+00:00(ad esempio2026-09-28T10:15:00.123456+00:00). SQLite restituisce datetime privi di fuso: prima della serializzazione vengono marcati come UTC, cosìnew Date()nel browser non li interpreta come ora locale.server_timediGET /rounds/currentè generato direttamente con fuso UTC e ha lo stesso formato. Codice:app/api/timeutil.py:isoformat_utc· Test:tests/unit/test_api_timeutil.py,tests/unit/test_users.py -
[API-05] Esistono quattro modalità di autenticazione, fissate per endpoint (colonna "Autenticazione" della panoramica):
Modalità Come si presenta Se manca o è invalida Bearer ( get_current_user)header Authorization: Bearer <JWT>401facoltativa ( get_optional_user)lo stesso header, se c'è la richiesta prosegue come anonima admin ( require_admin)header X-Admin-Token: <ADMIN_TOKEN>403nessuna — — Le regole di emissione e verifica del JWT (algoritmo, durata, claim
tv, revoca al cambio password) sono in sicurezza.md; qui conta cosa vede il client. Codice:app/auth/dependencies.py:get_current_user,app/auth/dependencies.py:get_optional_user,app/api/routes/admin.py:require_admin -
[API-06] Autenticazione Bearer: il token è quello restituito da
/auth/register,/auth/logino/users/me/change-password. Un token illeggibile, scaduto o con firma errata, un utente non più esistente e un token superato (emesso prima dell'ultimo cambio o reset della password) rispondono tutti401con codicesession_expired. L'assenza dell'header (o uno schema diverso daBearer) risponde401con codicenot_authenticated, nel formato strutturato di [API-12], con headerWWW-Authenticate: Bearer. Il frontend tratta i due codici allo stesso modo: torna alla schermata di login. Codice:app/auth/dependencies.py:get_current_user· Test:tests/unit/test_users.py,tests/unit/test_bug_reports.pyStato: da implementare (B-94), per l'header assente · Oggi: l'assenza dell'header è intercettata da FastAPI prima del codice applicativo e risponde con il corpo non strutturato{"detail": "Not authenticated"}. -
[API-07] Autenticazione facoltativa: senza header, con un header che non inizia con
Bearer, con un token invalido o superato, la richiesta è trattata come anonima — mai un errore. Serve agli endpoint raggiungibili anche da non loggati che personalizzano la risposta se l'utente è noto (GET /rounds/current,POST /bug-reports). Codice:app/auth/dependencies.py:get_optional_user· Test:tests/unit/test_rounds_route.py,tests/unit/test_bug_reports.py -
[API-08] Autenticazione admin: l'header
X-Admin-Tokenè confrontato a tempo costante (sui byte UTF-8, così un valore non ASCII non provoca un500) conADMIN_TOKENdel.env. UnADMIN_TOKENvuoto nega tutto, anche un header vuoto. Il rifiuto è403con codiceinvalid_admin_token, nel formato strutturato di [API-12]. Non c'è identità per singolo operatore (vedi sicurezza.md). Codice:app/api/routes/admin.py:require_admin· Test:tests/unit/test_admin.pyStato: da implementare (B-94) · Oggi:403con il corpo non strutturato{"detail": "invalid admin token"}. -
[API-09] Intestazioni di cache:
- le quattro pagine (
/,/admin,/guida,/report-bug) rispondono conCache-Control: no-store; - il flusso SSE risponde con
Cache-Control: no-storeeX-Accel-Buffering: no; - il QR risponde con
Cache-Control: private, max-age=86400, immutable; - ogni file servito dal mount dei file statici (
/app.js,/style.css,/i18n.js,/admin.js,/admin.css,/index.html…) risponde conCache-Control: no-cache, oltre aETag/Last-Modified: il browser può tenerne una copia ma la rivalida a ogni uso, così dopo un aggiornamento del server non esegue unapp.jsvecchio contro l'API nuova; - le risposte JSON non hanno
Cache-Control. Codice:app/main.py:index_page,app/main.py:admin_panel,app/main.py:user_guide,app/main.py:report_bug_page,app/api/routes/rounds.py:round_stream,app/api/routes/qr.py:address_qr,app/main.py(app.mount) Stato: da implementare (B-94), per i file statici · Oggi: i file serviti dal mount non hannoCache-Control, soloETag/Last-Modified, e il browser può riusarli senza rivalidarli.
- le quattro pagine (
-
[API-10] La documentazione automatica di FastAPI (
/docs,/redoc,/openapi.json) è disattivata a meno cheENABLE_API_DOCSnon sia impostato a vero nel.env: elencherebbe a chiunque l'intera superficie dell'API, endpoint admin compresi. Disattivata, quei percorsi rispondono404. Codice:app/main.py(costruttoreFastAPI),app/config.py:Settings· Test:tests/unit/test_docs_disabled.py -
[API-11] Dove un controllo dipende dall'indirizzo IP del chiamante (limiti di login e registrazione, tetto per IP del flusso SSE e, da implementare con B-94, quota per IP di
POST /bug-reports), l'IP è l'ultimo elemento diX-Forwarded-For(quello scritto dal proxy fidato), o in sua assenza l'indirizzo della connessione, o"unknown". Il motivo e il ruolo di Caddy sono in sicurezza.md. Codice:app/api/client_ip.py:client_ip· Test:tests/unit/test_client_ip.py
Contratto degli errori
-
[API-12] Ogni errore voluto dal codice applicativo risponde con un
detailstrutturato:{ "detail": { "code": "insufficient_balance", "message": "insufficient balance", "params": {"required_sats": 1000000000} } }code— identificatore stabile, insnake_case; è ciò che il client usa.message— testo in inglese per client che non conoscono i codici (curl, test); l'API è monolingua per scelta.params— i valori che il messaggio interpola (importi come*_sats, limiti, attese), sempre presente, eventualmente{}.
Gli errori nascono come
ApiError(o sue sottoclassiBetError,WithdrawalError) nei servizi, oppure direttamente conhttp_errornelle route. Codice:app/api/errors.py:ApiError,app/api/errors.py:http_error,app/api/errors.py:from_api_error -
[API-13] Gli errori di
POST /betsePOST /withdrawalssollevati dal servizio rispondono400, trannebroadcast_failed, che risponde502(è la rete ad aver rifiutato la transazione, non il chiamante), eservice_busy, che risponde503(il database non ha concesso il lock di scrittura entro il timeout, DAT-74; nulla è stato costruito né trasmesso).broadcast_failedsi usa solo per un rifiuto esplicito del server (un errore JSON-RPC in risposta al broadcast), e significa che tutto è stato annullato. Un esito ambiguo del broadcast (timeout, connessione caduta dopo l'invio) non è un errore: la richiesta risponde202 Acceptedcon lo stesso corpo del successo estatus: "building", perché la transazione può essere già sulla rete e la riga resta al riconciliatore (scommesse-e-prelievi.md B-76). Un rifiuto "già noto / già nel mempool" vale come successo (201). Codice:app/api/routes/bets.py:create_bet,app/api/routes/withdrawals.py:create_withdrawal· Test:tests/unit/test_bets.py,tests/unit/test_broadcast_outcome.py -
[API-14] Qualunque eccezione non gestita risponde
500con lo stesso formato strutturato, codiceinternal_error, messaggio"internal server error",paramsvuoto. Il testo e lo stack dell'eccezione finiscono solo nel log (logs/app.log), mai nella risposta. Codice:app/main.py:log_unhandled_exception· Test:tests/unit/test_rounds_route.py -
[API-15] Un corpo, un parametro di percorso o di query che non supera la validazione risponde
422nel formato strutturato di [API-12], con codiceinvalid_request,message"invalid request"e inparams.errorsla lista degli errori di validazione, uno per campo invalido, ciascuno{"loc", "msg", "type"}in inglese (anche il JSON malformato arriva così, contype: "json_invalid"). Il valore ricevuto (input) non è ripetuto nella risposta, così una password troppo lunga non torna indietro in chiaro. Codice:app/main.py(gestore diRequestValidationError, da scrivere),app/static/app.js:apiErrorMessage· Test:tests/unit/test_users.py,tests/unit/test_admin.py,tests/unit/test_bug_reports.pyStato: da implementare (B-94) · Oggi:422nel formato nativo di FastAPI:detailè una lista di oggetti{type, loc, msg, input, ctx?}, senzacode; il frontend riconosce la lista e mostra il messaggio generico tradottoerror.invalid_request. -
[API-16] Nessuna risposta d'errore ha
detailcome semplice stringa: anche gli errori generati dagli endpoint admin, dal flusso SSE e dal framework seguono il formato strutturato di [API-12], con questi codici:Stato Codice atteso Dove Oggi ( detailstringa)401not_authenticatedheader Authorizationassente su un endpoint Bearer ([API-06])"Not authenticated"403invalid_admin_tokenogni endpoint /admin/*([API-08])"invalid admin token"404user_not_foundGET /admin/users/{id}/privkey,POST /admin/users/{id}/reset-password"user not found"404bug_report_not_foundPOST /admin/bug-reports/{id}/status"bug report not found"404not_foundpercorso inesistente (anche un metodo GETsu un percorso che esiste solo inPOST, perché lo intercetta il mount dei file statici)"Not Found"405method_not_allowedmetodo diverso da GET/HEADsu un percorso non definito da un router"Method Not Allowed"503too_many_streamsGET /rounds/streamoltre il tetto globale"too many concurrent update streams"I
404/405del framework si ottengono con un gestore diStarletteHTTPExceptionche traduce lo stato nel codice della tabella. Codice:app/api/routes/admin.py:require_admin,app/api/routes/admin.py:user_privkey,app/api/routes/admin.py:reset_user_password,app/api/routes/admin.py:update_bug_report_status,app/api/routes/rounds.py:round_stream,app/main.py· Test:tests/unit/test_admin.py,tests/unit/test_bug_reports.pyStato: da implementare (B-94) · Oggi: tutte le risposte della tabella hannodetailcome stringa inglese (colonna "Oggi"), e il frontend la mostra così com'è. -
[API-17] Il client traduce
codecon la chiaveerror.<code>diapp/static/i18n.js, passandoparamsall'interpolazione e aggiungendo per ogni parametro numerico<nome>_satsun gemello<nome>_plmgià formattato in PLM; se la chiave non esiste mostramessage. Ogni codice che l'API può restituire deve avere la sua chiaveerror.<code>in tutte e sette le lingue (en, it, es, fr, de, ru, zh). Un401con codicesession_expiredriporta inoltre il frontend alla schermata di login. Codice:app/static/app.js:apiErrorMessage,app/static/app.js:errorParams,app/static/i18n.js:TRANSLATIONS
Endpoint
Autenticazione
POST /auth/register
- [API-18] Crea un account e restituisce subito una sessione.
-
Autenticazione: nessuna.
-
Corpo:
Campo Tipo Vincoli usernamestringa 3–32 caratteri, solo A-Z a-z 0-9 _ . -passwordstringa 8–256 caratteri ( MIN_PASSWORD_LENGTH= 8) -
Risposta
201:{"access_token": <JWT>, "address": <indirizzo plm1…>}— l'indirizzo di deposito permanente dell'utente. -
Errori:
422(validazione del corpo);429 rate_limited(quota IP esaurita,params.retry_after_seconds);409 username_taken(nome già usato, senza distinzione tra maiuscole e minuscole, anche se l'altro account si è registrato nello stesso istante);409 derivation_index_conflict(dopo 5 tentativi non è stato possibile assegnare un indice di derivazione libero);503 registration_suspended(all'avvio sono stati trovati movimenti sulla catena per indici di derivazione successivi all'ultimo assegnato, cioè il database potrebbe essere un backup più vecchio della realtà; la registrazione resta sospesa finché l'operatore non riconcilia, operativita.md B-90 — da implementare). -
Limite di frequenza: al massimo 5 account creati per IP in una finestra mobile di un'ora; la quota è verificata prima dell'hash Argon2 e consumata solo da una registrazione riuscita (un nome già preso o un errore di validazione non la consumano). Dettagli in sicurezza.md.
-
Effetti: inserisce l'utente con il nome così come digitato, il prossimo indice di derivazione e l'indirizzo derivato, e scrive
user_registeredcon indice e indirizzo nella stessa transazione; avvia (senza attenderla, best effort) la sottoscrizione Electrum dell'indirizzo. Non notifica l'SSE. Il flusso completo è in registrazione-e-depositi.md. Codice:app/auth/routes.py:register,app/auth/routes.py:RegisterRequest· Test:tests/unit/test_auth.py,tests/unit/test_users.pyStato: da implementare (B-92, B-90) · Oggi: non scrive audit, e non esiste la sospensioneregistration_suspended.
-
POST /auth/login
- [API-19] Apre una sessione.
- Autenticazione: nessuna.
- Corpo:
{"username": stringa, "password": stringa}, ciascuno di al massimo 256 caratteri (oltre:422 invalid_request, prima di qualunque hash Argon2 e senza consumare i contatori di fallimento). Lo username è confrontato senza distinzione tra maiuscole e minuscole. Stato: da implementare (B-94) · Oggi: nessun vincolo di lunghezza. - Risposta
200:{"access_token": <JWT>, "address": <indirizzo>}. - Errori:
422;429 rate_limited(params.retry_after_seconds, arrotondato per eccesso al secondo);401 invalid_credentials— identico per username inesistente e password sbagliata. - Limite di frequenza: due contatori di fallimenti con attesa
esponenziale, uno per username (dal 5° fallimento) e uno per IP (dal 20°).
Il blocco è verificato prima della password, quindi durante un blocco
anche la password giusta riceve
429. Un login riuscito azzera solo il contatore dello username. Valori e motivazioni in sicurezza.md. - Effetti: nessuno oltre ai contatori in memoria; nessun audit.
Codice:
app/auth/routes.py:login,app/auth/rate_limit.py:AuthRateLimiters· Test:tests/unit/test_auth.py,tests/unit/test_rate_limit.py
Utente
GET /users/me
- [API-20] Profilo e saldo dell'utente autenticato.
-
Autenticazione: Bearer.
-
Risposta
200:Campo Tipo Significato idintero identificativo dell'utente usernamestringa come digitato alla registrazione addressstringa indirizzo permanente (deposito, vincite, resto) balance_satsintero saldo confermato e spendibile ( User.cached_balance_sats); è quello su cui si basano scommesse e prelievipending_balance_satsintero saldo comprensivo del resto ancora non confermato di scommesse/prelievi in volo — il valore da mostrare; è un totale, non una differenza has_pendingbooleano vero se esiste almeno una transazione in volo dell'utente (il frontend mostra il saldo in ambra) created_atstringa ISO data di registrazione Come si calcolano i due saldi è in registrazione-e-depositi.md.
-
Errori:
401(vedi API-06). -
Effetti: nessuno. Codice:
app/api/routes/users.py:me,app/wallet/balance.py:compute_pending_balance· Test:tests/unit/test_users.py,tests/unit/test_balance.py
-
POST /users/me/change-password
- [API-21] Cambio password self-service.
- Autenticazione: Bearer.
- Corpo:
{"current_password": stringa, "new_password": stringa}, ciascuno di al massimo 256 caratteri. - Risposta
200:{"access_token": <JWT>}— un nuovo token per la sessione corrente. - Errori, nell'ordine in cui sono verificati:
401di autenticazione;422 invalid_request(un campo oltre 256 caratteri);429 rate_limited(params.retry_after_seconds);401 current_password_incorrect;400 password_too_short(params.minimum= 8). La lunghezza minima è verificata nel gestore (quindi400, non422come alla registrazione). - Limite di frequenza: un contatore di fallimenti di
current_passwordper utente, con attesa esponenziale come quello del login; il blocco è verificato prima dell'hash Argon2. Soglie in sicurezza.md (B-94). - Effetti: sostituisce l'hash e incrementa
User.token_version: ogni token emesso prima, compreso quello usato per questa richiesta, da quel momento riceve401 session_expired. Scrivepassword_changednella stessa transazione. Non notifica l'SSE. Codice:app/api/routes/users.py:change_password,app/auth/rate_limit.py:RateLimiter· Test:tests/unit/test_users.pyStato: da implementare (B-94) · Oggi: nessuna lunghezza massima, nessun limite di frequenza (la password attuale si può provare senza limiti) e nessuna voce di audit.
GET /users/me/last-round-result
- [API-22] Esito dell'ultimo round chiuso a cui l'utente ha
partecipato, letto dal database. È la rete di sicurezza per un dispositivo
che ha perso la finestra in cui
GET /rounds/currentmostrava il vincitore.-
Autenticazione: Bearer.
-
Risposta
200:Campo Tipo Significato round_idintero o nulll'ultimo round in stato closedcon una riga di partecipazione dell'utente;nullse non ce n'è nessunowonbooleano vero se l'utente è il vincitore di quel round amount_satsintero o nullimporto effettivamente pagato al vincitore ( Round.winner_amount_sats, già al netto della commissione del pagamento), solo sewonSenza round chiusi giocati la risposta è
{"round_id": null, "won": false, "amount_sats": null}. -
Errori:
401. -
Effetti: nessuno. Codice:
app/api/routes/users.py:last_round_result
-
Scommesse e prelievi
POST /bets
- [API-23] Piazza la scommessa dell'utente sul round in corso, al costo
fisso attuale. Regole, sequenza e rollback sono in
scommesse-e-prelievi.md.
-
Autenticazione: Bearer.
-
Corpo: nessuno.
-
Risposta
201(il server attivo ha accettato la transazione) o202(esito del broadcast ambiguo, API-13):Campo Tipo Significato round_idintero round a cui la scommessa è iscritta bet_txidstringa txid della transazione (trasmessa con 201, forse trasmessa con202)bet_amount_satsintero importo che arriva al montepremi, cioè il costo del round meno la commissione di rete statusstringa stato del partecipante: "broadcast"con201,"building"con202 -
Errori:
503 network_unavailable(nessuna connessione Electrum, verificato prima di ogni altro controllo); poi, tutti400salvo indicazione:no_round_open,round_closing,already_betting,round_full(params.max_participants; anche quando il round ha raggiuntomax_round_pool_sats, B-95),balance_pending_confirmation(params.pending_sats; il costo sarebbe coperto dal saldo comprensivo del resto non ancora confermato, ma non da quello confermato, B-96),insufficient_balance(params.required_sats),too_many_inputs(params.max_inputs),balance_leaves_no_change(params.required_extra_sats),amount_below_network_fee,amount_below_dust_limit,503 service_busy,502 broadcast_failed(solo rifiuto esplicito). Il significato di ciascuno è nel catalogo. -
Serializzazione: la richiesta tiene il blocco per-utente condiviso con
POST /withdrawalsper tutta la costruzione e la trasmissione. -
Effetti: può aprire un round se nessuno è attivo e le condizioni lo permettono (lo stesso controllo dello scheduler, fatto dentro la richiesta); segna le monete spese, crea partecipante e transazione in sospeso, trasmette al server attivo e poi, best effort, a tutti i server di riserva. Audit
bet_placedin caso di successo,bet_broadcast_failedse la rete rifiuta; notifica SSE in tutti e tre i casi (successo, rifiuto, esito ambiguo) e all'apertura del round. Codice:app/api/routes/bets.py:create_bet,app/bets/service.py:place_bet· Test:tests/unit/test_bets.py,tests/unit/test_round_snapshot.pyStato: da implementare (B-96) · Oggi: un costo coperto solo da resto non confermato rispondeinsufficient_balance.
-
POST /withdrawals
- [API-24] Invia fondi a un indirizzo esterno. Regole e sequenza in
scommesse-e-prelievi.md.
-
Autenticazione: Bearer.
-
Corpo:
Campo Tipo Vincoli external_addressstringa verificato nel servizio (bech32 valido con prefisso plm1)amount_satsintero nessun vincolo di schema; il minimo è verificato nel servizio -
Risposta
201(il server attivo ha accettato la transazione) o202(esito del broadcast ambiguo, API-13):Campo Tipo Significato txidstringa txid della transazione (trasmessa con 201, forse trasmessa con202)amount_requested_satsintero importo chiesto amount_sent_satsintero importo che arriva al destinatario: richiesto meno commissione, e meno quanto serve a lasciare un resto minimo se si preleva tutto statusstringa stato del prelievo: "broadcast"con201,"building"con202 -
Errori:
422;503 network_unavailable(verificato prima della validazione dell'indirizzo, quindi un indirizzo invalido riceve503finché la rete non torna); poi400:invalid_address,withdrawal_to_own_address,withdrawal_to_platform_address(destinazione uguale a un indirizzo di montepremi, di riserva o delle commissioni),amount_below_minimum(params.minimum_sats= costo attuale della scommessa; verificato anche sull'importo ridotto di un prelievo dell'intero saldo, B-96),balance_pending_confirmation(params.pending_sats),insufficient_balance(params.required_sats),too_many_inputs(params.max_inputs),amount_below_network_fee,amount_below_dust_limit;503 service_busy;502 broadcast_failed(solo rifiuto esplicito). -
Serializzazione: stesso blocco per-utente di
POST /bets. -
Effetti: segna le monete spese, crea prelievo e transazione in sospeso, trasmette al server attivo e poi, best effort, a tutti i server di riserva. Audit
withdrawal_sent, oppurewithdrawal_broadcast_failedcon il prelievo lasciato in statofailedper un rifiuto esplicito; con un esito ambiguo il prelievo restabuilding. Notifica SSE in tutti e tre i casi. Codice:app/api/routes/withdrawals.py:create_withdrawal,app/withdrawals/service.py:request_withdrawal· Test:tests/unit/test_withdrawals.pyStato: da implementare (B-96) · Oggi: l'importo ridotto di un prelievo dell'intero saldo non è riconfrontato con il minimo.
-
Round
GET /rounds/current
- [API-25] Stato del round attivo, pensato per essere interrogato a ciclo
dalle due interfacce (anche la pillola Electrum/altezza di
/adminleggechain_tip_heightda qui).-
Autenticazione: facoltativa (serve solo per
user_played). -
Risposta
200— tutti i campi sono sempre presenti:Campo Tipo Significato server_timestringa ISO ora del server, per allineare i conti alla rovescia del client round_idintero o nullround attivo; nullse non ce n'èstatusstringa o nullopen,closing,drawingopaying_out; un roundclosednon compare maiopened_atstringa ISO o nullapertura del round closes_atstringa ISO o nullscadenza per le scommesse: opened_at+ la durata fissata all'apertura del round (non quella attuale di configurazione)participant_countintero partecipanti confermati, cioè quelli tra cui si estrae bet_amount_satsintero costo che pagherebbe una nuova scommessa: con un round attivo il costo fissato all'apertura del round ( rounds.bet_amount_sats), senza round attivo quello configuratojackpot_satsintero 70% (arrotondato per difetto) della somma degli importi effettivi delle scommesse confermate; limite superiore, perché la commissione del pagamento è ancora da sottrarre pending_participant_countintero tutti i partecipanti, confermati e in volo (totale, non differenza) pending_jackpot_satsintero 70% della somma di tutte le scommesse, confermate e in volo has_pending_betsbooleano vero se pending_participant_count>participant_countdraw_animation_secondsintero durata minima dell'animazione dell'estrazione, dalla configurazione winner_user_idintero o nullvincitore, valorizzato dall'estrazione in poi ( paying_out)winner_amount_satsintero o nullimporto al vincitore, valorizzato solo quando la transazione di pagamento è stata costruita draw_block_heightintero o nullaltezza del blocco usato per l'estrazione draw_block_hashstringa o nullhash di quel blocco; è uno degli ingressi del seme, non il seme (vedi API-45) draw_waiting_sincestringa ISO o nullinizio dell'attesa del blocco; valorizzato solo mentre statusèdrawingserver_seed_hashstringa o nullimpegno del server sul segreto del round, pubblicato dall'apertura; nullper un round aperto prima di B-73participants_hashstringa o nullimpronta dell'elenco ordinato dei partecipanti, valorizzata dall'ingresso in drawingdraw_heightintero o nullaltezza del blocco di estrazione, fissata all'ingresso in drawingdraw_confirmationsintero o nullconferme richieste sul blocco di estrazione, fissate all'apertura del round pool_addressstringa o nullindirizzo del montepremi del round, verificabile su un esploratore chain_tip_heightintero o nullultima altezza nota della catena; nullfinché non è notalottery_pausedbooleano lotteria in pausa, per qualunque motivo paused_reasonstringa o nulloperator,reorg,payout_blocked,resurrected_tx, oppurenullse non in pausa (modello-dati.md, DAT-69)lottery_configuredbooleano falso finché manca configurazione indispensabile (oggi: fee_address), e quindi nessun round può aprirsiuser_playedbooleano vero se il chiamante autenticato ha una riga di partecipazione in questo round, in qualunque stato; sempre falso per un anonimo Il segreto del round (
server_seed) non compare mai in questa risposta: si legge solo da API-45, dopo che il vincitore è stato scritto.Senza round attivo sono valorizzati solo
server_time,bet_amount_sats,draw_animation_seconds,chain_tip_height,lottery_paused,paused_reasonelottery_configured; gli altri valgononull,0ofalse. Il significato di dominio di ogni fase è in ciclo-di-vita-del-round.md. -
Errori: nessuno previsto (un token invalido non è un errore, API-07).
-
Effetti: non apre round (a differenza di
POST /bets); crea la riga di configurazione con i valori predefiniti se non esiste ancora. Codice:app/api/routes/rounds.py:current_round,app/rounds/service.py:get_active_round,app/rounds/service.py:round_deadline,app/rounds/service.py:winner_share,app/rounds/service.py:rounds_can_open· Test:tests/unit/test_rounds_route.py
-
GET /rounds/stream
- [API-26] Canale Server-Sent Events: una notifica senza contenuto
("qualcosa è cambiato, rileggi") ogni volta che cambia lo stato di round,
scommesse o saldi. È aggiuntivo al polling, che resta attivo; i punti del
codice che notificano e il ruolo del canale sono in
architettura.md.
- Autenticazione: nessuna; la notifica non contiene dati.
- Risposta
200,text/event-stream, conCache-Control: no-storeeX-Accel-Buffering: no. Messaggi:event: update/data: {}— a ogni notifica; notifiche arrivate prima che il client abbia letto la precedente si fondono in una sola (coda di capacità 1 per client);: keep-alive(commento) — dopo 20 secondi senza notifiche.
- Chiusura: la connessione di un client scomparso senza chiusura pulita è rilevata entro 5 secondi. Oltre 5 flussi contemporanei dallo stesso IP, l'apertura di un nuovo flusso chiude il più vecchio di quell'IP.
- Errori:
503 too_many_streams(formato strutturato, API-16) quando i flussi aperti in totale sono già 500; il clientEventSourceriprova da solo e intanto resta il polling. - Chiusura all'arresto del server: allo spegnimento ogni flusso aperto viene chiuso dal server, così l'arresto non attende i client (architettura.md B-93).
- Effetti: nessuno.
- Cosa non notifica:
PUT /admin/config,POST /admin/pauseePOST /admin/resumenon pubblicano sul canale;/vedelottery_paused/paused_reasone la nuova configurazione al polling successivo diGET /rounds/current. Codice:app/api/routes/rounds.py:round_stream,app/rounds/events.py:RoundEventBroadcaster.subscribe,app/rounds/events.py:RoundEventBroadcaster.publish· Test:tests/unit/test_round_events.pyStato: da implementare (B-94, B-93) · Oggi: il503ha il corpo non strutturato{"detail": "too many concurrent update streams"}, e un flusso aperto non viene chiuso all'arresto: uvicorn lo attende finché Docker non termina il processo.
GET /rounds/{round_id}/proof
- [API-45] La prova pubblica dell'estrazione di un round: tutto ciò che
serve a chiunque per ricalcolare il vincitore senza fidarsi del server.
L'algoritmo e la codifica esatta dei byte sono definiti in
ciclo-di-vita-del-round.md (B-73); qui è
specificata la forma della risposta.
-
Autenticazione: nessuna.
-
Parametro di percorso:
round_idintero (non intero:422). -
Risposta
200, per un round in qualunque stato; i campi non ancora determinati valgononull(o lista vuota):Campo Tipo Valorizzato da Significato round_idintero sempre — statusstringa sempre stato del round algorithmstringa sempre "commit-reveal-v1"per i round aperti con B-73;"block-hash-v0"per quelli precedenti, per cui la prova è incompleta (vedi sotto)pool_addressstringa sempre indirizzo del montepremi del round server_seed_hashstringa apertura sha256(server_seed), in esadecimaleserver_seedstringa o nullsolo dopo che il vincitore è stato scritto ( paying_outeclosed)il segreto del round, 32 byte in esadecimale draw_heightintero o nullingresso in drawingaltezza del blocco di estrazione draw_confirmationsintero apertura conferme richieste su quel blocco prima di leggerne l'hash participantslista ingresso in drawingelenco in ordine di draw_position:{"draw_position", "txid", "vout", "address", "amount_sats", "status"}, dovetxid:voutè l'output della scommessa versopool_address,addressl'indirizzo del partecipante (dove andrebbe la vincita),amount_satsl'importo netto della scommessa estatusconfirmedoreorged_outparticipants_hashstringa o nullingresso in drawingimpronta dell'elenco participantsdraw_block_hashstringa o nullestrazione hash del blocco a draw_heightseedstringa o nullestrazione il seme in decimale ( rounds.seed_int)winner_indexintero o nullestrazione seed modnumero di elementi diparticipantswinner_addressstringa o nullestrazione participants[winner_index].address -
Come si verifica (round
commit-reveal-v1):- prima della chiusura, annotare
server_seed_hashdaGET /rounds/currento da questo endpoint; - dopo la rivelazione, controllare che
sha256(server_seed)sia uguale a quel valore: il segreto non è stato cambiato dopo l'apertura; - ricalcolare
participants_hashdaparticipants, e controllare su un esploratore o un nodo che ognitxid:voutsia un output confermato versopool_address; - controllare su un esploratore o un nodo che il blocco a
draw_heightabbia hashdraw_block_hashe che la catena abbia almenodraw_confirmationsblocchi da lì; - ricalcolare
seeddaserver_seed,draw_block_hasheparticipants_hash, poiwinner_index = seed mod nconnil numero di elementi diparticipants, e confrontarewinner_address.
Per un round
block-hash-v0la risposta haserver_seed_hash,server_seed,draw_heighteparticipants_hashanulleparticipantsvuota, perché quei round non hanno registrato né un impegno né l'ordine dei partecipanti:seededraw_block_hashsi possono confrontare, il vincitore no. - prima della chiusura, annotare
-
Riservatezza:
server_seednon compare in nessuna risposta, di nessun endpoint (admin compresi), finchérounds.winner_user_idèNULL(modello-dati.md, DAT-64). Gli indirizzi dei partecipanti sono già pubblici sulla catena: le scommesse li collegano apool_address. -
Errori:
404 round_not_found;422. -
Effetti: nessuno. Codice:
app/api/routes/rounds.py:round_proof,app/rounds/draw.py:draw_winner· Test:tests/unit/test_draw_proof.py
-
QR
GET /qr/{address}
- [API-27] Immagine PNG del QR code di un indirizzo PLM, usata dal pannello
Deposito con un semplice
<img>.- Autenticazione: nessuna (un
<img>non può portare il token). - Parametro di percorso:
address, al massimo 100 caratteri, bech32 valido con prefissoplm1(stessa verifica dei prelievi). - Risposta
200:image/png, conCache-Control: private, max-age=86400, immutable. - Errori:
400 invalid_address. - Effetti: nessuno. Il disegno avviene fuori dal ciclo di eventi ed è
memorizzato per indirizzo (ultimi 512 indirizzi), così una richiesta
ripetuta costa solo una lettura in memoria.
Codice:
app/api/routes/qr.py:address_qr,app/api/routes/qr.py:_render_png,app/wallet/address.py:is_valid_plm_address· Test:tests/unit/test_qr.py
- Autenticazione: nessuna (un
Segnalazioni di bug
POST /bug-reports
- [API-28] Invia una segnalazione, da loggati o no.
-
Autenticazione: facoltativa; se il chiamante è autenticato la segnalazione gli viene attribuita.
-
Corpo:
Campo Tipo Vincoli descriptionstringa 1–2000 caratteri (lunghezza misurata sul testo ricevuto); spazi iniziali e finali rimossi; non può essere vuota dopo la rimozione contactstringa o nullfacoltativo, al massimo 256 caratteri; spazi rimossi; una stringa vuota diventa null -
Risposta
201:{"id": intero}. -
Errori:
422;429 rate_limited(params.retry_after_seconds). -
Limite di frequenza: una quota di segnalazioni per IP (API-11) in una finestra mobile, verificata prima dell'inserimento e consumata solo da un inserimento riuscito, anche per un chiamante autenticato; soglie in sicurezza.md (B-94).
-
Effetti: inserisce la segnalazione in stato
open. Nessun audit, nessuna notifica SSE. Codice:app/api/routes/bug_reports.py:create_bug_report,app/api/routes/bug_reports.py:BugReportCreate,app/auth/rate_limit.py:RollingQuota· Test:tests/unit/test_bug_reports.pyStato: da implementare (B-94) · Oggi: nessun limite di frequenza: un anonimo può inserire segnalazioni senza limite.
-
GET /bug-reports/mine
- [API-29] Le segnalazioni inviate dall'utente mentre era autenticato, per
seguirne lo stato. Quelle anonime non vi compaiono mai.
- Autenticazione: Bearer.
- Risposta
200: lista, dalla più recente, di{"id", "description", "status", "created_at"};statusèopen,readoresolved. Il campocontactnon è restituito. - Errori:
401. Codice:app/api/routes/bug_reports.py:list_my_bug_reports· Test:tests/unit/test_bug_reports.py
Amministrazione
Tutti gli endpoint /admin/* richiedono X-Admin-Token
(API-08) e rispondono 403 invalid_admin_token
senza di esso. Nessuno restituisce rounds.server_seed prima che il
vincitore del round sia stato scritto (API-45). Gli eventi
di audit citati sono descritti in audit-log.md; il significato
dei parametri operativi in
ciclo-di-vita-del-round.md e
modello-dati.md.
GET /admin/config
- [API-30] Legge la configurazione operativa (la riga unica di
round_config, creata con i valori predefiniti se manca).- Risposta
200:{"fee_address", "bet_amount_sats", "round_duration_seconds", "round_cooldown_seconds", "fee_rate_sat_vb", "rbf_timeout_seconds", "draw_animation_seconds", "deposit_confirmations", "draw_confirmations", "max_round_pool_sats", "paused", "paused_reason"}. Codice:app/api/routes/admin.py:read_config· Test:tests/unit/test_admin.py
- Risposta
PUT /admin/config
- [API-31] Modifica a caldo uno o più parametri operativi.
-
Corpo: tutti i campi facoltativi; quelli assenti o
nullnon cambiano.Campo Vincoli fee_addressspazi rimossi; bech32 PLM valido ( plm1…). Non può essere svuotato tramite l'APIbet_amount_sats> 0 e ≤ 10 000 000 000 000 (100 000 PLM) round_duration_seconds30 – 604 800 (7 giorni) round_cooldown_seconds0 – 86 400 fee_rate_sat_vb1 – 10 000 ( MAX_FEE_RATE_SAT_VB)rbf_timeout_seconds60 – 604 800 draw_animation_seconds0 – 600 deposit_confirmations1 – 100 draw_confirmations1 – 20 max_round_pool_sats0(disattivato), oppure dabet_amount_satsa 4 000 000 000 000 000I limiti sono quelli di modello-dati.md (tabella
round_config), che ne è il proprietario.pausedepaused_reasonnon sono accettati qui (sono ignorati come campi sconosciuti): la pausa ha i suoi endpoint registrati nell'audit. -
Risposta
200: la configurazione completa, come API-30. -
Errori:
422per qualunque vincolo violato (nessun campo viene applicato). Una modifica dibet_amount_satsofee_rate_sat_vbcon cui un round non si potrebbe pagare (RND-40) dà422condetail = {"code": "config_not_payable", "message", "params": {"reason"}}, dovereasonèwinner_share_below_network_feeocommission_below_dust_limit; il controllo usa i valori risultanti dopo la modifica e, se cambiafee_rate_sat_vb, anche il costo con cui si è aperto il round in corso. Un tettomax_round_pool_satsdiverso da 0 e minore del costo della scommessa (dopo la modifica) dà422condetail.code = "max_round_pool_below_bet_amount". -
Effetti: scrive
config_updatedcon i valori prima/dopo dei soli campi davvero cambiati; nessuna scrittura se nulla cambia. Durata, pausa tra round, costo della scommessa,fee_addressedraw_confirmationssi applicano dal round successivo, come il tettomax_round_pool_sats. Nessuna notifica SSE. Codice:app/api/routes/admin.py:update_config,app/api/routes/admin.py:RoundConfigUpdate· Test:tests/unit/test_admin.py
-
POST /admin/pause e POST /admin/resume
- [API-32] Attivano e disattivano la pausa: impedisce l'apertura del
prossimo round, il round in corso prosegue fino al pagamento (salvo un
pagamento trattenuto, vedi sotto). La semantica di
paused_reasonè in modello-dati.md (DAT-69).POST /admin/pause— corpo: facoltativo,{"note": stringa}di al massimo 256 caratteri, un promemoria libero dell'operatore che finisce solo nel payload dilottery_paused. Impostapaused = trueepaused_reason = 'operator', tranne quando è già attivo un motivo automatico (reorg,payout_blocked,resurrected_tx), che resta.POST /admin/resume— corpo: nessuno. È l'unico modo di uscire da qualunque pausa, anche automatica: impostapaused = false,paused_reason = NULLe azzerarounds.payout_blocked_atdel round attivo, se valorizzato (il pagamento viene poi riverificato, DAT-68). Il payload dilottery_resumedriporta ilpaused_reasonche è stato tolto, così il registro dice che cosa l'operatore ha accettato di riprendere.- Risposta
200(entrambi): la configurazione completa, come API-30, compresopaused_reason. - Errori:
422(nota oltre 256 caratteri). - Effetti: scrivono
lottery_paused/lottery_resumeda ogni chiamata, anche se lo stato era già quello. Nessuna notifica SSE:/vedelottery_pausedal polling successivo. Codice:app/api/routes/admin.py:pause_lottery,app/api/routes/admin.py:resume_lottery· Test:tests/unit/test_admin.py,tests/unit/test_pause_reason.py
GET /admin/users
- [API-33] Elenco degli utenti, per
idcrescente, a pagine.- Query:
limitintero 1–500, predefinito 100;after_idintero facoltativo: restituisce solo gli utenti conidmaggiore. La pagina successiva si chiede conafter_iduguale all'iddell'ultimo elemento ricevuto; una lista più corta dilimitè l'ultima pagina. Fuori intervallo:422. - Risposta
200: lista di{"id", "username", "address", "balance_sats", "created_at"};balance_satsè il saldo confermato. Codice:app/api/routes/admin.py:list_users· Test:tests/unit/test_admin.pyStato: da implementare (B-94) · Oggi: restituisce tutti gli utenti in una sola risposta, senza paginazione né limite.
- Query:
GET /admin/users/{user_id}/privkey
- [API-34] Esporta la chiave privata WIF dell'indirizzo di un utente, per
interventi manuali. È una scelta voluta (vedi
limiti-noti.md).
- Parametro:
user_idintero. - Risposta
200:{"address", "wif"}. - Errori:
404 user_not_found(oggi non strutturato,"user not found", API-16);422seuser_idnon è un intero. - Effetti: scrive
admin_privkey_accesseda ogni accesso. Codice:app/api/routes/admin.py:user_privkey· Test:tests/unit/test_admin.py
- Parametro:
POST /admin/users/{user_id}/reset-password
- [API-35] Imposta una nuova password casuale per un utente bloccato.
- Risposta
200:{"username", "new_password"}— la password (16 caratteri URL-safe) è mostrata solo in questa risposta. - Errori:
404 user_not_found(oggi non strutturato, API-16);422. - Effetti: sostituisce l'hash, incrementa
token_version(ogni sessione aperta dell'utente riceve401 session_expired), scriveadmin_password_reset. Codice:app/api/routes/admin.py:reset_user_password· Test:tests/unit/test_admin.py
- Risposta
GET /admin/rounds
- [API-36] Storico dei round, dal più recente.
- Query:
limitintero 1–500, predefinito 50 (fuori intervallo:422). - Risposta
200: lista di{"id", "status", "opened_at", "closed_at", "draw_block_height", "draw_block_hash", "winner_user_id", "winner_username", "pool_amount_sats", "winner_amount_sats", "fee_amount_sats", "payout_txid", "pool_address", "server_seed_hash", "draw_height", "bump_reserve_sats", "payout_blocked_at"}; i campi non ancora valorizzati sononull.server_seednon è tra i campi: la prova completa di un round è in API-45. Codice:app/api/routes/admin.py:list_rounds· Test:tests/unit/test_admin.py
- Query:
GET /admin/audit-log
- [API-37] Registro di audit, dalla voce più recente, filtrabile e a
pagine.
-
Query, tutti facoltativi e combinati in and:
Parametro Tipo Filtro limitintero 1–500, predefinito 200 numero massimo di voci before_idintero solo voci con idminore: la pagina successiva si chiede con l'iddell'ultima voce ricevuta; una lista più corta dilimitè l'ultima paginaevent_typestringa, ripetibile solo voci di uno dei tipi indicati, confrontati alla lettera (un tipo inesistente dà una lista vuota) round_idintero solo voci con quel round_iduser_idintero solo voci con quel user_idfromstringa ISO 8601 con offset solo voci con created_at≥ questo istantetostringa ISO 8601 con offset solo voci con created_at< questo istanteUn valore non interpretabile (un intero non valido, un istante senza offset) risponde
422 invalid_request. -
Risposta
200: lista di{"id", "event_type", "payload" (oggetto), "user_id", "round_id", "created_at"}, periddecrescente. Codice:app/api/routes/admin.py:list_audit_log· Test:tests/unit/test_admin.pyStato: da implementare (B-92), per filtri e paginazione · Oggi: accetta sololimit, quindi oltre le ultime 500 voci il registro non è consultabile dall'API.
-
GET /admin/pending-transactions
- [API-38] Transazioni tracciate (scommesse, pagamenti, prelievi), dalla
più recente.
- Query:
limitintero 1–500, predefinito 50;statusfacoltativo, confrontato alla lettera (un valore inesistente restituisce una lista vuota, non un errore). - Risposta
200: lista di{"id", "kind", "status", "round_id", "withdrawal_id", "user_id", "current_txid", "fee_rate_sat_vb", "attempt_count", "broadcast_at", "replaced_by_txid"}. Non sono espostifailure_reason,last_broadcast_atné la transazione grezza. Stati e significato in transazioni-e-riconciliazione.md. Codice:app/api/routes/admin.py:list_pending_transactions· Test:tests/unit/test_admin.py
- Query:
GET /admin/electrum
- [API-47] Stato della connessione alla rete, per l'indicatore Electrum di
/admin. Risponde200con:connected(booleano),endpoint(server attivo comehost:port,nullse non connesso),tip_height,last_header_age_seconds,fallback_count(server di riserva configurati),quorum_enforced(falso solo conALLOW_NO_QUORUM=true, connessione-electrum.md ELC-19) elast_rotation(motivo e ora dell'ultimo cambio di server, ad esempio punta ferma, connessione-electrum.md ELC-21). Codice:app/api/routes/admin.py,app/electrum/listener.py:ElectrumListener.current_endpoint· Test: — Stato: da implementare (B-86, B-92) · Oggi: l'endpoint non esiste;/adminmostra solo l'altezza della catena letta daGET /rounds/current.
GET /admin/bug-reports
- [API-39] Tutte le segnalazioni, dalla più recente.
- Query:
limitintero 1–500, predefinito 200. - Risposta
200: lista di{"id", "description", "contact", "user_id", "username", "status", "created_at"};user_ideusernamesononullper le segnalazioni anonime. Codice:app/api/routes/admin.py:list_bug_reports· Test:tests/unit/test_bug_reports.py
- Query:
POST /admin/bug-reports/{report_id}/status
- [API-40] Cambia lo stato di una segnalazione.
- Corpo:
{"status": "open" | "read" | "resolved"}; qualunque transizione è ammessa, anche all'indietro. - Risposta
200: la segnalazione aggiornata, come API-39. - Errori:
422(stato sconosciuto);404 bug_report_not_found(oggi non strutturato,"bug report not found", API-16). - Effetti: scrive
bug_report_status_changed(con stato precedente e nuovo) solo se lo stato cambia davvero. Codice:app/api/routes/admin.py:update_bug_report_status· Test:tests/unit/test_bug_reports.py
- Corpo:
Servizio
GET /health
- [API-41] Risponde sempre
200 {"status": "ok"}finché il processo serve richieste. Non verifica né il database né la connessione Electrum: per lo stato della rete si usachain_tip_heightdiGET /rounds/current. È il controllo di vitalità (il processo risponde); quello di prontezza è [API-46]. Codice:app/main.py:health
GET /health/ready
-
[API-46] Risponde
200 {"status": "ready", "checks": {...}}solo se tutte queste condizioni sono vere, altrimenti503con lo stesso corpo e"status": "not_ready":- la connessione Electrum è attiva (
listener.clientnon nullo); - l'ultimo header è stato accettato da meno di 360 secondi (connessione-electrum.md ELC-29);
- lo scheduler dei round ha battuto da meno di 30 secondi (il battito si aggiorna a ogni giro e a ogni iterazione dell'attesa del blocco di estrazione, architettura.md ARC-61).
checksriporta ogni condizione con il suo valore:electrum_connected(booleano),last_header_age_seconds(intero onull),scheduler_heartbeat_age_seconds(intero onull). Nessuna autenticazione, nessun dato sensibile: non espone né il server attivo né l'altezza. È ilhealthcheckdel container. Codice:app/main.py:health· Test: — Stato: da implementare (B-92) · Oggi: l'endpoint non esiste; esiste soloGET /health([API-41]). - la connessione Electrum è attiva (
Pagine e file statici
-
[API-42]
GET /,/admin,/guidae/report-bugrestituiscono rispettivamenteindex.html,admin.html,guida.htmlereport-bug.htmldiapp/static/, conCache-Control: no-store, senza autenticazione (il pannello admin chiede il token lato client). Sono esclusi dallo schema OpenAPI. Codice:app/main.py:index_page,app/main.py:admin_panel,app/main.py:user_guide,app/main.py:report_bug_page -
[API-43] Ogni altro percorso non gestito da un router è servito dal mount dei file statici su
/(app/static/, conhtml=True): i file esistenti rispondono200, gli altri404 {"detail": "Not Found"}, un metodo diverso daGET/HEAD405. Il mount è registrato dopo tutti i router, quindi non può oscurare un endpoint dell'API. Codice:app/main.py(app.mount)
Catalogo dei codici di errore
- [API-44] L'insieme dei codici che l'API può restituire nel formato
strutturato è esattamente quello della tabella seguente; un codice nuovo va
aggiunto qui, con la sua chiave
error.<code>in tutte e sette le lingue diapp/static/i18n.js. Codice:app/api/errors.py:ApiError,app/static/i18n.js:TRANSLATIONS
| Codice | Stato | Sollevato da | params |
Significato | error.<code> (7 lingue) |
|---|---|---|---|---|---|
session_expired |
401 | app/auth/dependencies.py:get_current_user |
— | token illeggibile, scaduto, utente inesistente o token superato da un cambio/reset password | sì |
invalid_credentials |
401 | app/auth/routes.py:login |
— | username inesistente o password sbagliata (risposta identica) | sì |
rate_limited |
429 | app/auth/routes.py:register, app/auth/routes.py:login; anche app/api/routes/users.py:change_password e app/api/routes/bug_reports.py da implementare (B-94) |
retry_after_seconds |
quota di registrazione, blocco per tentativi falliti, o quota di segnalazioni | sì |
username_taken |
409 | app/auth/routes.py:register |
— | nome già usato (senza distinzione di maiuscole) | sì |
derivation_index_conflict |
409 | app/auth/routes.py:register |
— | nessun indice di derivazione libero dopo 5 tentativi | sì |
current_password_incorrect |
401 | app/api/routes/users.py:change_password |
— | password attuale errata | sì |
password_too_short |
400 | app/api/routes/users.py:change_password |
minimum |
nuova password sotto MIN_PASSWORD_LENGTH |
sì |
network_unavailable |
503 | app/api/routes/bets.py:create_bet, app/api/routes/withdrawals.py:create_withdrawal |
— | nessuna connessione Electrum attiva | sì |
no_round_open |
400 | app/bets/service.py:place_bet |
— | nessun round attivo e nessuno apribile (pausa, fee_address mancante, pausa tra round in corso) |
sì |
round_closing |
400 | app/bets/service.py:place_bet |
— | il round attivo non accetta più scommesse (scadenza passata o stato diverso da open), anche se scoperto dopo la costruzione |
sì |
already_betting |
400 | app/bets/service.py:place_bet |
— | l'utente ha già una riga di partecipazione nel round | sì |
round_full |
400 | app/bets/service.py:place_bet |
max_participants |
il round ha già 400 partecipanti (in qualunque stato) | sì |
insufficient_balance |
400 | app/bets/service.py:place_bet, app/withdrawals/service.py:request_withdrawal, app/wallet/psbt_builder.py:select_utxos |
required_sats (assente se sollevato da select_utxos) |
le monete confermate e non spese non coprono l'importo | sì |
balance_pending_confirmation |
400 | app/withdrawals/service.py:request_withdrawal; anche app/bets/service.py:place_bet da implementare (B-96) |
pending_sats |
l'importo sarebbe coperto dal saldo comprensivo del non confermato, ma non da quello confermato | sì |
too_many_inputs |
400 | app/wallet/psbt_builder.py:select_utxos |
max_inputs |
servirebbero più di 50 monete in ingresso | sì |
balance_leaves_no_change |
400 | app/wallet/psbt_builder.py:build_signed_transaction (solo scommesse) |
required_extra_sats |
dopo la scommessa il resto sarebbe sotto il limite di polvere (294 sat) | sì |
amount_below_network_fee |
400 | app/wallet/psbt_builder.py:build_signed_transaction |
— | l'importo non copre la commissione di rete | sì |
amount_below_dust_limit |
400 | app/wallet/psbt_builder.py:build_signed_transaction |
— | al netto della commissione l'importo sarebbe polvere | sì |
broadcast_failed |
502 | app/bets/service.py:place_bet, app/withdrawals/service.py:request_withdrawal |
— (il motivo del rifiuto è nel message) |
la rete ha rifiutato la trasmissione; tutto è stato annullato | sì |
invalid_address |
400 | app/withdrawals/service.py:request_withdrawal, app/api/routes/qr.py:address_qr |
— | non è un indirizzo bech32 PLM valido | sì |
withdrawal_to_own_address |
400 | app/withdrawals/service.py:request_withdrawal |
— | destinazione uguale all'indirizzo di deposito dell'utente | sì |
amount_below_minimum |
400 | app/withdrawals/service.py:request_withdrawal |
minimum_sats |
prelievo sotto il costo attuale della scommessa | sì |
internal_error |
500 | app/main.py:log_unhandled_exception |
— | eccezione non gestita | sì |
withdrawal_to_platform_address |
400 | app/withdrawals/service.py:request_withdrawal |
— | destinazione uguale a un indirizzo di montepremi, di riserva o delle commissioni | sì |
service_busy |
503 | app/bets/service.py:place_bet, app/withdrawals/service.py:request_withdrawal, app/auth/routes.py:register |
— | il database non ha concesso il lock di scrittura entro il timeout; nulla è stato trasmesso, si può riprovare | sì |
registration_unavailable |
503 | app/auth/routes.py:register |
— | registrazione sospesa finché il controllo degli indici di derivazione dopo un ripristino non è concluso o risolto (registrazione-e-depositi.md, B-90) — da implementare | no — da aggiungere |
not_authenticated |
401 | dipendenza Bearer di FastAPI | — | header Authorization assente — oggi detail stringa — da implementare (B-94) |
no — da aggiungere |
invalid_admin_token |
403 | app/api/routes/admin.py:require_admin |
— | token admin assente o errato — oggi detail stringa — da implementare (B-94) |
no — da aggiungere |
user_not_found |
404 | app/api/routes/admin.py |
— | utente inesistente — oggi detail stringa — da implementare (B-94) |
no — da aggiungere |
bug_report_not_found |
404 | app/api/routes/admin.py |
— | segnalazione inesistente — oggi detail stringa — da implementare (B-94) |
no — da aggiungere |
round_not_found |
404 | app/api/routes/rounds.py (GET /rounds/{id}/proof) |
— | round inesistente | sì |
not_found |
404 | gestore delle eccezioni HTTP in app/main.py |
— | percorso inesistente — oggi detail stringa — da implementare (B-94) |
no — da aggiungere |
method_not_allowed |
405 | gestore delle eccezioni HTTP in app/main.py |
— | metodo non ammesso — oggi detail stringa — da implementare (B-94) |
no — da aggiungere |
too_many_streams |
503 | app/api/routes/rounds.py (SSE) |
— | tetto globale degli stream raggiunto — oggi detail stringa — da implementare (B-94) |
no — da aggiungere |
Due codici di app/wallet/psbt_builder.py — winner_share_below_network_fee
e commission_below_dust_limit — riguardano solo la costruzione del
pagamento, che avviene nello scheduler e non attraverso l'API: non arrivano mai
a un client (finiscono nel registro di audit, vedi
ciclo-di-vita-del-round.md) e non hanno
traduzione.
Chiavi error.* presenti in i18n.js che il server non produce: invalid_request
(usata dal frontend per le risposte 422, API-15),
invalid_amount (validazione lato client del modulo di prelievo) e
guide_unavailable (attualmente non usata da nessun file).