ADR-007 (выбор источника binance-market/NATS) пришёл из другой сессии — оставлен как есть, с апдейт-сноской. Реализация зафиксирована отдельным ADR-008: NATS-консьюмер в шлюзе, цена x1e8 строковым парсером, раздача фронту по WebSocket, деплой в k3s на testfront.binom.pw.
8.6 KiB
PRICE-NATS.md — откуда берётся цена SOL и как она доходит до фронта
Репо: pump-game-ops (мета-репо, документация). Статус: живой, отражает фактическое
состояние на 2026-09-13.
Документ отвечает на один вопрос: откуда игра берёт цену SOL и каким путём она доезжает до экрана игрока. Раньше на этом месте стояла заглушка со случайным блужданием — теперь цена настоящая, с биржи Binance, через шину NATS.
1. Суть в трёх строках
- Сервис
binance-market(живёт в k3s, nsinvest) собирает сделки с Binance и публикует каждую цену в NATS — в subjectmarket.price.<symbol>. - Игровой шлюз (
smart-updown-token/relayer) подписывается наmarket.price.solusdt, берёт оттуда цену и:- отдаёт её контракту как
entry_price/exit_price(целое, цена × 1e8); - транслирует её фронту по WebSocket.
- отдаёт её контракту как
- Тестовый фронт (
test-front) подключается к этому WebSocket и просто выводит цену — игрок видит, что рынок живёт.
Направление ставки (UP/DOWN) больше НЕ случайное: контракт сравнивает exit_price с
entry_price, а обе величины теперь настоящие.
2. Диаграмма компонентов (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. Последовательность (кто что делает по шагам)
@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 (живой пример):
{
"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
Проверка (без графики):
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:
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.