function renderDashboardNetworkInfo(info) { if (!info || typeof info !== "object") { return; } latestKnownBlockHeight = Number(info.height || latestKnownBlockHeight || 0); setBridgeStatus(`${networkDisplayName(info)} connected`, "success"); networkInfoHeight && (networkInfoHeight.textContent = formatNumber(info.height)); networkInfoNetwork && (networkInfoNetwork.textContent = networkDisplayName(info)); networkInfoDifficulty && (networkInfoDifficulty.textContent = formatNumber(info.next_block_difficulty)); networkInfoMempool && (networkInfoMempool.textContent = formatNumber(info.total_mempool_transactions)); networkInfoChainTx && (networkInfoChainTx.textContent = formatNumber(info.total_block_transactions)); } function renderNodeInfo(info, checkedAt = new Date()) { const broadcastNode = activeBroadcastNode(); nodeInfoNode && (nodeInfoNode.textContent = broadcastNode || "--"); if (!info || typeof info !== "object") { nodeInfoHeight && (nodeInfoHeight.textContent = "--"); nodeInfoTime && (nodeInfoTime.textContent = "--"); nodeInfoLastChecked && (nodeInfoLastChecked.textContent = "--"); nodeInfoLargestFee && (nodeInfoLargestFee.textContent = "--"); return; } nodeInfoHeight && (nodeInfoHeight.textContent = formatNumber(info.height)); nodeInfoTime && (nodeInfoTime.textContent = formatNetworkTime(info.time)); nodeInfoLastChecked && (nodeInfoLastChecked.textContent = checkedAt.toLocaleTimeString()); nodeInfoLargestFee && (nodeInfoLargestFee.textContent = formatLargestFee(info.largest_tx_fee)); } function resetNetworkInfoDisplays() { const networkName = activeNetwork === "mainnet" ? "Mainnet" : "Testnet"; networkInfoHeight && (networkInfoHeight.textContent = "--"); networkInfoNetwork && (networkInfoNetwork.textContent = `${networkName} / ${activeBaseCoin()}`); networkInfoDifficulty && (networkInfoDifficulty.textContent = "--"); networkInfoMempool && (networkInfoMempool.textContent = "--"); networkInfoChainTx && (networkInfoChainTx.textContent = "--"); setBridgeStatus(`${networkName} disconnected`, "error"); renderNodeInfo(null); } async function refreshNetworkInfo({ target = "dashboard", force = false } = {}) { if (!walletLoaded) { renderNodeInfo(null); setBridgeStatus(`${activeNetwork === "mainnet" ? "Mainnet" : "Testnet"} disconnected`, "error"); return null; } const invoke = window.__TAURI__?.core?.invoke; const broadcastNode = activeBroadcastNode(); if (!invoke || !broadcastNode) { setBridgeStatus(`${activeNetwork === "mainnet" ? "Mainnet" : "Testnet"} unavailable`, "error"); return null; } if (target === "node" && nodeInfoRefreshInFlight) { return null; } if (target !== "node" && networkInfoRefreshInFlight) { return null; } if (target === "node") { nodeInfoRefreshInFlight = true; } else { networkInfoRefreshInFlight = true; } try { const info = await invokeWalletRpcWithBackoff( "Network info lookup", () => invoke("network_info", { broadcastNode }), () => walletLoaded && activeBroadcastNode() === broadcastNode ); const checkedAt = new Date(); renderDashboardNetworkInfo(info); if (target === "node" || force) { renderNodeInfo(info, checkedAt); } return info; } catch (error) { setBridgeStatus(`${activeNetwork === "mainnet" ? "Mainnet" : "Testnet"} disconnected`, "error"); if (target === "node") { nodeInfoLastChecked && (nodeInfoLastChecked.textContent = String(error)); } return null; } finally { if (target === "node") { nodeInfoRefreshInFlight = false; } else { networkInfoRefreshInFlight = false; } } } function sleepMs(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function walletRpcBackoffDelay(attempt) { return Math.min(WALLET_RPC_BACKOFF_BASE_MS * 2 ** Math.max(0, attempt - 1), WALLET_RPC_BACKOFF_MAX_MS); } async function invokeWalletRpcWithBackoff(label, operation, shouldContinue = () => true) { let lastError = null; for (let attempt = 1; attempt <= WALLET_RPC_RETRY_LIMIT; attempt += 1) { if (!shouldContinue()) { throw new Error(`${label} cancelled`); } try { return await operation(); } catch (error) { lastError = error; if (attempt >= WALLET_RPC_RETRY_LIMIT || !shouldContinue()) { break; } await sleepMs(walletRpcBackoffDelay(attempt)); } } throw lastError || new Error(`${label} failed`); } function bytesToHex(bytes) { return Array.from(bytes || []) .map((byte) => Number(byte).toString(16).padStart(2, "0")) .join(""); } function bytesToText(bytes) { return String.fromCharCode(...Array.from(bytes || [])).trim(); } function readU32LE(bytes, offset) { return ( Number(bytes[offset] || 0) | (Number(bytes[offset + 1] || 0) << 8) | (Number(bytes[offset + 2] || 0) << 16) | (Number(bytes[offset + 3] || 0) << 24) ) >>> 0; } function readU64LE(bytes, offset) { let value = 0n; for (let index = 7; index >= 0; index -= 1) { value = (value << 8n) + BigInt(Number(bytes[offset + index] || 0)); } return value; } function formatAtomicValue(value) { const atomic = typeof value === "bigint" ? value : BigInt(value || 0); const whole = atomic / 100000000n; const fraction = atomic % 100000000n; const fractionText = fraction.toString().padStart(8, "0").replace(/0+$/, ""); return fractionText ? `${whole}.${fractionText}` : whole.toString(); } function bytesToAddressHex(bytes) { return bytesToHex(bytes).toLowerCase(); } function addressLabel(addressHex) { return addressHex ? `${addressHex.slice(0, 8)}...${addressHex.slice(-8)}` : "Unknown"; } function isActiveAddress(addressHex) { return Boolean(addressHex && activeWalletAddressHex && addressHex.toLowerCase() === activeWalletAddressHex.toLowerCase()); } function isActiveShortAddress(address) { return Boolean(address && activeWalletAddressShort && String(address).trim().toLowerCase() === activeWalletAddressShort.toLowerCase()); } function isActiveWalletAddress(address) { const clean = String(address || "").trim().toLowerCase(); if (!clean) { return false; } return clean.includes(".") ? isActiveShortAddress(clean) : isActiveAddress(clean); } function assetWithSeries(asset, series) { const cleanAsset = (asset || "").trim() || activeBaseCoin(); return Number(series || 0) > 0 ? `${cleanAsset} #${series}` : cleanAsset; } function signedAmount(sign, value) { const formatted = formatAtomicValue(value); if (!formatted || formatted === "0") { return formatted; } return `${sign}${formatted}`; } function signedBaseFee(sign, value) { const amount = signedAmount(sign, value); return amount && amount !== "0" ? `${amount} ${activeBaseCoin()}` : "--"; } function statusForBlock(blockHeight, currentHeight = latestKnownBlockHeight) { if (!currentHeight || !blockHeight || currentHeight < blockHeight) { return "Pending"; } return currentHeight - blockHeight >= CONFIRMED_AFTER_BLOCKS ? "Confirmed" : "Pending"; } function walletStatusForBlock(blockHeight, currentHeight = latestKnownBlockHeight) { const height = Number(blockHeight || 0); if (!height) { return "Unconfirmed"; } return statusForBlock(height, currentHeight); } function blockValueFromRecord(record) { const block = record?.block ?? record?.block_height ?? record?.blockHeight ?? null; const numeric = Number(block || 0); return numeric > 0 ? numeric : null; } function dashboardRow(base, type, asset, counterparty, amount, fee = "--") { return { ...base, type, asset, counterparty, amount, fee }; } function txTypeName(type) { return { 0: "Genesis", 1: "Reward", 2: "Transfer", 3: "Create Token", 4: "Create NFT", 5: "Marketing", 6: "Swap", 7: "Create Loan", 8: "Loan Payment", 9: "Collateral Claim", 10: "Burn", 11: "Issue Token", 12: "Vanity Address" }[type] || "Transaction"; } function parseLoanContractSummary(transaction) { if (transaction && typeof transaction === "object" && !Array.isArray(transaction)) { const unsigned = transaction.unsigned_loan_contract; if (!unsigned) { return null; } return { loanCoin: unsigned.loan_coin || "Loan", loanAmount: unsigned.loan_amount || 0, lender: unsigned.lender || "", collateral: unsigned.collateral || "Collateral", collateralAmount: unsigned.collateral_amount || 0, borrower: unsigned.borrower || "" }; } const bytes = Array.from(transaction || []); if (Number(bytes[0] || 0) !== 7) { return null; } return { loanCoin: bytesToText(bytes.slice(5, 20)) || "Loan", loanAmount: readU64LE(bytes, 20), lender: bytesToAddressHex(bytes.slice(28, 50)), collateral: bytesToText(bytes.slice(50, 71)) || "Collateral", collateralAmount: readU64LE(bytes, 71), borrower: bytesToAddressHex(bytes.slice(79, 101)) }; }