# 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= --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 секунд** и попробовать взять цену заново — чтобы ставка не закрывалась возвратом залога на «плоской» цене, а получала шанс на реальный исход. **Суть:** ставка не меняется → ждём (не финализируем сразу возвратом). - [ ] **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: (тег = номер релиза, как раньше) # 2. раскладываем на nginx и включаем как активную: ssh root@192.168.76.66 "pct exec 152 -- /opt/game/tools/publish-release.sh --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//Build/WebGL.framework.js ``` Без `--activate` сборка просто ложится в `/opt/game/releases//` и доступна по `//` — корень не меняется. Откат версии = `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/` не удаляются сами (~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":"","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 (сейчас жмёт уже сжатое)