From 03c9dba78fee6bab593e79084300163db406a4bc Mon Sep 17 00:00:00 2001 From: void-57 Date: Fri, 24 Oct 2025 16:26:45 +0530 Subject: [PATCH] Implement searched addresses database functionality and UI --- avaxSearchDB.js | 105 ++++++++++++++++ index.html | 329 ++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 320 +++++++++++++++++++++++++++++++++++++--------- 3 files changed, 697 insertions(+), 57 deletions(-) create mode 100644 avaxSearchDB.js diff --git a/avaxSearchDB.js b/avaxSearchDB.js new file mode 100644 index 0000000..41d1d71 --- /dev/null +++ b/avaxSearchDB.js @@ -0,0 +1,105 @@ +class SearchedAddressDB { + constructor() { + this.dbName = "AvaxWalletDB"; + this.version = 1; + this.storeName = "searchedAddresses"; + this.db = null; + } + + async init() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.dbName, this.version); + request.onerror = () => reject(request.error); + request.onsuccess = () => { + this.db = request.result; + resolve(); + }; + request.onupgradeneeded = (event) => { + const db = event.target.result; + if (!db.objectStoreNames.contains(this.storeName)) { + const store = db.createObjectStore(this.storeName, { + keyPath: "address", + }); + store.createIndex("timestamp", "timestamp", { unique: false }); + } + }; + }); + } + + async saveSearchedAddress( + avaxAddress, + balance, + timestamp = Date.now(), + sourceInfo = null + ) { + if (!this.db) await this.init(); + return new Promise((resolve, reject) => { + const transaction = this.db.transaction([this.storeName], "readwrite"); + const store = transaction.objectStore(this.storeName); + const getRequest = store.get(avaxAddress); + getRequest.onsuccess = () => { + const existingRecord = getRequest.result; + let finalSourceInfo = sourceInfo; + if (existingRecord && existingRecord.sourceInfo && !sourceInfo) { + finalSourceInfo = existingRecord.sourceInfo; + } else if ( + existingRecord && + existingRecord.sourceInfo && + sourceInfo === null + ) { + finalSourceInfo = existingRecord.sourceInfo; + } + const data = { + address: avaxAddress, + balance, + timestamp, + formattedBalance: `${balance} AVAX`, + sourceInfo: finalSourceInfo, + }; + const putRequest = store.put(data); + putRequest.onsuccess = () => resolve(); + putRequest.onerror = () => reject(putRequest.error); + }; + getRequest.onerror = () => reject(getRequest.error); + }); + } + + async getSearchedAddresses() { + if (!this.db) await this.init(); + return new Promise((resolve, reject) => { + const transaction = this.db.transaction([this.storeName], "readonly"); + const store = transaction.objectStore(this.storeName); + const index = store.index("timestamp"); + const request = index.getAll(); + request.onsuccess = () => { + const results = request.result.sort( + (a, b) => b.timestamp - a.timestamp + ); + resolve(results); + }; + request.onerror = () => reject(request.error); + }); + } + + async deleteSearchedAddress(avaxAddress) { + if (!this.db) await this.init(); + return new Promise((resolve, reject) => { + const transaction = this.db.transaction([this.storeName], "readwrite"); + const store = transaction.objectStore(this.storeName); + const request = store.delete(avaxAddress); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } + + async clearAllSearchedAddresses() { + if (!this.db) await this.init(); + return new Promise((resolve, reject) => { + const transaction = this.db.transaction([this.storeName], "readwrite"); + const store = transaction.objectStore(this.storeName); + const request = store.clear(); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } +} diff --git a/index.html b/index.html index 2ac1a39..a5e53f5 100644 --- a/index.html +++ b/index.html @@ -14,6 +14,7 @@ +
@@ -446,6 +447,27 @@ + + + @@ -619,6 +641,12 @@ sidebar.classList.remove("active"); sidebarOverlay.classList.remove("active"); + + if (pageId === "transactions" && searchedAddressDB) { + setTimeout(() => { + updateSearchedAddressesList(); + }, 100); + } } navLinks.forEach((link) => { @@ -1260,6 +1288,9 @@ const inputHelp = document.getElementById("inputHelp"); const input = document.getElementById("transactionInput"); const button = document.getElementById("searchButton"); + const recentAddressesContainer = document.getElementById( + "searchedAddressesContainer" + ); // Clear previous results clearSearchResults(); @@ -1272,6 +1303,11 @@ inputHelp.textContent = "Enter a AVAX address to view transactions, or use AVAX/FLO/BTC private key to derive address"; button.innerHTML = ' Search'; + + // Show recent addresses for address search + if (searchedAddressDB) { + updateSearchedAddressesList(); + } } else if (searchType === "hash") { inputLabel.innerHTML = ' Transaction Hash'; @@ -1279,6 +1315,11 @@ inputHelp.textContent = "Enter a transaction hash to view transaction details on SnowTrace explorer"; button.innerHTML = ' Lookup Hash'; + + // Hide recent addresses for hash search + if (recentAddressesContainer) { + recentAddressesContainer.style.display = "none"; + } } } @@ -1477,6 +1518,52 @@ "block"; document.getElementById("displayedAddress").textContent = address; + let sourceInfo = null; + if (isValidPrivateKey(input) && input !== address) { + // Determine which blockchain the private key belongs to + try { + const wallet = await avaxCrypto.generateMultiChain(input); + + let blockchain = null; + let originalAddress = null; + + // set sourceInfo if private key is from FLO or BTC + if (wallet.FLO && wallet.FLO.privateKey === input) { + blockchain = "FLO"; + originalAddress = wallet.FLO.address; + } else if (wallet.BTC && wallet.BTC.privateKey === input) { + blockchain = "BTC"; + originalAddress = wallet.BTC.address; + } + + //create sourceInfo if blockchain is FLO or BTC + if (blockchain && originalAddress) { + sourceInfo = { + originalPrivateKey: input, + originalAddress: originalAddress, + blockchain: blockchain, + derivedAvaxAddress: address, + }; + } + } catch (error) { + console.error("Error detecting blockchain type:", error); + } + } + + if (searchedAddressDB) { + try { + await searchedAddressDB.saveSearchedAddress( + address, + parseFloat(balance.avax), + Date.now(), + sourceInfo + ); + await updateSearchedAddressesList(); + } catch (error) { + console.error("Failed to save address to database:", error); + } + } + await loadTransactionPage(); showNotification("Loaded successfully!", "success"); @@ -1801,6 +1888,248 @@ closeConfirmationPopup(); } }); + + // Searched addresses database + let searchedAddressDB = null; + + async function initializeSearchDB() { + try { + searchedAddressDB = new SearchedAddressDB(); + await searchedAddressDB.init(); + await updateSearchedAddressesList(); + } catch (error) { + console.error("Failed to initialize search database:", error); + } + } + + async function updateSearchedAddressesList() { + try { + const searchedAddresses = + await searchedAddressDB.getSearchedAddresses(); + displaySearchedAddresses(searchedAddresses); + } catch (error) { + console.error("Error loading searched addresses:", error); + } + } + + function displaySearchedAddresses(addresses) { + const container = document.getElementById("searchedAddressesContainer"); + const list = document.getElementById("searchedAddressesList"); + + if (!container || !list) return; + + if (addresses.length === 0) { + container.style.display = "none"; + return; + } + + container.style.display = "block"; + + let html = ""; + addresses.forEach((addr, index) => { + // Check if this was derived from a private key + const hasSourceInfo = + addr.sourceInfo && + addr.sourceInfo.originalPrivateKey && + addr.sourceInfo.blockchain; + + html += ` +
+ ${ + hasSourceInfo + ? ` +
+
+ + +
+
+ ` + : "" + } +
+
+
+
+ ${ + hasSourceInfo + ? addr.sourceInfo.originalAddress + : addr.address + } +
+
+
+ + + ${addr.formattedBalance || `${addr.balance} AVAX`} + + ${new Date( + addr.timestamp + ).toLocaleDateString()} +
+
+
+ ${ + hasSourceInfo + ? ` + + ` + : ` + + ` + } + + +
+
+
+ `; + }); + + list.innerHTML = html; + } + + async function toggleAddressType(index, type) { + try { + const addresses = await searchedAddressDB.getSearchedAddresses(); + const addr = addresses[index]; + const displayElement = document.getElementById( + `address-display-${index}` + ); + const item = document.querySelector(`[data-index="${index}"]`); + + if (!displayElement || !addr.sourceInfo) return; + + const buttons = item.querySelectorAll(".btn-toggle-address"); + buttons.forEach((btn) => btn.classList.remove("active")); + item.querySelector(`[data-type="${type}"]`).classList.add("active"); + + if (type === "avax") { + displayElement.textContent = addr.address; + displayElement.title = addr.address; + } else if (type === addr.sourceInfo.blockchain.toLowerCase()) { + displayElement.textContent = addr.sourceInfo.originalAddress; + displayElement.title = addr.sourceInfo.originalAddress; + } + } catch (error) { + console.error("Error toggling address type:", error); + } + } + + // Copy current displayed address + async function copyCurrentAddress(index) { + const displayElement = document.getElementById( + `address-display-${index}` + ); + if (displayElement) { + await copyAddressToClipboard(displayElement.textContent); + } + } + + // Copy address to clipboard + async function copyAddressToClipboard(address) { + try { + await navigator.clipboard.writeText(address); + showNotification("Address copied to clipboard", "success"); + } catch (error) { + console.error("Failed to copy address:", error); + showNotification("Failed to copy address", "error"); + } + } + + // Recheck balance for an address + async function recheckBalance(address) { + try { + showNotification("Rechecking balance...", "info"); + const balance = await getBalanceRPC(address); + + if (searchedAddressDB) { + const addresses = await searchedAddressDB.getSearchedAddresses(); + const addr = addresses.find((a) => a.address === address); + await searchedAddressDB.saveSearchedAddress( + address, + parseFloat(balance.avax), + Date.now(), + addr ? addr.sourceInfo : null + ); + await updateSearchedAddressesList(); + } + + showNotification("Balance updated successfully", "success"); + } catch (error) { + console.error("Failed to recheck balance:", error); + showNotification("Failed to update balance", "error"); + } + } + + // Delete a searched address + async function deleteSearchedAddress(address) { + if (!searchedAddressDB) return; + + if ( + confirm( + `Are you sure you want to delete this address from your search history?\n\n${address}` + ) + ) { + try { + await searchedAddressDB.deleteSearchedAddress(address); + await updateSearchedAddressesList(); + showNotification("Address removed from search history", "success"); + } catch (error) { + console.error("Failed to delete searched address:", error); + showNotification("Failed to delete address", "error"); + } + } + } + + // Clear all searched addresses + async function clearAllSearchedAddresses() { + if (!searchedAddressDB) return; + + if ( + confirm( + "Are you sure you want to clear all searched addresses? This action cannot be undone." + ) + ) { + try { + await searchedAddressDB.clearAllSearchedAddresses(); + await updateSearchedAddressesList(); + showNotification("All searched addresses cleared", "success"); + } catch (error) { + console.error("Failed to clear searched addresses:", error); + showNotification("Failed to clear addresses", "error"); + } + } + } + + initializeSearchDB(); diff --git a/style.css b/style.css index ac02316..5aa8385 100644 --- a/style.css +++ b/style.css @@ -1428,63 +1428,6 @@ body { color: #ef4444; } -.tx-info { - flex: 1; -} - -.tx-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.25rem; - width: 100%; -} - -.tx-direction { - font-weight: 600; - font-size: 1rem; - display: flex; - align-items: center; - white-space: nowrap; -} - -.tx-amount { - font-weight: 600; - font-size: 1rem; -} - -.tx-amount.incoming { - color: #10b981; -} - -.tx-amount.outgoing { - color: #ef4444; -} - -.tx-date { - color: var(--text-light); - font-size: 0.8rem; -} - -.tx-status { - display: inline-block; - padding: 0.25rem 0.5rem; - font-size: 0.75rem; - font-weight: 600; - border-radius: 3px; - margin-top: 0.5rem; -} - -.tx-status.success { - background-color: rgba(40, 167, 69, 0.1); - color: var(--success-color); -} - -.tx-status.failed { - background-color: rgba(220, 53, 69, 0.1); - color: var(--danger-color); -} - /* Notification System */ .notification-drawer { position: fixed; @@ -2612,3 +2555,266 @@ body { line-height: 1.4; } } + +/* Searched Addresses */ +.searched-addresses-card { + margin-top: 2rem; + border-radius: 12px; + overflow: hidden; +} + +.searched-addresses-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid var(--border-color); +} + +.searched-addresses-header h3 { + margin: 0; + font-size: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.searched-addresses-header h3 i { + color: var(--primary-color); +} + +.btn-clear-all { + background: none; + border: none; + cursor: pointer; + color: var(--danger-color); + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.875rem; + padding: 0.25rem 0.5rem; + border-radius: 4px; + transition: var(--transition); +} + +.btn-clear-all:hover { + background-color: rgba(239, 68, 68, 0.1); +} + +.searched-addresses-list { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; + max-height: 300px; + overflow-y: auto; + width: 100%; +} + +.searched-address-item { + display: flex; + flex-direction: column; + background: var(--card-bg); + border-radius: 8px; + border: 1px solid var(--border-color); + padding: 0.75rem; + position: relative; + transition: all 0.2s ease; + width: 100%; + box-sizing: border-box; +} + +.searched-address-item:hover { + border-color: var(--primary-color); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.searched-address-item.has-source-info { + padding-bottom: 1rem; +} + +.address-toggle-section { + margin-bottom: 0.5rem; + width: 100%; +} + +.address-toggle-group { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.btn-toggle-address { + background: var(--bg-light); + border: 1px solid var(--border-color); + color: var(--text-secondary); + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-toggle-address.active { + background: var(--primary-color); + color: white; + border-color: var(--primary-color); +} + +.address-content-wrapper { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 0.5rem; +} + +.address-info { + flex-grow: 1; + min-width: 0; + width: 100%; +} + +.address-display { + display: flex; + align-items: center; + gap: 0.5rem; + width: 100%; +} + +.address-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + font-family: "Courier New", monospace; + font-size: 0.875rem; + color: var(--text-primary); +} + +.address-meta { + display: flex; + gap: 1rem; + margin-top: 0.5rem; + font-size: 0.75rem; + color: var(--text-secondary); +} + +.address-meta .balance { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.address-meta .balance i { + color: var(--primary-color); +} + +.address-meta .date { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.address-actions { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.action-btn { + background: none; + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 0.35rem 0.6rem; + font-size: 0.875rem; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.25rem; + transition: all 0.2s ease; + color: var(--text-secondary); +} + +.action-btn:hover { + background-color: var(--bg-light); +} + +.action-btn.copy-btn:hover, +.action-btn.recheck-btn:hover { + background-color: rgba(59, 130, 246, 0.1); + border-color: var(--primary-color); +} + +.action-btn.copy-btn i, +.action-btn.recheck-btn i { + color: var(--primary-color); +} + +.action-btn.delete-btn:hover { + background-color: rgba(239, 68, 68, 0.1); + border-color: var(--danger-color); +} + +.action-btn.delete-btn i { + color: var(--danger-color); +} + +/* Mobile responsive styles for searched addresses */ +@media (max-width: 768px) { + .searched-addresses-card { + margin-top: 1.5rem; + } + + .searched-addresses-header { + padding: 0.75rem; + flex-wrap: wrap; + gap: 0.5rem; + } + + .searched-addresses-header h3 { + font-size: 0.9rem; + } + + .btn-clear-all { + font-size: 0.75rem; + } + + .searched-addresses-list { + padding: 0.5rem; + max-height: 250px; + } + + .address-text { + white-space: normal; + word-break: break-all; + font-size: 0.75rem; + } + + .address-content-wrapper { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .address-actions { + width: 100%; + justify-content: flex-start; + gap: 0.35rem; + } + + .action-btn { + font-size: 0.75rem; + padding: 0.3rem 0.5rem; + } + + .address-meta { + flex-wrap: wrap; + gap: 0.5rem; + } + + .btn-toggle-address { + font-size: 0.7rem; + padding: 0.2rem 0.4rem; + } +}