docs(api): контракт API для клиента — HTTP + WS-события
API.md: полное описание ручек, трёх WS-событий (price/bet_open/bet_closed), полей с масштабами, ошибок (409 = уже есть открытая ставка), авто-закрытия. Все значения сверены с живым стендом. README: убрано устаревшее 'поллинг до SETTLED' — поллинга нет, события идут по WS. Отдельно задокументирован SEC-9: ~33.6% ставок (замер на 580 мин) зависают из-за exit==entry и вечного ретрая.
This commit is contained in:
@@ -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) |
|
||||||
@@ -17,7 +17,9 @@ Unity-фронт (`fun-game-front2`) хорош для продакшена, н
|
|||||||
- **Пополнить** — swap SOL→фантики через `POST /api/pump/swap` (подпись в Phantom).
|
- **Пополнить** — swap SOL→фантики через `POST /api/pump/swap` (подпись в Phantom).
|
||||||
- **Вывести** — swap фантики→SOL.
|
- **Вывести** — swap фантики→SOL.
|
||||||
- **Ставка** (вверх/вниз) в фантиках через контракт: `GET /api/state` →
|
- **Ставка** (вверх/вниз) в фантиках через контракт: `GET /api/state` →
|
||||||
подпись create_bet → `POST /api/bet` → поллинг до SETTLED → показ выигрыша.
|
`POST /api/bet` → **события по WebSocket** (`bet_open` при постановке, `bet_closed` при
|
||||||
|
расчёте). Поллинг не нужен: шлюз закрывает ставку сам по таймеру.
|
||||||
|
Полный контракт API (HTTP + WS, поля событий, ошибки) — **[API.md](API.md)**.
|
||||||
- Лог всех действий + кнопки «Копировать» / «Очистить».
|
- Лог всех действий + кнопки «Копировать» / «Очистить».
|
||||||
|
|
||||||
## Как пользоваться
|
## Как пользоваться
|
||||||
|
|||||||
Reference in New Issue
Block a user