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/confirmTx.js b/src/popup/views/confirmTx.js
index 3b6c1ff..0e6e117 100644
--- a/src/popup/views/confirmTx.js
+++ b/src/popup/views/confirmTx.js
@@ -14,6 +14,7 @@ const {
showError,
hideError,
showView,
+ showFlash,
addressTitle,
addressDotHtml,
escapeHtml,
@@ -95,11 +96,22 @@ function show(txInfo) {
// Token contract section (ERC-20 only)
const tokenSection = $("confirm-token-section");
if (isErc20) {
+ const dot = addressDotHtml(txInfo.token);
const link = etherscanTokenLink(txInfo.token);
$("confirm-token-contract").innerHTML =
- escapeHtml(txInfo.token) +
- `
${EXT_ICON}`;
+ `
${dot}` +
+ `
${escapeHtml(txInfo.token)}` +
+ `
${EXT_ICON}` +
+ `
`;
tokenSection.classList.remove("hidden");
+ // Attach click-to-copy on the contract address
+ const copyEl = tokenSection.querySelector("[data-copy]");
+ if (copyEl) {
+ copyEl.onclick = () => {
+ navigator.clipboard.writeText(copyEl.dataset.copy);
+ showFlash("Copied!");
+ };
+ }
} else {
tokenSection.classList.add("hidden");
}
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 fda8a66..6778405 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;
@@ -159,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,
+};