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

281 lines
12 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: 1800px; }
.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, select { padding: 9px 10px; min-width: 190px; 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; }
code { background: var(--code); padding: 2px 4px; border-radius: 4px; }
</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>
</div>
<div id="walletInfo" class="muted"></div>
</div>
<div class="panel">
<h3>Лимиты менеджера</h3>
<div id="limitsInfo" class="muted">Загрузка...</div>
</div>
<div class="panel">
<h3>Создать билет менеджером</h3>
<div class="row">
<label>Очередь:
<select id="queueId">
<option value="1">Очередь 1</option>
<option value="2">Очередь 2</option>
</select>
</label>
<label>Получатель: <input id="recipient" placeholder="Base58 адрес" /></label>
<label>Сумма выплаты (USD): <input id="payoutUsd" value="50" /></label>
</div>
<div class="row">
<button id="createBtn">Создать билет</button>
</div>
<div id="createResult" 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("m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR");
const RPC_URL = "https://api.devnet.solana.com";
const connection = new solanaWeb3.Connection(RPC_URL, "confirmed");
const SEEDS = {
managerAllowance: "shine_p_v3_manager_allow",
queues: "shine_payments_v3_queues",
ticketQ1: "shine_payments_v3_q1_ticket",
ticketQ2: "shine_payments_v3_q2_ticket",
};
let walletPubkey = null;
let queuesCache = 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 centsToUsdStr(c) {
return trimZeros((Number(c) / 100).toFixed(2));
}
function usdToCents(usdStr) {
const v = Number(usdStr.replace(",", "."));
if (!Number.isFinite(v) || v <= 0) throw new Error("Некорректная сумма USD");
return BigInt(Math.round(v * 100));
}
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 isManagerErrors(msg) {
const s = String(msg || "").toLowerCase();
return s.includes("managerlimitexceeded") || s.includes("invalidmanagerwallet");
}
function parseManagerAllowance(data) {
let o = 0;
const version = data[o++];
const manager = new solanaWeb3.PublicKey(data.slice(o, o + 32)); o += 32;
const q1 = readU64(data, o); o += 8;
const q2 = readU64(data, o); o += 8;
return { version, manager, q1, q2 };
}
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 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 refresh();
}
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 deriveManagerAllowancePda(managerWallet) {
const [pda] = solanaWeb3.PublicKey.findProgramAddressSync(
[utf8(SEEDS.managerAllowance), managerWallet.toBytes()],
PROGRAM_ID
);
return pda;
}
function deriveQueuesPda() {
const [pda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.queues)], PROGRAM_ID);
return pda;
}
function deriveTicketPda(queueId, index) {
const seed = queueId === 1 ? SEEDS.ticketQ1 : SEEDS.ticketQ2;
const [pda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(seed), u64ToBytes(index)], PROGRAM_ID);
return pda;
}
async function loadCore() {
if (!walletPubkey) throw new Error("Сначала подключите кошелек менеджера.");
const allowancePda = deriveManagerAllowancePda(walletPubkey);
const queuesPda = deriveQueuesPda();
const [allowanceAi, queuesAi] = await Promise.all([
connection.getAccountInfo(allowancePda, "confirmed"),
connection.getAccountInfo(queuesPda, "confirmed"),
]);
if (!queuesAi) throw new Error("Queues PDA не найден. Сначала выполните init.");
queuesCache = parseQueues(queuesAi.data);
return {
allowancePda,
allowance: allowanceAi ? parseManagerAllowance(allowanceAi.data) : null,
queuesPda,
queues: queuesCache,
};
}
async function refresh() {
const el = document.getElementById("limitsInfo");
try {
const core = await loadCore();
if (!core.allowance) {
el.innerHTML = `<span class="warn">Для этого кошелька лимиты менеджера пока не выданы.</span>`;
return;
}
el.innerHTML = `
<div>Manager: <code>${core.allowance.manager.toBase58()}</code></div>
<div>PDA лимитов: <code>${core.allowancePda.toBase58()}</code></div>
<div>Доступно Q1: <b>${centsToUsdStr(core.allowance.q1)} USD</b></div>
<div>Доступно Q2: <b>${centsToUsdStr(core.allowance.q2)} USD</b></div>
`;
} catch (e) {
el.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
}
}
async function createManagerTicket() {
const out = document.getElementById("createResult");
out.textContent = "";
try {
const provider = getProvider();
if (!walletPubkey) await connectWallet();
else if (!provider.isConnected) await provider.connect();
const core = await loadCore();
if (!core.allowance) throw new Error("Для этого кошелька лимиты менеджера не выданы.");
const queueId = Number(document.getElementById("queueId").value);
if (queueId !== 1 && queueId !== 2) throw new Error("Очередь должна быть 1 или 2");
const recipient = new solanaWeb3.PublicKey(document.getElementById("recipient").value.trim());
const payout = usdToCents(document.getElementById("payoutUsd").value.trim());
const nextIndex = queueId === 1 ? (core.queues.q1Total + 1n) : (core.queues.q2Total + 1n);
const ticketPda = deriveTicketPda(queueId, nextIndex);
const disc = await ixDiscriminator("manager_add_ticket");
const data = concat(disc, new Uint8Array([queueId]), recipient.toBytes(), u64ToBytes(payout));
const keys = [
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
{ pubkey: core.allowancePda, isSigner: false, isWritable: true },
{ pubkey: core.queuesPda, isSigner: false, isWritable: true },
{ pubkey: ticketPda, isSigner: false, isWritable: true },
{ pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false },
];
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 refresh();
} catch (e) {
const raw = String(e.message || e);
if (isManagerErrors(raw)) {
out.innerHTML = `<span class="warn">Операция отклонена: лимит менеджера недостаточен или кошелек не имеет прав менеджера.</span>`;
return;
}
out.innerHTML = `<span class="err">${raw}</span>`;
}
}
document.getElementById("connectBtn").addEventListener("click", connectWallet);
document.getElementById("refreshBtn").addEventListener("click", refresh);
document.getElementById("createBtn").addEventListener("click", createManagerTicket);
refresh();
</script>
</body>
</html>