- корень smart-updown.binom.pw -> nginx на Proxmox (LXC 152, 192.168.76.152) - /api,/ws -> куб (router PathPrefix на внешнем Traefik VDS), куб не менялся - версионирование: /opt/game/releases/<N>/, активная = симлинк /opt/game/current - tools/proxmox-nginx: unpack-release.sh, publish-release.sh, game.nginx.conf, smart-updown.traefik.yaml, README - приёмка живьём: корень 200 nginx/1.24.0 (Ubuntu), /27/ /28/ 200, /api/state 200 JSON, /api/ws цена, autoclose-check 9/9 - питфоллы: OCI-manifest в skopeo dir-дампе, слэш в конце версии, /solana-rpc мимо PathPrefix /api
26 KiB
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; вывод = наоборот. Клиент на бэкенде (KtorPumpApiClient) уже готов в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.
- NATS-подписка на топик
- За:
- Уже работает и развёрнут в кластере
kube(nsinvest), chartbinom/binance-market, последний релиз v10. Не надо поднимать ещё один процесс ради цены. - Есть и NATS, и WS-контракт — обе формы потребления готовы.
- Покрытие
solusdtуже в списке символов; дополнительных подписок не требуется. - Цена приходит в тиках сделок (не сэмплинг раз в N секунд) — лучшее разрешение для сравнения entry/exit в round-based игре.
- Общий сервис инвест-кластера, используется не только этой игрой; его судьба — не наша забота.
- Уже работает и развёрнут в кластере
- Против:
- Один источник (Binance). Если Binance недоступен — игра без цены. Заглушка-fallback
на
RandomPriceSourceостаётся как dev-only. - Зависимость по латентности от стороннего сервиса. В кластере это несколько мс, приемлемо.
- Один источник (Binance). Если Binance недоступен — игра без цены. Заглушка-fallback
на
- Результат: задачи «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, subjectmarket.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, запрос цены ждёт таймаут, фронт показывает последнее значение и переподключается. Отдельный сценарий деградации — тема для будущего решения.
- Транспорт: core 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с programId3NWEK…, а на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.
- Kotlin-бэк выключен обратимо:
- За: один бэк = одно состояние на оба стенда (счётчик 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,.so217496 б лежат в LXC 151; на адрес аирдропнуто 5 SOL). Это задел на будущее, не потеря.
ADR-010 — /api,/ws вынесены в ingress вне helm; образ фронта поднимается явным --set image.tag (2026-09-15)
- Дата: 2026-09-15
- Статус: принято, реализовано
- Контекст — две независимые ловушки, обе пойманы живьём:
- Чарт
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). - Шаблон деплоя объявляет образ как
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.
- Чарт
- Решение:
/apiи/wsживут в отдельном ingresssmart-updown-api(манифестk8s/smart-updown-api-ingress.yaml), front2-ingress остаётсяpaths: [/]. Релиз игры больше не может их снести.- Фронт игры поднимать всегда явным тегом и с сохранением 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):
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).
ADR-011 — Статика игры вынесена из куба на nginx (LXC 152), версии — каталогами (2026-09-15)
Контекст. До этого и корень, и /api,/ws жили в кубе: внешний Traefik (VDS) вёл весь домен
smart-updown.binom.pw на k3s 192.168.76.120, статику отдавал под fun-game-front2
(nginx внутри образа), API — отдельный ingress smart-updown-api → TS-шлюз :8895.
Владелец решил разделить плоскости: бэк честно деплоится в куб, а сама игра (статика)
раздаётся с nginx, развёрнутого на Proxmox — со версионированием по каталогам (/<номер>/).
Решение:
- nginx на Proxmox — новый LXC 152 (
game-web, 192.168.76.152, Ubuntu 24.04, 2 ядра, 1 ГБ RAM, диск 8 GB наlocal-lvm,onboot=1, unprivileged). Отдельный LXC потому, что статика игры — «10 строк конфига и каталог», держать её в кубе рядом с бэком смысла нет. - Версионирование каталогами:
/opt/game/releases/<N>/— файлы сборки N (Build/,TemplateData/,index.html);/opt/game/current— симлинк на активную сборку;https://smart-updown.binom.pw/→ активная сборка,/28/…→ конкретная сборка.
- Маршрутизация на внешнем Traefik (VDS) — два router'а на один host, вместо одного:
smart-updown-api-router:Host(...) && (PathPrefix('/api') || PathPrefix('/ws'))→192.168.76.120(куб);smart-updown-router:Host(...)→192.168.76.152(nginx на проксмоксе). PathPrefix-роутер специфичнее Host-роутера, поэтому/api,/wsне перехватываются статикой. IP LXC 152 достижим с VDS через wg-туннель (192.168.76.0/22 dev wg0).
- В кубе ничего не менялось:
ingress smart-updown-api(/api,/ws→ шлюз) как был, вне helm. Подfun-game-front2остался жив, но домен он больше не обслуживает (статика ушла на nginx) — в куб его релизить больше не обязательно; релизы — только доставка статики на nginx. - Инструменты (
tools/proxmox-nginx/):unpack-release.sh(распаковка dir-дампа образа вreleases/<N>),publish-release.sh(тянет образ из реестра, раскладывает, по--activateпереключаетcurrent),game.nginx.conf,smart-updown.traefik.yaml.
Питфоллы, пойманные живьём:
skopeo copy docker://… dir:кладёт слои по sha256-digest, аmanifest.json— это OCI-манифест (словарьlayers[].digest, не список строк как в docker-archiv). Парситьjson['layers'][i]['digest'], а не печатать элемент целиком (иначеtar: Cannot connect to {'mediaType': ...}).- Голый шаблон
/28(без слэша) отдаёт 301 — этоtry_files ... /$build$subpath/, т.е. редирект на/28/. Для относительных путей Unity (src="Build/WebGL.loader.js") важно, чтобы браузерный URL заканчивался слэшем; иначеBuild/...уедет в/Build/...и вернётся 404. Ссылаться на версии только со слэшем (/28/), либо ставить редирект в nginx (return 301 /$build/). /solana-rpc/не перехватывается PathPrefix/api— это отдельный path, и на VDS он уходит в корневой router (nginx). Поэтому страховочныйlocation /solana-rpc/в nginx нужен: старые сборки игры стучатся именно туда (проверено:getHealth→okчерез домен).- Имена ассетов Unity внутри сборки не меняются (
WebGL.data/.wasm/.framework.js), но теперь URL содержит номер сборки →immutable-кэш больше не «прилипает» между релизами (закрыт остаточный риск из ADR-010). Внутри одной версии ассеты неизменяемы, т.к. каталог версии не перезаписывается.
Приёмка (живой домен, 2026-09-15): корень / → 200 от nginx/1.24.0 (Ubuntu) (LXC 152);
/27/,/28/ → 200, ассеты версий различаются (md5 WebGL.data: 76b6f3… vs 47453d…);
/api/state → 200 JSON (куб), /api/ws → [open] + {"type":"price"}; сквозной
tools/autoclose-check.cjs на BASE=https://smart-updown.binom.pw/api — 9/9; testfront не задет.
Откат: вернуть /opt/traefik/static/smart-updown.yaml из бэкапа VDS
(/root/bak-smart-updown-nginx-2026-09-15/) — домен снова целиком уйдёт в куб.
Альтернативы: (а) оставить статику в кубе и версионировать каталогами внутри образа — тянет пересборку образа на каждый билд игры и не решает вопрос кэша; (б) поднять в LXC не nginx, а podman-контейнер с nginx — лишний слой на 10 строках конфига; (в) отдавать статику прямо с k3s-Traefik по PVC — не то, о чём просил владелец (он хотел nginx на проксмоксе).