pysqlite's legacy mode emitted BEGIN only at the first DML, so every read before it ran outside any transaction and a lock wait surfaced as "database is locked" at an arbitrary later statement; place_bet also mistook that error for a write-snapshot conflict, which SQLite cannot produce. - _register_sqlite_pragmas: synchronous=FULL, isolation_level=None and a `begin` event that emits BEGIN / BEGIN IMMEDIATE. - begin_immediate(session): takes the write lock before the first read; called first thing in place_bet, request_withdrawal, the scheduler transitions, _trigger_payout, the confirmation poller, the reconciler, bump_fee's persist phase, deposit crediting / external-spend marking / reinstating, and the derivation-index allocation at registration. - A lock not granted before a broadcast answers service_busy (503, all seven languages), nothing built or sent; after a broadcast nothing is undone, the rows stay `building` for the reconciler. - Spec, storico, BUGS.md and CLAUDE.md updated; 37 new tests on a real file DB. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
65 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 — da implementare (B-73) |
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.pyStato: da implementare (B-76) · Oggi: qualunque eccezione del broadcast, compresi il timeout di 15 s e la caduta della connessione, annulla tutto e risponde502 broadcast_failed. -
[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.pyStato: da implementare (B-76, B-95, B-96) · Oggi: ogni esito diverso dal successo è502 broadcast_failedcon tutto annullato; la trasmissione va solo al server attivo; un costo coperto solo da resto non confermato rispondeinsufficient_balance; non c'è tetto al montepremi; il costo è quello configurato al momento della scommessa, non quello del round.
-
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, B-80),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-76, B-80, B-96) · Oggi: ogni esito diverso dal successo è502 broadcast_failedcon il prelievofailed; la trasmissione va solo al server attivo; un prelievo verso l'indirizzo del pool o delle commissioni è accettato; 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 configurato — da implementare (B-95); oggi sempre 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-73 — da implementare (B-73)participants_hashstringa o nullimpronta dell'elenco ordinato dei partecipanti, valorizzata dall'ingresso in drawing— da implementare (B-73)draw_heightintero o nullaltezza del blocco di estrazione, fissata all'ingresso in drawing— da implementare (B-73)draw_confirmationsintero o nullconferme richieste sul blocco di estrazione, fissate all'apertura del round — da implementare (B-73, B-95) pool_addressstringa o nullindirizzo del montepremi del round, verificabile su un esploratore — da implementare (B-80) 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) — da implementare (B-78, B-82, B-85)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.pyStato: da implementare (B-73, B-78, B-80, B-82, B-85, B-95), per i campi marcati · Oggi: quei campi non esistono,bet_amount_satsè sempre il costo configurato, edraw_block_hashè il seme dell'estrazione.
-
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", "status"}, dovetxid:voutè l'output della scommessa versopool_address,addressl'indirizzo del partecipante (dove andrebbe la vincita) 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(endpoint da scrivere),app/rounds/draw.py:draw_winner· Test: — Stato: da implementare (B-73) · Oggi: l'endpoint non esiste; l'unico dato pubblico dell'estrazione èdraw_block_hashinGET /rounds/current, finché il round è attivo.
-
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.pyStato: da implementare (B-73, B-82, B-85, B-95) · Oggi: mancanodeposit_confirmations,draw_confirmations,max_round_pool_satsepaused_reason.
- 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 — da implementare (B-82) draw_confirmations1 – 20 — da implementare (B-73) max_round_pool_sats0(disattivato), oppure dabet_amount_satsa 4 000 000 000 000 000 — da implementare (B-95)I 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). -
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 (per costo efee_address: da implementare, B-95; oggi subito). Nessuna notifica SSE. Codice:app/api/routes/admin.py:update_config,app/api/routes/admin.py:RoundConfigUpdate· Test:tests/unit/test_admin.pyStato: da implementare (B-73, B-82, B-95), per i tre campi nuovi e la copia all'apertura · Oggi: sono accettati solo i primi sette campi.
-
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.pyStato: da implementare (B-78, B-82, B-85) · Oggi: nessun corpo, nessunpaused_reason, e la pausa è solo manuale.
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
decisioni-e-limiti.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.pyStato: da implementare (B-73, B-79, B-80, B-82), per gli ultimi cinque campi · Oggi: la lista si ferma apayout_txid.
- 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 — da implementare (B-80) | no — da aggiungere |
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 — da implementare (B-73) | no — da aggiungere |
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).