Flux métier
Cette section suit les sept parcours principaux de BlockLucky à travers le code, du clic dans l'application jusqu'à la base de données et à la blockchain. Chaque page contient un diagramme de séquence, puis une explication pas à pas qui cite les fichiers et les fonctions réellement appelés, les erreurs possibles avec leur code HTTP et la manière dont le flux résiste aux doublons.
Toutes les pages décrivent le mode BLOCKCHAIN_MODE=onchain, celui de la production et de Docker Compose. Les différences du mode simulated sont signalées à la fin de chaque page concernée.
Carte des flux
| Flux | Déclencheur | Point d'entrée | Synchrone | Asynchrone | Page |
|---|---|---|---|---|---|
| Inscription et portefeuille | Citoyen | POST /api/v1/auth/signup (mobile) ou /auth/login (web), puis POST /api/v1/auth/2fa/verify ; ensuite POST /api/v1/user/identity |
Défi OTP, création du compte et du portefeuille, EtherPoints de bienvenue, JWT, envoi chiffré de la pièce d'identité | Email du code, dotation de bienvenue, email de bienvenue, décision de la Mairie et son email | Inscription et portefeuille |
| Achat et cadeau | Citoyen dont l'âge est vérifié | POST /api/v1/lottery/buy, POST /api/v1/lottery/gift |
Contrôle de l'âge et du ticket unique, transaction signée par le citoyen, attente du reçu, indexation, EtherPoints | Frappe du badge POAP, événement points:awarded |
Achat et cadeau de ticket |
| Tirage et cagnotte | Keeper ou Mairie | runKeeperOnce, POST /api/v1/lottery/draw |
Salle d'attente de DRAW_REVEAL_DELAY_SECONDS côté keeper, demande VRF |
Réponse VRF, indexation de WinnerPicked, annonce draw:winner, retrait de la cagnotte |
Tirage et versement de la cagnotte |
| Annulation et remboursement | Keeper ou Mairie | runKeeperOnce, POST /api/v1/lottery/refund |
Annulation dans le contrat | Retrait des remboursements pour chaque payeur | Annulation et remboursement |
| Disjoncteur | Mairie | POST /api/v1/admin/circuit-breaker |
pause() ou unpause(), journal d'audit |
- | Disjoncteur |
| Échange de récompense | Citoyen dont l'âge est vérifié, puis commerçant | POST /api/v1/marketplace/claim ou /scratch, POST /api/v1/marketplace/redeem |
Débit des points, code promo, décrément du stock | Email du code promo | Échange de récompense |
| Suppression RGPD | Citoyen | POST /api/v1/user/gdpr/delete-request puis POST /api/v1/user/gdpr/confirm-delete |
Jeton, anonymisation, révocation des sessions | Email de confirmation | Suppression RGPD |
Vue d'ensemble de la loterie
Les quatre flux de la loterie s'enchaînent autour du cycle de vie d'un tour dans le contrat :
stateDiagram-v2 [*] --> Open: _openRound Open --> Open: buyTickets, buyTicketsFor Open --> Drawing: requestDraw Drawing --> Completed: fulfillRandomWords Drawing --> Drawing: retryDraw après drawTimeout Open --> Cancelled: closeExpiredRound ou cancelRound Drawing --> Cancelled: cancelRound Completed --> [*] Cancelled --> [*]
Chaque passage à Completed ou Cancelled ouvre immédiatement le tour suivant (_openRound) dans la même transaction. La base suit ces états avec l'énumération Status : OPEN, LOCKED (pour Drawing), COMPLETED, CANCELLED. Le détail du contrat est dans Cycle de vie d'un tour.
Principes communs
Chaîne d'une requête
Toutes les requêtes HTTP traversent la même chaîne, montée dans backend/src/app.ts : contexte de requête, métriques, Helmet, CORS, parsing JSON, mode maintenance, rate limit global (300 requêtes par 15 minutes et par IP), puis le routeur du module. Dans le routeur, l'ordre est toujours :
authenticate -> requireRoles ou requireAgeVerification -> idempotency -> validate -> controller -> service -> repo ou passerelleLe controller appelle un service, puis répond avec sendSuccess. Toute exception remonte par next(error) jusqu'à errorHandler (backend/src/middlewares/error.middleware.ts). Voir Réponses et erreurs.
Erreurs communes à tous les flux
| Code | Cause | Origine |
|---|---|---|
| 400 | Corps invalide selon le schéma Zod (errors liste les champs), JSON mal formé |
validate, errorHandler |
| 401 | Jeton absent, expiré, révoqué ou invalide | authenticate dans backend/src/middlewares/auth.middleware.ts |
| 403 | Rôle insuffisant, rôle modifié depuis la connexion, compte supprimé, ou administrateur sans 2FA alors que ADMIN_MFA_REQUIRED=true |
requireRoles dans backend/src/middlewares/rbac.middleware.ts |
| 403 | Âge non vérifié (AGE_VERIFICATION_REQUIRED) sur l'achat, le cadeau, l'échange et le ticket à gratter |
requireAgeVerification dans backend/src/middlewares/age-verification.middleware.ts |
| 409 | Unicité ou contrainte CHECK violée en base, conflit de transaction | errorHandler |
| 413 | Corps supérieur à 1 Mo, ou à 12 Mo pour l'envoi d'une pièce d'identité | errorHandler |
| 429 | Rate limit global ou d'authentification dépassé | backend/src/middlewares/rateLimiter.middleware.ts |
| 503 | Mode maintenance, délai de requête PostgreSQL dépassé, vérification des droits impossible | app.ts, errorHandler, requireRoles |
Erreurs du contrat
La passerelle backend/src/blockchain/lottery.gateway.ts décode l'erreur personnalisée renvoyée par le contrat (decodeContractError) et la traduit par toAppError :
| Erreur | Code HTTP | Message renvoyé |
|---|---|---|
EnforcedPause |
503 | Contrat en pause de sécurité, avec circuitBreaker: true |
RoundNotOpen, RoundExpired, RoundSoldOut |
409 | Tirage en cours, tour terminé, ou tickets épuisés |
PlayerLimitReached, InvalidQuantity, IncorrectPayment |
400 | Limite par participant, quantité ou montant invalide |
DrawNotReady, RoundNotCancellable |
409 | Conditions de tirage ou d'annulation non remplies |
NotWinner |
403 | Le compte n'a pas gagné ce tour |
PrizeAlreadyClaimed, RoundNotCompleted, NothingToWithdraw |
409 | Cagnotte déjà versée, tirage non terminé, aucune commission |
AccessControlUnauthorizedAccount |
403 | Le compte opérateur n'a pas le rôle requis |
Code ethers INSUFFICIENT_FUNDS |
402 | Solde d'ethers fictifs insuffisant |
Codes ethers NETWORK_ERROR, TIMEOUT, SERVER_ERROR |
503 | Réseau injoignable, avec canRetry: true |
| Toute autre erreur | 502 | Message de repli propre à l'opération, avec canRetry: true |
Les détails (contractError, canRetry, circuitBreaker) sont renvoyés dans le champ errors de la réponse. Les clients web et mobile s'en servent pour proposer de réessayer ou afficher le disjoncteur.
Idempotence
Trois mécanismes complémentaires protègent contre les doublons :
| Mécanisme | Portée | Fichier |
|---|---|---|
En-tête Idempotency-Key |
POST /lottery/buy, /lottery/gift, /lottery/claim-prize, /marketplace/claim. La première réponse de statut inférieur à 500 est conservée 24 h et rejouée avec Idempotent-Replayed: true ; une requête identique en cours reçoit 409 |
backend/src/middlewares/idempotency.middleware.ts |
| Unicité en base | Tickets uniques par (lotteryId, ticketIndex), codes promo uniques, email et adresse de portefeuille uniques |
backend/prisma/schema.prisma |
| Identifiant de job BullMQ | <type>-<tour>-<adresse> pour la file chain-actions : un même retrait n'est mis en file qu'une fois |
backend/src/blockchain/chain-actions.ts |
Attention
L'en-tête Idempotency-Key est facultatif : sans lui, le middleware laisse passer. Ni le frontend web ni l'application mobile ne l'envoient aujourd'hui. Un double clic sur « Acheter » envoie donc deux transactions. Le contrat accepte les deux tant que la limite par participant n'est pas atteinte.
Temps réel
Deux événements produits par ces flux sont relayés du worker vers l'API par Redis pub/sub : points:awarded (achat) et draw:winner (tirage). Ils sont diffusés à tous les clients connectés, invités compris. Voir Relais temps réel.
Tests qui couvrent ces flux
| Test | Ce qu'il exerce |
|---|---|
backend/__tests__/chain/onchain-lottery.chain.ts |
Achat, cadeau, tirage VRF, versement de la cagnotte et disjoncteur sur un vrai nœud Hardhat (npm run test:chain) |
backend/__tests__/integration/full-draw-flow.test.ts |
Parcours complet avec mocks Prisma et Redis |
backend/__tests__/integration/blockchain-indexer.test.ts |
Indexation des événements |
backend/__tests__/api/idempotency.test.ts |
Rejeu et conflit de l'en-tête Idempotency-Key |