Files
test-front/API.md
T
assistant d9b1cc1ed8 docs(api): контракт API для клиента — HTTP + WS-события
API.md: полное описание ручек, трёх WS-событий (price/bet_open/bet_closed),
полей с масштабами, ошибок (409 = уже есть открытая ставка), авто-закрытия.
Все значения сверены с живым стендом.

README: убрано устаревшее 'поллинг до SETTLED' — поллинга нет, события идут по WS.

Отдельно задокументирован SEC-9: ~33.6% ставок (замер на 580 мин) зависают
из-за exit==entry и вечного ретрая.
2026-09-14 00:20:12 +03:00

9.8 KiB
Raw Blame History

API тестового стенда — контракт для клиента

Всё, что нужно, чтобы подключиться к шлюзу игры «ставка на направление цены SOL» и получать события в реальном времени. Источник — код шлюза (smart-updown-token/relayer/src/gateway.ts, settle.ts).

База: https://testfront.binom.pw Пути нормализуются: /api/state и /state — одно и то же. CORS открыт (Access-Control-Allow-Origin: *) — можно дёргать прямо из браузера.

Стенд тестовый (SMART_MODE=test): фантики фейковые, SOL из фаусета, контракт наш.


Короткий рецепт: «поставить и получить события»

const BASE = 'https://testfront.binom.pw';

// 1. Кошелёк + деньги (только test-mode)
const { address } = await (await fetch(`${BASE}/wallet`, { method: 'POST' })).json();
await fetch(`${BASE}/faucet/${address}`);       // наливает 5 SOL + 10 000 фантиков

// 2. Подписка на события
const ws = new WebSocket(`${BASE.replace('https', 'wss')}/api/ws`);
ws.onmessage = (e) => {
  const m = JSON.parse(e.data);
  if (m.type === 'price')      console.log('курс  ', m.price);        // "101.02"
  if (m.type === 'bet_open')   console.log('ставка открыта', m.id, m.side, m.entryPrice);
  if (m.type === 'bet_closed') console.log('ставка закрыта', m.id, m.exitPrice, m.statusName);
};

// 3. Ставка. ВНИМАНИЕ: поле называется amountUnits, НЕ amount!
await fetch(`${BASE}/bet`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ address, side: 'UP', amountUnits: 500000000 }), // 500 фантиков
});
// Закрывать не нужно — шлюз закроет сам по таймеру (см. «Авто-закрытие»).

HTTP-ручки

GET /state — состояние контракта

{
  "counter": "3", "nextBetId": "3",
  "minAmount": "100000000", "maxAmount": "10000000000",
  "minAmountHuman": "100.000000", "maxAmountHuman": "10000.000000",
  "multiplierBps": "13000", "expirySeconds": "15", "paused": 0,
  "admin": "E7Hg55…", "relayer": "E7Hg55…",
  "vaultBalance": "26000000000", "vaultBalanceHuman": "26000.000000"
}

multiplierBps: 13000 = выплата 1.3× при выигрыше. expirySeconds: 15 = окно ставки. paused: 1 = приём ставок остановлен.

POST /wallet — создать кошелёк (только test-mode)

{} → {"address": "DxfxzWYF…"}. Денег не наливает — отдельно через /faucet.

GET /faucet/{address} — налить SOL + фантиков (только test-mode)

Работает на произвольный адрес (Phantom и т.п.): {"address":"…","sol":5,"solLamports":5000000000,"tokenUnits":"10000000000","tokenHuman":"10000.000000"}. Вне test-mode → 403.

GET /balance/{address} — балансы

{"sol":5,"solLamports":5000000000,"tokenUnits":"10000000000","tokenHuman":"10000.000000"}

POST /bet — поставить

Тело: {"address": "…", "side": "UP" | "DOWN", "amountUnits": 500000000}

  • address — обязателен, получен из /wallet (или любой пополненный через /faucet).
  • side — регистр не важен (up/UP).
  • amountUnits — целое в базовых единицах токена (у нашего минта decimals=6: 500000000 = 500 фантиков). Минимум 100000000 (100 фантиков). ⚠️ Поле amount вместо amountUnits → 400 {"error":"amountUnits must be a positive integer (token base units)"}.

Ответ 200: {"tx":"4y2HYuu…","id":"0","bettor":"…","entryPrice":10112000000}

Закрывать не нужно — шлюз закроет сам по экспирации.

Ошибки:

Код Когда
400 нет/неверный amountUnits, неизвестный side, сумма вне min/max
403 ставки приостановлены (paused=1)
409 у адреса уже есть открытая ставка — {"error":"you already have an open bet: close it before placing a new one","openBetId":"0"}
409 контракт не инициализирован
500 RPC недоступен

Правило: одна открытая ставка на адрес. Проверка идёт запросом в цепочку, не в память процесса — обойти перезапуском клиента нельзя.

GET /price — текущая цена из NATS (отладка)

{"symbol":"solusdt","price":"101.45000000","ts":1789334371122}. Пока цены нет → 503.

POST /close — НЕ СУЩЕСТВУЕТ (404)

Ручки закрытия нет намеренно: закрытие делает сервер сам по таймеру.


WebSocket: GET /api/ws

wss://testfront.binom.pw/api/ws

При подключении сервер сразу шлёт снапшот цены (если она уже есть), дальше — поток. Все события — широковещательные: один сокет видит ставки всех игроков. Нужны только свои — фильтруй по полю bettor.

1. price — курс SOL

{"type":"price","symbol":"solusdt","price":"101.02000000","ts":1789326824749}

price — строка, чтобы не терять точность. Тик — по мере прихода из NATS (медиана ~0.5 с, провалы до нескольких секунд).

2. bet_open — ставку поставили (игра началась)

{"type":"bet_open","id":"2","bettor":"8KbUFCHV…","side":"DOWN",
 "amountUnits":"500000000","entryPrice":10105000000,"expireTime":1789326835}
  • entryPrice — цена входа, целое с масштабом 1e8 (10105000000 = 101.05).
  • expireTime — unix-секунды, момент закрытия окна.

3. bet_closed — ставку рассчитали (игра закончилась)

{"type":"bet_closed","id":"2","bettor":"8KbUFCHV…",
 "exitPrice":10104000000,"status":1,"statusName":"payout_done"}
  • exitPrice — цена выхода, тот же масштаб 1e8.
  • statusName: payout_done (игрок выиграл) | house_won (выиграл дом) | open (ещё открыта).

Исход считает контракт: UP выигрывает при exitPrice > entryPrice, DOWN — при <.

Авто-закрытие

Планировщик в шлюзе каждые 2 с (SMART_SETTLE_INTERVAL_MS, дефолт 2000) обходит ставки программы, для просроченных берёт цену из локальной истории и зовёт close_bet. При успехе всем в сокет уходит bet_closed. Клиент не должен ничего закрывать сам.


⚠️ Известная проблема: часть ставок зависает без исхода

Симптом: пришло bet_open, а bet_closed не приходит вообще; повторная ставка с того же адреса вечно отвечает 409.

Причина: если цена на момент экспирации в точности равна цене входа, контракт (close_bet.rs:66) молча выходит return Ok(()) — статус остаётся open, ставка не финализируется. А поскольку цена выхода берётся по фиксированному моменту экспирации, повторные попытки планировщика получают ту же самую пару значений — и цикл не заканчивается никогда (лог: settle: #N still open (exit==entry at …), will retry).

Масштаб (замер на 580 минутах живой истории, 34 769 окон): 33.6 % ставок попадают в равные цены — примерно каждая третья.

Что это значит для фронта: каждая третья ставка не завершится, а игрок потеряет возможность играть с этого адреса (правило «одна открытая ставка» + 409). Обходите: для каждого игрока используйте новый адрес на ставку, либо показывайте «ставка в ожидании».

Статус: не исправлено, ждёт решения владельца (политика ничьей / закрытие соседней ценой из истории). Подробности — pump-game-ops/docs/SECURITY.md, пункт SEC-9.


Адреса стенда

Что Значение
Шлюз https://testfront.binom.pw (/api/…)
WS wss://testfront.binom.pw/api/ws
Программа 9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf
Минт фантиков 6fxySAjTQTkzyyZ7u8EQqv3SyJtv2Ydr6hphquHbkGS9 (decimals 6)
RPC localnet http://192.168.76.181:8899 (изнутри сети; рестарт возможен)
Источник цены NATS market.price.solusdt (Binance SOL/USDT)