/** * ЖИВАЯ приёмка API v2 шлюза smart-updown (ставка без суммы, 30% от баланса). * * BASE=https://smart-updown.binom.pw/api node gateway-api-v2-check.cjs * * Проверяет: * 1. GET /state — все СТАРЫЕ поля на месте (обратная совместимость) * 2. GET /state — новые поля: bettor, betPercent, canBet, canBetReasonCode, * canBetReason, betAmountUnits, balance, openBet, lastBet * 3. /state — openBet и lastBet взаимоисключающие; у ставки есть status * 4. /state — можно ли ставить (canBet) и что баланс читается * 5. POST /bet {side:"up"} — БЕЗ суммы: accepted=true, есть betId и txSig * 6. POST /bet — retryAfterSeconds это ЧИСЛО (дельта), 0 < r <= expiry * 7. GET /state сразу после — openBet не null, status="open" * 8. POST /bet повторно — accepted=false, reasonCode=OPEN_BET, retryAfterSeconds > 0 * 9. POST /bet {side:"кривой"} — 400 * 10. ждать экспирацию ~20 с — /state: openBet=null, lastBet.status="closed" */ const BASE = process.env.BASE || "https://smart-updown.binom.pw/api"; 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)); // Поля, которые читает тестовый фронт и которые НЕЛЬЗЯ терять. const LEGACY_STATE = [ "counter", "nextBetId", "minAmount", "maxAmount", "minAmountHuman", "maxAmountHuman", "multiplierBps", "expirySeconds", "paused", "admin", "relayer", "vaultBalance", "vaultBalanceHuman", ]; const NEW_STATE = [ "bettor", "betPercent", "betAmountUnits", "betAmountHuman", "canBet", "canBetReasonCode", "canBetReason", "balance", "openBet", "lastBet", ]; (async () => { console.log(`BASE=${BASE}\n`); // --- 1-2. /state: старые и новые поля --- const st = await j("/state"); const b = st.body || {}; const missingLegacy = LEGACY_STATE.filter((k) => !(k in b)); check("GET /state → 200", st.status === 200, `status=${st.status}`); check("нет потери старых полей", missingLegacy.length === 0, missingLegacy.length ? "нет: " + missingLegacy.join(",") : `${LEGACY_STATE.length} полей на месте`); const missingNew = NEW_STATE.filter((k) => !(k in b)); check("новые поля присутствуют", missingNew.length === 0, missingNew.length ? "нет: " + missingNew.join(",") : `${NEW_STATE.length} полей на месте`); console.log(` bettor=${b.bettor} betPercent=${b.betPercent}`); console.log(` canBet=${b.canBet} code=${b.canBetReasonCode} reason=${b.canBetReason}`); console.log(` balance=${JSON.stringify(b.balance)}`); console.log(` betAmountUnits=${b.betAmountUnits} (${b.betAmountHuman})`); // --- 3. взаимоисключение openBet / lastBet + поле status --- const openBet = b.openBet || null; const lastBet = b.lastBet || null; check("openBet и lastBet не оба заполнены", !(openBet && lastBet), `openBet=${openBet ? openBet.id : "null"} lastBet=${lastBet ? lastBet.id : "null"}`); const shown = openBet || lastBet; if (shown) { check("у ставки есть поле status (open|closed)", shown.status === "open" || shown.status === "closed", `status=${shown.status}`); check("у ставки есть side/amountUnits/entryPrice", !!shown.side && !!shown.amountUnits && shown.entryPrice !== undefined, `side=${shown.side} amount=${shown.amountUnits} entry=${shown.entryPrice}`); } else { console.log(" (ставок ещё нет — пропускаем проверки формы)"); } // --- 4. можно ли ставить --- if (b.canBet !== true) { check("canBet=true (готовы ставить)", false, `code=${b.canBetReasonCode} reason=${b.canBetReason}`); console.log("\nдальше невозможно: шлюз отказывает. Причина выше."); process.exit(1); } check("canBet=true (готовы ставить)", true, `размер=${b.betAmountUnits}`); // --- 5. ставка БЕЗ суммы --- const bet1 = await j("/bet", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ side: "up" }), }); const r1 = bet1.body || {}; console.log(` ответ /bet: ${JSON.stringify(r1).slice(0, 300)}`); check("POST /bet {side} → 200", bet1.status === 200, `status=${bet1.status}`); check("ставка принята (accepted=true)", r1.accepted === true, `accepted=${r1.accepted} code=${r1.reasonCode} reason=${r1.reason}`); const betId = r1.betId || r1.id; check("есть betId (п.6)", !!betId, `betId=${betId}`); check("есть txSig (п.6)", typeof r1.txSig === "string" && r1.txSig.length > 20, `txSig=${r1.txSig ? r1.txSig.slice(0, 24) + "…" : r1.txSig}`); // --- 6. retryAfterSeconds — ДЕЛЬТА --- const retry = r1.retryAfterSeconds; const expiry = Number(b.expirySeconds || 15); check("retryAfterSeconds — число-дельта (п.5)", typeof retry === "number" && retry > 0 && retry <= expiry, `retryAfterSeconds=${retry} (expiry=${expiry})`); // --- 7. /state сразу после: открытая ставка --- await sleep(1500); const st2 = (await j("/state")).body || {}; const ob = st2.openBet; check("openBet заполнен после ставки (п.7)", !!ob, `openBet=${ob ? ob.id : "null"}`); if (ob) { check("openBet.status = \"open\"", ob.status === "open", `status=${ob.status}`); check("openBet.id совпадает с ответом /bet", String(ob.id) === String(betId), `openBet.id=${ob.id} betId=${betId}`); check("openBet.retryAfterSeconds > 0", typeof ob.retryAfterSeconds === "number" && ob.retryAfterSeconds >= 0, `retryAfterSeconds=${ob.retryAfterSeconds}`); } // --- 8. вторая ставка при открытой: отказ OPEN_BET --- const bet2 = await j("/bet", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ side: "down" }), }); const r2 = bet2.body || {}; check("вторая ставка отклонена (accepted=false)", r2.accepted === false, `accepted=${r2.accepted}`); check("reasonCode=OPEN_BET", r2.reasonCode === "OPEN_BET", `code=${r2.reasonCode}`); check("есть reason по-русски (п.2)", typeof r2.reason === "string" && r2.reason.length > 5, `reason=${r2.reason}`); check("retryAfterSeconds > 0 при отказе по открытой (п.5)", typeof r2.retryAfterSeconds === "number" && r2.retryAfterSeconds > 0, `retryAfterSeconds=${r2.retryAfterSeconds}`); // --- 9. кривой side --- const bet3 = await j("/bet", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ side: "вбок" }), }); check("кривой side → 400", bet3.status === 400, `status=${bet3.status}`); // --- 10. дождаться авто-закрытия --- console.log("\nждём авто-закрытие (~22 с)…"); await sleep(22000); const st3 = (await j("/state")).body || {}; check("после экспирации openBet = null", st3.openBet === null, `openBet=${st3.openBet ? st3.openBet.id : "null"}`); const lb = st3.lastBet; check("lastBet заполнен после закрытия (п.7)", !!lb, `lastBet=${lb ? lb.id : "null"}`); if (lb) { check("lastBet.status = \"closed\"", lb.status === "closed", `status=${lb.status}`); check("lastBet.result заполнен", !!lb.result, `result=${lb.result} statusCode=${lb.statusCode}`); } check("canBet снова true после расчёта", st3.canBet === true, `canBet=${st3.canBet} code=${st3.canBetReasonCode}`); // --- итог --- const failed = results.filter((r) => !r.ok); console.log(`\n=== ИТОГ: ${results.length - failed.length}/${results.length} пройдено ===`); if (failed.length) { console.log("ПРОВАЛЫ:"); for (const f of failed) console.log(` - ${f.name} :: ${f.detail || ""}`); process.exit(1); } console.log("все проверки пройдены"); })().catch((e) => { console.error("исключение:", e); process.exit(2); });