Files
subochev 30f851fb87 docs: ADR-008 (реализация цены из NATS) + PRICE-NATS.md + PlantUML (фаза 7)
ADR-007 (выбор источника binance-market/NATS) пришёл из другой сессии —
оставлен как есть, с апдейт-сноской. Реализация зафиксирована отдельным
ADR-008: NATS-консьюмер в шлюзе, цена x1e8 строковым парсером, раздача
фронту по WebSocket, деплой в k3s на testfront.binom.pw.
2026-09-13 05:58:05 +03:00

214 lines
8.6 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.
# PRICE-NATS.md — откуда берётся цена SOL и как она доходит до фронта
**Репо:** `pump-game-ops` (мета-репо, документация). **Статус:** живой, отражает фактическое
состояние на 2026-09-13.
Документ отвечает на один вопрос: **откуда игра берёт цену SOL** и каким путём она доезжает
до экрана игрока. Раньше на этом месте стояла заглушка со случайным блужданием — теперь цена
**настоящая**, с биржи Binance, через шину NATS.
---
## 1. Суть в трёх строках
1. Сервис `binance-market` (живёт в k3s, ns `invest`) собирает сделки с Binance и **публикует**
каждую цену в NATS — в subject `market.price.<symbol>`.
2. Игровой шлюз (`smart-updown-token/relayer`) **подписывается** на `market.price.solusdt`,
берёт оттуда цену и:
- отдаёт её **контракту** как `entry_price` / `exit_price` (целое, цена × 1e8);
- **транслирует** её фронту по WebSocket.
3. Тестовый фронт (`test-front`) подключается к этому WebSocket и **просто выводит цену** —
игрок видит, что рынок живёт.
Направление ставки (UP/DOWN) больше НЕ случайное: контракт сравнивает `exit_price` с
`entry_price`, а обе величины теперь настоящие.
---
## 2. Диаграмма компонентов (PlantUML)
```plantuml
@startuml price-flow
title Цена SOL: Binance -> NATS -> шлюз -> контракт -> фронт
skinparam componentStyle rectangle
skinparam shadowing false
actor "Игрок" as User
rectangle "Внешняя сеть" {
[Binance WS API] as Binance
}
rectangle "k3s (192.168.76.120)" {
rectangle "ns invest" {
component "binance-market\n(сборщик цен)" as BM
}
rectangle "ns game" {
component "testfront-gateway\n(шлюз релейера, :8895)" as GW
component "testfront\n(статика test-front)" as FE
}
}
cloud "NATS\n192.168.88.93:4222" as NATS
database "Solana localnet\n192.168.76.181:8899" as SOL
Binance --> BM : WS-поток сделок
BM --> NATS : publish\nmarket.price.solusdt\n{"price":"102.30..."}
NATS --> GW : subscribe\n(только чтение)
GW --> SOL : create_bet(entry_price)\nclose_bet(exit_price)
GW --> FE : WS /api/ws\n{"type":"price","price":"102.30"}
FE --> User : «SOL: 102.30»
note right of GW
Цена из NATS -> целое x1e8
("102.30000000" -> 10230000000)
Без float: строковый парсер
end note
note bottom of NATS
Core NATS, без JetStream:
истории нет, только живая подписка
end note
@enduml
```
Диаграмма в PlantUML-исходнике выше; чтобы посмотреть глазами — вставить её в
<https://www.plantuml.com/plantuml/uml/> (или отрендерить `plantuml` локально). Исходник
лежит и отдельным файлом: `docs/diagrams/price-flow.puml`.
---
## 3. Последовательность (кто что делает по шагам)
```plantuml
@startuml price-sequence
title Одна ставка: от тика Binance до результата
participant "binance-market" as BM
participant "NATS" as N
participant "Шлюз (gateway.ts)" as GW
participant "Контракт smart_updown" as C
participant "Фронт (test-front)" as FE
== фон: цена течёт всегда ==
BM -> N : market.price.solusdt {"price":"101.95","ts":...}
N -> GW : подписка: новый тик
GW -> GW : priceStringToInt("101.95...") = 10195000000
GW -> FE : WS {"type":"price","price":"101.95000000"}
FE -> FE : вывести «SOL: 101.95»
== игрок ставит ==
FE -> GW : POST /api/bet {side:"UP", amountUnits}
GW -> N : (ждёт цену, если ещё не пришла)
GW -> C : create_bet(entry_price = 10195000000)
C --> GW : tx ok, id = N
GW --> FE : {id, bettor, entryPrice}
== игрок закрывает (после expiry ~15с) ==
FE -> GW : POST /api/close {id}
GW -> C : close_bet(exit_price = текущая цена)
C -> C : exit > entry ? игрок : дом
C --> GW : status: payout_done | house_won
GW --> FE : {statusName, exitPrice}
@enduml
```
---
## 4. Контракт с NATS (что именно приходит)
- **URL:** `nats://192.168.88.93:4222` (доступен из k3s-подов — проверено).
- **Subject:** `market.price.solusdt` (игра интересуется **только** Solana).
- **Payload** (живой пример):
```json
{
"symbol": "solusdt",
"type": "price",
"price": "101.89000000",
"qty": "0.07900000",
"tradeId": 672033931,
"eventTime": 1789266205660,
"ts": 1789266205814,
"initial": false
}
```
`price` — **строка**, доллары США, ровно 8 знаков после запятой. Это важно: перевод в целое
делается **строковым парсером**, а не `Number(price) * 1e8` — float на 8 знаках теряет точность,
а контракт сравнивает цены строго.
| Строка из NATS | Целое для контракта (×1e8) |
|---|---|
| `"101.89000000"` | `10189000000` |
| `"0.00000001"` | `1` |
| `"1234567.12345678"` | `123456712345678` |
| `"100"` | `10000000000` |
---
## 5. Что изменилось в коде
| Файл | Было | Стало |
|---|---|---|
| `relayer/src/priceSource.ts` | `simulated = 100_000_000`, случайный дрейф `Math.random()` | `getPrice()` → цена из NATS × 1e8 |
| `relayer/src/natsPrice.ts` | не было | NATS-подписка, последняя цена, слушатели, `waitForPrice` |
| `relayer/src/gateway.ts` | только REST | + WS `/ws` (трансляция цены), + `GET /price`, нормализация `/api/...` → `/...` |
| `test-front/index.html` | цены не показывал | блок «Цена SOL (WebSocket)» + автопереподключение |
Совместимость: `setPriceOverride()` сохранён — им можно подставить цену вручную (тесты,
сценарий «цена не меняется»).
---
## 6. Что происходит, если цены нет
Сейчас это трактуется как **авария** (обсуждается отдельно). Поведение временное и простое:
- шлюз **не падает** — поднимается без NATS, логирует ошибку подключения;
- запрос цены **ждёт** появления до таймаута `PRICE_WAIT_TIMEOUT_MS` (по умолчанию 15 секунд),
потом отдаёт ошибку;
- фронт продолжает показывать последнее известное значение и переподключается к WS
с нарастающей паузой (3 → 15 секунд).
То есть отсутствие цены — не «падение сервиса», а заметная деградация, которую видно и в
логах шлюза, и на экране.
---
## 7. Где это живёт и как проверить
**Прод:**
- фронт: <https://testfront.binom.pw/> (реальный внешний домен)
- цена по REST: <https://testfront.binom.pw/api/price>
- цена по WebSocket: `wss://testfront.binom.pw/api/ws`
- состояние игры: <https://testfront.binom.pw/api/state>
**Проверка (без графики):**
```bash
curl -s https://testfront.binom.pw/api/price
# {"symbol":"solusdt","price":"102.09...","ts":...}
# WS (нужен любой ws-клиент), ожидаем поток:
# {"type":"price","symbol":"solusdt","price":"102.09...","ts":...}
```
**В k3s:**
```bash
kubectl get pods -n game | grep -E 'testfront|relayer'
kubectl logs -n game -l app.kubernetes.io/name=smart-updown-relayer | grep natsPrice
# nats: connected nats://192.168.88.93:4222
# natsPrice: first price 102.04000000 (ts=...)
```
**Роутинг:** `testfront.binom.pw` → Traefik на VDS `176.109.100.6` → `http://192.168.76.120`
(один хост, два контейнера: `/` — статика, `/api` и `/ws` — шлюз). Конфиг роутера:
`/opt/traefik/static/testfront.yaml` на VDS.