docs(security): SEC-2/SEC-3/SEC-8 закрыты, SEC-4 усилен (updown-relayer:6)
- SEC-3 закрыт: ручка POST /close удалена, ставки закрывает сам шлюз (404) - SEC-8 закрыт: авто-закрытие + одна открытая ставка на адрес (409) - SEC-2 закрыт по факту: планировщик settle.ts работает, проверено живьём - SEC-4 усилен: пара UP+DOWN больше не набирается (вторя ставка -> 409) - SEC-5 частично: счётчик больше не накрутить массово, но /bet без авторизации - tools/autoclose-check.cjs: сквозной живой тест (9/9 пройдено)
This commit is contained in:
+24
-9
@@ -10,10 +10,19 @@
|
|||||||
Пункты нумеруются сквозным `SEC-N` (не меняется при добавлении/закрытии пунктов) — ссылаться на них
|
Пункты нумеруются сквозным `SEC-N` (не меняется при добавлении/закрытии пунктов) — ссылаться на них
|
||||||
номером. Отмечать закрытое — `[ ]` → `[x]`, в закрытую строку добавлять дату и коммит/релиз.
|
номером. Отмечать закрытое — `[ ]` → `[x]`, в закрытую строку добавлять дату и коммит/релиз.
|
||||||
|
|
||||||
**Сводка:** открыто 12 — критичных 3 (`SEC-3`, `SEC-5`), экономика 3 (`SEC-7…SEC-9`),
|
**Сводка:** открыто 9 — критичных 1 (`SEC-5`, частично закрыт), экономика 2 (`SEC-7`,`SEC-9`),
|
||||||
инфраструктура 3 (`SEC-10…SEC-12`), отложено 3 (`SEC-13…SEC-15`). Закрыто: 4 —
|
инфраструктура 3 (`SEC-10…SEC-12`), отложено 3 (`SEC-13…SEC-15`). Закрыто: 7 —
|
||||||
`SEC-6` (2026-09-13, `updown-relayer:2` / `test-front:3`),
|
`SEC-6` (2026-09-13, `updown-relayer:2` / `test-front:3`),
|
||||||
`SEC-1`, `SEC-2`, `SEC-4` (2026-09-13, `updown-relayer:3`, коммит `0b136c6`).
|
`SEC-1`, `SEC-2`, `SEC-4` (2026-09-13, `updown-relayer:3`, коммит `0b136c6`),
|
||||||
|
`SEC-3`, `SEC-8` (2026-09-13, `updown-relayer:6` — авто-закрытие + одна открытая ставка на адрес),
|
||||||
|
плюс `SEC-2` и `SEC-4` усилены в `:6`.
|
||||||
|
|
||||||
|
**Итерация 2026-09-13 (`updown-relayer:6`) — что сделано:** авто-закрытие ставок серверным
|
||||||
|
планировщиком (`settle.ts`), ручка `POST /close` удалена (`404`), WS-события `bet_open`/`bet_closed`
|
||||||
|
в существующем канале (`/ws`), ограничение «одна открытая ставка на адрес» (`409`),
|
||||||
|
пакетный обход цепочки вместо поштучного (9.3× быстрее).
|
||||||
|
*Проверено живьём:* 9/9 проверок сквозного сценария (кошелёк → faucet → ставка → блок второй
|
||||||
|
ставки → авто-закрытие через 16 с → `/close` 404).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -22,11 +31,15 @@
|
|||||||
- [x] **SEC-1. Нет верхней границы окна закрытия ставки** — **ЗАКРЫТ 2026-09-13 по сути** (релиз `updown-relayer:3`, коммит `0b136c6`). Контракт по-прежнему не ограничивает время закрытия, **но выбирать момент расчёта больше нельзя**: шлюз берёт цену закрытия из локальной истории на момент `expire_time` ставки (`history.priceAtOrBefore(expireTime*1000)`), а не текущую. Поздний клик теперь даёт ту же цену, что и своевременный. Место правки: `relayer/src/gateway.ts` (`handleClose`), `relayer/src/priceHistory.ts`.
|
- [x] **SEC-1. Нет верхней границы окна закрытия ставки** — **ЗАКРЫТ 2026-09-13 по сути** (релиз `updown-relayer:3`, коммит `0b136c6`). Контракт по-прежнему не ограничивает время закрытия, **но выбирать момент расчёта больше нельзя**: шлюз берёт цену закрытия из локальной истории на момент `expire_time` ставки (`history.priceAtOrBefore(expireTime*1000)`), а не текущую. Поздний клик теперь даёт ту же цену, что и своевременный. Место правки: `relayer/src/gateway.ts` (`handleClose`), `relayer/src/priceHistory.ts`.
|
||||||
*Проверено на живом стенде (ставка №10):* цена на момент экспирации `9977000000`, текущая цена в момент клика `9976000000` → `exitPrice=9977000000` (взята история), ставка финализирована `payout_done`. Уязвимый код взял бы `9976000000`.
|
*Проверено на живом стенде (ставка №10):* цена на момент экспирации `9977000000`, текущая цена в момент клика `9976000000` → `exitPrice=9977000000` (взята история), ставка финализирована `payout_done`. Уязвимый код взял бы `9976000000`.
|
||||||
*Остаток (не критично):* в контракте верхней границы так и нет — открытый вопрос «как долго позволять закрывать» остаётся косметикой, поскольку цена уже зафиксирована. Строгий фикс на цепочке (тот, что был предложен изначально) — по-прежнему желателен как второй слой.
|
*Остаток (не критично):* в контракте верхней границы так и нет — открытый вопрос «как долго позволять закрывать» остаётся косметикой, поскольку цена уже зафиксирована. Строгий фикс на цепочке (тот, что был предложен изначально) — по-прежнему желателен как второй слой.
|
||||||
- [x] **SEC-2. Нет авто-закрытия ставок на сервере** — **ЗАКРЫТ 2026-09-13 по сути** (релиз `updown-relayer:3`). Серверное авто-закрытие по-прежнему не сделано (следующая итерация, по решению владельца), **но риск снят**: исход ставки больше не зависит от того, кто и когда позвал `/close`, потому что цена закрытия определяется историей. Опоздание сервера перестало быть риском — дозакрытие даст ту же цену. Место: `relayer/src/gateway.ts` (`handleClose`).
|
- [x] **SEC-2. Нет авто-закрытия ставок на сервере** — **ЗАКРЫТ 2026-09-13** (релиз `updown-relayer:6`). Серверное авто-закрытие **сделано**: `relayer/src/settle.ts` — `startScheduler` крутит тик каждые `SMART_SETTLE_INTERVAL_MS` (дефолт 2000 мс), находит просроченные открытые ставки, берёт цену из истории на момент `expire_time` и сам зовёт `close_bet`. Клиент больше не участвует в закрытии вообще — ручки `/close` нет.
|
||||||
*Остаток:* без авто-закрытия незакрытые ставки продолжают запирать средства в ваулте (см. SEC-8) — вопрос экономики, не честности расчёта.
|
*Проверено на живом стенде:* ставка `id=14` закрыта **сама через 16 с** после экспирации, `exitPrice=10037000000`, `status=2` (`house_won`), по WS пришло событие `bet_closed`.
|
||||||
- [ ] **SEC-3. `POST /close` доступен из интернета без авторизации** — любой может закрыть чужую ставку в выбранный им момент. Проверено: `curl -X POST https://testfront.binom.pw/api/close -d '{"id":"1"}'` → обработчик выполняется. Фикс: авторизация + серверное закрытие.
|
*Дополнительно:* в шлюзе включён пакетный обход (`getProgramAccounts`, 1 запрос вместо N) — замерено **3 мс против 28 мс** у поштучного перебора, 9.3× быстрее, и время тика больше не растёт с числом ставок.
|
||||||
- [x] **SEC-4. Безрисковый арбитраж на паре ставок UP+DOWN** — **ЗАКРЫТ 2026-09-13 по сути** (релиз `updown-relayer:3`, коммит `0b136c6`). Схема опиралась на возможность выбрать выгодный момент закрытия; теперь цена закрытия фиксируется историей на момент `expire_time`, и обе половины пары закрываются по одной и той же цене независимо от времени клика. Арбитраж исчезает вместе с SEC-1. *Прямого прогона пары UP+DOWN не делали — закрытие выводится из закрытия SEC-1 (тот же механизм).*
|
- [x] **SEC-3. `POST /close` доступен из интернета без авторизации** — **ЗАКРЫТ 2026-09-13** (релиз `updown-relayer:6`). Ручка **удалена**: закрывать ставки снаружи больше нечем. Закрытие делает сам шлюз — серверный планировщик (`relayer/src/settle.ts`, `startScheduler`) каждые `SMART_SETTLE_INTERVAL_MS` (дефолт 2000 мс) обходит Bet-аккаунты, для просроченных (`expire_time*1000 <= now`) берёт цену из истории на момент экспирации и сам зовёт `close_bet`. Выгоды позвать чужую ставку в удобный момент больше нет — и звать нечего.
|
||||||
- [ ] **SEC-5. `POST /bet` без авторизации** — любой может создавать ставки на произвольные адреса и накручивать счётчик раундов `global.counter`. Проверено: `curl -X POST .../api/bet` → 200. Фикс: токен/подпись запроса.
|
*Проверено на живом стенде:* `POST /close` → **404**; ставка `id=14` (UP, вход `10038000000`) закрыта **автоматически через 16 с** после экспирации, `exitPrice=10037000000`, `status=2` (`house_won`) — при цене вниз ставка UP проиграла, то есть расчёт корректен по направлению.
|
||||||
|
*Остаток (не критично):* авторизации на `/bet` по-прежнему нет (см. SEC-5).
|
||||||
|
- [x] **SEC-4. Безрисковый арбитраж на паре ставок UP+DOWN** — **ЗАКРЫТ 2026-09-13** (релиз `updown-relayer:6`, вторая линия обороны). Схема опиралась на возможность выбрать выгодный момент закрытия; цена закрытия фиксируется историей на момент `expire_time` (релиз `:3`), и **вдобавок пара больше физически не набирается**: шлюз отклоняет вторую ставку, пока открыта первая. Арбитраж закрыт дважды — и по цене, и по возможности занять обе стороны.
|
||||||
|
*Проверено на живом стенде:* на кошелёк с открытой ставкой `id=14` ставка DOWN → **409** `{"error":"you already have an open bet: close it before placing a new one","openBetId":"14"}`. Первая ставка при этом ставится нормально (`200`).
|
||||||
|
- [ ] **SEC-5. `POST /bet` без авторизации** — любой может создавать ставки на произвольные адреса. Проверено: `curl -X POST .../api/bet` → 200. **Частично закрыт 2026-09-13** (релиз `updown-relayer:6`): накрутка счётчика `global.counter` больше не работает в промышленных масштабах — на адрес допускается только **одна открытая** ставка, вторая → `409`. Ручка по-прежнему не авторизована, поэтому перебор адресов формально возможен. Фикс: токен/подпись запроса.
|
||||||
- [x] **SEC-6. `GET /faucet/{address}` открыт наружу и выдаёт реальные средства** — **ЗАКРЫТ 2026-09-13** (релизы `updown-relayer:2`, `test-front:3`). Что было: из двух механизмов ручки (`handleFaucet`) — **(а) SOL-налив** (`requestAirdrop`) технически только localnet (mainnet-RPC отдаёт `410 Gone`); **(б) налив фантиков** (`mintTo`) — **НЕ был привязан к сети**: работал везде, где у шлюза есть mint authority минта. Гейтом был только конфиг-флаг `SMART_ALLOW_AIRDROP` с дефолтом **`"1"` (включено)**, проверки кластера в коде не было; SOL-барьер обходился (airdrop звался лишь при балансе <2 SOL, `ops.ts:204`) — адрес с ≥2 SOL доходил прямо до `mintTo`. Было воспроизведено: `curl …/api/faucet/6Q1ko…` → `{"sol":5,"tokenUnits":"10000000000"}` на произвольный адрес без авторизации.
|
- [x] **SEC-6. `GET /faucet/{address}` открыт наружу и выдаёт реальные средства** — **ЗАКРЫТ 2026-09-13** (релизы `updown-relayer:2`, `test-front:3`). Что было: из двух механизмов ручки (`handleFaucet`) — **(а) SOL-налив** (`requestAirdrop`) технически только localnet (mainnet-RPC отдаёт `410 Gone`); **(б) налив фантиков** (`mintTo`) — **НЕ был привязан к сети**: работал везде, где у шлюза есть mint authority минта. Гейтом был только конфиг-флаг `SMART_ALLOW_AIRDROP` с дефолтом **`"1"` (включено)**, проверки кластера в коде не было; SOL-барьер обходился (airdrop звался лишь при балансе <2 SOL, `ops.ts:204`) — адрес с ≥2 SOL доходил прямо до `mintTo`. Было воспроизведено: `curl …/api/faucet/6Q1ko…` → `{"sol":5,"tokenUnits":"10000000000"}` на произвольный адрес без авторизации.
|
||||||
**Что сделано:** (1) новый модуль `relayer/src/faucetGate.ts` — `parseMode` + `faucetDenyReason` с политикой **fail-closed**: отказ, если режим не `test`, если genesis-hash недоступен, или если он принадлежит mainnet-beta/devnet/testnet (`5eykt4…`/`EtWTRA…`/`4uhcVJ…`). (2) `SMART_MODE` (`test|release`), **дефолт `release`** — безопасно забыть, а не вспомнить. (3) `handleFaucet`: 403 на уровне режима и 403 на уровне сети, в лог `faucet DENIED (<причина>)`. (4) Начисление **вынесено из `/bet`** — ставка больше не печатает денег, а `address` стал обязательным; перед ставкой проверяются балансы (SOL ≥ 0.01, фантиков ≥ суммы). (5) Новая ручка `POST /wallet` — создать custodial-кошелёк, **ничего не наливает**. (6) На пульте `test-front` — отдельный блок «Кошелёк и пополнение» с кнопками «Создать кошелёк» / «Пополнить кошелёк».
|
**Что сделано:** (1) новый модуль `relayer/src/faucetGate.ts` — `parseMode` + `faucetDenyReason` с политикой **fail-closed**: отказ, если режим не `test`, если genesis-hash недоступен, или если он принадлежит mainnet-beta/devnet/testnet (`5eykt4…`/`EtWTRA…`/`4uhcVJ…`). (2) `SMART_MODE` (`test|release`), **дефолт `release`** — безопасно забыть, а не вспомнить. (3) `handleFaucet`: 403 на уровне режима и 403 на уровне сети, в лог `faucet DENIED (<причина>)`. (4) Начисление **вынесено из `/bet`** — ставка больше не печатает денег, а `address` стал обязательным; перед ставкой проверяются балансы (SOL ≥ 0.01, фантиков ≥ суммы). (5) Новая ручка `POST /wallet` — создать custodial-кошелёк, **ничего не наливает**. (6) На пульте `test-front` — отдельный блок «Кошелёк и пополнение» с кнопками «Создать кошелёк» / «Пополнить кошелёк».
|
||||||
**Проверено на живом стенде:** `/bet` без адреса → `400 "address is required…"` (и ничего не начисляет); `/bet` на непополненный кошелёк → `400 "insufficient funds…"`; полный круг `POST /wallet` → `GET /faucet` → `POST /bet` → `entryPrice` (HTTP 200); **гейт в обратную сторону: `smart.mode=release` → `403 {"error":"faucet is disabled: only test mode (SMART_MODE=test)"}`** — начисления нет. Тесты: 6 новых проверок гейта, `npm test` 24/24; мутационная приёмка пройдена (ослабление проверки публичных кластеров → падают ровно 2 теста).
|
**Проверено на живом стенде:** `/bet` без адреса → `400 "address is required…"` (и ничего не начисляет); `/bet` на непополненный кошелёк → `400 "insufficient funds…"`; полный круг `POST /wallet` → `GET /faucet` → `POST /bet` → `entryPrice` (HTTP 200); **гейт в обратную сторону: `smart.mode=release` → `403 {"error":"faucet is disabled: only test mode (SMART_MODE=test)"}`** — начисления нет. Тесты: 6 новых проверок гейта, `npm test` 24/24; мутационная приёмка пройдена (ослабление проверки публичных кластеров → падают ровно 2 теста).
|
||||||
@@ -36,7 +49,9 @@
|
|||||||
## Важно — экономика и устойчивость
|
## Важно — экономика и устойчивость
|
||||||
|
|
||||||
- [ ] **SEC-7. Множитель 1.3× требует от дома винрейта ~77 %** — `SMART_MULTIPLIER_BPS=13000`; порог безубытка = 1/1.3 = 76.9 %. При честной монете дом убыточен, а с дырой окна закрытия игрок забирает близко к 100 %. Фикс: пересчитать экономику под реальный edge.
|
- [ ] **SEC-7. Множитель 1.3× требует от дома винрейта ~77 %** — `SMART_MULTIPLIER_BPS=13000`; порог безубытка = 1/1.3 = 76.9 %. При честной монете дом убыточен, а с дырой окна закрытия игрок забирает близко к 100 %. Фикс: пересчитать экономику под реальный edge.
|
||||||
- [ ] **SEC-8. Нефинализированные ставки запирают средства в ваулте** — `vault` не уменьшается, пока ставку не закроют. Наблюдение: `vaultBalanceHuman` 28100 → 30100 при незакрытых ставках. Фикс: серверное закрытие + клинап просроченных.
|
- [x] **SEC-8. Нефинализированные ставки запирают средства в ваулте** — **ЗАКРЫТ 2026-09-13** (релиз `updown-relayer:6`). Причины было две, обе устранены. **(1) Закрывать было некому:** авто-закрытия не существовало, а клиент мог просто не позвать `/close` — теперь шлюз закрывает сам (см. SEC-2), и ставка не может остаться висеть. **(2) Можно было набрать много ставок разом:** ничто не мешало занять сто ставок, заперев сто залогов; теперь на адрес допускается **одна открытая** ставка (вторая → `409`), поэтому размер запертого одним человеком залога ограничен одной ставкой.
|
||||||
|
*Проверено на живом стенде:* вторая ставка при открытой первой → `409`; висевшая ставка `id=14` закрыта автоматически через 16 с, средства разблокированы.
|
||||||
|
*Остаток:* если ставка попала в «равные цены» (`exit == entry`, см. SEC-9), планировщик повторяет попытку и логирует `still open … will retry` — молчаливый повтор, средства при этом всё равно остаются запертыми до первого движения цены. Это отдельная проблема SEC-9.
|
||||||
- [ ] **SEC-9. Равные `exit_price == entry_price` не финализируют ставку** — `close_bet` делает `return Ok(())`, статус остаётся `open`; при «плоской» цене ставка зависает без исхода. Место: `close_bet.rs:66`. Фикс: определить политику (продление / возврат / ничья) вместо молчаливого выхода.
|
- [ ] **SEC-9. Равные `exit_price == entry_price` не финализируют ставку** — `close_bet` делает `return Ok(())`, статус остаётся `open`; при «плоской» цене ставка зависает без исхода. Место: `close_bet.rs:66`. Фикс: определить политику (продление / возврат / ничья) вместо молчаливого выхода.
|
||||||
|
|
||||||
## Средний приоритет — инфраструктура
|
## Средний приоритет — инфраструктура
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* ЖИВАЯ проверка итерации: авто-закрытие + одна ставка на адрес + WS-события.
|
||||||
|
* Запуск на хосте ассистента (Node 22, глобальный fetch + WebSocket).
|
||||||
|
*
|
||||||
|
* node autoclose-check.cjs
|
||||||
|
*
|
||||||
|
* Проверяет по шагам:
|
||||||
|
* 1. POST /wallet — новый кошелёк
|
||||||
|
* 2. GET /faucet/{addr} — пополнить
|
||||||
|
* 3. WS /api/ws — подключиться и слушать события
|
||||||
|
* 4. POST /bet — 1-я ставка: ждём 200 + событие bet_open
|
||||||
|
* 5. POST /bet — 2-я ставка: ждём 409 (одна открытая на адрес)
|
||||||
|
* 6. ждать экспирацию ~15 с — ждём событие bet_closed (авто-закрытие)
|
||||||
|
* 7. POST /close — должен быть 404 (ручку убрали)
|
||||||
|
*/
|
||||||
|
const BASE = process.env.BASE || "https://testfront.binom.pw";
|
||||||
|
const WS_URL = process.env.WS_URL || "wss://testfront.binom.pw/api/ws";
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
function check(name, ok, detail) {
|
||||||
|
results.push({ name, ok, detail });
|
||||||
|
console.log(`${ok ? "OK " : "FAIL"} | ${name}${detail ? " — " + detail : ""}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const j = async (path, init) => {
|
||||||
|
const r = await fetch(BASE + path, init);
|
||||||
|
let body = null;
|
||||||
|
try { body = await r.json(); } catch { /* не JSON */ }
|
||||||
|
return { status: r.status, body };
|
||||||
|
};
|
||||||
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
// --- 3. WS заранее: события редкие, слушаем всё время ---
|
||||||
|
const events = [];
|
||||||
|
let ws;
|
||||||
|
try {
|
||||||
|
ws = new WebSocket(WS_URL);
|
||||||
|
ws.onmessage = (m) => {
|
||||||
|
try {
|
||||||
|
const d = JSON.parse(m.data);
|
||||||
|
if (d.type !== "price") { events.push(d); console.log(" WS<-", JSON.stringify(d)); }
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
};
|
||||||
|
await new Promise((res, rej) => {
|
||||||
|
ws.onopen = res;
|
||||||
|
ws.onerror = (e) => rej(new Error("ws error " + (e.message || "")));
|
||||||
|
setTimeout(() => rej(new Error("ws timeout")), 10000);
|
||||||
|
});
|
||||||
|
check("WS подключён", true, WS_URL);
|
||||||
|
} catch (e) {
|
||||||
|
check("WS подключён", false, e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 1. кошелёк ---
|
||||||
|
const w = await j("/wallet", { method: "POST" });
|
||||||
|
const addr = w.body && (w.body.address || w.body.pubkey || w.body.bettor);
|
||||||
|
check("POST /wallet → 200 + адрес", w.status === 200 && !!addr, `status=${w.status} addr=${addr}`);
|
||||||
|
if (!addr) { console.log("\nдальше невозможно: нет адреса"); process.exit(1); }
|
||||||
|
|
||||||
|
// --- 2. faucet ---
|
||||||
|
let f = await j(`/faucet/${addr}`);
|
||||||
|
if (f.status !== 200) { f = await j(`/faucet/${addr}`, { method: "POST" }); }
|
||||||
|
check("GET /faucet → пополнен", f.status === 200, `status=${f.status} ${JSON.stringify(f.body).slice(0,120)}`);
|
||||||
|
await sleep(2500);
|
||||||
|
|
||||||
|
const bal = await j(`/balance/${addr}`);
|
||||||
|
check("GET /balance → 200", bal.status === 200, JSON.stringify(bal.body).slice(0, 160));
|
||||||
|
|
||||||
|
// --- 4. первая ставка ---
|
||||||
|
const bet1 = await j("/bet", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ address: addr, side: "UP", amountUnits: 100000000 }),
|
||||||
|
});
|
||||||
|
check("POST /bet (1-я) → 200", bet1.status === 200, `status=${bet1.status} ${JSON.stringify(bet1.body).slice(0,180)}`);
|
||||||
|
const betId = bet1.body && (bet1.body.id ?? bet1.body.betId ?? bet1.body.bet_id);
|
||||||
|
console.log(` id ставки = ${betId}`);
|
||||||
|
|
||||||
|
// --- 5. вторая ставка — должна быть отклонена ---
|
||||||
|
const bet2 = await j("/bet", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ address: addr, side: "DOWN", amountUnits: 100000000 }),
|
||||||
|
});
|
||||||
|
const blocked = bet2.status === 409 || bet2.status === 400;
|
||||||
|
check("POST /bet (2-я, пока 1-я открыта) → отклонена", blocked,
|
||||||
|
`status=${bet2.status} ${JSON.stringify(bet2.body).slice(0,180)}`);
|
||||||
|
|
||||||
|
// --- 6. авто-закрытие: ждём событие ---
|
||||||
|
const t0 = Date.now();
|
||||||
|
let closed = null;
|
||||||
|
while (Date.now() - t0 < 45000) {
|
||||||
|
closed = events.find((e) => e.type === "bet_closed" && String(e.id) === String(betId));
|
||||||
|
if (closed) break;
|
||||||
|
await sleep(1000);
|
||||||
|
}
|
||||||
|
const secs = ((Date.now() - t0) / 1000).toFixed(0);
|
||||||
|
check("WS: событие bet_closed (авто-закрытие)", !!closed, closed ? `через ${secs} с: ${JSON.stringify(closed)}` : `не пришло за 45 с`);
|
||||||
|
|
||||||
|
const opened = events.find((e) => e.type === "bet_open" && String(e.id) === String(betId));
|
||||||
|
check("WS: событие bet_open", !!opened, opened ? JSON.stringify(opened) : "не пришло");
|
||||||
|
|
||||||
|
// --- 7. ручка /close убрана ---
|
||||||
|
const cl = await j("/close", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ id: betId }),
|
||||||
|
});
|
||||||
|
check("POST /close → 404 (ручку убрали)", cl.status === 404, `status=${cl.status}`);
|
||||||
|
|
||||||
|
// --- итог ---
|
||||||
|
const bad = results.filter((r) => !r.ok);
|
||||||
|
console.log(`\n=== ИТОГ: ${results.length - bad.length}/${results.length} пройдено ===`);
|
||||||
|
if (bad.length) console.log("провалы: " + bad.map((b) => b.name).join("; "));
|
||||||
|
if (ws) try { ws.close(); } catch { /* ignore */ }
|
||||||
|
process.exit(bad.length ? 1 : 0);
|
||||||
|
})().catch((e) => { console.error("ERR", e); process.exit(2); });
|
||||||
Reference in New Issue
Block a user