# 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://: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.`). - **Решение:** игровой шлюз (`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= --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 -- 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`). ---