Files
pump-game-ops/TASKS.md
T

269 lines
24 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.
# TASKS.md — список задач (трекер проекта)
Здесь живёт **весь** список задач — не только мои. Трекаем проект целиком. Бинарные составляющие:
- фаркул/pivotable-задачи (что можно отложить или развернуть иначе) помечаются `[pivotable]`;
- общие/инфра (деплой, стабильность, мок) — `[pump.meta]`.
**Статусы:** `TODO` / `IN_PROGRESS` / `DONE` / `BLOCKED`.
---
## ⭐ Актуальное состояние (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 (релейер под токен)**.
**Готово:**
- Контракт `smart-updown-token` — переведён на SPL-токен (фантики), IDL+тесты обновлены. Коммит `45d64d4`.
- Ktor swap-клиент в бэкенде (ввод/вывод) — заготовка.
- Тестовый фронт `test-front` — каркас (Phantom, балансы, пополнение/вывод).
**В работе / вперёд:**
- **Фаза 2 (активная):** релейер перевести с SOL на токен (ops/pda/config). После неё + E2E на localnet.
- **Веха «первый щуп»:** 👉 *как только релейер на фантиках + E2E на localnet прогнаны — через `test-front` вы самостоятельно щупаете: пополнить фантики → ставка → результат.* Это первый реальный user-тест (без Unity).
**Параллельные TODO (не блокируют Фазу 2):** реальная цена (вместо `RandomPriceSource` — источник выбран: `binance-market`, ADR-007, остаётся интеграция), эмиссия, ликвидность/комиссии swap.
---
## Фаза 0 — Экономика и спека (кросс)
- [ ] **TODO** ADR-002 зафиксирован, документировать `ECONOMY.md`: токен, курс (Способ 1), decimals=6, mint-адреса — `[pump.meta]`
- [x] **DONE** Решить «вверх/вниз» — предмет ставки = цена SOL (ADR-006), валюта = фантики
- [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]`
## Фаза 1 — Контракт на токене (ядро) — репо `smart-updown-token` (копия Smart-game)
- [x] **DONE** Создана копия контракта `smart-updown-token` (ветка main, чистый) — работаем тут
- [x] **DONE** Перевести `create_bet` с SOL (lamports) на SPL-токен: приём фантиков с ATA игрока на ваулт
- [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
- [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
- [x] **DONE** PriceSource: реальная цена (вместо `RandomPriceSource`) — интеграция с `binance-market` (ADR-007), NATS-консьюмер `market.price.solusdt` в `src/natsPrice.ts`, цена ×1e8 (ADR-008)
- [ ] **TODO** Интеграционный тест релейера на токеновом флоу
## Фаза 4 — Бэкенд: задействовать swap
- [ ] **DONE** Ktor `PumpApiClient` + `POST /agents/swap` + DTO + заготовка (в `smart-updown-server`)
- [ ] **TODO** Проверить реальный swap на devnet (не мок)
- [ ] **TODO** Обвязать `GameService`: ввод/вывод фантиков через swap
## Фаза 5 — Фронт (Unity)
- [ ] **TODO** Кнопки «Пополнить»/«Вывести», баланс в фантиках
- [ ] **TODO** SolanaBridge.jslib: подпись токеновой create_bet (ATA игрока в ключах)
- [ ] **TODO** Wallet.cs: баланс в фантиках (ATA + getTokenAccountBalance), CanAfford по токену
## Фаза 5b — Тестовый фронт (`test-front`, для ручной гонки экономики без Unity)
- [x] **DONE** Каркас: Phantom, балансы (фантики+SOL), swap пополнение/вывод через `/api/pump/swap`
- [x] **DONE** Ставка вверх/вниз через контракт с поллингом до SETTLED
- [ ] **TODO** Перевести подпись под токеновую версию контракта (после Фазы 1)
- [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 ввод → ставка токеном → раунд → вывод
- [ ] **TODO** Обновить `API.md`/`INTEGRATION.md`, развернуть на mainnet
- [ ] **TODO** Стабильность: версионировать конфиги, mint-адрес в конфиге
---
## Ссылки
- Решения/экономика: `docs/DECISIONS.md`, `docs/ECONOMY.md`
- Карта репозиториев: `README.md`
## ⭐ Добавлено 2026-09-15 — боевой домен и находки приёмки
**Где мы:** боевой домен `smart-updown.binom.pw` переведён на **тот же TS-шлюз** (`:8895`),
что и `testfront.binom.pw` (некастодиальный Kotlin-бэк выключен, обратимо). Пути `/api`,`/ws`
вынесены в **отдельный ingress вне helm** — релиз игры их больше не сносит.
**Найдено при приёмке живьём:**
- `SEC-16` — безвозвратная утечка ренты PDA-аккаунта ставки: **1 559 040 lamports с кошелька
игрока за каждую ставку**, аккаунт не закрывается никогда. Подробности — `docs/SECURITY.md`.
*Вердикт: на тестах терпимо (маскируется краном), перед mainnet — блокер.*
- Расчёт (выплаты) — **корректен 3/3**: выигрыш `1.3×`, проигрыш `0`, ничья — возврат залога.
- Кран зачисляет на цепь с лагом **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=<N> --wait --timeout 4m # <N> = тег релиза, НЕ опираться на чарт
```
⚠️ Без явного `--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.<hash>.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 секунд** и попробовать взять цену заново —
чтобы ставка не закрывалась возвратом залога на «плоской» цене, а получала шанс на реальный исход.
**Суть:** ставка не меняется → ждём (не финализируем сразу возвратом).
- [ ] **TODO** Вынести окно ожидания в конфиг: сейчас `PRICE_WAIT_TIMEOUT_MS` (default `15000`)
отвечает лишь за **ожидание первой цены от NATS** (`relayer/src/priceSource.ts:9`) и к ничьей
отношения не имеет — нужен **отдельный** параметр (напр. `TIE_WAIT_TIMEOUT_MS`) и логика
отложенного ретрая в `settle.ts` (`priceAtOrBefore(expireTime*1000)` — сейчас берётся
фиксированный момент, ретрай каждые 2 с вечно получит ту же пару, см. `SEC-9`).
- [ ] **TODO** Решить, что делать по истечении окна ожидания: возврат залога (текущее поведение
`SEC-9`) или иной исход.
## Фаза 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`
- **Безопасность: `docs/SECURITY.md` — чек-лист `SEC-1…SEC-15` (аудит живого стенда 2026-09-13)**
## ⭐ 2026-09-15 (2) — статика игры ушла из куба на nginx (LXC 152), версии каталогами
**Где мы:** корень `smart-updown.binom.pw` раздаёт **nginx на Proxmox (LXC 152 `game-web`,
192.168.76.152)** со версионированием по каталогам; `/api` и `/ws` по-прежнему идут в куб
на TS-шлюз отдельным router'ом внешнего Traefik. Решение и разбор — **ADR-011** в `docs/DECISIONS.md`.
**Как выпустить новую сборку игры (памятка — вместо helm upgrade фронта):**
```bash
# 1. CI репозитория fun-game-front2 собирает Unity WebGL и пушит образ
# images.binom.pw/fun/game-front2:<N> (тег = номер релиза, как раньше)
# 2. раскладываем на nginx и включаем как активную:
ssh root@192.168.76.66 "pct exec 152 -- /opt/game/tools/publish-release.sh <N> --activate"
# 3. проверяем ДОМЕН (не helm):
curl -sI https://smart-updown.binom.pw/ | grep -iE 'server|last-modified' # nginx/1.24.0 (Ubuntu)
curl -s -o /dev/null -w '%{http_code}\n' https://smart-updown.binom.pw/<N>/Build/WebGL.framework.js
```
Без `--activate` сборка просто ложится в `/opt/game/releases/<N>/` и доступна по `/<N>/` —
корень не меняется. Откат версии = `publish-release.sh <старый N> --activate` (каталоги не удаляются).
**Инструменты:** `tools/proxmox-nginx/` — `unpack-release.sh` (распаковать dir-дамп образа),
`publish-release.sh` (тянуть образ + разложить + переключить), `game.nginx.conf`,
`smart-updown.traefik.yaml` (боевой конфиг внешнего Traefik). Скопированы в LXC 152
в `/opt/game/tools/` и `/etc/nginx/sites-available/game`.
**Проверено живьём:** `/` → 200 `nginx/1.24.0 (Ubuntu)`; `/27/`,`/28/` → 200, ассеты версий
различаются; `/api/state` → 200 JSON; `/api/ws` → цена идёт; сквозной `autoclose-check.cjs` — **9/9**.
**Задачи на будущее по этой схеме:**
- [ ] **TODO** `[pump.meta]` Ссылки на версии — **только со слэшем** (`/28/`): голый `/28` даёт 301.
Если понадобится бесслэшевый вид — добавить в nginx `return 301 /$build/`.
- [ ] **TODO** `[pump.meta]` Чистка старых сборок на 152: каталоги `releases/<N>` не удаляются сами
(~68 МБ на сборку). Политика ретеншена (сколько версий держим) — за владельцем.
- [ ] **TODO** `[pump.meta]` Автоматизировать шаг публикации в CI `fun-game-front2`: после push образа
дёргать `publish-release.sh` на 152 (сейчас — ручной вызов одной командой).
- [ ] **TODO** `[pump.meta]` Уточнить, нужен ли под `fun-game-front2` в кубе вообще (домен он больше
не обслуживает) — либо оставить как «предыдущая схема на откат», либо выключить.
## ⭐ 2026-09-15 (3) — выпущена сборка 29 + закрыта кэш-мина (редирект корня)
**Сборка 29 опубликована и активна.** Релиз Gitea `29` (id 1046) на коммите `25f4637`
(«fix(webgl): не ронять плеер на старте из-за дыма падения и PC URP») → CI run #29 `success`
(~8 мин) → образ `images.binom.pw/fun/game-front2:29` → `publish-release.sh 29 --activate`.
**Поймана и исправлена реальная мина (ADR-011-доп).** Корень раздавал файлы напрямую, и ассеты Unity
(которые `index.html` зовёт ОТНОСИТЕЛЬНО — `buildUrl = "Build"`) уходили по URL **без номера сборки**
`/Build/WebGL.wasm` под `immutable` 7д. Игрок неделю получал бы старый билд. **Фикс:** корень теперь
**302 → `/29/`**; ассеты идут под номером версии; `immutable` сужен до `~^/[0-9]+/…`.
**Проверено живьём:** `/` → 302 → `/29/`; браузер приземляется на `/29/` и тянет `/29/Build/*` — все
200, ошибок в консоли нет; `/27/`,`/28/` живы; `/api/*` и `/solana-rpc/` → 200; `autoclose-check` **9/9**;
`testfront` цел.
**Памятка на будущее — выпуск сборки целиком:**
```bash
# 1. релиз в Gitea на нужном коммите (CI срабатывает ТОЛЬКО на release, не на push):
curl -s -u subochev:PASS -X POST "https://git.binom.pw/api/v1/repos/subochev/fun-game-front2/releases" \
-H 'Content-Type: application/json' \
-d '{"tag_name":"30","name":"30","target_commitish":"<sha>","draft":false}'
# 2. дождаться прогона (обычно ~8 мин):
# tools/wait-ci-run.sh (в фоне, печатает итог)
# 3. разложить на nginx и сделать активной:
ssh root@192.168.76.66 "pct exec 152 -- /opt/game/tools/publish-release.sh 30 --activate"
# 4. проверить ДОМЕН:
curl -s -o /dev/null -w '%{http_code} -> %{redirect_url}\n' https://smart-updown.binom.pw/ # 302 -> /30/
```
**TODO:**
- [ ] `[pump.meta]` Автоматизировать шаг 3 (сейчас ручная команда после CI).
- [ ] `[pump.meta]` Ретеншен старых сборок на 152 (сейчас 27, 28, 29 = ~200 МБ; политика за владельцем).
## ⭐ 2026-09-15 (4) — именованная сборка `sol` из zip + два дефекта в ней
**Сделано:** фронтендер прислал `build_sol.zip` (вне релизного цикла, без номера). Выложил под именем
`sol` → **https://smart-updown.binom.pw/sol/** (см. ADR-012, `tools/proxmox-nginx/publish-zip.sh`).
Активная сборка не тронута — корень по-прежнему `/29/`. Адрес: `/sol/`.
**Правки в nginx (иначе не работало):**
- локейшн сборок расширен с чисел до имён → **обязательно вернуть `^~` служебным путям**
(`/api`, `/ws`, `/solana-rpc/`), иначе расширенный regex перехватывает `/api/...` и ломает бэк;
- `immutable` — только для номерных каталогов; именованные кэшировать нельзя (могут перезалить);
- в маску добавлен `unityweb` (сборки фронтендера на `.unityweb`-ассетах).
**⚠️ Два дефекта в присланной сборке (отдать фронтендеру):**
- [ ] **`[pump.front]` Двойной префикс API: игра зовёт `/api/api/state` → 404.**
`SmartUpdownConfig.ApiBase()` для WebGL = `Origin()+"/api"`, а `BackendClient.HttpGetAsync("/api/state")`
добавляет `/api/...` ещё раз. Лечится в одном месте: либо `ApiBase()` без `/api`, либо пути без `/api`.
- [ ] **`[pump.front]` `ReferenceError: getProvider is not defined`** — падение при подключении Phantom.
В `framework.js` есть вызовы `getProvider()`/`errMsg()`/`loadWeb3()`, но нет определений: в
`Assets/Plugins/WebGL/SolanaBridge.jslib` они объявлены внутри IIFE, а через `mergeInto` экспортированы
только `SolanaBridge*`-методы. Не регресс от nginx — воспроизводится и на сборке 29.
**Важное подтверждение:** билд `sol` в браузере **дошёл до игрового цикла** (`Phase -> Idle` →
`Phase -> Approach`) — раньше в headless ни одна сборка так далеко не шла. Статика с nginx отдаётся корректно.
## 2026-09-15 (5) — исследование: редакторная сборка vs наша Docker/CI (ничего не меняли)
**Вердикт: Docker не виноват.** Разница — наш `BuildScript.cs` насильно ставит
`WebGLCompressionFormat.Disabled`; фронт собирает с **Brotli + Decompression Fallback** (отсюда
`.unityweb`). Было в браузер 26 МБ (52 МБ wasm + gzip на лету), стало бы 16.4 МБ. Код в обеих сборках
эквивалентен (маркеры в wasm совпадают), версия Unity одинаковая (6000.3.16f1). Наш конвейер
(GameCI-образ + `-batchmode -executeMethod`) — стандартная практика, не костыль. См. ADR-013.
**⚠️ Важная поправка:** оба дефекта (`/api/api/state` → 404 и `getProvider is not defined`) есть и в
НАШЕЙ сборке 29 — это исходники, а не «его сборка». Задача переехала в кодовую правку.
- [ ] решить: переводить ли сборку на Brotli + Decompression Fallback (или gzip_static) — **ждём владельца**
- [ ] `[pump.front]` убрать двойной `/api` в `BackendClient`/`SmartUpdownConfig`
- [ ] `[pump.front]` `getProvider`: экспортировать хелперы jslib наружу (или инлайнить)
- [ ] `[pump.ops]` `gzip off` для `.unityweb` в nginx LXC 152 (сейчас жмёт уже сжатое)