// Send view: collect To, Amount, Token. Then go to confirmation. const { $, showFlash, addressDotHtml, addressTitle, escapeHtml, } = require("./helpers"); const { state, currentAddress, currentNetwork } = require("../../shared/state"); let ctx; const { getProvider } = require("../../shared/balances"); const { KNOWN_SYMBOLS, resolveSymbol } = require("../../shared/tokenList"); const { getAddress } = require("ethers"); const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; /** * Validate a destination address string. * Returns { valid: true } or { valid: false, error: "..." }. */ function validateToAddress(value) { const v = value.trim(); if (!v) return { valid: false, error: "" }; // ENS names: contains a dot and doesn't start with 0x if (v.includes(".") && !v.startsWith("0x")) { // Basic ENS format check: at least one label before and after dot if (/^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$/.test(v)) { return { valid: true }; } return { valid: false, error: "Please enter a valid ENS name.", }; } // Must look like an Ethereum address if (!/^0x[0-9a-fA-F]{40}$/.test(v)) { return { valid: false, error: "Please enter a valid Ethereum address.", }; } // Reject zero address if (v.toLowerCase() === ZERO_ADDRESS) { return { valid: false, error: "Sending to the zero address is not allowed.", }; } // EIP-55 checksum validation: all-lowercase is ok, otherwise must match checksum if (v !== v.toLowerCase()) { try { const checksummed = getAddress(v); if (checksummed !== v) { return { valid: false, error: "Address checksum is invalid. Please double-check the address.", }; } } catch { return { valid: false, error: "Address checksum is invalid. Please double-check the address.", }; } } // Warn if sending to own address const addr = currentAddress(); if (addr && v.toLowerCase() === addr.address.toLowerCase()) { // Allow but will warn — we return valid with a warning return { valid: true, warning: "This is your own address. Are you sure?", }; } return { valid: true }; } function updateToValidation() { const input = $("send-to"); const errorEl = $("send-to-error"); const btn = $("btn-send-review"); const value = input.value.trim(); if (!value) { errorEl.textContent = ""; btn.disabled = true; btn.classList.add("opacity-50"); return; } const result = validateToAddress(value); if (!result.valid) { errorEl.textContent = result.error; errorEl.style.color = "#cc0000"; btn.disabled = true; btn.classList.add("opacity-50"); } else if (result.warning) { errorEl.textContent = result.warning; errorEl.style.color = "#b8860b"; btn.disabled = false; btn.classList.remove("opacity-50"); } else { errorEl.textContent = ""; btn.disabled = false; btn.classList.remove("opacity-50"); } } const EXT_ICON = `` + ``; function isSpoofedToken(t) { const upper = (t.symbol || "").toUpperCase(); if (!KNOWN_SYMBOLS.has(upper)) return false; const legit = KNOWN_SYMBOLS.get(upper); if (legit === null) return true; return t.address.toLowerCase() !== legit; } function renderSendTokenSelect(addr) { const sel = $("send-token"); sel.innerHTML = ''; const fraudSet = new Set( (state.fraudContracts || []).map((a) => a.toLowerCase()), ); for (const t of addr.tokenBalances || []) { if (isSpoofedToken(t)) continue; if (fraudSet.has(t.address.toLowerCase())) continue; if (state.hideLowHolderTokens && (t.holders || 0) < 1000) continue; const opt = document.createElement("option"); opt.value = t.address; opt.textContent = t.symbol; sel.appendChild(opt); } } function updateSendBalance() { const addr = currentAddress(); if (!addr) return; const dot = addressDotHtml(addr.address); const link = `${currentNetwork().explorerUrl}/address/${addr.address}`; const extLink = `${EXT_ICON}`; const title = addressTitle(addr.address, state.wallets); let fromHtml = ""; if (title) { fromHtml += `