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;