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.
180 lines
5.4 KiB
JavaScript
180 lines
5.4 KiB
JavaScript
// 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 };
|