diff --git a/src/popup/index.html b/src/popup/index.html index 0d6961c..6c1c5fe 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -422,6 +422,11 @@ class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg" placeholder="Address (0x...) or ENS name" /> +
diff --git a/src/popup/views/send.js b/src/popup/views/send.js index fda8a66..7400e93 100644 --- a/src/popup/views/send.js +++ b/src/popup/views/send.js @@ -11,6 +11,107 @@ const { state, currentAddress } = 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 = `` + @@ -88,6 +189,13 @@ function init(_ctx) { ctx = _ctx; $("send-token").addEventListener("change", updateSendBalance); + // Initial state: disable review button until address is entered + $("btn-send-review").disabled = true; + $("btn-send-review").classList.add("opacity-50"); + + // Validate address on input + $("send-to").addEventListener("input", updateToValidation); + $("btn-send-review").addEventListener("click", async () => { const to = $("send-to").value.trim(); const amount = $("send-amount").value.trim(); @@ -95,6 +203,15 @@ function init(_ctx) { showFlash("Please enter a recipient address."); return; } + + // Re-validate before proceeding + const validation = validateToAddress(to); + if (!validation.valid) { + showFlash( + validation.error || "Please enter a valid Ethereum address.", + ); + return; + } if (!amount || isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) { showFlash("Please enter a valid amount."); return;