import { renderHeader } from '../components/header.js'; import { authService, state } from '../state.js'; import { createRandomSolanaWallet, createSolanaWalletFromPrivateBase58, formatSol, getBalanceSol, getTopupSiteUrl, getWalletFromStoredDeviceKey, requestAirdropSol, transferSol, } from '../services/solana-wallet-service.js'; import { formatAr, getArweaveBalance, getArweaveTopupSiteUrl, getArweaveWalletFromStoredDeviceKey, transferAr, } from '../services/arweave-wallet-service.js'; import { loadEncryptedUserSecrets } from '../services/key-vault.js'; import { calcLimitTopupPriceLamports, getLimitStepBytes, getShineBlockchainUsage, getShineUsersEconomyConfig, updateShineUserPdaOnSolana, } from '../services/shine-blockchain-wallet-service.js?v=2026052803'; export const pageMeta = { id: 'wallet-view', title: 'Кошелёк' }; const SOLANA_PRIVATE_BASE58_MAX_LEN = 44; function nowRu() { return new Date().toLocaleString('ru-RU'); } function formatKbFromBytes(rawBytes) { const bytes = typeof rawBytes === 'bigint' ? Number(rawBytes) : Number(rawBytes || 0); if (!Number.isFinite(bytes) || bytes <= 0) return '0 KB'; const kb = bytes / 1024; return `${kb.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} KB`; } function lamportsToSolText(lamportsBigInt) { const value = Number(lamportsBigInt || 0n) / 1_000_000_000; return value.toLocaleString('ru-RU', { minimumFractionDigits: 0, maximumFractionDigits: 9 }); } function createModeBackButton(renderWalletChoice) { const backBtn = document.createElement('button'); backBtn.className = 'text-btn'; backBtn.textContent = '← К выбору кошелька'; backBtn.addEventListener('click', () => { renderWalletChoice(); }); return backBtn; } function sessionArgsOrThrow() { const login = String(state.session.login || '').trim(); const storagePwd = String(state.session.storagePwdInMemory || '').trim(); if (!login || !storagePwd) { throw new Error('Нет активной сессии. Выполните вход заново.'); } return { login, storagePwd }; } export function render({ navigate }) { const screen = document.createElement('section'); screen.className = 'stack'; const status = document.createElement('p'); status.className = 'meta-muted'; const setStatus = (text) => { status.textContent = String(text || ''); }; const content = document.createElement('div'); content.className = 'stack'; screen.append( renderHeader({ title: 'Кошелёк', leftAction: { label: '←', onClick: () => navigate('profile-view') }, }), content, status, ); let activeModeToken = 0; let arweaveWalletCtx = null; function clearArweaveSecretsInMemory() { if (!arweaveWalletCtx?.jwk) return; Object.keys(arweaveWalletCtx.jwk).forEach((key) => { arweaveWalletCtx.jwk[key] = ''; }); arweaveWalletCtx.jwk = null; arweaveWalletCtx = null; } function renderWalletChoice() { activeModeToken += 1; clearArweaveSecretsInMemory(); content.innerHTML = ''; const card = document.createElement('div'); card.className = 'card stack'; card.innerHTML = `

Кошелёк

Выберите режим кошелька.

`; const solanaBtn = document.createElement('button'); solanaBtn.className = 'primary-btn'; solanaBtn.style.width = '100%'; solanaBtn.textContent = 'Solana кошелёк'; solanaBtn.addEventListener('click', () => { void renderSolanaWallet(); }); const arweaveBtn = document.createElement('button'); arweaveBtn.className = 'primary-btn'; arweaveBtn.style.width = '100%'; arweaveBtn.textContent = 'Arweave кошелёк'; arweaveBtn.addEventListener('click', () => { void renderArweaveWallet(); }); const shineBchBtn = document.createElement('button'); shineBchBtn.className = 'primary-btn'; shineBchBtn.style.width = '100%'; shineBchBtn.textContent = 'Блокчейн Сияния'; shineBchBtn.addEventListener('click', () => { void renderShineBlockchainWallet(); }); card.append(solanaBtn, arweaveBtn, shineBchBtn); content.append(card); setStatus('Выберите тип кошелька.'); } async function renderShineBlockchainWallet() { const modeToken = ++activeModeToken; clearArweaveSecretsInMemory(); content.innerHTML = ''; const backBtn = createModeBackButton(renderWalletChoice); const card = document.createElement('div'); card.className = 'card stack'; const limitLabel = document.createElement('p'); limitLabel.className = 'meta-muted'; limitLabel.textContent = 'Лимит блокчейна'; const limitValue = document.createElement('h2'); limitValue.style.fontSize = '26px'; limitValue.textContent = '— KB'; const usedLabel = document.createElement('p'); usedLabel.className = 'meta-muted'; usedLabel.textContent = 'Израсходовано (закреплено в Solana)'; const usedValue = document.createElement('h2'); usedValue.style.fontSize = '26px'; usedValue.textContent = '— KB'; const leftLabel = document.createElement('p'); leftLabel.className = 'meta-muted'; leftLabel.textContent = 'Осталось (по Solana)'; const leftValue = document.createElement('h2'); leftValue.style.fontSize = '30px'; leftValue.textContent = '— KB'; const pdaLabel = document.createElement('p'); pdaLabel.className = 'meta-muted'; pdaLabel.style.wordBreak = 'break-all'; pdaLabel.textContent = 'PDA: —'; const endpointLabel = document.createElement('p'); endpointLabel.className = 'meta-muted'; endpointLabel.textContent = `RPC: ${state.entrySettings.solanaServer}`; const updatedLabel = document.createElement('p'); updatedLabel.className = 'meta-muted'; updatedLabel.textContent = 'Обновлено: —'; const serverTitle = document.createElement('h3'); serverTitle.style.margin = '8px 0 0'; serverTitle.textContent = 'Фактическое состояние на сервере'; const serverBlocksLabel = document.createElement('p'); serverBlocksLabel.className = 'meta-muted'; serverBlocksLabel.textContent = 'Блоков: —'; const serverSizeLabel = document.createElement('p'); serverSizeLabel.className = 'meta-muted'; serverSizeLabel.textContent = 'Размер цепочки: —'; const serverLastLabel = document.createElement('p'); serverLastLabel.className = 'meta-muted'; serverLastLabel.textContent = 'Крайний блок: —'; const serverLastHashLabel = document.createElement('p'); serverLastHashLabel.className = 'meta-muted'; serverLastHashLabel.style.wordBreak = 'break-all'; serverLastHashLabel.style.fontSize = '11px'; serverLastHashLabel.textContent = 'Hash: —'; const solanaTitle = document.createElement('h3'); solanaTitle.style.margin = '8px 0 0'; solanaTitle.textContent = 'Закреплено в Solana'; const solanaBlocksLabel = document.createElement('p'); solanaBlocksLabel.className = 'meta-muted'; solanaBlocksLabel.textContent = 'Блоков: —'; const solanaLastLabel = document.createElement('p'); solanaLastLabel.className = 'meta-muted'; solanaLastLabel.textContent = 'Крайний блок: —'; const solanaLastHashLabel = document.createElement('p'); solanaLastHashLabel.className = 'meta-muted'; solanaLastHashLabel.style.wordBreak = 'break-all'; solanaLastHashLabel.style.fontSize = '11px'; solanaLastHashLabel.textContent = 'Hash: —'; card.append( limitLabel, limitValue, usedLabel, usedValue, leftLabel, leftValue, pdaLabel, endpointLabel, updatedLabel, serverTitle, serverBlocksLabel, serverSizeLabel, serverLastLabel, serverLastHashLabel, solanaTitle, solanaBlocksLabel, solanaLastLabel, solanaLastHashLabel, ); const actions = document.createElement('div'); actions.className = 'stack'; actions.innerHTML = ` `; const refreshBtn = actions.querySelector('#refresh-shine-bch'); const syncBtn = actions.querySelector('#sync-shine-solana'); const topupBtn = actions.querySelector('#topup-shine-limit'); const fetchServerState = async () => { const user = await authService.getUser(String(state.session.login || '').trim()); if (!user?.exists) throw new Error('Пользователь не найден на сервере'); const lastNumber = Number(user.serverLastGlobalNumber ?? -1); return { sizeBytes: Number(user.serverBlockchainSizeBytes || 0), sizeLimitBytes: Number(user.serverBlockchainSizeLimitBytes || 0), lastNumber, blocksCount: lastNumber >= 0 ? (lastNumber + 1) : 0, lastHash: String(user.serverLastGlobalHash || ''), }; }; const resolveWalletSigningMaterial = async () => { const { login, storagePwd } = sessionArgsOrThrow(); let saved; try { saved = await loadEncryptedUserSecrets(login, storagePwd); } catch { saved = null; } let rootKey = String(saved?.rootKey || '').trim(); let blockchainKey = String(saved?.blockchainKey || '').trim(); const deviceKey = String(saved?.deviceKey || '').trim(); if (!deviceKey) throw new Error('На устройстве нет device.key. Выполните вход заново.'); if (rootKey && blockchainKey) { return { rootPrivatePkcs8B64: rootKey, blockchainPrivatePkcs8B64: blockchainKey, devicePrivatePkcs8B64: deviceKey }; } const password = window.prompt( 'Для операции нужен root key (и blockchain key), но они не сохранены на устройстве.\nВведите пароль аккаунта для временного восстановления ключей:', '', ); if (password == null) throw new Error('Операция отменена пользователем'); const keyBundle = await authService.derivePasswordKeyBundle(login, password); rootKey = keyBundle?.rootPair?.privatePkcs8B64 || ''; blockchainKey = keyBundle?.blockchainPair?.privatePkcs8B64 || ''; if (!rootKey || !blockchainKey) throw new Error('Не удалось восстановить root/blockchain key из пароля'); const shouldSave = window.confirm( 'Сохранить root key и blockchain key в зашифрованном контейнере этого устройства?\nВнимание: хранить ключи на телефоне менее безопасно.', ); if (shouldSave) { await authService.persistSelectedKeys(login, storagePwd, keyBundle, { saveRoot: true, saveBlockchain: true }); } return { rootPrivatePkcs8B64: rootKey, blockchainPrivatePkcs8B64: blockchainKey, devicePrivatePkcs8B64: deviceKey }; }; const setButtonsDisabled = (disabled) => { refreshBtn.disabled = disabled; syncBtn.disabled = disabled; topupBtn.disabled = disabled; }; const refreshUsage = async () => { setButtonsDisabled(true); try { const [usage, serverState] = await Promise.all([ getShineBlockchainUsage({ login: String(state.session.login || '').trim(), solanaEndpoint: state.entrySettings.solanaServer, }), fetchServerState(), ]); if (modeToken !== activeModeToken) return; const solanaBlocks = usage.lastBlockNumber >= 0 ? usage.lastBlockNumber + 1 : 0; const serverBlocks = serverState.blocksCount; limitValue.textContent = formatKbFromBytes(usage.paidLimitBytes); usedValue.textContent = formatKbFromBytes(usage.usedBytes); leftValue.textContent = formatKbFromBytes(usage.leftBytes); pdaLabel.textContent = `PDA: ${usage.userPda}`; endpointLabel.textContent = `RPC: ${usage.endpoint}`; updatedLabel.textContent = `Обновлено: ${nowRu()}`; serverBlocksLabel.textContent = `Блоков: ${serverBlocks.toLocaleString('ru-RU')}`; serverSizeLabel.textContent = `Размер цепочки: ${formatKbFromBytes(serverState.sizeBytes)} из ${formatKbFromBytes(serverState.sizeLimitBytes)}`; serverLastLabel.textContent = `Крайний блок: ${serverState.lastNumber}`; serverLastHashLabel.textContent = `Hash: ${serverState.lastHash || '—'}`; solanaBlocksLabel.textContent = `Блоков: ${solanaBlocks.toLocaleString('ru-RU')}`; solanaLastLabel.textContent = `Крайний блок: ${usage.lastBlockNumber}`; solanaLastHashLabel.textContent = `Hash: ${usage.lastBlockHashHex || '—'}`; setStatus('Данные лимита и состояния блокчейна обновлены.'); } catch (error) { if (modeToken !== activeModeToken) return; setStatus(`Не удалось обновить состояние блокчейна: ${error?.message || 'unknown'}`); } finally { setButtonsDisabled(false); } }; syncBtn.addEventListener('click', async () => { setButtonsDisabled(true); try { const [serverState, signing] = await Promise.all([ fetchServerState(), resolveWalletSigningMaterial(), ]); const result = await updateShineUserPdaOnSolana({ login: String(state.session.login || '').trim(), solanaEndpoint: state.entrySettings.solanaServer, rootPrivatePkcs8B64: signing.rootPrivatePkcs8B64, blockchainPrivatePkcs8B64: signing.blockchainPrivatePkcs8B64, devicePrivatePkcs8B64: signing.devicePrivatePkcs8B64, additionalLimitBytes: 0n, nextUsedBytes: BigInt(Math.max(0, serverState.sizeBytes)), nextLastBlockNumber: serverState.lastNumber, nextLastBlockHashHex: serverState.lastHash, }); if (modeToken !== activeModeToken) return; setStatus(`Состояние закреплено в Solana. Tx: ${result.signature}`); await refreshUsage(); } catch (error) { if (modeToken !== activeModeToken) return; setStatus(`Не удалось закрепить состояние в Solana: ${error?.message || 'unknown'}`); } finally { setButtonsDisabled(false); } }); topupBtn.addEventListener('click', async () => { setButtonsDisabled(true); try { const economy = await getShineUsersEconomyConfig({ solanaEndpoint: state.entrySettings.solanaServer }); const step = getLimitStepBytes(); const input = window.prompt( `Введите, на сколько увеличить лимит (в байтах, шаг ${step.toString()}).\nЦена за шаг: ${lamportsToSolText(economy.lamportsPerLimitStep)} SOL`, step.toString(), ); if (!input) { setStatus('Увеличение лимита отменено.'); return; } const addBytes = BigInt(String(input).trim()); const priceLamports = calcLimitTopupPriceLamports(addBytes, economy.lamportsPerLimitStep); const confirm = window.confirm( `Будет увеличено на ${formatKbFromBytes(addBytes)}.\n` + `С вашего Solana-счёта будет списано ~${lamportsToSolText(priceLamports)} SOL.\n` + `Продолжить?`, ); if (!confirm) { setStatus('Увеличение лимита отменено.'); return; } const signing = await resolveWalletSigningMaterial(); const result = await updateShineUserPdaOnSolana({ login: String(state.session.login || '').trim(), solanaEndpoint: state.entrySettings.solanaServer, rootPrivatePkcs8B64: signing.rootPrivatePkcs8B64, blockchainPrivatePkcs8B64: signing.blockchainPrivatePkcs8B64, devicePrivatePkcs8B64: signing.devicePrivatePkcs8B64, additionalLimitBytes: addBytes, }); if (modeToken !== activeModeToken) return; setStatus(`Лимит увеличен. Tx: ${result.signature}`); await refreshUsage(); } catch (error) { if (modeToken !== activeModeToken) return; setStatus(`Не удалось увеличить лимит: ${error?.message || 'unknown'}`); } finally { setButtonsDisabled(false); } }); refreshBtn.addEventListener('click', () => { void refreshUsage(); }); content.append(backBtn, card, actions); setStatus('Загрузка данных блокчейна Сияния...'); await refreshUsage(); } async function renderSolanaWallet() { const modeToken = ++activeModeToken; clearArweaveSecretsInMemory(); content.innerHTML = ''; let walletCtx = null; let walletAddress = ''; const backBtn = createModeBackButton(renderWalletChoice); const card = document.createElement('div'); card.className = 'card stack'; const balanceWrap = document.createElement('div'); const balanceLabel = document.createElement('p'); balanceLabel.className = 'meta-muted'; balanceLabel.textContent = 'Баланс (Solana)'; const balanceValue = document.createElement('h2'); balanceValue.style.fontSize = '30px'; balanceValue.textContent = '— SOL'; const updatedLabel = document.createElement('p'); updatedLabel.className = 'meta-muted'; updatedLabel.textContent = 'Обновлено: —'; const endpointLabel = document.createElement('p'); endpointLabel.className = 'meta-muted'; endpointLabel.textContent = `RPC: ${state.entrySettings.solanaServer}`; balanceWrap.append(balanceLabel, balanceValue, updatedLabel, endpointLabel); const addressCard = document.createElement('div'); addressCard.className = 'card'; addressCard.style.padding = '10px'; addressCard.innerHTML = `

Публичный адрес (wallet.key = device.key)

`; const addressEl = addressCard.querySelector('#wallet-address-value'); card.append(balanceWrap, addressCard); const actions = document.createElement('div'); actions.className = 'stack'; actions.innerHTML = `
`; const copyBtn = actions.querySelector('#copy-address'); const refreshBtn = actions.querySelector('#refresh-balance'); const sendBtn = actions.querySelector('#send-sol'); const topupBtn = actions.querySelector('#topup-sol'); const generatedCard = document.createElement('div'); generatedCard.className = 'card stack'; generatedCard.innerHTML = `

Создание нового кошелька Solana

Введите приватный ключ Base58 (32 байта) или сгенерируйте случайный.

`; const privateLabel = document.createElement('label'); privateLabel.className = 'meta-muted'; privateLabel.textContent = 'Приватный ключ (Base58, 32 байта)'; privateLabel.setAttribute('for', 'solana-private-base58-input'); const privateInput = document.createElement('input'); privateInput.id = 'solana-private-base58-input'; privateInput.type = 'text'; privateInput.placeholder = 'Введите приватный ключ Base58'; privateInput.maxLength = SOLANA_PRIVATE_BASE58_MAX_LEN; privateInput.autocomplete = 'off'; privateInput.spellcheck = false; const privateState = document.createElement('p'); privateState.className = 'meta-muted'; privateState.textContent = 'Ожидается Base58-строка приватного ключа.'; const generatedPublicLabel = document.createElement('label'); generatedPublicLabel.className = 'meta-muted'; generatedPublicLabel.textContent = 'Публичный ключ (Base58)'; generatedPublicLabel.setAttribute('for', 'solana-generated-public-key'); const generatedPublicInput = document.createElement('input'); generatedPublicInput.id = 'solana-generated-public-key'; generatedPublicInput.type = 'text'; generatedPublicInput.readOnly = true; generatedPublicInput.placeholder = 'Будет сгенерирован после нажатия кнопки'; const generatedPrivateLabel = document.createElement('label'); generatedPrivateLabel.className = 'meta-muted'; generatedPrivateLabel.textContent = 'Сгенерированный приватный ключ (Base58)'; generatedPrivateLabel.setAttribute('for', 'solana-generated-private-key'); const generatedPrivateInput = document.createElement('input'); generatedPrivateInput.id = 'solana-generated-private-key'; generatedPrivateInput.type = 'text'; generatedPrivateInput.readOnly = true; generatedPrivateInput.placeholder = 'Появится после генерации'; const generationActions = document.createElement('div'); generationActions.className = 'row'; generationActions.innerHTML = ` `; const copyGeneratedActions = document.createElement('div'); copyGeneratedActions.className = 'row'; copyGeneratedActions.innerHTML = ` `; generatedCard.append( privateLabel, privateInput, privateState, generationActions, generatedPrivateLabel, generatedPrivateInput, generatedPublicLabel, generatedPublicInput, copyGeneratedActions, ); const randomGenerateBtn = generationActions.querySelector('#generate-random-solana'); const fromPrivateGenerateBtn = generationActions.querySelector('#generate-from-private-solana'); const copyGeneratedPrivateBtn = copyGeneratedActions.querySelector('#copy-generated-private-solana'); const copyGeneratedPublicBtn = copyGeneratedActions.querySelector('#copy-generated-public-solana'); const BASE58_RE = /^[1-9A-HJ-NP-Za-km-z]+$/; const validatePrivateInput = () => { const value = String(privateInput.value || '').trim(); if (!value) { privateState.textContent = 'Ожидается Base58-строка приватного ключа.'; return false; } if (!BASE58_RE.test(value)) { privateState.textContent = 'Недопустимый формат: используйте только Base58.'; return false; } if (value.length > SOLANA_PRIVATE_BASE58_MAX_LEN) { privateState.textContent = 'Слишком длинное значение.'; return false; } try { const alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; let num = 0n; for (const c of value) { num = num * 58n + BigInt(alphabet.indexOf(c)); } let hex = num.toString(16); if (hex.length % 2) hex = `0${hex}`; const decoded = hex ? hex.match(/.{1,2}/g)?.map((h) => parseInt(h, 16)) || [] : []; let leadingZeros = 0; while (leadingZeros < value.length && value[leadingZeros] === '1') leadingZeros += 1; const byteLen = leadingZeros + decoded.length; if (byteLen < 32) { privateState.textContent = 'Слишком короткое значение: нужно 32 байта.'; return false; } if (byteLen > 32) { privateState.textContent = 'Слишком длинное значение: нужно ровно 32 байта.'; return false; } } catch { privateState.textContent = 'Ошибка декодирования Base58.'; return false; } privateState.textContent = 'Подходит'; return true; }; privateInput.addEventListener('input', () => { validatePrivateInput(); }); const setGenerationDisabled = (disabled) => { randomGenerateBtn.disabled = disabled; fromPrivateGenerateBtn.disabled = disabled; copyGeneratedPrivateBtn.disabled = disabled; copyGeneratedPublicBtn.disabled = disabled; }; randomGenerateBtn.addEventListener('click', async () => { setGenerationDisabled(true); try { const generated = await createRandomSolanaWallet(); if (modeToken !== activeModeToken) return; generatedPrivateInput.value = generated.privateKey32Base58; generatedPublicInput.value = generated.address; privateState.textContent = 'Случайный кошелёк создан.'; setStatus('Случайный кошелёк Solana успешно сгенерирован.'); } catch (error) { if (modeToken !== activeModeToken) return; setStatus(`Ошибка генерации случайного кошелька: ${error?.message || 'unknown'}`); } finally { setGenerationDisabled(false); } }); fromPrivateGenerateBtn.addEventListener('click', async () => { if (!validatePrivateInput()) { setStatus('Исправьте приватный ключ перед генерацией.'); return; } setGenerationDisabled(true); try { const generated = await createSolanaWalletFromPrivateBase58(privateInput.value); if (modeToken !== activeModeToken) return; generatedPrivateInput.value = generated.privateKey32Base58; generatedPublicInput.value = generated.address; privateState.textContent = 'Подходит'; setStatus('Публичный ключ сгенерирован из введённого приватного ключа.'); } catch (error) { if (modeToken !== activeModeToken) return; setStatus(`Ошибка генерации из приватного ключа: ${error?.message || 'unknown'}`); } finally { setGenerationDisabled(false); } }); copyGeneratedPrivateBtn.addEventListener('click', async () => { const value = String(generatedPrivateInput.value || '').trim(); if (!value) { setStatus('Сначала сгенерируйте приватный ключ.'); return; } try { await navigator.clipboard.writeText(value); setStatus('Приватный ключ скопирован.'); } catch { setStatus('Не удалось скопировать приватный ключ в этом браузере.'); } }); copyGeneratedPublicBtn.addEventListener('click', async () => { const value = String(generatedPublicInput.value || '').trim(); if (!value) { setStatus('Сначала сгенерируйте публичный ключ.'); return; } try { await navigator.clipboard.writeText(value); setStatus('Публичный ключ скопирован.'); } catch { setStatus('Не удалось скопировать публичный ключ в этом браузере.'); } }); const refreshBalance = async () => { if (!walletAddress) { setStatus('Кошелёк не инициализирован.'); return; } refreshBtn.disabled = true; try { const balance = await getBalanceSol({ endpoint: state.entrySettings.solanaServer, address: walletAddress, }); if (modeToken !== activeModeToken) return; balanceValue.textContent = `${formatSol(balance.sol, 6)} SOL`; updatedLabel.textContent = `Обновлено: ${nowRu()}`; endpointLabel.textContent = `RPC: ${balance.endpoint}`; setStatus('Баланс обновлён.'); } catch (error) { if (modeToken !== activeModeToken) return; setStatus(`Не удалось получить баланс: ${error?.message || 'unknown'}`); } finally { refreshBtn.disabled = false; } }; copyBtn.addEventListener('click', async () => { if (!walletAddress) return; try { await navigator.clipboard.writeText(walletAddress); setStatus('Адрес скопирован в буфер обмена'); } catch { setStatus('Не удалось скопировать адрес в этом браузере'); } }); refreshBtn.addEventListener('click', () => { void refreshBalance(); }); sendBtn.addEventListener('click', async () => { if (!walletCtx?.keypair) { setStatus('Перевод недоступен: wallet.key не загружен.'); return; } const toAddress = window.prompt('Введите адрес получателя (Solana):', ''); if (!toAddress) return; const amountRaw = window.prompt('Введите сумму SOL для перевода:', '0.01'); if (!amountRaw) return; sendBtn.disabled = true; try { const tx = await transferSol({ endpoint: state.entrySettings.solanaServer, fromKeypair: walletCtx.keypair, toAddress, amountSol: Number(String(amountRaw || '').replace(',', '.')), }); if (modeToken !== activeModeToken) return; setStatus(`Перевод отправлен. Signature: ${tx.signature}`); await refreshBalance(); } catch (error) { if (modeToken !== activeModeToken) return; setStatus(`Ошибка перевода: ${error?.message || 'unknown'}`); } finally { sendBtn.disabled = false; } }); topupBtn.addEventListener('click', async () => { if (!walletAddress) { setStatus('Кошелёк не инициализирован.'); return; } const openSite = window.confirm( 'Кнопка будет переводить на отдельный сайт пополнения.\n\nНажмите OK, чтобы открыть сайт.\nНажмите Отмена, чтобы выполнить тестовое пополнение (airdrop 1 SOL на DevNet).', ); if (openSite) { window.open(getTopupSiteUrl(walletAddress), '_blank', 'noopener,noreferrer'); setStatus('Открыт сайт пополнения. Пока также доступно тестовое пополнение.'); return; } topupBtn.disabled = true; try { const drop = await requestAirdropSol({ endpoint: state.entrySettings.solanaServer, address: walletAddress, amountSol: 1, }); if (modeToken !== activeModeToken) return; setStatus(`Тестовое пополнение выполнено (airdrop 1 SOL). Signature: ${drop.signature}`); await refreshBalance(); } catch (error) { if (modeToken !== activeModeToken) return; setStatus(`Ошибка тестового пополнения: ${error?.message || 'unknown'}`); } finally { topupBtn.disabled = false; } }); content.append(backBtn, card, actions, generatedCard); setStatus('Инициализация wallet.key...'); try { walletCtx = await getWalletFromStoredDeviceKey(sessionArgsOrThrow()); if (modeToken !== activeModeToken) return; walletAddress = walletCtx.address; addressEl.textContent = walletAddress; await refreshBalance(); } catch (error) { if (modeToken !== activeModeToken) return; addressEl.textContent = 'wallet.key недоступен'; setStatus(`Не удалось инициализировать кошелёк: ${error?.message || 'unknown'}`); } } async function renderArweaveWallet() { const modeToken = ++activeModeToken; content.innerHTML = ''; let walletAddress = ''; const backBtn = createModeBackButton(renderWalletChoice); const card = document.createElement('div'); card.className = 'card stack'; const balanceWrap = document.createElement('div'); const balanceLabel = document.createElement('p'); balanceLabel.className = 'meta-muted'; balanceLabel.textContent = 'Баланс (Arweave)'; const balanceValue = document.createElement('h2'); balanceValue.style.fontSize = '30px'; balanceValue.textContent = '— AR'; const updatedLabel = document.createElement('p'); updatedLabel.className = 'meta-muted'; updatedLabel.textContent = 'Обновлено: —'; const gatewayLabel = document.createElement('p'); gatewayLabel.className = 'meta-muted'; gatewayLabel.textContent = `Gateway: ${state.entrySettings.arweaveServer}`; balanceWrap.append(balanceLabel, balanceValue, updatedLabel, gatewayLabel); const addressCard = document.createElement('div'); addressCard.className = 'card'; addressCard.style.padding = '10px'; addressCard.innerHTML = `

Публичный адрес Arweave (SAWD-v1)

`; const addressEl = addressCard.querySelector('#wallet-address-value'); const helpCard = document.createElement('details'); helpCard.className = 'card'; helpCard.style.padding = '10px'; helpCard.innerHTML = ` Как получен этот адрес?

SHiNE берёт ваш локальный device.key и по стандарту SAWD-v1 получает из него нативный Arweave-кошелёк. Приватный ключ не отправляется на сервер. После первого расчёта он хранится только в зашифрованном контейнере этого устройства.

`; card.append(balanceWrap, addressCard, helpCard); const actions = document.createElement('div'); actions.className = 'stack'; actions.innerHTML = `
`; const copyBtn = actions.querySelector('#copy-address'); const refreshBtn = actions.querySelector('#refresh-balance'); const sendBtn = actions.querySelector('#send-ar'); const topupBtn = actions.querySelector('#topup-ar'); const refreshBalance = async () => { if (!walletAddress) { setStatus('Arweave-кошелёк не инициализирован.'); return; } refreshBtn.disabled = true; try { const balance = await getArweaveBalance({ gateway: state.entrySettings.arweaveServer, address: walletAddress, }); if (modeToken !== activeModeToken) return; balanceValue.textContent = `${formatAr(balance.ar, 6)} AR`; updatedLabel.textContent = `Обновлено: ${nowRu()}`; gatewayLabel.textContent = `Gateway: ${balance.gateway}`; setStatus('Баланс обновлён.'); } catch (error) { if (modeToken !== activeModeToken) return; setStatus(`Не удалось получить баланс: ${error?.message || 'unknown'}`); } finally { refreshBtn.disabled = false; } }; copyBtn.addEventListener('click', async () => { if (!walletAddress) return; try { await navigator.clipboard.writeText(walletAddress); setStatus('Адрес скопирован'); } catch { setStatus('Не удалось скопировать адрес в этом браузере'); } }); refreshBtn.addEventListener('click', () => { void refreshBalance(); }); sendBtn.addEventListener('click', async () => { if (!arweaveWalletCtx?.jwk) { setStatus('Перевод недоступен: Arweave-кошелёк не инициализирован.'); return; } const toAddress = window.prompt('Введите адрес получателя (Arweave):', ''); if (!toAddress) return; const amountRaw = window.prompt('Введите сумму AR для перевода:', '0.01'); if (!amountRaw) return; sendBtn.disabled = true; try { const tx = await transferAr({ gateway: state.entrySettings.arweaveServer, jwk: arweaveWalletCtx.jwk, toAddress, amountAr: amountRaw, }); if (modeToken !== activeModeToken) return; setStatus(`Перевод отправлен. Transaction ID: ${tx.id}`); await refreshBalance(); } catch (error) { if (modeToken !== activeModeToken) return; setStatus(`Ошибка перевода: ${error?.message || 'unknown'}`); } finally { sendBtn.disabled = false; } }); topupBtn.addEventListener('click', () => { window.open(getArweaveTopupSiteUrl(), '_blank', 'noopener,noreferrer'); setStatus('Открыта страница пополнения.'); }); content.append(backBtn, card, actions); setStatus('Генерация Arweave-кошелька...'); try { arweaveWalletCtx = await getArweaveWalletFromStoredDeviceKey({ ...sessionArgsOrThrow(), onStatus: (message) => { const text = String(message || '').trim(); if (!text) return; if (text.includes('впервые получаем Arweave-кошелёк')) { setStatus('Сейчас мы впервые получаем Arweave-кошелёк из вашего приватного device key. Это может занять немного времени. После этого кошелёк будет храниться только в зашифрованном контейнере этого устройства.'); return; } setStatus(text); }, }); if (modeToken !== activeModeToken) return; walletAddress = arweaveWalletCtx.address; addressEl.textContent = walletAddress; await refreshBalance(); } catch (error) { if (modeToken !== activeModeToken) return; addressEl.textContent = 'wallet.key недоступен'; clearArweaveSecretsInMemory(); setStatus(`Не удалось инициализировать Arweave-кошелёк: ${error?.message || 'unknown'}`); } } renderWalletChoice(); return screen; }