Files
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

179 lines
9.8 KiB
Markdown
Raw Permalink 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`
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 из фаусета, контракт наш.
---
## Короткий рецепт: «поставить и получить события»
```js
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` — состояние контракта
```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`. Клиент **не должен** ничего закрывать сам.
---
## Ничья при `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) |