import { renderHeader } from '../components/header.js'; import { state } from '../state.js'; import { formatSol, getBalanceSol, getTopupSiteUrl, getWalletFromStoredDeviceKey, requestAirdropSol, transferSol, } from '../services/solana-wallet-service.js'; import { formatAr, getArweaveBalance, getArweaveTopupSiteUrl, getArweaveWalletFromStoredDeviceKey, transferAr, } from '../services/arweave-wallet-service.js'; export const pageMeta = { id: 'wallet-view', title: 'Кошелёк' }; function nowRu() { return new Date().toLocaleString('ru-RU'); } 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(); }); card.append(solanaBtn, arweaveBtn); content.append(card); setStatus('Выберите тип кошелька.'); } 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 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(), '_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); 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; }