Add transaction confirmation screen and password modal
All checks were successful
check / check (push) Successful in 13s
All checks were successful
check / check (push) Successful in 13s
New send flow: Send → Confirm → Password → Broadcast. Send view: collects To (with ENS resolution), Amount, Token. "Review" button advances to confirmation. No password field. Confirm Transaction view: shows From, To (with ENS name), Amount (with USD value), and runs pre-send checks: - Scam address warning (checked against local blocklist) - Self-send warning - Insufficient balance error (disables Send button) Password modal: full-screen overlay, appears only after user clicks Send on the confirmation screen. Decrypts the wallet secret, signs and broadcasts the transaction. Wrong password is caught inline. scamlist.js: hardcoded set of known scam/fraud addresses (Tornado Cash sanctioned, drainer contracts, address poisoning). Checked locally, no external API.
This commit is contained in:
179
src/popup/views/confirmTx.js
Normal file
179
src/popup/views/confirmTx.js
Normal file
@@ -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) =>
|
||||
`<div class="border border-border border-dashed p-2 mb-1 text-xs font-bold">WARNING: ${w}</div>`,
|
||||
)
|
||||
.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) => `<div class="text-xs">${e}</div>`)
|
||||
.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 };
|
||||
@@ -9,6 +9,7 @@ const VIEWS = [
|
||||
"main",
|
||||
"address",
|
||||
"send",
|
||||
"confirm-tx",
|
||||
"receive",
|
||||
"add-token",
|
||||
"settings",
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user