(function () { const root = document.getElementById('qortal-account-root'); if (!root) { return; } const debugEnabled = root.dataset.debugEnabled === '1'; const requestUrl = root.dataset.qappsRequestUrl || ''; const gatewayProxyTemplate = root.dataset.gatewayProxyUrl || ''; const nodeBalanceUrl = root.dataset.nodeBalanceUrl || ''; const mappingsUrl = root.dataset.userMappingsUrl || ''; const qappsApproveUrl = root.dataset.qappsApproveUrl || ''; const qappsUnlockUrl = root.dataset.qappsUnlockUrl || ''; const qappsUnlockStatusUrl = root.dataset.qappsUnlockStatusUrl || ''; const userCreateWalletUrl = root.dataset.userCreateWalletUrl || ''; const userBackupWalletUrl = root.dataset.userBackupWalletUrl || ''; const userRequestInitialQortUrl = root.dataset.userRequestInitialQortUrl || ''; const initialQortRequestsUrl = root.dataset.initialQortRequestsUrl || ''; const initialQortActionUrl = root.dataset.initialQortActionUrl || ''; const settingsPath = root.dataset.settingsPath || '/settings/user/qortal_integration'; const nextcloudPublicUrl = root.dataset.nextcloudPublicUrl || ''; const isAdmin = root.dataset.isAdmin === '1'; const currentUserId = root.dataset.currentUserId || ''; const qappsNamesRaw = root.dataset.qappsNames || '[]'; const qappsCardsRaw = root.dataset.qappsCards || '[]'; const qappsEnabled = root.dataset.qappsEnabled === '1'; const qappsUrl = root.dataset.qappsUrl || ''; const highlightTarget = String(new URLSearchParams(window.location.search).get('highlight') || '').trim().toLowerCase(); let pendingRegisterHighlight = highlightTarget === 'register-name'; const onboardingCard = document.getElementById('qortal-account-onboarding'); const walletPane = document.getElementById('qortal-account-wallet-pane'); const settingsLink = document.getElementById('qortal-account-settings-link'); const createPasswordEl = document.getElementById('qortal-account-create-password'); const createKdfEl = document.getElementById('qortal-account-create-kdf'); const createButton = document.getElementById('qortal-account-create-button'); const createResultEl = document.getElementById('qortal-account-create-result'); const addressEl = document.getElementById('qortal-account-address'); const balanceEl = document.getElementById('qortal-account-balance'); const balanceCardEl = document.getElementById('qortal-account-balance-card'); const primaryCardEl = document.getElementById('qortal-account-primary-card'); const primaryNameEl = document.getElementById('qortal-account-primary-name'); const namesEl = document.getElementById('qortal-account-names'); const namesEmptyEl = document.getElementById('qortal-account-names-empty'); const copyAddressButton = document.getElementById('qortal-account-copy-address'); const requestQortWrapEl = document.getElementById('qortal-account-request-wrap'); const requestQortButton = document.getElementById('qortal-account-request-qort'); const requestQortHelp = document.getElementById('qortal-account-request-help'); const requestQortResultEl = document.getElementById('qortal-account-request-result'); const requestFeedbackEl = document.getElementById('qortal-account-request-feedback'); const requestDetailsEl = document.getElementById('qortal-account-request-details'); const appGalleryCardEl = document.getElementById('qortal-account-app-gallery-card'); const appGalleryBodyEl = document.getElementById('qortal-account-app-gallery-body'); const appGalleryToggleButton = document.getElementById('qortal-account-app-gallery-toggle'); const appGalleryEl = document.getElementById('qortal-account-app-gallery'); const userRequestsBodyEl = document.getElementById('qortal-account-user-requests-body'); const userRequestsToggleButton = document.getElementById('qortal-account-user-requests-toggle'); const userRequestsListEl = document.getElementById('qortal-account-user-requests-list'); const userRequestsEmptyEl = document.getElementById('qortal-account-user-requests-empty'); const adminRequestsCardEl = document.getElementById('qortal-account-admin-requests-card'); const adminRequestsBodyEl = document.getElementById('qortal-account-admin-requests-body'); const adminRequestsToggleButton = document.getElementById('qortal-account-admin-requests-toggle'); const adminRequestsListEl = document.getElementById('qortal-account-admin-requests-list'); const adminRequestsEmptyEl = document.getElementById('qortal-account-admin-requests-empty'); const namesBodyEl = document.getElementById('qortal-account-names-body'); const namesToggleButton = document.getElementById('qortal-account-names-toggle'); const registerNameButton = document.getElementById('qortal-account-register-name-button'); const downloadBackupButton = document.getElementById('qortal-account-download-backup'); const refreshButton = document.getElementById('qortal-account-refresh'); const advancedToggleButton = document.getElementById('qortal-account-advanced-toggle'); const advancedBody = document.getElementById('qortal-account-advanced-body'); const sendRecipientEl = document.getElementById('qortal-account-send-recipient'); const sendAmountEl = document.getElementById('qortal-account-send-amount'); const sendFeeEl = document.getElementById('qortal-account-send-fee'); const sendFeeEditButton = document.getElementById('qortal-account-send-fee-edit-button'); const sendButton = document.getElementById('qortal-account-send-button'); const sendResultEl = document.getElementById('qortal-account-send-result'); const transactionsRefreshButton = document.getElementById('qortal-account-transactions-refresh'); const transactionsStatusEl = document.getElementById('qortal-account-transactions-status'); const transactionsEmptyEl = document.getElementById('qortal-account-transactions-empty'); const transactionsListEl = document.getElementById('qortal-account-transactions-list'); const testButton = document.getElementById('qortal-account-test'); const liveToggleButton = document.getElementById('qortal-account-toggle-live'); const lastRefreshEl = document.getElementById('qortal-account-last-refresh'); const statusEl = document.getElementById('qortal-account-status'); const walletIdEl = document.getElementById('qortal-account-wallet-id'); const publicKeyEl = document.getElementById('qortal-account-public-key'); const balanceRawEl = document.getElementById('qortal-account-balance-raw'); const detailsEl = document.getElementById('qortal-account-details'); const registerModal = document.getElementById('qortal-account-register-modal'); const registerNameInputEl = document.getElementById('qortal-account-register-name-input'); const registerErrorEl = document.getElementById('qortal-account-register-error'); const registerResultEl = document.getElementById('qortal-account-register-result'); const registerConfirmButton = document.getElementById('qortal-account-register-confirm'); const registerCancelButton = document.getElementById('qortal-account-register-cancel'); const approvalModal = document.getElementById('qortal-account-approval'); const approvalRequestEl = document.getElementById('qortal-account-approval-request'); const approvalScopeEl = document.getElementById('qortal-account-approval-scope'); const approvalWalletStatusEl = document.getElementById('qortal-account-approval-wallet-status'); const approvalUnlockFieldsEl = document.getElementById('qortal-account-approval-unlock-fields'); const approvalPasswordEl = document.getElementById('qortal-account-approval-password'); const approvalTtlEl = document.getElementById('qortal-account-approval-ttl'); const approvalErrorEl = document.getElementById('qortal-account-approval-error'); const approvalConfirmButton = document.getElementById('qortal-account-approval-confirm'); const approvalCancelButton = document.getElementById('qortal-account-approval-cancel'); const refreshIntervalMs = 30_000; let intervalId = null; let inFlight = false; let liveEnabled = true; let walletLockState = { known: false, unlocked: null, expiresAtMs: null, }; let activeWalletContext = { hasWallet: false, mapping: null, wallet: null, address: '', balance: null, primaryName: '', names: [], accountDetails: null, }; const txRecipientLabelCache = new Map(); let qappsNames = []; let qappsCards = []; try { const parsedQappsNames = JSON.parse(qappsNamesRaw); if (Array.isArray(parsedQappsNames)) { qappsNames = parsedQappsNames .map(function (entry) { return typeof entry === 'string' ? entry.trim() : ''; }) .filter(function (entry) { return entry !== ''; }); } } catch (_error) { qappsNames = []; } try { const parsedQappsCards = JSON.parse(qappsCardsRaw); if (Array.isArray(parsedQappsCards)) { qappsCards = parsedQappsCards .filter(function (entry) { return entry && typeof entry === 'object' && typeof entry.address === 'string' && entry.address.trim() !== ''; }) .map(function (entry) { return { name: typeof entry.name === 'string' ? entry.name.trim() : '', address: entry.address.trim(), description: typeof entry.description === 'string' ? entry.description.trim() : '', iconMode: typeof entry.iconMode === 'string' && entry.iconMode.trim() !== '' ? entry.iconMode.trim().toLowerCase() : 'auto', iconUrl: typeof entry.iconUrl === 'string' ? entry.iconUrl.trim() : '', }; }); } } catch (_error) { qappsCards = []; } if (debugEnabled) { const debugCards = root.querySelectorAll('.qortal-debug-only'); debugCards.forEach(function (el) { el.classList.remove('qortal-hidden'); }); } function setStatus(message, isError) { if (!statusEl) { return; } statusEl.textContent = message; statusEl.classList.toggle('qortal-error', Boolean(isError)); } function updateLastRefresh() { if (lastRefreshEl) { lastRefreshEl.textContent = new Date().toLocaleTimeString(); } } function setSettingsLink() { if (!settingsLink) { return; } const base = (nextcloudPublicUrl || (window.location.origin + (typeof OC !== 'undefined' && OC.webroot ? OC.webroot : ''))) .replace(/\/$/, ''); settingsLink.href = base + settingsPath; } function getSectionStorageKey(storageKey) { return 'qortal_account_section_' + String(storageKey || '').trim().toLowerCase(); } function setSectionExpanded(button, body, expanded) { if (!button || !body) { return; } const isExpanded = Boolean(expanded); body.classList.toggle('qortal-hidden', !isExpanded); button.textContent = isExpanded ? 'Hide' : 'Show'; button.setAttribute('aria-expanded', isExpanded ? 'true' : 'false'); } function initializeSectionToggle(button, body, storageKey, defaultExpanded) { if (!button || !body) { return; } let expanded = Boolean(defaultExpanded); try { const stored = window.localStorage ? window.localStorage.getItem(getSectionStorageKey(storageKey)) : null; if (stored === 'show') { expanded = true; } if (stored === 'hide') { expanded = false; } } catch (_error) { // ignore localStorage read errors } setSectionExpanded(button, body, expanded); button.addEventListener('click', function () { const nextExpanded = body.classList.contains('qortal-hidden'); setSectionExpanded(button, body, nextExpanded); try { if (window.localStorage) { window.localStorage.setItem(getSectionStorageKey(storageKey), nextExpanded ? 'show' : 'hide'); } } catch (_error) { // ignore localStorage write errors } }); } function applyRegisterHighlight() { if (!pendingRegisterHighlight) { return; } const target = primaryCardEl || registerNameButton; if (!target) { return; } if (primaryCardEl) { primaryCardEl.classList.add('qortal-highlight-target'); } if (registerNameButton) { registerNameButton.classList.add('qortal-highlight-action'); } target.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' }); window.setTimeout(function () { if (primaryCardEl) { primaryCardEl.classList.remove('qortal-highlight-target'); } if (registerNameButton) { registerNameButton.classList.remove('qortal-highlight-action'); } }, 9000); pendingRegisterHighlight = false; } function formatValue(value) { if (value === undefined || value === null || value === '') { return '—'; } if (typeof value === 'string') { return value; } return JSON.stringify(value); } function formatBalance(value) { if (value === undefined || value === null || value === '') { return '—'; } const num = Number(value); if (Number.isFinite(num)) { return num.toFixed(8); } return String(value); } function parseBalanceNumber(value) { const numeric = Number(value); return Number.isFinite(numeric) ? numeric : null; } function shouldShowInitialQortRequest(value) { const numeric = parseBalanceNumber(value); if (numeric === null) { return false; } return numeric <= 0; } function updateBalanceVisualState(value) { if (!balanceCardEl) { return; } balanceCardEl.classList.remove( 'qortal-wallet-summary__item--balance-good', 'qortal-wallet-summary__item--balance-warn', 'qortal-wallet-summary__item--balance-bad' ); const numeric = parseBalanceNumber(value); if (numeric === null) { return; } if (numeric <= 0) { balanceCardEl.classList.add('qortal-wallet-summary__item--balance-bad'); return; } if (numeric >= 1.5) { balanceCardEl.classList.add('qortal-wallet-summary__item--balance-good'); return; } balanceCardEl.classList.add('qortal-wallet-summary__item--balance-warn'); } function setFeeEditable(isEditable) { if (!sendFeeEl) { return; } sendFeeEl.readOnly = !isEditable; if (!isEditable) { sendFeeEl.value = '0.01'; } if (sendFeeEditButton) { sendFeeEditButton.textContent = isEditable ? 'Lock Fee' : 'Edit Fee'; } } function showCard(card, visible) { if (!card) { return; } card.classList.toggle('qortal-hidden', !visible); } function setCreateResult(message, isError) { if (!createResultEl) { return; } createResultEl.textContent = message || ''; createResultEl.classList.toggle('qortal-error', Boolean(isError)); } function setRequestQortResult(payload, isError) { if (!requestQortResultEl) { return; } requestQortResultEl.textContent = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2); requestQortResultEl.classList.toggle('qortal-error', Boolean(isError)); } function setRequestFeedback(message, isError) { if (!requestFeedbackEl) { return; } requestFeedbackEl.textContent = message || ''; requestFeedbackEl.classList.toggle('qortal-hidden', !message); requestFeedbackEl.classList.toggle('qortal-error', Boolean(isError)); } function updateRequestDetailsText() { if (!requestDetailsEl) { return; } const uniqueNames = Array.from(new Set(qappsNames)); let appsDescription = 'On this cloud, QORT may be used for decentralized publishing and secure data operations.'; if (uniqueNames.length > 0) { appsDescription = 'On this cloud, QORT is used in: ' + uniqueNames.join(', ') + '.'; } requestDetailsEl.textContent = appsDescription + ' QDN is a distributed network where published data remains available beyond any single cloud instance.' + ' You can export your Qortal backup and import it in other Qortal interfaces to access your data across devices.' + ' QORT is the currency that powers this network.'; } function extractAppNameFromQortalAddress(address) { const raw = String(address || '').trim(); if (!raw.toLowerCase().startsWith('qortal://')) { return ''; } const trimmed = raw.slice('qortal://'.length).replace(/^\/+/, ''); if (!trimmed) { return ''; } const parts = trimmed.split('/').filter(Boolean); if (parts.length === 0) { return ''; } const first = String(parts[0] || '').toUpperCase(); if ((first === 'APP' || first === 'WEBSITE') && parts.length > 1) { return String(parts[1] || '').trim(); } return String(parts[0] || '').trim(); } function resolveQappIconUrl(card) { if (!card || typeof card !== 'object') { return ''; } if (card.iconMode === 'custom' && card.iconUrl) { if (card.iconUrl.toLowerCase().startsWith('qortal://')) { const proxyPath = card.iconUrl.slice('qortal://'.length).replace(/^\/+/, ''); if (proxyPath) { return buildGatewayProxyUrl(proxyPath); } } return card.iconUrl; } const appName = extractAppNameFromQortalAddress(card.address); if (!appName) { return ''; } return buildGatewayProxyUrl('arbitrary/THUMBNAIL/' + appName + '/qortal_avatar'); } function buildQappLaunchUrl(address) { const base = String(qappsUrl || '').trim(); if (!base) { return ''; } const separator = base.indexOf('?') === -1 ? '?' : '&'; return base + separator + 'qapp=' + encodeURIComponent(String(address || '').trim()); } function renderQappsGallery() { if (!appGalleryCardEl || !appGalleryEl) { return; } appGalleryEl.innerHTML = ''; if (!qappsEnabled || !Array.isArray(qappsCards) || qappsCards.length === 0) { appGalleryCardEl.classList.add('qortal-hidden'); return; } const fragment = document.createDocumentFragment(); qappsCards.forEach(function (card) { const launchUrl = buildQappLaunchUrl(card.address); if (!launchUrl) { return; } const tile = document.createElement('a'); tile.className = 'qortal-app-card'; tile.href = launchUrl; tile.title = card.name || card.address; tile.setAttribute('aria-label', (card.name || card.address) + ' - Open Q-App'); const iconUrl = resolveQappIconUrl(card); if (iconUrl) { const image = document.createElement('img'); image.className = 'qortal-app-card__image'; image.loading = 'lazy'; image.alt = card.name || 'Q-App'; image.src = iconUrl; image.onerror = function () { image.remove(); if (!tile.querySelector('.qortal-app-card__fallback')) { const fallback = document.createElement('div'); fallback.className = 'qortal-app-card__fallback'; fallback.textContent = card.name || card.address; tile.appendChild(fallback); } }; tile.appendChild(image); } else { const fallback = document.createElement('div'); fallback.className = 'qortal-app-card__fallback'; fallback.textContent = card.name || card.address; tile.appendChild(fallback); } const title = document.createElement('div'); title.className = 'qortal-app-card__title'; title.textContent = card.name || card.address; tile.appendChild(title); const description = document.createElement('div'); description.className = 'qortal-app-card__desc'; description.textContent = card.description || 'Open this Q-App.'; tile.appendChild(description); fragment.appendChild(tile); }); appGalleryEl.appendChild(fragment); appGalleryCardEl.classList.toggle('qortal-hidden', appGalleryEl.children.length === 0); } function setRegisterError(message) { if (!registerErrorEl) { return; } registerErrorEl.textContent = message || ''; registerErrorEl.classList.toggle('qortal-hidden', !message); } function setRegisterResult(payload, isError) { if (!registerResultEl) { return; } registerResultEl.textContent = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2); registerResultEl.classList.toggle('qortal-error', Boolean(isError)); } function getCsrfHeaders(contentType) { const headers = {}; if (contentType) { headers['Content-Type'] = contentType; } if (typeof OC !== 'undefined' && OC.requestToken) { headers.requesttoken = OC.requestToken; } return headers; } async function requestJson(url, options) { const response = await fetch(url, options); const payload = await response.json(); if (!response.ok) { throw new Error(payload.error || 'Request failed'); } if (payload && payload.ok === false) { throw new Error(payload.error || 'Request failed'); } return payload; } async function qortalRequest(action, payload) { if (!requestUrl) { throw new Error('Q-Apps request URL is not configured'); } const response = await fetch(requestUrl, { method: 'POST', headers: getCsrfHeaders('application/json'), body: JSON.stringify({ requestType: action, payload: payload || {} }), }); const json = await response.json(); if (!response.ok || json.ok === false || json.error) { const error = new Error(json.error || 'qortal_request_failed'); error.status = response.status; error.details = json.details || null; error.payload = json; throw error; } return json.data !== undefined ? json.data : json; } function extractMappings(payload) { if (!payload) { return []; } if (Array.isArray(payload.mappings)) { return payload.mappings; } if (payload.data && Array.isArray(payload.data.mappings)) { return payload.data.mappings; } return []; } function pickMapping(mappings) { if (!Array.isArray(mappings)) { return null; } let fallback = null; for (const entry of mappings) { if (!entry || typeof entry !== 'object') { continue; } if (entry.walletId && entry.status === 'linked') { return entry; } if (!fallback && entry.walletId) { fallback = entry; } if (!fallback && entry.qortalAddress) { fallback = entry; } } return fallback; } function extractBalanceAmount(balance) { const pickAmount = function (value) { if (!value || typeof value !== 'object') { return undefined; } return ( value.balance ?? value.available ?? value.confirmed ?? value.amount ?? value.confirmedBalance ?? value.total ?? value.value ); }; if (Array.isArray(balance)) { for (const entry of balance) { const amount = pickAmount(entry); if (amount !== undefined && amount !== null && amount !== '') { return amount; } } return undefined; } if (balance && typeof balance === 'object') { return pickAmount(balance); } if (balance !== undefined && balance !== null && balance !== '') { return balance; } return undefined; } function looksLikeQortalAddress(value) { const candidate = (value || '').trim(); return /^Q[1-9A-HJ-NP-Za-km-z]{20,}$/.test(candidate); } function normalizeNameInput(value) { return String(value || '').trim().replace(/^@+/, ''); } function validateRegisterName(name) { const trimmed = String(name || '').trim(); if (!trimmed) { return 'Name is required'; } if (trimmed.length < 3) { return 'Name must be at least 3 characters'; } if (trimmed.length > 40) { return 'Name must be 40 characters or fewer'; } if (/[\u0000-\u001f\u007f]/.test(trimmed)) { return 'Name contains unsupported control characters'; } return ''; } function mapRegisterNameError(error) { const raw = String((error && error.message) || '').trim(); if (!raw) { return 'Name registration failed'; } const lower = raw.toLowerCase(); if (lower.includes('name is already registered') || lower.includes('already registered')) { return 'This name is already registered. Choose another name.'; } if (lower.includes('transaction invalid') && lower.includes('nam')) { return 'Name is unavailable or invalid. Choose another name and try again.'; } if (lower.includes('invalid name')) { return 'Name is invalid. Choose another name and try again.'; } if (lower.includes('not enough balance') || lower.includes('insufficient')) { return 'Not enough QORT balance to register a name.'; } return raw; } async function isNameAlreadyRegistered(name) { const desired = String(name || '').trim(); if (!desired) { return false; } try { const result = await qortalRequest('GET_NAME_DATA', { name: desired }); if (result && typeof result === 'object' && typeof result.name === 'string' && result.name.trim() !== '') { return true; } if (typeof result === 'string' && result.trim() !== '') { return true; } } catch (error) { const message = String((error && error.message) || '').toLowerCase(); if (message.includes('unknown name') || message.includes('name unknown') || message.includes('not found')) { return false; } } const url = buildGatewayProxyUrl('names/' + desired); if (!url) { return false; } try { const response = await fetch(url, { method: 'GET', headers: getCsrfHeaders(), }); if (response.status === 404) { return false; } const text = (await response.text()).trim(); if (!response.ok) { const lower = text.toLowerCase(); if ( lower.includes('unknown name') || lower.includes('name unknown') || lower.includes('"error":401') || lower.includes('not found') ) { return false; } return false; } if (text === '') { return false; } return true; } catch (_error) { return false; } } function encodeGatewayPath(path) { return String(path || '') .split('/') .map(function (segment) { return encodeURIComponent(segment); }) .join('/'); } function buildGatewayProxyUrl(path) { if (!gatewayProxyTemplate) { return ''; } if (gatewayProxyTemplate.includes('__PATH__')) { return gatewayProxyTemplate.replace('__PATH__', encodeGatewayPath(path)); } return gatewayProxyTemplate + encodeURIComponent(path); } async function fetchNodeBalance(address) { if (!nodeBalanceUrl || !address) { throw new Error('node_balance_url_missing'); } const response = await requestJson(nodeBalanceUrl + '?address=' + encodeURIComponent(address), { method: 'GET', headers: getCsrfHeaders(), }); if (Object.prototype.hasOwnProperty.call(response, 'balance')) { return response.balance; } if (response.ok && response.data !== undefined) { return response.data; } return response; } function updateNames(names, primaryName) { if (!namesEl || !namesEmptyEl) { return; } namesEl.innerHTML = ''; const labels = []; if (primaryName) { labels.push(primaryName); } if (Array.isArray(names)) { names.forEach(function (entry) { if (typeof entry === 'string' && entry.trim() !== '') { labels.push(entry.trim()); } else if (entry && typeof entry === 'object' && typeof entry.name === 'string' && entry.name.trim() !== '') { labels.push(entry.name.trim()); } }); } const unique = Array.from(new Set(labels)); if (unique.length === 0) { namesEmptyEl.classList.remove('qortal-hidden'); return; } namesEmptyEl.classList.add('qortal-hidden'); unique.forEach(function (name) { const chip = document.createElement('span'); chip.className = 'qortal-chip'; chip.textContent = name; namesEl.appendChild(chip); }); } function setTransactionsStatus(message, isError) { if (!transactionsStatusEl) { return; } transactionsStatusEl.textContent = message || ''; transactionsStatusEl.classList.toggle('qortal-error', Boolean(isError)); transactionsStatusEl.classList.toggle('qortal-hidden', !message); } function clearTransactionsView(statusMessage) { if (transactionsListEl) { transactionsListEl.innerHTML = ''; } if (transactionsEmptyEl) { transactionsEmptyEl.classList.add('qortal-hidden'); } setTransactionsStatus(statusMessage || 'No transactions loaded yet.', false); } function normalizeTxType(tx) { const raw = tx && tx.type !== undefined && tx.type !== null ? String(tx.type).toUpperCase() : 'UNKNOWN'; if (raw === 'ARBITRARY') { return 'QDN_PUBLISH'; } return raw; } function formatDateYmd(timestamp) { const numeric = Number(timestamp); if (Number.isFinite(numeric)) { try { return new Date(numeric).toISOString().slice(0, 10); } catch (_error) { return 'unknown date'; } } const parsed = Date.parse(String(timestamp || '')); if (!Number.isFinite(parsed)) { return 'unknown date'; } try { return new Date(parsed).toISOString().slice(0, 10); } catch (_error) { return 'unknown date'; } } function truncateText(value, maxLength) { const text = String(value || ''); if (text.length <= maxLength) { return text; } return text.slice(0, Math.max(0, maxLength - 3)) + '...'; } function isPaymentLikeTx(tx) { if (!tx || typeof tx !== 'object') { return false; } const type = String(tx.type || '').toUpperCase(); return type.includes('PAYMENT') || type === 'SEND_COIN' || Object.prototype.hasOwnProperty.call(tx, 'recipient') || Object.prototype.hasOwnProperty.call(tx, 'amount'); } function extractTxRecipient(tx) { if (!tx || typeof tx !== 'object') { return ''; } const candidates = [ tx.recipient, tx.recipientAddress, tx.to, tx.toAddress, tx.owner, ]; for (const candidate of candidates) { if (typeof candidate === 'string' && looksLikeQortalAddress(candidate)) { return candidate; } } return ''; } function extractTxAmount(tx) { if (!tx || typeof tx !== 'object') { return ''; } const amount = tx.amount ?? tx.quantity ?? tx.totalAmount ?? tx.total ?? tx.value; if (amount === undefined || amount === null || amount === '') { return ''; } return String(amount); } function extractTransactionsArray(payload) { if (Array.isArray(payload)) { return payload; } if (!payload || typeof payload !== 'object') { return []; } if (Array.isArray(payload.transactions)) { return payload.transactions; } if (payload.data && Array.isArray(payload.data.transactions)) { return payload.data.transactions; } if (payload.data && Array.isArray(payload.data)) { return payload.data; } return []; } async function resolveAddressLabel(address) { const normalized = String(address || '').trim(); if (!looksLikeQortalAddress(normalized)) { return ''; } if (txRecipientLabelCache.has(normalized)) { return txRecipientLabelCache.get(normalized) || ''; } let label = ''; try { const primary = await qortalRequest('GET_PRIMARY_NAME', { address: normalized }); if (typeof primary === 'string' && primary.trim() !== '') { label = primary.trim(); } } catch (_error) { // fallback below } if (!label) { try { const url = buildGatewayProxyUrl('names/primary/' + normalized); if (url) { const response = await fetch(url, { method: 'GET', headers: getCsrfHeaders() }); if (response.ok) { const text = (await response.text()).trim(); if (text.startsWith('{') || text.startsWith('[')) { const parsed = JSON.parse(text || '{}'); if (typeof parsed === 'string') { label = parsed.trim(); } else if (parsed && typeof parsed === 'object' && typeof parsed.name === 'string') { label = parsed.name.trim(); } } else if (text !== '') { label = text; } } } } catch (_error) { // ignore } } txRecipientLabelCache.set(normalized, label || ''); return label || ''; } async function fetchCreatorTransactions(publicKey) { const normalized = String(publicKey || '').trim(); if (!normalized) { return []; } const baseUrl = buildGatewayProxyUrl('transactions/creator/' + normalized); if (!baseUrl) { throw new Error('Gateway proxy URL is not configured for transactions'); } const params = new URLSearchParams({ confirmationStatus: 'CONFIRMED', limit: '100', reverse: 'true', }); const separator = baseUrl.includes('?') ? '&' : '?'; const response = await fetch(baseUrl + separator + params.toString(), { method: 'GET', headers: getCsrfHeaders(), }); const text = await response.text(); if (!response.ok) { throw new Error('Failed to load transactions (HTTP ' + response.status + ')'); } if (!text.trim()) { return []; } let parsed; try { parsed = JSON.parse(text); } catch (_error) { throw new Error('Invalid transactions response'); } return extractTransactionsArray(parsed); } function buildTxSummary(tx, labelsByAddress) { const txType = normalizeTxType(tx); const dateText = formatDateYmd(tx && tx.timestamp); if (txType === 'QDN_PUBLISH') { const identifier = tx && tx.identifier ? truncateText(tx.identifier, 54) : 'no identifier'; const nameLabel = tx && tx.name ? (' by ' + truncateText(tx.name, 28)) : ''; return { title: txType + ': ' + identifier, meta: dateText + nameLabel, }; } if (isPaymentLikeTx(tx)) { const amount = extractTxAmount(tx); const recipientAddress = extractTxRecipient(tx); const recipientName = recipientAddress ? (labelsByAddress.get(recipientAddress) || '') : ''; const recipientLabel = recipientName || (recipientAddress ? truncateText(recipientAddress, 22) : 'unknown recipient'); const amountLabel = amount ? (amount + ' QORT') : 'unknown amount'; return { title: txType + ': ' + amountLabel + ' to ' + recipientLabel, meta: dateText, }; } const signature = tx && tx.signature ? ('sig ' + truncateText(tx.signature, 16)) : ''; return { title: txType, meta: signature ? (dateText + ' • ' + signature) : dateText, }; } async function refreshTransactions(context) { if (!transactionsListEl || !transactionsStatusEl || !transactionsEmptyEl) { return; } if (!context || !context.hasWallet) { clearTransactionsView('Link a wallet to load transactions.'); return; } const publicKey = (context.accountDetails && (context.accountDetails.publicKey || context.accountDetails.publicKey58 || context.accountDetails.ownerPublicKey)) || (context.wallet && (context.wallet.publicKey || context.wallet.publicKey58)) || ''; if (!publicKey) { clearTransactionsView('Public key unavailable for transaction history.'); return; } setTransactionsStatus('Loading transactions…', false); transactionsEmptyEl.classList.add('qortal-hidden'); transactionsListEl.innerHTML = ''; try { const transactions = await fetchCreatorTransactions(publicKey); if (!Array.isArray(transactions) || transactions.length === 0) { transactionsEmptyEl.classList.remove('qortal-hidden'); setTransactionsStatus('No confirmed transactions found.', false); return; } const recipientAddresses = Array.from(new Set( transactions .filter(function (tx) { return isPaymentLikeTx(tx); }) .map(function (tx) { return extractTxRecipient(tx); }) .filter(function (address) { return looksLikeQortalAddress(address); }) )); const labelsByAddress = new Map(); if (recipientAddresses.length > 0) { const labelEntries = await Promise.all(recipientAddresses.map(async function (address) { const label = await resolveAddressLabel(address); return [address, label]; })); labelEntries.forEach(function (entry) { labelsByAddress.set(entry[0], entry[1]); }); } const fragment = document.createDocumentFragment(); transactions.forEach(function (tx) { const summary = buildTxSummary(tx, labelsByAddress); const item = document.createElement('details'); item.className = 'qortal-tx-item'; const summaryEl = document.createElement('summary'); const titleEl = document.createElement('span'); titleEl.className = 'qortal-tx-title'; titleEl.textContent = summary.title; titleEl.title = summary.title; const metaEl = document.createElement('span'); metaEl.className = 'qortal-tx-meta'; metaEl.textContent = summary.meta; metaEl.title = summary.meta; const detailEl = document.createElement('pre'); detailEl.className = 'qortal-tx-detail'; detailEl.textContent = JSON.stringify(tx, null, 2); summaryEl.appendChild(titleEl); summaryEl.appendChild(metaEl); item.appendChild(summaryEl); item.appendChild(detailEl); fragment.appendChild(item); }); transactionsListEl.appendChild(fragment); setTransactionsStatus('Showing ' + transactions.length + ' transaction(s).', false); } catch (error) { setTransactionsStatus(error.message || 'Failed to load transactions.', true); transactionsEmptyEl.classList.add('qortal-hidden'); } } function requestStatusClass(status) { const normalized = String(status || '').toLowerCase(); if (normalized === 'sent') { return 'status-sent'; } if (normalized === 'denied') { return 'status-denied'; } return 'status-pending'; } function requestStatusLabel(status) { const normalized = String(status || '').toLowerCase(); if (normalized === 'sent') { return 'Sent'; } if (normalized === 'denied') { return 'Denied'; } return 'Pending'; } function formatRequestAmount(value) { const numeric = Number(value); if (!Number.isFinite(numeric)) { return '0.00000000'; } return numeric.toFixed(8); } function clearRequestLists() { if (userRequestsListEl) { userRequestsListEl.innerHTML = ''; } if (adminRequestsListEl) { adminRequestsListEl.innerHTML = ''; } } function buildRequestCard(request, userTotals, adminControls) { const item = document.createElement('div'); const status = String(request.status || 'pending').toLowerCase(); item.className = 'qortal-request-item ' + requestStatusClass(status); const head = document.createElement('div'); head.className = 'qortal-request-head'; const title = document.createElement('p'); title.className = 'qortal-request-title'; const userLabel = request.userDisplayName || request.userId || 'User'; title.textContent = adminControls ? userLabel + ' • ' + requestStatusLabel(status) : 'Request • ' + requestStatusLabel(status); const date = document.createElement('span'); date.className = 'qortal-request-date'; date.textContent = formatDateYmd(request.createdAt || request.updatedAt || ''); head.appendChild(title); head.appendChild(date); item.appendChild(head); const meta = document.createElement('div'); meta.className = 'qortal-request-meta'; const primaryName = request.primaryName ? String(request.primaryName) : 'none'; const address = request.address ? truncateText(String(request.address), 32) : 'unknown'; const totalSent = formatRequestAmount(request.totalSent || 0); const userTotal = adminControls ? formatRequestAmount(userTotals[String(request.userId || '')] || 0) : totalSent; meta.textContent = 'Address: ' + address + ' • Primary name: ' + primaryName + ' • Request total sent: ' + totalSent + ' QORT' + ' • User total sent: ' + userTotal + ' QORT'; item.appendChild(meta); const messages = Array.isArray(request.messages) ? request.messages : []; if (messages.length > 0) { const list = document.createElement('ul'); list.className = 'qortal-request-message-list'; messages.slice(-6).forEach(function (entry) { const li = document.createElement('li'); li.className = 'qortal-request-message'; const from = entry && entry.fromDisplayName ? String(entry.fromDisplayName) : 'Admin'; const message = entry && entry.message ? String(entry.message) : ''; const at = entry && entry.at ? formatDateYmd(String(entry.at)) : ''; li.textContent = from + (at ? ' (' + at + ')' : '') + ': ' + message; list.appendChild(li); }); item.appendChild(list); } if (adminControls) { const actions = document.createElement('div'); actions.className = 'qortal-request-actions'; const sendButton = document.createElement('button'); sendButton.className = 'button button-primary'; sendButton.textContent = 'Send 2 QORT'; sendButton.disabled = status !== 'pending'; sendButton.addEventListener('click', function () { sendInitialQortForRequest(request); }); actions.appendChild(sendButton); const messageButton = document.createElement('button'); messageButton.className = 'button'; messageButton.textContent = 'Message'; messageButton.addEventListener('click', function () { sendAdminMessageForRequest(request); }); actions.appendChild(messageButton); const denyButton = document.createElement('button'); denyButton.className = 'button button-error'; denyButton.textContent = 'Deny'; denyButton.disabled = status !== 'pending'; denyButton.addEventListener('click', function () { denyInitialQortRequest(request); }); actions.appendChild(denyButton); item.appendChild(actions); } return item; } function extractRequestListPayload(payload) { if (!payload || typeof payload !== 'object') { return { requests: [], userTotals: {}, isAdmin: false }; } const data = payload.data && typeof payload.data === 'object' ? payload.data : payload; const requests = Array.isArray(data.requests) ? data.requests : []; const userTotals = data.userTotals && typeof data.userTotals === 'object' ? data.userTotals : {}; return { requests: requests, userTotals: userTotals, isAdmin: data.isAdmin === true, }; } function renderInitialQortRequests(requests, userTotals) { clearRequestLists(); const ownRequests = Array.isArray(requests) ? requests.filter(function (request) { return String(request.userId || '') === String(currentUserId || ''); }) : []; if (userRequestsListEl && userRequestsEmptyEl) { const userScoped = isAdmin ? ownRequests : (Array.isArray(requests) ? requests : []); if (userScoped.length === 0) { userRequestsEmptyEl.classList.remove('qortal-hidden'); } else { userRequestsEmptyEl.classList.add('qortal-hidden'); const userFragment = document.createDocumentFragment(); userScoped.forEach(function (request) { userFragment.appendChild(buildRequestCard(request, userTotals, false)); }); userRequestsListEl.appendChild(userFragment); } } if (!isAdmin || !adminRequestsCardEl || !adminRequestsListEl || !adminRequestsEmptyEl) { return; } adminRequestsCardEl.classList.remove('qortal-hidden'); if (!Array.isArray(requests) || requests.length === 0) { adminRequestsEmptyEl.classList.remove('qortal-hidden'); return; } adminRequestsEmptyEl.classList.add('qortal-hidden'); const adminFragment = document.createDocumentFragment(); requests.forEach(function (request) { adminFragment.appendChild(buildRequestCard(request, userTotals, true)); }); adminRequestsListEl.appendChild(adminFragment); } async function refreshInitialQortRequests() { if (!initialQortRequestsUrl) { return; } try { const payload = await requestJson(initialQortRequestsUrl, { method: 'GET', headers: getCsrfHeaders(), }); const parsed = extractRequestListPayload(payload); renderInitialQortRequests(parsed.requests, parsed.userTotals); } catch (_error) { // Keep UI functional if request list endpoint is unavailable. } } async function runInitialQortAdminAction(requestId, action, extra) { if (!initialQortActionUrl) { throw new Error('Initial QORT admin action endpoint is not configured'); } const body = new URLSearchParams(); body.set('requestId', String(requestId || '')); body.set('action', String(action || '')); const details = extra && typeof extra === 'object' ? extra : {}; if (details.message) { body.set('message', String(details.message)); } if (details.amount !== undefined && details.amount !== null) { body.set('amount', String(details.amount)); } if (details.txSignature) { body.set('txSignature', String(details.txSignature)); } return requestJson(initialQortActionUrl, { method: 'POST', headers: getCsrfHeaders('application/x-www-form-urlencoded; charset=UTF-8'), body: body.toString(), }); } async function fetchPrimaryName(address) { if (!address) { return ''; } try { const primary = await qortalRequest('GET_PRIMARY_NAME', { address: address }); if (typeof primary === 'string' && primary.trim() !== '') { return primary.trim(); } } catch (_error) { // continue to fallback } try { const url = buildGatewayProxyUrl('names/primary/' + address); if (!url) { return ''; } const response = await fetch(url, { method: 'GET', headers: getCsrfHeaders(), }); if (!response.ok) { return ''; } const text = (await response.text()).trim(); if (!text) { return ''; } if (text.startsWith('{') || text.startsWith('[')) { const parsed = JSON.parse(text); if (typeof parsed === 'string') { return parsed.trim(); } if (parsed && typeof parsed === 'object' && typeof parsed.name === 'string') { return parsed.name.trim(); } return ''; } return text; } catch (_error) { return ''; } } function updateWalletFields(mapping, wallet, balancePayload) { if (walletIdEl) { walletIdEl.textContent = formatValue((mapping && mapping.walletId) || (wallet && wallet.walletId)); } if (publicKeyEl) { publicKeyEl.textContent = formatValue(wallet && (wallet.publicKey || wallet.publicKey58)); } if (balanceRawEl) { balanceRawEl.textContent = formatValue(balancePayload); } } function updateDashboardState(context) { const hasWallet = Boolean(context && context.hasWallet); showCard(onboardingCard, !hasWallet); showCard(walletPane, hasWallet); if (!hasWallet) { if (addressEl) { addressEl.textContent = '—'; } if (balanceEl) { balanceEl.textContent = '—'; } if (primaryNameEl) { primaryNameEl.textContent = '—'; } updateNames([], ''); if (registerNameButton) { registerNameButton.disabled = true; } if (sendButton) { sendButton.disabled = true; } if (sendFeeEditButton) { sendFeeEditButton.disabled = true; } updateBalanceVisualState(null); if (requestQortWrapEl) { requestQortWrapEl.classList.add('qortal-hidden'); } clearTransactionsView('Link a wallet to load transactions.'); return; } if (addressEl) { addressEl.textContent = formatValue(context.address); } if (balanceEl) { balanceEl.textContent = formatBalance(context.balance); } if (primaryNameEl) { primaryNameEl.textContent = context.primaryName ? context.primaryName : '—'; } updateNames(context.names, context.primaryName || ''); if (sendButton) { sendButton.disabled = false; } if (sendFeeEditButton) { sendFeeEditButton.disabled = false; } updateBalanceVisualState(context.balance); const numericBalance = parseBalanceNumber(context.balance); const showInitialQortButton = shouldShowInitialQortRequest(context.balance); if (requestQortWrapEl) { requestQortWrapEl.classList.toggle('qortal-hidden', !showInitialQortButton); } const canRegisterName = Number.isFinite(numericBalance) && numericBalance >= 1.5; if (registerNameButton) { registerNameButton.disabled = !canRegisterName; registerNameButton.title = canRegisterName ? 'Register a new Qortal name' : 'Requires at least 1.5 QORT to register a name'; } } function normalizeApprovalScope(scope, requestType) { const normalizedScope = String(scope || '').trim(); if (normalizedScope) { return normalizedScope; } return String(requestType || '').trim().toUpperCase(); } function isApprovalRequiredError(error) { if (!error) { return false; } const message = error.message || ''; return typeof message === 'string' && message.includes('approval_required'); } function isWalletLockedError(error) { if (!error) { return false; } const message = error.message || ''; if (typeof message === 'string' && message.includes('wallet_locked')) { return true; } const details = error.details; if (details && typeof details === 'object') { if (details.error === 'wallet_locked') { return true; } const nestedDetail = details.data && details.data.detail ? details.data.detail : null; if (nestedDetail && typeof nestedDetail === 'object' && nestedDetail.error === 'wallet_locked') { return true; } } return false; } function extractApprovalContext(error, requestType) { const context = { scope: '', walletId: '', }; function captureFromValue(value, depth) { if (depth > 3 || value === null || value === undefined) { return; } if (typeof value === 'string') { if (!context.scope) { const scopeMatch = value.match(/"scope"\s*:\s*"([^"]+)"/); if (scopeMatch && scopeMatch[1]) { context.scope = scopeMatch[1]; } } if (!context.walletId) { const walletMatch = value.match(/"walletId"\s*:\s*"([^"]+)"/); if (walletMatch && walletMatch[1]) { context.walletId = walletMatch[1]; } } const jsonStart = value.indexOf('{'); const jsonEnd = value.lastIndexOf('}'); if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) { try { const parsed = JSON.parse(value.slice(jsonStart, jsonEnd + 1)); captureFromValue(parsed, depth + 1); } catch (_err) { // ignore parse errors } } return; } if (typeof value !== 'object') { return; } if (!context.scope && typeof value.scope === 'string') { context.scope = value.scope; } if (!context.walletId && typeof value.walletId === 'string') { context.walletId = value.walletId; } if (value.detail !== undefined) { captureFromValue(value.detail, depth + 1); } if (value.data !== undefined) { captureFromValue(value.data, depth + 1); } if (value.error !== undefined) { captureFromValue(value.error, depth + 1); } } captureFromValue(error && error.details ? error.details : null, 0); captureFromValue(error && error.message ? error.message : '', 0); context.scope = normalizeApprovalScope(context.scope, requestType); if (!context.walletId) { context.walletId = (activeWalletContext && activeWalletContext.mapping && activeWalletContext.mapping.walletId) || (activeWalletContext && activeWalletContext.wallet && activeWalletContext.wallet.walletId) || ''; } return context; } function parseUnlockStatus(value) { let candidate = value; for (let i = 0; i < 4; i += 1) { if (!candidate || typeof candidate !== 'object' || candidate.data === undefined || candidate.data === null) { break; } if (typeof candidate.data !== 'object') { break; } candidate = candidate.data; } if (!candidate || typeof candidate !== 'object' || typeof candidate.unlocked !== 'boolean') { return null; } const unlocked = candidate.unlocked; const rawExpires = candidate.expiresAt !== undefined ? candidate.expiresAt : candidate.expires_at; let expiresAtMs = null; if (typeof rawExpires === 'number' && Number.isFinite(rawExpires) && rawExpires > 0) { expiresAtMs = rawExpires < 10_000_000_000 ? rawExpires * 1000 : rawExpires; } else if (typeof rawExpires === 'string' && /^\d+$/.test(rawExpires.trim())) { const parsed = Number(rawExpires.trim()); if (Number.isFinite(parsed) && parsed > 0) { expiresAtMs = parsed < 10_000_000_000 ? parsed * 1000 : parsed; } } return { unlocked, expiresAtMs }; } function markWalletUnknown() { walletLockState = { known: false, unlocked: null, expiresAtMs: null }; } function markWalletLocked() { walletLockState = { known: true, unlocked: false, expiresAtMs: null }; } function markWalletUnlocked(expiresAtMs) { walletLockState = { known: true, unlocked: true, expiresAtMs: typeof expiresAtMs === 'number' ? expiresAtMs : null, }; } function walletStatusPresentation() { if (!walletLockState.known) { return { text: 'Wallet lock: unknown (password may be required)', className: 'qortal-wallet-status-unknown', showUnlockFields: true, }; } if (!walletLockState.unlocked) { return { text: 'Wallet lock: LOCKED (password required)', className: 'qortal-wallet-status-locked', showUnlockFields: true, }; } if (walletLockState.expiresAtMs && walletLockState.expiresAtMs > Date.now()) { const minutes = Math.max(1, Math.ceil((walletLockState.expiresAtMs - Date.now()) / 60000)); return { text: 'Wallet lock: UNLOCKED for ~' + minutes + ' minute(s)', className: 'qortal-wallet-status-ok', showUnlockFields: false, }; } return { text: 'Wallet lock: UNLOCKED', className: 'qortal-wallet-status-ok', showUnlockFields: false, }; } function updateApprovalWalletStatus() { const status = walletStatusPresentation(); if (approvalWalletStatusEl) { approvalWalletStatusEl.textContent = status.text; approvalWalletStatusEl.classList.remove('qortal-wallet-status-ok', 'qortal-wallet-status-locked', 'qortal-wallet-status-unknown'); approvalWalletStatusEl.classList.add(status.className); } if (approvalUnlockFieldsEl) { approvalUnlockFieldsEl.classList.toggle('qortal-hidden', !status.showUnlockFields); } if (!status.showUnlockFields && approvalPasswordEl) { approvalPasswordEl.value = ''; } return status; } function showApprovalError(message) { if (!approvalErrorEl) { return; } approvalErrorEl.textContent = message || ''; approvalErrorEl.classList.toggle('qortal-hidden', !message); } async function refreshWalletLockState(walletId) { const id = String(walletId || '').trim(); if (!id || !qappsUnlockStatusUrl) { markWalletUnknown(); return false; } try { const joiner = qappsUnlockStatusUrl.indexOf('?') === -1 ? '?' : '&'; const response = await fetch(qappsUnlockStatusUrl + joiner + 'walletId=' + encodeURIComponent(id), { method: 'GET', headers: getCsrfHeaders(), }); const payload = await response.json(); if (!response.ok || (payload && payload.error)) { markWalletUnknown(); return false; } const parsed = parseUnlockStatus(payload); if (!parsed) { markWalletUnknown(); return false; } if (!parsed.unlocked) { markWalletLocked(); return true; } markWalletUnlocked(parsed.expiresAtMs); return true; } catch (_error) { markWalletUnknown(); return false; } } function closeApprovalModal() { if (!approvalModal) { return; } approvalModal.classList.add('qortal-hidden'); showApprovalError(''); if (approvalPasswordEl) { approvalPasswordEl.value = ''; approvalPasswordEl.onkeydown = null; } if (approvalConfirmButton) { approvalConfirmButton.disabled = false; approvalConfirmButton.textContent = 'Approve and Send'; approvalConfirmButton.onclick = null; } if (approvalCancelButton) { approvalCancelButton.disabled = false; approvalCancelButton.onclick = null; } } async function showApprovalModal(context) { if (!approvalModal || !approvalConfirmButton || !approvalCancelButton) { return { action: 'cancel' }; } if (approvalRequestEl) { approvalRequestEl.textContent = String(context.requestType || 'ACTION'); } if (approvalScopeEl) { approvalScopeEl.textContent = String(context.scope || 'ACTION'); } showApprovalError(''); updateApprovalWalletStatus(); approvalModal.classList.remove('qortal-hidden'); if (approvalUnlockFieldsEl && !approvalUnlockFieldsEl.classList.contains('qortal-hidden') && approvalPasswordEl) { approvalPasswordEl.focus(); approvalPasswordEl.onkeydown = function (event) { if (event.key === 'Enter' && !approvalConfirmButton.disabled) { event.preventDefault(); approvalConfirmButton.click(); } }; } else { approvalConfirmButton.focus(); } return new Promise(function (resolve) { approvalConfirmButton.onclick = function () { const status = walletStatusPresentation(); const password = approvalPasswordEl ? approvalPasswordEl.value.trim() : ''; if (status.showUnlockFields && !password) { showApprovalError('Wallet password is required because wallet appears locked.'); return; } approvalConfirmButton.disabled = true; approvalConfirmButton.textContent = 'Approving...'; approvalCancelButton.disabled = true; resolve({ action: 'approve', password: password, ttlSeconds: approvalTtlEl && approvalTtlEl.checked ? 600 : undefined, }); }; approvalCancelButton.onclick = function () { closeApprovalModal(); resolve({ action: 'cancel' }); }; }); } async function unlockWallet(walletId, password, ttlSeconds) { if (!qappsUnlockUrl) { throw new Error('Unlock endpoint is not configured'); } const response = await fetch(qappsUnlockUrl, { method: 'POST', headers: getCsrfHeaders('application/json'), body: JSON.stringify({ walletId, password, ...(ttlSeconds ? { ttlSeconds } : {}), }), }); const payload = await response.json(); if (!response.ok || payload.ok === false || payload.error) { markWalletLocked(); throw new Error(payload.error || 'wallet_unlock_failed'); } const expiresAtMs = ttlSeconds ? Date.now() + (Number(ttlSeconds) * 1000) : null; markWalletUnlocked(expiresAtMs); return payload; } async function approveOnce(walletId, scope, requestType) { if (!qappsApproveUrl) { throw new Error('Approval endpoint is not configured'); } const response = await fetch(qappsApproveUrl, { method: 'POST', headers: getCsrfHeaders('application/json'), body: JSON.stringify({ walletId, scope, app: 'qortal://NC/ACCOUNT', requestType, }), }); const payload = await response.json(); if (!response.ok || payload.ok === false || payload.error) { throw new Error(payload.error || 'approval_failed'); } const token = payload.data && payload.data.approvalToken ? payload.data.approvalToken : payload.approvalToken; if (!token) { throw new Error('approval_token_missing'); } return token; } async function sendWithApproval(payload, requestType) { const action = String(requestType || '').trim().toUpperCase(); if (!action) { throw new Error('requestType is required'); } let workingPayload = Object.assign({}, payload || {}); let attempt = 0; while (attempt < 4) { try { return await qortalRequest(action, workingPayload); } catch (error) { const approvalRequired = isApprovalRequiredError(error); const walletLocked = isWalletLockedError(error); if (!approvalRequired && !walletLocked) { throw error; } const approvalContext = extractApprovalContext(error, action); const walletId = approvalContext.walletId || workingPayload.walletId || (activeWalletContext && activeWalletContext.mapping && activeWalletContext.mapping.walletId) || ''; if (!walletId) { throw error; } await refreshWalletLockState(walletId); updateApprovalWalletStatus(); const decision = await showApprovalModal({ requestType: action, scope: approvalContext.scope || action, walletId: walletId, }); if (!decision || decision.action === 'cancel') { throw new Error('approval_cancelled'); } if (decision.password) { try { await unlockWallet(walletId, decision.password, decision.ttlSeconds); } catch (unlockError) { showApprovalError(unlockError.message || 'wallet_unlock_failed'); throw unlockError; } } const scope = normalizeApprovalScope(approvalContext.scope, action); const token = await approveOnce(walletId, scope, action); workingPayload = Object.assign({}, payload || {}, { approvalToken: token }); closeApprovalModal(); attempt += 1; } } throw new Error('approval_failed'); } function parsePositiveNumber(raw, fieldName) { const value = Number(raw); if (!Number.isFinite(value) || value <= 0) { throw new Error(fieldName + ' must be a positive number'); } return value; } function extractAddressFromNameInfo(input) { if (typeof input === 'string') { return looksLikeQortalAddress(input) ? input : ''; } if (Array.isArray(input)) { for (const entry of input) { const nested = extractAddressFromNameInfo(entry); if (nested) { return nested; } } return ''; } if (!input || typeof input !== 'object') { return ''; } const directCandidates = [ input.owner, input.address, input.ownerAddress, input.owner_address, input.qortalAddress, input.qortal_address, input.registrant, input.creatorAddress, ]; for (const candidate of directCandidates) { if (typeof candidate === 'string' && looksLikeQortalAddress(candidate)) { return candidate; } } const nestedCandidates = [ input.nameData, input.name_info, input.nameInfo, input.result, input.data, ]; for (const nested of nestedCandidates) { const candidate = extractAddressFromNameInfo(nested); if (candidate) { return candidate; } } return ''; } async function fetchNameDataViaGateway(name) { const normalizedName = normalizeNameInput(name); if (!normalizedName) { return null; } const url = buildGatewayProxyUrl('names/' + normalizedName); if (!url) { return null; } const response = await fetch(url, { method: 'GET', headers: getCsrfHeaders() }); if (!response.ok) { return null; } const text = await response.text(); if (!text) { return null; } try { return JSON.parse(text); } catch (_error) { return null; } } async function resolveRecipientAddress(input) { const raw = (input || '').trim(); if (!raw) { throw new Error('Recipient is required'); } if (looksLikeQortalAddress(raw)) { return raw; } const normalizedName = normalizeNameInput(raw); if (!normalizedName) { throw new Error('Recipient is required'); } try { const info = await qortalRequest('GET_NAME_DATA', { name: normalizedName }); const address = extractAddressFromNameInfo(info); if (address) { return address; } } catch (_error) { // continue fallback } try { const gatewayInfo = await fetchNameDataViaGateway(normalizedName); const address = extractAddressFromNameInfo(gatewayInfo); if (address) { return address; } } catch (_error) { // final error below } throw new Error('Name "' + raw + '" does not resolve to a Qortal address'); } function extractSenderFields(wallet, accountDetails) { const account = accountDetails && typeof accountDetails === 'object' ? accountDetails : {}; const senderPublicKey = account.publicKey || account.publicKey58 || account.ownerPublicKey || (wallet && (wallet.publicKey || wallet.publicKey58)) || ''; const reference = account.lastReference || account.reference || account.lastreference || account.last_reference || ''; return { senderPublicKey: typeof senderPublicKey === 'string' ? senderPublicKey : '', reference: typeof reference === 'string' ? reference : '', }; } async function tryHydrateSenderFields(context, senderFields) { if (senderFields.reference && senderFields.senderPublicKey) { return senderFields; } const address = (context && (context.address || (context.mapping && context.mapping.qortalAddress))) || ''; if (!address) { return senderFields; } try { const accountData = await qortalRequest('GET_ACCOUNT_DATA', { address }); const refreshed = extractSenderFields(context ? context.wallet : null, accountData); return { senderPublicKey: refreshed.senderPublicKey || senderFields.senderPublicKey || '', reference: refreshed.reference || senderFields.reference || '', }; } catch (_error) { return senderFields; } } async function ensureWalletContext() { if (activeWalletContext.hasWallet && activeWalletContext.mapping && activeWalletContext.wallet) { return activeWalletContext; } await refreshAll('context'); if (!activeWalletContext.hasWallet) { throw new Error('No linked wallet found'); } return activeWalletContext; } async function sendCoinTransfer(recipientInput, amount, fee) { const recipientAddress = await resolveRecipientAddress(recipientInput); const context = await ensureWalletContext(); let accountDetails = context.accountDetails; if (!accountDetails) { try { accountDetails = await qortalRequest('GET_USER_ACCOUNT', {}); } catch (_error) { accountDetails = null; } } let senderFields = extractSenderFields(context.wallet, accountDetails); senderFields = await tryHydrateSenderFields(context, senderFields); const payload = { coin: 'QORT', walletId: (context.mapping && context.mapping.walletId) || (context.wallet && context.wallet.walletId) || undefined, recipient: recipientAddress, amount: amount, fee: fee, }; if (senderFields.reference && senderFields.senderPublicKey) { payload.tx = { timestamp: Date.now(), reference: senderFields.reference, fee: fee, senderPublicKey: senderFields.senderPublicKey, recipient: recipientAddress, amount: amount, }; } const result = await sendWithApproval(payload, 'SEND_COIN'); return { result: result, recipientAddress: recipientAddress, }; } function extractSignatureFromResult(payload) { if (!payload || typeof payload !== 'object') { return ''; } if (typeof payload.signature === 'string' && payload.signature.trim() !== '') { return payload.signature.trim(); } const nestedCandidates = [payload.result, payload.data, payload.tx, payload.transaction]; for (const candidate of nestedCandidates) { const signature = extractSignatureFromResult(candidate); if (signature !== '') { return signature; } } if (Array.isArray(payload.results)) { for (const candidate of payload.results) { const signature = extractSignatureFromResult(candidate); if (signature !== '') { return signature; } } } return ''; } async function sendQort() { if (!sendRecipientEl || !sendAmountEl || !sendFeeEl || !sendResultEl || !sendButton) { return; } try { sendButton.disabled = true; setStatus('sending…', false); const recipientInput = sendRecipientEl.value.trim(); const amount = parsePositiveNumber(sendAmountEl.value.trim(), 'Amount'); const feeRaw = sendFeeEl.value.trim(); const fee = feeRaw === '' ? 0.01 : parsePositiveNumber(feeRaw, 'Fee'); const transfer = await sendCoinTransfer(recipientInput, amount, fee); const result = transfer.result; sendResultEl.textContent = JSON.stringify({ ok: true, recipientInput: recipientInput, recipientAddress: transfer.recipientAddress, amount: amount, fee: fee, result: result, }, null, 2); setStatus('transfer sent', false); await refreshAll('manual'); } catch (error) { closeApprovalModal(); sendResultEl.textContent = JSON.stringify({ ok: false, error: error.message || 'send_failed', }, null, 2); setStatus(error.message || 'send failed', true); } finally { if (sendButton) { sendButton.disabled = !activeWalletContext.hasWallet; } } } async function sendInitialQortForRequest(request) { if (!request || !request.id || !request.address) { setStatus('Invalid request payload', true); return; } try { setStatus('Sending initial QORT…', false); const transfer = await sendCoinTransfer(String(request.address), 2, 0.01); const txSignature = extractSignatureFromResult(transfer.result); await runInitialQortAdminAction(String(request.id), 'send', { amount: 2, txSignature: txSignature, }); setStatus('Initial QORT sent', false); await refreshInitialQortRequests(); await refreshAll('manual'); } catch (error) { closeApprovalModal(); setStatus(error.message || 'Failed to send initial QORT', true); } } async function sendAdminMessageForRequest(request) { if (!request || !request.id) { return; } const message = window.prompt('Send a message to this user:'); if (!message || !message.trim()) { return; } try { await runInitialQortAdminAction(String(request.id), 'message', { message: message.trim(), }); setStatus('Message sent to user', false); await refreshInitialQortRequests(); } catch (error) { setStatus(error.message || 'Failed to send message', true); } } async function denyInitialQortRequest(request) { if (!request || !request.id) { return; } const message = window.prompt('Optional deny reason (recommended):', ''); if (!window.confirm('Deny this initial QORT request?')) { return; } try { await runInitialQortAdminAction(String(request.id), 'deny', { message: message ? message.trim() : '', }); setStatus('Request denied', false); await refreshInitialQortRequests(); } catch (error) { setStatus(error.message || 'Failed to deny request', true); } } function openRegisterModal() { if (!registerModal) { return; } setRegisterError(''); registerModal.classList.remove('qortal-hidden'); if (registerNameInputEl) { registerNameInputEl.focus(); } } function closeRegisterModal() { if (!registerModal) { return; } registerModal.classList.add('qortal-hidden'); setRegisterError(''); } async function registerName() { try { const context = await ensureWalletContext(); const numericBalance = Number(context.balance); if (!Number.isFinite(numericBalance) || numericBalance < 1.5) { throw new Error('At least 1.5 QORT is required to register a name'); } const desiredName = normalizeNameInput(registerNameInputEl ? registerNameInputEl.value : ''); const validationMessage = validateRegisterName(desiredName); if (validationMessage) { throw new Error(validationMessage); } setStatus('checking name availability…', false); const alreadyRegistered = await isNameAlreadyRegistered(desiredName); if (alreadyRegistered) { throw new Error('This name is already registered. Choose another name.'); } if (registerConfirmButton) { registerConfirmButton.disabled = true; } setStatus('registering name…', false); const payload = { walletId: (context.mapping && context.mapping.walletId) || (context.wallet && context.wallet.walletId) || undefined, name: desiredName, }; const result = await sendWithApproval(payload, 'REGISTER_NAME'); setRegisterResult({ ok: true, name: desiredName, result: result }, false); setStatus('name registration submitted', false); await refreshAll('manual'); } catch (error) { const friendlyError = mapRegisterNameError(error); setRegisterError(friendlyError); setRegisterResult({ ok: false, error: friendlyError }, true); setStatus(friendlyError, true); } finally { if (registerConfirmButton) { registerConfirmButton.disabled = false; } } } async function createWallet() { if (!userCreateWalletUrl || !createPasswordEl || !createButton) { return; } const password = createPasswordEl.value.trim(); const kdfThreads = createKdfEl ? createKdfEl.value.trim() : ''; if (!password) { setCreateResult('Password is required.', true); return; } createButton.disabled = true; setCreateResult('Creating wallet...', false); setStatus('creating wallet…', false); try { const body = new URLSearchParams(); body.set('password', password); if (kdfThreads) { body.set('kdfThreads', kdfThreads); } const payload = await requestJson(userCreateWalletUrl, { method: 'POST', headers: getCsrfHeaders('application/x-www-form-urlencoded; charset=UTF-8'), body: body.toString(), }); setCreateResult(JSON.stringify(payload, null, 2), false); createPasswordEl.value = ''; if (createKdfEl) { createKdfEl.value = ''; } await refreshAll('manual'); setStatus('wallet created', false); } catch (error) { setCreateResult(error.message || 'wallet_creation_failed', true); setStatus(error.message || 'wallet creation failed', true); } finally { createButton.disabled = false; } } async function requestInitialQort() { if (!userRequestInitialQortUrl) { setRequestQortResult({ ok: false, error: 'Request endpoint is not configured' }, true); setRequestFeedback('Request endpoint is not configured.', true); return; } const context = activeWalletContext; if (!context || !context.hasWallet || !context.address) { setRequestQortResult({ ok: false, error: 'A linked wallet is required first' }, true); setRequestFeedback('A linked wallet is required first.', true); return; } if (requestQortButton) { requestQortButton.disabled = true; } if (requestQortHelp) { requestQortHelp.classList.remove('qortal-hidden'); } setStatus('sending initial QORT request…', false); try { const body = new URLSearchParams(); body.set('address', context.address); body.set('balance', context.balance !== undefined && context.balance !== null ? String(context.balance) : ''); body.set('primaryName', context.primaryName || ''); body.set('names', Array.isArray(context.names) ? JSON.stringify(context.names) : '[]'); const payload = await requestJson(userRequestInitialQortUrl, { method: 'POST', headers: getCsrfHeaders('application/x-www-form-urlencoded; charset=UTF-8'), body: body.toString(), }); if (debugEnabled) { setRequestQortResult(payload, false); } const requestDate = payload && payload.data && payload.data.request && payload.data.request.createdAt ? formatDateYmd(payload.data.request.createdAt) : formatDateYmd(Date.now()); setRequestFeedback('Request sent on ' + requestDate + '. Admin review pending.', false); setStatus('request sent to admins', false); await refreshInitialQortRequests(); } catch (error) { if (debugEnabled) { setRequestQortResult({ ok: false, error: error.message || 'request_failed' }, true); } setRequestFeedback(error.message || 'request_failed', true); setStatus(error.message || 'request failed', true); } finally { if (requestQortButton) { requestQortButton.disabled = false; } } } async function copyAddress() { const address = activeWalletContext && activeWalletContext.address ? activeWalletContext.address : ''; if (!address) { setStatus('No address available to copy', true); return; } try { await navigator.clipboard.writeText(address); setStatus('Qortal address copied', false); } catch (_error) { const input = document.createElement('input'); input.value = address; document.body.appendChild(input); input.select(); document.execCommand('copy'); input.remove(); setStatus('Qortal address copied', false); } } function toggleAdvanced() { if (!advancedBody || !advancedToggleButton) { return; } const willShow = advancedBody.classList.contains('qortal-hidden'); advancedBody.classList.toggle('qortal-hidden', !willShow); advancedToggleButton.textContent = willShow ? 'Hide' : 'Show'; } function normalizeBackupObject(value) { if (!value || typeof value !== 'object') { return null; } if (value.backup && typeof value.backup === 'object') { return value.backup; } if (value.data && typeof value.data === 'object') { return normalizeBackupObject(value.data); } return value; } function downloadJsonFile(filename, payload) { const serialized = JSON.stringify(payload, null, 2); const blob = new Blob([serialized], { type: 'application/json' }); const blobUrl = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = blobUrl; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(blobUrl); } async function downloadBackupFile() { if (!downloadBackupButton || !userBackupWalletUrl) { return; } const walletId = (activeWalletContext && activeWalletContext.mapping && activeWalletContext.mapping.walletId) || (activeWalletContext && activeWalletContext.wallet && activeWalletContext.wallet.walletId) || ''; if (!walletId) { window.alert('No linked wallet found. Create or link a wallet first.'); return; } const password = window.prompt('Enter your wallet password to create and download an encrypted backup JSON:'); if (!password) { return; } try { downloadBackupButton.disabled = true; const body = new URLSearchParams(); body.set('walletId', walletId); body.set('password', password); const payload = await requestJson(userBackupWalletUrl, { method: 'POST', headers: getCsrfHeaders('application/x-www-form-urlencoded; charset=UTF-8'), body: body.toString(), }); const backup = normalizeBackupObject(payload); if (!backup || typeof backup !== 'object') { throw new Error('Backup payload was invalid'); } const address = (activeWalletContext && activeWalletContext.address) ? activeWalletContext.address : walletId; const timestamp = new Date().toISOString().slice(0, 19).replace(/[-:T]/g, ''); const filename = 'qortal-backup-' + address + '-' + timestamp + '.json'; downloadJsonFile(filename, backup); } catch (error) { window.alert(error.message || 'Failed to create backup'); } finally { downloadBackupButton.disabled = false; } } async function refreshAll(context) { if (inFlight) { return; } inFlight = true; setStatus('loading…', false); try { const mappingsPayload = await requestJson(mappingsUrl, { method: 'GET', headers: getCsrfHeaders(), }); const mappings = extractMappings(mappingsPayload); const mapping = pickMapping(mappings); const hasWallet = Boolean(mapping && mapping.walletId); if (!hasWallet) { activeWalletContext = { hasWallet: false, mapping: mapping || null, wallet: null, address: '', balance: null, primaryName: '', names: [], accountDetails: null, }; updateDashboardState(activeWalletContext); if (detailsEl) { detailsEl.textContent = 'Link a wallet to enable account details.'; } await refreshInitialQortRequests(); setStatus('wallet not linked', true); updateLastRefresh(); inFlight = false; return; } const wallet = await qortalRequest('GET_USER_WALLET', { coin: 'QORT' }); let address = wallet && (wallet.address || wallet.address0 || (wallet.addresses && wallet.addresses[0])); if (!address && mapping && mapping.qortalAddress) { address = mapping.qortalAddress; } let balancePayload = null; let balanceAmount = undefined; try { balancePayload = await qortalRequest('GET_WALLET_BALANCE', address ? { coin: 'QORT', address: address } : { coin: 'QORT' }); balanceAmount = extractBalanceAmount(balancePayload); } catch (error) { balancePayload = { error: error.message || 'balance_fetch_failed' }; } if ((balanceAmount === undefined || balanceAmount === null || balanceAmount === '') && address) { try { const directBalance = await qortalRequest('GET_BALANCE', { address: address }); balanceAmount = extractBalanceAmount(directBalance); if (balanceAmount !== undefined && balanceAmount !== null && balanceAmount !== '') { balancePayload = directBalance; } } catch (_error) { // continue fallback } } if ((balanceAmount === undefined || balanceAmount === null || balanceAmount === '') && address) { try { const nodeBalance = await fetchNodeBalance(address); balanceAmount = extractBalanceAmount(nodeBalance); if (balanceAmount !== undefined && balanceAmount !== null && balanceAmount !== '') { balancePayload = nodeBalance; } } catch (_error) { // ignore } } let names = []; if (address) { try { names = await qortalRequest('GET_ACCOUNT_NAMES', { address: address }); } catch (_error) { names = []; } } if (!Array.isArray(names) && names && typeof names === 'object' && Array.isArray(names.names)) { names = names.names; } if (!Array.isArray(names)) { names = []; } let primaryName = await fetchPrimaryName(address || ''); if (!primaryName && Array.isArray(names) && names.length > 0) { const first = names[0]; if (typeof first === 'string' && first.trim() !== '') { primaryName = first.trim(); } else if (first && typeof first === 'object' && typeof first.name === 'string' && first.name.trim() !== '') { primaryName = first.name.trim(); } } let accountDetails = null; try { accountDetails = await qortalRequest('GET_USER_ACCOUNT', {}); } catch (error) { if (address) { try { accountDetails = await qortalRequest('GET_ACCOUNT_DATA', { address: address }); } catch (err) { accountDetails = { error: err.message || 'account_fetch_failed' }; } } else { accountDetails = { error: error.message || 'account_fetch_failed' }; } } activeWalletContext = { hasWallet: true, mapping: mapping, wallet: wallet, address: address || '', balance: balanceAmount, primaryName: primaryName, names: names, accountDetails: accountDetails, }; updateWalletFields(mapping, wallet, balancePayload); updateDashboardState(activeWalletContext); applyRegisterHighlight(); if (detailsEl) { detailsEl.textContent = JSON.stringify(accountDetails, null, 2); } await refreshTransactions(activeWalletContext); await refreshWalletLockState(mapping.walletId || (wallet && wallet.walletId) || ''); await refreshInitialQortRequests(); setStatus(context === 'test' ? 'test ok' : 'ok', false); } catch (error) { if (detailsEl) { detailsEl.textContent = error.message || 'Failed to refresh'; } setStatus(error.message || 'error', true); } finally { updateLastRefresh(); inFlight = false; } } function startLive() { if (intervalId) { clearInterval(intervalId); } intervalId = window.setInterval(function () { refreshAll('auto'); }, refreshIntervalMs); liveEnabled = true; if (liveToggleButton) { liveToggleButton.textContent = 'Pause Live Refresh'; } } function stopLive() { if (intervalId) { clearInterval(intervalId); intervalId = null; } liveEnabled = false; if (liveToggleButton) { liveToggleButton.textContent = 'Resume Live Refresh'; } } setSettingsLink(); initializeSectionToggle(appGalleryToggleButton, appGalleryBodyEl, 'app_gallery', true); initializeSectionToggle(userRequestsToggleButton, userRequestsBodyEl, 'user_requests', false); initializeSectionToggle(adminRequestsToggleButton, adminRequestsBodyEl, 'admin_requests', false); initializeSectionToggle(namesToggleButton, namesBodyEl, 'names', false); renderQappsGallery(); updateRequestDetailsText(); if (adminRequestsCardEl) { adminRequestsCardEl.classList.toggle('qortal-hidden', !isAdmin); } setFeeEditable(false); refreshAll('init'); startLive(); if (refreshButton) { refreshButton.addEventListener('click', function () { refreshAll('manual'); }); } if (testButton) { testButton.addEventListener('click', function () { refreshAll('test'); }); } if (liveToggleButton) { liveToggleButton.addEventListener('click', function () { if (liveEnabled) { stopLive(); } else { startLive(); } }); } if (createButton) { createButton.addEventListener('click', function () { createWallet(); }); } if (copyAddressButton) { copyAddressButton.addEventListener('click', function () { copyAddress(); }); } if (downloadBackupButton) { downloadBackupButton.addEventListener('click', function () { downloadBackupFile(); }); } if (requestQortButton) { requestQortButton.addEventListener('click', function () { requestInitialQort(); }); } if (advancedToggleButton) { advancedToggleButton.addEventListener('click', function () { toggleAdvanced(); }); } if (sendButton) { sendButton.addEventListener('click', function () { sendQort(); }); } if (sendFeeEditButton) { sendFeeEditButton.addEventListener('click', function () { if (!sendFeeEl) { return; } setFeeEditable(sendFeeEl.readOnly); }); } if (transactionsRefreshButton) { transactionsRefreshButton.addEventListener('click', function () { refreshTransactions(activeWalletContext); }); } if (registerNameButton) { registerNameButton.addEventListener('click', function () { openRegisterModal(); }); } if (registerCancelButton) { registerCancelButton.addEventListener('click', function () { closeRegisterModal(); }); } if (registerConfirmButton) { registerConfirmButton.addEventListener('click', function () { registerName(); }); } if (registerNameInputEl) { registerNameInputEl.addEventListener('keydown', function (event) { if (event.key === 'Enter' && registerConfirmButton && !registerConfirmButton.disabled) { event.preventDefault(); registerName(); } }); } })();