docs: ADR-008 (реализация цены из NATS) + PRICE-NATS.md + PlantUML (фаза 7)
ADR-007 (выбор источника binance-market/NATS) пришёл из другой сессии — оставлен как есть, с апдейт-сноской. Реализация зафиксирована отдельным ADR-008: NATS-консьюмер в шлюзе, цена x1e8 строковым парсером, раздача фронту по WebSocket, деплой в k3s на testfront.binom.pw.
This commit is contained in:
@@ -8,7 +8,23 @@
|
||||
|
||||
---
|
||||
|
||||
## ⭐ Актуальное состояние (2026-09-09)
|
||||
## ⭐ Актуальное состояние (2026-09-13)
|
||||
|
||||
**Где мы:** цена SOL — **реальная, из NATS**, шлюз и тестовый фронт крутятся в k3s на
|
||||
`testfront.binom.pw`. Первый щуп экономики закрыт: ставка → раунд → результат считается
|
||||
по живой рыночной цене.
|
||||
|
||||
**Новое (2026-09-13):**
|
||||
- Цена SOL берётся из NATS (`market.price.solusdt`), заглушка удалена — **ADR-007**,
|
||||
детали в `docs/PRICE-NATS.md`.
|
||||
- Шлюз раздаёт цену фронту по WebSocket: `wss://testfront.binom.pw/api/ws`.
|
||||
- `test-front` показывает живую цену SOL.
|
||||
- Оба контейнера (статика + шлюз) — в k3s, ns `game`, один домен через ингресс.
|
||||
- Solana localnet пересобрана с нуля, контракт задеплоен (`9ALs…`).
|
||||
|
||||
---
|
||||
|
||||
## ⭐ Предыдущее состояние (2026-09-09)
|
||||
|
||||
**Где мы:** Фаза 1 (контракт переведён на фантики) — **DONE**. Сейчас вход в **Фазу 2 (релейер под токен)**.
|
||||
|
||||
@@ -28,7 +44,7 @@
|
||||
## Фаза 0 — Экономика и спека (кросс)
|
||||
- [ ] **TODO** ADR-002 зафиксирован, документировать `ECONOMY.md`: токен, курс (Способ 1), decimals=6, mint-адреса — `[pump.meta]`
|
||||
- [x] **DONE** Решить «вверх/вниз» — предмет ставки = цена SOL (ADR-006), валюта = фантики
|
||||
- [ ] **IN_PROGRESS** Цена SOL: реальный источник (ADR-007) — `subochev/binance-market` (NATS `market.price.solusdt` или WS `/ws/price?symbol=solusdt`); остаётся интеграция `PriceSource` в релейере
|
||||
- [x] **DONE** Цена SOL: реальный источник (ADR-007) — `subochev/binance-market` через NATS `market.price.solusdt`; интеграция `PriceSource` в релейере сделана (ADR-008, 2026-09-13)
|
||||
- [ ] **TODO** Кто держит ликвидность swap и комиссии — `[pump.meta]`
|
||||
- [ ] **TODO** Эмиссия: разовая vs постоянная — `[pump.meta]`
|
||||
|
||||
@@ -38,12 +54,13 @@
|
||||
- [x] **DONE** Выплата в `close_bet` фантиками из ATA-ваулта на ATA игрока
|
||||
- [x] **DONE** `state`/`constants`: единицы = token units (decimals=6), + VAULT_SEED
|
||||
- [x] **DONE** Обновить IDL (idl/ + relayer/idl/) + Anchor-тесты (10/10) — коммит 45d64d4
|
||||
- [ ] **TODO** Развернуть/поднять localnet стенд для E2E токен-флоу (приёмка на железе)
|
||||
- [x] **DONE** Localnet стенд поднят заново (ledger пересобран), контракт задеплоен, mint создан, `init` прогнан (2026-09-13)
|
||||
- [x] **DONE** E2E живьём: ставка → close по реальной цене из NATS (`entryPrice=10195000000` при курсе 101.95)
|
||||
|
||||
## Фаза 2 — Релейер и цена (АКТИВНАЯ — следующий шаг)
|
||||
- [ ] **IN_PROGRESS** SolanaClient/ops: подписывать/отправлять токеновую ставку (ATA + ваулт) — перевести релейер с SOL на токен
|
||||
- [ ] **TODO** readGlobal: баланс vault_token вместо SOL; ensureVaultFunded — наливать токен в vault
|
||||
- [ ] **IN_PROGRESS** PriceSource: реальная цена (вместо `RandomPriceSource`) — интеграция с `binance-market` (ADR-007); реализация NATS-консьюмера `market.price.solusdt`
|
||||
- [x] **DONE** PriceSource: реальная цена (вместо `RandomPriceSource`) — интеграция с `binance-market` (ADR-007), NATS-консьюмер `market.price.solusdt` в `src/natsPrice.ts`, цена ×1e8 (ADR-008)
|
||||
- [ ] **TODO** Интеграционный тест релейера на токеновом флоу
|
||||
|
||||
## Фаза 4 — Бэкенд: задействовать swap
|
||||
@@ -60,8 +77,9 @@
|
||||
- [x] **DONE** Каркас: Phantom, балансы (фантики+SOL), swap пополнение/вывод через `/api/pump/swap`
|
||||
- [x] **DONE** Ставка вверх/вниз через контракт с поллингом до SETTLED
|
||||
- [ ] **TODO** Перевести подпись под токеновую версию контракта (после Фазы 1)
|
||||
- [ ] **TODO** Реальный провайдер цены вместо RandomPriceSource (за нами) → `binance-market` (ADR-007)
|
||||
- [ ] **TODO** Развернуть / дать постоянный URL (nginx/podman) — удобно открывать
|
||||
- [x] **DONE** Реальный провайдер цены вместо RandomPriceSource (за нами) → `binance-market` через NATS (ADR-007/ADR-008)
|
||||
- [x] **DONE** Живая цена SOL по WebSocket (`wss://testfront.binom.pw/api/ws`), вывод на странице — релиз 2
|
||||
- [x] **DONE** Развернуть / дать постоянный URL — `https://testfront.binom.pw` (k3s, ns `game`, ингресс; релиз 2)
|
||||
|
||||
## Фаза 6 — E2E и деплой
|
||||
- [ ] **TODO** Полный круг на devnet: создать токен → swap ввод → ставка токеном → раунд → вывод
|
||||
@@ -72,4 +90,18 @@
|
||||
|
||||
## Ссылки
|
||||
- Решения/экономика: `docs/DECISIONS.md`, `docs/ECONOMY.md`
|
||||
- Карта репозиториев: `README.md`
|
||||
- Карта репозиториев: `README.md`
|
||||
## Фаза 7 — Цена из NATS и деплой в куб (2026-09-13)
|
||||
- [x] **DONE** ADR-007: цена SOL из NATS (`market.price.solusdt`), заглушка удалена
|
||||
- [x] **DONE** `natsPrice.ts` — подписка NATS, последняя цена, `waitForPrice`, ×1e8 строковым парсером
|
||||
- [x] **DONE** `gateway.ts` — WS `/ws`, `GET /price`, нормализация `/api/...` → `/...`
|
||||
- [x] **DONE** `test-front` — блок «Цена SOL (WebSocket)»: цена, статус, автопереподключение
|
||||
- [x] **DONE** Dockerfile + helm для шлюза и фронта; деплой в k3s ns `game`
|
||||
- [x] **DONE** Роут `testfront.binom.pw` переведён с 76.199 на куб 76.120 (Traefik VDS)
|
||||
- [x] **DONE** Релизы: `smart-updown-token` 7, `test-front` 2
|
||||
- [x] **DONE** Документация: `docs/PRICE-NATS.md`, `docs/diagrams/price-flow.puml`, ADR-007
|
||||
- [ ] **TODO** Пилот: смена `entry_price` на «реальный источник как оракул» до боя (Pyth/Switchboard)
|
||||
- [ ] **TODO** Деградация при отсутствии цены: отдельное решение (бэкфилл/остановка приёма ставок)
|
||||
|
||||
## Ссылки (дополнено 2026-09-13)
|
||||
- Цена: `docs/PRICE-NATS.md`, диаграмма `docs/diagrams/price-flow.puml`
|
||||
|
||||
+36
-1
@@ -96,4 +96,39 @@
|
||||
- **Результат:** задачи «TODO Цена SOL: реальный источник» (Фаза 0) и «PriceSource: реальная цена
|
||||
(вместо RandomPriceSource)» (Фаза 2) переводятся в `IN_PROGRESS` со ссылкой на это решение.
|
||||
Никаких новых подписок/оракулов не делаем — только интеграция в релейере (`smart-updown-token`/
|
||||
`relayer` или `smart-updown-server`). Внешний статус сервиса см. README → «Внешние зависимости».
|
||||
`relayer` или `smart-updown-server`). Внешний статус сервиса см. README → «Внешние зависимости».
|
||||
**→ Обновление 2026-09-13: реализовано, см. ADR-008** (NATS-консьюмер в шлюзе, цена ×1e8,
|
||||
раздача фронту по WS).
|
||||
---
|
||||
|
||||
## ADR-008 — Реализация: цена SOL из NATS в шлюзе + раздача фронту по WS
|
||||
- **Дата:** 2026-09-13
|
||||
- **Статус:** принято, реализовано
|
||||
- **Контекст:** ADR-006 зафиксировал предмет ставки (цена SOL), ADR-007 выбрал источник
|
||||
(`binance-market` через NATS). Оставалось реализовать: сама цена была случайной
|
||||
заглушкой `RandomPriceSource` / `simulated = 100_000_000 + Math.random()`. Исход раунда был
|
||||
случаен, а не рыночен. При этом в контуре уже работает `binance-market` — сборщик сделок с
|
||||
Binance, который публикует каждую цену в NATS (`market.price.<symbol>`).
|
||||
- **Решение:** игровой шлюз (`smart-updown-token/relayer`) подписывается на NATS
|
||||
(`market.price.solusdt`) и берёт цену оттуда. Заглушка удалена.
|
||||
- **Транспорт:** core NATS `nats://192.168.88.93:4222`, subject `market.price.solusdt`.
|
||||
Режим — **только чтение**; `binance-market` не трогаем.
|
||||
- **Шкала:** строка из NATS переводится в целое ×**1e8** строковым парсером (без float):
|
||||
`"101.89000000"` → `10189000000`. То же юнит-пространство, что у оракульных цен и у
|
||||
прежней заглушки (`100_000_000`), поэтому контракт сравнивает цены корректно.
|
||||
- **Раздача фронту:** шлюз транслирует каждый тик по WebSocket (`/ws`, снаружи
|
||||
`wss://testfront.binom.pw/api/ws`), сообщение — только цена:
|
||||
`{"type":"price","symbol":"solusdt","price":"102.30...","ts":...}`.
|
||||
- **Отсутствие цены:** считается аварийным состоянием и обрабатывается мягко — шлюз
|
||||
стартует без NATS, запрос цены ждёт таймаут, фронт показывает последнее значение и
|
||||
переподключается. Отдельный сценарий деградации — тема для будущего решения.
|
||||
- **За:** исход раунда становится рыночным (entry/exit — реальные цены Binance);
|
||||
переиспользуется уже работающий сборщик, торговый контур не дублируется; шина отделяет
|
||||
источник цены от её потребителей (игра, аналитика, будущие сервисы).
|
||||
- **Против:** появляется зависимость от NATS и от `binance-market`; core NATS не хранит
|
||||
историю (нет JetStream) — при разрыве подписки цена не «догоняется», нужен живой поток.
|
||||
- **Альтернативы:** (а) прямая подписка шлюза на Binance WS — дублирование сборщика, лишние
|
||||
внешние коннекты; (б) оракул on-chain (Pyth/Switchboard) — правильнее для боя, но
|
||||
дороже и требует фидов; (в) заглушка — отклонено как единственный вариант, оставлена
|
||||
возможность (`setPriceOverride`) для тестов.
|
||||
- **Детали и диаграммы:** `docs/PRICE-NATS.md`, `docs/diagrams/price-flow.puml`.
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,43 @@
|
||||
@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 market.price.solusdt
|
||||
NATS --> GW : subscribe (только чтение)
|
||||
GW --> SOL : create_bet(entry_price)\nclose_bet(exit_price)
|
||||
GW --> FE : WS /api/ws {"type":"price"}
|
||||
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
|
||||
Reference in New Issue
Block a user