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 (релиз игры их сносил).
66 lines
3.0 KiB
JavaScript
66 lines
3.0 KiB
JavaScript
#!/usr/bin/env node
|
|
// ЧЬИ это 1.56e6 lamports за раунд: кошелёк ИГРОКА или кошелёк ШЛЮЗА?
|
|
// Мерим SOL игрока на цепи в 3 точках каждого раунда + SOL админа-шлюза.
|
|
const B = "https://smart-updown.binom.pw/api";
|
|
const RPC = "https://smart-updown.binom.pw/solana-rpc/";
|
|
const BET = 100000000n;
|
|
|
|
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, 120));
|
|
return x.result;
|
|
};
|
|
const sol = (a) => rpc("getBalance", [a]).then((r) => r.value);
|
|
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
(async () => {
|
|
const w = await post("/wallet", {});
|
|
if (w.s !== 200 || !w.d?.address) { console.log("нет адреса:", w.s, JSON.stringify(w.d)); process.exit(1); }
|
|
const p = w.d.address;
|
|
console.log(`игрок: ${p}`);
|
|
|
|
// админ-кошелёк шлюза — ищем в ответах состояния
|
|
const st = (await j("/state")).d || {};
|
|
const admin = st.admin || st.house || st.wallet || null;
|
|
console.log(`админ из /state: ${admin || "не указан"}`);
|
|
|
|
await j(`/faucet/${p}`);
|
|
for (let i = 0; i < 140 && (await sol(p)) === 0; i++) await wait(500);
|
|
console.log(`после крана на цепи: ${await sol(p)} lamports\n`);
|
|
|
|
const A = [];
|
|
|
|
for (let i = 1; i <= 3; i++) {
|
|
const before = BigInt(await sol(p));
|
|
const bet = await post("/bet", { address: p, side: i % 2 ? "UP" : "DOWN", amountUnits: BET.toString() });
|
|
if (bet.s !== 200) { console.log(`раунд ${i}: ставка ${bet.s} ${JSON.stringify(bet.d)}`); break; }
|
|
await wait(1200);
|
|
const mid = BigInt(await sol(p));
|
|
|
|
// ждём закрытия и осадки
|
|
let after = mid, t0 = Date.now();
|
|
for (let t = 0; t < 90; t++) { await wait(700); const x = BigInt(await sol(p)); if (x !== mid) { after = x; break; } }
|
|
|
|
console.log(`раунд ${i} (id=${bet.d.id})`);
|
|
console.log(` SOL игрока: до ${before} → после ставки ${mid} → в конце ${after}`);
|
|
console.log(` Δ за раунд: ${after - before} lamports` + (admin ? ` | админ ${await sol(admin)}` : ""));
|
|
A.push(after - before);
|
|
await wait(1200);
|
|
}
|
|
|
|
console.log(`\n=== дельты SOL игрока по раундам: ${A.join(", ")} ===`);
|
|
console.log(`среднее: ${A.length ? (A.reduce((a, b) => a + b, 0n) / BigInt(A.length)) : 0} lamports/раунд`);
|
|
const fin = (await j(`/balance/${p}`)).d;
|
|
console.log(`отчёт шлюза по игроку: ${JSON.stringify(fin)}`);
|
|
process.exit(0);
|
|
})();
|