2762 lines
83 KiB
JavaScript
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();
|
|
}
|
|
});
|
|
}
|
|
})();
|