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

8.6 KiB
Raw Permalink Blame History

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)

@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. Где это живёт и как проверить

Прод:

Проверка (без графики):

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.