Скрипт проверяет на живом стенде (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
Инструменты проверки игрового контура
Скрипты для живой приёмки механик ставок (не заменяют 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без cwdnode_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.