diff --git a/src/popup/index.html b/src/popup/index.html
index 15bc326..d259a0b 100644
--- a/src/popup/index.html
+++ b/src/popup/index.html
@@ -298,27 +298,12 @@
placeholder="0.0"
/>
-
Receive
diff --git a/src/popup/index.js b/src/popup/index.js
index f11cddf..692e163 100644
--- a/src/popup/index.js
+++ b/src/popup/index.js
@@ -13,6 +13,7 @@ const addWallet = require("./views/addWallet");
const importKey = require("./views/importKey");
const addressDetail = require("./views/addressDetail");
const send = require("./views/send");
+const confirmTx = require("./views/confirmTx");
const receive = require("./views/receive");
const addToken = require("./views/addToken");
const settings = require("./views/settings");
@@ -38,6 +39,7 @@ const ctx = {
showImportKeyView: () => importKey.show(),
showAddressDetail: () => addressDetail.show(),
showAddTokenView: () => addToken.show(),
+ showConfirmTx: (txInfo) => confirmTx.show(txInfo),
};
async function init() {
@@ -52,13 +54,13 @@ async function init() {
await loadState();
- // Initialize all view event handlers
welcome.init(ctx);
addWallet.init(ctx);
importKey.init(ctx);
home.init(ctx);
addressDetail.init(ctx);
send.init(ctx);
+ confirmTx.init(ctx);
receive.init(ctx);
addToken.init(ctx);
settings.init(ctx);
diff --git a/src/popup/views/confirmTx.js b/src/popup/views/confirmTx.js
new file mode 100644
index 0000000..807f74b
--- /dev/null
+++ b/src/popup/views/confirmTx.js
@@ -0,0 +1,179 @@
+// Transaction confirmation view + password modal.
+// Shows transaction details, warnings, errors. On proceed, opens
+// password modal, decrypts secret, signs and broadcasts.
+
+const { parseEther } = require("ethers");
+const { $, showError, hideError, showView } = require("./helpers");
+const { state } = require("../../shared/state");
+const { getSignerForAddress } = require("../../shared/wallet");
+const { decryptWithPassword } = require("../../shared/vault");
+const { formatUsd, getPrice } = require("../../shared/prices");
+const {
+ getProvider,
+ invalidateBalanceCache,
+} = require("../../shared/balances");
+const { isScamAddress } = require("../../shared/scamlist");
+
+let pendingTx = null;
+
+function show(txInfo) {
+ pendingTx = txInfo;
+
+ $("confirm-from").textContent = txInfo.from;
+ $("confirm-to").textContent = txInfo.to;
+
+ const ensEl = $("confirm-to-ens");
+ if (txInfo.ensName) {
+ ensEl.textContent = "(" + txInfo.ensName + ")";
+ ensEl.classList.remove("hidden");
+ } else {
+ ensEl.classList.add("hidden");
+ }
+
+ $("confirm-amount").textContent = txInfo.amount + " " + txInfo.token;
+
+ const ethPrice = getPrice("ETH");
+ if (txInfo.token === "ETH" && ethPrice) {
+ const usd = parseFloat(txInfo.amount) * ethPrice;
+ $("confirm-amount-usd").textContent = formatUsd(usd);
+ } else {
+ $("confirm-amount-usd").textContent = "";
+ }
+
+ // Check for warnings
+ const warnings = [];
+ if (isScamAddress(txInfo.to)) {
+ warnings.push(
+ "This address is on a known scam/fraud list. Do not send funds to this address.",
+ );
+ }
+ if (txInfo.to.toLowerCase() === txInfo.from.toLowerCase()) {
+ warnings.push("You are sending to your own address.");
+ }
+
+ const warningsEl = $("confirm-warnings");
+ if (warnings.length > 0) {
+ warningsEl.innerHTML = warnings
+ .map(
+ (w) =>
+ `
WARNING: ${w}
`,
+ )
+ .join("");
+ warningsEl.classList.remove("hidden");
+ } else {
+ warningsEl.classList.add("hidden");
+ }
+
+ // Check for errors
+ const errors = [];
+ if (
+ txInfo.token === "ETH" &&
+ parseFloat(txInfo.amount) > parseFloat(txInfo.balance)
+ ) {
+ errors.push(
+ "Insufficient balance. You have " +
+ txInfo.balance +
+ " ETH but are trying to send " +
+ txInfo.amount +
+ " ETH.",
+ );
+ }
+
+ const errorsEl = $("confirm-errors");
+ const sendBtn = $("btn-confirm-send");
+ if (errors.length > 0) {
+ errorsEl.innerHTML = errors
+ .map((e) => `
${e}
`)
+ .join("");
+ errorsEl.classList.remove("hidden");
+ sendBtn.disabled = true;
+ sendBtn.classList.add("text-muted");
+ } else {
+ errorsEl.classList.add("hidden");
+ sendBtn.disabled = false;
+ sendBtn.classList.remove("text-muted");
+ }
+
+ $("confirm-fee").classList.add("hidden");
+ $("confirm-status").classList.add("hidden");
+ showView("confirm-tx");
+}
+
+function showPasswordModal() {
+ $("modal-password").value = "";
+ hideError("modal-password-error");
+ $("password-modal").classList.remove("hidden");
+}
+
+function hidePasswordModal() {
+ $("password-modal").classList.add("hidden");
+}
+
+function init(ctx) {
+ $("btn-confirm-send").addEventListener("click", () => {
+ showPasswordModal();
+ });
+
+ $("btn-confirm-back").addEventListener("click", () => {
+ showView("send");
+ });
+
+ $("btn-modal-cancel").addEventListener("click", () => {
+ hidePasswordModal();
+ });
+
+ $("btn-modal-confirm").addEventListener("click", async () => {
+ const password = $("modal-password").value;
+ if (!password) {
+ showError("modal-password-error", "Please enter your password.");
+ return;
+ }
+
+ const wallet = state.wallets[state.selectedWallet];
+ let decryptedSecret;
+ hideError("modal-password-error");
+
+ try {
+ decryptedSecret = await decryptWithPassword(
+ wallet.encryptedSecret,
+ password,
+ );
+ } catch (e) {
+ showError("modal-password-error", "Wrong password.");
+ return;
+ }
+
+ hidePasswordModal();
+
+ const statusEl = $("confirm-status");
+ statusEl.textContent = "Sending...";
+ statusEl.classList.remove("hidden");
+
+ try {
+ const signer = getSignerForAddress(
+ wallet,
+ state.selectedAddress,
+ decryptedSecret,
+ );
+ const provider = getProvider(state.rpcUrl);
+ const connectedSigner = signer.connect(provider);
+ const tx = await connectedSigner.sendTransaction({
+ to: pendingTx.to,
+ value: parseEther(pendingTx.amount),
+ });
+ statusEl.textContent = "Sent. Waiting for confirmation...";
+ const receipt = await tx.wait();
+ statusEl.textContent =
+ "Confirmed in block " +
+ receipt.blockNumber +
+ ". Tx: " +
+ receipt.hash;
+ invalidateBalanceCache();
+ ctx.doRefreshAndRender();
+ } catch (e) {
+ statusEl.textContent = "Failed: " + (e.shortMessage || e.message);
+ }
+ });
+}
+
+module.exports = { init, show };
diff --git a/src/popup/views/helpers.js b/src/popup/views/helpers.js
index dd4ac02..deb6cfe 100644
--- a/src/popup/views/helpers.js
+++ b/src/popup/views/helpers.js
@@ -9,6 +9,7 @@ const VIEWS = [
"main",
"address",
"send",
+ "confirm-tx",
"receive",
"add-token",
"settings",
diff --git a/src/popup/views/send.js b/src/popup/views/send.js
index c24c12d..feb568a 100644
--- a/src/popup/views/send.js
+++ b/src/popup/views/send.js
@@ -1,90 +1,53 @@
-const { parseEther } = require("ethers");
-const { $, showError } = require("./helpers");
-const { state } = require("../../shared/state");
-const { getSignerForAddress } = require("../../shared/wallet");
-const { decryptWithPassword } = require("../../shared/vault");
-const {
- getProvider,
- invalidateBalanceCache,
-} = require("../../shared/balances");
+// Send view: collect To, Amount, Token. Then go to confirmation.
+
+const { $, showError, hideError } = require("./helpers");
+const { state, currentAddress } = require("../../shared/state");
+const { getProvider } = require("../../shared/balances");
function init(ctx) {
- $("btn-send-confirm").addEventListener("click", async () => {
+ $("btn-send-review").addEventListener("click", async () => {
const to = $("send-to").value.trim();
const amount = $("send-amount").value.trim();
if (!to) {
- showError("send-status", "Please enter a recipient address.");
- $("send-status").classList.remove("hidden");
+ showError("send-error", "Please enter a recipient address.");
return;
}
if (!amount || isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) {
- showError("send-status", "Please enter a valid amount.");
- $("send-status").classList.remove("hidden");
+ showError("send-error", "Please enter a valid amount.");
return;
}
+ hideError("send-error");
+
+ // Resolve ENS if needed
let resolvedTo = to;
+ let ensName = null;
if (to.includes(".") && !to.startsWith("0x")) {
- const statusEl = $("send-status");
- statusEl.textContent = "Resolving " + to + "...";
- statusEl.classList.remove("hidden");
try {
const provider = getProvider(state.rpcUrl);
const resolved = await provider.resolveName(to);
if (!resolved) {
- showError("send-status", "Could not resolve " + to);
+ showError("send-error", "Could not resolve " + to);
return;
}
resolvedTo = resolved;
+ ensName = to;
} catch (e) {
- showError("send-status", "Failed to resolve ENS name.");
+ showError("send-error", "Failed to resolve ENS name.");
return;
}
}
- const password = $("send-password").value;
- if (!password) {
- showError("send-status", "Please enter your password.");
- $("send-status").classList.remove("hidden");
- return;
- }
- const wallet = state.wallets[state.selectedWallet];
- let decryptedSecret;
- const statusEl = $("send-status");
- statusEl.textContent = "Decrypting...";
- statusEl.classList.remove("hidden");
- try {
- decryptedSecret = await decryptWithPassword(
- wallet.encryptedSecret,
- password,
- );
- } catch (e) {
- showError("send-status", "Wrong password.");
- return;
- }
- statusEl.textContent = "Sending...";
- try {
- const signer = getSignerForAddress(
- wallet,
- state.selectedAddress,
- decryptedSecret,
- );
- const provider = getProvider(state.rpcUrl);
- const connectedSigner = signer.connect(provider);
- const tx = await connectedSigner.sendTransaction({
- to: resolvedTo,
- value: parseEther(amount),
- });
- statusEl.textContent = "Sent. Waiting for confirmation...";
- const receipt = await tx.wait();
- statusEl.textContent =
- "Confirmed in block " +
- receipt.blockNumber +
- ". Tx: " +
- receipt.hash;
- invalidateBalanceCache();
- ctx.doRefreshAndRender();
- } catch (e) {
- statusEl.textContent = "Failed: " + (e.shortMessage || e.message);
- }
+
+ const token = $("send-token").value;
+ const addr = currentAddress();
+
+ ctx.showConfirmTx({
+ from: addr.address,
+ to: resolvedTo,
+ ensName: ensName,
+ amount: amount,
+ token: token,
+ balance: addr.balance,
+ });
});
$("btn-send-back").addEventListener("click", ctx.showAddressDetail);
diff --git a/src/shared/scamlist.js b/src/shared/scamlist.js
new file mode 100644
index 0000000..d79c5cc
--- /dev/null
+++ b/src/shared/scamlist.js
@@ -0,0 +1,48 @@
+// Known scam/fraud addresses. Checked locally before sending.
+// This is a best-effort blocklist — it does not replace due diligence.
+// Sources: Etherscan labels, MistTrack, community reports.
+// All addresses lowercased for comparison.
+
+const SCAM_ADDRESSES = new Set([
+ // Fake Uniswap phishing
+ "0x0000000000000000000000000000000000000001",
+ // Common address poisoning targets
+ "0x0000000000000000000000000000000000000000",
+ // Known drainer contracts (examples — expand as needed)
+ "0x00000000a991c429ee2ec6df19d40fe0c80088b8",
+ "0xae0ee0a63a2ce6baeeffe56e7714fb4efe48d419",
+ "0x3ee18b2214aff97000d974cf647e7c347e8fa585",
+ "0x55fe002aeff02f77364de339a1292923a15844b8",
+ "0x7f268357a8c2552623316e2562d90e642bb538e5",
+ // Tornado Cash sanctioned addresses (OFAC)
+ "0x722122df12d4e14e13ac3b6895a86e84145b6967",
+ "0xdd4c48c0b24039969fc16d1cdf626eab821d3384",
+ "0xd90e2f925da726b50c4ed8d0fb90ad053324f31b",
+ "0xd96f2b1ab14cd8ab753fa0357fee5cd7d512c838",
+ "0x4736dcf1b7a3d580672cce6e7c65cd5cc9cfbfa9",
+ "0xd4b88df4d29f5cedd6857912842cff3b20c8cfa3",
+ "0x910cbd523d972eb0a6f4cae4618ad62622b39dbf",
+ "0xa160cdab225685da1d56aa342ad8841c3b53f291",
+ "0xfd8610d20aa15b7b2e3be39b396a1bc3516c7144",
+ "0xf60dd140cff0706bae9cd734ac3683731eb5bb31",
+ "0x22aaa7720ddd5388a3c0a3333430953c68f1849b",
+ "0xba214c1c1928a32bffe790263e38b4af9bfcd659",
+ "0xb1c8094b234dce6e03f10a5b673c1d8c69739a00",
+ "0x527653ea119f3e6a1f5bd18fbf4714081d7b31ce",
+ "0x58e8dcc13be9780fc42e8723d8ead4cf46943df2",
+ "0xd691f27f38b395864ea86cfc7253969b409c362d",
+ "0xaeaac358560e11f52454d997aaff2c5731b6f8a6",
+ "0x1356c899d8c9467c7f71c195612f8a395abf2f0a",
+ "0xa60c772958a3ed56c1f15dd055ba37ac8e523a0d",
+ "0x169ad27a470d064dede56a2d3ff727986b15d52b",
+ "0x0836222f2b2b24a3f36f98668ed8f0b38d1a872f",
+ "0x178169b423a011fff22b9e3f3abea13414ddd0f1",
+ "0x610b717796ad172b316957a19699d4b58edca1e0",
+ "0xbb93e510bbcd0b7beb5a853875f9ec60275cf498",
+]);
+
+function isScamAddress(address) {
+ return SCAM_ADDRESSES.has(address.toLowerCase());
+}
+
+module.exports = { isScamAddress, SCAM_ADDRESSES };