571 lines
27 KiB
HTML
571 lines
27 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>
|
||
: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("FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm");
|
||
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 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 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>
|