Files
test-front/API.md
T
Caffeine 07e00f106c fix(api): рабочий рецепт — /api в базе обязателен для POST
Найдено исполнением рецепта из самого файла: `const BASE = 'https://testfront.binom.pw'`
→ `POST /wallet` возвращал 405 Method Not Allowed. GET-ручки терпят отсутствие
префикса (`/state` == `/api/state`), а POST-ручки — нет. Внешний разработчик
спотыкался на первой же строке примера.

Что исправлено:
- база в примере: `https://testfront.binom.pw/api` + предупреждение про /api
- POST /wallet в примере: добавлены заголовок Content-Type и тело `{}` (без них 405)
- WS-адрес в примере: явный `wss://` вместо `BASE.replace('https','wss')`,
  который после смены базы дал бы кривой `wss://testfront.binom.pw/api.replace…`
- amountUnits в примере — строкой (поддержаны оба вида, число тоже работает)

Проверено: рецепт, выдранный из файла регуляркой и запущенный как есть, отдаёт
курс, `bet_open` и `bet_closed`. WS принимает подключение с чужого Origin
(проверено тремя вариантами заголовка).
2026-09-14 02:32:32 +03:00

9.8 KiB
Raw Blame History

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

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

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

⚠️ /api в базе обязателен. GET-ручки (/state, /price, /balance, /faucet) отвечают и без префикса — …/state и …/api/state одно и то же. Но POST-ручки (/wallet, /bet) без /api не работают: POST …/wallet → 405 Method Not Allowed. Держите /api в базе, и всё будет единообразно.

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


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

const BASE = 'https://testfront.binom.pw/api';   // /api обязателен, см. выше
const H = { 'Content-Type': 'application/json' };

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

// 2. Подписка на события
const ws = new WebSocket('wss://testfront.binom.pw/api/ws');   // только wss: страница по HTTPS
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: H,
  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"}
{"type":"bet_closed","id":"3","bettor":"8KbUFCHV…",
 "exitPrice":10105000000,"status":3,"statusName":"refunded"}
  • exitPrice — цена выхода, тот же масштаб 1e8.
  • statusName: payout_done (игрок выиграл, выплата с множителем) | house_won (выиграл дом, ставка остаётся в ваулте) | refunded (ничья при exitPrice == entryPrice: залог возвращён игроку ровно суммой ставки, без множителя) | open (ещё открыта).

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

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

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


Ничья при exitPrice == entryPrice

Если цена на момент экспирации в точности равна цене входа, ставка не зависает: контракт возвращает игроку ровно сумму ставки без множителя, ставка переходит в статус 3, по WS уходит bet_closed со statusName: "refunded".

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

Рекомендация фронту: обрабатывать три исхода bet_closed (payout_done / house_won / refunded) и не показывать refunded как проигрыш.

Исторически (до 14.09.2026) такие ставки зависали навсегда; разбор — 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)