SHiNE-server/shine-UI/js/pages/solana-users-init-view.js

196 lines
6.4 KiB
JavaScript

import { renderHeader } from '../components/header.js';
import {
SHINE_USERS_ECONOMY_CONFIG_SEED,
SHINE_USERS_PROGRAM_ID,
SOLANA_CLUSTER,
} from '../solana-programs.js';
import { state } from '../state.js';
export const pageMeta = { id: 'solana-users-init-view', title: 'Solana Init (users)' };
let solanaLibPromise = null;
function loadSolanaLib() {
if (!solanaLibPromise) {
solanaLibPromise = import('https://esm.sh/@solana/web3.js@1.98.4?bundle');
}
return solanaLibPromise;
}
function getProvider() {
const p = globalThis?.solana;
if (!p || !p.isPhantom) return null;
return p;
}
function shortAddr(value = '') {
const v = String(value || '').trim();
if (v.length < 12) return v;
return `${v.slice(0, 6)}...${v.slice(-6)}`;
}
export function render({ navigate }) {
const screen = document.createElement('section');
screen.className = 'stack';
const card = document.createElement('div');
card.className = 'card stack';
const status = document.createElement('p');
status.className = 'meta-muted';
status.textContent = 'Подключите кошелёк и выполните init_users_economy_config.';
const programId = SHINE_USERS_PROGRAM_ID;
const programInput = document.createElement('input');
programInput.className = 'input';
programInput.type = 'text';
programInput.readOnly = true;
programInput.value = programId;
const rpcInput = document.createElement('input');
rpcInput.className = 'input';
rpcInput.type = 'text';
rpcInput.readOnly = true;
rpcInput.value = String(state.entrySettings.solanaServer || '');
const walletLine = document.createElement('p');
walletLine.className = 'meta-muted';
walletLine.textContent = 'Кошелёк: не подключен';
const economyPdaLine = document.createElement('p');
economyPdaLine.className = 'meta-muted';
economyPdaLine.textContent = 'PDA economy config: —';
const txLine = document.createElement('p');
txLine.className = 'meta-muted';
txLine.textContent = 'TX: —';
const connectBtn = document.createElement('button');
connectBtn.className = 'text-btn';
connectBtn.type = 'button';
connectBtn.textContent = 'Подключить кошелёк';
const initBtn = document.createElement('button');
initBtn.className = 'primary-btn';
initBtn.type = 'button';
initBtn.textContent = 'Запустить init_users_economy_config';
initBtn.disabled = true;
let provider = null;
let walletPubkey = null;
let economyPda = null;
async function recomputePda() {
const solana = await loadSolanaLib();
const pid = new solana.PublicKey(programId);
const [pda] = solana.PublicKey.findProgramAddressSync(
[new TextEncoder().encode(SHINE_USERS_ECONOMY_CONFIG_SEED)],
pid,
);
economyPda = pda;
economyPdaLine.textContent = `PDA economy config: ${pda.toBase58()}`;
}
connectBtn.addEventListener('click', async () => {
status.textContent = 'Подключение кошелька...';
try {
provider = getProvider();
if (!provider) throw new Error('Phantom не найден');
const result = await provider.connect();
walletPubkey = result?.publicKey || provider.publicKey;
if (!walletPubkey) throw new Error('Кошелёк не вернул public key');
walletLine.textContent = `Кошелёк: ${walletPubkey.toBase58()} (${shortAddr(walletPubkey.toBase58())})`;
initBtn.disabled = false;
status.textContent = 'Кошелёк подключен.';
} catch (e) {
status.textContent = `Ошибка подключения: ${e?.message || 'unknown'}`;
}
});
initBtn.addEventListener('click', async () => {
if (!provider || !walletPubkey || !economyPda) return;
initBtn.disabled = true;
status.textContent = 'Отправка транзакции...';
txLine.textContent = 'TX: —';
try {
const solana = await loadSolanaLib();
const connection = new solana.Connection(String(state.entrySettings.solanaServer || ''), 'confirmed');
const programPubkey = new solana.PublicKey(programId);
const ix = new solana.TransactionInstruction({
programId: programPubkey,
keys: [
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
{ pubkey: economyPda, isSigner: false, isWritable: true },
{ pubkey: solana.SystemProgram.programId, isSigner: false, isWritable: false },
],
data: Uint8Array.from([1]),
});
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
const tx = new solana.Transaction({
feePayer: walletPubkey,
blockhash,
lastValidBlockHeight,
}).add(ix);
const signed = await provider.signTransaction(tx);
const sig = await connection.sendRawTransaction(signed.serialize(), { skipPreflight: false });
await connection.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight }, 'confirmed');
txLine.textContent = `TX: ${sig}`;
status.textContent = 'Успешно: init_users_economy_config выполнен.';
} catch (e) {
const message = String(e?.message || 'unknown');
txLine.textContent = 'TX: ошибка';
if (message.toLowerCase().includes('already') || message.includes('1000')) {
status.textContent = 'PDA уже инициализирован. Повторный init не требуется.';
} else {
status.textContent = `Ошибка init: ${message}`;
}
} finally {
initBtn.disabled = false;
}
});
void recomputePda();
card.append(
(() => {
const t = document.createElement('p');
t.className = 'meta-muted';
t.textContent = `Cluster: ${SOLANA_CLUSTER}`;
return t;
})(),
(() => {
const label = document.createElement('label');
label.className = 'field-label';
label.textContent = 'Program ID (shine_users)';
return label;
})(),
programInput,
(() => {
const label = document.createElement('label');
label.className = 'field-label';
label.textContent = 'RPC endpoint';
return label;
})(),
rpcInput,
walletLine,
economyPdaLine,
txLine,
connectBtn,
initBtn,
status,
);
screen.append(
renderHeader({
title: 'Solana Init (users)',
leftAction: { label: '←', onClick: () => navigate('developer-settings-view') },
}),
card,
);
return screen;
}