From 3e001a995bbc543a77b11d5c375b32cfb9cc1873 Mon Sep 17 00:00:00 2001 From: void-57 Date: Tue, 14 Oct 2025 01:56:30 +0530 Subject: [PATCH] Implemented modal styling with animations for confirmation dialogs, improving user experience. --- index.html | 1550 ++++++++++++++++++++++++++++++++++++++++++++++------ style.css | 483 +++++++++++++++- 2 files changed, 1860 insertions(+), 173 deletions(-) diff --git a/index.html b/index.html index 75b8cb1..5679d7f 100644 --- a/index.html +++ b/index.html @@ -14,8 +14,56 @@ + + + +
@@ -126,8 +174,7 @@

Security Notice

Your private keys are generated locally and never leave your - device. Make sure to backup your keys securely before using the - wallet. + device. Make sure to backup your keys securely before using.

@@ -144,14 +191,14 @@
Private Key (TON/FLO/BTC)
+
+ Enter a TON address to view transactions, or use TON/FLO/BTC + private key to derive address +
@@ -416,6 +503,27 @@ + + + @@ -432,7 +540,9 @@
- +
- Supported formats: TON private key, Bitcoin private key, or FLO - private key + Supported formats: TON, BTC, or FLO private key
@@ -564,6 +673,13 @@ // Close mobile sidebar sidebar.classList.remove("active"); sidebarOverlay.classList.remove("active"); + + // Refresh searched addresses when showing transactions page + if (pageId === "transactions" && searchedAddressDB) { + setTimeout(() => { + updateSearchedAddressesList(); + }, 100); + } } // Handle navigation clicks @@ -710,14 +826,17 @@ // Display wallet info const tonData = wallet.TON || wallet; + // Convert TON address to friendly format + const friendlyTonAddress = await convertTob64(tonData.address); + output.innerHTML = `
-

Multi-Chain Wallet Generated Successfully!

-

Your new multi-chain wallet has been created with TON, FLO, and BTC addresses. Keep your private keys safe and secure.

+

Addresses Generated Successfully!

+

Your multi-blockchain addresses have been created. All addresses are derived from the same private key.

@@ -730,9 +849,9 @@
- ${tonData.address || "N/A"} + ${friendlyTonAddress || "N/A"} @@ -771,7 +890,7 @@
- +
${wallet.FLO?.privateKey || "N/A"}
`; - showNotification("Failed to generate wallet", "error"); + showNotification("Failed to generate ", "error"); } finally { button.disabled = false; button.innerHTML = originalHTML; @@ -937,7 +1056,6 @@ document.getElementById("recipientAddress").value; const amount = document.getElementById("sendAmount").value; const output = document.getElementById("sendOutput"); - const submitButton = e.target.querySelector('button[type="submit"]'); // Get the converted TON private key const tonPrivateKey = @@ -959,57 +1077,114 @@ return; } - const originalHTML = submitButton.innerHTML; - submitButton.disabled = true; - submitButton.innerHTML = - ' Sending...'; - - output.innerHTML = ` -
-
-

Processing Transaction

- Processing -
-
- -

Broadcasting transaction to TON network...

-
-
- `; - - try { - // Send transaction using converted TON private key - const { wallet, seqno, senderAddr } = - await tonBlockchainAPI.sendTonTransaction( - tonPrivateKey, - recipientAddress, - amount - ); - + // Validate amount + if (isNaN(amount) || parseFloat(amount) <= 0) { output.innerHTML = ` -
-
-

Waiting for Confirmation

- Confirming +
+
+
-
- -

Transaction sent! Waiting for blockchain confirmation...

-

Sent ${amount} TON to ${recipientAddress}

+
+

Invalid Amount

+

Please enter a valid amount greater than 0

`; + return; + } - // Wait for confirmation and get hash - const confirmationResult = - await tonBlockchainAPI.waitForTransactionConfirmation( - wallet, - seqno, - senderAddr + // Get sender address for confirmation + let senderAddress; + try { + if ( + typeof tonCrypto !== "undefined" && + tonCrypto.recoverFromInput + ) { + const wallet = await tonCrypto.recoverFromInput(privateKeyInput); + const tonData = wallet.TON || wallet; + senderAddress = await convertTob64(tonData.address); + } else { + // Fallback to getSenderWallet + const wallet = await tonBlockchainAPI.getSenderWallet( + privateKeyInput ); - - // Show success with hash + senderAddress = await convertTob64(wallet.address); + } + } catch (error) { output.innerHTML = ` +
+
+ +
+
+

Invalid Private Key

+

Cannot derive address from the provided private key

+
+
+ `; + return; + } + + // Show confirmation popup instead of proceeding directly + showConfirmationPopup( + senderAddress, + recipientAddress, + amount, + async function () { + const submitButton = document.querySelector( + '#sendForm button[type="submit"]' + ); + const originalHTML = submitButton.innerHTML; + submitButton.disabled = true; + submitButton.innerHTML = + ' Sending...'; + + output.innerHTML = ` +
+
+

Processing Transaction

+ Processing +
+
+ +

Broadcasting transaction to TON network...

+
+
+ `; + + try { + // Send transaction using converted TON private key + const { wallet, seqno, senderAddr } = + await tonBlockchainAPI.sendTonTransaction( + tonPrivateKey, + recipientAddress, + amount + ); + + output.innerHTML = ` +
+
+

Waiting for Confirmation

+ Confirming +
+
+ +

Transaction sent! Waiting for blockchain confirmation...

+

Sent ${amount} TON to ${recipientAddress}

+
+
+ `; + + // Wait for confirmation and get hash + const confirmationResult = + await tonBlockchainAPI.waitForTransactionConfirmation( + wallet, + seqno, + senderAddr + ); + + // Show success with hash + output.innerHTML = `
@@ -1027,7 +1202,7 @@
- +
${confirmationResult.urlHash}
`; - showNotification("Transaction confirmed successfully!", "success"); - document.getElementById("sendForm").reset(); - } catch (error) { - output.innerHTML = ` + showNotification( + "Transaction confirmed successfully!", + "success" + ); + document.getElementById("sendForm").reset(); + } catch (error) { + console.error("Send transaction error:", error); + + let errorTitle = "Transaction Failed"; + let errorMessage = + error.message || "Failed to send transaction"; + let notificationMessage = "Transaction failed"; + + // Check for specific error types + if (error.message) { + const msg = error.message.toLowerCase(); + + if ( + msg.includes("insufficient") || + msg.includes("balance") || + msg.includes("not enough") + ) { + errorTitle = "Insufficient Balance"; + errorMessage = + "You don't have enough TON balance to complete this transaction. Please check your balance and try again."; + notificationMessage = "Insufficient TON balance"; + } else if ( + msg.includes("invalid address") || + msg.includes("bad address") + ) { + errorTitle = "Invalid Address"; + errorMessage = + "The recipient address is not valid. Please check the address format and try again."; + notificationMessage = "Invalid recipient address"; + } else if ( + msg.includes("network") || + msg.includes("connection") + ) { + errorTitle = "Network Error"; + errorMessage = + "Unable to connect to the TON network. Please check your internet connection and try again."; + notificationMessage = "Network connection error"; + } else if ( + msg.includes("seqno") || + msg.includes("sequence") + ) { + errorTitle = "Transaction Sequence Error"; + errorMessage = + "Transaction sequence error. Please wait a moment and try again."; + notificationMessage = "Transaction sequence error"; + } + } + + output.innerHTML = `
-

Transaction Failed

-

${error.message || "Failed to send transaction"}

+

${errorTitle}

+

${errorMessage}

+ ${ + error.message && error.message !== errorMessage + ? `

Technical details: ${error.message}

` + : "" + }
`; - showNotification( - "Transaction failed: " + (error.message || "Unknown error"), - "error" - ); - } finally { - submitButton.disabled = false; - submitButton.innerHTML = originalHTML; - } + showNotification(notificationMessage, "error"); + } finally { + submitButton.disabled = false; + submitButton.innerHTML = originalHTML; + } + } + ); }); // Handle private key input for balance display and multi-chain conversion @@ -1164,7 +1393,7 @@ !walletData.TON.privateKey || !walletData.TON.address ) { - throw new Error("No TON wallet data found"); + throw new Error("No TON data found"); } tonPrivateKey = walletData.TON.privateKey; @@ -1243,28 +1472,10 @@ // Render current page of transactions async function renderPage() { const output = document.getElementById("transactionsOutput"); - const start = currentPage * pageSize; - const end = start + pageSize; - const pageTxs = transactions.slice(start, end); - if (pageTxs.length === 0) { - output.innerHTML = ` -
-
- -

No transactions found.

-
-
- `; - updatePagination(); - return; - } - - let transactionHTML = '
'; - - // Apply filter here instead of pre-filtering - const filteredTxs = []; - for (const tx of pageTxs) { + // First, apply filter to all transactions to get the filtered list + const filteredAllTxs = []; + for (const tx of transactions) { const hasIncoming = tx.in_msg && tx.in_msg.source; const hasOutgoing = tx.out_msgs && tx.out_msgs.length > 0; @@ -1273,11 +1484,39 @@ (currentFilter === "received" && hasIncoming) || (currentFilter === "sent" && hasOutgoing) ) { - filteredTxs.push(tx); + filteredAllTxs.push(tx); } } - if (filteredTxs.length === 0) { + // If no transactions match the filter at all + if (filteredAllTxs.length === 0) { + output.innerHTML = ` +
+
+ +

No ${ + currentFilter === "all" ? "" : currentFilter + } transactions found.

+
+
+ `; + updatePagination(); + return; + } + + // Calculate pagination based on filtered transactions + const totalFilteredPages = Math.ceil(filteredAllTxs.length / pageSize); + + if (currentPage >= totalFilteredPages) { + currentPage = Math.max(0, totalFilteredPages - 1); + } + + // Get transactions for current page from filtered results + const start = currentPage * pageSize; + const end = start + pageSize; + const pageTxs = filteredAllTxs.slice(start, end); + + if (pageTxs.length === 0) { output.innerHTML = `
@@ -1292,7 +1531,9 @@ return; } - for (const tx of filteredTxs) { + let transactionHTML = '
'; + + for (const tx of pageTxs) { const time = new Date(tx.utime * 1000).toLocaleString(); const hash = tx.hash; const success = tx.success ? "confirmed" : "failed"; @@ -1316,21 +1557,23 @@ transactionHTML += `
-
-
- +
+
+
+ +
+
+
Received
+
+${inValue} TON
+
-
-
Received
-
+${inValue} TON
+
+ ${time} + + ${success === "confirmed" ? "CONFIRMED" : "FAILED"} +
-
- ${time} - - ${success === "confirmed" ? "CONFIRMED" : "FAILED"} - -
@@ -1369,21 +1612,23 @@ transactionHTML += `
-
-
- +
+
+
+ +
+
+
Sent
+
-${outValue} TON
+
-
-
Sent
-
-${outValue} TON
+
+ ${time} + + ${success === "confirmed" ? "CONFIRMED" : "FAILED"} +
-
- ${time} - - ${success === "confirmed" ? "CONFIRMED" : "FAILED"} - -
@@ -1416,19 +1661,748 @@ updatePagination(); } - // Transaction history functionality - async function loadTransactions() { - const address = document - .getElementById("transactionAddress") - .value.trim(); + // Handle search type switching + document.querySelectorAll('input[name="searchType"]').forEach((radio) => { + radio.addEventListener("change", function () { + const containers = document.querySelectorAll( + ".radio-button-container" + ); + containers.forEach((c) => c.classList.remove("active")); + + if (this.checked) { + this.closest(".radio-button-container").classList.add("active"); + updateSearchInterface(this.value); + } + }); + }); + + function updateSearchInterface(searchType) { + const inputLabel = document.getElementById("inputLabel"); + const inputHelp = document.getElementById("inputHelp"); + const input = document.getElementById("transactionInput"); + const button = document.getElementById("searchButton"); + + clearSearchResults(); + + input.value = ""; + + if (searchType === "address") { + inputLabel.innerHTML = + ' TON Address or Private Key'; + input.placeholder = "Enter TON address, or TON/FLO/BTC private key"; + inputHelp.textContent = + "Enter a TON address to view transactions, or use TON/FLO/BTC private key to derive address"; + button.innerHTML = ' Load Transactions'; + } else if (searchType === "hash") { + inputLabel.innerHTML = + ' Transaction Hash'; + input.placeholder = "Enter transaction hash to view details"; + inputHelp.textContent = + "Enter a transaction hash to view transaction details and generate explorer URL"; + button.innerHTML = ' Lookup Hash'; + } + } + + // Clear all search results and displays + function clearSearchResults() { + // Hide balance display + const balanceDiv = document.getElementById("transactionBalance"); + balanceDiv.style.display = "none"; + + // Clear transaction output + const output = document.getElementById("transactionsOutput"); + output.innerHTML = ""; + + // Hide loading section + const loadingSection = document.getElementById("transactionLoading"); + loadingSection.style.display = "none"; + + // Hide filter section + const filterSection = document.getElementById( + "transactionFilterSection" + ); + filterSection.style.display = "none"; + + // Hide pagination section + const paginationSection = document.getElementById("paginationSection"); + paginationSection.style.display = "none"; + + // Reset transaction state + currentAddress = ""; + transactions = []; + beforeLt = null; + allFetched = false; + currentPage = 0; + currentFilter = "all"; + + // Reset filter buttons to default + document.querySelectorAll(".filter-btn").forEach((btn) => { + btn.classList.remove("active"); + }); + document.querySelector('[data-filter="all"]').classList.add("active"); + } + + async function handleSearch() { + const searchType = document.querySelector( + 'input[name="searchType"]:checked' + ).value; + const input = document.getElementById("transactionInput").value.trim(); + + if (!input) { + showNotification("Please enter a search value", "warning"); + return; + } + + if (searchType === "address") { + await loadTransactions(input); + } else if (searchType === "hash") { + await lookupTransactionHash(input); + } + } + + async function displaySearchedAddresses() { + if (!searchedAddressesDB) { + return; + } + + try { + const addresses = await searchedAddressesDB.getSearchedAddresses(); + const container = document.getElementById("searchedAddressesList"); + const containerCard = document.getElementById( + "searchedAddressesContainer" + ); + + if (!container) { + return; + } + + if (addresses.length === 0) { + console.log("No addresses found, hiding container"); + if (containerCard) { + containerCard.style.display = "none"; + } + return; + } + + if (containerCard) { + containerCard.style.display = "block"; + } + + container.innerHTML = addresses + .map((addr, index) => { + // Check if this address has source info (private key) + const hasPrivateKey = + addr.sourceInfo && addr.sourceInfo.isFromPrivateKey; + + return ` +
+ ${ + hasPrivateKey + ? ` +
+
+ + +
+
+ ` + : "" + } +
+
+
+
${addr.address}
+
+
+ + + ${ + addr.balance !== null + ? `${addr.balance.toFixed(6)} TON` + : "Unknown" + } + + ${new Date( + addr.timestamp + ).toLocaleDateString()} +
+
+
+ ${ + hasPrivateKey + ? ` + + ` + : ` + + ` + } + + +
+
+
+ `; + }) + .join(""); + console.log( + "Container innerHTML set:", + container.innerHTML.substring(0, 200) + ); + } catch (error) { + console.error("Failed to display searched addresses:", error); + } + } + + async function deleteSearchedAddress(address) { + if (!searchedAddressesDB) return; + + if ( + confirm( + `Are you sure you want to delete this address from your search history?\n\n${address}` + ) + ) { + try { + await searchedAddressesDB.deleteSearchedAddress(address); + await displaySearchedAddresses(); + showNotification("Address removed from search history", "success"); + } catch (error) { + console.error("Failed to delete searched address:", error); + showNotification("Failed to delete address", "error"); + } + } + } + + async function clearAllSearchedAddresses() { + if (!searchedAddressesDB) return; + + if ( + confirm( + "Are you sure you want to clear all searched addresses? This action cannot be undone." + ) + ) { + try { + await searchedAddressesDB.clearAllSearchedAddresses(); + await displaySearchedAddresses(); + showNotification("All searched addresses cleared", "success"); + } catch (error) { + console.error("Failed to clear searched addresses:", error); + showNotification("Failed to clear addresses", "error"); + } + } + } + + 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"); + } + } + + // Searched addresses database functionality + 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 (any blockchain) + const hasSourceInfo = + addr.sourceInfo && + addr.sourceInfo.originalPrivateKey && + addr.sourceInfo.originalAddress !== addr.address; + + html += ` +
+ ${ + hasSourceInfo + ? ` +
+
+ + +
+
+
+
+
+
+ ${addr.sourceInfo.originalAddress} +
+
+
+
+ + + +
+
+ ` + : ` +
+
+ +
+
+
+
+
+
+ ${addr.address} +
+
+
+
+ + + +
+
+ ` + } +
+ `; + }); + + list.innerHTML = html; + } + + // Toggle between address types in searched addresses + async function toggleAddressType(addressIndex, type) { + try { + const addresses = await searchedAddressDB.getSearchedAddresses(); + if (!addresses[addressIndex]) return; + + const addressItem = addresses[addressIndex]; + const container = document.querySelector( + `[data-index="${addressIndex}"]` + ); + if (!container) return; + + const toggleButtons = container.querySelectorAll( + ".btn-toggle-address" + ); + toggleButtons.forEach((btn) => btn.classList.remove("active")); + + const activeButton = container.querySelector(`[data-type="${type}"]`); + if (activeButton) { + activeButton.classList.add("active"); + } + + container.setAttribute("data-current-type", type); + + const addressDisplay = container.querySelector( + `#address-display-${addressIndex}` + ); + if (addressDisplay) { + if (type === "ton") { + // Show TON address + addressDisplay.textContent = addressItem.address; + addressDisplay.title = addressItem.address; + } else { + // Show original blockchain address (FLO/BTC) + const originalAddress = + addressItem.sourceInfo?.originalAddress || addressItem.address; + addressDisplay.textContent = originalAddress; + addressDisplay.title = originalAddress; + } + } + } catch (error) { + console.error("Error toggling address type:", error); + } + } + + async function copyCurrentAddress(addressIndex) { + try { + const addresses = await searchedAddressDB.getSearchedAddresses(); + if (!addresses[addressIndex]) return; + + const addressItem = addresses[addressIndex]; + const container = document.querySelector( + `[data-index="${addressIndex}"]` + ); + if (!container) return; + + const currentType = + container.getAttribute("data-current-type") || "ton"; + + let addressToCopy; + let addressLabel; + + if (currentType === "ton") { + addressToCopy = addressItem.address; + addressLabel = "TON address"; + } else { + addressToCopy = + addressItem.sourceInfo?.originalAddress || addressItem.address; + addressLabel = `${ + addressItem.sourceInfo?.blockchain || "Original" + } address`; + } + + await navigator.clipboard.writeText(addressToCopy); + showNotification(`${addressLabel} copied to clipboard`, "success"); + } catch (error) { + console.error("Error copying current address:", error); + showNotification("Failed to copy address", "error"); + } + } + + 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"); + } + } + + async function deleteSearchedAddress(address) { + try { + if ( + confirm( + `Are you sure you want to delete this address from your search history?` + ) + ) { + 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"); + } + } + + async function clearAllSearchedAddresses() { + try { + if ( + confirm( + "Are you sure you want to clear all searched addresses? This action cannot be undone." + ) + ) { + 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"); + } + } + + async function recheckBalance(address) { + try { + await loadTransactions(address); + showNotification("Balance rechecked successfully", "success"); + } catch (error) { + console.error("Failed to recheck balance:", error); + showNotification( + "Failed to recheck balance: " + error.message, + "error" + ); + } + } + + // Initialize database when page loads + document.addEventListener("DOMContentLoaded", () => { + initializeSearchDB(); + }); + + // Transaction hash lookup functionality + async function lookupTransactionHash(hash) { + const output = document.getElementById("transactionsOutput"); + const button = document.getElementById("searchButton"); + + if (!validateTransactionHash(hash)) { + showNotification("Invalid transaction hash format", "error"); + return; + } + + button.disabled = true; + const originalHTML = button.innerHTML; + button.innerHTML = + ' Looking up...'; + + try { + const urlHash = hash.replace(/\+/g, "-").replace(/\//g, "_"); + const tonViewerUrl = `https://tonviewer.com/transaction/${urlHash}`; + + output.innerHTML = ` +
+
+
+ +
+
+

Transaction Hash Found

+

Transaction hash validated and explorer links generated.

+
+
+ +
+
+

Transaction Details

+ Hash Lookup +
+ +
+ +
+ ${hash} + +
+
+ +
+ + +
+ +
+
+ `; + + showNotification("Transaction hash lookup successful!", "success"); + } catch (error) { + output.innerHTML = ` +
+
+ +
+
+

Hash Lookup Failed

+

Unable to process transaction hash: ${error.message}

+
+
+ `; + showNotification("Hash lookup failed", "error"); + } finally { + button.disabled = false; + button.innerHTML = originalHTML; + } + } + + // Validate transaction hash format + function validateTransactionHash(hash) { + // TON transaction hashes are typically 44 characters base64 + const base64Regex = /^[A-Za-z0-9+/]{43}=?$/; + const hexRegex = /^[a-fA-F0-9]{64}$/; + return base64Regex.test(hash) || hexRegex.test(hash); + } + + // Validate TON address format + function validateTonAddress(address) { + // TON addresses start with EQ, UQ, or kQ and are base64url encoded + const tonAddressRegex = /^[EUk]Q[A-Za-z0-9_-]{46}$/; + return tonAddressRegex.test(address); + } + + // Validate if input is a valid address or private key (not a transaction hash) + function isValidAddressOrPrivateKey(input) { + // Check if it's a TON address + if (validateTonAddress(input)) { + return true; + } + + // Check if it's a hex private key (64 or 128 characters) + const hexOnly = /^[0-9a-fA-F]+$/.test(input); + if (hexOnly && (input.length === 64 || input.length === 128)) { + return true; + } + + const base58Regex = + /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/; + if ( + base58Regex.test(input) && + input.length >= 51 && + input.length <= 56 + ) { + return true; + } + + if (input.length === 44 && /^[A-Za-z0-9+/]{43}=?$/.test(input)) { + return false; + } + + if (input.length < 32 || input.length > 128) { + return false; + } + + return true; + } + + // Enhanced transaction loading with multi-chain support + async function loadTransactions(inputValue = null) { + const input = + inputValue || + document.getElementById("transactionInput").value.trim(); const output = document.getElementById("transactionsOutput"); const balanceDiv = document.getElementById("transactionBalance"); - const button = document.querySelector( - 'button[onclick="loadTransactions()"]' - ); + const button = document.getElementById("searchButton"); - if (!address) { - showNotification("Please enter a TON address", "warning"); + if (!input) { + showNotification( + "Please enter a TON address or private key", + "warning" + ); + return; + } + + // Validate input for address search mode + if (validateTransactionHash(input)) { + output.innerHTML = ` +
+
+ +
+
+

Invalid Input for Address Search

+

You've entered a transaction hash, but this section is for TON addresses and private keys only.

+

Please switch to "Transaction Hash" mode to search for transaction hashes, or enter a valid TON address or private key (TON/FLO/BTC format).

+
+
+ `; + showNotification( + "Transaction hashes not allowed in address search mode", + "error" + ); + return; + } + + if (!isValidAddressOrPrivateKey(input)) { + output.innerHTML = ` +
+
+ +
+
+

Invalid Input Format

+

The input doesn't appear to be a valid TON address or private key.

+

Please enter:

+
    +
  • A TON address (starts with EQ, UQ, or kQ)
  • +
  • A hex private key (64 or 128 characters)
  • +
  • A TON/FLO/BTC private key
  • +
+
+
+ `; + showNotification("Invalid address or private key format", "error"); return; } @@ -1444,8 +2418,62 @@ paginationSection.style.display = "none"; button.disabled = true; + const originalHTML = button.innerHTML; + button.innerHTML = ' Loading...'; - currentAddress = address; + let finalAddress = input; + + // Validate and convert input to TON address + try { + // Check if input is already a TON address + if (validateTonAddress(input)) { + finalAddress = input; + } else { + // Try to derive TON address from private key (multi-chain support) + const hexOnly = /^[0-9a-fA-F]+$/.test(input); + + if (hexOnly && (input.length === 64 || input.length === 128)) { + // Direct TON hex private key + const tonPrivateKey = + input.length === 128 ? input.substring(0, 64) : input; + const { address } = await tonBlockchainAPI.getSenderWallet( + tonPrivateKey + ); + finalAddress = address.toString(true, true, true); + } else { + // Try multi-chain conversion (TON/FLO/BTC) + const walletData = await tonCrypto.recoverFromInput(input); + if (walletData.TON && walletData.TON.address) { + finalAddress = walletData.TON.address; + } else { + throw new Error( + "Invalid input: not a valid TON address or private key" + ); + } + } + } + } catch (conversionError) { + loadingSection.style.display = "none"; + button.disabled = false; + button.innerHTML = originalHTML; + + output.innerHTML = ` +
+
+ +
+
+

Invalid Input

+

Please enter a valid TON address or private key (TON/FLO/BTC format).

+

Error: ${conversionError.message}

+
+
+ `; + showNotification("Invalid address or private key format", "error"); + return; + } + + currentAddress = finalAddress; beforeLt = null; allFetched = false; transactions = []; @@ -1463,7 +2491,7 @@ // Show address addressDisplay.style.display = "block"; - displayedAddress.textContent = address; + displayedAddress.textContent = finalAddress; // Show loading for both balances tonBalanceElement.innerHTML = @@ -1471,12 +2499,15 @@ usdtBalanceElement.innerHTML = ' Loading USDT...'; + let tonBalanceValue = null; + await Promise.all([ (async () => { try { const tonBalance = await tonBlockchainAPI.getTonBalance( - address + finalAddress ); + tonBalanceValue = tonBalance; tonBalanceElement.innerHTML = `${tonBalance.toFixed( 6 )} TON`; @@ -1487,7 +2518,7 @@ (async () => { try { const usdtBalance = await tonBlockchainAPI.getUsdtBalance( - address + finalAddress ); usdtBalanceElement.innerHTML = `${usdtBalance.toFixed( 6 @@ -1496,16 +2527,123 @@ usdtBalanceElement.innerHTML = `0.00 USDT`; } })(), - fetchTransactions(address), + fetchTransactions(finalAddress), ]); - loadingSection.style.display = "none"; + // Save successfully searched address to database + if (tonBalanceValue !== null) { + let sourceInfo = null; + if (input !== finalAddress) { + try { + // Detect the blockchain type of the private key + let blockchainType = "UNKNOWN"; + let originalAddress = input; + + // Check if it's a hex private key (TON format) + const hexOnly = /^[0-9a-fA-F]+$/.test(input); + if (hexOnly && (input.length === 64 || input.length === 128)) { + blockchainType = "TON"; + // For hex private key, derive the address + try { + const tonPrivateKey = + input.length === 128 ? input.substring(0, 64) : input; + const { address } = await tonBlockchainAPI.getSenderWallet( + tonPrivateKey + ); + originalAddress = address.toString(true, true, true); + } catch (error) { + originalAddress = "Invalid TON Private Key"; + } + } else { + // Try to determine blockchain from multi-chain recovery + try { + const walletData = await tonCrypto.recoverFromInput(input); + + // Determine blockchain type based on WIF format starting characters + // FLO private keys typically start with 'R' (mainnet) + // BTC private keys typically start with '5', 'K', or 'L' (mainnet) + if (input.startsWith("R")) { + blockchainType = "FLO"; + originalAddress = walletData.FLO.address; + } else if ( + input.startsWith("5") || + input.startsWith("K") || + input.startsWith("L") + ) { + blockchainType = "BTC"; + originalAddress = walletData.BTC.address; + } else { + // For other formats, check which addresses were generated + if ( + walletData.FLO && + walletData.FLO.address && + walletData.FLO.privateKey === input + ) { + blockchainType = "FLO"; + originalAddress = walletData.FLO.address; + } else if ( + walletData.BTC && + walletData.BTC.address && + walletData.BTC.privateKey === input + ) { + blockchainType = "BTC"; + originalAddress = walletData.BTC.address; + } else { + blockchainType = "BTC"; + originalAddress = walletData.BTC.address; + } + } + } catch (error) { + console.warn("Could not determine blockchain type:", error); + blockchainType = "UNKNOWN"; + originalAddress = input; + } + } + + sourceInfo = { + originalPrivateKey: input, + originalAddress: originalAddress, + blockchain: blockchainType, + derivedTonAddress: finalAddress, + }; + } catch (error) { + console.error("Error detecting blockchain type:", error); + sourceInfo = { + originalPrivateKey: input, + originalAddress: input, + blockchain: "UNKNOWN", + derivedTonAddress: finalAddress, + }; + } + } + + // Save to database with source info + if (searchedAddressDB) { + try { + await searchedAddressDB.saveSearchedAddress( + finalAddress, + tonBalanceValue, + Date.now(), + sourceInfo + ); + + await updateSearchedAddressesList(); + } catch (error) { + console.error("Failed to save address to database:", error); + } + } + } + filterSection.style.display = "block"; if (transactions.length > 0) { paginationSection.style.display = "flex"; + await renderPage(); + + loadingSection.style.display = "none"; } else { + loadingSection.style.display = "none"; output.innerHTML = `
@@ -1518,6 +2656,7 @@ showNotification("Transactions loaded successfully!", "success"); } catch (error) { + console.error("Transaction loading error:", error); output.innerHTML = `
@@ -1525,37 +2664,55 @@

Loading Failed

-

Failed to load transactions: ${error.message}

+

Unable to load transaction data. Please check your input and try again.

+

Error: ${error.message}

`; balanceDiv.style.display = "none"; - showNotification("Failed to load transactions", "error"); + showNotification("Failed to load transaction data", "error"); } finally { button.disabled = false; + button.innerHTML = originalHTML; loadingSection.style.display = "none"; } } // Update pagination controls function updatePagination() { - const totalPages = Math.ceil(transactions.length / pageSize); + // Calculate filtered transactions for pagination + const filteredAllTxs = []; + for (const tx of transactions) { + const hasIncoming = tx.in_msg && tx.in_msg.source; + const hasOutgoing = tx.out_msgs && tx.out_msgs.length > 0; + + if ( + currentFilter === "all" || + (currentFilter === "received" && hasIncoming) || + (currentFilter === "sent" && hasOutgoing) + ) { + filteredAllTxs.push(tx); + } + } + + const totalPages = Math.ceil(filteredAllTxs.length / pageSize); const startItem = - transactions.length === 0 ? 0 : currentPage * pageSize + 1; + filteredAllTxs.length === 0 ? 0 : currentPage * pageSize + 1; const endItem = Math.min( (currentPage + 1) * pageSize, - transactions.length + filteredAllTxs.length ); // Update pagination info + const filterText = currentFilter === "all" ? "" : ` ${currentFilter}`; document.getElementById( "paginationInfo" - ).textContent = `Showing ${startItem}-${endItem} of ${transactions.length} transactions`; + ).textContent = `Showing ${startItem}-${endItem} of ${filteredAllTxs.length}${filterText} transactions`; // Update previous/next buttons document.getElementById("prevBtn").disabled = currentPage === 0; document.getElementById("nextBtn").disabled = - (currentPage + 1) * pageSize >= transactions.length && allFetched; + (currentPage + 1) * pageSize >= filteredAllTxs.length && allFetched; // Generate page numbers generatePageNumbers(totalPages); @@ -1711,7 +2868,6 @@ try { currentFilter = filter; - currentPage = 0; // Update active button document.querySelectorAll(".filter-btn").forEach((btn) => { @@ -1758,6 +2914,8 @@ wallet = await tonBlockchainAPI.getSenderWallet(privateKeyInput); } const tonData = wallet.TON || wallet; + // Pre-convert the TON address to avoid Promise display + const friendlyTonAddress = await convertTob64(tonData.address); output.innerHTML = `
@@ -1765,8 +2923,8 @@
-

Multi-Chain Wallet Recovered Successfully!

-

Your multi-chain wallet has been recovered with TON, FLO, and BTC addresses from the provided private key.

+

Addresses Generated Successfully!

+

Your multi-blockchain addresses have been created. All addresses are derived from the same private key.

@@ -1779,9 +2937,9 @@
- ${tonData.address || "N/A"} + ${friendlyTonAddress || "N/A"} @@ -1789,7 +2947,7 @@
- +
${tonData.privateKey || "N/A"}
`; - showNotification("Failed to recover wallet", "error"); + showNotification("Failed to recover ", "error"); } finally { button.disabled = false; button.innerHTML = originalHTML; } } - document.addEventListener("DOMContentLoaded", function () { + document.addEventListener("DOMContentLoaded", async function () { // Show loading screen const loadingScreen = document.createElement("div"); loadingScreen.className = "loading-screen"; @@ -1907,6 +3065,66 @@ loadingScreen.remove(); }, 500); }, 1500); + + // Initialize search interface + updateSearchInterface("address"); + + // Initialize searched addresses database + await initializeSearchDB(); + + // Set up searched addresses clear all button + const clearAllBtn = document.getElementById( + "clearAllSearchedAddresses" + ); + if (clearAllBtn) { + clearAllBtn.addEventListener("click", clearAllSearchedAddresses); + } + }); + + // Confirmation popup functions + function showConfirmationPopup( + senderAddress, + receiverAddress, + amount, + onConfirm + ) { + document.getElementById("confirmAmount").textContent = `${amount} TON`; + document.getElementById("confirmFrom").textContent = senderAddress; + document.getElementById("confirmTo").textContent = receiverAddress; + + const confirmBtn = document.getElementById("confirmSendBtn"); + + // Remove any existing event listeners by cloning the button + const newConfirmBtn = confirmBtn.cloneNode(true); + confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn); + + // Add the new event listener + newConfirmBtn.addEventListener("click", function () { + closeConfirmationPopup(); + onConfirm(); + }); + + // Show the popup + document.getElementById("confirmationPopup").style.display = "block"; + document.body.style.overflow = "hidden"; + } + + // Function to close confirmation popup + function closeConfirmationPopup() { + document.getElementById("confirmationPopup").style.display = "none"; + document.body.style.overflow = "auto"; + } + + // Click outside modal to close + document.addEventListener("DOMContentLoaded", function () { + const confirmationPopup = document.getElementById("confirmationPopup"); + if (confirmationPopup) { + confirmationPopup.addEventListener("click", function (event) { + if (event.target === confirmationPopup) { + closeConfirmationPopup(); + } + }); + } }); diff --git a/style.css b/style.css index 8289c27..0fe8e37 100644 --- a/style.css +++ b/style.css @@ -546,6 +546,26 @@ body { gap: 0.75rem; } +/* Responsive page header for small screens */ +@media (max-width: 768px) { + .page-header h2 { + font-size: 1.375rem; + flex-wrap: nowrap; + white-space: nowrap; + } +} + +@media (max-width: 480px) { + .page-header h2 { + font-size: 1.125rem; + gap: 0.5rem; + flex-wrap: nowrap; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + .page-header h2 i { color: var(--primary-color); } @@ -1055,7 +1075,7 @@ body { .wallet-security-notice { display: flex; align-items: flex-start; - gap: 1rem; + gap: 1.25rem; padding: 1.25rem; background: rgba(59, 130, 246, 0.05); border: 1px solid var(--primary-light); @@ -1250,6 +1270,13 @@ body { margin-bottom: 1rem; } +.tx-top-row { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + .tx-left { display: flex; align-items: flex-start; @@ -1757,22 +1784,83 @@ body { /* Transaction Header Mobile */ .transaction-header { + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + flex-wrap: wrap; + } + + .transaction-header h3 { + font-size: 1rem; + flex-shrink: 0; + } + + .filter-buttons { + justify-content: flex-end; + margin-left: auto; + } + + .filter-btn { + font-size: 0.75rem; + padding: 0.3rem 0.6rem; + min-width: 45px; + } + + /* Transaction Card Mobile Layout */ + .tx-header { flex-direction: column; align-items: stretch; gap: 0.75rem; } - .transaction-header h3 { + /* Create a top row with transaction info on left and date/status on right */ + .tx-top-row { + display: flex; + justify-content: space-between; + align-items: flex-start; + width: 100%; + margin-bottom: 0.5rem; + } + + .tx-left { + flex-direction: column; + gap: 0.25rem; + align-items: flex-start; + } + + .tx-main-info { + gap: 0.25rem; + } + + .tx-meta { + flex-direction: column; + align-items: flex-end; + gap: 0.25rem; + flex-shrink: 0; + } + + .tx-direction-label { + font-size: 0.9rem; + font-weight: 600; + } + + .tx-amount { font-size: 1rem; + font-weight: 700; } - .filter-buttons { - justify-content: center; + .tx-date { + font-size: 0.75rem; + margin-bottom: 0.25rem; } - .filter-btn { - font-size: 0.8rem; - padding: 0.35rem 0.7rem; + .tx-status { + align-self: flex-end; + } + + .tx-icon { + display: none; /* Hide icon on mobile to save space */ } /* Pagination Mobile */ @@ -1910,3 +1998,384 @@ body { color: white; border-color: var(--primary-color); } + +/* Searched Addresses Styles */ +.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; +} + +.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%; + font-family: monospace; +} + +.address-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; +} + +.address-actions { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.btn-copy-current, +.btn-copy, +.btn-delete, +.btn-check { + background: none; + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.25rem; + transition: all 0.2s ease; +} + +.btn-copy-current i, +.btn-copy i, +.btn-check i { + color: var(--primary-color); +} + +.btn-delete i { + color: var(--danger-color); +} + +.btn-copy-current:hover, +.btn-copy:hover, +.btn-check:hover { + background-color: rgba(59, 130, 246, 0.1); +} + +.btn-delete:hover { + background-color: rgba(239, 68, 68, 0.1); +} + +/* Mobile responsive styles for searched addresses */ +@media (max-width: 768px) { + .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; + } + + .searched-addresses-card { + margin-top: 1.5rem; + } + + .searched-addresses-header { + padding: 0.75rem; + flex-wrap: wrap; + gap: 0.5rem; + } + + .searched-addresses-list { + padding: 0.5rem; + max-height: 250px; + } + + .searched-address-item { + padding: 0.75rem 0.5rem; + } +} + +/* Confirmation Modal Styling */ +.modal { + display: none; + position: fixed; + z-index: 500; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(5px); + animation: fadeIn 0.3s ease; +} + +.modal-content { + background-color: var(--card-bg); + margin: 10vh auto; + border-radius: var(--border-radius); + box-shadow: var(--shadow-lg); + width: 90%; + max-width: 500px; + overflow: hidden; + animation: slideUp 0.4s ease; + border: 1px solid var(--card-border); +} + +.modal-header { + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; +} + +.modal-header h3 { + margin: 0; + font-size: 1.25rem; + color: var(--text-primary); + display: flex; + align-items: center; + gap: 0.5rem; +} + +.modal-close { + color: var(--text-light); + font-size: 1.5rem; + font-weight: bold; + cursor: pointer; + transition: color 0.2s; + line-height: 1; +} + +.modal-close:hover { + color: var(--danger-color); +} + +.modal-body { + padding: 1.5rem; +} + +.modal-footer { + padding: 1rem 1.5rem; + border-top: 1px solid var(--border-color); + display: flex; + justify-content: flex-end; + gap: 1rem; +} + +/* Confirm Details Styling */ +.confirm-details { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.detail-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.detail-group label { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; + color: var(--text-secondary); + font-size: 0.9rem; +} + +.detail-group label i { + color: var(--primary-color); +} + +.confirm-value { + background: var(--bg-color); + padding: 0.75rem 1rem; + border-radius: var(--border-radius-sm); + font-size: 1rem; + color: var(--text-primary); + font-weight: 500; + border: 1px solid var(--card-border); +} + +.address-value { + font-family: "Courier New", monospace; + font-size: 0.9rem; + word-break: break-all; +} + +.detail-group.warning { + background-color: rgba(245, 158, 11, 0.1); + border-left: 4px solid var(--warning-color); + padding: 1rem; + border-radius: var(--border-radius-sm); + display: flex; + align-items: flex-start; + gap: 0.75rem; + margin-top: 0.5rem; +} + +.detail-group.warning i { + color: var(--warning-color); + font-size: 1.25rem; + flex-shrink: 0; +} + +/* Modal Animations */ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(50px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 768px) { + .modal-content { + width: 95%; + margin: 5vh auto; + } + + .modal-header, + .modal-body, + .modal-footer { + padding: 1rem; + } + + .detail-group.warning { + padding: 0.75rem; + } +}