#!/usr/bin/env node // ПОЛНЫЙ ТЕСТ БЭКА (TS-шлюз) на игровом домене: контракт ручек, коды ошибок, WS, расчёт. const B = process.env.BASE || "https://smart-updown.binom.pw/api"; const RPC = process.env.RPC || "https://smart-updown.binom.pw/solana-rpc/"; const WS_URL = process.env.WS_URL || "wss://smart-updown.binom.pw/api/ws"; const WebSocket = require(process.env.WS_MODULE || "ws"); const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; const BET = 100000000n; const res = []; const ok = (name, cond, detail = "") => { res.push({ name, pass: !!cond, detail }); console.log(`${cond ? "OK " : "FAIL"} | ${name}${detail ? " — " + String(detail).slice(0, 150) : ""}`); }; const j = async (p, init) => { const r = await fetch(B + p, init); const t = await r.text(); let d = null; try { d = JSON.parse(t); } catch { d = t; } return { s: r.status, d }; }; const post = (p, body) => j(p, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); const rpc = async (m, p, commitment) => { const body = { jsonrpc: "2.0", id: 1, method: m, params: p }; if (commitment) body.params = [...p, { commitment }]; const r = await fetch(RPC, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); const x = await r.json(); if (x.error) throw new Error(JSON.stringify(x.error).slice(0, 120)); return x.result; }; const sol = (a, c) => rpc("getBalance", [a], c).then((r) => r.value); const tok = async (a, c) => { const cfg = { encoding: "jsonParsed" }; if (c) cfg.commitment = c; const r = await rpc("getTokenAccountsByOwner", [a, { programId: TOKEN_PROGRAM }, cfg]); return r.value.length ? BigInt(r.value[0].account.data.parsed.info.tokenAmount.amount) : 0n; }; const wait = (ms) => new Promise((r) => setTimeout(r, ms)); (async () => { console.log("### БЭК:", B, "\n"); // ---------- A. КОНТРАКТ РУЧЕК ---------- const st = await j("/state"); ok("GET /state → 200", st.s === 200, st.s); ok(" поля state на месте", ["counter", "nextBetId", "minAmount", "maxAmount", "multiplierBps", "expirySeconds", "paused", "admin", "relayer", "vaultBalance"] .every((k) => st.d && st.d[k] !== undefined), Object.keys(st.d || {}).join(",")); const pr = await j("/price"); ok("GET /price → 200 + symbol/price/ts", pr.s === 200 && pr.d.symbol && pr.d.price && pr.d.ts, JSON.stringify(pr.d).slice(0, 90)); const w = await post("/wallet", {}); const addr = w.d && w.d.address; ok("POST /wallet → 200 + адрес", w.s === 200 && !!addr, addr); const f1 = await j(`/faucet/${addr}`); ok("GET /faucet/{addr} → 200", f1.s === 200, JSON.stringify(f1.d).slice(0, 80)); const f2 = await j(`/faucet/${addr}`); ok(" повторный faucet идемпотентен (не удваивает)", f2.s === 200 && f2.d.sol === f1.d.sol, `${f1.d.sol} → ${f2.d.sol}`); const bal = await j(`/balance/${addr}`); ok("GET /balance/{addr} → 200 + sol/tokenUnits", bal.s === 200 && bal.d.solLamports !== undefined && bal.d.tokenUnits !== undefined, JSON.stringify(bal.d).slice(0, 90)); // ---------- B. КОДЫ ОШИБОК ---------- ok("POST /bet без адреса → 400", (await post("/bet", { side: "UP", amountUnits: "100000000" })).s === 400); ok("POST /bet кривая сторона → 400", (await post("/bet", { address: addr, side: "SIDEWAYS", amountUnits: "100000000" })).s === 400); ok("POST /bet нулевая сумма → 400", (await post("/bet", { address: addr, side: "UP", amountUnits: "0" })).s === 400); ok("POST /bet сумма ниже минимума → 400", (await post("/bet", { address: addr, side: "UP", amountUnits: "1" })).s === 400); ok("POST /bet сумма выше максимума → 400", (await post("/bet", { address: addr, side: "UP", amountUnits: "99999999999999" })).s === 400); ok("POST /bet чужой (не в keystore) адрес → 400", (await post("/bet", { address: "A2WeRZfCvhwUx8oQHPPhHbFiQxwn4XTURYBKetUdtHCZ", side: "UP", amountUnits: "100000000" })).s === 400); ok("GET /balance/{мусор} → 400", (await j("/balance/не-адрес")).s === 400); ok("POST /close → 404 (ручку убрали)", (await post("/close", {})).s === 404); ok("GET /bet/{id} → 404 (ручки НЕТ — фронту Игры нужна)", (await j("/bet/61")).s === 404); ok("POST /bet на несуществующий путь → 404", (await post("/nosuch", {})).s === 404); // ---------- C. WS ---------- const ws = new WebSocket(WS_URL); const ev = []; await new Promise((r) => { ws.on("open", r); ws.on("error", r); setTimeout(r, 8000); }); ok("WS /ws поднимается", ws.readyState === 1); ws.on("message", (m) => { try { ev.push(JSON.parse(m)); } catch {} }); await wait(5000); ok(" WS отдаёт price", ev.some((e) => e.type === "price"), ev.find((e) => e.type === "price") && JSON.stringify(ev.find((e) => e.type === "price")).slice(0, 80)); // ---------- D. ЛАТЕНТНОСТЬ КРАНА (цепь vs шлюз) ---------- console.log("\n### ЛАТЕНТНОСТЬ ЗАЧИСЛЕНИЯ (шлюз рапортует сразу, цепь — когда дошло)"); const w2 = await post("/wallet", {}); const a2 = w2.d.address; const t0 = Date.now(); await j(`/faucet/${a2}`); const gwAfter = await j(`/balance/${a2}`); let landed = null; for (let t = 0; t < 120; t++) { // до 30 с, шаг 250 мс await wait(250); if (await sol(a2) > 0) { landed = Date.now() - t0; break; } } console.log(` шлюз сразу после крана: sol=${gwAfter.d.sol}`); console.log(` на ЦЕПИ появилось через: ${landed === null ? ">30 c" : landed + " мс"}`); ok(" кран: цепь догоняет шлюз (лаг)", landed !== null && landed < 3000, `${landed} мс`); // ---------- E. ПОЛНЫЙ ЦИКЛ + ТОЧНЫЙ ЗАМЕР РАСЧЁТА ---------- console.log("\n### ПОЛНЫЙ ЦИКЛ (точный замер расчёта на цепи)"); await wait(12000); // ждём, пока кран осядет let cycle = 0, win = 0, loss = 0, tie = 0, mathOk = 0, mathBad = 0; for (const side of ["UP", "DOWN", "UP"]) { cycle++; const tokBefore = await tok(addr, "finalized"), solBefore = await sol(addr, "finalized"); const bet = await post("/bet", { address: addr, side, amountUnits: BET.toString() }); if (bet.s !== 200) { console.log(` раунд ${cycle}: ставка отклонена ${bet.s} ${JSON.stringify(bet.d)}`); break; } // ждём события закрытия let closed = null; for (let t = 0; t < 40; t++) { await wait(1000); closed = ev.filter((e) => e.type === "bet_closed" && String(e.id) === String(bet.d.id)).pop(); if (closed) break; } // ловим МОМЕНТ, когда фантики на цепи изменились const tEv = Date.now(); let delta = 0n, settledAt = null; for (let t = 0; t < 80; t++) { const tt = await tok(addr, "finalized"); if (tt !== tokBefore) { delta = tt - tokBefore; settledAt = Date.now() - tEv; break; } await wait(500); } const solAfter = await sol(addr, "finalized"); const nm = { 1: "ВЫИГРЫШ", 2: "ПРОИГРЫШ", 3: "НИЧЬЯ" }[closed && closed.status] || "?"; if (closed && closed.status === 1) win++; else if (closed && closed.status === 2) loss++; else if (closed && closed.status === 3) tie++; const expect = closed ? (closed.status === 1 ? BET * 3n / 10n : closed.status === 2 ? -BET : 0n) : null; const math = delta === expect; if (settledAt !== null) (math ? mathOk++ : mathBad++); console.log(` раунд ${cycle} (id=${bet.d.id}): ${nm} | фантики ${tokBefore} → ${tokBefore + delta} (Δ${delta}, ожидалось ${expect}) ${math ? "✓" : "✗"} | расчёт на цепи через ${settledAt} мс | SOL Δ=${solAfter - solBefore}`); ok(` раунд ${cycle}: исход ${nm} + расчёт сошёлся`, math, `Δ=${delta}`); await wait(2000); } const balEnd = await j(`/balance/${addr}`); console.log(`\n баланс шлюза в конце: ${JSON.stringify(balEnd.d)}`); console.log(` события WS: ${ev.filter((e) => e.type !== "price").map((e) => e.type).join(", ")}`); ok(" WS bet_open приходит", ev.some((e) => e.type === "bet_open")); ok(" WS bet_closed приходит", ev.some((e) => e.type === "bet_closed")); const bad = res.filter((r) => !r.pass); console.log(`\n=== ИТОГ: ${res.length - bad.length}/${res.length} пройдено ===`); if (bad.length) console.log("ПРОВАЛЫ:\n" + bad.map((b) => " - " + b.name + " [" + b.detail + "]").join("\n")); console.log(`исходы: выигрыш=${win} проигрыш=${loss} ничья=${tie}; математика: сошлось=${mathOk} не сошлось=${mathBad}`); ws.close(); process.exit(bad.length ? 1 : 0); })();