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

386 lines
16 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>
body { font-family: Arial, sans-serif; margin: 24px; max-width: 1400px; }
.panel { border: 1px solid #ddd; border-radius: 8px; padding: 14px; margin-bottom: 14px; }
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin: 8px 0; }
input { padding: 8px; min-width: 180px; }
button { padding: 8px 12px; cursor: pointer; }
.muted { color: #666; }
.ok { color: #0a7a3c; }
.warn { color: #9f5f00; }
.err { color: #b30000; white-space: pre-wrap; }
.paid { color: #0a7a3c; font-weight: 700; }
code { background: #f6f6f6; padding: 2px 4px; border-radius: 4px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 6px; text-align: left; font-size: 14px; }
</style>
</head>
<body>
<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="managerAllowed">загрузка...</code></div>
<div class="row">
<label>Коэффициент (например 5): <input id="coefInput" value="5" /></label>
<label>Лимит (SOL): <input id="limitInput" value="100" /></label>
<button id="updateCoefBtn">Обновить</button>
</div>
<div id="updateResult" 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="loadQueueBtn">Показать всю очередь</button>
</div>
<div id="queueTable" class="muted"></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("4QDCcaURt7phJGcvDS4VQQtrqDmUSMXvkWnMnMKCiUyE");
const RPC_URL = "https://api.devnet.solana.com";
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
const SEEDS = {
config: "shine_payments_v2_config",
coef: "shine_payments_v2_coef_limit",
queues: "shine_payments_v2_queues",
inflow: "shine_payments_v2_inflow_vault",
ticketQ1: "shine_payments_v2_q1_ticket",
};
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 solToLamports(solStr) {
const v = Number(solStr);
if (!Number.isFinite(v) || v <= 0) throw new Error("Некорректная сумма SOL");
return BigInt(Math.round(v * 1_000_000_000));
}
async function ixDiscriminator(name) {
const msg = utf8("global:" + name);
const hash = await crypto.subtle.digest("SHA-256", msg);
return new Uint8Array(hash).slice(0, 8);
}
function isUnauthorizedManager(msg) {
const s = String(msg || "").toLowerCase();
return s.includes("unauthorizedmanager") || s.includes("0x1774");
}
function parseConfig(data) {
let o = 0;
const version = data[o++];
const dao = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
const manager = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
const inflow = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
const reward = readU64(data, o); o += 8;
return { version, dao, manager, inflow, reward };
}
function parseCoef(data) {
let o = 0;
const version = data[o++];
const coefPpm = readU64(data, o); o += 8;
const limit = readU64(data, o); o += 8;
return { version, coefPpm, limit };
}
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;
return { version, q1Total, q1Paid, q1SumTotal, q1SumPaid, q2Total, q2Paid, q2SumTotal, q2SumPaid };
}
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 getProvider() {
if (!window.solana || !window.solana.isPhantom) throw new Error("Phantom не найден");
return window.solana;
}
async function connectWallet() {
const provider = getProvider();
const r = await provider.connect();
walletPubkey = new solanaWeb3.PublicKey(r.publicKey.toString());
document.getElementById("walletInfo").textContent = "Кошелек: " + walletPubkey.toBase58();
await refreshAll();
}
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 q1TicketPda(index) {
const [pda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.ticketQ1), 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));
document.getElementById("managerAllowed").textContent = core.config.manager.toBase58();
el.innerHTML = `
<div>DAO: <code>${core.config.dao.toBase58()}</code></div>
<div>Inflow vault: <code>${core.config.inflow.toBase58()}</code></div>
<div class="muted">Inflow vault — это входящий PDA-кошелек выплат программы.</div>
<div>Manager: <code>${core.config.manager.toBase58()}</code></div>
<div>Награда за шаг: <b>${lamportsToSolStr(core.config.reward)} SOL</b></div>
<div>Коэффициент: <b>${coefText}</b>, лимит: <b>${lamportsToSolStr(core.coef.limit)} SOL</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=${lamportsToSolStr(core.queues.q1SumTotal)} SOL, sum_paid=${lamportsToSolStr(core.queues.q1SumPaid)} SOL</div>
<div>Q2: total=${core.queues.q2Total}, paid=${core.queues.q2Paid}, sum_total=${lamportsToSolStr(core.queues.q2SumTotal)} SOL, sum_paid=${lamportsToSolStr(core.queues.q2SumPaid)} SOL</div>
`;
} catch (e) {
el.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
document.getElementById("managerAllowed").textContent = "не определен";
}
}
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 disc = await ixDiscriminator("init");
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: disc });
const sig = await sendInstruction(ix);
out.innerHTML = `<span class="ok">Init выполнен. Tx: <code>${sig}</code></span>`;
await refreshAll();
} catch (e) {
const raw = String(e.message || e);
if (isUnauthorizedManager(raw)) {
const mgr = document.getElementById("managerAllowed").textContent;
out.innerHTML = `<span class="warn">Вы подключены не под тем аккаунтом. Изменение доступно только управляющему кошельку: <code>${mgr}</code>.</span>`;
return;
}
out.innerHTML = `<span class="err">${raw}</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 limitLamports = solToLamports(document.getElementById("limitInput").value.trim());
const disc = await ixDiscriminator("update_coef_limit");
const data = concat(disc, u64ToBytes(coefPpm), u64ToBytes(limitLamports));
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) {
out.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
}
}
async function showQueue() {
const out = document.getElementById("queueTable");
out.textContent = "Загрузка...";
try {
const core = await loadCore();
if (core.notInited) throw new Error("Сначала выполните init");
if (core.queues.q1Total === 0n) {
out.innerHTML = `<span class="muted">Очередь 1 пока пустая.</span>`;
return;
}
const rows = [];
for (let i = 1n; i <= core.queues.q1Total; i++) {
const pda = q1TicketPda(i);
const ai = await connection.getAccountInfo(pda, "confirmed");
if (!ai) {
rows.push(`<tr><td>${i.toString()}</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.isPaid ? '<span class="paid">да</span>' : "нет"}</td>
<td><code>${pda.toBase58()}</code></td>
<td><code>${t.recipient.toBase58()}</code></td>
<td>${lamportsToSolStr(t.payout)} SOL</td>
<td>${lamportsToSolStr(t.debtBefore)} SOL</td>
<td>${t.queueId}</td>
</tr>
`);
}
out.innerHTML = `
<table>
<thead>
<tr>
<th>#</th><th>Выплачен</th><th>PDA</th><th>Получатель</th><th>Сумма</th><th>Debt Before</th><th>Очередь</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();
}
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("loadQueueBtn").addEventListener("click", showQueue);
refreshAll();
</script>
</body>
</html>