Zum Inhalt

06 - Claim Flow (Gewinn-Auszahlung)

Dokument: Gasless Claim-System Stand: 28.02.2026 Scope: Merkle-basiertes Claiming via Relayer Status: Audit-ready


1. Übersicht

Das Claim-System ermöglicht es Nutzern, Lotteriegewinne und Affiliate-Provisionen gasless abzurufen. Claims werden über Merkle-Tree-Proofs verifiziert und über einen Relayer on-chain ausgeführt.

Beteiligte Systeme

System Rolle
Frontend (Claim-Seite) UI, Signierung, Status-Tracking
Backend (API Routes) Payload-Generierung, Relayer-Steuerung
Relayer (Wallet) On-Chain TX-Submission
ClaimRouter (Contract) Claim-Ausführung, Merkle-Verifikation
PrizeVaultV3 (Contract) Winner-Auszahlung
AffiliateVaultV2 (Contract) Affiliate-Auszahlung

2. Ablaufdiagramm

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Payload    │────▶│  Approval   │────▶│  Signatur   │────▶│   Execute   │
│   abrufen    │     │   (USDT)    │     │  (EIP-712)  │     │  (Relayer)  │
│ /claim/load  │     │  On-Chain   │     │  Client     │     │ /claim/exec │
└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘

3. Detaillierter Flow

Schritt 1: Payload abrufen

Endpoint: GET /api/claim/payload

Authentifizierung (EIP-191 Signatur):

Message: "Claim payload request\nAddress: 0x...\nChainId: 421614\nTimestamp: 1709..."
Headers:
  x-claim-address: 0x...
  x-claim-signature: 0x...
  x-claim-timestamp: 1709...
Replay-Schutz: Timestamp ± 5 Minuten

Response:

{
  "kycRequired": false,
  "kycStatus": "verified",
  "threshold": 10000000000,
  "winner": {
    "total": "5000000",
    "claims": [{
      "type": "winner",
      "dayId": 100,
      "player": "0x...",
      "paidTo": "0x...",
      "amount": "5000000",
      "proof": ["0x...", "0x..."]
    }],
    "pending": false,
    "remaining": 0
  },
  "affiliate": {
    "total": "250000",
    "claims": [...],
    "pending": false,
    "remaining": 0,
    "locked": false,
    "lockReason": null,
    "requiredTips": 5,
    "actualTips": 12
  },
  "claimRouterAddress": "0x..."
}

Backend-Logik: - Wallet-Signatur verifizieren (Replay-Schutz ± 5 Min) - KYC-Status vs. Schwellenwert prüfen - settlement_winner_payouts mit Merkle Proofs laden - settlement_affiliate_payouts mit Merkle Proofs laden - claim_reassignments prüfen - Affiliate-Restriktionen prüfen

Einschränkungen:

Regel Beschreibung
Max Claims/Batch 20 Claims pro Request
Affiliate-Tag Nur Sonntags claimbar
Affiliate-Aktivität Mind. 5 bezahlte Tips in den letzten 6 Tagen
KYC-Schwellenwert In bo_config konfiguriert

Schritt 2: USDT Approval (falls nötig)

Prüfe Allowance des ClaimRouter Contracts
Falls unzureichend: USDT.approve(claimRouterAddress, MAX_UINT256)

Schritt 3: ClaimPermit signieren

EIP-712 Typ-Definition:

Domain: { name: "ChainbetsClaim", version: "1", chainId, verifyingContract }
Types: {
  ClaimPermit: [
    { name: "claimer", type: "address" },
    { name: "totalWinnerAmount", type: "uint256" },
    { name: "totalAffiliateAmount", type: "uint256" },
    { name: "maxGasCostUsdt", type: "uint256" },
    { name: "deadline", type: "uint256" },
    { name: "nonce", type: "bytes32" },
    { name: "claimsHash", type: "bytes32" }
  ]
}

Permit-Generierung:

const nonce = generateNonce();           // 16 random Bytes
const deadline = calculateDeadline(5);   // +5 Minuten
const claimsHash = computeClaimsHash(winnerClaims, affiliateClaims);

claimsHash-Berechnung:

keccak256(abi.encode(
  [dayId, player/affiliate, amount][]  // Alle Claims konkateniert
))

Schritt 4: Ausführung via Relayer

Endpoint: POST /api/claim/execute

Request:

{
  "permit": {
    "claimer": "0x...",
    "totalWinnerAmount": "5000000",
    "totalAffiliateAmount": "250000",
    "maxGasCostUsdt": "100000",
    "deadline": 1709...,
    "nonce": "0x...",
    "claimsHash": "0x..."
  },
  "signature": "0x...",
  "winnerClaims": [{
    "dayId": 100,
    "originalWallet": "0x...",
    "paidTo": "0x...",
    "amount": "5000000",
    "proof": ["0x...", "0x..."]
  }],
  "affiliateClaims": [{
    "dayId": 100,
    "affiliate": "0x...",
    "amount": "250000",
    "proof": ["0x..."]
  }]
}

Contract-Call:

ClaimRouter.claimAllFor(
  permit, signature,
  winnerClaims[], affiliateClaims[],
  actualGasCostUsdt
)

Response:

{
  "success": true,
  "txHash": "0x...",
  "blockNumber": 12345,
  "gasUsed": "250000",
  "gasCostUsdt": "75000",
  "netPayout": "5175000",
  "winnerClaimsExecuted": 1,
  "affiliateClaimsExecuted": 1
}

Backend nach Ausführung: - settlement_winner_payouts: claimed=true - settlement_affiliate_payouts: claimed=true - claim_events: Event loggen


4. Optimistic Update

Endpoint: POST /api/claim/optimistic-update

Nach TX-Submission, vor Bestätigung:

{
  "txHash": "0x...",
  "payload": {
    "winner": { "claims": [...] },
    "affiliate": { "claims": [...] }
  }
}

Setzt claimed_pending=true und pending_tx_hash in DB.


5. Claim-Typen

5.1 Winner Claims

Feld Beschreibung
dayId Settlement-Tag
originalWallet Ursprüngliche Spieler-Wallet
paidTo Auszahlungs-Wallet (kann reassigned sein)
amount Gewinnbetrag (USDT, 6 Decimals)
proof Merkle-Proof-Array

5.2 Affiliate Claims

Feld Beschreibung
dayId Settlement-Tag
affiliate Affiliate-Wallet
amount Provisionsbetrag (USDT, 6 Decimals)
proof Merkle-Proof-Array

5.3 Affiliate-Einschränkungen

Regel Beschreibung
Auszahlungstag Nur Sonntags
Mindestaktivität 5 bezahlte Tips in den letzten 6 Tagen
Lock-Gründe not_sunday, insufficient_activity

6. Contract-Versionen

Datei: lib/contractVersions.ts

Claims können sich über verschiedene Contract-Versionen erstrecken (bei Migrationen):

function getContractsForDay(dayId): ContractVersion {
  // Query: contract_versions WHERE from_day_id <= dayId <= to_day_id
  // Fallback: .env Werte
}

function groupClaimsByVersion(dayIds): Map<ContractVersion, dayId[]>

7. Multicall-Batch-Verarbeitung

Datei: lib/buildClaimMulticall.ts

Für direkte Claims (ohne Relayer) wird Multicall3 verwendet:

Multicall3.aggregate3([
  { target: PrizeVaultV3, data: claimMerkle(dayId, wallet, paidTo, amount, proof) },
  { target: AffiliateVault, data: claimAffiliate(dayId, affiliate, to, amount, proof) },
  ...
])

Multicall3-Adresse: 0xcA11bde05977b3631167028862bE2a173976CA11


8. Debug-Endpoint

Endpoint: GET /api/claim/debug?address=0x...

Keine Signatur erforderlich. Gibt detaillierte Claim-Informationen zurück:

{
  "contracts": { "prizeVault": "0x...", "claimRouter": "0x..." },
  "trustedClaimers": { "prizeVault": "0x...", "match": { "prize": true } },
  "winnerClaims": [{
    "db": { "claimed": false, "merkleRoot": "0x..." },
    "onChain": { "claimed": false, "merkleRoot": "0x...", "rootMatch": true },
    "canClaim": true
  }],
  "summary": {
    "totalWinnerInDb": 3,
    "claimableWinner": 2,
    "alreadyClaimedWinner": 1
  }
}

9. Event-Logging

Datei: lib/claimEventLogger.ts

Event-Typ Beschreibung
CLAIM_SIGNATURE_ERROR Signatur-Fehler
CLAIM_TX_ERROR TX-Ausführungsfehler
CLAIM_PENDING_SET Optimistic Update gesetzt
CLAIM_TX_REVERTED TX reverted
CLAIM_CONFIRMED Claim erfolgreich
BACKFILL_TRIGGERED Backfill ausgelöst
BACKFILL_CLAIM_CONFIRMED Backfill-Claim bestätigt

DB-Tabelle: claim_events(event_type, scope, tx_hash, details)


Weiterführende Dokumente: - 05 - Buy Flow - 08 - Blockchain - 10 - Sicherheit