Files
SHiNE-server/shine-UI/js/pages/topup-view.js
T

175 lines
7.2 KiB
JavaScript

import { renderHeader } from '../components/header.js';
import { state } from '../state.js';
import {
createSolanaWalletFromPrivateBase58,
formatSol,
getBalanceSol,
getTopupSiteUrl,
transferSol,
} from '../services/solana-wallet-service.js';
import { loadSolanaWeb3 } from '../vendor/solana-web3-loader.js';
export const pageMeta = { id: 'topup-view', title: 'Пополнение solana счета', showAppChrome: false };
const DEVNET_ENDPOINT = 'https://api.devnet.solana.com';
const SENDER_PRIVATE_32_BASE58 = '6xqAuKYvA8qrCdAkcw7Y8aMgvBnYk8JLxWLma5BzbAvu';
const REGISTRATION_TOPUP_AMOUNT_SOL = 0.02;
// Канонический Solana-адрес пополнения = публичный device-ключ из сгенерированного набора ключей.
// Тот же путь, что в registration-payment-view (deriveUserWalletAddress); не выводим адрес
// напрямую из пароля, иначе он расходится с device-ключом регистрации.
async function clientWalletAddressFromBundle() {
const keyBundle = state.registrationDraft.preGeneratedKeyBundle;
if (!keyBundle || !keyBundle.clientPair) {
throw new Error('Ключи ещё не сгенерированы. Вернитесь на экран регистрации.');
}
const raw = atob(keyBundle.clientPair.publicKeyB64);
const bytes = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i += 1) bytes[i] = raw.charCodeAt(i);
const { PublicKey } = await loadSolanaWeb3();
return new PublicKey(bytes).toBase58();
}
export function render({ navigate }) {
const screen = document.createElement('section');
screen.className = 'stack';
const walletValue = document.createElement('input');
walletValue.className = 'input';
walletValue.type = 'text';
walletValue.value = state.registrationPayment.walletAddress || '';
walletValue.readOnly = true;
walletValue.style.fontSize = '13px';
const status = document.createElement('p');
status.className = 'meta-muted';
status.textContent = 'Проверяем кошелек...';
status.style.whiteSpace = 'pre-wrap';
status.style.overflowWrap = 'anywhere';
status.style.wordBreak = 'break-word';
status.style.fontSize = '13px';
const copyButton = document.createElement('button');
copyButton.className = 'ghost-btn';
copyButton.type = 'button';
copyButton.textContent = 'Скопировать';
copyButton.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(walletValue.value);
copyButton.textContent = 'Скопировано';
window.setTimeout(() => {
copyButton.textContent = 'Скопировать';
}, 1500);
} catch {
window.alert('Не удалось скопировать номер кошелька.');
}
});
const walletRow = document.createElement('div');
walletRow.className = 'inline-input-row';
walletRow.append(walletValue, copyButton);
const card = document.createElement('div');
card.className = 'card stack';
card.innerHTML = `
<p class="auth-copy">Можете или пополнить счёт тестовыми соланами и продолжить регистрацию.</p>
<div class="card stack" style="padding:12px; max-width:320px;">
<div class="field-label" style="margin-bottom:6px;">Кошелёк для пополнения (client key = Solana wallet)</div>
</div>
<div class="stack" style="gap:6px;">
<p class="meta-muted">Или можете отдельно открыть страницу тестового пополнения.</p>
<a class="link-card" id="topup-site-link" href="${getTopupSiteUrl(state.registrationPayment.walletAddress || '')}" target="_blank" rel="noreferrer">Открыть страницу тестового пополнения</a>
</div>
`;
card.children[1].append(walletRow);
const testButton = document.createElement('button');
testButton.className = 'ghost-btn';
testButton.type = 'button';
testButton.textContent = 'Тестовое пополнение для регистрации';
testButton.addEventListener('click', async () => {
const address = String(walletValue.value || '').trim();
if (!address) {
window.alert('Адрес кошелька не найден.');
return;
}
if (!senderKeypair) {
status.textContent = 'Ошибка: тестовый кошелёк отправителя ещё не инициализирован.';
return;
}
testButton.disabled = true;
try {
status.textContent = 'Отправляем тестовое пополнение для регистрации...';
const tx = await transferSol({
endpoint: DEVNET_ENDPOINT,
fromKeypair: senderKeypair,
toAddress: address,
amountSol: REGISTRATION_TOPUP_AMOUNT_SOL,
});
const bal = await getBalanceSol({
endpoint: DEVNET_ENDPOINT,
address,
});
state.registrationPayment.balanceSOL = String(bal.sol);
status.style.fontSize = '12px';
status.textContent = [
'Тестовое пополнение для регистрации выполнено.',
`Кошелёк пополнен на ${REGISTRATION_TOPUP_AMOUNT_SOL} SOL.`,
`Новый баланс: ${formatSol(bal.sol, 6)} SOL.`,
`Signature: ${tx.signature}`,
].join('\n');
} catch (error) {
status.style.fontSize = '13px';
status.textContent = `Ошибка тестового пополнения: ${error?.message || 'unknown'}`;
} finally {
testButton.disabled = false;
}
});
const backButton = document.createElement('button');
backButton.className = 'primary-btn';
backButton.type = 'button';
backButton.textContent = 'Продолжить регистрацию';
backButton.addEventListener('click', () => navigate('registration-payment-view'));
const actions = document.createElement('div');
actions.className = 'auth-footer-actions';
actions.append(testButton, backButton);
let senderKeypair = null;
(async () => {
try {
if (!walletValue.value) {
const address = await clientWalletAddressFromBundle();
state.registrationPayment.walletAddress = address;
walletValue.value = address;
}
const topupSiteLink = card.querySelector('#topup-site-link');
if (topupSiteLink instanceof HTMLAnchorElement) {
topupSiteLink.href = getTopupSiteUrl(walletValue.value);
}
const balance = await getBalanceSol({
endpoint: state.entrySettings.solanaServer,
address: walletValue.value,
});
state.registrationPayment.balanceSOL = String(balance.sol);
status.textContent = `Текущий баланс: ${formatSol(balance.sol, 6)} SOL`;
const sender = await createSolanaWalletFromPrivateBase58(SENDER_PRIVATE_32_BASE58);
senderKeypair = sender.keypair;
} catch (error) {
status.textContent = `Не удалось получить баланс: ${error?.message || 'unknown'}`;
}
})();
screen.append(
renderHeader({
title: 'Пополнение solana счета',
leftAction: { label: '←', onClick: () => navigate('registration-payment-view') },
}),
card,
status,
actions,
);
return screen;
}