Files
test-front/API.md
T
Caffeine 30a17f7d99 feat(ui): третий исход в UI — возврат залога при ничьей
onBetClosed показывал бинарно: всё, что не payout_done, — «ПРОИГРАЛ (house_won)»
красным. Ничья (деньги вернулись игроку) выглядела как проигрыш.

Теперь три исхода + нейтральная заглушка:
- payout_done → ВЫИГРАЛ (ok)
- house_won  → ПРОИГРАЛ (err)
- refunded   → ВОЗВРАТ (ничья, ставка возвращена) (info)
- иное       → РАССЧИТАНА (<statusName>) (info), не валится в проигрыш

API.md: пример bet_closed со status 3 + описание refunded.
Проверено: node --check инлайн-JS OK, образ smart/test-front:5, раздача наружу.
2026-09-14 02:09:15 +03:00

181 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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"}
```
```json
{"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`. Клиент **не должен** ничего закрывать сам.
---
## ⚠️ Известная проблема: часть ставок зависает без исхода
**Симптом:** пришло `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) |