Тестовый TS-шлюз (relayer/), обслуживающий smart-updown.binom.pw и
testfront.binom.pw. Контракт и prod-Kotlin сервер не тронуты.
1+2. GET /state: canBet (boolean) + canBetReasonCode/canBetReason (русский
текст) — можно ли ставить с фиксированным кошельком. Учитывает:
захардкоженный адрес (GW_FIXED_BETTOR), наличие открытой ставки,
хватает ли фантиков на ставку, лимиты [min,max], paused, наличие цены.
3-6. POST /bet: тело {"side":"up"|"down"} — сумма с фронта НЕ приходит,
размер = GW_BET_PERCENT (30) % от баланса фантиков, клампится в
[minAmount, maxAmount]. Ответ: {accepted, reasonCode, reason,
retryAfterSeconds, betId, txSig, ...}.
7. GET /state: openBet / lastBet (взаимоисключающие: открытая имеет
приоритет) + поле status ("open"/"closed"), result, retryAfterSeconds.
8. Ничья (refunded, exit==entry) игроку НЕ показывается: планировщик молча
пересоздаёт ставку тем же размером и направлением (хук replayBet) и
шлёт WS-событие bet_replay вместо bet_closed. Лимит GW_MAX_REPLAYS
(дефолт 10) обрывает цепочку при «замороженной» цене.
9. relayer/API.md — документация всех endpoint-ов и WS-событий.
Новые файлы: src/betRules.ts (правила ставки: canBetDecision,
computeBetAmountUnits), API.md.
Новые env: GW_FIXED_BETTOR, GW_BET_PERCENT (30), GW_MAX_REPLAYS (10).
Проверено: npm run typecheck + npm run test — 127 проверок, 0 провалов;
живая приёмка tools/gateway-api-v2-check.cjs — 26/26 на
https://smart-updown.binom.pw/api (включая реальную ставку → payout_done).
Раньше при равенстве цен close_bet делал `return Ok(())` — ставка навсегда
оставалась в статусе open, а залог запирался в ваулте (замер: 33.6% окон).
Из-за правила «одна открытая ставка на адрес» игрок получал 409 пожизненно.
Теперь ничья возвращает залог целиком (без множителя) и финализирует ставку:
- constants.rs: STATUS_REFUNDED = 3 (0/1/2 не тронуты — зашиты в шлюз и в цепи)
- close_bet.rs: CPI-перевод amount из ваулта беттору под сидами ваулт-PDA,
запись exit_price/status, событие BetClosed(status=3, payout=amount)
- settle.ts: STATUS_REFUNDED, statusName "refunded", статус 3 в условии финализации
- check.ts: проверка формы события refunded
- тест close_bet_equal_price_stays_open → close_bet_equal_price_refunds
(+ повторное закрытие отклоняется)
Проверено: cargo test-sbf 10/10, npm run typecheck EXIT=0, две мутации
(снятие статуса, снятие перевода) валят именно новый тест.
Задеплоено: программа 9ALs… (хэш из цепи совпал побайтово), шлюз rev 15.
- settle.ts: серверный планировщик (startScheduler, тик SMART_SETTLE_INTERVAL_MS)
находит просроченные открытые ставки, берёт цену из истории на expire_time,
сам зовёт close_bet; рассылает bet_closed
- gateway.ts: ручка POST /close удалена (404); проверка 'одна открытая ставка
на адрес' под withBetLock -> 409 с openBetId; bet_open по WS при создании;
lastState прокидывается в settleOnce (без него лог спамил каждый тик)
- settle.ts: пакетный getProgramAccounts вместо поштучного перебора по id
(замер: 3 мс против 28 мс, 9.3x, 1 запрос вместо 15); фильтр не-Bet аккаунтов
по BET_ACCOUNT_SIZE (Global исключён из unmapped)
- check.ts: +38 проверок (decodeBet, openBetOf, dueForSettle, события, границы)
Проблема: handleClose брал getPrice() (текущую цену), поэтому игрок мог
подождать выгодного момента и закрыть ставку тогда, когда ему надо
(эксплуатировалось вживую: ставка висела open >2 мин после экспирации 15с).
Решение (только шлюз, контракт не тронут):
- src/priceHistory.ts (новый) — локальная история цены на встроенном node:sqlite
(Node 22, без новых зависимостей). Запись только при изменении цены
(~300 строк/сутки вместо 92k тиков), ретеншен 24ч, prune одним DELETE.
- natsPrice: в PriceTick добавлено eventTime (время сделки у Binance) — источник
истины по времени; ts не тронут.
- config: SMART_PRICE_HISTORY_PATH (/app/data/price-history.db),
SMART_PRICE_RETENTION_HOURS (24).
- gateway: каждый тик пишется в историю; handleClose берёт
history.priceAtOrBefore(expireTime*1000) — последняя цена НЕ ПОЗЖЕ экспирации.
Отказ 400 'bet not expired yet' до экспирации, 400 на уже закрытую,
503 если истории нет (не выдумываем цену).
- helm: env SMART_PRICE_HISTORY_PATH / SMART_PRICE_RETENTION_HOURS.
- check.ts: 10 проверок истории, включая ключевую «НЕ отдаёт цену позже момента».
Верификация: typecheck 0 ошибок; npm test 34/34; мутационная приёмка — подмена
<= на >=/ASC роняет 4 проверки, снятие дедупа и смена ретеншена роняют свои.
Живой стенд: exitPrice = цена на момент экспирации (9977000000), тогда как
текущая цена в момент клика была 9976000000 — взята история, не 'сейчас'.
- /bet больше НЕ наливает SOL и фантики; address обязателен (400 с подсказкой)
- пред-проверка балансов перед ставкой (SOL >= 0.01, фантиков >= amount), 400
- новый POST /wallet — создать custodial-кошелёк, ничего не наливает
- src/faucetGate.ts: parseMode/faucetDenyReason, fail-closed
- SMART_MODE (test|release), дефолт release; /faucet: 403 если не test
- genesis-проверка: mainnet/devnet/testnet/genesis недоступен -> 403
- helm: SMART_MODE из .Values.smart.mode
- check.ts: 6 проверок гейта (мутационная приёмка пройдена)
Шлюз в кубе падал с EACCES при записи .gateway-bettors.json: том от
local-path принадлежит root, контейнер бежит под node (uid 1000), а
seed-файл копировался в режиме 0444. Добавлены securityContext.fsGroup=1000
и chmod 0644 в init-контейнере. Проверено: ставка и закрытие проходят
через живой домен (entryPrice/exitPrice — реальные цены из NATS).
priceSource больше не рандомит: цена берётся из NATS (market.price.solusdt),
конверсия строка->целое x1e8 без float. Шлюз раздаёт цену по WS (/ws,
нормализация /api/... -> /...) — фронт просто выводит её. Добавлены
Dockerfile и helm-чарт для деплоя в k3s.
- create_bet: escrow фантиков с bettor на vault (PDA[global,vault]) через anchor_spl::token
- close_bet: выплата фантиков из vault на bettor (CPI с signer_seeds vault)
- state/constants: comments про единицы (token units decimals=6), + VAULT_SEED
- IDL регенерирован (idl/ + relayer/idl/) с аккаунтами bettor_token/vault_token/token_program
- тесты обновлены под токен (10/10 + doctest)