Files
subochev a4ac702907 test(gateway): живая приёмка API v2 шлюза smart-updown — 26/26
Скрипт проверяет на живом стенде (BASE=https://smart-updown.binom.pw/api):
/state с canBet/canBetReason/betPercent/balance/openBet/lastBet + status,
POST /bet без суммы (30% от баланса, кламп), отказы 200 {accepted:false,
reasonCode, retryAfterSeconds}, авто-закрытие ставки по экспирации,
переход openBet -> lastBet. Запуск: BASE=... node tools/gateway-api-v2-check.cjs
2026-09-17 00:11:56 +03:00
..

Инструменты проверки игрового контура

Скрипты для живой приёмки механик ставок (не заменяют npm test в репозитории релейера, а проверяют боевой стенд).

exploit-decisive.cjs — решающая проверка окна закрытия

Проверяет главное: цена закрытия берётся из истории на момент экспирации, а не в момент клика.

Как запускать (внутри пода шлюза — там есть node_modules и доступ к RPC):

P=$(kubectl -n game get pod -l app.kubernetes.io/instance=testfront-gateway -o jsonpath='{.items[0].metadata.name}')
kubectl -n game cp tools/exploit-decisive.cjs "$P:/app/data/probe/exploit-decisive.cjs" -c gateway
kubectl -n game exec "$P" -c gateway -- sh -c "cd /app && node /app/data/probe/exploit-decisive.cjs 300"

Питфоллы (каждый пойман вживую)

  • Нельзя просто сравнивать exitPrice с текущей ценой. Цена часто стоит на месте, и сравнение вырождается в «совпало» — ложный успех. Скрипт ждёт, пока цена на экспирации и текущая разойдутся, и только тогда закрывает. Первый прогон без этого дал ложное «совпало» дважды.
  • Копировать только в /app/data/, не в /app (Permission denied, под не root).
  • Запускать с cd /app, иначе Cannot find module '@solana/web3.js' (из /tmp//app/data без cwd node_modules не виден).
  • Ставка при exit == entry: с 2026-09-14 (SEC-9) возвращает залог статусом 3 и финализируется — залипаний больше нет. Открытый вопрос: хочется вместо немедленного возврата подождать ещё 15 с и взять цену заново (см. TASKS.md, «ждать, если цена не изменилась»).

Читает bet_time / expire_time прямо из аккаунта Bet (сырые байты) — off-chain данными не верим. Раскладка аккаунта (Anchor, после 8-байтового дискриминатора): amount u64 @8, bet_time i64 @16, expire_time i64 @24, entry_price u64 @32, exit_price u64 @40, side u64 @48, status u64 @56, bettor Pubkey @64.

price-history-probe.cjs — разбор истории цены

Смотрит, что реально накопилось в binance-market (Postgres 192.168.76.182:5434, БД binance_market) вокруг конкретной ставки: сделки в окне, цена на входе, цена на экспирации, и что было бы при «ждать выгодной цены».

node tools/price-history-probe.cjs <programId> <betId>

⚠️ Символ в БД — в нижнем регистре (solusdt, не SOLUSDT). Запрос с верхним регистром вернёт пусто, и это легко принять за «данных нет».

whose-fee.cjs — с ЧЬЕГО кошелька уходит SOL за раунд (доказательство SEC-16)

Мерит SOL игрока в трёх точках каждого раунда (до ставки → после ставки → после расчёта) и SOL шлюза-админа. Замер: игрок 5 000 000 000 → 4 998 440 960 (−1 559 040 lamports), шлюз не меняется — значит ренту PDA-аккаунта ставки платит игрок.

node tools/whose-fee.cjs

bet-pda-rent.cjs — сколько ренты заперто в аккаунтах ставок

Обходит getProgramAccounts (фильтр dataSize:96) и показывает, что у каждой ставки PDA держит ровно 1 559 040 lamports. Аккаунт не закрывается никогда (close_bet не делает close = bettor) → рента теряется безвозвратно. Второе доказательство SEC-16.

node tools/bet-pda-rent.cjs

faucet-latency.cjs / sol-balance-cycle.cjs — почему кажется, что «SOL не обновился»

GET /faucet/{addr} отвечает мгновенно, но getBalance на цепи показывает 0 ещё 8–13 с (замеры: 13309 / 13345 / 13345 мс). Игра читает баланс не через /api/balance, а через свой nginx-прокси /solana-rpc/getBalance → прямо с цепи → попадает в окно лага. Симптом «налил, а SOL не обновился» — это лаг зачисления, а не отставание шлюза.

node tools/faucet-latency.cjs      # лаг крана: цепь против отчёта шлюза
node tools/sol-balance-cycle.cjs   # баланс после крана: сразу 0, через ~20 с 5 SOL

settle-math-test.cjs — математика расчёта (правильный способ замера)

⚠️ Шлюз кастодиальный, поэтому дельту надо считать от момента после lock (фантики ушли в vault при ставке), а НЕ от старта раунда — иначе в дельту попадают и ставка, и выплата, и «математика не сходится» ложно. Замерено: выигрыш +130 000 000 (1.3×), проигрыш 0, ничья +100 000 000 (возврат). Считать «от старта» = получить ложный провал (проверено живьём).

node tools/settle-math-test.cjs

backend-full-test.cjs — контракт ручек и коды ошибок шлюза

Приёмка API живьём: /state, /price, /wallet, /faucet, /balance, /bet, идемпотентность крана, 400 на кривые данные, 409 на вторую открытую ставку, 404 на снятые ручки (/close, /bet/{id}), WS-события bet_open/bet_closed. Полный прогон с расчётом на цепи.

node tools/backend-full-test.cjs

⚠️ /bet требует число в amountUnits (строка → 400) и address в теле. ⚠️ getTokenAccountsByOwner принимает 2–3 параметра: commitment кладётся внутрь третьего (cfg), а не четвёртым — иначе -32602 Expected from 2 to 3 parameters.

k8s/smart-updown-api-ingress.yaml — пути /api,/ws вне helm

⚠️ Чарт fun-game-front2 рендерит только path /, поэтому любой helm upgrade игры сносит /api и /ws с боевого домена (домен начинает отдавать HTML игры на /api/*). Лечение — отдельный ingress smart-updown-api вне helm; front2-ingress остаётся paths: [/].

ssh root@192.168.76.120 "export KUBECONFIG=/etc/rancher/k3s/k3s.yaml; \
  kubectl apply -f /root/smart-updown-api-ingress.yaml"
# проверка после каждого релиза игры:
kubectl -n game get ingress -o custom-columns='NAME:.metadata.name,HOST:.spec.rules[0].host,PATHS:.spec.rules[0].http.paths[*].path'

⚠️ НЕ править paths через kubectl patch --type=json с remove по индексам: при нескольких путях индексы съезжают, остаётся висячий /ws, а последний path удалить нельзя (paths: Required value). Надёжно — собрать нужный список целиком и kubectl replace --force.