## ARENA BATTLE — TEK SİTE CÜZDANI / SEAMLESS WALLET v1

## Mimari
Harcama yapılabilen bakiyenin tek sahibi operatörün mevcut cüzdanıdır. Arena Battle ayrı bir cüzdana para aktarmaz; sağlayıcıdaki bakiye, operatör cevabının sürümlü kopyasıdır. Site başlığı ve oyun aynı `availableMinor` değerini gösterir. Diğer oyunlar da aynı operatör veritabanı işlemleriyle bu bakiyeyi harcamalıdır.

SDK: `/sdk/slot-battles-seamless-wallet.mjs` (Node 22.16+, node:sqlite).
Çalışan referans: `examples/operator-seamless.cjs`. SQLite örneğini mevcut site cüzdanınıza aynı işlem sınırı içinde uyarlayın; ikinci bir harcanabilir bakiye açmayın. Başlangıç bakiyesi yalnız yerel test hesaplarında verilir. Gerçek site üyesine otomatik 50.000 TL verilmez.
Yapılandırma: WALLET-CONFIG.txt. Siteye özel callback URL'si, mode=seamless ve sunucuda saklanan imza sırrı gereklidir.

## Kimlik ve oturum
Site sunucusu giriş yapan üyeyi kendi doğrulanmış oturumundan bulur. `externalId` kalıcı site üye anahtarıdır; tarayıcıdan gelen isim, bakiye veya kullanıcı ID'siyle değiştirilmez. Sağlayıcı `tenantId`+`externalId` ikilisi için aynı oyuncuyu/alias'ı kullanır; örneğin betcio / member-100 → cio1001. Aynı kişinin farklı sitelerdeki üyelikleri ayrıdır.

Launch isteğini yalnız site sunucusu API anahtarıyla üretir. Tarayıcıya kısa ömürlü launch bileti/URL'si gider; API anahtarı ve wallet sırrı gitmez. Iframe ve modül aynı cüzdan protokolünü kullanır.

## İmzalı callback
Sağlayıcı, operatörün yapılandırdığı tek HTTPS adresine POST application/json gönderir. Yerel entegrasyon testinde loopback HTTP kullanılabilir. Yönlendirme yapılmaz. İstek ve cevap ham UTF-8 JSON gövdesi üzerinden imzalanır.

```http İmza başlıkları
X-Slot-Wallet-Timestamp: <unix-milliseconds>
X-Slot-Wallet-Signature: <lowercase-hex-hmac-sha256>
```

Yer tutucuları cüzdan SDK’sının ürettiği değerlerle değiştirin. İmza formülü (sözde kod):

```text İmza formülü
signature = HMAC_SHA256(secret, timestamp + "." + rawBody)
```

En fazla 60 saniye saat farkı kabul edilir; tekrar denemede aynı JSON ve `requestId`, yeni timestamp/imza gönderilir. Karşılaştırma sabit zamanlı olmalıdır. Doğrulanmış isteklerin başarı ve hata cevapları da aynı başlıklarla imzalanır. İmzası geçersiz isteğe para hareketi uygulanmaz. Tarayıcı Origin başlığı içeren callback kabul edilmez. Body sınırı 64 KiB.

## İstek gövdesi
{
  "version": 1,
  "tenantId": "site-id",
  "externalId": "member-100",
  "requestId": "reserve:immutable-reference",
  "action": "reserve",
  "currency": "TRY",
  "amountMinor": 4000000,
  "holdReference": "reserve:immutable-reference",
  "battle": {
    "id": "room-id", "title": "Akşam savaşı", "status": "waiting",
    "buyInMinor": 4000000, "capacity": 3, "playerCount": 3,
    "gameIds": ["sweet-bonanza-1000"], "feeBps": 500
  }
}
Para birimi TRY; bütün tutarlar tam sayı kuruştur (40.000 TL = 4.000.000). Kesirli/sayı metni/negatif tutarlar kabul edilmez. Reserve/refund için pozitif en fazla 1.000.000.000 kuruş; settle en fazla 1.000.000.000.000 kuruş. `requestId` ve `holdReference` 8–160 karakter: harf, rakam, - _ . : @.

## İşlemler
balance: Kimliği doğrulanmış üyenin güncel `availableMinor`, `lockedMinor` ve revision değerini oku. `amountMinor`/`holdReference`/battle gönderilmez. Cevap receipt=null. Hiçbir para hareketi oluşturmaz.

reserve: `amountMinor` katılım bedelidir. available'dan bir kez düşür, aynı miktarı locked'a ekle ve savaş rezervasyonunu `requestId` ile oluştur. Bakiye kontrolü, kilit, hesap sürümü ve hareket kaydı tek veritabanı işleminde kesinleşmelidir. Başarısız yetersiz bakiye isteğinin referansı tüketilmez.

refund: `holdReference` eski `reserve.requestId`'dir. Yalnız aynı üyeye/savaşa ait açık rezervasyon ve tam rezervasyon tutarı kabul edilir. locked'dan çıkar, available'a geri koy. Oda başlamadan iptal/ayrılma gibi nedenler battle ile gelir.

settle: `holdReference` eski `reserve.requestId`'dir. Açık rezervasyonu tüket, locked'dan çıkar, `amountMinor` ödülü available'a ekle. Kaybedene `amountMinor`=0 gönderilir; yine de sonuç hareketi ve battle detayı kaydedilir. Katılım bedelini ikinci kez düşürme; rezervasyon aşamasında zaten düşüldü.

## Yanıt (HTTP 200)
{
  "version": 1, "tenantId": "site-id", "externalId": "member-100",
  "requestId": "reserve:immutable-reference", "currency": "TRY",
  "receipt": {
    "id": "operator-ledger-id", "requestId": "reserve:immutable-reference",
    "action": "reserve", "amountMinor": 4000000,
    "holdReference": "reserve:immutable-reference", "battleId": "room-id",
    "createdAt": 1792900000000
  },
  "balance": {"availableMinor": 1000000, "lockedMinor": 4000000, "revision": 1}
}
receipt değişmez işlem makbuzudur; tekrar istekte aynen geri gelir. balance ise cevap anındaki GÜNCEL bakiyedir, makbuz anındaki eski bakiye değildir. revision her başarılı site cüzdanı değişikliğinde artar; başka oyunun yaptığı harcama da dahil. Aynı revision ile farklı bakiye dönmeyin.

## Tekrarlar ve hatalar
Aynı `requestId` + aynı içerik bir kez işlenir; yeniden istek yeni para hareketi üretmez. Aynı `requestId` başka içerikle gelirse HTTP409 `REFERENCE_CONFLICT`. İşlem ve idempotency kaydı aynı DB transaction'ında saklanır. Sunucu cevabı kaybolsa da tekrar eski makbuzu bulur. İade/sonuç işlemlerini kalıcı iş kuyruğuyla yeniden deneyin; timeout'u kayıp veya iade diye yorumlamayın.
Hata alanları (aşağıdaki kod yer tutucudur; gerçek hata kodunu kullanın):

```json Hata alanları
{
  "error": "açıklama",
  "code": "..."
}
```

İmzası doğrulanmış, JSON olarak okunabilen isteğin hata cevabında ayrıca version, `tenantId`, `externalId`, `requestId` ve currency alanlarını aynı istekten aynen döndürün; sağlayıcı reddi bu istekle eşleştirir. Bağlamı veya imzası doğrulanamayan cevap kesin ret sayılmaz, mutabakat için yeniden denenir. Önemli kodlar: `INSUFFICIENT_FUNDS`(409), `HOLD_CONFLICT`(409), `REFERENCE_CONFLICT`(409), `PLAYER_NOT_FOUND`(404), `INVALID_REQUEST`(400), `INVALID_SIGNATURE`(401). Başarı cevabını cüzdan transaction'ı COMMIT olmadan vermeyin.

## Savaş hareket detayları
Her mutasyonda battle.id zorunludur. Seçim öncesinde `gameIds` vardır; henüz bilinmeyen alanlar gönderilmez. Seçim/sonuç tamamlandığında uygun alanlar: title, status, `buyInMinor`, capacity, `playerCount`, `gameId`, `gameIds`, `gameName`, provider, `bonusType` (tür ID'si), `bonusName`, `scoreMinor`, `prizeMinor`, `feeMinor`, `feeBps`, `totalPayoutMinor`, `netPoolMinor`, `outcome(win/loss/refund/pending)`, winners, `createdAt`, `endedAt`.

Reserve geçmişinde katılım eksi tutardır. Refund ve ödül artıdır. Kaybedenin settle kaydı sıfırdır; oyun adı, tur skoru ve savaş kimliği yine görünür. Aynı savaş için reserve/settle satırlarını `battleId` ile gruplamak mümkündür. Savaşın rastgele oyunu henüz seçilmediyse giriş hareketi oyun havuzunu gösterir; sonuç hareketi seçilen gerçek oyunu gösterir.

## Örnek hesap
- Kullanılabilir 50.000 TL, katılım 40.000 TL → kullanılabilir 10.000 TL, kilitli 40.000 TL.
- Savaş başlamadan iade → kullanılabilir 50.000 TL, kilitli 0 TL.
- İptal yerine oyuncuya 150.000 TL ödül → kullanılabilir 160.000 TL, kilitli 0 TL.
- Kayıp → kullanılabilir 10.000 TL, kilitli 0 TL ve sıfır ödüllü sonuç hareketi.

## Referans sunucu
`examples/operator-seamless.cjs`'nin `createSeamlessOperatorExample` seçenekleri:

```javascript Referans sunucu seçenekleri
createSeamlessOperatorExample({
  providerOrigin, tenantSlug, tenantId, apiKey, walletSecret,
  walletFile, mode, accounts, port
});
```

`mode` iframe veya module olabilir.
accounts yalnız yerel fixture içindir: [`{externalId,login,password,openingBalanceMinor}`]. Üretimde kendi üye oturumunuzu ve mevcut cüzdanınızı kullanın. Örnek login ve cookie akışı kimliği sunucudan türetir; browser gövdesinden `externalId`/amount kabul etmez.

Ortam değişkenleri: `PROVIDER_ORIGIN`, `OPERATOR_SLUG`, `OPERATOR_TENANT_ID`, `OPERATOR_API_KEY`, `OPERATOR_WALLET_SECRET`, `OPERATOR_WALLET_FILE`, `OPERATOR_ACCOUNTS_FILE`, `OPERATOR_MODE`, `OPERATOR_PORT`.

Callback: `/wallet/callback`. Oturumlu kendi bakiye/geçmişi: `GET /integration/wallet` ve `GET /integration/history`. Örnek header iki göstergede aynı `availableMinor`'ı kullanır; wallet-changed olayı ve 2 saniyelik yedek sorgu ile yenilenir.

SDK internal `siteAdjustment()` yalnız güvenilir operatör kodundan kullanılır; başka oyunun aynı bakiyeyi nasıl düşürdüğünü gösterir. İnternete açık bir manuel bakiye değiştirme endpoint'i değildir.

## Doğrulama

```shell Yerel doğrulama
node --test tests/v15-r24-seamless-operator.test.cjs tests/v15-r24-seamless-example.test.cjs
```

Testler gerçek yerel HTTP ve SQLite ile imza, rezervasyon, iade, kazanç/kayıp, restart, aynı referansın tekrarı, üye izolasyonu ve iframe/modül sunumunu doğrular. Muhasebe testlerinde tur sonuçları kontrollü fixture'dır; bu testler üçüncü taraf oyun veya yayın kalite testi değildir.
