import { Buffer } from "buffer"; import CardanoAPI from "./lib/cardanoBlockchainAPI.js"; import CardanoSearchDB from "./lib/cardanoSearchDB.js"; window.Buffer = Buffer; const cardanoAPI = new CardanoAPI('2b5b7753-64ae-42b4-bf28-bbeee0d42e49'); // Initialize Search History Database const searchDB = new CardanoSearchDB(); searchDB.init().catch(err => console.error('[SearchDB] Init error:', err)); let currentWallet = null; let currentAddress = null; let currentPage = 1; const PAGE_LIMIT = 10; let eventListenersInitialized = false; // Validation Utilities const validators = { isCardanoAddress: (address) => { if (!address || typeof address !== 'string') return false; return address.startsWith('addr1') && address.length >= 50; }, isPrivateKey: (privateKey) => { if (!privateKey || typeof privateKey !== 'string') return false; privateKey = privateKey.trim(); // 64 hex chars (32 bytes) if (/^[a-fA-F0-9]{64}$/.test(privateKey)) return true; // With 0x prefix if (/^0x[a-fA-F0-9]{64}$/.test(privateKey)) return true; // WIF format (Bitcoin/FLO style) if (/^[5KLR][1-9A-HJ-NP-Za-km-z]{50,51}$/.test(privateKey)) return true; // Cardano Root Key (256 hex chars = 128 bytes) if (/^[a-fA-F0-9]{256}$/.test(privateKey)) return true; return false; }, isValidAmount: (amount) => { if (!amount || amount === '') return false; const num = parseFloat(amount); return !isNaN(num) && num > 0 && num <= 1000000000; }, sanitizeInput: (input) => { if (typeof input !== 'string') return ''; return input.trim().replace(/[<>'"]/g, ''); } }; // Initialize on DOM load async function initApp() { try { initializeTheme(); initializeNavigation(); initializeEventListeners(); hideLoadingScreen(); } catch (error) { console.error('Initialization error:', error); showNotification('Application initialization failed. Please refresh the page.', 'error'); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initApp); } else { initApp(); } function hideLoadingScreen() { setTimeout(() => { const loadingScreen = document.getElementById('loadingScreen'); if (!loadingScreen) return; loadingScreen.style.opacity = '0'; setTimeout(() => { loadingScreen.style.display = 'none'; }, 500); }, 1000); } // Theme Management function initializeTheme() { const savedTheme = localStorage.getItem('theme') || 'light'; document.documentElement.setAttribute('data-theme', savedTheme); updateThemeIcon(savedTheme); } function updateThemeIcon(theme) { const icon = document.querySelector('#themeToggle i'); icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon'; } document.getElementById('themeToggle').addEventListener('click', () => { const currentTheme = document.documentElement.getAttribute('data-theme'); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('theme', newTheme); updateThemeIcon(newTheme); }); // Navigation function initializeNavigation() { const navLinks = document.querySelectorAll('.nav-link, .nav-btn'); const sidebarOverlay = document.getElementById('sidebarOverlay'); const sidebar = document.getElementById('sidebar'); navLinks.forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const page = link.getAttribute('data-page'); showPage(page); navLinks.forEach(l => l.classList.remove('active')); document.querySelectorAll(`.nav-link[data-page="${page}"], .nav-btn[data-page="${page}"]`) .forEach(l => l.classList.add('active')); sidebar.classList.remove('active'); sidebarOverlay.classList.remove('active'); }); }); sidebarOverlay.addEventListener('click', () => { sidebar.classList.remove('active'); sidebarOverlay.classList.remove('active'); }); } function showPage(pageId) { document.querySelectorAll('.page').forEach(page => { page.classList.add('hidden'); }); document.getElementById(pageId + 'Page').classList.remove('hidden'); } /** * Navigate to Transactions page and view transaction details * @param {string} txHash - Transaction hash to search for */ function viewTransactionDetails(txHash) { // Navigate to transactions page showPage('transactions'); // Update navigation active state document.querySelectorAll('.nav-link, .nav-btn').forEach(link => { link.classList.remove('active'); if (link.dataset.page === 'transactions') { link.classList.add('active'); } }); // Set search type to hash const hashRadio = document.querySelector('input[name="searchType"][value="hash"]'); if (hashRadio) { hashRadio.checked = true; // Trigger the handleSearchTypeChange event const event = new Event('change'); hashRadio.dispatchEvent(event); } // Set the transaction hash in the search input const searchInput = document.getElementById('searchInput'); if (searchInput) { searchInput.value = txHash; } // Trigger the search after a short delay setTimeout(() => { if (window.searchTransactions) { window.searchTransactions(); } }, 100); } function initializeEventListeners() { if (eventListenersInitialized) return; eventListenersInitialized = true; // Generate wallet buttons document.getElementById('generateBtn').addEventListener('click', handleGenerate); // Send form document.getElementById('sendForm').addEventListener('submit', handleSendSubmission); // Send Private Key Input Listener for Balance const sendKeyInput = document.getElementById('sendPrivateKey'); if (sendKeyInput) { sendKeyInput.addEventListener('blur', handleSenderKeyUpdate); sendKeyInput.addEventListener('paste', () => setTimeout(handleSenderKeyUpdate, 100)); } // Search Type Selection const searchTypeInputs = document.querySelectorAll('input[name="searchType"]'); searchTypeInputs.forEach(input => { input.addEventListener('change', handleSearchTypeChange); }); // Transaction search document.getElementById('searchBtn').addEventListener('click', searchTransactions); document.getElementById('searchInput').addEventListener('keypress', (e) => { if (e.key === 'Enter') searchTransactions(); }); // Transaction filter buttons document.querySelectorAll('.filter-btn').forEach(btn => { btn.addEventListener('click', () => { // Remove active from all buttons document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); // Add active to clicked button btn.classList.add('active'); // Re-run search with current page (this will apply the filter) searchTransactions(currentPage); }); }); // Recover wallet buttons document.getElementById('recoverBtn').addEventListener('click', handleRecoverPrivateKey); // Modal Close Listeners const modal = document.getElementById('confirmModal'); const closeModal = document.getElementById('closeModal'); const cancelBtn = document.getElementById('cancelTxBtn'); if (closeModal) closeModal.onclick = () => modal.style.display = "none"; if (cancelBtn) cancelBtn.onclick = () => modal.style.display = "none"; window.onclick = (event) => { if (event.target == modal) { modal.style.display = "none"; } } } // Wallet Generation from BTC/FLO Key async function handleGenerate() { const outputDiv = document.getElementById('walletOutput'); const generateBtn = document.getElementById('generateBtn'); if (generateBtn.disabled) return; generateBtn.disabled = true; generateBtn.innerHTML = ' Generating...'; outputDiv.innerHTML = '
'; try { const wallet = await window.cardanoCrypto.generateWallet(); if (!wallet || !wallet.Cardano) { throw new Error('Failed to generate address - please try again'); } currentWallet = wallet; const html = `

Address Generated Successfully!

Your multi-chain address has been created.

${displayCardanoWallet(wallet.Cardano, wallet.originalKey || wallet.extractedKey)} ${wallet.BTC ? displayBlockchain('Bitcoin', 'fab fa-bitcoin', wallet.BTC) : ''} ${wallet.FLO ? displayBlockchain('FLO', 'fas fa-leaf', wallet.FLO) : ''} `; outputDiv.innerHTML = html; showNotification('Address generated successfully!', 'success'); } catch (error) { console.error('Error generating address:', error); outputDiv.innerHTML = displayError('Failed to generate address', error.message); showNotification('Failed to generate address', 'error'); } finally { generateBtn.disabled = false; generateBtn.innerHTML = ' Generate'; } } // Recover from Private Key async function handleRecoverPrivateKey() { const privateKeyInput = document.getElementById('recoverPrivateKey'); const privateKey = validators.sanitizeInput(privateKeyInput.value.trim()); const outputDiv = document.getElementById('recoverOutput'); const recoverBtn = document.getElementById('recoverBtn'); privateKeyInput.classList.remove('error'); if (!privateKey) { privateKeyInput.classList.add('error'); showNotification('Please enter a private key', 'error'); return; } if (!validators.isPrivateKey(privateKey)) { privateKeyInput.classList.add('error'); showNotification('Invalid private key format', 'error'); return; } if (recoverBtn.disabled) return; recoverBtn.disabled = true; recoverBtn.innerHTML = ' Recovering...'; outputDiv.innerHTML = '
'; try { const wallet = await window.cardanoCrypto.importFromKey(privateKey); if (!wallet || !wallet.Cardano) { throw new Error('Failed to recover address'); } currentWallet = wallet; const html = `

Address Recovered Successfully!

Your Address has been restored from private key.

${displayCardanoWallet(wallet.Cardano, wallet.originalKey || wallet.extractedKey)} ${wallet.BTC ? displayBlockchain('Bitcoin', 'fab fa-bitcoin', wallet.BTC) : ''} ${wallet.FLO ? displayBlockchain('FLO', 'fas fa-leaf', wallet.FLO) : ''} `; outputDiv.innerHTML = html; showNotification('Address recovered successfully!', 'success'); } catch (error) { console.error('Error recovering wallet:', error); privateKeyInput.classList.add('error'); outputDiv.innerHTML = displayError('Recovery Failed', error.message); showNotification('Failed to recover address', 'error'); } finally { recoverBtn.disabled = false; recoverBtn.innerHTML = ' Recover from Private Key'; } } // Handle Sender Key Update (Show Balance) async function handleSenderKeyUpdate() { const keyInput = document.getElementById('sendPrivateKey'); const balanceDisplay = document.getElementById('balanceDisplay'); const availableBalance = document.getElementById('availableBalance'); const senderAddressEl = document.getElementById('senderAddress'); const key = validators.sanitizeInput(keyInput.value.trim()); if (!key) { balanceDisplay.style.display = 'none'; return; } if (!validators.isPrivateKey(key)) { return; } try { // Show loading state balanceDisplay.style.display = 'block'; availableBalance.innerHTML = ''; senderAddressEl.textContent = 'Deriving address...'; // Recover wallet from key (handles ADA/BTC/FLO) const wallet = await window.cardanoCrypto.importFromKey(key); if (!wallet || !wallet.Cardano) { throw new Error('Could not derive Cardano wallet'); } const address = wallet.Cardano.address; senderAddressEl.textContent = address; // Fetch balance const balance = await cardanoAPI.getBalance(address); const balanceAda = (Number(BigInt(balance)) / 1000000).toFixed(6); availableBalance.innerHTML = `${balanceAda} ADA`; } catch (error) { console.error('Error fetching sender balance:', error); availableBalance.innerHTML = 'Error'; senderAddressEl.textContent = 'Error deriving address'; } } // Send Transaction async function handleSendSubmission(e) { e.preventDefault(); const sendBtn = document.getElementById('sendBtn'); const recipientInput = document.getElementById('recipientAddress'); const amountInput = document.getElementById('sendAmount'); const recipient = validators.sanitizeInput(recipientInput.value); const amount = validators.sanitizeInput(amountInput.value); if (!validators.isCardanoAddress(recipient)) { showNotification('Invalid recipient address', 'error'); return; } if (!validators.isValidAmount(amount)) { showNotification('Invalid amount', 'error'); return; } if (sendBtn.disabled) return; // UI Loading State const originalBtnText = sendBtn.innerHTML; sendBtn.disabled = true; sendBtn.innerHTML = ' Sending...'; try { // Get Private Key and Derive Wallet const keyInput = document.getElementById('sendPrivateKey'); const privateKey = validators.sanitizeInput(keyInput.value.trim()); let senderWallet; if (privateKey) { // Derive from input key senderWallet = await window.cardanoCrypto.importFromKey(privateKey); } else if (currentWallet) { // Fallback to currentWallet senderWallet = currentWallet; } else { throw new Error('Please enter a private key'); } if (!senderWallet || !senderWallet.Cardano) { throw new Error('Invalid wallet derived from private key'); } let senderPrivKeyHex = senderWallet.Cardano.spendingPrivateKeyHex; if (!senderPrivKeyHex && senderWallet.Cardano.rootKey) { // Derive if not directly available senderPrivKeyHex = await window.cardanoCrypto.getSpendPrivateKey(senderWallet.Cardano.rootKey); } if (!senderPrivKeyHex) { throw new Error('Could not retrieve spending key'); } const senderAddress = senderWallet.Cardano.address; // Convert amount to Lovelace const amountLovelace = BigInt(Math.floor(parseFloat(amount) * 1000000)).toString(); // Estimate Fee and Confirm const feeEstimate = await cardanoAPI.estimateFee( senderAddress, recipient, amountLovelace ); if (!feeEstimate.success) { throw new Error(`Fee estimation failed: ${feeEstimate.error}`); } // Check for sufficient balance if (parseFloat(feeEstimate.balanceAda) < parseFloat(feeEstimate.totalCostAda)) { throw new Error(`Insufficient balance. You have ${feeEstimate.balanceAda} ADA but need ${feeEstimate.totalCostAda} ADA (including fees).`); } // Show custom confirmation modal const confirmed = await showConfirmationModal({ amount: feeEstimate.amountAda, fee: feeEstimate.feeAda, total: feeEstimate.totalCostAda, recipient: recipient }); if (!confirmed) { showNotification('Transaction cancelled', 'info'); return; } // Send Transaction const confirmBtn = document.getElementById('confirmTxBtn'); if (confirmBtn) confirmBtn.innerHTML = ' Sending...'; const result = await cardanoAPI.sendAda( senderPrivKeyHex, senderAddress, recipient, amountLovelace ); // Close modal document.getElementById('confirmModal').style.display = 'none'; if (confirmBtn) confirmBtn.innerHTML = ' Confirm & Send'; // Success showNotification(`Transaction sent! Hash: ${result.txId}`, 'success'); // Display success details in the output area const sendOutput = document.getElementById('sendOutput'); if (sendOutput) { sendOutput.innerHTML = `

Transaction Successful!

Your transaction has been broadcast to the network.

Transaction Hash:
${result.txId}

View Transaction Details

`; } recipientInput.value = ''; amountInput.value = ''; // Refresh history if on transactions page if (document.getElementById('transactionsPage').classList.contains('hidden') === false) { searchTransactions(); } } catch (error) { console.error('Send failed:', error); // Close modal if open const modal = document.getElementById('confirmModal'); if (modal) modal.style.display = 'none'; const confirmBtn = document.getElementById('confirmTxBtn'); if (confirmBtn) confirmBtn.innerHTML = ' Confirm & Send'; showNotification(`Send failed: ${error.message}`, 'error'); // Display error details in the output area const sendOutput = document.getElementById('sendOutput'); if (sendOutput) { sendOutput.innerHTML = `

Transaction Failed

Your transaction could not be sent to the network.

Error Details:
${error.message}

Please check the error message above and try again. Common issues include insufficient balance, invalid address, or network connectivity problems.

`; } } finally { sendBtn.disabled = false; sendBtn.innerHTML = originalBtnText; } } // Search Transactions async function searchTransactions(page = 1) { if (typeof page !== 'number') page = 1; const searchBtn = document.getElementById('searchBtn'); // Prevent concurrent searches if (searchBtn.disabled) return; const originalBtnContent = searchBtn.innerHTML; searchBtn.disabled = true; searchBtn.innerHTML = ''; try { const searchTypeInput = document.querySelector('input[name="searchType"]:checked'); const searchType = searchTypeInput ? searchTypeInput.value : 'address'; const searchInput = document.getElementById('searchInput'); // Use wallet address only if searching by address const query = searchInput.value.trim() || (searchType === 'address' ? currentWallet?.Cardano?.address : ''); if (!query) { showNotification('Please enter a value to search', 'error'); return; } const txList = document.getElementById('transactionList'); const filterSection = document.getElementById('transactionFilterSection'); const paginationSection = document.getElementById('paginationSection'); const balanceSection = document.getElementById('transactionBalance'); const addressDisplay = document.getElementById('transactionAddressDisplay'); const displayedAddress = document.getElementById('displayedAddress'); const adaBalance = document.getElementById('adaBalance'); const searchedAddressesSection = document.getElementById('searchedAddressesSection'); // Handle Hash Search if (searchType === 'hash') { if (!/^[0-9a-fA-F]{64}$/.test(query)) { showNotification('Invalid transaction hash format', 'error'); return; } // Update URL for sharing if (window.updateURL) { window.updateURL('tx', query); } balanceSection.style.display = 'none'; filterSection.style.display = 'none'; paginationSection.style.display = 'none'; if (searchedAddressesSection) searchedAddressesSection.style.display = 'none'; txList.innerHTML = '
'; try { const tx = await cardanoAPI.getTransaction(query); if (!tx) { txList.innerHTML = '
Transaction not found.
'; return; } // Determine transaction status and styling const status = tx.status || 'confirmed'; const statusLabel = tx.statusLabel || 'Confirmed'; const statusColor = tx.statusColor || '#28a745'; const statusIcon = status === 'confirmed' ? 'fa-check-circle' : status === 'pending' ? 'fa-clock' : 'fa-exclamation-circle'; const date = tx.timestamp ? new Date(typeof tx.timestamp === 'string' ? tx.timestamp : tx.timestamp * 1000).toLocaleString() : 'Pending...'; const fees = tx.fees ? (Number(tx.fees) / 1000000).toFixed(6) : 'N/A'; // Calculate amounts const totalOutput = tx.outputs ? tx.outputs.reduce((acc, out) => acc + BigInt(out.value), 0n) : 0n; const totalOutputAda = (Number(totalOutput) / 1000000).toFixed(6); // Get first output amount const firstOutput = tx.outputs && tx.outputs.length > 0 ? tx.outputs[0] : null; const firstOutputAda = firstOutput ? (Number(BigInt(firstOutput.value)) / 1000000).toFixed(6) : '0.000000'; // Get From/To (first ones for simplicity) let fromAddr = tx.inputs && tx.inputs.length > 0 ? tx.inputs[0].address : 'Unknown'; let toAddr = firstOutput ? firstOutput.address : 'Unknown'; // Convert hex to bech32 if (fromAddr !== 'Unknown') fromAddr = cardanoAPI.hexToAddress(fromAddr); if (toAddr !== 'Unknown') toAddr = cardanoAPI.hexToAddress(toAddr); // Handle pending/failed transactions with message const messageHTML = tx.message ? `
${tx.message}
` : ''; txList.innerHTML = `
${messageHTML}

Transaction Details

${firstOutputAda} ADA
${date}
${statusLabel}
Transaction Hash: ${tx.hash}
From: ${fromAddr}
To (Primary): ${toAddr}
Amount Sent: ${firstOutputAda} ADA
Total Output: ${totalOutputAda} ADA
Gas Used: ${fees} ADA
Status:
${statusLabel}
Coin Type: Cardano (ADA)
`; } catch (error) { console.error('Error fetching transaction:', error); txList.innerHTML = `
Error: ${error.message}
`; } return; } // Address Search Logic let address = query; let sourceType = 'address'; let walletData = null; // Store full wallet data if derived from key // Check if input is a private key and derive address if so if (!validators.isCardanoAddress(address)) { if (validators.isPrivateKey(address)) { try { showNotification('Deriving address from private key...', 'info'); // Import wallet from key to get the address const wallet = await window.cardanoCrypto.importFromKey(address); if (wallet && wallet.Cardano && wallet.Cardano.address) { address = wallet.Cardano.address; walletData = wallet; // Determine the specific key type based on which addresses are available if (wallet.BTC && wallet.FLO) { sourceType = 'btc-flo-key'; // Has both BTC and FLO } else if (wallet.BTC) { sourceType = 'btc-key'; // BTC only } else if (wallet.FLO) { sourceType = 'flo-key'; // FLO only } else { sourceType = 'ada-key'; // Cardano only } showNotification('Address derived successfully', 'success'); } else { throw new Error('Could not derive Cardano address from key'); } } catch (error) { console.error('Key derivation failed:', error); showNotification('Failed to derive address from private key: ' + error.message, 'error'); return; } } else { showNotification('Invalid Cardano address or private key format', 'error'); return; } } // Update URL for sharing (after deriving address if needed) if (window.updateURL) { window.updateURL('address', address); } currentPage = page; filterSection.style.display = 'block'; balanceSection.style.display = 'block'; addressDisplay.style.display = 'block'; displayedAddress.textContent = address; txList.innerHTML = '
'; try { // Fetch balance const balance = await cardanoAPI.getBalance(address); const balanceAda = (Number(BigInt(balance)) / 1000000).toFixed(6); adaBalance.innerHTML = `${balanceAda} ADA`; // Save to search history try { // Prepare addresses object if wallet data exists const walletAddresses = walletData ? { BTC: walletData.BTC?.address || null, FLO: walletData.FLO?.address || null, Cardano: walletData.Cardano?.address || address } : null; // Only save if we have new data (from private key) or it's a new address if (walletData || sourceType !== 'address') { await searchDB.saveSearchedAddress(address, balanceAda, Date.now(), sourceType, walletAddresses); } else { // Just update the balance and timestamp without changing sourceType/addresses await searchDB.updateBalance(address, balanceAda, Date.now()); } await loadSearchedAddresses(); } catch (dbError) { console.error('[SearchDB] Error saving address:', dbError); } // Fetch history const history = await cardanoAPI.getHistory(address, currentPage, PAGE_LIMIT); if (!history || history.length === 0) { txList.innerHTML = `

No Transactions Yet

This address has no transaction history.

`; paginationSection.style.display = 'none'; return; } const activeFilter = document.querySelector('.filter-btn.active').dataset.filter; const filteredHistory = history.filter(tx => { const isIncoming = BigInt(tx.netAmount) > 0n; if (activeFilter === 'received') return isIncoming; if (activeFilter === 'sent') return !isIncoming; return true; }); if (filteredHistory.length === 0) { txList.innerHTML = `

No Matching Transactions

No transactions match the selected filter.

`; return; } // Render transactions txList.innerHTML = filteredHistory.map(tx => { const isIncoming = BigInt(tx.netAmount) > 0n; const amountAda = (Math.abs(Number(BigInt(tx.netAmount).toString())) / 1000000).toFixed(6); let date = 'Unknown Date'; if (tx.timestamp) { const timestamp = typeof tx.timestamp === 'string' ? tx.timestamp : tx.timestamp * 1000; date = new Date(timestamp).toLocaleString(); } // Determine From/To addresses let fromAddr = isIncoming ? (tx.inputs[0]?.address || 'Unknown') : address; let toAddr = isIncoming ? address : (tx.outputs[0]?.address || 'Multiple Outputs'); // Convert hex to bech32 if needed if (fromAddr !== 'Unknown') fromAddr = cardanoAPI.hexToAddress(fromAddr); if (toAddr !== 'Multiple Outputs') toAddr = cardanoAPI.hexToAddress(toAddr); return `

${isIncoming ? 'Received' : 'Sent'}

${isIncoming ? '+' : '-'}${amountAda} ADA
${date}
Confirmed
Hash: ${tx.txHash}
From: ${fromAddr}
To: ${toAddr}
`; }).join(''); // Update Pagination updatePagination(history.length); } catch (error) { console.error('Error fetching transactions:', error); const txList = document.getElementById('transactionList'); if (txList) txList.innerHTML = displayError('Failed to fetch transactions', error.message); } } finally { searchBtn.disabled = false; searchBtn.innerHTML = originalBtnContent; } } function updatePagination(resultCount) { const paginationSection = document.getElementById('paginationSection'); const prevBtn = document.getElementById('prevPageBtn'); const nextBtn = document.getElementById('nextPageBtn'); const pageInfo = document.getElementById('paginationInfo'); paginationSection.style.display = 'flex'; // Update info text const start = (currentPage - 1) * PAGE_LIMIT + 1; const end = start + resultCount - 1; pageInfo.textContent = `Showing ${start}-${end} transactions`; // Update buttons prevBtn.disabled = currentPage === 1; // If we got fewer results than limit, we're on the last page nextBtn.disabled = resultCount < PAGE_LIMIT; prevBtn.onclick = () => searchTransactions(currentPage - 1); nextBtn.onclick = () => searchTransactions(currentPage + 1); } // Initialize Filter Buttons document.addEventListener('DOMContentLoaded', () => { const filterBtns = document.querySelectorAll('.filter-btn'); filterBtns.forEach(btn => { btn.addEventListener('click', () => { // Remove active class from all filterBtns.forEach(b => b.classList.remove('active')); // Add active class to clicked btn.classList.add('active'); // Reload transactions with new filter searchTransactions(1); }); }); }); // Display Functions function displayCardanoWallet(cardano, masterKey) { return `

Cardano (ADA)

Primary
${cardano.address}
${cardano.stakeAddress ? `
${cardano.stakeAddress}
` : ''} ${masterKey ? `
${masterKey}
` : ''}
${cardano.spendKeyBech32}
${cardano.stakeKeyBech32}
`; } function displayBlockchain(name, icon, data) { return `

${name}

Secondary
${data.address}
${data.privateKey}
`; } function displayError(title, message) { return `

${title}

${validators.sanitizeInput(message)}

`; } // Utility Functions window.togglePasswordVisibility = function(inputId) { const input = document.getElementById(inputId); const icon = input.nextElementSibling.querySelector('i'); if (input.type === 'password') { input.type = 'text'; icon.className = 'fas fa-eye-slash'; } else { input.type = 'password'; icon.className = 'fas fa-eye'; } }; window.clearInput = function(inputId) { const input = document.getElementById(inputId); if (input) { input.value = ''; input.classList.remove('error', 'success', 'warning'); input.focus(); } }; window.copyToClipboard = function(text) { navigator.clipboard.writeText(text).then(() => { showNotification('Copied to clipboard!', 'success'); }).catch(() => { showNotification('Failed to copy', 'error'); }); }; function showNotification(message, type = 'info') { const drawer = document.getElementById('notificationDrawer'); const notification = document.createElement('div'); notification.className = `notification ${type}`; notification.innerHTML = ` ${message} `; drawer.appendChild(notification); setTimeout(() => { notification.remove(); }, 4000); } // Mobile responsive adjustments function handleResize() { if (window.innerWidth > 768) { document.getElementById('sidebar').classList.remove('active'); document.getElementById('sidebarOverlay').classList.remove('active'); } } window.addEventListener('resize', handleResize); handleResize(); window.searchTransactions = searchTransactions; window.copyToClipboard = copyToClipboard; window.togglePasswordVisibility = togglePasswordVisibility; window.clearInput = clearInput; window.viewTransactionDetails = viewTransactionDetails; // Handle Search Type Change function handleSearchTypeChange(e) { const type = e.target.value; const addressLabel = document.getElementById('addressSearchType'); const hashLabel = document.getElementById('hashSearchType'); const searchInput = document.getElementById('searchInput'); const inputLabel = document.querySelector('label[for="searchInput"]'); const inputHelp = document.querySelector('.form-text'); // Clear previous results const balanceSection = document.getElementById('transactionBalance'); const filterSection = document.getElementById('transactionFilterSection'); const txList = document.getElementById('transactionList'); const paginationSection = document.getElementById('paginationSection'); if (balanceSection) balanceSection.style.display = 'none'; if (filterSection) filterSection.style.display = 'none'; if (paginationSection) paginationSection.style.display = 'none'; // Check if transactionList exists, otherwise try transactionResults const resultsContainer = document.getElementById('transactionList') || document.getElementById('transactionResults'); if (resultsContainer) resultsContainer.innerHTML = ''; // Clear input searchInput.value = ''; if (type === 'address') { addressLabel.classList.add('active'); hashLabel.classList.remove('active'); searchInput.placeholder = 'Enter ADA address or private key (BTC/FLO/ADA)'; if (inputLabel) inputLabel.textContent = 'ADA Address or Private Key'; if (inputHelp) inputHelp.textContent = 'Enter ADA address or ADA/FLO/BTC private key to view transactions'; // Show searched addresses section for address search loadSearchedAddresses(); } else { hashLabel.classList.add('active'); addressLabel.classList.remove('active'); searchInput.placeholder = 'Enter Transaction Hash'; if (inputLabel) inputLabel.textContent = 'Transaction Hash'; if (inputHelp) inputHelp.textContent = 'Enter a transaction hash to view its details'; // Hide searched addresses section for hash search const searchedAddressesSection = document.getElementById('searchedAddressesSection'); if (searchedAddressesSection) searchedAddressesSection.style.display = 'none'; } } // Confirmation Modal Helper function showConfirmationModal(details) { return new Promise((resolve) => { const modal = document.getElementById('confirmModal'); const confirmBtn = document.getElementById('confirmTxBtn'); const cancelBtn = document.getElementById('cancelTxBtn'); const closeBtn = document.getElementById('closeModal'); // Populate details document.getElementById('confirmAmount').textContent = `${details.amount} ADA`; document.getElementById('confirmFee').textContent = `${details.fee} ADA`; document.getElementById('confirmTotal').textContent = `${details.total} ADA`; document.getElementById('confirmRecipient').textContent = details.recipient; // Show modal modal.style.display = 'block'; // Define handlers const handleConfirm = () => { cleanup(); resolve(true); }; const handleCancel = () => { cleanup(); modal.style.display = 'none'; resolve(false); }; const cleanup = () => { confirmBtn.removeEventListener('click', handleConfirm); cancelBtn.removeEventListener('click', handleCancel); closeBtn.removeEventListener('click', handleCancel); }; // Attach listeners confirmBtn.addEventListener('click', handleConfirm); cancelBtn.addEventListener('click', handleCancel); closeBtn.addEventListener('click', handleCancel); }); } /** * Load and display searched addresses history */ async function loadSearchedAddresses() { try { const addresses = await searchDB.getSearchedAddresses(); // Check if we're in address search mode const searchTypeInput = document.querySelector('input[name="searchType"]:checked'); const searchType = searchTypeInput ? searchTypeInput.value : 'address'; // Only show if in address search mode if (searchType === 'address') { displaySearchedAddresses(addresses); } else { // Hide if in hash search mode const section = document.getElementById('searchedAddressesSection'); if (section) section.style.display = 'none'; } } catch (error) { console.error('[SearchDB] Error loading addresses:', error); } } /** * Display searched addresses in the UI */ function displaySearchedAddresses(addresses) { const section = document.getElementById('searchedAddressesSection'); const list = document.getElementById('searchedAddressesList'); if (!addresses || addresses.length === 0) { section.style.display = 'none'; return; } section.style.display = 'block'; list.innerHTML = addresses.map((item, index) => { // Determine which toggle buttons to show based on source type let toggleButtons = ''; const sourceInfo = item.sourceInfo || 'address'; const itemAddresses = item.addresses || {}; // Default to showing Cardano address const displayAddress = item.address; if (sourceInfo === 'btc-flo-key' && itemAddresses.BTC && itemAddresses.FLO) { // Has both BTC and FLO - show all three toggleButtons = `
`; } else if ((sourceInfo === 'btc-key' || sourceInfo === 'btc-flo-key') && itemAddresses.BTC) { // BTC key - show BTC and ADA only toggleButtons = `
`; } else if ((sourceInfo === 'flo-key' || sourceInfo === 'btc-flo-key') && itemAddresses.FLO && !itemAddresses.BTC) { // FLO key - show FLO and ADA only toggleButtons = `
`; } else if (sourceInfo === 'ada-key') { // Cardano-only private key - show only ADA toggleButtons = `
`; } else { // Direct ADA address - show only ADA toggleButtons = `
`; } return `
${toggleButtons}
${displayAddress.substring(0, 20)}...${displayAddress.substring(displayAddress.length - 10)}
${item.formattedBalance || item.balance + ' ADA'} ${new Date(item.timestamp).toLocaleDateString()}
`; }).join(''); } /** * Load a searched address (triggered when clicking on history item) */ async function loadSearchedAddress(address) { const searchInput = document.getElementById('searchInput'); if (searchInput) { searchInput.value = address; await searchTransactions(); } } /** * Delete a single searched address from history */ async function deleteSearchedAddress(address) { try { await searchDB.deleteSearchedAddress(address); await loadSearchedAddresses(); showNotification('Address removed from history', 'success'); } catch (error) { console.error('[SearchDB] Error deleting address:', error); showNotification('Failed to remove address', 'error'); } } /** * Switch between different chain addresses (BTC/FLO/ADA) */ function switchChainDisplay(itemIndex, chain) { const item = document.querySelector(`.searched-address-item[data-index="${itemIndex}"]`); if (!item) return; const chainData = item.querySelector('.chain-data'); const addressValue = item.querySelector('.searched-address-value'); const toggleButtons = item.querySelectorAll('.chain-toggle'); if (!chainData || !addressValue) return; // Get the address for the selected chain let newAddress = ''; if (chain === 'BTC') { newAddress = chainData.dataset.btc; } else if (chain === 'FLO') { newAddress = chainData.dataset.flo; } else { // ADA newAddress = chainData.dataset.ada; } if (!newAddress) { showNotification(`${chain} address not available`, 'error'); return; } // Update the displayed address const truncated = `${newAddress.substring(0, 20)}...${newAddress.substring(newAddress.length - 10)}`; addressValue.textContent = truncated; addressValue.dataset.chain = chain; // Update button styles toggleButtons.forEach(btn => { if (btn.textContent === chain) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); // Update the copy button to copy the currently displayed address const copyBtn = item.querySelector('.btn-icon-sm[title="Copy address"]'); if (copyBtn) { copyBtn.onclick = (e) => { e.stopPropagation(); copyToClipboard(newAddress); }; } } /** * Clear all searched addresses history */ async function clearSearchHistory() { if (!confirm('Are you sure you want to clear all searched addresses history?')) { return; } try { await searchDB.clearAllSearchedAddresses(); await loadSearchedAddresses(); showNotification('Search history cleared', 'success'); } catch (error) { console.error('[SearchDB] Error clearing history:', error); showNotification('Failed to clear history', 'error'); } } window.loadSearchedAddress = loadSearchedAddress; window.deleteSearchedAddress = deleteSearchedAddress; window.clearSearchHistory = clearSearchHistory; window.switchChainDisplay = switchChainDisplay; // Load searched addresses when transactions page is shown document.addEventListener('DOMContentLoaded', () => { loadSearchedAddresses(); // Reload when switching to transactions page const transactionsNavLinks = document.querySelectorAll('[data-page="transactions"]'); transactionsNavLinks.forEach(link => { link.addEventListener('click', () => { setTimeout(() => loadSearchedAddresses(), 100); }); }); }); /** * Updates the browser URL with search parameters * @param {string} type - 'address' or 'tx' * @param {string} value - The address or transaction hash */ function updateURL(type, value) { if (!value) return; const url = new URL(window.location); // Clear all search params first url.searchParams.delete('address'); url.searchParams.delete('tx'); // Set the new parameter url.searchParams.set(type, value); // Update URL without reloading the page window.history.pushState({}, '', url); console.log("[DeepLink] URL updated: " + type + "=" + value.substring(0, 10) + "..."); } /** * Clears search parameters from URL */ function clearURL() { const url = new URL(window.location); url.searchParams.delete('address'); url.searchParams.delete('tx'); window.history.pushState({}, '', url); } /** * Loads data from URL parameters on page load */ function loadFromURL() { const urlParams = new URLSearchParams(window.location.search); // Check for address parameter const address = urlParams.get('address'); const txHash = urlParams.get('tx'); if (address || txHash) { console.log("[DeepLink] Loading from URL parameters..."); // Navigate to transactions page first const transactionsPage = document.getElementById('transactionsPage'); if (transactionsPage) { // Hide all pages document.querySelectorAll('.page').forEach(page => page.classList.add('hidden')); // Show transactions page transactionsPage.classList.remove('hidden'); // Update navigation document.querySelectorAll('.nav-link, .nav-btn').forEach(link => { link.classList.remove('active'); if (link.dataset.page === 'transactions') { link.classList.add('active'); } }); } // Set search type and populate input const searchInput = document.getElementById('searchInput'); if (txHash) { console.log("[DeepLink] Loading transaction: " + txHash.substring(0, 10) + "..."); // Set to hash search mode const hashRadio = document.querySelector('input[name="searchType"][value="hash"]'); if (hashRadio) { hashRadio.checked = true; // Trigger visual update for radio buttons document.querySelectorAll('.radio-button-container').forEach(container => { container.classList.remove('active'); }); hashRadio.closest('.radio-button-container').classList.add('active'); if (searchInput) { searchInput.placeholder = 'Enter Transaction Hash'; } } // Populate input and trigger search if (searchInput) { searchInput.value = txHash; } setTimeout(() => { if (window.searchTransactions) { window.searchTransactions(); } }, 100); } else if (address) { console.log("[DeepLink] Loading address: " + address.substring(0, 10) + "..."); // Set to address search mode const addressRadio = document.querySelector('input[name="searchType"][value="address"]'); if (addressRadio) { addressRadio.checked = true; // Trigger visual update for radio buttons document.querySelectorAll('.radio-button-container').forEach(container => { container.classList.remove('active'); }); addressRadio.closest('.radio-button-container').classList.add('active'); if (searchInput) { searchInput.placeholder = 'Enter ADA address or ADA/FLO/BTC private key'; } } // Populate input and trigger search if (searchInput) { searchInput.value = address; } setTimeout(() => { if (window.searchTransactions) { window.searchTransactions(); } }, 100); } } } window.updateURL = updateURL; window.clearURL = clearURL; window.searchTransactions = searchTransactions; // Function to process URL parameters function processURLParams() { setTimeout(() => { try { const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('address') || urlParams.get('tx')) { loadFromURL(); } } catch (error) { console.error('[DeepLink] Error processing URL parameters:', error); } }, 1000); } // Check if page is already loaded if (document.readyState === 'complete') { processURLParams(); } else { window.addEventListener('load', processURLParams); }