Files
pump-game-ops/docs/DECISIONS.md
T
Hermes Agent 21a3a37a66 docs: ADR-009/010, карта деплоя, деплой-памятка фронта, задачи фронтендеру
Приёмка и починка 15.09.2026:
- ADR-009: один бэк на оба домена — прод-домен smart-updown.binom.pw переведён на тот же
  TS-шлюз (:8895), что и testfront; Kotlin-бэк (3NWEK) выключен обратимо. Разобраны расхождения
  API игры и шлюза (их должны править во фронте).
- ADR-010: две ловушки деплоя фронта — (1) чарт рендерит только path '/', поэтому helm-релиз
  сносит /api,/ws → вынесены в отдельный ingress smart-updown-api вне helm;
  (2) захардкоженный values.image.tag бьёт Chart.AppVersion → helm upgrade НЕ поднимает новый
  образ (новый RS не создаётся), на домене остаётся старый билд при 'Upgrade complete'.
  Приведена правильная команда (--reuse-values --set image.tag=<N>) и способ ПРОВЕРКИ по кубу
  и домену, а не по helm. Отмечен остаточный риск: Unity-ассеты immutable 7 суток при
  неизменяемых именах файлов → кэш браузера держит старый билд.
- README: карта деплоя (кто где живёт), статус проекта на 15.09, актуализирован блок
  безопасности (SEC-16 — утечка SOL, блокер перед mainnet), Kotlin-бэк помечен выключенным,
  добавлен TS-шлюз как боевой бэк, ссылки на tools/ и k8s/.
- TASKS.md: деплой-памятка фронта + задачи фронтендеру (убрать image.tag, хэш в именах
  ассетов, привести API игры к контракту шлюза).
2026-09-15 12:13:23 +03:00

206 lines
20 KiB
Markdown
Raw 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.
# DECISIONS.md — журнал решений (ADR)
Каждое решение — короткая запись, чтобы потом не гадать, почему всё устроено так.
Формат: дата, статус (принято/рассмотрение/заменено), суть, аргументы за/против, результат.
---
## ADR-001 — Используем свой токен на pump.fun (мемкойн Binary Rocket)
- **Дата:** 2026-09-09
- **Статус:** принято
- **Суть:** выпускаем СВОЙ токен на pump.fun, а не пользуемся чужим.
- **За:** бренд/маркетинг, контроль экономики, стимул игрокам, единая валюта ставок.
- **Против:** надо эмитировать и следить за курсом; чужой токен дал бы меньше работы.
- **Решение:** свой токен. decimals = 6, Token-2022, создание через `create_v2` (по доке pump.fun).
## ADR-002 — Курс живой рыночный (Способ 1)
- **Дата:** 2026-09-09
- **Статус:** принято
- **Суть:** вне игры баланс — в фантиках, курс берётся с рынка (bonding curve/AMM pump.fun).
Ввод = swap SOL→фантики, вывод = swap фантики→SOL. Никакой фикс-привязки/стейблока.
- **За:** нативно pump.fun, привычно игроку-трейдеру, технически проще, маркетинг (рост/падение токена).
- **Против:** баланс «гуляет» с рынком; определённым аудиториям неудобно.
- **Решение:** Способ 1. (Это и диктует, что ставки идут фантиками через контракт.)
## ADR-003 — Ставки идут фантиками через контракт
- **Дата:** 2026-09-09
- **Статус:** принято (вытекает из ADR-002)
- **Суть:** игрок ставит токен, контракт держит фантики в своём ATA-ваулте и платит фантиками.
- **Следствие:** контракт переводится с нативного SOL (lamports) на SPL-токен — это ядро Фазы 1.
- **Против:** трогаем хорошо работающий контракт; но без этого сценарий ввода-игры-вывода не работает.
## ADR-004 — Ввод/вывод через pump.fun swap (`POST /agents/swap`)
- **Дата:** 2026-09-09
- **Статус:** принято
- **Суть:** пополнение = `inputMint: NATIVE_MINT(SOL), outputMint: наш_mint`; вывод = наоборот.
Клиент на бэкенде (Ktor `PumpApiClient`) уже готов в `smart-updown-server`.
- **За:** возвращает готовую подписанную swap-транзакцию, бэкенд не держит ликвидность.
- **Против:** весь ввод/вывод завязан на доступность pump.fun API.
## ADR-05 (идея) — мета-репозиторий для документации и трекинга
- **Дата:** 2026-09-09
- **Статус:** принято
- **Суть:** создать `pump-game-ops`, где живут карта репозиториев, решения, экономика и список задач.
- **За:** единая точка оркестрации, не плаваем по чату. **Против:** риск превратиться во второй трекер.
---
## ADR-005 — Флоу входа/вывода подтверждён (как описал юзер)
- **Дата:** 2026-09-09
- **Статус:** принято
- **Суть:** человек заходит в игру → видит баланс 0 → кнопка «Пополнить» (одна, без выбора) →
swap SOL→фантики → баланс появляется → ставит фантики через контракт (честное списание/начисление
по исходу) → при выходе нажимает «Вывести» → swap фантики→SOL, баланс уменьшается.
- **За:** точный пользовательский сценарий, подтверждённый владельцем; простая UX-модель.
- **Решение:** именно так и работаем — это источник требований для Фазы 1 (контракт на токене) и Фазы 5 (UI).
---
## ADR-006 — Предмет ставки: цена SOL (валюта — фантики)
- **Дата:** 2026-09-09
- **Статус:** принято
- **Суть:** валютой ставки являются наши фантики (токен Binary Rocket), но ставка делается на
**направление цены SOL** (вверх/вниз) — как и в текущей версии игры. Контракт по-прежнему
сравнивает entry/exit_price цены SOL.
- **Цена SOL сейчас** — случайная заглушка (`RandomPriceSource`). Позже заменяется на реальную
(провайдер/оракул). Это отдельная задача, не блокирует Фазу 1 (контракт от способа получения цены не зависит).
**→ Обновление 2026-09-11:** реальный источник выбран — см. ADR-007.
- **Следствие:** фантики = касса/валюта ввода-вывода; контракт оперирует токеном, но исход считает
по цене SOL. Экономика токена (ADR-002 живой курс) не влияет на логику «вверх/вниз».
---
## ADR-007 — Источник цены SOL = upstream-сервис `binance-market`
- **Дата:** 2026-09-11
- **Статус:** принято
- **Суть:** цена SOL для `PriceSource` берётся из уже работающего в инвест-кластере сервиса
`binance-market` (репо `subochev/binance-market`), а не из своей оракуль-логики и не из прямого
опроса Binance. Контракт между игрой и сервисом — публичный:
- **NATS-подписка** на топик `market.price.solusdt` (предпочтительно, в кластере); либо
- **WebSocket** `ws://<binance-market>:8080/ws/price?symbol=solusdt` (fallback / внешние сети).
Источник цены внутри `binance-market` — last-trade (`aggTrade.p` с биржи Binance), тики идут
на каждой сделке → фактически real-time.
- **За:**
- Уже работает и развёрнут в кластере `kube` (ns `invest`), chart `binom/binance-market`,
последний релиз v10. Не надо поднимать ещё один процесс ради цены.
- Есть и NATS, и WS-контракт — обе формы потребления готовы.
- Покрытие `solusdt` уже в списке символов; дополнительных подписок не требуется.
- Цена приходит в тиках сделок (не сэмплинг раз в N секунд) — лучшее разрешение для
сравнения entry/exit в round-based игре.
- Общий сервис инвест-кластера, используется не только этой игрой; его судьба — не наша забота.
- **Против:**
- Один источник (Binance). Если Binance недоступен — игра без цены. Заглушка-fallback
на `RandomPriceSource` остаётся как dev-only.
- Зависимость по латентности от стороннего сервиса. В кластере это несколько мс,
приемлемо.
- **Результат:** задачи «TODO Цена SOL: реальный источник» (Фаза 0) и «PriceSource: реальная цена
(вместо RandomPriceSource)» (Фаза 2) переводятся в `IN_PROGRESS` со ссылкой на это решение.
Никаких новых подписок/оракулов не делаем — только интеграция в релейере (`smart-updown-token`/
`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`.
## ADR-009 — Один бэк на оба домена: прод-домен переведён на TS-шлюз (2026-09-15)
- **Дата:** 2026-09-15
- **Статус:** принято, реализовано
- **Контекст:** на `smart-updown.binom.pw` (Unity-игра) стоял **канонический Kotlin-бэк**
`smart-updown-server` с programId `3NWEK…`, а на `testfront.binom.pw` — **TS-шлюз**
`updown-relayer` (:8895), кастодиальный, на контракте `9ALs…`. Два бэка = два контракта =
разные цепочки состояния: у Kotlin-бэка вообще НЕТ WebSocket, а его программа в цепи
отсутствовала (`getAccountInfo` → `null`), поэтому `/api/state` отдавал
`global state is not initialized`. Игра при этом читала баланс напрямую из цепи через свой
nginx-прокси `/solana-rpc/getBalance` — то есть жила в третьей реальности.
- **Решение:** **не деплоить `3NWEK` под Kotlin**, а повесить игровой домен на **тот же** TS-шлюз,
что и тестовый: `/api` и `/ws` → `testfront-gateway-smart-updown-relayer:8895`. Формулировка
владельца: «тестовый фронт мы для этого и делали»; игру переделают под API шлюза — расхождение
контрактов API не блокер.
- Kotlin-бэк выключен **обратимо**: `replicas=0` + его ingress удалён (иначе два ingress'а на
один хост+path = недетерминированный роутинг Traefik).
- Бэкап состояния до правки — `/root/bak-smart-updown-15.09/` на 192.168.76.120.
- **За:** один бэк = одно состояние на оба стенда (счётчик 55=55, касса 29320); у игры
появляется живой WS; тестовый фронт больше не «отдельный мир»; откат — две команды.
- **Против:** API шлюза и ожидания игры расходятся (`minAmount`/`maxAmount` строкой против
`minAmountLamports`/`entryPrice`; тело ставки `{address, side, amountUnits}` против
`{side, amount, entryPrice, betId, signedTx}`; `GET /api/bet/{id}` у шлюза нет) — игру надо
править. Принято осознанно.
- **Что осталось не сделано:** программа `3NWEK` в цепь так и не задеплоена (ключ
`/root/game_program_kp.json`, `.so` 217496 б лежат в LXC 151; на адрес аирдропнуто 5 SOL).
Это задел на будущее, не потеря.
## ADR-010 — `/api`,`/ws` вынесены в ingress вне helm; образ фронта поднимается явным `--set image.tag` (2026-09-15)
- **Дата:** 2026-09-15
- **Статус:** принято, реализовано
- **Контекст — две независимые ловушки, обе пойманы живьём:**
1. Чарт `fun-game-front2` (`helm/templates/ingress.yaml`) рендерит **ровно один** path `/`
(жёстко, host из `values.yaml`) — значений для `/api`,`/ws` в чарте НЕТ. Значит **любой**
`helm upgrade` игры перерисовывает ingress и **сносит** `/api`,`/ws`: домен начинает отдавать
HTML игры на `/api/*` (nginx 405 на `POST /wallet`, `/api/state` = HTML Unity вместо JSON).
2. Шаблон деплоя объявляет образ как
`image: "{{ Values.image.name }}:{{ default Chart.AppVersion Values.image.tag }}"`, а в
`helm/values.yaml` **захардкожен `image.tag`** (был `26`). Непустой `Values.image.tag`
побеждает `appVersion`, поэтому `helm upgrade` на чарт `28` отрендерил под со **старым**
тегом `26`: спека не изменилась, новый ReplicaSet **не создался**, старый под (от 8 сентября)
продолжал жить. Внешне — «на домене открывается старый билд» при зелёном CI и
`Upgrade complete` в истории helm.
- **Решение:**
1. `/api` и `/ws` живут в **отдельном ingress `smart-updown-api`** (манифест
`k8s/smart-updown-api-ingress.yaml`), front2-ingress остаётся `paths: [/]`. Релиз игры
больше не может их снести.
2. Фронт игры поднимать **всегда** явным тегом и с сохранением values:
`helm upgrade fun-game-front2 binom/fun-game-front2 -n game --reuse-values --set image.tag=<N> --wait`.
Без `--reuse-values` затираются `imagePullSecrets: regcred` и `ingress.host`.
- **Проверка результата — по КУБУ и домену, а не по helm** (helm отрапортует «успех» и в случае 2):
```bash
kubectl get pod -n game -l app.kubernetes.io/name=fun-game-front2 \
-o custom-columns='NAME:.metadata.name,IMAGE:.spec.containers[0].image,CREATED:.metadata.creationTimestamp'
kubectl exec -n game <pod> -- ls -la /usr/share/nginx/html/Build/ # дата файлов = дата билда
curl -sI https://smart-updown.binom.pw/Build/WebGL.data | grep -i last-modified
kubectl -n game get ingress \
-o custom-columns='NAME:.metadata.name,HOST:.spec.rules[0].host,PATHS:.spec.rules[0].http.paths[*].path'
```
- **Против / остаточный риск (не забыть):** в `nginx.conf` все Unity-ассеты объявлены
`Cache-Control: public, max-age=604800, immutable`, а имена файлов (`WebGL.data`, `WebGL.wasm`,
`WebGL.framework.js`) между сборками **НЕ меняются**. Браузер, уже игравший, будет до 7 суток
отдавать старые ассеты из кэша, игнорируя новый билд (`index.html` спасает `no-store`, ассеты — нет).
Лечится сбросом кэша/инкогнито у игрока. **Правильно на будущее** — хэш содержимого в имени
файла на шаге сборки (CI), тогда `immutable` становится корректным.
- **Альтернативы:** (а) патчить чарт фронта (добавить в него `/api`,`/ws`) — чужой репозиторий и
чужой релизный цикл; (б) держать образ на `latest` — нет воспроизводимости; (в) убрать `image.tag`
из `values.yaml` и опираться только на `Chart.AppVersion` — правильнее, но это правка чужого репо
(отдать фронтендеру, см. `TASKS.md`).
---