259 lines
11 KiB
HTML
259 lines
11 KiB
HTML
<!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, select { padding: 8px; min-width: 200px; }
|
|
button { padding: 8px 12px; cursor: pointer; }
|
|
.muted { color: #666; }
|
|
.ok { color: #0a7a3c; }
|
|
.warn { color: #9f5f00; }
|
|
.err { color: #b30000; white-space: pre-wrap; }
|
|
code { background: #f6f6f6; padding: 2px 4px; border-radius: 4px; }
|
|
</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>
|
|
</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>Сумма выплаты (SOL): <input id="payoutSol" value="0.5" /></label>
|
|
</div>
|
|
<div class="row">
|
|
<button id="createBtn">Создать билет</button>
|
|
</div>
|
|
<div id="createResult" 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 = {
|
|
managerAllowance: "shine_payments_v2_manager_allowance",
|
|
queues: "shine_payments_v2_queues",
|
|
ticketQ1: "shine_payments_v2_q1_ticket",
|
|
ticketQ2: "shine_payments_v2_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 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 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>${lamportsToSolStr(core.allowance.q1)} SOL</b></div>
|
|
<div>Доступно Q2: <b>${lamportsToSolStr(core.allowance.q2)} SOL</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 = solToLamports(document.getElementById("payoutSol").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>
|