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

275 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>DAO-права менеджеров — 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 { padding: 9px 10px; min-width: 220px; 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>DAO: права менеджеров (Devnet)</h1>
<div class="muted">Программа: <code id="programId"></code></div>
<div class="panel">
<div class="warn">
Пока реального DAO-голосования нет: роль DAO выполняет тестовый кошелек
<code>FUc28vNixp7F3nnkpGVt6nuJbgvJ4429v4B5wS52Df6P</code>.<br />
Позже это заменяется на вызов из настоящего DAO-казначейства/голосования.
</div>
</div>
<div class="panel">
<div class="row">
<button id="connectBtn">Подключить кошелек</button>
<button id="refreshBtn">Обновить</button>
</div>
<div id="walletInfo" class="muted"></div>
<div id="daoInfo" class="muted"></div>
</div>
<div class="panel">
<h3>Выдать/добавить лимиты менеджеру</h3>
<div class="row">
<label>Кошелек менеджера: <input id="managerWallet" placeholder="Base58" /></label>
<label>Добавить лимит Q1 (USD): <input id="addQ1" value="100" /></label>
<label>Добавить лимит Q2 (USD): <input id="addQ2" value="50" /></label>
</div>
<div class="row">
<button id="grantBtn">Выдать лимиты</button>
</div>
<div id="grantResult" class="muted"></div>
</div>
<div class="panel">
<h3>Текущие лимиты менеджера</h3>
<div class="row">
<button id="loadManagerBtn">Показать лимиты</button>
</div>
<div id="managerState" 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 = {
config: "shine_payments_v3_config",
managerAllowance: "shine_p_v3_manager_allow",
};
let walletPubkey = null;
let configCache = 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 isUnauthorizedDao(msg) {
const s = String(msg || "").toLowerCase();
return s.includes("unauthorizeddao");
}
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 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 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 deriveConfigPda() {
const [pda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.config)], PROGRAM_ID);
return pda;
}
function deriveManagerAllowancePda(managerWallet) {
const [pda] = solanaWeb3.PublicKey.findProgramAddressSync([utf8(SEEDS.managerAllowance), managerWallet.toBytes()], PROGRAM_ID);
return pda;
}
async function loadConfig() {
const configPda = deriveConfigPda();
const ai = await connection.getAccountInfo(configPda, "confirmed");
if (!ai) throw new Error("Config PDA не найден. Сначала выполните init.");
configCache = { configPda, config: parseConfig(ai.data) };
return configCache;
}
async function refresh() {
const el = document.getElementById("daoInfo");
try {
const { config } = await loadConfig();
el.innerHTML = `
<div>DAO-кошелек: <code>${config.dao.toBase58()}</code></div>
<div class="muted">Выдавать лимиты может только этот кошелек.</div>
`;
} catch (e) {
el.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
}
}
async function grantLimits() {
const out = document.getElementById("grantResult");
out.textContent = "";
try {
const provider = getProvider();
if (!walletPubkey) await connectWallet();
else if (!provider.isConnected) await provider.connect();
const { configPda } = configCache || await loadConfig();
const manager = new solanaWeb3.PublicKey(document.getElementById("managerWallet").value.trim());
const addQ1 = usdToCents(document.getElementById("addQ1").value.trim());
const addQ2 = usdToCents(document.getElementById("addQ2").value.trim());
if (addQ1 === 0n && addQ2 === 0n) throw new Error("Нужно указать сумму хотя бы для одной очереди.");
const allowancePda = deriveManagerAllowancePda(manager);
const disc = await ixDiscriminator("grant_manager_limits");
const data = concat(disc, manager.toBytes(), u64ToBytes(addQ1), u64ToBytes(addQ2));
const keys = [
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
{ pubkey: configPda, isSigner: false, isWritable: true },
{ pubkey: allowancePda, 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>`;
} catch (e) {
const raw = String(e.message || e);
if (isUnauthorizedDao(raw)) {
const dao = configCache?.config?.dao?.toBase58?.() || "не определен";
out.innerHTML = `<span class="warn">Вы подключены не под DAO-кошельком. Нужен: <code>${dao}</code>.</span>`;
return;
}
out.innerHTML = `<span class="err">${raw}</span>`;
}
}
async function loadManagerLimits() {
const out = document.getElementById("managerState");
out.textContent = "";
try {
const manager = new solanaWeb3.PublicKey(document.getElementById("managerWallet").value.trim());
const allowancePda = deriveManagerAllowancePda(manager);
const ai = await connection.getAccountInfo(allowancePda, "confirmed");
if (!ai) {
out.innerHTML = `<span class="warn">Лимиты для этого менеджера ещё не выданы (PDA не создан).</span>`;
return;
}
const st = parseManagerAllowance(ai.data);
out.innerHTML = `
<div>Manager: <code>${st.manager.toBase58()}</code></div>
<div>PDA: <code>${allowancePda.toBase58()}</code></div>
<div>Доступно Q1: <b>${centsToUsdStr(st.q1)} USD</b></div>
<div>Доступно Q2: <b>${centsToUsdStr(st.q2)} USD</b></div>
`;
} catch (e) {
out.innerHTML = `<span class="err">${String(e.message || e)}</span>`;
}
}
document.getElementById("connectBtn").addEventListener("click", connectWallet);
document.getElementById("refreshBtn").addEventListener("click", refresh);
document.getElementById("grantBtn").addEventListener("click", grantLimits);
document.getElementById("loadManagerBtn").addEventListener("click", loadManagerLimits);
refresh();
</script>
</body>
</html>