From d9b1cc1ed80e32b9fbf8e6184e4fdbfbfa93ee47 Mon Sep 17 00:00:00 2001 From: assistant Date: Mon, 14 Sep 2026 00:20:12 +0300 Subject: [PATCH] =?UTF-8?q?docs(api):=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=20API=20=D0=B4=D0=BB=D1=8F=20=D0=BA=D0=BB?= =?UTF-8?q?=D0=B8=D0=B5=D0=BD=D1=82=D0=B0=20=E2=80=94=20HTTP=20+=20WS-?= =?UTF-8?q?=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API.md: полное описание ручек, трёх WS-событий (price/bet_open/bet_closed), полей с масштабами, ошибок (409 = уже есть открытая ставка), авто-закрытия. Все значения сверены с живым стендом. README: убрано устаревшее 'поллинг до SETTLED' — поллинга нет, события идут по WS. Отдельно задокументирован SEC-9: ~33.6% ставок (замер на 580 мин) зависают из-за exit==entry и вечного ретрая. --- API.md | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 +- 2 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 API.md diff --git a/API.md b/API.md new file mode 100644 index 0000000..4bc97ac --- /dev/null +++ b/API.md @@ -0,0 +1,176 @@ +# 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 из фаусета, контракт наш. + +--- + +## Короткий рецепт: «поставить и получить события» + +```js +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` — состояние контракта +```json +{ + "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 +```json +{"type":"price","symbol":"solusdt","price":"101.02000000","ts":1789326824749} +``` +`price` — строка, чтобы не терять точность. Тик — по мере прихода из NATS (медиана ~0.5 с, +провалы до нескольких секунд). + +### 2. `bet_open` — ставку поставили (игра началась) +```json +{"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` — ставку рассчитали (игра закончилась) +```json +{"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) | diff --git a/README.md b/README.md index 464ceec..4f2e858 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ Unity-фронт (`fun-game-front2`) хорош для продакшена, н - **Пополнить** — swap SOL→фантики через `POST /api/pump/swap` (подпись в Phantom). - **Вывести** — swap фантики→SOL. - **Ставка** (вверх/вниз) в фантиках через контракт: `GET /api/state` → - подпись create_bet → `POST /api/bet` → поллинг до SETTLED → показ выигрыша. + `POST /api/bet` → **события по WebSocket** (`bet_open` при постановке, `bet_closed` при + расчёте). Поллинг не нужен: шлюз закрывает ставку сам по таймеру. + Полный контракт API (HTTP + WS, поля событий, ошибки) — **[API.md](API.md)**. - Лог всех действий + кнопки «Копировать» / «Очистить». ## Как пользоваться