diff --git a/README.md b/README.md index 89857a6..2819ef2 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ |---|---|---|---|---|---| | 1 | **Смарт-контракт (оригинал)** | `akovalenko/Smart-game` | `~/WORK/games/Smart-game` | Rust/Anchor (Solana) | игровая логика ставок вверх/вниз, ваулт, выплата (**SOL**, не трогаем) | | 1b | **Смарт-контракт (фантики)** | `subochev/smart-updown-token` | `~/WORK/games/smart-updown-token` | Rust/Anchor (Solana) | копия Smart-game для перевода на наши фантики (SPL-токен) — **работаем тут** | -| 2 | **Бэкенд (релейер)** | `subochev/smart-updown-server` | `~/WORK/games/smart-updown-server` | Kotlin/Spring Boot + solanaj | ретрансляция ставок, свап-клиент к pump.fun, релейер | +| 2 | **Бэкенд (релейер) — ⏸ ВЫКЛЮЧЕН** | `subochev/smart-updown-server` | `~/WORK/games/smart-updown-server` | Kotlin/Spring Boot + solanaj | канонический Kotlin-бэк, programId `3NWEK…` (в цепь не задеплоен, WS нет). **Выключен 15.09.2026** (`replicas=0`, ingress удалён) — оба домена переведены на TS-шлюз; см. ADR-009 | +| 2b | **Игровой шлюз (боевой)** | `subochev/smart-updown-token` → `relayer/` | `~/WORK/games/smart-updown-token/relayer` | TypeScript (Node 22) | **кастодиальный TS-шлюз `:8895`** — реально обслуживает оба домена: ставки, кран, расчёт, WS. Контракт `9ALs…` | | 3 | **Фронт (Unity)** | `subochev/fun-game-front2` | `~/WORK/games/fun-game-front2` | Unity WebGL + Phantom | UI, кошелёк, подпись create_bet | | 4 | **Тестовый фронт** | `subochev/test-front` | `~/WORK/games/test-front` | HTML/JS + Phantom | технический дашборд для ручного теста экономики (пополнение/ставка/вывод) без Unity | | 5 | **Мета-репо (этот)** | `subochev/pump-game-ops` | `~/WORK/games/pump-game-ops` | Markdown | документация, решения, задачи | @@ -27,6 +28,9 @@ > Примечания: > - Remote-адреса: `https://git.binom.pw//.git`. > - Ветки: `smart-updown-server` и `fun-game-front2` — `main`; `Smart-game` сейчас на `extract-sources`. +> - ⚠️ **Актуальный бэк — TS-шлюз (пункт 2b), не Kotlin.** Kotlin-бэк выключен, но его код и образ +> целы: откат — `kubectl scale deploy -n game smart-updown-server-smart-updown-server --replicas=1` +> плюс возврат его ingress из `/root/bak-smart-updown-15.09/` (см. ADR-009). ## Внешние зависимости (upstream) @@ -58,20 +62,48 @@ binance-market — это **не часть игры**, а **общий инфр ## Статус проекта (сводка) -Задачи и статусы — в `TASKS.md` (или `docs/TASKS.md`). Здесь — только верхи: -- [ ] Фаза 0 — экономика (токен, курс, mint) — **в работе** -- [ ] Фаза 1 — контракт на токене — **не начато** -- [ ] Фаза 4 — задействовать свап-клиент — **клиент готов** -- [ ] Фаза 5 — фронт (баланс/пополнение/вывод) — **не начато** -- [ ] Интеграция с `binance-market` (NATS/WS-клиент `PriceSource`) — **см. ADR-007** +Задачи и статусы — в `TASKS.md`. Здесь — только верхи (актуально на **2026-09-15**): +- [x] **Игра работает на живом стенде** — оба домена на ОДНОМ TS-шлюзе (ADR-009). +- [x] Цена SOL — из `binance-market` по NATS, раздача фронту по WS (ADR-007/008). +- [x] Авто-закрытие ставок на шлюзе, расчёт — **математика проверена 3/3** (выигрыш 1.3×. + проигрыш 0, ничья — возврат залога). +- [x] Фаза 5 — фронт (Unity WebGL) — **живой, деплоится тегами** (деплой-памятка в `TASKS.md`). +- [ ] **Перед mainnet:** закрыть `SEC-16` (безвозвратная утечка SOL с игрока) — правка контракта + + редеплой; см. `docs/SECURITY.md`. +- [ ] Фаза 4 — задействовать свап-клиент (`pump.fun`) — **клиент готов**; Фаза 0/1 (экономика + токена, контракт на фантиках) — по `TASKS.md`. + +## Карта деплоя (кто где живёт) + +| Что | Где | Адрес/идентификатор | +|---|---|---| +| **k3s-кластер** | VM `192.168.76.120`, ns `game` | — | +| **Игровой бэк (боевой)** | TS-шлюз `updown-relayer`, сервис `testfront-gateway-smart-updown-relayer:8895` | оба домена: `/api`, `/ws` | +| **Домены** | `smart-updown.binom.pw` (Unity-игра), `testfront.binom.pw` (тех-дашборд) | один и тот же бэк | +| **Фронт игры** | deployment `fun-game-front2` (nginx + Unity WebGL) | образ `images.binom.pw/fun/game-front2:<тег>` | +| **Ingress-пути** | **`smart-updown-api`** — `/api`,`/ws` (ВНЕ helm); `fun-game-front2-…` — только `/` | ADR-010 | +| **Solana localnet** | LXC 151 на Proxmox `192.168.76.66` | RPC `192.168.76.181:8899` | +| **Контракт (в цепи)** | `9ALs…S1fCpf` (фантики, 4 инструкции) | прод-игра работает тут | +| **Реестр образов** | `images.binom.pw` | — | ## Безопасность -**Открытый чек-лист — `docs/SECURITY.md`** (пункты `SEC-1…SEC-15`, аудит живого стенда 2026-09-13). -Критично на сегодня (`SEC-1…SEC-6`): у контракта нет верхней границы окна закрытия ставки и нет -серверного авто-закрытия — момент расчёта выбирает клиент, что даёт безрисковый арбитраж. Плюс -открытые наружу `/bet`, `/close`, `/faucet` без авторизации. +**Чек-лист — `docs/SECURITY.md`** (пункты `SEC-1…SEC-16`; аудит живого стенда 2026-09-13, +дополнен приёмкой боевого домена 2026-09-15). + +- **Закрыто:** отсутствие авто-закрытия (шлюз сам закрывает по расписанию), окно выбора момента + расчёта игроком (цена берётся из истории на момент экспирации), зависание при равных ценах + (`SEC-9` → возврат залога), `POST /close` удалён (404). +- 🔴 **Открыто, блокер перед mainnet — `SEC-16`:** рента PDA-аккаунта ставки (**1 559 040 lamports**, + = 0.00155904 SOL) списывается с кошелька **игрока** и **не возвращается никогда** (аккаунт не + закрывается). На тестах маскируется краном; на mainnet это реальные деньги игрока, тающие без + проигрыша. Лечение — `close = bettor` в `close_bet` + редеплой. +- **Остальное открытое:** NATS без аутентификации, `/bet` и `/faucet` без авторизации (на + тестовом стенде допустимо), отложенные `SEC-13…SEC-15`. ## Прочее -- Ночная экономика и mint-адреса: `docs/ECONOMY.md` (создаётся в Фазе 0). -- Разделы задач общего характера (например, мок/деплой/стабильность) — в `TASKS.md`. \ No newline at end of file +- Ночная экономика и mint-адреса: `docs/ECONOMY.md`. +- Разделы задач общего характера (мок/деплой/стабильность) — в `TASKS.md`. +- **Диагностические и приёмочные скрипты** (живой стенд) — в `tools/`, с питфоллами замера в + `tools/README.md`. +- **k8s-манифесты вне helm** (отдельный ingress `/api`,`/ws`) — в `k8s/`. \ No newline at end of file diff --git a/TASKS.md b/TASKS.md index d0e3d22..e53ac40 100644 --- a/TASKS.md +++ b/TASKS.md @@ -106,6 +106,28 @@ - Кран зачисляет на цепь с лагом **8–13 с** (шлюз рапортует мгновенно) — отсюда ложный симптом «SOL не обновился». +**Деплой-памятка фронта игры (`fun-game-front2`) — прочитать перед каждым релизом:** +```bash +helm repo update binom +helm upgrade fun-game-front2 binom/fun-game-front2 -n game \ + --reuse-values --set image.tag= --wait --timeout 4m # = тег релиза, НЕ опираться на чарт +``` +⚠️ Без явного `--set image.tag` образ НЕ обновится: в `helm/values.yaml` захардкожен `image.tag`, +он перебивает `Chart.AppVersion` → спека не меняется, новый под не создаётся, на домене остаётся +старый билд при «Upgrade complete» в helm. Полный разбор — `docs/DECISIONS.md` (ADR-010). +Проверять по кубу/домену: `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'`. + +**Задачи фронтендеру (отдать во фронт-репо `fun-game-front2`):** +- [ ] **Убрать `image.tag` из `helm/values.yaml`** и опираться только на `Chart.AppVersion` + (в CI он уже подставляется из тега релиза) — тогда `helm upgrade` перестанет требовать `--set`. +- [ ] **Хэш содержимого в имени ассетов** (`WebGL..data/.wasm/.framework.js`) на шаге сборки: + сейчас nginx отдаёт ассеты как `immutable, max-age=604800` при НЕизменяемых именах, поэтому + у уже игравших игроков до 7 суток держится старый билд в кэше браузера. +- [ ] **Привести API игры к контракту TS-шлюза** (`docs/API шлюза` / `test-front/API.md`): + тело ставки `{address, side, amountUnits}`; поля `minAmount`/`maxAmount` — строкой; `GET /api/bet/{id}` + у шлюза отсутствует (см. ADR-009). + **Фича на будущее — «ждать, если цена не изменилась» (НЕ ДОДЕЛАНА):** - [ ] **TODO** `[pump.meta]` Если по сделанной ставке цена на момент экспирации **не изменилась** (`exit == entry`, ничья), нужно **подождать ещё 15 секунд** и попробовать взять цену заново — diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 2f71e69..d65ef89 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -132,3 +132,74 @@ дороже и требует фидов; (в) заглушка — отклонено как единственный вариант, оставлена возможность (`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`). + +---