SHiNE-server/shine-solana/shine/programs/shine_payments/web/admin_tools.html

577 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Тех. инструменты — Shine Payments Devnet</title>
<style>
:root {
color-scheme: dark;
--bg: #0f1218;
--panel: #171b24;
--text: #e8edf6;
--muted: #97a3b8;
--line: #2a3242;
--ok: #55d48a;
--warn: #ffbf5e;
--err: #ff7d7d;
--btn: #273247;
--btn-hover: #32415c;
--code: #1e2633;
}
* { box-sizing: border-box; }
body { font-family: Arial, sans-serif; margin: 20px; background: var(--bg); color: var(--text); }
.topbar { margin-bottom: 12px; }
.back { color: var(--muted); text-decoration: none; font-size: 18px; }
.wrap { width: 100%; max-width: 1850px; }
.panel { border: 1px solid var(--line); border-radius: 8px; padding: 14px; margin-bottom: 14px; background: var(--panel); width: 100%; }
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; }
input { padding: 9px 10px; min-width: 170px; border: 1px solid var(--line); border-radius: 8px; background: #131824; color: var(--text); }
button { padding: 9px 14px; border: 1px solid var(--line); border-radius: 8px; cursor: pointer; background: var(--btn); color: var(--text); }
button:hover { background: var(--btn-hover); }
.muted { color: var(--muted); }
.ok { color: var(--ok); }
.warn { color: var(--warn); }
.err { color: var(--err); white-space: pre-wrap; }
.paid { color: var(--ok); font-weight: 700; }
.formula { font-family: monospace; color: #c9d7f0; }
code { background: var(--code); padding: 2px 4px; border-radius: 4px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid var(--line); padding: 6px; text-align: left; font-size: 14px; vertical-align: top; }
</style>
</head>
<body>
<div class="wrap">
<div class="topbar"><a class="back" href="./index.html">На главную</a></div>
<h1>Техническая страница (Devnet)</h1>
<div class="muted">Программа: <code id="programId"></code></div>
<div class="panel">
<div class="row">
<button id="connectBtn">Подключить кошелек</button>
<button id="refreshBtn">Обновить всё</button>
<button id="initBtn">Init (один раз)</button>
</div>
<div id="walletInfo" class="muted"></div>
<div id="initResult" class="muted"></div>
</div>
<div class="panel">
<h3>Коэффициент, лимит и награда шага выплат</h3>
<div class="muted">Право изменения: <code id="daoAllowed">загрузка...</code></div>
<div class="row">
<label>Коэффициент (например 5): <input id="coefInput" value="5" /></label>
<label>Лимит (USD): <input id="limitInput" value="10000" /></label>
<label>Награда шага (SOL, max 0.01): <input id="rewardInput" value="0.008" /></label>
<button id="updateCoefBtn">Обновить</button>
</div>
<div class="formula">Лимит покупки Q1 = max(limit_usd_cents - q1_sum_total_usd_cents, 0)</div>
<div class="formula">Шаг выплаты Q1 = ticket + dao(1x) + reward; Q2 = ticket + dao(2x) + reward; Q3 = ticket + dao(3x) + reward</div>
<div id="updateResult" class="muted"></div>
</div>
<div class="panel">
<h3>Shine Users: экономические параметры</h3>
<div class="muted">Право изменения: <code id="usersDaoAllowed">загрузка...</code></div>
<div id="usersEconomyState" class="muted">Загрузка...</div>
<div class="row">
<label>Комиссия регистрации (SOL): <input id="usersRegFeeInput" value="0.01" /></label>
<label>Цена шага лимита (SOL): <input id="usersLimitStepFeeInput" value="0.0001" /></label>
<label>Стартовый бонус лимита: <input id="usersBonusInput" value="100000" /></label>
<button id="usersUpdateBtn">Обновить</button>
<button id="usersInitBtn">Init Users Economy</button>
</div>
<div id="usersUpdateResult" class="muted"></div>
</div>
<div class="panel">
<h3>Адреса и агрегаты</h3>
<div id="balances" class="muted">Загрузка...</div>
</div>
<div class="panel">
<h3>Очередь 1 (все билеты)</h3>
<div class="row"><button id="loadQ1Btn">Показать очередь 1</button></div>
<div id="queue1Table" class="muted"></div>
</div>
<div class="panel">
<h3>Очередь 2 (все билеты)</h3>
<div class="row"><button id="loadQ2Btn">Показать очередь 2</button></div>
<div id="queue2Table" class="muted"></div>
</div>
<div class="panel">
<h3>Очередь 3 (все билеты)</h3>
<div class="row"><button id="loadQ3Btn">Показать очередь 3</button></div>
<div id="queue3Table" class="muted"></div>
</div>
</div>
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
<script>
const PROGRAM_ID = new solanaWeb3.PublicKey("c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW");
const USERS_PROGRAM_ID = new solanaWeb3.PublicKey("3bYrnXwLc56oVPUBAjY8zTMLwHCYq29b5rUMu3b64SQJ");
const RPC_URL = "https://api.devnet.solana.com";
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
const SEEDS = {
config: "shine_payments_config",
coef: "shine_payments_coef_limit",
queues: "shine_payments_queues",
inflow: "shine_payments_inflow_vault",
ticketQ1: "shine_payments_q1_ticket",
ticketQ2: "shine_payments_q2_ticket",
ticketQ3: "shine_payments_q3_ticket",
};
const IX = { init: 1, updateCoefLimit: 2 };
const USERS_IX = { initUsersEconomyConfig: 1, updateUsersEconomyConfig: 2 };
const USERS_SEEDS = {
economyConfig: "shine_users_v1_economy_config",
};
const MAX_REWARD_LAMPORTS = 10_000_000n;
let walletPubkey = null;
let cache = null;
document.getElementById("programId").textContent = PROGRAM_ID.toBase58();
function utf8(s) { return new TextEncoder().encode(s); }
function u64ToBytes(v) {
let x = BigInt(v);
const out = new Uint8Array(8);
for (let i = 0; i < 8; i++) { out[i] = Number(x & 255n); x >>= 8n; }
return out;
}
function readU64(data, offset) {
let x = 0n;
for (let i = 0; i < 8; i++) x |= BigInt(data[offset + i]) << (8n * BigInt(i));
return x;
}
function concat(...parts) {
const len = parts.reduce((n, p) => n + p.length, 0);
const out = new Uint8Array(len);
let o = 0;
for (const p of parts) { out.set(p, o); o += p.length; }
return out;
}
function trimZeros(v) {
return v.replace(/(\.\d*?[1-9])0+$/u, "$1").replace(/\.0+$/u, "").replace(/\.$/u, "");
}
function lamportsToSolStr(l) {
return trimZeros((Number(l) / 1_000_000_000).toFixed(9));
}
function centsToUsdStr(c) {
return trimZeros((Number(c) / 100).toFixed(2));
}
function solToLamports(solStr) {
const v = Number(solStr.replace(",", "."));
if (!Number.isFinite(v) || v <= 0) throw new Error("Некорректная сумма SOL");
return BigInt(Math.round(v * 1_000_000_000));
}
function usdToCents(usdStr) {
const v = Number(usdStr.replace(",", "."));
if (!Number.isFinite(v) || v <= 0) throw new Error("Некорректная сумма USD");
return BigInt(Math.round(v * 100));
}
function ixData(tag, ...parts) {
return concat(new Uint8Array([tag]), ...parts);
}
function isUnauthorizedDao(msg) {
const s = String(msg || "").toLowerCase();
return s.includes("unauthorizeddao") || s.includes("0x1775");
}
function isUsersDaoUnauthorized(msg) {
const s = String(msg || "").toLowerCase();
return s.includes("invalidsigner") || s.includes("0x3ed");
}
function parseConfig(data) {
let o = 0;
const version = data[o++];
const dao = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
const inflow = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
return { version, dao, inflow };
}
function parseCoef(data) {
let o = 0;
const version = data[o++];
const coefPpm = readU64(data, o); o += 8;
const limitUsdCents = readU64(data, o); o += 8;
const reward = readU64(data, o); o += 8;
return { version, coefPpm, limitUsdCents, reward };
}
function parseQueues(data) {
let o = 0;
const version = data[o++];
const q1Total = readU64(data, o); o += 8;
const q1Paid = readU64(data, o); o += 8;
const q1SumTotal = readU64(data, o); o += 8;
const q1SumPaid = readU64(data, o); o += 8;
const q2Total = readU64(data, o); o += 8;
const q2Paid = readU64(data, o); o += 8;
const q2SumTotal = readU64(data, o); o += 8;
const q2SumPaid = readU64(data, o); o += 8;
const q3Total = readU64(data, o); o += 8;
const q3Paid = readU64(data, o); o += 8;
const q3SumTotal = readU64(data, o); o += 8;
const q3SumPaid = readU64(data, o); o += 8;
return { version, q1Total, q1Paid, q1SumTotal, q1SumPaid, q2Total, q2Paid, q2SumTotal, q2SumPaid, q3Total, q3Paid, q3SumTotal, q3SumPaid };
}
function parseTicket(data) {
let o = 0;
const version = data[o++];
const queueId = data[o++];
const index = readU64(data, o); o += 8;
const isPaid = data[o++] === 1;
const recipient = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
const payout = readU64(data, o); o += 8;
const debtBefore = readU64(data, o); o += 8;
return { version, queueId, index, isPaid, recipient, payout, debtBefore };
}
function parseUsersEconomyConfig(data) {
let o = 0;
const version = data[o++];
const registrationFeeLamports = readU64(data, o); o += 8;
const lamportsPerLimitStep = readU64(data, o); o += 8;
const startBonusLimit = readU64(data, o); o += 8;
return { version, registrationFeeLamports, lamportsPerLimitStep, startBonusLimit };
}
function getProvider() {
if (!window.solana || !window.solana.isPhantom) throw new Error("Phantom не найден");
return window.solana;
}
async function connectWallet() {
const out = document.getElementById("walletInfo");
try {
const provider = getProvider();
const r = await provider.connect();
walletPubkey = new solanaWeb3.PublicKey(r.publicKey.toString());
out.textContent = "Кошелек: " + walletPubkey.toBase58();
await refreshAll();
} catch (e) {
out.innerHTML = `<span class="err">${String(e?.message || e)}</span>`;
throw e;
}
}
async function sendInstruction(ix) {
const provider = getProvider();
if (!walletPubkey) await connectWallet();
const tx = new solanaWeb3.Transaction().add(ix);
tx.feePayer = walletPubkey;
const bh = await connection.getLatestBlockhash("confirmed");
tx.recentBlockhash = bh.blockhash;
const signed = await provider.signTransaction(tx);
const sig = await connection.sendRawTransaction(signed.serialize(), { skipPreflight: false });
await connection.confirmTransaction({ signature: sig, blockhash: bh.blockhash, lastValidBlockHeight: bh.lastValidBlockHeight }, "confirmed");
return sig;
}
function derivePdas() {
const [configPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.config)], PROGRAM_ID);
const [coefPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.coef)], PROGRAM_ID);
const [queuesPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.queues)], PROGRAM_ID);
const [inflowPda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.inflow)], PROGRAM_ID);
return { configPda, coefPda, queuesPda, inflowPda };
}
function deriveUsersPdas() {
const [usersEconomyConfigPda] = solanaWeb3.PublicKey.findProgramAddressSync(
[utf8(USERS_SEEDS.economyConfig)],
USERS_PROGRAM_ID
);
return { usersEconomyConfigPda };
}
function ticketPda(queueId, index) {
const seed = queueId === 1 ? SEEDS.ticketQ1 : (queueId === 2 ? SEEDS.ticketQ2 : SEEDS.ticketQ3);
const [pda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(seed), u64ToBytes(index)], PROGRAM_ID);
return pda;
}
async function loadCore() {
const pdas = derivePdas();
const [cfgAi, coefAi, qAi, inflowAi] = await Promise.all([
connection.getAccountInfo(pdas.configPda, "confirmed"),
connection.getAccountInfo(pdas.coefPda, "confirmed"),
connection.getAccountInfo(pdas.queuesPda, "confirmed"),
connection.getAccountInfo(pdas.inflowPda, "confirmed"),
]);
if (!cfgAi || !coefAi || !qAi || !inflowAi) {
cache = { pdas, notInited: true };
return cache;
}
const config = parseConfig(cfgAi.data);
const coef = parseCoef(coefAi.data);
const queues = parseQueues(qAi.data);
const [daoBal, inflowRent] = await Promise.all([
connection.getBalance(config.dao, "confirmed"),
connection.getMinimumBalanceForRentExemption(inflowAi.data.length, "confirmed"),
]);
cache = {
pdas, config, coef, queues,
inflowLamports: BigInt(inflowAi.lamports),
inflowAvailable: BigInt(Math.max(0, inflowAi.lamports - inflowRent)),
daoBalance: BigInt(daoBal),
};
return cache;
}
async function refreshBalances() {
const el = document.getElementById("balances");
try {
const core = await loadCore();
if (core.notInited) {
el.innerHTML = `<span class="warn">PDA ещё не инициализированы.</span>`;
return;
}
const coefText = trimZeros((Number(core.coef.coefPpm) / 1_000_000).toFixed(6));
const limitRemain = core.coef.limitUsdCents > core.queues.q1SumTotal ? (core.coef.limitUsdCents - core.queues.q1SumTotal) : 0n;
document.getElementById("daoAllowed").textContent = core.config.dao.toBase58();
el.innerHTML = `
<div>DAO: <code>${core.config.dao.toBase58()}</code></div>
<div class="muted">Сейчас это тестовый DAO-кошелек. В production здесь будет адрес реального DAO.</div>
<div>Inflow vault: <code>${core.config.inflow.toBase58()}</code></div>
<div class="muted">Inflow vault — входящий PDA-кошелек выплат программы.</div>
<div>Награда за шаг: <b>${lamportsToSolStr(core.coef.reward)} SOL</b></div>
<div>Коэффициент: <b>${coefText}</b>, лимит: <b>${centsToUsdStr(core.coef.limitUsdCents)} USD</b></div>
<div>Осталось лимита для покупки Q1: <b>${centsToUsdStr(limitRemain)} USD</b></div>
<div>Баланс DAO: <b>${lamportsToSolStr(core.daoBalance)} SOL</b></div>
<div>Баланс inflow vault: <b>${lamportsToSolStr(core.inflowLamports)} SOL</b></div>
<div>Доступно в inflow (сверх ренты): <b>${lamportsToSolStr(core.inflowAvailable)} SOL</b></div>
<div>Q1: total=${core.queues.q1Total}, paid=${core.queues.q1Paid}, sum_total=${centsToUsdStr(core.queues.q1SumTotal)} USD, sum_paid=${centsToUsdStr(core.queues.q1SumPaid)} USD</div>
<div>Q2: total=${core.queues.q2Total}, paid=${core.queues.q2Paid}, sum_total=${centsToUsdStr(core.queues.q2SumTotal)} USD, sum_paid=${centsToUsdStr(core.queues.q2SumPaid)} USD</div>
<div>Q3: total=${core.queues.q3Total}, paid=${core.queues.q3Paid}, sum_total=${centsToUsdStr(core.queues.q3SumTotal)} USD, sum_paid=${centsToUsdStr(core.queues.q3SumPaid)} USD</div>
`;
} catch (e) {
el.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
document.getElementById("daoAllowed").textContent = "не определен";
}
}
async function refreshUsersEconomy() {
const out = document.getElementById("usersEconomyState");
try {
const usersPdas = deriveUsersPdas();
const ai = await connection.getAccountInfo(usersPdas.usersEconomyConfigPda, "confirmed");
document.getElementById("usersDaoAllowed").textContent = "FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P";
if (!ai) {
out.innerHTML = `<span class="warn">PDA Users Economy еще не инициализирован.</span><div>PDA: <code>${usersPdas.usersEconomyConfigPda.toBase58()}</code></div>`;
return;
}
const c = parseUsersEconomyConfig(ai.data);
document.getElementById("usersRegFeeInput").value = lamportsToSolStr(c.registrationFeeLamports);
document.getElementById("usersLimitStepFeeInput").value = lamportsToSolStr(c.lamportsPerLimitStep);
document.getElementById("usersBonusInput").value = c.startBonusLimit.toString();
out.innerHTML = `
<div>Users program: <code>${USERS_PROGRAM_ID.toBase58()}</code></div>
<div>Economy config PDA: <code>${usersPdas.usersEconomyConfigPda.toBase58()}</code></div>
<div>registration_fee_lamports: <b>${c.registrationFeeLamports.toString()}</b> (~${lamportsToSolStr(c.registrationFeeLamports)} SOL)</div>
<div>lamports_per_limit_step: <b>${c.lamportsPerLimitStep.toString()}</b> (~${lamportsToSolStr(c.lamportsPerLimitStep)} SOL)</div>
<div>start_bonus_limit: <b>${c.startBonusLimit.toString()}</b></div>
`;
} catch (e) {
out.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
}
}
async function runInit() {
const out = document.getElementById("initResult");
out.textContent = "";
try {
const provider = getProvider();
if (!walletPubkey) await connectWallet();
else if (!provider.isConnected) await provider.connect();
const pdas = derivePdas();
const keys = [
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
{ pubkey: pdas.configPda, isSigner: false, isWritable: true },
{ pubkey: pdas.coefPda, isSigner: false, isWritable: true },
{ pubkey: pdas.queuesPda, isSigner: false, isWritable: true },
{ pubkey: pdas.inflowPda, isSigner: false, isWritable: true },
{ pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false },
];
const ix = new solanaWeb3.TransactionInstruction({ programId: PROGRAM_ID, keys, data: ixData(IX.init) });
const sig = await sendInstruction(ix);
out.innerHTML = `<span class="ok">Init выполнен. Tx: <code>${sig}</code></span>`;
await refreshAll();
} catch (e) {
out.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
}
}
async function updateCoefLimit() {
const out = document.getElementById("updateResult");
out.textContent = "";
try {
const provider = getProvider();
if (!walletPubkey) await connectWallet();
else if (!provider.isConnected) await provider.connect();
const core = await loadCore();
if (core.notInited) throw new Error("Сначала выполните init");
const coef = Number(document.getElementById("coefInput").value.trim());
if (!Number.isFinite(coef) || coef <= 0) throw new Error("Некорректный коэффициент");
const coefPpm = BigInt(Math.round(coef * 1_000_000));
const limitUsdCents = usdToCents(document.getElementById("limitInput").value.trim());
const rewardLamports = solToLamports(document.getElementById("rewardInput").value.trim());
if (rewardLamports > MAX_REWARD_LAMPORTS) throw new Error("Награда не должна быть больше 0.01 SOL");
const data = ixData(IX.updateCoefLimit, u64ToBytes(coefPpm), u64ToBytes(limitUsdCents), u64ToBytes(rewardLamports));
const keys = [
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
{ pubkey: core.pdas.configPda, isSigner: false, isWritable: true },
{ pubkey: core.pdas.coefPda, isSigner: false, isWritable: true },
];
const ix = new solanaWeb3.TransactionInstruction({ programId: PROGRAM_ID, keys, data });
const sig = await sendInstruction(ix);
out.innerHTML = `<span class="ok">Обновлено. Tx: <code>${sig}</code></span>`;
await refreshAll();
} catch (e) {
const raw = String(e.message || e);
if (isUnauthorizedDao(raw)) {
const dao = document.getElementById("daoAllowed").textContent;
out.innerHTML = `<span class="warn">Вы подключены не под тем аккаунтом. Изменение доступно только DAO-кошельку: <code>${dao}</code>.</span>`;
return;
}
out.innerHTML = `<span class="err">${raw}</span>`;
}
}
async function initUsersEconomy() {
const out = document.getElementById("usersUpdateResult");
out.textContent = "";
try {
const provider = getProvider();
if (!walletPubkey) await connectWallet();
else if (!provider.isConnected) await provider.connect();
const usersPdas = deriveUsersPdas();
const keys = [
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
{ pubkey: usersPdas.usersEconomyConfigPda, isSigner: false, isWritable: true },
{ pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false },
];
const ix = new solanaWeb3.TransactionInstruction({ programId: USERS_PROGRAM_ID, keys, data: ixData(USERS_IX.initUsersEconomyConfig) });
const sig = await sendInstruction(ix);
out.innerHTML = `<span class="ok">Users Economy init выполнен. Tx: <code>${sig}</code></span>`;
await refreshUsersEconomy();
} catch (e) {
out.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
}
}
async function updateUsersEconomy() {
const out = document.getElementById("usersUpdateResult");
out.textContent = "";
try {
const provider = getProvider();
if (!walletPubkey) await connectWallet();
else if (!provider.isConnected) await provider.connect();
const usersPdas = deriveUsersPdas();
const registrationFeeLamports = solToLamports(document.getElementById("usersRegFeeInput").value.trim());
const lamportsPerLimitStep = solToLamports(document.getElementById("usersLimitStepFeeInput").value.trim());
const startBonusLimit = BigInt(document.getElementById("usersBonusInput").value.trim());
if (startBonusLimit < 0n) throw new Error("Стартовый бонус не может быть отрицательным");
const data = ixData(
USERS_IX.updateUsersEconomyConfig,
u64ToBytes(registrationFeeLamports),
u64ToBytes(lamportsPerLimitStep),
u64ToBytes(startBonusLimit)
);
const keys = [
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
{ pubkey: usersPdas.usersEconomyConfigPda, isSigner: false, isWritable: true },
];
const ix = new solanaWeb3.TransactionInstruction({ programId: USERS_PROGRAM_ID, keys, data });
const sig = await sendInstruction(ix);
out.innerHTML = `<span class="ok">Users Economy обновлен. Tx: <code>${sig}</code></span>`;
await refreshUsersEconomy();
} catch (e) {
const raw = String(e.message || e);
if (isUsersDaoUnauthorized(raw)) {
const dao = document.getElementById("usersDaoAllowed").textContent;
out.innerHTML = `<span class="warn">Изменение доступно только DAO-кошельку: <code>${dao}</code>.</span>`;
return;
}
out.innerHTML = `<span class="err">${raw}</span>`;
}
}
function currentDebtBeforeTicket(ticket, queues) {
if (ticket.isPaid) return 0n;
const paidSum = ticket.queueId === 1 ? queues.q1SumPaid : (ticket.queueId === 2 ? queues.q2SumPaid : queues.q3SumPaid);
return ticket.debtBefore > paidSum ? (ticket.debtBefore - paidSum) : 0n;
}
async function showQueue(queueId) {
const out = document.getElementById(queueId === 1 ? "queue1Table" : (queueId === 2 ? "queue2Table" : "queue3Table"));
out.textContent = "Загрузка...";
try {
const core = await loadCore();
if (core.notInited) throw new Error("Сначала выполните init");
const total = queueId === 1 ? core.queues.q1Total : (queueId === 2 ? core.queues.q2Total : core.queues.q3Total);
if (total === 0n) {
out.innerHTML = `<span class="muted">Очередь ${queueId} пока пустая.</span>`;
return;
}
const rows = [];
for (let i = 1n; i <= total; i++) {
const pda = ticketPda(queueId, i);
const ai = await connection.getAccountInfo(pda, "confirmed");
if (!ai) {
rows.push(`<tr><td>${i.toString()}</td><td>${queueId}</td><td colspan="6" class="err">PDA не найден</td></tr>`);
continue;
}
const t = parseTicket(ai.data);
rows.push(`
<tr>
<td>${t.index.toString()}</td>
<td>${t.queueId}</td>
<td>${t.isPaid ? '<span class="paid">выплачен</span>' : "ожидание"}</td>
<td><code>${t.recipient.toBase58()}</code></td>
<td>${centsToUsdStr(t.payout)} USD</td>
<td>${centsToUsdStr(t.debtBefore)} USD</td>
<td>${centsToUsdStr(currentDebtBeforeTicket(t, core.queues))} USD</td>
<td><code>${pda.toBase58()}</code></td>
</tr>
`);
}
out.innerHTML = `
<table>
<thead>
<tr>
<th>#</th>
<th>Очередь</th>
<th>Статус</th>
<th>Получатель</th>
<th>Сумма выплаты (USD)</th>
<th>Очередь до него (от старта)</th>
<th>Очередь до него (актуально)</th>
<th>PDA</th>
</tr>
</thead>
<tbody>${rows.join("")}</tbody>
</table>
`;
} catch (e) {
out.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
}
}
async function refreshAll() {
await refreshBalances();
await refreshUsersEconomy();
}
document.getElementById("connectBtn").addEventListener("click", connectWallet);
document.getElementById("refreshBtn").addEventListener("click", refreshAll);
document.getElementById("initBtn").addEventListener("click", runInit);
document.getElementById("updateCoefBtn").addEventListener("click", updateCoefLimit);
document.getElementById("usersInitBtn").addEventListener("click", initUsersEconomy);
document.getElementById("usersUpdateBtn").addEventListener("click", updateUsersEconomy);
document.getElementById("loadQ1Btn").addEventListener("click", () => showQueue(1));
document.getElementById("loadQ2Btn").addEventListener("click", () => showQueue(2));
document.getElementById("loadQ3Btn").addEventListener("click", () => showQueue(3));
refreshAll();
</script>
</body>
</html>