import { createRequesterPairingMaterial, decryptPairingPayloadFromEnvelope, deriveEspPairingPasswordHash } from './js/lib/device-pairing.js'; import { loadPluginSettings, loadSessionMaterial, savePluginSettings, saveSessionMaterial, clearSessionMaterial } from './js/lib/session-store.js'; import { ShineApiClient } from './js/lib/shine-api.js'; import { DEFAULT_SHINE_SERVER_LOGIN, buildHttpBase, readWalletProfileByLogin, resolveShineServerByUserLogin, } from './js/lib/shine-server-resolver.js'; const state = { api: null, settings: { serverLogin: DEFAULT_SHINE_SERVER_LOGIN, serverHttp: buildHttpBase('shineup.me'), serverUrl: 'wss://shineup.me/ws', login: '', }, requesterMaterial: null, pairingId: '', expiresAtMs: 0, shortCode: '', trustedSessionOnline: false, pollTimer: 0, activeSession: null, connectionOnline: false, walletProfile: null, signing: { selectedDeviceName: '', devicesResolvedAtMs: 0, }, currentWallet: null, pendingApprovals: [], siteApprovalChain: Promise.resolve(), sessionAttachInProgress: false, statusText: '', statusKind: 'info', }; const WALLET_RPC_REQUEST_TYPE = 9100; const WALLET_RPC_RESPONSE_TYPE = 9101; async function configureSidePanelBehavior() { if (!chrome.sidePanel?.setPanelBehavior) { return; } try { await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); } catch (error) { console.warn('Failed to configure SHiNE side panel behavior:', error); } } function normalizeOrigin(value = '') { const raw = String(value || '').trim(); if (!raw) return ''; try { return new URL(raw).origin; } catch { return raw; } } function makeCodeError(message, code) { const error = new Error(String(message || 'Wallet error')); error.code = String(code || '').trim().toUpperCase(); return error; } function setStatus(message = '', kind = 'info') { state.statusText = String(message || ''); state.statusKind = kind === 'error' ? 'error' : 'info'; } function makePendingApprovalSnapshot(payload = {}) { const pendingId = String(payload?.id || '').trim(); const pendingApprovals = Array.isArray(state.pendingApprovals) ? state.pendingApprovals : []; const queueIndex = pendingApprovals.findIndex((item) => String(item?.id || '').trim() === pendingId); const queueLength = pendingApprovals.length; return { id: pendingId, kind: String(payload?.kind || 'sign_transaction').trim() || 'sign_transaction', origin: String(payload?.origin || '').trim(), publicKeyBase58: String(payload?.publicKeyBase58 || '').trim(), comment: String(payload?.comment || '').trim(), createdAtMs: Number(payload?.createdAtMs || Date.now()), status: String(payload?.status || 'queued').trim() || 'queued', queuePosition: queueIndex >= 0 ? queueIndex + 1 : 1, queueLength: queueLength || 1, transactionSummary: payload?.transactionSummary && typeof payload.transactionSummary === 'object' ? { ...payload.transactionSummary } : null, }; } function getCurrentPendingApproval() { return Array.isArray(state.pendingApprovals) && state.pendingApprovals.length ? state.pendingApprovals[0] : null; } function removePendingApproval(pendingId, { rejectError = null } = {}) { const pendingApprovals = Array.isArray(state.pendingApprovals) ? state.pendingApprovals : []; const index = pendingApprovals.findIndex((item) => String(item?.id || '').trim() === String(pendingId || '').trim()); if (index < 0) return; const [pending] = pendingApprovals.splice(index, 1); if (pending.timeoutId) { clearTimeout(pending.timeoutId); } if (rejectError && pending.abortController) { try { pending.abortController.abort(rejectError); } catch {} } } async function openSidePanelForSender(sender) { if (!chrome.sidePanel?.open || !sender?.tab?.id) return; try { await chrome.sidePanel.open({ tabId: sender.tab.id }); } catch {} } function stopPoll() { if (state.pollTimer) { clearTimeout(state.pollTimer); state.pollTimer = 0; } } function clearPairingState() { stopPoll(); state.requesterMaterial = null; state.pairingId = ''; state.expiresAtMs = 0; state.shortCode = ''; state.trustedSessionOnline = false; } function ensureApi(serverUrl = state.settings.serverUrl) { const normalized = String(serverUrl || '').trim() || 'wss://shineup.me/ws'; if (!state.api || state.api.serverUrl !== normalized) { state.api?.close(); state.api = new ShineApiClient(normalized); } return state.api; } async function loadStateFromStorage() { const settings = await loadPluginSettings(); state.settings = { serverLogin: String(settings?.serverLogin || state.settings.serverLogin || DEFAULT_SHINE_SERVER_LOGIN).trim(), serverHttp: String(settings?.serverHttp || state.settings.serverHttp || buildHttpBase('shineup.me')).trim() || buildHttpBase('shineup.me'), serverUrl: String(settings?.serverUrl || state.settings.serverUrl || 'wss://shineup.me/ws').trim() || 'wss://shineup.me/ws', login: String(settings?.login || '').trim(), connectedOrigins: Array.isArray(settings?.connectedOrigins) ? settings.connectedOrigins.map((item) => normalizeOrigin(item)).filter(Boolean) : [], }; const storedSession = await loadSessionMaterial(); if (storedSession || !state.sessionAttachInProgress) { state.activeSession = storedSession; } state.walletProfile = state.activeSession?.walletProfile || null; state.signing = { selectedDeviceName: String(state.activeSession?.selectedDeviceName || ''), devicesResolvedAtMs: Number(state.activeSession?.devicesResolvedAtMs || 0), }; state.currentWallet = state.activeSession?.currentWallet || null; } async function persistSettings(nextSettings = {}) { state.settings = { ...state.settings, ...nextSettings, }; if (!Array.isArray(state.settings.connectedOrigins)) { state.settings.connectedOrigins = []; } await savePluginSettings(state.settings); return state.settings; } function isOriginApproved(origin) { const normalized = normalizeOrigin(origin); return !!normalized && Array.isArray(state.settings.connectedOrigins) && state.settings.connectedOrigins.includes(normalized); } async function setOriginApproved(origin, approved) { const normalized = normalizeOrigin(origin); const current = new Set(Array.isArray(state.settings.connectedOrigins) ? state.settings.connectedOrigins : []); if (approved) { if (normalized) current.add(normalized); } else if (normalized) { current.delete(normalized); } await persistSettings({ connectedOrigins: [...current] }); } async function resolveServerForLogin(login) { const cleanLogin = String(login || state.settings.login || '').trim(); if (!cleanLogin) { state.settings = { ...state.settings, login: '', serverLogin: '', }; await savePluginSettings(state.settings); return { ok: true, resolved: false }; } const resolved = await resolveShineServerByUserLogin(cleanLogin); state.settings = { ...state.settings, login: cleanLogin, serverLogin: resolved.serverLogin, serverHttp: resolved.serverHttp, serverUrl: resolved.serverUrl, }; await savePluginSettings(state.settings); return { ok: true, resolved: true, ...resolved }; } async function saveActiveSessionRecord() { if (!state.activeSession) return; const nextRecord = { ...state.activeSession, walletProfile: state.walletProfile, selectedDeviceName: state.signing.selectedDeviceName, devicesResolvedAtMs: state.signing.devicesResolvedAtMs, currentWallet: state.currentWallet, }; state.activeSession = nextRecord; await saveSessionMaterial(nextRecord); } function shortKey(value = '', size = 10) { const raw = String(value || '').trim(); return raw ? raw.slice(0, size) : ''; } function extractErrorCode(message = '') { const match = String(message || '').match(/\(([A-Z0-9_]+)\)\s*$/i); return match ? String(match[1]).toUpperCase() : ''; } function toWalletErrorMessage(error, fallback = 'Не удалось выполнить операцию кошелька.') { const raw = String(error?.message || '').trim(); const code = String(error?.code || extractErrorCode(raw) || '').toUpperCase(); if (code === 'PAIRING_NOT_AVAILABLE') { return 'Для этого логина ещё не включено подключение по коду. На доверенном устройстве откройте «Подключить по коду» и нажмите «Включить подключение по коду» или задайте дополнительный пароль.'; } if (code === 'PAIRING_NO_TRUSTED_SESSION_ONLINE') { return 'Сейчас нет ни одной онлайн доверенной сессии этого пользователя. Откройте SHiNE на другом уже подключённом устройстве и держите его в сети.'; } if (code === 'PAIRING_PASSWORD_INVALID') { return 'Дополнительный пароль подключения не подходит.'; } return raw || fallback; } function homeserverSessionNameFromClientInfo(value = '') { const raw = String(value || '').trim(); const match = raw.match(/^ESP32 homeserver:(.+)$/i); return match ? String(match[1] || '').trim() : ''; } function mergeHomeserverStatuses(publishedHomeservers = [], serverSessions = []) { const published = Array.isArray(publishedHomeservers) ? publishedHomeservers : []; const homeserverSessions = Array.isArray(serverSessions) ? serverSessions.filter((item) => Number(item?.sessionType || 0) === 100) : []; const onlineHomeservers = homeserverSessions.filter((item) => !!item?.onlineOnThisServer); const byName = new Map(); onlineHomeservers.forEach((item) => { const sessionName = homeserverSessionNameFromClientInfo(item?.clientInfoFromClient); if (sessionName) { byName.set(sessionName, item); } }); return published.map((item) => { const matched = byName.get(String(item?.sessionName || '').trim()) || null; let onlineState = matched ? 'online' : 'offline'; let activeSessionId = matched?.sessionId ? String(matched.sessionId) : ''; if (!matched && published.length === 1 && onlineHomeservers.length === 1) { onlineState = 'online'; activeSessionId = String(onlineHomeservers[0]?.sessionId || ''); } return { ...item, activeSessionId, onlineState, onlineLabel: onlineState === 'online' ? 'online' : onlineState === 'offline' ? 'offline' : 'unknown', }; }); } async function hydrateWalletProfile(login) { const cleanLogin = String(login || state.activeSession?.login || state.settings.login || '').trim(); if (!cleanLogin) throw new Error('Нет логина для чтения PDA кошелька.'); const profile = await readWalletProfileByLogin(cleanLogin); const selectedDeviceName = state.signing.selectedDeviceName || String(profile?.homeserverSessions?.[0]?.sessionName || ''); state.walletProfile = { ...profile, homeserverSessions: Array.isArray(profile.homeserverSessions) ? profile.homeserverSessions.map((item) => ({ ...item, onlineState: 'unknown', onlineLabel: 'unknown', activeSessionId: '', })) : [], }; state.signing = { ...state.signing, selectedDeviceName, }; await saveActiveSessionRecord(); return state.walletProfile; } async function resumeActiveSession({ keepConnected = false } = {}) { const sessionRecord = await loadSessionMaterial(); state.activeSession = sessionRecord; if (!sessionRecord) { state.connectionOnline = false; setStatus('Wallet-session ещё не подключена.', 'info'); return { ok: true, connected: false }; } try { await persistSettings({ serverLogin: String(sessionRecord?.serverLogin || state.settings.serverLogin || DEFAULT_SHINE_SERVER_LOGIN).trim(), serverHttp: String(sessionRecord?.serverHttp || state.settings.serverHttp || buildHttpBase('shineup.me')).trim(), serverUrl: String(sessionRecord?.serverUrl || state.settings.serverUrl || 'wss://shineup.me/ws').trim(), login: String(sessionRecord?.login || state.settings.login || '').trim(), }); const resumed = await ensureApi().resumeSession(sessionRecord); state.connectionOnline = !!keepConnected; if (!keepConnected) { ensureApi().close(); state.api = null; setStatus(`Wallet-session сохранена для @${resumed.login}. Подключение будет открываться только по действию.`, 'info'); } else { setStatus(`Wallet-session активна для @${resumed.login}.`, 'info'); } return { ok: true, connected: true, login: resumed.login, sessionId: resumed.sessionId }; } catch (error) { state.connectionOnline = false; setStatus(error.message || 'Не удалось восстановить wallet-session.', 'error'); return { ok: false, connected: false, error: state.statusText }; } } async function attachApprovedSession(payload) { if (String(payload?.type || '') !== 'shine-esp-session-attach') { throw new Error('Доверенное устройство вернуло неподдерживаемый payload. Для plugin нужен session-only approve.'); } const login = String(payload?.login || state.settings.login || '').trim(); const approvedSession = payload?.session || {}; const sessionRecord = { login, sessionId: String(approvedSession?.sessionId || '').trim(), sessionKey: state.requesterMaterial?.sessionKey || '', sessionPrivPkcs8: state.requesterMaterial?.sessionPrivPkcs8 || '', sessionType: Number(approvedSession?.sessionType || 50) || 50, serverLogin: state.settings.serverLogin, serverHttp: state.settings.serverHttp, serverUrl: state.settings.serverUrl, }; if (!sessionRecord.login || !sessionRecord.sessionId || !sessionRecord.sessionKey || !sessionRecord.sessionPrivPkcs8) { throw new Error('Получен неполный session-only payload'); } state.sessionAttachInProgress = true; try { state.activeSession = sessionRecord; state.walletProfile = null; state.currentWallet = null; state.signing = { ...state.signing, selectedDeviceName: '', devicesResolvedAtMs: 0, }; await saveActiveSessionRecord(); await hydrateWalletProfile(login); await saveActiveSessionRecord(); await persistSettings({ login: sessionRecord.login, serverLogin: sessionRecord.serverLogin, serverHttp: sessionRecord.serverHttp, serverUrl: sessionRecord.serverUrl, }); state.connectionOnline = false; state.currentWallet = null; } finally { state.sessionAttachInProgress = false; } } async function pollPairingStatus() { if (!state.pairingId || !state.requesterMaterial) return; try { const payload = await ensureApi().getTrustedDeviceLoginStatus(state.pairingId); const stateValue = String(payload?.state || ''); if (stateValue === 'created') { state.pollTimer = setTimeout(() => { void pollPairingStatus(); }, 2200); return; } if (stateValue === 'approved') { const decoded = await decryptPairingPayloadFromEnvelope(payload?.encryptedPayload, state.requesterMaterial); await attachApprovedSession(decoded); clearPairingState(); setStatus('Wallet-session создана и сохранена. Кошелёк остаётся офлайн до запроса подписи.', 'info'); return; } if (stateValue === 'rejected') { clearPairingState(); setStatus('Заявка отклонена на доверенном устройстве.', 'error'); return; } if (stateValue === 'expired' || stateValue === 'canceled') { clearPairingState(); setStatus('Ожидание подключения завершено.', 'error'); return; } state.pollTimer = setTimeout(() => { void pollPairingStatus(); }, 2200); } catch (error) { clearPairingState(); setStatus(error.message || 'Не удалось проверить pairing-статус.', 'error'); } } async function startPairing({ login, usePassword, password }) { const cleanLogin = String(login || '').trim(); if (!cleanLogin) { throw new Error('Введите логин.'); } await persistSettings({ login: cleanLogin }); await resolveServerForLogin(cleanLogin); clearPairingState(); setStatus('Проверяем пользователя и создаём wallet-session заявку...', 'info'); const api = ensureApi(); const user = await api.getUser(cleanLogin); if (user?.exists !== true) { throw new Error('Пользователь не найден.'); } state.requesterMaterial = await createRequesterPairingMaterial(); const passwordHash = usePassword ? await deriveEspPairingPasswordHash(cleanLogin, String(password || '')) : ''; const payload = await api.startTrustedDeviceLogin({ login: cleanLogin, passwordHash, requesterSessionKey: state.requesterMaterial.sessionKey, payloadType: 1, }); state.pairingId = String(payload?.pairingId || '').trim(); state.expiresAtMs = Number(payload?.expiresAtMs || 0); state.shortCode = String(payload?.shortCode || ''); state.trustedSessionOnline = !!payload?.trustedSessionOnline; if (!state.pairingId) { throw new Error('Сервер не вернул pairingId.'); } state.pollTimer = setTimeout(() => { void pollPairingStatus(); }, 1800); setStatus('Wallet-session заявка создана. Ожидаем подтверждение на доверенном устройстве.', 'info'); return { pairingId: state.pairingId, shortCode: String(payload?.shortCode || ''), expiresAtMs: state.expiresAtMs, trustedSessionOnline: !!payload?.trustedSessionOnline, }; } async function cancelPairing() { if (!state.pairingId || !state.requesterMaterial?.sessionKey) { clearPairingState(); return { ok: true }; } await ensureApi().cancelTrustedDeviceLogin(state.pairingId, state.requesterMaterial.sessionKey); clearPairingState(); setStatus('Ожидание подключения отменено.', 'info'); return { ok: true }; } async function disconnectSession() { ensureApi().close(); state.api = null; await clearSessionMaterial(); state.activeSession = null; state.connectionOnline = false; state.walletProfile = null; state.signing = { selectedDeviceName: '', devicesResolvedAtMs: 0, }; state.currentWallet = null; setStatus('Сохранённая wallet-session удалена из plugin.', 'info'); return { ok: true }; } async function refreshWalletDevices() { if (!state.activeSession?.login) { throw new Error('Сначала подключите wallet-session.'); } await hydrateWalletProfile(state.activeSession.login); const resumed = await resumeActiveSession({ keepConnected: true }); if (!resumed.ok) { throw new Error(resumed.error || 'Не удалось открыть wallet-session.'); } try { const sessions = await ensureApi().listSessions(); state.walletProfile = { ...state.walletProfile, homeserverSessions: mergeHomeserverStatuses(state.walletProfile?.homeserverSessions, sessions), }; state.signing.devicesResolvedAtMs = Date.now(); if (!state.signing.selectedDeviceName && state.walletProfile.homeserverSessions[0]?.sessionName) { state.signing.selectedDeviceName = state.walletProfile.homeserverSessions[0].sessionName; } await saveActiveSessionRecord(); setStatus('Список доверенных homeserver-устройств обновлён.', 'info'); return { ok: true, devices: state.walletProfile.homeserverSessions, }; } finally { ensureApi().close(); state.api = null; state.connectionOnline = false; } } async function updateSigningSelection({ selectedDeviceName } = {}) { state.signing = { ...state.signing, selectedDeviceName: String(selectedDeviceName || state.signing.selectedDeviceName || ''), }; await saveActiveSessionRecord(); return { ok: true }; } async function resolveSelectedHomeserverSession() { if (!state.activeSession?.login) { throw new Error('Сначала подключите wallet-session.'); } if (!state.signing.selectedDeviceName) { throw new Error('Не выбрано устройство homeserver.'); } let selectedDevice = (state.walletProfile?.homeserverSessions || []).find((item) => item.sessionName === state.signing.selectedDeviceName); if (!selectedDevice) { throw new Error('Выбранное устройство не найдено в PDA аккаунта.'); } if (!selectedDevice.activeSessionId) { await refreshWalletDevices(); selectedDevice = (state.walletProfile?.homeserverSessions || []).find((item) => item.sessionName === state.signing.selectedDeviceName); } if (!selectedDevice?.activeSessionId) { throw new Error('Выбранный homeserver сейчас не найден онлайн на сервере SHiNE.'); } return selectedDevice; } async function callWalletRpc(requestData, timeoutMs = 8000, abortSignal = null) { const selectedDevice = await resolveSelectedHomeserverSession(); const resumed = await resumeActiveSession({ keepConnected: true }); if (!resumed.ok) { throw new Error(resumed.error || 'Не удалось открыть wallet-session.'); } const requestId = String(requestData?.requestId || `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`); const callId = `wallet-rpc-${requestId}`; const payload = { ...requestData, requestId, timeMs: Number(requestData?.timeMs || Date.now()), }; try { const response = await new Promise((resolve, reject) => { let settled = false; let timeoutId = 0; let off = () => {}; let removeAbortListener = () => {}; const cleanup = () => { if (settled) return; settled = true; if (timeoutId) clearTimeout(timeoutId); off(); removeAbortListener(); }; timeoutId = setTimeout(() => { cleanup(); reject(new Error('Таймаут ответа от ESP32.')); }, timeoutMs); off = ensureApi().onEvent('IncomingCallSignal', (evt) => { const eventPayload = evt?.payload || {}; if (String(eventPayload?.callId || '') !== callId) return; if (Number(eventPayload?.type || 0) !== WALLET_RPC_RESPONSE_TYPE) return; cleanup(); try { resolve(JSON.parse(String(eventPayload?.data || '{}'))); } catch { reject(new Error('ESP32 вернул некорректный JSON.')); } }); if (abortSignal) { const onAbort = () => { cleanup(); reject(abortSignal.reason instanceof Error ? abortSignal.reason : new Error('Ожидание подписи отменено.')); }; if (abortSignal.aborted) { onAbort(); return; } abortSignal.addEventListener('abort', onAbort, { once: true }); removeAbortListener = () => { abortSignal.removeEventListener('abort', onAbort); }; } ensureApi().callSignalToSession({ toLogin: state.activeSession.login, targetSessionId: selectedDevice.activeSessionId, callId, type: WALLET_RPC_REQUEST_TYPE, data: JSON.stringify(payload), }).catch((error) => { cleanup(); reject(error); }); }); return { response, selectedDevice, requestId }; } finally { ensureApi().close(); state.api = null; state.connectionOnline = false; } } function verifyWalletAgainstPda(wallet) { const type = String(wallet?.type || '').trim(); const pub = String(wallet?.publicKeyBase58 || '').trim(); const rootKey = String(state.walletProfile?.publicKeys?.rootKeyBase58 || '').trim(); const clientKey = String( state.walletProfile?.publicKeys?.clientKeyBase58 || '', ).trim(); if (type === 'client.key') { return { verified: !!clientKey && clientKey === pub, verificationText: clientKey === pub ? 'Совпадает с clientKey из PDA.' : 'Не совпадает с clientKey из PDA.', }; } if (type === 'root.key') { return { verified: !!rootKey && rootKey === pub, verificationText: rootKey === pub ? 'Совпадает с rootKey из PDA.' : 'Не совпадает с rootKey из PDA.', }; } return { verified: null, verificationText: 'Для custom-кошелька проверка через PDA пока не выполняется.', }; } async function requestCurrentWallet() { const { response, selectedDevice, requestId } = await callWalletRpc({ v: 1, operation: 'get_wallet_public_key', requestId: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`, }); if (!response?.ok) { throw new Error(`ESP32 отклонил запрос: ${String(response?.error || 'unknown_error')}`); } state.currentWallet = { type: String(response?.wallet?.type || '').trim(), publicKeyBase58: String(response?.wallet?.publicKeyBase58 || '').trim(), homeserverName: selectedDevice.sessionName, requestId: String(response?.requestId || requestId), timeMs: Number(response?.timeMs || 0), ...verifyWalletAgainstPda(response?.wallet || {}), }; await saveActiveSessionRecord(); setStatus(`Кошелёк получен с ${selectedDevice.sessionName}.`, 'info'); return { ok: true, wallet: state.currentWallet }; } async function cancelPendingSiteApproval() { const pending = getCurrentPendingApproval(); if (!pending) { setStatus('Сейчас нет активного ожидания подписи.', 'info'); return { ok: true }; } removePendingApproval(pending.id, { rejectError: makeCodeError('User canceled request in extension.', 'USER_REJECTED'), }); setStatus('Ожидание подписи отменено в расширении.', 'info'); return { ok: true }; } async function markPendingSiteApprovalResolved(pendingId) { removePendingApproval(pendingId); } function enqueueSiteApproval(work) { const run = state.siteApprovalChain.then(work, work); state.siteApprovalChain = run.catch(() => {}); return run; } async function activatePendingApproval(pending, sender = null) { const abortController = new AbortController(); pending.status = 'active'; pending.abortController = abortController; pending.timeoutId = setTimeout(() => { removePendingApproval(pending.id, { rejectError: makeCodeError('Signing request timed out in extension.', 'USER_REJECTED'), }); setStatus('Ожидание подписи истекло в расширении.', 'error'); }, 120000); setStatus(`Сайт ${pending.origin} запросил подпись. Подтвердите или отмените на доверенном устройстве.`, 'info'); await openSidePanelForSender(sender); return pending; } function beginSiteTransactionFlow(payload = {}) { const pending = makePendingApprovalSnapshot({ ...payload, kind: 'sign_transaction', id: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`, createdAtMs: Date.now(), status: 'queued', }); state.pendingApprovals.push({ ...pending, timeoutId: 0, abortController: null, }); return pending; } async function siteConnect({ origin, onlyIfTrusted = false } = {}) { const normalizedOrigin = normalizeOrigin(origin); if (!normalizedOrigin) { throw makeCodeError('Site origin is missing.', 'BAD_ORIGIN'); } if (onlyIfTrusted && !isOriginApproved(normalizedOrigin)) { throw makeCodeError('Site is not trusted yet.', 'NOT_TRUSTED'); } const result = await requestCurrentWallet(); const publicKeyBase58 = String(result?.wallet?.publicKeyBase58 || '').trim(); if (!publicKeyBase58) { throw makeCodeError('Wallet public key is not available.', 'WALLET_UNAVAILABLE'); } if (!isOriginApproved(normalizedOrigin)) { await setOriginApproved(normalizedOrigin, true); } setStatus(`Site ${normalizedOrigin} connected to ${shortKey(publicKeyBase58, 8)}.`, 'info'); return { ok: true, publicKeyBase58, walletType: String(result?.wallet?.type || '').trim(), }; } async function siteDisconnect({ origin } = {}) { const normalizedOrigin = normalizeOrigin(origin); setStatus(normalizedOrigin ? `Site ${normalizedOrigin} disconnected.` : 'Site disconnected.', 'info'); return { ok: true }; } async function siteSignTransaction({ origin, publicKeyBase58, transactionBase64, comment, transactionSummary } = {}, sender = null) { const normalizedOrigin = normalizeOrigin(origin); if (!normalizedOrigin) { throw makeCodeError('Site origin is missing.', 'BAD_ORIGIN'); } if (!isOriginApproved(normalizedOrigin)) { throw makeCodeError('Site is not trusted yet.', 'NOT_TRUSTED'); } const cleanPub = String(publicKeyBase58 || '').trim(); const cleanTx = String(transactionBase64 || '').trim(); if (!cleanPub || !cleanTx) { throw makeCodeError('Transaction payload is incomplete.', 'BAD_REQUEST'); } const pending = beginSiteTransactionFlow({ origin: normalizedOrigin, publicKeyBase58: cleanPub, comment: String(comment || '').trim(), transactionSummary: transactionSummary || null, }); return enqueueSiteApproval(async () => { await activatePendingApproval(getCurrentPendingApproval() || pending, sender); const activePending = getCurrentPendingApproval() || pending; const requestId = `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`; const signComment = String(comment || '').trim() || `Site ${normalizedOrigin} requested transaction signature`; try { const { response } = await callWalletRpc({ v: 1, operation: 'sign_transaction', requestId, publicKeyBase58: cleanPub, transactionBase64: cleanTx, comment: signComment, }, 120000, activePending.abortController?.signal || null); if (!response?.ok) { const errorCode = String(response?.error || 'unknown_error').trim().toUpperCase(); if (errorCode === 'REJECTED_BY_USER') { throw makeCodeError('User rejected transaction signature on ESP32.', 'USER_REJECTED'); } throw makeCodeError(`ESP32 rejected transaction signature: ${String(response?.error || 'unknown_error')}`, errorCode || 'RPC_REJECTED'); } setStatus(`Подпись для ${normalizedOrigin} завершена.`, 'info'); return { ok: true, publicKeyBase58: String(response?.publicKeyBase58 || cleanPub).trim(), signedTransactionBase64: String(response?.signedTransactionBase64 || '').trim(), signatureBase58: String(response?.signatureBase58 || '').trim(), }; } finally { await markPendingSiteApprovalResolved(activePending.id); } }); } function snapshot() { return { settings: { ...state.settings }, pairing: { active: !!state.pairingId, pairingId: state.pairingId, expiresAtMs: state.expiresAtMs, shortCode: state.shortCode, trustedSessionOnline: state.trustedSessionOnline, }, session: state.activeSession ? { ...state.activeSession } : null, connectionOnline: !!state.activeSession, walletProfile: state.walletProfile ? { ...state.walletProfile } : null, currentWallet: state.currentWallet ? { ...state.currentWallet } : null, pendingApproval: getCurrentPendingApproval() ? makePendingApprovalSnapshot(getCurrentPendingApproval()) : null, signing: { ...state.signing }, status: { text: state.statusText, kind: state.statusKind, }, }; } chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { (async () => { const type = String(message?.type || ''); if (type === 'wallet:getState') { await loadStateFromStorage(); sendResponse({ ok: true, state: snapshot() }); return; } if (type === 'wallet:saveSettings') { await persistSettings(message?.payload || {}); sendResponse({ ok: true, state: snapshot() }); return; } if (type === 'wallet:resolveServerInfo') { const result = await resolveServerForLogin(String(message?.payload?.login || '').trim()); sendResponse({ ok: true, result, state: snapshot() }); return; } if (type === 'wallet:startPairing') { const result = await startPairing(message?.payload || {}); sendResponse({ ok: true, result, state: snapshot() }); return; } if (type === 'wallet:cancelPairing') { const result = await cancelPairing(); sendResponse({ ok: true, result, state: snapshot() }); return; } if (type === 'wallet:resumeSession') { const result = await resumeActiveSession(); sendResponse({ ok: true, result, state: snapshot() }); return; } if (type === 'wallet:refreshWalletDevices') { const result = await refreshWalletDevices(); sendResponse({ ok: true, result, state: snapshot() }); return; } if (type === 'wallet:updateSigningSelection') { const result = await updateSigningSelection(message?.payload || {}); sendResponse({ ok: true, result, state: snapshot() }); return; } if (type === 'wallet:requestCurrentWallet') { const result = await requestCurrentWallet(); sendResponse({ ok: true, result, state: snapshot() }); return; } if (type === 'wallet:disconnectSession') { const result = await disconnectSession(); sendResponse({ ok: true, result, state: snapshot() }); return; } if (type === 'wallet:cancelPendingSiteApproval') { const result = await cancelPendingSiteApproval(); sendResponse({ ok: true, result, state: snapshot() }); return; } if (type === 'wallet:siteConnect') { const result = await siteConnect(message?.payload || {}); sendResponse({ ok: true, result, state: snapshot() }); return; } if (type === 'wallet:siteDisconnect') { const result = await siteDisconnect(message?.payload || {}); sendResponse({ ok: true, result, state: snapshot() }); return; } if (type === 'wallet:siteSignTransaction') { const result = await siteSignTransaction(message?.payload || {}, _sender); sendResponse({ ok: true, result, state: snapshot() }); return; } sendResponse({ ok: false, error: 'UNKNOWN_MESSAGE' }); })().catch((error) => { const message = toWalletErrorMessage(error, 'Unknown error'); setStatus(message, 'error'); sendResponse({ ok: false, error: message, code: String(error?.code || ''), state: snapshot() }); }); return true; }); chrome.runtime.onInstalled.addListener(() => { void configureSidePanelBehavior(); }); chrome.runtime.onStartup.addListener(() => { void configureSidePanelBehavior(); }); void configureSidePanelBehavior(); void loadStateFromStorage().then(async () => { if (state.activeSession?.login) { await hydrateWalletProfile(state.activeSession.login).catch(() => {}); setStatus(`Wallet-session сохранена для @${state.activeSession.login}. Подключение будет открываться только по действию.`, 'info'); } }).catch((error) => { setStatus(error?.message || 'Не удалось инициализировать wallet plugin.', 'error'); });