docs(sec): SEC-9 закрыт — возврат залога при ничьей
Сводка: открыто 8 (было 9), закрыто 8. SEC-9 перенесён в закрытые с полной историей: причина (close_bet.rs:66), замер доли ничьих (33.6%), решение владельца (правка контракта), состав правок, собственная мутационная приёмка, деплой и живой эффект (залипшие #0/#1 закрылись сами, ваулт −1000). Хвост SEC-8 о «вечном залипании» помечен как устранённый. Инструменты приёмки: - tie-refund-accept.cjs — ставки циклом, проверка статуса на цепи (живой прогон: 6 возвратов из 25 ставок) - tie-distribution.cjs, policy-economics.cjs — выбор политики ничьей - vault-check.cjs — разбор Bet-аккаунтов сырыми байтами
This commit is contained in:
+19
-5
@@ -10,11 +10,13 @@
|
|||||||
Пункты нумеруются сквозным `SEC-N` (не меняется при добавлении/закрытии пунктов) — ссылаться на них
|
Пункты нумеруются сквозным `SEC-N` (не меняется при добавлении/закрытии пунктов) — ссылаться на них
|
||||||
номером. Отмечать закрытое — `[ ]` → `[x]`, в закрытую строку добавлять дату и коммит/релиз.
|
номером. Отмечать закрытое — `[ ]` → `[x]`, в закрытую строку добавлять дату и коммит/релиз.
|
||||||
|
|
||||||
**Сводка:** открыто 9 — критичных 1 (`SEC-5`, частично закрыт), экономика 2 (`SEC-7`,`SEC-9`),
|
**Сводка:** открыто 8 — критичных 1 (`SEC-5`, частично закрыт), экономика 1 (`SEC-7`),
|
||||||
инфраструктура 3 (`SEC-10…SEC-12`), отложено 3 (`SEC-13…SEC-15`). Закрыто: 7 —
|
инфраструктура 3 (`SEC-10…SEC-12`), отложено 3 (`SEC-13…SEC-15`). Закрыто: 8 —
|
||||||
`SEC-6` (2026-09-13, `updown-relayer:2` / `test-front:3`),
|
`SEC-6` (2026-09-13, `updown-relayer:2` / `test-front:3`),
|
||||||
`SEC-1`, `SEC-2`, `SEC-4` (2026-09-13, `updown-relayer:3`, коммит `0b136c6`),
|
`SEC-1`, `SEC-2`, `SEC-4` (2026-09-13, `updown-relayer:3`, коммит `0b136c6`),
|
||||||
`SEC-3`, `SEC-8` (2026-09-13, `updown-relayer:6` — авто-закрытие + одна открытая ставка на адрес),
|
`SEC-3`, `SEC-8` (2026-09-13, `updown-relayer:6` — авто-закрытие + одна открытая ставка на адрес),
|
||||||
|
**`SEC-9`** (2026-09-14, `updown-relayer:7` / `test-front:5` — возврат залога при ничьей в контракте,
|
||||||
|
коммит `42dfa58`),
|
||||||
плюс `SEC-2` и `SEC-4` усилены в `:6`.
|
плюс `SEC-2` и `SEC-4` усилены в `:6`.
|
||||||
|
|
||||||
**Итерация 2026-09-13 (`updown-relayer:6`) — что сделано:** авто-закрытие ставок серверным
|
**Итерация 2026-09-13 (`updown-relayer:6`) — что сделано:** авто-закрытие ставок серверным
|
||||||
@@ -51,9 +53,21 @@
|
|||||||
- [ ] **SEC-7. Множитель 1.3× требует от дома винрейта ~77 %** — `SMART_MULTIPLIER_BPS=13000`; порог безубытка = 1/1.3 = 76.9 %. При честной монете дом убыточен, а с дырой окна закрытия игрок забирает близко к 100 %. Фикс: пересчитать экономику под реальный edge.
|
- [ ] **SEC-7. Множитель 1.3× требует от дома винрейта ~77 %** — `SMART_MULTIPLIER_BPS=13000`; порог безубытка = 1/1.3 = 76.9 %. При честной монете дом убыточен, а с дырой окна закрытия игрок забирает близко к 100 %. Фикс: пересчитать экономику под реальный edge.
|
||||||
- [x] **SEC-8. Нефинализированные ставки запирают средства в ваулте** — **ЗАКРЫТ 2026-09-13** (релиз `updown-relayer:6`). Причины было две, обе устранены. **(1) Закрывать было некому:** авто-закрытия не существовало, а клиент мог просто не позвать `/close` — теперь шлюз закрывает сам (см. SEC-2), и ставка не может остаться висеть. **(2) Можно было набрать много ставок разом:** ничто не мешало занять сто ставок, заперев сто залогов; теперь на адрес допускается **одна открытая** ставка (вторая → `409`), поэтому размер запертого одним человеком залога ограничен одной ставкой.
|
- [x] **SEC-8. Нефинализированные ставки запирают средства в ваулте** — **ЗАКРЫТ 2026-09-13** (релиз `updown-relayer:6`). Причины было две, обе устранены. **(1) Закрывать было некому:** авто-закрытия не существовало, а клиент мог просто не позвать `/close` — теперь шлюз закрывает сам (см. SEC-2), и ставка не может остаться висеть. **(2) Можно было набрать много ставок разом:** ничто не мешало занять сто ставок, заперев сто залогов; теперь на адрес допускается **одна открытая** ставка (вторая → `409`), поэтому размер запертого одним человеком залога ограничен одной ставкой.
|
||||||
*Проверено на живом стенде:* вторая ставка при открытой первой → `409`; висевшая ставка `id=14` закрыта автоматически через 16 с, средства разблокированы.
|
*Проверено на живом стенде:* вторая ставка при открытой первой → `409`; висевшая ставка `id=14` закрыта автоматически через 16 с, средства разблокированы.
|
||||||
*Остаток:* если ставка попала в «равные цены» (`exit == entry`, см. SEC-9), планировщик повторяет попытку и логирует `still open … will retry` — молчаливый повтор, средства при этом всё равно остаются запертыми до первого движения цены. Это отдельная проблема SEC-9.
|
*Остаток (закрыт):* если ставка попадала в «равные цены» (`exit == entry`), залог оставался запертым, а из-за правила «одна ставка на адрес» игрок терял возможность играть (`409`). **Устранено 2026-09-14 вместе с `SEC-9`** — ничья возвращает залог и финализирует ставку, поэтому залипаний больше нет.
|
||||||
- [ ] **SEC-9. Равные `exit_price == entry_price` не финализируют ставку** — `close_bet` делает `return Ok(())`, статус остаётся `open`; при «плоской» цене ставка зависает без исхода. Место: `close_bet.rs:66`. Фикс: определить политику (продление / возврат / ничья) вместо молчаливого выхода.
|
- [x] **SEC-9. Равные `exit_price == entry_price` не финализировали ставку — ЗАКРЫТО 2026-09-14 (возврат залога в контракте).**
|
||||||
|
**Было:** `close_bet.rs:66` делал `return Ok(())`, статус оставался `open`; `settle.ts:303-304` берёт цену выхода по **фиксированному** моменту экспирации, поэтому ретрай каждые 2 с вечно получал ту же пару — залипание ВЕЧНОЕ. Замер: равные цены — **33.6 % окон** (11 682 из 34 769 за 580 минут). Наложенный эффект `SEC-8`: игрок с залипшей ставкой получал `409` навсегда.
|
||||||
|
**Решение владельца:** править контракт (залог лежит в ваулте под PDA — тронуть может только контракт, значит «настоящий возврат» возможен лишь правкой Rust). Программа обновляемая, authority `GXd83Q…` — у нас.
|
||||||
|
**Сделано:**
|
||||||
|
* `constants.rs`: `STATUS_REFUNDED = 3` (0/1/2 не тронуты — зашиты в шлюз и в живых аккаунтах цепи).
|
||||||
|
* `close_bet.rs`: при `exit == entry` — CPI-перевод `amount` из ваулта беттору под сидами ваулт-PDA, запись `exit_price` и `status = 3`, событие `BetClosed { user_won: false, payout: amount, status: 3 }`.
|
||||||
|
* `settle.ts`: `STATUS_REFUNDED`, `statusName → "refunded"`, статус 3 в условии финализации (иначе возврат попадал в `unexpected status` и WS-событие игроку не уходило).
|
||||||
|
* `check.ts`: проверка формы события `refunded`.
|
||||||
|
* Тест `close_bet_equal_price_stays_open` (закреплял старое поведение) → `close_bet_equal_price_refunds` + отклонение повторного закрытия.
|
||||||
|
* UI (`test-front/index.html`): три исхода вместо двух — ничья больше не показывается как «ПРОИГРАЛ (house_won)»; `API.md` дополнен.
|
||||||
|
**Проверка (своими руками, не отчётом агента):** `cargo test-sbf` 10/10 зелёных; `npm run typecheck` EXIT=0; две мутации (снять запись статуса / снять CPI-перевод) валят ровно новый тест, после отката снова 10/10.
|
||||||
|
**Деплой:** программа `9ALs…` апгрейднута (ProgramData расширен 247 848 → 267 848, хэш дампа из цепи совпал с локальным `.so` побайтово); шлюз `updown-relayer:7` (helm rev 15); фронт `test-front:5` (rev 5).
|
||||||
|
**Живой эффект:** две залипшие ставки (`#0`, `#1`) закрылись **сами** статусом 3; в обеих транзакциях ваулт −500 и игрок +500 фантиков (проверено `preTokenBalances`/`postTokenBalances`), ваулт 26 850 → 25 850. Канал `bet_closed` → `statusName: "refunded"` подтверждён.
|
||||||
|
*Остаток:* политика выбрана как «возврат залога без множителя»; доля ничьих ~33 % означает, что треть ставок не приносит дому ни прибыли, ни убытка (экономика остаётся положительной за счёт множителя 1.3×).
|
||||||
## Средний приоритет — инфраструктура
|
## Средний приоритет — инфраструктура
|
||||||
|
|
||||||
- [ ] **SEC-10. NATS без аутентификации** — `auth_required` не выставлен, анонимная публикация в шину проходит (проверено на тестовом subject; боевой не трогали). Пока цена условна — терпимо; как только станет денежной, у шины нет прав на запись и любой сможет напечатать свой курс. Фикс: auth + subject-ACL, шлюзу — read-only на `market.price.*`.
|
- [ ] **SEC-10. NATS без аутентификации** — `auth_required` не выставлен, анонимная публикация в шину проходит (проверено на тестовом subject; боевой не трогали). Пока цена условна — терпимо; как только станет денежной, у шины нет прав на запись и любой сможет напечатать свой курс. Фикс: auth + subject-ACL, шлюзу — read-only на `market.price.*`.
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// Экономика: что делает каждая политика ничьей с балансом дома.
|
||||||
|
// Считаем по реальному распределению |Δ| из живой истории.
|
||||||
|
// Ставка 1 фантик, выигрыш 1.3× (multiplier_bps=13000). Ничья: возврат 1.0 (залог целиком).
|
||||||
|
const { DatabaseSync } = require('node:sqlite');
|
||||||
|
const db = new DatabaseSync(process.env.DB || '/app/data/price-history.db');
|
||||||
|
const rows = db.prepare('SELECT ts, price FROM price_history ORDER BY ts ASC').all();
|
||||||
|
const vals = rows.map((r) => ({ ts: Number(r.ts), p: Number(r.price) }));
|
||||||
|
const SCALE = 1e8, W = 15000, STEP = 1000;
|
||||||
|
const at = (ts) => { let lo = 0, hi = vals.length - 1, res = null;
|
||||||
|
while (lo <= hi) { const m = (lo + hi) >> 1;
|
||||||
|
if (vals[m].ts <= ts) { res = vals[m].p; lo = m + 1; } else hi = m - 1; } return res; };
|
||||||
|
|
||||||
|
const hist = {};
|
||||||
|
let n = 0;
|
||||||
|
for (let t = vals[0].ts; t + W <= vals[vals.length - 1].ts; t += STEP) {
|
||||||
|
const a = at(t), b = at(t + W);
|
||||||
|
if (a === null || b === null) continue;
|
||||||
|
const kop = Math.round(Math.abs(b - a) / SCALE * 100);
|
||||||
|
hist[kop] = (hist[kop] || 0) + 1; n++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Считаем деньги. Игрок ставит вслепую (случайный UP/DOWN) => половина окон выигрышная,
|
||||||
|
// половина проигрышная. На ничьих политика решает.
|
||||||
|
const MULT = 1.3;
|
||||||
|
function simulate(policy) {
|
||||||
|
let vaultDelta = 0, refunded = 0, decided = 0;
|
||||||
|
for (const [k, cnt] of Object.entries(hist)) {
|
||||||
|
const kop = Number(k);
|
||||||
|
if (kop === 0 && policy === 'refund') { vaultDelta += 0; refunded += cnt; continue; }
|
||||||
|
if (kop === 0 && policy === 'neighbour') { /* трактуем как решение: половина выиграла */
|
||||||
|
vaultDelta += cnt * (0.5 * (1 - MULT) + 0.5 * 1); decided += cnt; continue; }
|
||||||
|
if (kop === 0 && policy === 'house') { vaultDelta += cnt * 1; continue; } // дом забирает
|
||||||
|
decided += cnt;
|
||||||
|
vaultDelta += cnt * (0.5 * (1 - MULT) + 0.5 * 1);
|
||||||
|
}
|
||||||
|
return { vaultDelta, refunded, decided };
|
||||||
|
}
|
||||||
|
console.log('окон:', n);
|
||||||
|
for (const p of ['refund', 'neighbour', 'house']) {
|
||||||
|
const r = simulate(p);
|
||||||
|
const per = r.vaultDelta / n;
|
||||||
|
console.log(`политика=${p.padEnd(10)} дом за окно=${per.toFixed(4)} фантика | возвращено окон=${r.refunded} (${((100*r.refunded)/n).toFixed(1)}%)`);
|
||||||
|
}
|
||||||
|
console.log('--- при 1000 ставках ---');
|
||||||
|
for (const p of ['refund', 'neighbour', 'house']) {
|
||||||
|
const r = simulate(p);
|
||||||
|
console.log(` ${p.padEnd(10)} дом: ${(r.vaultDelta / n * 1000).toFixed(1)} фантиков на 1000 ставок`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// Полное распределение: тики между входом и выходом 15-сек окна + шаг самого фида.
|
||||||
|
const { DatabaseSync } = require('node:sqlite');
|
||||||
|
const db = new DatabaseSync(process.env.DB || '/app/data/price-history.db');
|
||||||
|
const rows = db.prepare('SELECT ts, price FROM price_history ORDER BY ts ASC').all();
|
||||||
|
const vals = rows.map((r) => ({ ts: Number(r.ts), p: Number(r.price) }));
|
||||||
|
const SCALE = 1e8;
|
||||||
|
console.log('записей:', vals.length, '| диапазон:', (vals[0].p / SCALE).toFixed(2), '→', (vals[vals.length - 1].p / SCALE).toFixed(2));
|
||||||
|
|
||||||
|
// Шаг самого фида: как меняется цена от записи к записи
|
||||||
|
const stepHist = {};
|
||||||
|
let zeroSteps = 0;
|
||||||
|
for (let i = 1; i < vals.length; i++) {
|
||||||
|
const d = Math.round(Math.abs(vals[i].p - vals[i - 1].p) / SCALE * 100); // в сотых
|
||||||
|
stepHist[d] = (stepHist[d] || 0) + 1;
|
||||||
|
if (d === 0) zeroSteps++;
|
||||||
|
}
|
||||||
|
const stepKeys = Object.keys(stepHist).map(Number).sort((a, b) => a - b);
|
||||||
|
console.log('--- шаг фида (от записи к записи, в копейках) ---');
|
||||||
|
for (const k of stepKeys.slice(0, 12)) console.log(` ровно ${k} коп: ${stepHist[k]}`);
|
||||||
|
console.log(' уникальных шагов:', stepKeys.length, '| «без изменений»:', zeroSteps, `(${((100 * zeroSteps) / (vals.length - 1)).toFixed(1)}%)`);
|
||||||
|
|
||||||
|
const at = (ts) => {
|
||||||
|
let lo = 0, hi = vals.length - 1, res = null;
|
||||||
|
while (lo <= hi) {
|
||||||
|
const m = (lo + hi) >> 1;
|
||||||
|
if (vals[m].ts <= ts) { res = vals[m].p; lo = m + 1; } else hi = m - 1;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const W = 15000, STEP = 1000;
|
||||||
|
const hist = {};
|
||||||
|
let n = 0;
|
||||||
|
for (let t = vals[0].ts; t + W <= vals[vals.length - 1].ts; t += STEP) {
|
||||||
|
const a = at(t), b = at(t + W);
|
||||||
|
if (a === null || b === null) continue;
|
||||||
|
const kop = Math.round(Math.abs(b - a) / SCALE * 100);
|
||||||
|
hist[kop] = (hist[kop] || 0) + 1;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
const keys = Object.keys(hist).map(Number).sort((a, b) => a - b);
|
||||||
|
console.log('--- окон 15 с проверено:', n, '| уникальных значений |delta|:', keys.length);
|
||||||
|
console.log(' распределение по модулю разницы (копейки):');
|
||||||
|
for (const k of keys.slice(0, 15)) console.log(` |Δ|=${k} коп: ${hist[k]} (${((100 * hist[k]) / n).toFixed(2)}%)`);
|
||||||
|
const tie = hist[0] || 0;
|
||||||
|
console.log(`НИЧЬЯ (|Δ|=0): ${tie} = ${((100 * tie) / n).toFixed(2)}%`);
|
||||||
|
const near = (keys.filter((k) => k > 0 && k <= 5).reduce((s, k) => s + hist[k], 0));
|
||||||
|
console.log(`почти-ничья (|Δ| = 1..5 коп): ${near} = ${((100 * near) / n).toFixed(2)}%`);
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// Приёмка возврата при ничьей на ЖИВОМ стенде.
|
||||||
|
// Цикл: новый кошелёк → фаусет → ставка → ожидание авто-закрытия → чтение статуса.
|
||||||
|
// Победа = увидеть status=3 (REFUNDED) с exit==entry на НОВОЙ ставке.
|
||||||
|
const GW = process.env.GW || 'https://testfront.binom.pw/api';
|
||||||
|
const RPC = process.env.RPC || 'http://192.168.76.181:8899';
|
||||||
|
const PROG = '9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf';
|
||||||
|
const { Connection, PublicKey } = require('@solana/web3.js');
|
||||||
|
|
||||||
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
async function j(path, body) {
|
||||||
|
const r = await fetch(GW + path, {
|
||||||
|
method: body ? 'POST' : 'GET',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
});
|
||||||
|
const t = await r.text();
|
||||||
|
let d = null; try { d = JSON.parse(t); } catch {}
|
||||||
|
if (!r.ok) throw new Error(`HTTP ${r.status} ${t.slice(0, 120)}`);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function betPda(id) {
|
||||||
|
const b = Buffer.alloc(8); b.writeBigUInt64LE(BigInt(id));
|
||||||
|
return PublicKey.findProgramAddressSync([Buffer.from('bet'), b], new PublicKey(PROG))[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readBet(conn, id) {
|
||||||
|
const a = await conn.getAccountInfo(betPda(id));
|
||||||
|
if (!a) return null;
|
||||||
|
const d = a.data;
|
||||||
|
return {
|
||||||
|
amount: d.readBigUInt64LE(8),
|
||||||
|
entry: d.readBigUInt64LE(32),
|
||||||
|
exit: d.readBigUInt64LE(40),
|
||||||
|
status: Number(d.readBigUInt64LE(56)),
|
||||||
|
bettor: new PublicKey(d.subarray(64, 96)).toBase58(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const conn = new Connection(RPC, 'confirmed');
|
||||||
|
const MAX = Number(process.env.MAX || 8);
|
||||||
|
let ties = 0, normal = 0;
|
||||||
|
|
||||||
|
for (let i = 1; i <= MAX; i++) {
|
||||||
|
let id;
|
||||||
|
try {
|
||||||
|
const w = await j('/wallet', {});
|
||||||
|
await j('/faucet/' + w.address);
|
||||||
|
const bet = await j('/bet', { side: 'UP', amountUnits: '100000000', address: w.address });
|
||||||
|
id = bet.id;
|
||||||
|
process.stdout.write(`[${i}] ставка #${id} открыта entry=${bet.entryPrice} … `);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[${i}] ОШИБКА ставки: ${e.message}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ждём авто-закрытие (expiry 15s + тик планировщика 2s + запас)
|
||||||
|
let b = null;
|
||||||
|
for (let t = 0; t < 45; t++) {
|
||||||
|
await sleep(2000);
|
||||||
|
b = await readBet(conn, id).catch(() => null);
|
||||||
|
if (b && b.status !== 0) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!b) { console.log('аккаунт не читается'); continue; }
|
||||||
|
if (b.status === 0) { console.log(`осталась open (exit=${b.exit}) — не закрылась`); continue; }
|
||||||
|
|
||||||
|
const names = { 1: 'payout_done', 2: 'house_won', 3: 'REFUNDED' };
|
||||||
|
const name = names[b.status] || ('status=' + b.status);
|
||||||
|
if (b.status === 3) {
|
||||||
|
ties++;
|
||||||
|
const ok = b.exit === b.entry;
|
||||||
|
console.log(`✅ НИЧЬЯ→ВОЗВРАТ: exit=${b.exit} entry=${b.entry} ${ok ? '(равны!)' : '(НЕ равны — ОШИБКА!)'}`);
|
||||||
|
} else {
|
||||||
|
normal++;
|
||||||
|
console.log(`${name}: exit=${b.exit} entry=${b.entry}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nИТОГ: возвратов=${ties}, обычных исходов=${normal}`);
|
||||||
|
console.log(ties > 0 ? 'ВЕРДИКТ: возврат при ничьей РАБОТАЕТ на новой ставке' : 'ВЕРДИКТ: ничью поймать не удалось (вероятность ~33.6% за попытку)');
|
||||||
|
})();
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// Разбор Bet-аккаунтов программы напрямую (Anchor в релейере нет).
|
||||||
|
const { Connection, PublicKey } = require('@solana/web3.js');
|
||||||
|
const CV = new PublicKey('9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf');
|
||||||
|
const MINT = new PublicKey('6fxySAjTQTkzyyZ7u8EQqv3SyJtv2Ydr6hphquHbkGS9');
|
||||||
|
const conn = new Connection('http://192.168.76.181:8899', 'confirmed');
|
||||||
|
|
||||||
|
const [vaultAuth] = PublicKey.findProgramAddressSync(
|
||||||
|
[Buffer.from('global'), Buffer.from('vault')], CV);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const accs = await conn.getProgramAccounts(CV);
|
||||||
|
const bets = [];
|
||||||
|
for (const { pubkey, account } of accs) {
|
||||||
|
const d = account.data;
|
||||||
|
if (d.length !== 96) continue;
|
||||||
|
// Раскладка Bet (state.rs): 8 disc | amount(0) bet_time(8) expire_time(16)
|
||||||
|
// entry_price(24) exit_price(32) side(40) status(48) bettor(56..88)
|
||||||
|
const u64 = (o) => Number(d.readBigUInt64LE(8 + o));
|
||||||
|
const i64 = (o) => Number(d.readBigInt64LE(8 + o));
|
||||||
|
bets.push({
|
||||||
|
pda: pubkey.toBase58().slice(0, 8),
|
||||||
|
bettor: new PublicKey(d.subarray(8 + 56, 8 + 88)).toBase58().slice(0, 8),
|
||||||
|
side: u64(40) === 0 ? 'UP' : 'DOWN',
|
||||||
|
amount: u64(0),
|
||||||
|
betTime: i64(8),
|
||||||
|
expireTime: i64(16),
|
||||||
|
entryPrice: u64(24),
|
||||||
|
exitPrice: u64(32),
|
||||||
|
status: ['open', 'payout_done', 'house_won'][u64(48)] ?? 'status=' + u64(48),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log('Bet-аккаунтов:', bets.length);
|
||||||
|
for (const b of bets) console.log(` ${b.pda}… ${b.side.padEnd(4)} amt=${b.amount} entry=${b.entryPrice} exit=${b.exitPrice} status=${b.status} expire=${new Date(b.expireTime * 1000).toISOString().slice(11, 19)}`);
|
||||||
|
const open = bets.filter((b) => b.status === 'open');
|
||||||
|
console.log('--- в статусе open:', open.length, '| заперто:', open.reduce((s, b) => s + b.amount, 0));
|
||||||
|
|
||||||
|
console.log('--- vault_authority PDA:', vaultAuth.toBase58());
|
||||||
|
const v = await conn.getTokenAccountsByOwner(vaultAuth, { mint: MINT });
|
||||||
|
for (const { pubkey, account } of v.value) {
|
||||||
|
console.log(' ваулт', pubkey.toBase58().slice(0, 10) + '…', 'держит:', account.data.readBigUInt64LE(64).toString());
|
||||||
|
}
|
||||||
|
})().catch((e) => console.log('ERR', e.message));
|
||||||
Reference in New Issue
Block a user