05 - Buy Flow (Ticket-Kauf)¶
Dokument: Meta-Transaction Ticket-Kauf-System Stand: 28.02.2026 Scope: Intent-basierter Kauf via Relayer Status: Audit-ready
1. Übersicht¶
Der Ticket-Kauf verwendet ein Meta-Transaction-System: Nutzer signieren EIP-712 Permits, der Relayer sendet die Transaktion on-chain. Nutzer zahlen kein ETH-Gas direkt - Gaskosten werden in USDT verrechnet.
Beteiligte Systeme¶
| System | Rolle |
|---|---|
| Frontend (useBuyFlow) | UI, Signierung, Status-Tracking |
| Backend (API Routes) | Intent-Verwaltung, Relayer-Steuerung |
| Relayer (Wallet) | On-Chain TX-Submission |
| GameManager (Contract) | Ticket-Erstellung on-chain |
| ReserveVault (Contract) | USDT-Pull (Approval-Empfänger) |
2. Ablaufdiagramm¶
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Intent │────▶│ Signatur │────▶│ Approval │────▶│ Submit │
│ erstellen │ │ (EIP-712) │ │ (USDT) │ │ (Relayer) │
│ /buy/intent │ │ Client │ │ On-Chain │ │ /buy/submit │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│
┌─────────────┐ ┌─────────────┐ │
│ Ergebnis │◀────│ Bestätig. │◀─────────────────────────┘
│ anzeigen │ │ /confirm │
└─────────────┘ └─────────────┘
3. Detaillierter Flow¶
Schritt 1: Intent erstellen¶
Endpoint: POST /api/buy/intent
Request:
{
"buyer": "0x...",
"chainId": 421614,
"poolId": 0-4,
"tips": [[1,2,3,4,5,6], [3,7,15,22,30,38]],
"paidTipCount": 2,
"bonusTipCount": 0
}
Response:
{
"intentId": "uuid",
"permit": {
"buyer": "0x...", "poolId": 2,
"tipsHash": "0x...", "deadline": 1234567890,
"nonce": 1, "maxGasCostUsdt": "50000"
},
"typedData": { "domain": {}, "types": {}, "message": {} },
"pricePerTip": "1000000",
"ticketCost": "2000000",
"gasCostUsdt": "50000",
"totalCost": "2050000",
"tipCount": 2, "bonusTipCount": 0,
"autoBonus": 0, "extraBonus": 0,
"affiliate": "0x...",
"needsGasFaucet": true,
"faucetCostUsdt": "25000"
}
Backend-Prüfungen: - Maintenance-Modus (chain_stats) - Spending Limits (daily/weekly/monthly) - Self-Exclusion Status - Tip-Validierung (6 Zahlen, 1-40, unique) - Extra-Bonus-Voucher prüfen - Affiliate-Rate laden - ETH-Balance prüfen (für Gas-Faucet-Bedarf) - USDT-Allowance prüfen
Fehler: - 400: Ungültige Eingabe, Tip-Mismatch, Limit überschritten, Minderjährig - 403: Self-excluded, Limit überschritten, Unzureichende Allowance - 409: TX bereits in Ausführung (EXECUTING Status) - 503: Maintenance
Schritt 2: EIP-712 Signatur¶
Client-seitig via ethers.js Signer
BuyPermit-Struktur:
Domain: { name: "ChainbetsGame", version: "1", chainId, verifyingContract }
Types: { BuyPermit: [buyer, poolId, tipsHash, deadline, nonce, maxGasCostUsdt] }
Endpoint (Bestätigung): POST /api/buy/confirm-signature
Request: { "intentId": "uuid", "nonce": "0x...", "signature": "0x..." }
Response: { "ok": true, "status": "SIGNED", "signatureHash": "0x...",
"extraBonusProof": "0x...", "extraBonusAmount": 3 }
Backend-Logik: - Signer aus EIP-712 Signatur recovern - Extra-Bonus-Proof generieren (BONUS_SIGNER_PK) - keccak256(abi.encode(buyer, extraBonus, nonce))
Schritt 3: USDT Approval (falls nötig)¶
Prüfung: Aktuelle Allowance vs. benötigter Betrag
Falls Allowance < Kosten:
→ USDT.approve(RESERVEVAULT_ADDRESS, MAX_UINT256)
→ Gas-Fee: 50% Buffer auf Base Fee
→ Gas-Faucet falls kein ETH vorhanden
Gas-Faucet Endpoint: POST /api/buy/gas-faucet
Request: { "intentId": "uuid", "wallet": "0x..." }
Response: { "ok": true, "status": "FUNDED", "txHash": "0x...", "amountWei": "..." }
Anti-Abuse: | Regel | Wert | |---|---| | Max. unrecouped Claims | 20 | | Max. pro Stunde | 50 | | Nur Web3Auth-Wallets | Ja | | Max. 1 Claim/Wallet | Bis recouped |
Schritt 4: Transaktion senden¶
Endpoint: POST /api/buy/submit-tx
Request: { "intentId": "uuid", "signature": "0x..." }
Response: { "ok": true, "status": "TX_CONFIRMED", "txHash": "0x...",
"blockNumber": 12345, "gasUsed": "150000" }
Backend-Logik: - Nonce-Manager für Sequenzierung - Dynamische Gas-Price-Berechnung mit Buffer - Force-Sync bei TX-Send-Fehler - Warten auf Receipt-Bestätigung
Contract-Call:
GameManager.buyTicketFor(
buyer, tipCount, bonusTipCount, affiliate,
permit, signature, extraBonusProof, gasCostUsdt
)
Schritt 5: Bestätigung¶
Endpoint: POST /api/buy/confirm-tx
Request: { "intentId": "uuid", "txHash": "0x..." }
Response: {
"ok": true, "status": "CONFIRMED",
"result": {
"ticketId": 42, "dayId": 100,
"rngNumbers": [5,12,18,23,34,40],
"matchesPerTip": [3, 1],
"totalMatches": 4
},
"amounts": { "tipCost": "2000000", "gasCost": "50000" },
"bonus": { "autoBonus": 0, "extraBonus": 0 }
}
Backend-Logik: - TX Receipt abrufen - TicketCreated + Deposited Events parsen - Treffer für jeden Tip OFF-CHAIN berechnen - Tickets und ticket_tips in DB einfügen - Extra-Bonus-Voucher für 3-Treffer-Gewinne erstellen - Bestehende Extra-Bonus-Voucher konsumieren (FIFO)
Schritt 6: Ergebnis anzeigen¶
- RevealScreen mit Zahlen-Animation
- Treffer hervorheben
- Confetti bei Gewinnen (3-stufig)
- Balance-Refresh
4. Bonus-System¶
4.1 Auto-Bonus (Deterministic)¶
| Tips | Bonus | Gesamt |
|---|---|---|
| 1-4 | 0 | 1-4 |
| 5-9 | 3 | 8-12 |
| 10-14 | 7 | 17-21 |
| 15-19 | 11 | 26-30 |
| 20-24 | 15 | 35-39 |
4.2 Extra-Bonus (Voucher-basiert)¶
| Quelle | Tips |
|---|---|
| 3-Treffer-Gewinn | 3 Bonus-Tips |
| Kein 3+ Treffer | 1 Trost-Tip |
| Profil-Bonus | 10 Tips |
| Login-Streak (7 Tage) | 5-8 Tips |
| Leaderboard | Variabel |
4.3 Bonus-Proof-Generierung¶
Backend signiert mit BONUS_SIGNER_PK:
hash = keccak256(abi.encode(buyer, extraBonusAmount, nonce))
proof = BONUS_SIGNER.signMessage(hash)
5. Intent-Recovery¶
Endpoint: POST /api/buy/resume-intent
Bei Seiten-Reload oder Verbindungsabbruch:
1. Intent-ID aus State/LocalStorage laden
2. POST /api/buy/resume-intent { intentId }
3. Backend gibt aktuellen Intent-Status zurück
4. Flow an passender Stelle fortsetzen
Status-Mapping: | Intent-Status | Frontend-Aktion | |---|---| | CREATED | Erneut signieren | | SIGNED | Weiter mit Submit | | EXECUTING | Warten auf Bestätigung | | CONFIRMED | Ergebnis anzeigen | | FAILED | Fehler anzeigen | | EXPIRED | Neuen Intent erstellen |
6. Fehlerbehandlung¶
6.1 Pre-Flight-Checks¶
| Check | Aktion bei Fehler |
|---|---|
| USDT-Balance zu niedrig | error_balance State |
| Settlement nicht abgeschlossen | error_unsettled State |
| Tages-/Wochen-/Monatslimit | error_limit State |
| Self-Exclusion aktiv | error_self_excluded State |
| TX bereits in Ausführung | error_tx_in_progress State |
6.2 Runtime-Fehler¶
| Fehler | State | Recovery |
|---|---|---|
| User lehnt Signatur ab | signature_cancelled |
Reset, erneut versuchen |
| Approval fehlgeschlagen | error_rejected |
Reset |
| Gas-Faucet Fehler | Non-fatal | Flow wird fortgesetzt |
| TX-Submission Fehler | error_network |
Resume-Intent |
| Bestätigungs-Timeout (30s) | error_timeout |
Resume-Intent |
| Permit abgelaufen | error_expired |
Neuen Intent erstellen |
6.3 Confirmation-Polling¶
7. Nonce-Manager¶
7.1 V1 (Spinlock-basiert)¶
| Parameter | Wert |
|---|---|
| Lock-Mechanismus | Spinlock mit 10ms Sleep |
| Timeout | 120 Sekunden |
| Netzwerk-Sync | Alle 30 Sekunden |
7.2 V2 (Queue-basiert, Production)¶
| Parameter | Wert |
|---|---|
| Queue-System | Async mit Promise-Resolution |
| Rate-Limiting | 50ms Minimum zwischen Allokationen |
| Queue-Timeout | 30 Sekunden |
| Gap-Detection | Warnung bei > 50 Nonces voraus |
| Stale-Cleanup | Automatisch |
Monitoring: GET /api/admin/nonce-status
8. Kostenstruktur¶
| Komponente | Beschreibung |
|---|---|
| Tip-Kosten | Anzahl bezahlter Tips × Pool-Preis |
| Gas-Kosten | Geschätzte Relayer-Gaskosten in USDT |
| Faucet-Kosten | Falls ETH für Approval nötig (einmalig) |
USDT-Precision: 6 Decimals (1 USDT = 1.000.000 Units)
Weiterführende Dokumente: - 06 - Claim Flow - 07 - API Referenz - 08 - Blockchain