import { getArweaveUploadPrice, loadArweaveFileMetadata, sha256HexFromArrayBuffer, uploadArweaveFile, validateArweaveTxId, } from '../services/arweave-file-service.js'; import { addArweaveWalletSecret, getArweaveBalance, getArweaveWalletChoices } from '../services/arweave-wallet-service.js'; import { escapeHtml, formatBytes, normalizeAttachment } from '../services/attachment-format.js'; const HISTORY_KEY = 'shine-ui-arweave-attachment-history-v1'; const MAX_HISTORY_ITEMS = 100; function readHistory(login) { try { const raw = sessionStorage.getItem(HISTORY_KEY); const parsed = raw ? JSON.parse(raw) : {}; const key = String(login || '').trim().toLowerCase(); const rows = Array.isArray(parsed?.[key]) ? parsed[key] : []; return rows.map((item) => normalizeAttachment(item)).filter(Boolean); } catch { return []; } } function saveHistory(login, rows) { try { const key = String(login || '').trim().toLowerCase(); const raw = sessionStorage.getItem(HISTORY_KEY); const parsed = raw ? JSON.parse(raw) : {}; parsed[key] = (Array.isArray(rows) ? rows : []).slice(0, MAX_HISTORY_ITEMS); sessionStorage.setItem(HISTORY_KEY, JSON.stringify(parsed)); } catch { // ignore sessionStorage errors } } function addToHistory(login, attachment) { const item = normalizeAttachment(attachment); const rows = readHistory(login).filter((row) => row.ar !== item.ar); rows.unshift(item); saveHistory(login, rows); return item; } function formatDateTime(ms) { const value = Number(ms || 0); if (!Number.isFinite(value) || value <= 0) return '—'; return new Date(value).toLocaleString('ru-RU'); } function setText(node, text) { if (node) node.textContent = String(text || ''); } function shortAddress(value) { const raw = String(value || '').trim(); if (raw.length <= 16) return raw; return `${raw.slice(0, 8)}...${raw.slice(-6)}`; } export function openArweaveAttachmentManager({ login, storagePwd, gateway, onSelect, } = {}) { const cleanLogin = String(login || '').trim(); const cleanStoragePwd = String(storagePwd || '').trim(); const cleanGateway = String(gateway || 'https://arweave.net').trim(); if (!cleanLogin || !cleanStoragePwd) return Promise.reject(new Error('Нет активной сессии.')); const root = document.createElement('div'); root.className = 'ar-attachment-manager-root'; document.body.append(root); let closed = false; let wallets = []; let selectedWalletId = 'derived-client-key'; let selectedFile = null; let selectedSha256 = ''; let priceInfo = null; let balanceInfo = null; let autoOpenedFileDialog = false; function selectedWallet() { return wallets.find((item) => String(item.id) === String(selectedWalletId)) || wallets[0] || null; } function close(resolve, result = null) { if (closed) return; closed = true; root.remove(); resolve(result); } function finish(resolve, attachment) { const item = addToHistory(cleanLogin, attachment); if (typeof onSelect === 'function') onSelect(item); close(resolve, item); } return new Promise((resolve) => { const bindBackdrop = () => { const modal = root.querySelector('[data-ar-attach-modal="true"]'); modal?.addEventListener('click', (event) => { if (event.target === modal) close(resolve, null); }); }; const renderWalletOptions = () => wallets.map((wallet) => ( `` )).join(''); const loadWallets = async (errorEl = null) => { try { wallets = await getArweaveWalletChoices({ login: cleanLogin, storagePwd: cleanStoragePwd }); if (!wallets.some((item) => String(item.id) === String(selectedWalletId))) { selectedWalletId = String(wallets[0]?.id || 'derived-client-key'); } } catch (error) { setText(errorEl, error?.message || 'Не удалось загрузить Arweave-кошельки.'); } }; const showUpload = async () => { root.innerHTML = ` `; bindBackdrop(); const walletEl = root.querySelector('#ar-attach-wallet'); const fileEl = root.querySelector('#ar-attach-file'); const metaEl = root.querySelector('[data-meta="true"]'); const errorEl = root.querySelector('[data-error="true"]'); const uploadBtn = root.querySelector('[data-action="upload"]'); await loadWallets(errorEl); if (walletEl) { walletEl.innerHTML = renderWalletOptions(); walletEl.value = selectedWalletId; walletEl.addEventListener('change', () => { selectedWalletId = String(walletEl.value || ''); uploadBtn.disabled = true; selectedFile = null; selectedSha256 = ''; priceInfo = null; balanceInfo = null; if (fileEl) fileEl.value = ''; setText(metaEl, ''); }); } root.querySelector('[data-action="cancel"]')?.addEventListener('click', () => close(resolve, null)); root.querySelector('[data-action="existing"]')?.addEventListener('click', showExisting); root.querySelector('[data-action="history"]')?.addEventListener('click', showHistory); root.querySelector('[data-action="add-wallet"]')?.addEventListener('click', showAddWallet); fileEl?.addEventListener('change', async () => { selectedFile = fileEl.files?.[0] || null; selectedSha256 = ''; priceInfo = null; balanceInfo = null; uploadBtn.disabled = true; setText(errorEl, ''); setText(metaEl, ''); if (!selectedFile) return; try { const wallet = selectedWallet(); if (!wallet?.address) throw new Error('Выберите Arweave-кошелёк.'); const buffer = await selectedFile.arrayBuffer(); selectedSha256 = await sha256HexFromArrayBuffer(buffer); priceInfo = await getArweaveUploadPrice({ gateway: cleanGateway, byteLength: selectedFile.size }); balanceInfo = await getArweaveBalance({ gateway: cleanGateway, address: wallet.address }); const hasFunds = BigInt(balanceInfo.winston) >= BigInt(priceInfo.winston); metaEl.innerHTML = `
Имя: ${escapeHtml(selectedFile.name || 'file')}
Размер: ${escapeHtml(formatBytes(selectedFile.size))}
SHA-256: ${escapeHtml(selectedSha256)}
Цена: ${escapeHtml(Number(priceInfo.ar).toLocaleString('ru-RU', { maximumFractionDigits: 6 }))} AR
Баланс кошелька: ${escapeHtml(Number(balanceInfo.ar).toLocaleString('ru-RU', { maximumFractionDigits: 6 }))} AR
`; if (!hasFunds) { setText(errorEl, 'Недостаточно AR на выбранном кошельке.'); return; } uploadBtn.disabled = false; } catch (error) { setText(errorEl, error?.message || 'Не удалось подготовить файл.'); } }); uploadBtn?.addEventListener('click', async () => { if (!selectedFile || !selectedSha256) { setText(errorEl, 'Выберите файл.'); return; } const wallet = selectedWallet(); if (!wallet?.jwk) { setText(errorEl, 'Выберите Arweave-кошелёк.'); return; } uploadBtn.disabled = true; setText(errorEl, 'Загружаем файл в Arweave...'); try { const uploaded = await uploadArweaveFile({ gateway: cleanGateway, jwk: wallet.jwk, file: selectedFile, shineType: 'attachment', tags: [ { name: 'SHiNE-Login', value: cleanLogin }, { name: 'SHiNE-Attachment-Name', value: selectedFile.name || 'file' }, ], }); finish(resolve, { name: selectedFile.name || 'file', size: selectedFile.size, sha256: selectedSha256, ar: uploaded.id, uploadedAtMs: Date.now(), }); } catch (error) { uploadBtn.disabled = false; setText(errorEl, error?.message || 'Не удалось загрузить файл в Arweave.'); } }); if (!autoOpenedFileDialog) { autoOpenedFileDialog = true; window.setTimeout(() => fileEl?.click(), 0); } }; const showExisting = () => { root.innerHTML = ` `; bindBackdrop(); const txEl = root.querySelector('#ar-existing-txid'); const nameEl = root.querySelector('#ar-existing-name'); const metaEl = root.querySelector('[data-meta="true"]'); const errorEl = root.querySelector('[data-error="true"]'); const checkBtn = root.querySelector('[data-action="check"]'); root.querySelector('[data-action="back"]')?.addEventListener('click', showUpload); checkBtn?.addEventListener('click', async () => { const txId = String(txEl?.value || '').trim(); const name = String(nameEl?.value || '').trim() || 'arweave-file'; if (!validateArweaveTxId(txId)) { setText(errorEl, 'Некорректный Transaction ID Arweave.'); return; } checkBtn.disabled = true; setText(errorEl, 'Скачиваем файл и считаем SHA-256...'); setText(metaEl, ''); try { const meta = await loadArweaveFileMetadata({ gateway: cleanGateway, txId }); const item = normalizeAttachment({ name, size: meta.sizeBytes, sha256: meta.sha256Hex, ar: txId, uploadedAtMs: Date.now(), }); metaEl.innerHTML = `
Размер: ${escapeHtml(formatBytes(item.size))}
SHA-256: ${escapeHtml(item.sha256)}
`; setText(errorEl, ''); const addBtn = document.createElement('button'); addBtn.className = 'primary-btn'; addBtn.type = 'button'; addBtn.textContent = 'Добавить в сообщение'; addBtn.addEventListener('click', () => finish(resolve, item)); metaEl.append(addBtn); } catch (error) { setText(errorEl, error?.message || 'Не удалось проверить файл Arweave.'); checkBtn.disabled = false; } }); window.setTimeout(() => txEl?.focus(), 0); }; const showHistory = () => { const rows = readHistory(cleanLogin); root.innerHTML = ` `; bindBackdrop(); root.querySelector('[data-action="back"]')?.addEventListener('click', showUpload); root.querySelectorAll('[data-pick]').forEach((button) => { button.addEventListener('click', () => { const index = Number(button.getAttribute('data-pick')); const item = rows[index]; if (item) finish(resolve, item); }); }); }; const showAddWallet = () => { root.innerHTML = ` `; bindBackdrop(); const labelEl = root.querySelector('#ar-wallet-label'); const secretEl = root.querySelector('#ar-wallet-secret'); const errorEl = root.querySelector('[data-error="true"]'); const saveBtn = root.querySelector('[data-action="save"]'); root.querySelector('[data-action="back"]')?.addEventListener('click', showUpload); saveBtn?.addEventListener('click', async () => { saveBtn.disabled = true; setText(errorEl, ''); try { const wallet = await addArweaveWalletSecret({ login: cleanLogin, storagePwd: cleanStoragePwd, label: String(labelEl?.value || '').trim(), secret: String(secretEl?.value || '').trim(), }); selectedWalletId = wallet.id; await showUpload(); } catch (error) { saveBtn.disabled = false; setText(errorEl, error?.message || 'Не удалось добавить Arweave-кошелёк.'); } }); }; void showUpload(); }); }