Files

2762 lines
83 KiB
JavaScript

(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();
}
});
}
})();