import { createTopBar } from '../components/topbar.js'; import { authService, state } from '../state.js'; import { navigateBack } from '../router.js'; import { extractLoginFromBlockchainName } from '../services/shine-routes.js'; import { formatSol, getBalanceSol, getSolanaWalletFromStoredSecret, getWalletFromStoredClientKey, getWalletFromStoredRootKey, solanaAddressFromPublicKeyBase64, transferSol, } from '../services/solana-wallet-service.js'; import { showToast } from '../services/channels-ux.js'; import { toUserMessage } from '../services/ui-error-texts.js'; export const pageMeta = { id: 'channel-donate-view', title: 'Донат автору' }; function escapeHtml(text) { return String(text || '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function normalizeHash(hash) { const normalized = String(hash || '').trim().toLowerCase(); return normalized || '0'; } function toSafeInt(value) { if (value === '' || value == null) return null; const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } async function resolveChannelSelector(route) { const ownerRef = String(route?.params?.ownerBlockchainName || '').trim(); const rootNo = toSafeInt(route?.params?.channelRootBlockNumber); const rootHash = normalizeHash(route?.params?.channelRootBlockHash); const channelName = String(route?.params?.channelName || '').trim(); if (ownerRef && rootNo != null) { return { ownerBlockchainName: ownerRef, channelRootBlockNumber: rootNo, channelRootBlockHash: rootHash, channelName, }; } if (!ownerRef || !channelName) { throw new Error('Не удалось определить канал из адреса страницы.'); } const ownerLogin = extractLoginFromBlockchainName(ownerRef); const ownerUser = await authService.getUser(ownerLogin); if (!ownerUser?.exists) throw new Error('Владелец канала не найден.'); const ownerBlockchainName = String(ownerUser.blockchainName || ownerRef).trim(); const ownerFeed = await authService.listSubscriptionsFeed(ownerLogin, 500); const ownedRows = Array.isArray(ownerFeed?.ownedChannels) ? ownerFeed.ownedChannels : []; const row = ownedRows.find((item) => ( String(item?.channel?.channelName || '').trim().toLowerCase() === channelName.toLowerCase() && String(item?.channel?.ownerBlockchainName || '').trim().toLowerCase() === ownerBlockchainName.toLowerCase() )); if (!row?.channel?.channelRoot) throw new Error('Канал не найден.'); return { ownerBlockchainName, channelRootBlockNumber: Number(row.channel.channelRoot.blockNumber), channelRootBlockHash: normalizeHash(row.channel.channelRoot.blockHash), channelName, }; } function publicKeyChoice(ownerUser, field, id, label) { const publicKeyB64 = String(ownerUser?.[field] || '').trim(); if (!publicKeyB64) return null; try { return { id, label, address: solanaAddressFromPublicKeyBase64(publicKeyB64), }; } catch { return null; } } export function render({ navigate, route, chrome }) { const screen = document.createElement('section'); screen.className = 'stack channels-screen channels-screen--channel-donate'; const topbar = createTopBar({ title: 'Донат автору', back: { label: '←', onClick: () => { if (window.history.length > 1) { navigateBack(); return; } navigate('channels-list'); }, ariaLabel: 'Назад', title: 'Назад', }, }); chrome?.setTopbar(topbar); const card = document.createElement('div'); card.className = 'card stack channel-donate-card'; const content = document.createElement('div'); content.className = 'stack channel-donate-content'; content.innerHTML = '
'; card.append(content); screen.append(card); let disposed = false; let refreshSeq = 0; let busy = false; const walletCache = new Map(); const cleanupWallets = () => { for (const wallet of walletCache.values()) { try { wallet?.keypair?.secretKey?.fill?.(0); } catch { // Секретные ключи живут только в памяти этой страницы. } } walletCache.clear(); }; const renderDonationForm = ({ channel, selector, ownerUser }) => { const ownerLogin = String(channel?.ownerLogin || extractLoginFromBlockchainName(selector?.ownerBlockchainName) || '').trim(); const channelTitle = String(channel?.displayTitle || channel?.displayName || channel?.channelName || 'Канал').trim(); const channelName = String(channel?.channelName || selector?.channelName || '').trim(); const recipientChoices = [ publicKeyChoice(ownerUser, 'solanaKey', 'root-key', 'Root key'), publicKeyChoice(ownerUser, 'blockchainKey', 'blockchain-key', 'Blockchain key'), publicKeyChoice(ownerUser, 'clientKey', 'client-key', 'Client key'), ].filter(Boolean); content.innerHTML = `${escapeHtml(String(result.signature || ''))}
`;
}
showToast('Перевод отправлен');
const balance = await getBalanceSol({
endpoint: state.entrySettings.solanaServer,
address: wallet.address,
});
if (!disposed && balanceEl) balanceEl.textContent = `${formatSol(balance.sol)} SOL`;
} catch (error) {
if (!disposed) setStatus(toUserMessage(error, 'Не удалось выполнить перевод.'), 'error');
} finally {
if (!disposed) setBusy(false);
}
});
updateRecipientAddress();
if (!recipientChoices.length) {
setStatus('У владельца канала нет доступных публичных ключей для перевода.', 'error');
} else {
void refreshBalance();
}
};
const refresh = async () => {
const seq = ++refreshSeq;
cleanupWallets();
content.innerHTML = '';
try {
const selector = await resolveChannelSelector(route);
const payload = await authService.getChannelMessages(selector, 1, 'asc', String(state.session.login || '').trim());
if (disposed || seq !== refreshSeq) return;
const channel = payload?.channel || {};
const ownerLogin = String(channel?.ownerLogin || extractLoginFromBlockchainName(selector.ownerBlockchainName) || '').trim();
const ownerUser = await authService.getUser(ownerLogin);
if (disposed || seq !== refreshSeq) return;
if (!ownerUser?.exists) throw new Error('Владелец канала не найден.');
renderDonationForm({ channel, selector, ownerUser });
} catch (error) {
if (disposed || seq !== refreshSeq) return;
content.innerHTML = `
`;
content.querySelector('#channel-donate-retry')?.addEventListener('click', () => void refresh());
}
};
screen.refresh = refresh;
screen.cleanup = () => {
if (disposed) return;
disposed = true;
refreshSeq += 1;
cleanupWallets();
};
void refresh();
return screen;
}