const STORAGE_TRANSACTION_TYPES = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113]; const STORAGE_ZERO_HASH = "0".repeat(64); const ADDRESS_DATA_DELETE_FEE_ATOMIC = 100000000n; const addressDataGroups = document.querySelector("[data-address-data-groups]"); const addressDataState = document.querySelector("[data-address-data-state]"); const addressDataSelect = document.querySelector("[data-address-data-select]"); const addressDataRefresh = document.querySelector("[data-address-data-refresh]"); const addressDataSelection = document.querySelector("[data-address-data-selection]"); let addressDataRecords = []; let addressDataLoading = false; let addressDataWalletToken = ""; let addressDataError = ""; let addressDataLabels = { labels: {} }; let addressDataLabelsLoaded = false; let addressDataLabelSavePromise = Promise.resolve(); function escapeAddressDataText(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function trimStoredText(value) { return String(value ?? "").replace(/[\0 ]+$/g, ""); } function normalizeStorageAddress(value) { return String(value || "").trim().toLowerCase().replace(/\.cltc$/, ""); } function isActiveStorageAddress(value) { const active = normalizeStorageAddress(activeWalletAddressHex || activeWalletAddressShort); return Boolean(active && normalizeStorageAddress(value) === active); } function storageUnsignedRecord(transaction) { if (!transaction || typeof transaction !== "object" || Array.isArray(transaction)) { return null; } return transaction.unsigned_storagekey || transaction.unsigned_storagebool || transaction.unsigned_storageu8 || transaction.unsigned_storageu16 || transaction.unsigned_storageu32 || transaction.unsigned_storageu64 || transaction.unsigned_storageu128 || transaction.unsigned_storage || transaction.unsigned_storagei8 || transaction.unsigned_storagei16 || transaction.unsigned_storagei32 || transaction.unsigned_storagei64 || transaction.unsigned_storagei128 || transaction.unsigned_deletekey || null; } function storageTypeLabel(type) { return { 101: "Bool", 102: "U8", 103: "U16", 104: "U32", 105: "U64", 106: "U128", 107: "String", 108: "I8", 109: "I16", 110: "I32", 111: "I64", 112: "I128" }[Number(type)] || "Value"; } function storageDisplayValue(type, value) { if (Number(type) === 101) { return value === true || value === 1 || value === "1" ? "true" : "false"; } return trimStoredText(value); } function storageRecordTime(record, unsigned) { return Number(unsigned?.time || unsigned?.timestamp || 0); } function storageRecordOrder(left, right) { const blockDifference = Number(left.record?.block || 0) - Number(right.record?.block || 0); if (blockDifference) { return blockDifference; } const timeDifference = storageRecordTime(left.record, left.unsigned) - storageRecordTime(right.record, right.unsigned); if (timeDifference) { return timeDifference; } return left.txid.localeCompare(right.txid); } function storageTransactionIds(index) { const seen = new Set(); const txids = []; for (const type of STORAGE_TRANSACTION_TYPES) { const values = index?.type_to_txids?.[String(type)]; for (const txid of Array.isArray(values) ? values : []) { if (txid && !seen.has(txid)) { seen.add(txid); txids.push(txid); } } } return txids; } function storageRecordNeedsDecode(record) { const type = Number(record?.txtype || record?.transaction_type || 0); return STORAGE_TRANSACTION_TYPES.includes(type) && Boolean(record?.transaction?.unknown); } async function repairLegacyStorageRecords(cache, index, walletToken) { const invoke = window.__TAURI__?.core?.invoke; const broadcastNode = activeBroadcastNode(); if (!invoke || !broadcastNode) { return cache; } const stale = Object.entries(cache.transactions || {}) .filter(([, record]) => storageRecordNeedsDecode(record)); if (!stale.length) { return cache; } setAddressDataState("Updating cached storage records...", "loading"); for (const [txid] of stale) { if (!isActiveWalletCacheToken(walletToken)) { return cache; } try { const tx = await invoke("transaction_by_txid", { broadcastNode, txid }); const record = repairCachedTransactionRecord(txid, { txid, block: tx.block_height || null, transaction: tx.transaction, transaction_type: tx.transaction_type, linked_transactions: {}, miner_earnings: [] }); cache.transactions[txid] = record; const fileName = index.txid_to_file?.[txid]; if (fileName) { await writeTransactionRecordFile(fileName, record, walletToken); } } catch (_) { // Keep the remaining locally decoded records visible if one old lookup fails. } } return cache; } function buildAddressDataGroups(cache) { const records = Object.entries(cache.transactions || {}) .map(([txid, record]) => ({ txid, record, unsigned: storageUnsignedRecord(record?.transaction), type: Number(record?.txtype || decodedTransactionType(record?.transaction) || 0) })) .filter((item) => item.unsigned && isActiveStorageAddress(item.unsigned.address)) .sort(storageRecordOrder); const groups = new Map(); const stringsByTxid = new Map(records.filter((item) => item.type === 107).map((item) => [item.txid, item])); const stringDepthCache = new Map(); const stringDepth = (item, visited = new Set()) => { if (stringDepthCache.has(item.txid)) { return stringDepthCache.get(item.txid); } const previous = String(item.unsigned.previous || "").toLowerCase(); if (!previous || previous === STORAGE_ZERO_HASH) { stringDepthCache.set(item.txid, 0); return 0; } if (visited.has(item.txid)) { return null; } const parent = stringsByTxid.get(previous); if (!parent) { return null; } visited.add(item.txid); const parentDepth = stringDepth(parent, visited); const depth = parentDepth === null ? null : parentDepth + 1; stringDepthCache.set(item.txid, depth); return depth; }; const ensureGroup = (storageId) => { const normalized = String(storageId || "").toLowerCase(); if (!groups.has(normalized)) { groups.set(normalized, { storageId: normalized, created: 0, updated: 0, scalarFields: new Map(), stringFields: new Map() }); } return groups.get(normalized); }; for (const item of records) { const timestamp = storageRecordTime(item.record, item.unsigned); if (item.type === 100) { const group = ensureGroup(item.txid); group.created = timestamp; group.updated = Math.max(group.updated, timestamp); continue; } const storageId = String(item.unsigned.storagekey || "").toLowerCase(); if (!storageId) { continue; } const group = ensureGroup(storageId); const key = trimStoredText(item.unsigned.key); group.updated = Math.max(group.updated, timestamp); if (item.type === 113) { const chunkMatch = key.match(/^(.*)_(\d{2})$/); if (chunkMatch && Number(chunkMatch[2]) < 20) { group.stringFields.get(chunkMatch[1])?.delete(Number(chunkMatch[2])); } else { group.scalarFields.delete(key); group.stringFields.delete(key); } continue; } if (item.type === 107) { const depth = stringDepth(item); if (depth === null || depth < 0 || depth > 19) { continue; } if (!group.stringFields.has(key)) { group.stringFields.set(key, new Map()); } group.stringFields.get(key).set(depth, { value: trimStoredText(item.unsigned.value), timestamp, txid: item.txid }); continue; } group.scalarFields.set(key, { key, value: storageDisplayValue(item.type, item.unsigned.value), type: storageTypeLabel(item.type), timestamp, txid: item.txid }); } return Array.from(groups.values()).map((group) => { const fields = Array.from(group.scalarFields.values()); for (const [key, chunks] of group.stringFields.entries()) { const orderedChunks = Array.from(chunks.entries()).sort(([left], [right]) => left - right); if (!orderedChunks.length) { continue; } fields.push({ key, value: orderedChunks.map(([, chunk]) => chunk.value).join(""), type: "String", timestamp: Math.max(...orderedChunks.map(([, chunk]) => chunk.timestamp)), txid: orderedChunks.at(-1)?.[1]?.txid || "" }); } fields.sort((left, right) => left.key.localeCompare(right.key)); return { ...group, fields }; }).sort((left, right) => (right.updated || right.created) - (left.updated || left.created)); } function setAddressDataState(message, mode = "") { if (!addressDataState) { return; } addressDataState.textContent = message || ""; addressDataState.classList.toggle("loading", mode === "loading"); addressDataState.classList.toggle("error", mode === "error"); } function normalizeAddressDataLabels(source) { const labels = source?.labels && typeof source.labels === "object" && !Array.isArray(source.labels) ? source.labels : {}; const normalized = {}; Object.entries(labels).forEach(([storageId, label]) => { const id = String(storageId || "").trim().toLowerCase(); const text = String(label || "").trim().slice(0, 80); if (/^[a-f0-9]{64}$/.test(id) && text) { normalized[id] = text; } }); return { labels: normalized }; } async function loadAddressDataLabels() { if (!walletLoaded || addressDataLabelsLoaded) { return; } const invoke = window.__TAURI__?.core?.invoke; if (!invoke) { return; } try { const raw = await invoke("read_storage_labels"); addressDataLabels = normalizeAddressDataLabels(JSON.parse(raw || "{}")); } catch (_) { addressDataLabels = { labels: {} }; } finally { addressDataLabelsLoaded = true; } } async function saveAddressDataLabel(storageId, label) { const id = String(storageId || "").trim().toLowerCase(); const text = String(label || "").trim().slice(0, 80); const walletToken = activeWalletCacheToken(); if (!/^[a-f0-9]{64}$/.test(id)) { return; } if (text) { addressDataLabels.labels[id] = text; } else { delete addressDataLabels.labels[id]; } syncAddressDataOptions(); const status = addressDataSelection?.querySelector("[data-address-data-label-status]"); if (status) { status.textContent = "Saving..."; } addressDataLabelSavePromise = addressDataLabelSavePromise .catch(() => {}) .then(() => { if (!isActiveWalletCacheToken(walletToken)) { return; } return window.__TAURI__?.core?.invoke?.("write_storage_labels", { storageLabelsJson: JSON.stringify(normalizeAddressDataLabels(addressDataLabels)) }); }); try { await addressDataLabelSavePromise; if (status?.isConnected) { status.textContent = "Saved"; } } catch (_) { if (status?.isConnected) { status.textContent = "Unable to save label"; status.classList.add("error"); } } } function syncAddressDataOptions() { if (!addressDataSelect) { return; } const currentSelection = addressDataSelect.value; if (!addressDataRecords.length) { addressDataSelect.innerHTML = ''; addressDataSelect.disabled = true; return; } addressDataSelect.innerHTML = '' + addressDataRecords.map((group) => { const optionLabel = addressDataLabels.labels[group.storageId] || group.storageId; return ``; }).join(""); addressDataSelect.disabled = false; addressDataSelect.value = addressDataRecords.some((group) => group.storageId === currentSelection) ? currentSelection : ""; } async function knownAddressDataStorageKeys() { await loadAddressDataLabels(); await loadAddressDataPage(); return addressDataRecords.map((group) => ({ storageId: group.storageId, label: addressDataLabels.labels[group.storageId] || "" })); } function renderAddressDataPage() { if (!addressDataGroups) { return; } syncAddressDataOptions(); const selectedStorageId = addressDataSelect?.value || ""; const selectedGroup = addressDataRecords.find((group) => group.storageId === selectedStorageId); if (addressDataSelection) { addressDataSelection.classList.toggle("hidden", !selectedGroup); addressDataSelection.innerHTML = selectedGroup ? `
${escapeAddressDataText(selectedGroup.storageId)}