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:
2026-09-13 18:49:37 +03:00
parent 4c0b5caed6
commit 6ee30c39fb
2 changed files with 142 additions and 9 deletions
+118
View File
@@ -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); });