shine-solana/shine/programs/shine_payments/dApp/init.html

405 lines
17 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 name="robots" content="noindex, nofollow">
<meta charset="UTF-8" />
<title>Shine Payments — Phantom demo (devnet, deep logs)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; padding: 20px; }
h1 { font-size: 18px; margin-bottom: 12px; }
.row { display: flex; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; }
button { padding: 8px 12px; border-radius: 8px; border: 1px solid #ccc; cursor: pointer; }
button:hover { background: #f5f5f5; }
#log {
background: #0a0a0a; color: #d1d5db; padding: 12px; border-radius: 10px;
min-height: 220px; max-height: 60vh; overflow: auto; line-height: 1.4; white-space: pre-wrap;
box-shadow: inset 0 0 0 1px #222;
}
.muted { color: #6b7280; }
.ok { color: #86efac; }
.err { color: #fca5a5; }
/* ——— banner с логотипом и предупреждением ——— */
.banner {
display: flex; align-items: center; gap: 14px;
padding: 12px 14px; margin: 12px 0 18px;
border: 1px solid #f1e5a8; border-radius: 12px;
background: #fffbe6; color: #7a5d00;
}
.banner img { width: 44px; height: 44px; border-radius: 8px; flex: 0 0 auto; }
.banner .txt { line-height: 1.35; }
.banner .txt b { font-weight: 700; }
.banner .en { margin-top: 6px; color: #6b7280; font-size: 13px; }
</style>
</head>
<body>
<!-- DEV BANNER (logo + RU/EN notice) -->
<div class="banner" role="note" aria-label="Dev notice">
<img src="./shine_nft_logo_256.png" alt="Shine logo">
<div class="txt">
<div><b>ВНИМАНИЕ:</b> это тестовая страница для внутренней разработки. Я разбираюсь и пишу смарт-контракт и dApp для будущего токена и тестирую его работу в Devnet и подключение кошелька Phantom. Страница не является рабочим продуктом и предназначена только для внутренних тестов.</div>
<div class="en"><b>NOTICE:</b> this is a development test page. Im building a smart contract and dApp for a future token and testing it on Devnet and Phantom wallet connection. This page is not a live product and is intended for internal testing only.</div>
</div>
</div>
<h1>Shine Payments — Phantom wallet (devnet)</h1>
<div class="row">
<button id="btnConnect">Connect Phantom</button>
<button id="btnInfo" disabled>Показать адрес и баланс</button>
<button id="btnAirdrop" disabled>Airdrop 1 SOL (devnet)</button>
<button id="btnInit" disabled>Выполнить init()</button>
<button id="btn-delete-init" disabled>🗑️ Удалить PDA (delete_init)</button>
</div>
<div class="muted">
В Phantom выбери сеть <b>Devnet</b>. Логи ниже и в консоли (F12 → Console).
</div>
<pre id="log"></pre>
<!-- Единственная зависимость: web3.js -->
<script src="https://unpkg.com/@solana/web3.js@1.95.0/lib/index.iife.min.js"></script>
<script>
(async () => {
const logEl = document.getElementById("log");
const autoScroll = () => { logEl.scrollTop = logEl.scrollHeight; };
const log = (...a) => { console.log(...a); logEl.textContent += a.join(" ") + "\n"; autoScroll(); };
const logOk = (...a) => log('%c' + a.join(" "), 'color:#86efac');
const logErr = (...a) => log('%c' + a.join(" "), 'color:#fca5a5');
// ===== ПАРАМЕТРЫ ПРОЕКТА =====
const RPC_URL = "https://api.devnet.solana.com";
const PROGRAM_ID = new solanaWeb3.PublicKey("92sgkgx7KHpbhQu81mNGHaKa7skJB7esArVdPM7paDSW");
const STATE_SEED = "shine_investments_state";
// Лучше "processed" для симуляций + "confirmed" для подтверждений
const connection = new solanaWeb3.Connection(RPC_URL, { commitment: "confirmed" });
const enc = new TextEncoder();
let provider = null; // Phantom provider (window.solana)
let walletPubkey = null; // PublicKey пользователя из Phantom
let logsSubId = null; // id подписки на логи программы
// ===== УТИЛИТЫ ОШИБОК/ЛОГОВ =====
function safeJson(v) {
try { return JSON.stringify(v, null, 2); } catch { return String(v); }
}
function printRpcError(prefix, e) {
// Структура ошибок Solana/Anchor часто лежит в e, e.message, e.data, e.logs, e.code
logErr(prefix);
if (!e) return;
if (e.message) logErr("message:", e.message);
if (e.code !== undefined) logErr("code:", e.code);
if (e.name) logErr("name:", e.name);
// web3.js/JSON-RPC иногда кладёт это сюда:
if (e.data) {
if (e.data.logs) {
logErr("logs:");
(e.data.logs || []).forEach(l => logErr(" " + l));
}
if (e.data.err) {
logErr("rpc err:", safeJson(e.data.err));
}
}
// Некоторые кошельки/обёртки кладут логи прямо в e.logs
if (e.logs) {
logErr("logs:");
(e.logs || []).forEach(l => logErr(" " + l));
}
// Стек — в конце
if (e.stack) {
logErr("stack:\n" + e.stack);
}
}
async function simulateAndLog(tx) {
// Симуляция перед отправкой — ключ к пониманию, где падает инструкция.
try {
const sim = await connection.simulateTransaction(tx, {
sigVerify: false, // подпись не требуется
commitment: "processed"
});
const v = sim.value;
log("🔎 simulate result — err:", safeJson(v.err));
if (v.logs?.length) {
log("🪵 simulate logs:");
v.logs.forEach(l => log(" " + l));
}
if (v.unitsConsumed !== undefined) {
log("⛽ compute units (simulate):", v.unitsConsumed);
}
return v;
} catch (e) {
printRpcError("❌ Ошибка simulateTransaction:", e);
return null;
}
}
async function confirmAndLog(signature, blockhashCtx) {
try {
const { blockhash, lastValidBlockHeight } = blockhashCtx;
log("🧱 confirm with blockhash/lastValid:", blockhash, "/", lastValidBlockHeight);
const res = await connection.confirmTransaction(
{ signature, blockhash, lastValidBlockHeight },
"confirmed"
);
log("📬 confirmation status:", safeJson(res.value));
return res.value;
} catch (e) {
printRpcError("❌ Ошибка confirmTransaction:", e);
return null;
}
}
async function getSigStatus(signature) {
try {
const st = await connection.getSignatureStatus(signature, { searchTransactionHistory: true });
log("🧾 signature status:", safeJson(st?.value));
return st?.value;
} catch (e) {
printRpcError("❌ Ошибка getSignatureStatus:", e);
}
}
// ===== Anchor discriminator (8 байт) =====
async function anchorDiscriminator8(name) {
const hash = await crypto.subtle.digest("SHA-256", enc.encode("global:" + name));
return new Uint8Array(hash).slice(0, 8);
}
// ===== PDA =====
async function getStatePda() {
const [pda] = await solanaWeb3.PublicKey.findProgramAddress(
[enc.encode(STATE_SEED)],
PROGRAM_ID
);
return pda;
}
// ===== Отправка через Phantom с расширенным логированием =====
async function sendViaPhantom(tx, blockhashCtx) {
// Вариант с signAndSendTransaction вернёт сразу signature,
// но иногда теряются preflight-детали. Мы дополнительно логируем simulate.
// Обязательно: транзакция без подписей, место под Lighthouse-инструкции.
await simulateAndLog(tx); // можно оставить, подпись не требуется
if (!provider.signAndSendTransaction) throw new Error("Phantom не поддерживает signAndSendTransaction");
const {signature} = await provider.signAndSendTransaction(tx);
logOk("✍️ отправлено, сигнатура:", signature);
await confirmAndLog(signature, blockhashCtx);
await getSigStatus(signature);
return signature;
}
function setButtonsEnabled(connected) {
document.getElementById("btnInfo").disabled = !connected;
document.getElementById("btnAirdrop").disabled = !connected;
document.getElementById("btnInit").disabled = !connected;
document.getElementById("btn-delete-init").disabled = !connected;
}
// ===== CONNECT =====
document.getElementById("btnConnect").addEventListener("click", async () => {
try {
if (!window.solana || !window.solana.isPhantom) {
logErr("❌ Phantom не найден. Установи расширение Phantom Wallet.");
return;
}
provider = window.solana;
// Подключаемся ТОЛЬКО по клику пользователя:
log("🔌 Ожидаем подключение Phantom…");
await provider.connect(); // без onlyIfTrusted — это уже явный жест пользователя
walletPubkey = provider.publicKey;
logOk("✅ Подключено:", walletPubkey.toBase58());
setButtonsEnabled(true);
// Подписка на логи программы — помогает увидеть то, что в simulate/confirm могло не попасть
try {
if (logsSubId) {
await connection.removeOnLogsListener(logsSubId);
logsSubId = null;
}
logsSubId = connection.onLogs(PROGRAM_ID, (ev) => {
log("🛰 onLogs:", ev.signature, "err:", safeJson(ev.err));
(ev.logs || []).forEach(l => log(" " + l));
}, "confirmed");
log("📡 Подписка на логи программы включена.");
} catch (e) {
printRpcError("⚠️ Не удалось подписаться на логи программы:", e);
}
provider.on?.("disconnect", async () => {
log("🔌 Отключено");
setButtonsEnabled(false);
walletPubkey = null;
if (logsSubId) {
try { await connection.removeOnLogsListener(logsSubId); } catch {}
logsSubId = null;
}
});
} catch (e) {
printRpcError("❌ Connect error:", e);
}
});
// ===== INFO =====
document.getElementById("btnInfo").addEventListener("click", async () => {
try {
if (!walletPubkey) throw new Error("Сначала подключи Phantom.");
const balanceLamports = await connection.getBalance(walletPubkey, "processed");
const statePda = await getStatePda();
log("👛 Кошелёк:", walletPubkey.toBase58());
log("💰 Баланс:", balanceLamports / solanaWeb3.LAMPORTS_PER_SOL, "SOL");
log("📦 statePda:", statePda.toBase58());
log("🌐 RPC:", RPC_URL);
} catch (e) {
printRpcError("❌ Ошибка INFO:", e);
}
});
// ===== AIRDROP (devnet) =====
document.getElementById("btnAirdrop").addEventListener("click", async () => {
try {
if (!walletPubkey) throw new Error("Сначала подключи Phantom.");
log("⛽ Запрос airdrop 1 SOL на", walletPubkey.toBase58());
const sig = await connection.requestAirdrop(walletPubkey, 1 * solanaWeb3.LAMPORTS_PER_SOL);
log("⏳ confirm airdrop…");
const { value: ctx } = await connection.getLatestBlockhashAndContext("processed");
await confirmAndLog(sig, ctx);
logOk("✅ Airdrop tx:", sig);
const bal = await connection.getBalance(walletPubkey);
log("💰 Новый баланс:", bal / solanaWeb3.LAMPORTS_PER_SOL, "SOL");
} catch (e) {
printRpcError("❌ Ошибка airdrop:", e);
}
});
// ===== INIT() =====
document.getElementById("btnInit").addEventListener("click", async () => {
try {
if (!walletPubkey) throw new Error("Сначала подключи Phantom.");
const statePda = await getStatePda();
log("🚀 Вызываем init()");
log(" payer: ", walletPubkey.toBase58());
log(" statePda: ", statePda.toBase58());
log(" programId:", PROGRAM_ID.toBase58());
// 8 байт дискриминатора Anchor для "global:init"
const data = await anchorDiscriminator8("init"); // Uint8Array длиной 8
// Аккаунты в том порядке, который ожидает on-chain метод
const keys = [
{ pubkey: walletPubkey, isSigner: true, isWritable: true }, // payer (signer)
{ pubkey: statePda, isSigner: false, isWritable: true }, // state PDA
{ pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false },
];
const ix = new solanaWeb3.TransactionInstruction({ programId: PROGRAM_ID, keys, data });
// Получаем блокхеш/lastValidBlockHeight и логируем
const { value: ctx } = await connection.getLatestBlockhashAndContext("processed");
const { blockhash, lastValidBlockHeight } = ctx;
log("⏱ blockhash:", blockhash);
log("⏱ lastValidBlockHeight:", lastValidBlockHeight);
const tx = new solanaWeb3.Transaction({
feePayer: walletPubkey,
recentBlockhash: blockhash,
}).add(ix);
// 1) Симуляция — сразу покажет логи Anchor/InstructionError
await simulateAndLog(tx);
log("📝 Подписываем в Phantom…");
// 2) Отправка + подтверждение с расширенными логами
const sig = await sendViaPhantom(tx, { blockhash, lastValidBlockHeight });
logOk("✅ Готово! tx:", sig);
log("🔗 Explorer:", `https://explorer.solana.com/tx/${sig}?cluster=devnet`);
} catch (e) {
// Печатаем максимально подробно
printRpcError("❌ Ошибка init:", e);
}
});
// ===== DELETE INIT =====
document.getElementById("btn-delete-init").addEventListener("click", async () => {
try {
// 1) Проверка подключения
if (!walletPubkey) throw new Error("Сначала подключи Phantom.");
// 2) PDA — те же сиды, что и в init
const statePda = await getStatePda();
log("🗑️ Вызываем delete_init()");
log(" signer: ", walletPubkey.toBase58());
log(" statePda: ", statePda.toBase58());
log(" programId:", PROGRAM_ID.toBase58());
// 3) Дискриминатор Anchor для "global:delete_init"
const data = await anchorDiscriminator8("delete_init"); // 8 байт
// 4) Аккаунты в порядке, который ожидает on-chain метод
const keys = [
{ pubkey: walletPubkey, isSigner: true, isWritable: true }, // signer (получатель ренты)
{ pubkey: statePda, isSigner: false, isWritable: true }, // state_pda
{ pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false },
];
const ix = new solanaWeb3.TransactionInstruction({
programId: PROGRAM_ID,
keys,
data, // только 8 байт дискриминатора, т.к. у метода нет аргументов
});
// 5) Блокхеш, формирование транзакции
const { value: ctx } = await connection.getLatestBlockhashAndContext("processed");
const { blockhash, lastValidBlockHeight } = ctx;
log("⏱ blockhash:", blockhash);
log("⏱ lastValidBlockHeight:", lastValidBlockHeight);
const tx = new solanaWeb3.Transaction({
feePayer: walletPubkey,
recentBlockhash: blockhash,
}).add(ix);
// 6) Симуляция → отправка → подтверждение
await simulateAndLog(tx);
log("📝 Подписываем в Phantom…");
const sig = await sendViaPhantom(tx, { blockhash, lastValidBlockHeight });
logOk("✅ delete_init выполнен. tx:", sig);
log("🔗 Explorer:", `https://explorer.solana.com/tx/${sig}?cluster=devnet`);
alert(`PDA удалён, рента возвращена подписанту.\nTx: ${sig}`);
} catch (e) {
printRpcError("❌ Ошибка delete_init:", e);
alert(`Ошибка delete_init: ${e.message || e}`);
}
});
// Больше НИКАКИХ автоконнектов на загрузке страницы.
if (window.solana?.isPhantom) {
provider = window.solana;
log(" Phantom найден. Нажми «Connect Phantom», чтобы подключиться.");
} else {
log(" Установи Phantom Wallet: https://phantom.app");
}
}
)();
</script>
</body>
</html>