import { renderHeader } from '../components/header.js'; import { authService, setAuthError, setAuthInfo, state, } from '../state.js'; import { toUserMessage } from '../services/ui-error-texts.js'; import { formatSol, getBalanceSol, getTopupSiteUrl, } from '../services/solana-wallet-service.js'; import { registerUserOnSolana } from '../services/solana-register-service.js'; export const pageMeta = { id: 'registration-payment-view', title: 'Оплата регистрации', showAppChrome: false }; const MIN_REQUIRED_SOL = 0.01; function parseBalanceSol(value) { const parsed = Number.parseFloat(String(value || '').replace(',', '.')); return Number.isFinite(parsed) ? parsed : 0; } function getCryptoRuntimeState() { const hasCrypto = Boolean(globalThis.crypto); const hasGetRandomValues = Boolean(globalThis.crypto && typeof globalThis.crypto.getRandomValues === 'function'); const hasSubtle = Boolean(globalThis.crypto && (globalThis.crypto.subtle || globalThis.crypto.webkitSubtle)); const secureContext = window.isSecureContext === true; return { hasCrypto, hasGetRandomValues, hasSubtle, secureContext }; } 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 = 'status-line is-unavailable'; status.style.display = 'none'; const walletValue = document.createElement('input'); walletValue.className = 'input'; walletValue.type = 'text'; walletValue.value = state.registrationPayment.walletAddress || ''; walletValue.readOnly = true; const walletRow = document.createElement('div'); walletRow.className = 'inline-input-row'; 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 { status.className = 'status-line is-unavailable'; status.textContent = 'Не удалось скопировать номер кошелька.'; status.style.display = ''; } }); walletRow.append(walletValue, copyButton); const balanceRow = document.createElement('div'); balanceRow.className = 'row wrap-row'; const balanceValue = document.createElement('strong'); balanceValue.textContent = `${formatSol(parseBalanceSol(state.registrationPayment.balanceSOL), 6)} SOL`; const refreshButton = document.createElement('button'); refreshButton.className = 'square-btn'; refreshButton.type = 'button'; refreshButton.textContent = '↻'; refreshButton.title = 'Обновить'; const refreshBalance = async ({ showError = true, addressOverride = '' } = {}) => { const address = String(addressOverride || walletValue.value || '').trim(); if (!address) return null; refreshButton.disabled = true; try { const balance = await getBalanceSol({ endpoint: state.entrySettings.solanaServer, address, }); state.registrationPayment.balanceSOL = String(balance.sol); balanceValue.textContent = `${formatSol(balance.sol, 6)} SOL`; return Number(balance.sol) || 0; } catch (error) { if (showError) { status.className = 'status-line is-unavailable'; status.textContent = `Не удалось обновить баланс: ${error?.message || 'unknown'}`; status.style.display = ''; } return null; } finally { refreshButton.disabled = false; } }; const deriveUserWalletAddress = async () => { const keyBundle = state.registrationDraft.preGeneratedKeyBundle; if (!keyBundle) throw new Error('Ключи ещё не сгенерированы. Вернитесь на предыдущий шаг.'); const { publicKeyB64 } = keyBundle.devicePair; const raw = atob(publicKeyB64); const bytes = new Uint8Array(raw.length); for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i); const { PublicKey } = await import('https://esm.sh/@solana/web3.js@1.98.4'); const address = new PublicKey(bytes).toBase58(); state.registrationPayment.walletAddress = address; walletValue.value = address; return address; }; refreshButton.addEventListener('click', () => { void refreshBalance(); }); balanceRow.append(balanceValue, refreshButton); const topupButton = document.createElement('button'); topupButton.className = 'ghost-btn'; topupButton.type = 'button'; topupButton.textContent = 'Пополнить кошелёк'; topupButton.addEventListener('click', async () => { try { const walletAddress = await deriveUserWalletAddress(); window.open(getTopupSiteUrl(walletAddress), '_blank', 'noopener,noreferrer'); } catch (error) { status.className = 'status-line is-unavailable'; status.textContent = `Не удалось подготовить кошелёк: ${error?.message || 'unknown'}`; status.style.display = ''; } }); const showKeysButton = document.createElement('button'); showKeysButton.className = 'ghost-btn'; showKeysButton.type = 'button'; showKeysButton.textContent = 'Показать сгенерированные ключи'; showKeysButton.addEventListener('click', () => navigate('registration-draft-keys-view')); const submitButton = document.createElement('button'); submitButton.className = 'primary-btn'; submitButton.type = 'button'; submitButton.textContent = 'Зарегистрироваться'; submitButton.addEventListener('click', async () => { status.style.display = 'none'; const cryptoState = getCryptoRuntimeState(); if (!cryptoState.hasCrypto || !cryptoState.hasGetRandomValues || !cryptoState.hasSubtle) { status.className = 'status-line is-unavailable'; status.textContent = 'Криптография браузера недоступна. Откройте приложение через HTTPS tunnel или localhost и повторите регистрацию.'; status.style.display = ''; return; } try { submitButton.disabled = true; submitButton.textContent = 'Регистрация...'; const walletAddress = await deriveUserWalletAddress(); const currentBalance = await refreshBalance({ showError: true, addressOverride: walletAddress }); if (currentBalance == null) return; if (currentBalance < MIN_REQUIRED_SOL) { status.className = 'status-line is-unavailable'; status.textContent = `Для регистрации нужно минимум ${MIN_REQUIRED_SOL} SOL. Сейчас на кошельке ${formatSol(currentBalance, 6)} SOL. Пополните на промо-странице или попросите перевод у знакомого с тестовыми SOL.`; status.style.display = ''; const openTopup = window.confirm('Открыть страницу пополнения с вашим кошельком?'); if (openTopup) { window.open(getTopupSiteUrl(walletAddress), '_blank', 'noopener,noreferrer'); } return; } const keyBundle = state.registrationDraft.preGeneratedKeyBundle; if (!keyBundle) throw new Error('Ключи не найдены. Вернитесь на предыдущий шаг.'); // Регистрация на Solana (смарт контракт) submitButton.textContent = 'Регистрация в Solana...'; try { await registerUserOnSolana({ login: state.registrationDraft.login, keyBundle, solanaEndpoint: state.entrySettings.solanaServer, }); } catch (solanaError) { const solanaMsg = String(solanaError?.message || ''); // Пользователь уже зарегистрирован в Solana — продолжаем if (!solanaMsg.includes('already') && !solanaMsg.includes('UserAlreadyExists')) { throw new Error(`Ошибка регистрации в Solana: ${solanaMsg}`); } } // Регистрация на сервере SHiNE submitButton.textContent = 'Регистрация на сервере...'; await authService.reconnect(state.entrySettings.shineServer); const result = await authService.registerUserWithKeyBundle(state.registrationDraft.login, keyBundle); state.registrationDraft.flowType = 'registration'; state.registrationDraft.sessionId = result.sessionId; state.registrationDraft.storagePwd = result.storagePwd; state.registrationDraft.pendingKeyBundle = result.keyBundle; state.registrationDraft.pendingSessionMaterial = result.sessionMaterial; setAuthInfo(`Регистрация завершена. Вы вошли как @${result.login}. Далее откройте вкладку «Каналы».`); navigate('registration-keys-view'); } catch (error) { const message = toUserMessage(error, 'Не удалось завершить регистрацию.'); setAuthError(message); status.className = 'status-line is-unavailable'; status.textContent = message; status.style.display = ''; } finally { submitButton.disabled = false; submitButton.textContent = 'Зарегистрироваться'; } }); card.innerHTML = `

Для регистрации в тестовой Solana нужно минимум 0,01 SOL на вашем кошельке.

Баланс (Solana)
`; card.children[1].append(walletRow); card.children[2].append(balanceRow); card.append(topupButton, showKeysButton, submitButton, status); screen.append( renderHeader({ title: 'Оплата регистрации', leftAction: { label: '←', onClick: () => navigate('register-view') }, }), card, ); (async () => { try { const walletAddress = await deriveUserWalletAddress(); await refreshBalance({ addressOverride: walletAddress }); } catch (error) { status.className = 'status-line is-unavailable'; status.textContent = `Не удалось подготовить wallet.key: ${error?.message || 'unknown'}`; status.style.display = ''; } })(); return screen; }