101a4b60a9
Найдено при приёмке боевого домена smart-updown.binom.pw (симптом «SOL не обновляется»). - SEC-16: при create_bet ренту PDA-аккаунта ставки (1 559 040 lamports = (96+32+128)*6960) платит БЕТТОР, а не шлюз; баланс шлюза-админа при этом не меняется (замер whose-fee.cjs). Аккаунт не закрывается никогда (в контракте 9ALs четыре инструкции, ни одной close = bettor) → рента теряется безвозвратно. В цепи 75 аккаунтов = 0.116928 SOL заперто, растёт линейно. Вердикт: на тестах терпимо (маскируется краном, 5 SOL = ~3207 раундов), перед mainnet — блокер. - TASKS.md: фича «если цена не изменилась — подождать ещё 15 с» (НЕ доделана), заметка про боевой домен на общем TS-шлюзе и лаг крана 8–13 с. - tools: доказательные скрипты (whose-fee, bet-pda-rent, faucet-latency, sol-balance-cycle, settle-math-test, backend-full-test) + README с питфоллами замера. - k8s/smart-updown-api-ingress.yaml: /api,/ws вынесены вне helm (релиз игры их сносил).
84 lines
4.5 KiB
JavaScript
84 lines
4.5 KiB
JavaScript
#!/usr/bin/env node
|
|
// КОРРЕКТНЫЙ замер расчёта: шлюз КАСТОДИАЛЬНЫЙ, фантики уходят в vault в момент
|
|
// ставки (lock), возврат/выплата — при закрытии. Значит дельту надо считать
|
|
// ОТ момента после lock, иначе в неё попадёт и ставка, и выплата (двойной счёт).
|
|
const B = "https://smart-updown.binom.pw/api";
|
|
const RPC = "https://smart-updown.binom.pw/solana-rpc/";
|
|
const WebSocket = require(process.env.WS_MODULE || "ws");
|
|
const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
const BET = 100000000n; // 100 фантиков
|
|
|
|
const j = async (p, init) => {
|
|
const r = await fetch(B + p, init);
|
|
return { s: r.status, d: await r.json().catch(() => null) };
|
|
};
|
|
const post = (p, b) => j(p, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(b) });
|
|
const rpc = async (m, p) => {
|
|
const r = await fetch(RPC, {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: m, params: p }),
|
|
});
|
|
const x = await r.json();
|
|
if (x.error) throw new Error(JSON.stringify(x.error).slice(0, 100));
|
|
return x.result;
|
|
};
|
|
const sol = (a) => rpc("getBalance", [a]).then((r) => r.value);
|
|
const tok = async (a) => {
|
|
const r = await rpc("getTokenAccountsByOwner", [a, { programId: TOKEN_PROGRAM }, { encoding: "jsonParsed" }]);
|
|
return r.value.length ? BigInt(r.value[0].account.data.parsed.info.tokenAmount.amount) : 0n;
|
|
};
|
|
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
const STATUS = { 1: "ВЫИГРЫШ", 2: "ПРОИГРЫШ", 3: "НИЧЬЯ(возврат)" };
|
|
|
|
(async () => {
|
|
const w = await post("/wallet", {});
|
|
const addr = w.d.address;
|
|
await j(`/faucet/${addr}`);
|
|
console.log(`игрок ${addr}\nкран выдан, ждём зачисления на цепь...`);
|
|
for (let i = 0; i < 120 && (await sol(addr)) === 0; i++) await wait(500);
|
|
console.log(`на цепи: ${await sol(addr)} lamports\n`);
|
|
|
|
const ws = new WebSocket("wss://smart-updown.binom.pw/api/ws");
|
|
const ev = [];
|
|
await new Promise((r) => { ws.on("open", r); ws.on("error", r); setTimeout(r, 6000); });
|
|
ws.on("message", (m) => { try { const e = JSON.parse(m); if (e.type !== "price") ev.push(e); } catch {} });
|
|
|
|
let good = 0, bad = 0;
|
|
for (let i = 1; i <= 4; i++) {
|
|
const start = await tok(addr);
|
|
const bet = await post("/bet", { address: addr, side: i % 2 ? "UP" : "DOWN", amountUnits: BET.toString() });
|
|
if (bet.s !== 200) { console.log(`раунд ${i}: ставка отклонена ${bet.s} ${JSON.stringify(bet.d)}`); break; }
|
|
|
|
// ждём LOCK: фантики ушли в vault (-BET)
|
|
let lock = null;
|
|
for (let t = 0; t < 40; t++) { await wait(400); const x = await tok(addr); if (x === start - BET) { lock = x; break; } }
|
|
if (lock === null) { console.log(`раунд ${i}: lock не виден (${start} → ${await tok(addr)})`); continue; }
|
|
|
|
// ждём закрытие
|
|
let closed = null;
|
|
for (let t = 0; t < 40; t++) {
|
|
await wait(750);
|
|
closed = ev.filter((e) => e.type === "bet_closed" && String(e.id) === String(bet.d.id)).pop();
|
|
if (closed) break;
|
|
}
|
|
// ждём PAYOUT: баланс МЕНЯЕТСЯ от зафиксированного lock
|
|
let after = lock, payoutAt = null;
|
|
const tC = Date.now();
|
|
for (let t = 0; t < 60; t++) { await wait(400); const x = await tok(addr); if (x !== lock) { after = x; payoutAt = Date.now() - tC; break; } }
|
|
|
|
const paid = after - lock; // 0 = проигрыш, +BET = возврат, +1.3*BET = выигрыш
|
|
const nm = closed ? STATUS[closed.status] : "НЕ ЗАКРЫЛОСЬ";
|
|
const expect = closed ? (closed.status === 1 ? BET * 13000n / 10000n : closed.status === 2 ? 0n : BET) : null;
|
|
const goodNow = expect !== null && paid === expect;
|
|
goodNow ? good++ : bad++;
|
|
console.log(`раунд ${i} (id=${bet.d.id}): ${nm}`);
|
|
console.log(` фантики: ${start} → lock ${lock} → после ${after}`);
|
|
console.log(` выплата из vault = ${paid} (ожидалось ${expect}) ${goodNow ? "✓ ВЕРНО" : "✗ РАСХОЖДЕНИЕ"} [осело через ${payoutAt} мс]`);
|
|
await wait(1500);
|
|
}
|
|
console.log(`\n=== математика расчёта: верно=${good} неверно=${bad} ===`);
|
|
console.log(`баланс шлюза: ${JSON.stringify((await j(`/balance/${addr}`)).d)}`);
|
|
ws.close();
|
|
process.exit(0);
|
|
})();
|