const titles = { dashboard: ["Dashboard", "Overview for the active address."], send: ["Send", "Create and preview transaction forms."], receive: ["Receive", "Show wallet address and payment request details."], history: ["History", "Full local transaction history for the active address."], transactions: ["Transactions", "Review local and confirmed transaction activity."], assets: ["Assets", "View CLC, tokens, and balances."], nfts: ["NFTs", "View and manage non-fungible token holdings."], loans: ["Loans", "Create loans, payments, and collateral claims."], marketing: ["Marketing", "Review campaign activity from agency wallets."], addressbook: ["Address Book", "Manage known addresses and labels."], settings: ["Settings", "Manage wallets, nodes, security, and app preferences."] }; let currentTransactionType = ""; function setScopedTransactionMessage(formType, element, message, type = "") { if (!element) { return; } if (message && (currentView !== "send" || currentTransactionType !== formType)) { return; } element.textContent = message; element.classList.toggle("hidden", !message); element.classList.toggle("error", type === "error"); element.classList.toggle("success", type === "success"); } const navButtons = document.querySelectorAll("[data-view]"); const dashboardView = document.querySelector("#dashboard-view"); const transactionView = document.querySelector("#transaction-view"); const receiveView = document.querySelector("#receive-view"); const historyView = document.querySelector("#history-view"); const assetsView = document.querySelector("#assets-view"); const nftsView = document.querySelector("#nfts-view"); const loansView = document.querySelector("#loans-view"); const marketingView = document.querySelector("#marketing-view"); const addressbookView = document.querySelector("#addressbook-view"); const settingsView = document.querySelector("#settings-view"); const viewTitle = document.querySelector("#view-title"); const viewSubtitle = document.querySelector("#view-subtitle"); const transactionTitle = document.querySelector("#transaction-panel-title"); const transactionCopy = document.querySelector("#transaction-panel-copy"); const transactionList = document.querySelector("[data-transaction-list]"); const formPanel = document.querySelector("[data-form-panel]"); const reviewPanel = document.querySelector("[data-review-panel]"); const unbroadcastListMessage = document.querySelector("[data-unbroadcast-list-message]"); const unbroadcastReviewGrid = document.querySelector("[data-unbroadcast-review-grid]"); const unbroadcastJsonPreview = document.querySelector("[data-unbroadcast-json]"); const unbroadcastReviewMessage = document.querySelector("[data-unbroadcast-review-message]"); const unbroadcastBackButton = document.querySelector("[data-unbroadcast-back]"); const unbroadcastDeleteButton = document.querySelector("[data-unbroadcast-delete]"); const unbroadcastSignButton = document.querySelector("[data-unbroadcast-sign]"); const unbroadcastBroadcastButton = document.querySelector("[data-unbroadcast-broadcast]"); const transferAssetSelect = document.querySelector("[data-transfer-asset]"); const transferReceiverInput = document.querySelector("[data-transfer-receiver]"); const transferAmountInput = document.querySelector("[data-transfer-amount]"); const transferFeeInput = document.querySelector("[data-transfer-fee]"); const transferCoin = document.querySelector("[data-transfer-coin]"); const transferNftSeries = document.querySelector("[data-transfer-nft-series]"); const transferBalance = document.querySelector("[data-transfer-balance]"); const transferSender = document.querySelector("[data-transfer-sender]"); const transferMessage = document.querySelector("[data-transfer-message]"); const transferActions = document.querySelector("[data-transfer-actions]"); const marketingCampaignForm = document.querySelector("[data-marketing-campaign-form]"); const marketingCampaignAdvertiserInput = document.querySelector("[data-marketing-campaign-advertiser]"); const marketingCampaignIdInput = document.querySelector("[data-marketing-campaign-id]"); const marketingCampaignLoadButton = document.querySelector("[data-marketing-campaign-load]"); const marketingCampaignRefreshButton = document.querySelector("[data-marketing-campaign-refresh]"); const marketingCampaignRows = document.querySelector("[data-marketing-campaign-rows]"); const marketingCampaignCards = document.querySelector("[data-marketing-campaign-cards]"); const marketingCampaignState = document.querySelector("[data-marketing-campaign-state]"); const marketingCampaignCount = document.querySelector("[data-marketing-campaign-count]"); const marketingCampaignPagination = document.querySelector("[data-marketing-campaign-pagination]"); const marketingCampaignPageInput = document.querySelector("[data-marketing-campaign-page-input]"); const marketingCampaignPageTotal = document.querySelector("[data-marketing-campaign-page-total]"); const marketingTotalRecords = document.querySelector("[data-marketing-total-records]"); const marketingTotalImpressions = document.querySelector("[data-marketing-total-impressions]"); const marketingTotalClicks = document.querySelector("[data-marketing-total-clicks]"); const marketingAvgImpressionValue = document.querySelector("[data-marketing-avg-impression-value]"); const marketingAvgClickValue = document.querySelector("[data-marketing-avg-click-value]"); const marketingTotalValue = document.querySelector("[data-marketing-total-value]"); const createTokenForm = document.querySelector("[data-create-token-form]"); const createTokenActions = document.querySelector("[data-create-token-actions]"); const createTokenTickerInput = document.querySelector("[data-create-token-ticker]"); const createTokenNumberInput = document.querySelector("[data-create-token-number]"); const createTokenFeeInput = document.querySelector("[data-create-token-fee]"); const createTokenAllowMoreInput = document.querySelector("[data-create-token-allow-more]"); const createTokenCap = document.querySelector("[data-create-token-cap]"); const createTokenCreator = document.querySelector("[data-create-token-creator]"); const createTokenFeeLabel = document.querySelector("[data-create-token-fee-label]"); const createTokenMessage = document.querySelector("[data-create-token-message]"); const issueTokenForm = document.querySelector("[data-issue-token-form]"); const issueTokenActions = document.querySelector("[data-issue-token-actions]"); const issueTokenTickerSelect = document.querySelector("[data-issue-token-ticker]"); const issueTokenNumberInput = document.querySelector("[data-issue-token-number]"); const issueTokenFeeInput = document.querySelector("[data-issue-token-fee]"); const issueTokenCreator = document.querySelector("[data-issue-token-creator]"); const issueTokenPolicy = document.querySelector("[data-issue-token-policy]"); const issueTokenFeeLabel = document.querySelector("[data-issue-token-fee-label]"); const issueTokenMessage = document.querySelector("[data-issue-token-message]"); const createNftForm = document.querySelector("[data-create-nft-form]"); const createNftActions = document.querySelector("[data-create-nft-actions]"); const createNftSeriesSelect = document.querySelector("[data-create-nft-series]"); const createNftNameInput = document.querySelector("[data-create-nft-name]"); const createNftIpfsInput = document.querySelector("[data-create-nft-ipfs]"); const createNftCountInput = document.querySelector("[data-create-nft-count]"); const createNftDescriptionInput = document.querySelector("[data-create-nft-description]"); const createNftFeeInput = document.querySelector("[data-create-nft-fee]"); const createNftCreator = document.querySelector("[data-create-nft-creator]"); const createNftTypeLabel = document.querySelector("[data-create-nft-type-label]"); const createNftCountLabel = document.querySelector("[data-create-nft-count-label]"); const createNftFeeLabel = document.querySelector("[data-create-nft-fee-label]"); const createNftMessage = document.querySelector("[data-create-nft-message]"); const swapOfferForm = document.querySelector("[data-swap-offer-form]"); const swapOfferActions = document.querySelector("[data-swap-offer-actions]"); const swapOfferAssetSelect = document.querySelector("[data-swap-offer-asset]"); const swapOfferAmountInput = document.querySelector("[data-swap-offer-amount]"); const swapOfferTipInput = document.querySelector("[data-swap-offer-tip]"); const swapRequestAssetSelect = document.querySelector("[data-swap-request-asset]"); const swapRequestAmountInput = document.querySelector("[data-swap-request-amount]"); const swapRequestTipInput = document.querySelector("[data-swap-request-tip]"); const swapCounterpartyInput = document.querySelector("[data-swap-counterparty]"); const swapExpirationInput = document.querySelector("[data-swap-expiration]"); const swapSender = document.querySelector("[data-swap-sender]"); const swapAvailable = document.querySelector("[data-swap-available]"); const swapFees = document.querySelector("[data-swap-fees]"); const swapSavedPathPanel = document.querySelector("[data-swap-saved-path-panel]"); const swapSavedPathInput = document.querySelector("[data-swap-saved-path]"); const swapCopyPathButton = document.querySelector("[data-swap-copy-path]"); const swapOfferMessage = document.querySelector("[data-swap-offer-message]"); const loanOfferForm = document.querySelector("[data-loan-offer-form]"); const loanOfferActions = document.querySelector("[data-loan-offer-actions]"); const loanAssetSelect = document.querySelector("[data-loan-asset]"); const loanAmountInput = document.querySelector("[data-loan-amount]"); const loanBorrowerInput = document.querySelector("[data-loan-borrower]"); const loanStartInput = document.querySelector("[data-loan-start]"); const loanCollateralSelect = document.querySelector("[data-loan-collateral]"); const loanCollateralAmountInput = document.querySelector("[data-loan-collateral-amount]"); const loanPaymentPeriodSelect = document.querySelector("[data-loan-payment-period]"); const loanPaymentNumberInput = document.querySelector("[data-loan-payment-number]"); const loanPaymentAmountInput = document.querySelector("[data-loan-payment-amount]"); const loanGracePeriodInput = document.querySelector("[data-loan-grace-period]"); const loanMaxLateValueInput = document.querySelector("[data-loan-max-late-value]"); const loanFeeInput = document.querySelector("[data-loan-fee]"); const loanLender = document.querySelector("[data-loan-lender]"); const loanAvailable = document.querySelector("[data-loan-available]"); const loanTotalRepayment = document.querySelector("[data-loan-total-repayment]"); const loanSavedPathPanel = document.querySelector("[data-loan-saved-path-panel]"); const loanSavedPathInput = document.querySelector("[data-loan-saved-path]"); const loanCopyPathButton = document.querySelector("[data-loan-copy-path]"); const loanOfferMessage = document.querySelector("[data-loan-offer-message]"); const loanPaymentForm = document.querySelector("[data-loan-payment-form]"); const loanPaymentActions = document.querySelector("[data-loan-payment-actions]"); const loanPaymentContractSelect = document.querySelector("[data-loan-payment-contract]"); const loanPaymentValueInput = document.querySelector("[data-loan-payment-value]"); const loanPaymentTipInput = document.querySelector("[data-loan-payment-tip]"); const loanPaymentFeeInput = document.querySelector("[data-loan-payment-fee]"); const loanPaymentLender = document.querySelector("[data-loan-payment-lender]"); const loanPaymentAsset = document.querySelector("[data-loan-payment-asset]"); const loanPaymentScheduled = document.querySelector("[data-loan-payment-scheduled]"); const loanPaymentConfirmed = document.querySelector("[data-loan-payment-confirmed]"); const loanPaymentPending = document.querySelector("[data-loan-payment-pending]"); const loanPaymentRemaining = document.querySelector("[data-loan-payment-remaining]"); const loanPaymentWarning = document.querySelector("[data-loan-payment-warning]"); const loanPaymentMessage = document.querySelector("[data-loan-payment-message]"); const collateralClaimForm = document.querySelector("[data-collateral-claim-form]"); const collateralClaimActions = document.querySelector("[data-collateral-claim-actions]"); const collateralClaimContractSelect = document.querySelector("[data-collateral-claim-contract]"); const collateralClaimFeeInput = document.querySelector("[data-collateral-claim-fee]"); const collateralClaimRole = document.querySelector("[data-collateral-claim-role]"); const collateralClaimReason = document.querySelector("[data-collateral-claim-reason]"); const collateralClaimLender = document.querySelector("[data-collateral-claim-lender]"); const collateralClaimBorrower = document.querySelector("[data-collateral-claim-borrower]"); const collateralClaimLoan = document.querySelector("[data-collateral-claim-loan]"); const collateralClaimCollateral = document.querySelector("[data-collateral-claim-collateral]"); const collateralClaimConfirmed = document.querySelector("[data-collateral-claim-confirmed]"); const collateralClaimRemaining = document.querySelector("[data-collateral-claim-remaining]"); const collateralClaimMessage = document.querySelector("[data-collateral-claim-message]"); const burnForm = document.querySelector("[data-burn-form]"); const burnActions = document.querySelector("[data-burn-actions]"); const burnAssetSelect = document.querySelector("[data-burn-asset]"); const burnAmountInput = document.querySelector("[data-burn-amount]"); const burnFeeInput = document.querySelector("[data-burn-fee]"); const burnAddress = document.querySelector("[data-burn-address]"); const burnAvailable = document.querySelector("[data-burn-available]"); const burnType = document.querySelector("[data-burn-type]"); const burnFeeLabel = document.querySelector("[data-burn-fee-label]"); const burnMessage = document.querySelector("[data-burn-message]"); const marketingForm = document.querySelector("[data-marketing-form]"); const marketingActions = document.querySelector("[data-marketing-actions]"); const marketingCampaignInput = document.querySelector("[data-marketing-campaign]"); const marketingAdTypeSelect = document.querySelector("[data-marketing-ad-type]"); const marketingKeywordInput = document.querySelector("[data-marketing-keyword]"); const marketingDisplayedInput = document.querySelector("[data-marketing-displayed]"); const marketingImpressionInput = document.querySelector("[data-marketing-impression]"); const marketingClickInput = document.querySelector("[data-marketing-click]"); const marketingImpressionValueInput = document.querySelector("[data-marketing-impression-value]"); const marketingClickValueInput = document.querySelector("[data-marketing-click-value]"); const marketingFeeInput = document.querySelector("[data-marketing-fee]"); const marketingAdvertiser = document.querySelector("[data-marketing-advertiser]"); const marketingFeeLabel = document.querySelector("[data-marketing-fee-label]"); const marketingMessage = document.querySelector("[data-marketing-message]"); const vanityForm = document.querySelector("[data-vanity-form]"); const vanityActions = document.querySelector("[data-vanity-actions]"); const vanityNameInput = document.querySelector("[data-vanity-name]"); const vanityFeeInput = document.querySelector("[data-vanity-fee]"); const vanityOwner = document.querySelector("[data-vanity-owner]"); const vanityPreview = document.querySelector("[data-vanity-preview]"); const vanityFeeLabel = document.querySelector("[data-vanity-fee-label]"); const vanityMessage = document.querySelector("[data-vanity-message]"); const bridgeStatus = document.querySelector("[data-bridge-status]"); const createWalletForm = document.querySelector("[data-create-wallet-form]"); const loadWalletForm = document.querySelector("[data-load-wallet-form]"); const importKeyForm = document.querySelector("[data-import-key-form]"); const importImageForm = document.querySelector("[data-import-image-form]"); const walletNameInput = document.querySelector("[data-wallet-name]"); const walletKeyInput = document.querySelector("[data-wallet-key]"); const walletKeyConfirmInput = document.querySelector("[data-wallet-key-confirm]"); const createWalletSubmit = document.querySelector("[data-create-wallet-submit]"); const startupMessage = document.querySelector("[data-startup-message]"); const walletListSelect = document.querySelector("[data-wallet-list]"); const loadWalletKeyInput = document.querySelector("[data-load-wallet-key]"); const loadWalletSubmit = document.querySelector("[data-load-wallet-submit]"); const loadMessage = document.querySelector("[data-load-message]"); const importWalletNameInput = document.querySelector("[data-import-wallet-name]"); const importPrivateKeyInput = document.querySelector("[data-import-private-key]"); const importWalletKeyInput = document.querySelector("[data-import-wallet-key]"); const importWalletKeyConfirmInput = document.querySelector("[data-import-wallet-key-confirm]"); const importKeySubmit = document.querySelector("[data-import-key-submit]"); const importMessage = document.querySelector("[data-import-message]"); const importImageWalletNameInput = document.querySelector("[data-import-image-wallet-name]"); const importImagePathInput = document.querySelector("[data-import-image-path]"); const importImageWalletKeyInput = document.querySelector("[data-import-image-wallet-key]"); const importImageWalletKeyConfirmInput = document.querySelector("[data-import-image-wallet-key-confirm]"); const importImageSubmit = document.querySelector("[data-import-image-submit]"); const importImageMessage = document.querySelector("[data-import-image-message]"); const lockWalletButton = document.querySelector("[data-lock-wallet]"); const browseWalletButton = document.querySelector("[data-browse-wallet]"); const browsePrivateKeyImageButton = document.querySelector("[data-browse-private-key-image]"); const networkButtons = document.querySelectorAll("[data-network-toggle]"); const networkLabels = document.querySelectorAll("[data-network-label]"); const clcBalance = document.querySelector("[data-clc-balance]"); const baseBalanceLabel = document.querySelector("[data-base-balance-label]"); const balanceStatus = document.querySelector("[data-balance-status]"); const tokenBalances = document.querySelector("[data-token-balances]"); const broadcastNodeButton = document.querySelector("[data-broadcast-node]"); const registrationBadge = document.querySelector(".registration-badge"); const registrationAction = document.querySelector("[data-registration-action]"); const registrationNotice = document.querySelector("[data-registration-notice]"); const recentTransactionsBody = document.querySelector("[data-recent-transactions]"); const recentTransactionsState = document.querySelector("[data-recent-transactions-state]"); const historyTransactionsBody = document.querySelector("[data-history-transactions]"); const historyCards = document.querySelector("[data-history-cards]"); const historyState = document.querySelector("[data-history-state]"); const historyCount = document.querySelector("[data-history-count]"); const historyPagination = document.querySelector("[data-history-pagination]"); const historyPageInput = document.querySelector("[data-history-page-input]"); const historyPageTotal = document.querySelector("[data-history-page-total]"); const historySearch = document.querySelector("[data-history-search]"); const historyExportOpen = document.querySelector("[data-history-export-open]"); const historyExportModal = document.querySelector("[data-history-export-modal]"); const historyExportClose = document.querySelector("[data-history-export-close]"); const historyExportMessage = document.querySelector("[data-history-export-message]"); const historyExportFormatButtons = document.querySelectorAll("[data-history-export-format]"); const assetsSearch = document.querySelector("[data-assets-search]"); const assetsSort = document.querySelector("[data-assets-sort]"); const assetsFilterButtons = document.querySelectorAll("[data-assets-filter]"); const assetsCards = document.querySelector("[data-assets-cards]"); const assetsState = document.querySelector("[data-assets-state]"); const loansSearch = document.querySelector("[data-loans-search]"); const loansRoleFilter = document.querySelector("[data-loans-role-filter]"); const loansStatusButtons = document.querySelectorAll("[data-loans-status-filter]"); const loansRefreshButton = document.querySelector("[data-loans-refresh]"); const loansCreateButton = document.querySelector("[data-loans-create]"); const loansList = document.querySelector("[data-loans-list]"); const loansState = document.querySelector("[data-loans-state]"); const loanDetailGrid = document.querySelector("[data-loan-detail-grid]"); const loanPaymentHistory = document.querySelector("[data-loan-payment-history]"); const loanCopyContractButton = document.querySelector("[data-loan-copy-contract]"); const loanMakePaymentButton = document.querySelector("[data-loan-make-payment]"); const loanClaimCollateralButton = document.querySelector("[data-loan-claim-collateral]"); const networkInfoHeight = document.querySelector("[data-network-info-height]"); const networkInfoNetwork = document.querySelector("[data-network-info-network]"); const networkInfoDifficulty = document.querySelector("[data-network-info-difficulty]"); const networkInfoMempool = document.querySelector("[data-network-info-mempool]"); const networkInfoChainTx = document.querySelector("[data-network-info-chain-tx]"); const nodeInfoNode = document.querySelector("[data-node-info-node]"); const nodeInfoHeight = document.querySelector("[data-node-info-height]"); const nodeInfoTime = document.querySelector("[data-node-info-time]"); const nodeInfoLastChecked = document.querySelector("[data-node-info-last-checked]"); const nodeInfoLargestFee = document.querySelector("[data-node-info-largest-fee]"); const paymentAssetSelect = document.querySelector("[data-payment-asset]"); const paymentAmountInput = document.querySelector("[data-payment-amount]"); const paymentLabelInput = document.querySelector("[data-payment-label]"); const paymentUriInput = document.querySelector("[data-payment-uri]"); const topbarPaymentUriForm = document.querySelector("[data-payment-uri-entry]"); const topbarPaymentUriInput = document.querySelector("[data-topbar-payment-uri]"); const paymentQrCanvas = document.querySelector("[data-payment-qr]"); const paymentQrLabel = document.querySelector("[data-payment-qr-label]"); const copyPaymentQrButton = document.querySelector("[data-copy-payment-qr]"); const copyPaymentUriButton = document.querySelector("[data-copy-payment-uri]"); const paymentMessage = document.querySelector("[data-payment-message]"); const receiveRegistration = document.querySelector("[data-receive-registration]"); const addressBookRows = document.querySelector("[data-address-book-rows]"); const addressBookCards = document.querySelector("[data-address-book-cards]"); const addressBookSearch = document.querySelector("[data-address-book-search]"); const addressBookSort = document.querySelector("[data-address-book-sort]"); const addressBookState = document.querySelector("[data-address-book-state]"); const addressBookRefreshVanity = document.querySelector("[data-address-book-refresh-vanity]"); const settingsSaveButton = document.querySelector("[data-settings-save]"); const settingsMessage = document.querySelector("[data-settings-message]"); const settingsWalletPath = document.querySelector("[data-settings-wallet-path]"); const settingsDefaultNode = document.querySelector("[data-settings-default-node]"); const settingsSavedNodes = document.querySelector("[data-settings-saved-nodes]"); const settingsUnbroadcastPath = document.querySelector("[data-settings-unbroadcast-path]"); const settingsAddressBookPath = document.querySelector("[data-settings-address-book-path]"); const settingsIpfsGateway = document.querySelector("[data-settings-ipfs-gateway]"); const settingsAutoLockTimeout = document.querySelector("[data-settings-auto-lock-timeout]"); const settingsClipboardTimeout = document.querySelector("[data-settings-clipboard-timeout]"); const settingsRequireKey = document.querySelector("[data-settings-require-key]"); const settingsBrowseButtons = document.querySelectorAll("[data-settings-browse]"); const settingsTestNodeButton = document.querySelector("[data-settings-test-node]"); const settingsNodeTestStatus = document.querySelector("[data-settings-node-test-status]"); const settingsCacheOpenButton = document.querySelector("[data-settings-cache-open]"); const cacheClearModal = document.querySelector("[data-cache-clear-modal]"); const cacheClearCloseButton = document.querySelector("[data-cache-clear-close]"); const cacheClearMessage = document.querySelector("[data-cache-clear-message]"); const nftsList = document.querySelector("[data-nfts-list]"); const nftsState = document.querySelector("[data-nfts-state]"); const nftsRefreshButton = document.querySelector("[data-nfts-refresh]"); const nftsPagination = document.querySelector("[data-nfts-pagination]"); const nftsPageInput = document.querySelector("[data-nfts-page-input]"); const nftsPageTotal = document.querySelector("[data-nfts-page-total]"); const nftDetailTitle = document.querySelector("[data-nft-detail-title]"); const nftDetailSubtitle = document.querySelector("[data-nft-detail-subtitle]"); const nftDetailName = document.querySelector("[data-nft-detail-name]"); const nftDetailAsset = document.querySelector("[data-nft-detail-asset]"); const nftDetailCreator = document.querySelector("[data-nft-detail-creator]"); const nftDetailGenesis = document.querySelector("[data-nft-detail-genesis]"); const nftDetailMetadata = document.querySelector("[data-nft-detail-metadata]"); const nftMetadataSummary = document.querySelector("[data-nft-metadata-summary]"); const nftHistory = document.querySelector("[data-nft-history]"); let activeNetwork = "testnet"; let currentView = "dashboard"; let walletLoaded = false; let registrationRegistered = false; let registrationCheckInFlight = false; let registrationRequestId = 0; let dashboardRegisteredLoadId = 0; let balanceRefreshTimer = null; let balanceRefreshInFlight = false; let balanceRefreshRequestId = 0; let dashboardLastBalanceStatus = ""; let activeWalletBalances = []; let activeUnbroadcastTransaction = null; let unbroadcastRefreshInFlight = false; let unbroadcastReviewInFlight = false; let networkInfoRefreshTimer = null; let nodeInfoRefreshTimer = null; let networkInfoRefreshInFlight = false; let nodeInfoRefreshInFlight = false; let recentTransactionsCacheReadId = 0; let latestTransactionsSyncId = 0; let latestTransactionsSyncInFlight = false; let latestDashboardTransactionsTimer = null; let latestDashboardTxids = []; let latestDashboardTransactionRecords = new Map(); let backgroundTransactionSyncId = 0; let historyRefreshTimer = null; let historyRefreshReadId = 0; let historyRefreshInFlight = false; let historyRefreshPending = false; let historyCurrentPage = 1; let historyLastRenderSignature = ""; let historySearchDebounceTimer = null; let transactionCacheMutationChain = Promise.resolve(); let guiRuntimeSettings = { auto_lock_timeout: "15 minutes", clipboard_clear_timeout: "60 seconds", require_key_before_signing: true }; let autoLockTimer = null; let clipboardClearTimer = null; let lastClipboardText = ""; let activeWalletAddressHex = ""; let activeWalletAddressShort = ""; let latestKnownBlockHeight = 0; const BALANCE_REFRESH_MS = 120000; const NETWORK_INFO_REFRESH_MS = 60000; const NODE_INFO_REFRESH_MS = 15000; const DASHBOARD_RECENT_LIMIT = 5; const BACKGROUND_HISTORY_PAGE_SIZE = 100; const BACKGROUND_SYNC_IDLE_MS = 15000; const DASHBOARD_LATEST_REFRESH_MS = 5000; const DASHBOARD_INITIAL_TX_FETCH_DELAY_MS = 500; const DASHBOARD_REFRESH_TX_FETCH_DELAY_MS = 2000; const BACKGROUND_TX_FETCH_DELAY_MS = 2000; const HISTORY_REFRESH_MS = 5000; const HISTORY_PAGE_SIZE = 100; const WALLET_RPC_RETRY_LIMIT = 3; const WALLET_RPC_BACKOFF_BASE_MS = 1500; const WALLET_RPC_BACKOFF_MAX_MS = 15000; const CONFIRMED_AFTER_BLOCKS = 20; const CHAIN_STATUS_PENDING = "pending"; const CHAIN_STATUS_FINALIZED = "finalized"; const startupTitle = document.querySelector(".startup-copy h1"); const startupDescription = document.querySelector(".startup-copy p"); const startModeButtons = document.querySelectorAll("[data-start-mode]"); const topWalletListSelect = document.querySelector("[data-top-wallet-list]"); const walletSwitchModal = document.querySelector("[data-wallet-switch-modal]"); const walletSwitchForm = document.querySelector("[data-wallet-switch-form]"); const walletSwitchName = document.querySelector("[data-wallet-switch-name]"); const walletSwitchKeyInput = document.querySelector("[data-wallet-switch-key]"); const walletSwitchSubmit = document.querySelector("[data-wallet-switch-submit]"); const walletSwitchMessage = document.querySelector("[data-wallet-switch-message]"); const walletSwitchCloseButton = document.querySelector("[data-wallet-switch-close]"); const walletSwitchCancelButton = document.querySelector("[data-wallet-switch-cancel]"); const backupKeyModal = document.querySelector("[data-backup-key-modal]"); const backupKeyForm = document.querySelector("[data-backup-key-form]"); const backupKeyTitle = document.querySelector("[data-backup-key-title]"); const backupKeyCopy = document.querySelector("[data-backup-key-copy]"); const backupKeyInput = document.querySelector("[data-backup-key-input]"); const backupKeyMessage = document.querySelector("[data-backup-key-message]"); const backupKeyCloseButton = document.querySelector("[data-backup-key-close]"); const backupKeyCancelButton = document.querySelector("[data-backup-key-cancel]"); const backupKeySubmitButton = document.querySelector("[data-backup-key-submit]"); let activeWalletPath = ""; let pendingWalletSwitch = null; let pendingBackupKeyRequest = null; const startupCopy = { load: ["Open Wallet", "Select a wallet file already on this device."], create: ["Create New Wallet", "Generate a new encrypted wallet file."], "import-key": ["Import Private Key", "Restore from a private key value."], "import-image": ["Import Private Key Image", "Restore from a base64 or image-based key backup."] }; function setActiveStartMode(mode) { startModeButtons.forEach((button) => { button.classList.toggle("primary-start", button.dataset.startMode === mode); }); const [title, description] = startupCopy[mode] || startupCopy.load; if (startupTitle) { startupTitle.textContent = title; } if (startupDescription) { startupDescription.textContent = description; } } function activeBaseCoin() { return activeNetwork === "mainnet" ? "CLC" : "CLTC"; } function shortenAddress(address) { if (!address || address.length <= 17) { return address || "No wallet"; } const suffix = address.includes(".") ? address.slice(address.lastIndexOf(".") - 4) : address.slice(-8); return `${address.slice(0, 4)}...${suffix}`; } function privateKeyImageSrc(privateKeyImage) { const image = (privateKeyImage || "").trim(); if (!image) { return ""; } return image.startsWith("data:") ? image : `data:image/png;base64,${image}`; } function setWalletImageMarks(privateKeyImage) { const imageSrc = privateKeyImageSrc(privateKeyImage); document.querySelectorAll("[data-wallet-image-mark]").forEach((item) => { if (!imageSrc) { item.classList.remove("wallet-image"); item.style.backgroundImage = ""; item.textContent = "C"; return; } item.classList.add("wallet-image"); item.style.backgroundImage = `url("${imageSrc}")`; item.textContent = ""; }); } function formatAtomicBalance(value) { const atomic = BigInt(value || "0"); const whole = atomic / 100000000n; const fractionalRaw = atomic % 100000000n; const fractional = fractionalRaw.toString().padStart(8, "0").replace(/0+$/, ""); return fractional ? `${whole}.${fractional}` : `${whole}`; } function formatNumber(value) { return Number(value || 0).toLocaleString(); } function formatNetworkTime(timestamp) { const numeric = Number(timestamp || 0); if (!numeric) { return "--"; } return new Date(numeric * 1000).toLocaleString(); } function formatLargestFee(value) { const formatted = formatAtomicBalance(value); return `${formatted} ${activeBaseCoin()}`; } function networkDisplayName(info) { const network = String(info?.network || activeNetwork || "").trim(); const prefix = String(info?.wallet_prefix || activeBaseCoin()).trim(); const label = network ? network.charAt(0).toUpperCase() + network.slice(1) : "Network"; return prefix ? `${label} / ${prefix}` : label; } async function populateTopWalletList() { const invoke = window.__TAURI__?.core?.invoke; if (!invoke || !topWalletListSelect) { return; } try { const wallets = await invoke("list_wallets", { network: activeNetwork }); topWalletListSelect.innerHTML = ""; if (!wallets.length) { const option = document.createElement("option"); option.value = ""; option.textContent = "No wallets found"; topWalletListSelect.appendChild(option); return; } wallets.forEach((wallet) => { const option = document.createElement("option"); option.value = wallet.wallet_path; option.textContent = wallet.wallet_name; if (wallet.wallet_path === activeWalletPath) { option.selected = true; } topWalletListSelect.appendChild(option); }); } catch (error) { console.error(error); } } function setBroadcastNodeOptions(nodes) { if (!broadcastNodeButton) { return; } const previous = activeBroadcastNode(); const normalized = Array.isArray(nodes) ? nodes.filter((node) => node?.address) : []; broadcastNodeButton.innerHTML = ""; if (!normalized.length) { const option = document.createElement("option"); option.value = ""; option.textContent = "No broadcast nodes"; broadcastNodeButton.appendChild(option); return; } normalized.forEach((node) => { const option = document.createElement("option"); option.value = node.address; option.textContent = node.address; option.title = node.label ? `${node.label}: ${node.address}` : node.address; broadcastNodeButton.appendChild(option); }); const selected = normalized.some((node) => node.address === previous) ? previous : normalized[0].address; broadcastNodeButton.value = selected; } async function populateBroadcastNodeList() { const invoke = window.__TAURI__?.core?.invoke; if (!invoke || !broadcastNodeButton) { return; } try { const nodes = await invoke("list_broadcast_nodes"); setBroadcastNodeOptions(nodes); } catch (error) { console.error(error); setBroadcastNodeOptions([]); } } function assetInitial(asset) { return (asset || "?").trim().slice(0, 1).toUpperCase() || "?"; } function balanceAssetLabel(balance) { if (balance.nft_series && balance.nft_series > 0) { return `${balance.asset} #${balance.nft_series}`; } return balance.asset; } let knownNftBalanceKeys = new Set(); let knownNftBalanceNode = ""; let knownNftBalanceLoadedAt = 0; let knownNftBalanceLoad = null; const DASHBOARD_NFT_BALANCE_CACHE_MS = 300_000; function balanceRegistryKey(balance) { const asset = String(balance?.asset || "").trim().toLowerCase(); const series = Number(balance?.nft_series || 0); return `${asset}|${series}`; } function isDashboardNftBalance(balance) { if (Number(balance?.nft_series || 0) > 0) { return true; } return knownNftBalanceKeys.has(balanceRegistryKey(balance)); } function dashboardNftBalanceKeysReady() { const broadcastNode = typeof activeBroadcastNode === "function" ? activeBroadcastNode() : ""; return ( Boolean(broadcastNode) && knownNftBalanceNode === broadcastNode ); } function dashboardNftBalanceKeysFresh() { const broadcastNode = typeof activeBroadcastNode === "function" ? activeBroadcastNode() : ""; return ( dashboardNftBalanceKeysReady() && Date.now() - knownNftBalanceLoadedAt < DASHBOARD_NFT_BALANCE_CACHE_MS ); } function invalidateDashboardNftBalanceKeys() { knownNftBalanceNode = ""; knownNftBalanceLoadedAt = 0; } async function refreshDashboardNftBalanceKeys() { const invoke = window.__TAURI__?.core?.invoke; const broadcastNode = typeof activeBroadcastNode === "function" ? activeBroadcastNode() : ""; if (!invoke || !broadcastNode) { return false; } if (dashboardNftBalanceKeysFresh()) { return false; } if (knownNftBalanceLoad) { return knownNftBalanceLoad; } knownNftBalanceLoad = invoke("nft_list", { broadcastNode }) .then((rows) => { const nextKeys = new Set(); (Array.isArray(rows) ? rows : []).forEach((nft) => { const name = String(nft?.nft_name || "").trim().toLowerCase(); if (!name) { return; } nextKeys.add(`${name}|${Number(nft?.series || 0)}`); }); const previous = Array.from(knownNftBalanceKeys).sort().join(","); const next = Array.from(nextKeys).sort().join(","); knownNftBalanceKeys = nextKeys; knownNftBalanceNode = broadcastNode; knownNftBalanceLoadedAt = Date.now(); return previous !== next; }) .catch(() => false) .finally(() => { knownNftBalanceLoad = null; }); return knownNftBalanceLoad; } function setBalanceStatus(message, type = "") { if (!balanceStatus) { return; } balanceStatus.textContent = message; balanceStatus.classList.toggle("error", type === "error"); balanceStatus.classList.toggle("success", type === "success"); } function setBridgeStatus(message, type = "") { if (!bridgeStatus) { return; } bridgeStatus.textContent = message; bridgeStatus.classList.toggle("error", type === "error"); bridgeStatus.classList.toggle("success", type === "success"); } function renderBalances(balances) { const normalized = Array.isArray(balances) ? balances : []; activeWalletBalances = normalized; if (typeof refreshTransferAssetOptions === "function") { refreshTransferAssetOptions(); } if (typeof refreshActiveTransactionFormAfterWalletData === "function") { refreshActiveTransactionFormAfterWalletData(); } const baseCoin = activeBaseCoin(); const baseBalance = normalized.find((balance) => { return (balance.asset || "").trim().toUpperCase() === baseCoin && !balance.nft_series; }); const nftKeysReady = dashboardNftBalanceKeysReady(); const tokenRows = normalized .filter((balance) => { const isBaseCoin = (balance.asset || "").trim().toUpperCase() === baseCoin && !balance.nft_series; return !isBaseCoin && !isDashboardNftBalance(balance); }) .sort((left, right) => { const leftBalance = BigInt(left.balance || "0"); const rightBalance = BigInt(right.balance || "0"); if (rightBalance !== leftBalance) { return rightBalance > leftBalance ? 1 : -1; } return balanceAssetLabel(left).localeCompare(balanceAssetLabel(right)); }); if (baseBalanceLabel) { baseBalanceLabel.textContent = `${baseCoin} Balance`; } if (clcBalance) { clcBalance.textContent = baseBalance ? `${formatAtomicBalance(baseBalance.balance)} ${baseCoin}` : `0 ${baseCoin}`; } if (!tokenBalances) { return; } const rerenderAfterNftKeyRefresh = () => { refreshDashboardNftBalanceKeys().then((changed) => { if (changed && Array.isArray(activeWalletBalances)) { renderBalances(activeWalletBalances); } }); }; tokenBalances.innerHTML = ""; const title = document.createElement("div"); title.className = "token-title"; title.textContent = nftKeysReady ? `Token Balances (${tokenRows.length})` : "Token Balances"; tokenBalances.appendChild(title); if (!nftKeysReady) { const pending = document.createElement("div"); pending.className = "empty-balance"; pending.textContent = "Loading token balances..."; tokenBalances.appendChild(pending); rerenderAfterNftKeyRefresh(); return; } if (!tokenRows.length) { const empty = document.createElement("div"); empty.className = "empty-balance"; empty.textContent = "No token balances found."; tokenBalances.appendChild(empty); rerenderAfterNftKeyRefresh(); return; } tokenRows.slice(0, 4).forEach((balance) => { const row = document.createElement("div"); row.className = "token-row"; const mark = document.createElement("span"); mark.className = "asset-mark mini"; mark.textContent = assetInitial(balance.asset); const asset = document.createElement("span"); asset.textContent = balanceAssetLabel(balance); const amount = document.createElement("b"); amount.textContent = formatAtomicBalance(balance.balance); row.append(mark, asset, amount); tokenBalances.appendChild(row); }); rerenderAfterNftKeyRefresh(); if (currentView === "assets" && typeof renderAssetsPage === "function") { renderAssetsPage(); } } function resetTransactionForms() { [ "resetTransferSession", "resetCreateTokenSession", "resetIssueTokenSession", "resetCreateNftSession", "resetSwapOfferSession", "resetLoanOfferSession", "resetLoanPaymentSession", "resetCollateralClaimSession", "resetBurnSession", "resetMarketingSession", "resetVanityAddressSession" ].forEach((name) => { if (typeof window[name] === "function") { window[name](); } }); [ "clearTransferForm", "clearCreateTokenForm", "clearIssueTokenForm", "clearCreateNftForm", "clearSwapOfferForm", "clearLoanOfferForm", "clearLoanPaymentForm", "clearCollateralClaimForm", "clearBurnForm", "clearMarketingForm", "clearVanityForm" ].forEach((name) => { if (typeof window[name] === "function") { window[name](); } }); [ transferMessage, createTokenMessage, issueTokenMessage, createNftMessage, swapOfferMessage, loanOfferMessage, loanPaymentMessage, collateralClaimMessage, burnMessage, marketingMessage, vanityMessage ].forEach((element) => { if (!element) { return; } element.textContent = ""; element.classList.add("hidden"); element.classList.remove("error", "success"); }); loanSavedPathPanel?.classList.add("hidden"); currentTransactionType = ""; } function setRecentTransactionsState(message, mode = "") { if (!recentTransactionsState) { return; } recentTransactionsState.textContent = message; recentTransactionsState.classList.toggle("hidden", !message); recentTransactionsState.classList.toggle("loading", mode === "loading"); } function clearRecentTransactionsRows() { if (recentTransactionsBody) { recentTransactionsBody.innerHTML = ""; } }