# 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.`. 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-исходнике выше; чтобы посмотреть глазами — вставить её в (или отрендерить `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. Где это живёт и как проверить **Прод:** - фронт: (реальный внешний домен) - цена по REST: - цена по WebSocket: `wss://testfront.binom.pw/api/ws` - состояние игры: **Проверка (без графики):** ```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.