From ef2f862d23ee31b118b8ef8287e07da1caf54807 Mon Sep 17 00:00:00 2001 From: user Date: Sat, 28 Feb 2026 11:42:06 -0800 Subject: [PATCH 1/2] fix: validate destination address on send view - Validate Ethereum addresses (0x + 40 hex chars) and ENS names - EIP-55 checksum validation for mixed-case addresses - Block sending to zero address (0x0000...0000) - Warn when sending to own address (allow but show warning) - Inline error messages with reserved space (no layout shift) - Disable Review button while address is invalid Closes #67 --- src/popup/index.html | 5 ++ src/popup/views/send.js | 117 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/src/popup/index.html b/src/popup/index.html index 92a32a7..d5bbf90 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -496,6 +496,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; -- 2.49.1 From 9de779155390e1a54f0bc35409c5ba4d1ee39e9e Mon Sep 17 00:00:00 2001 From: user Date: Sat, 28 Feb 2026 12:17:52 -0800 Subject: [PATCH 2/2] fix: reset validation state when navigating to send view Clear the error/warning text and disable the review button when entering the send view from home, address detail, or address token views. This prevents stale validation messages from persisting after leaving and returning to the send view. --- src/popup/views/addressDetail.js | 7 ++++++- src/popup/views/addressToken.js | 7 ++++++- src/popup/views/home.js | 7 ++++++- src/popup/views/send.js | 17 ++++++++++++++++- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/popup/views/addressDetail.js b/src/popup/views/addressDetail.js index 3c65e24..49c627e 100644 --- a/src/popup/views/addressDetail.js +++ b/src/popup/views/addressDetail.js @@ -15,7 +15,11 @@ const { filterTransactions, } = require("../../shared/transactions"); const { resolveEnsNames } = require("../../shared/ens"); -const { updateSendBalance, renderSendTokenSelect } = require("./send"); +const { + updateSendBalance, + renderSendTokenSelect, + resetSendValidation, +} = require("./send"); const { log } = require("../../shared/log"); const makeBlockie = require("ethereum-blockies-base64"); const { decryptWithPassword } = require("../../shared/vault"); @@ -259,6 +263,7 @@ function init(_ctx) { $("send-token").classList.remove("hidden"); $("send-token-static").classList.add("hidden"); updateSendBalance(); + resetSendValidation(); showView("send"); }); diff --git a/src/popup/views/addressToken.js b/src/popup/views/addressToken.js index 6f25fef..72c95c0 100644 --- a/src/popup/views/addressToken.js +++ b/src/popup/views/addressToken.js @@ -23,7 +23,11 @@ const { filterTransactions, } = require("../../shared/transactions"); const { resolveEnsNames } = require("../../shared/ens"); -const { updateSendBalance, renderSendTokenSelect } = require("./send"); +const { + updateSendBalance, + renderSendTokenSelect, + resetSendValidation, +} = require("./send"); const { log } = require("../../shared/log"); const makeBlockie = require("ethereum-blockies-base64"); @@ -372,6 +376,7 @@ function init(_ctx) { }); } updateSendBalance(); + resetSendValidation(); showView("send"); }); diff --git a/src/popup/views/home.js b/src/popup/views/home.js index 1b24d13..ea923e0 100644 --- a/src/popup/views/home.js +++ b/src/popup/views/home.js @@ -11,7 +11,11 @@ const { truncateMiddle, } = require("./helpers"); const { state, saveState, currentAddress } = require("../../shared/state"); -const { updateSendBalance, renderSendTokenSelect } = require("./send"); +const { + updateSendBalance, + renderSendTokenSelect, + resetSendValidation, +} = require("./send"); const { deriveAddressFromXpub } = require("../../shared/wallet"); const { formatUsd, @@ -388,6 +392,7 @@ function init(ctx) { $("send-token-static").classList.add("hidden"); renderSendTokenSelect(addr); updateSendBalance(); + resetSendValidation(); showView("send"); }); diff --git a/src/popup/views/send.js b/src/popup/views/send.js index 7400e93..6778405 100644 --- a/src/popup/views/send.js +++ b/src/popup/views/send.js @@ -276,4 +276,19 @@ function init(_ctx) { }); } -module.exports = { init, updateSendBalance, renderSendTokenSelect }; +function resetSendValidation() { + const errorEl = $("send-to-error"); + const btn = $("btn-send-review"); + if (errorEl) errorEl.textContent = ""; + if (btn) { + btn.disabled = true; + btn.classList.add("opacity-50"); + } +} + +module.exports = { + init, + updateSendBalance, + renderSendTokenSelect, + resetSendValidation, +}; -- 2.49.1