` +
+ `
`;
let pendingTx = null;
let elapsedTimer = null;
+function etherscanTokenLink(address) {
+ return `https://etherscan.io/token/${address}`;
+}
+
function show(txInfo) {
pendingTx = txInfo;
+ const isErc20 = txInfo.token !== "ETH";
+ const symbol = isErc20 ? txInfo.tokenSymbol || "?" : "ETH";
+
+ // Transaction type
+ if (isErc20) {
+ $("confirm-type").textContent =
+ "ERC-20 token transfer (" + symbol + ")";
+ } else {
+ $("confirm-type").textContent = "Native ETH transfer";
+ }
+
+ // Token contract section (ERC-20 only)
+ const tokenSection = $("confirm-token-section");
+ if (isErc20) {
+ const link = etherscanTokenLink(txInfo.token);
+ $("confirm-token-contract").innerHTML =
+ escapeHtml(txInfo.token) +
+ `
${EXT_ICON}`;
+ tokenSection.classList.remove("hidden");
+ } else {
+ tokenSection.classList.add("hidden");
+ }
+
+ // From
const fromTitle = addressTitle(txInfo.from, state.wallets);
$("confirm-from").innerHTML = formatAddressHtml(
txInfo.from,
@@ -31,6 +75,8 @@ function show(txInfo) {
null,
fromTitle,
);
+
+ // To
const toTitle = addressTitle(txInfo.to, state.wallets);
$("confirm-to").innerHTML = formatAddressHtml(
txInfo.to,
@@ -38,20 +84,44 @@ function show(txInfo) {
null,
toTitle,
);
-
- // Hide the separate ENS element — it's now inline in the address display
$("confirm-to-ens").classList.add("hidden");
- $("confirm-amount").textContent = txInfo.amount + " " + txInfo.token;
-
+ // Amount
+ $("confirm-amount").textContent = txInfo.amount + " " + symbol;
const ethPrice = getPrice("ETH");
- if (txInfo.token === "ETH" && ethPrice) {
+ const tokenPrice = getPrice(symbol);
+ if (isErc20 && tokenPrice) {
+ const usd = parseFloat(txInfo.amount) * tokenPrice;
+ $("confirm-amount-usd").textContent = formatUsd(usd);
+ } else if (!isErc20 && ethPrice) {
const usd = parseFloat(txInfo.amount) * ethPrice;
$("confirm-amount-usd").textContent = formatUsd(usd);
} else {
$("confirm-amount-usd").textContent = "";
}
+ // Balance
+ if (isErc20) {
+ const bal = txInfo.tokenBalance || "0";
+ $("confirm-balance").textContent = bal + " " + symbol;
+ if (tokenPrice) {
+ $("confirm-balance-usd").textContent = formatUsd(
+ parseFloat(bal) * tokenPrice,
+ );
+ } else {
+ $("confirm-balance-usd").textContent = "";
+ }
+ } else {
+ $("confirm-balance").textContent = (txInfo.balance || "0") + " ETH";
+ if (ethPrice) {
+ $("confirm-balance-usd").textContent = formatUsd(
+ parseFloat(txInfo.balance || "0") * ethPrice,
+ );
+ } else {
+ $("confirm-balance-usd").textContent = "";
+ }
+ }
+
// Check for warnings
const warnings = [];
if (isScamAddress(txInfo.to)) {
@@ -78,10 +148,24 @@ function show(txInfo) {
// Check for errors
const errors = [];
- if (
- txInfo.token === "ETH" &&
- parseFloat(txInfo.amount) > parseFloat(txInfo.balance)
- ) {
+ if (isErc20) {
+ const tokenBal = parseFloat(txInfo.tokenBalance || "0");
+ if (parseFloat(txInfo.amount) > tokenBal) {
+ errors.push(
+ "Insufficient " +
+ symbol +
+ " balance. You have " +
+ txInfo.tokenBalance +
+ " " +
+ symbol +
+ " but are trying to send " +
+ txInfo.amount +
+ " " +
+ symbol +
+ ".",
+ );
+ }
+ } else if (parseFloat(txInfo.amount) > parseFloat(txInfo.balance)) {
errors.push(
"Insufficient balance. You have " +
txInfo.balance +
@@ -106,9 +190,60 @@ function show(txInfo) {
sendBtn.classList.remove("text-muted");
}
- $("confirm-fee").classList.add("hidden");
+ // Gas estimate — show placeholder then fetch async
+ $("confirm-fee").classList.remove("hidden");
+ $("confirm-fee-amount").textContent = "Estimating...";
+ $("confirm-fee-usd").textContent = "";
$("confirm-status").classList.add("hidden");
showView("confirm-tx");
+
+ estimateGas(txInfo);
+}
+
+async function estimateGas(txInfo) {
+ try {
+ const provider = getProvider(state.rpcUrl);
+ const feeData = await provider.getFeeData();
+ const gasPrice = feeData.gasPrice;
+ let gasLimit;
+
+ if (txInfo.token === "ETH") {
+ gasLimit = await provider.estimateGas({
+ from: txInfo.from,
+ to: txInfo.to,
+ value: parseEther(txInfo.amount),
+ });
+ } else {
+ const contract = new Contract(txInfo.token, ERC20_ABI, provider);
+ const decimals = await contract.decimals();
+ const amount = parseUnits(txInfo.amount, decimals);
+ gasLimit = await contract.transfer.estimateGas(txInfo.to, amount, {
+ from: txInfo.from,
+ });
+ }
+
+ const gasCostWei = gasLimit * gasPrice;
+ const gasCostEth = formatEther(gasCostWei);
+ // Format to 6 significant decimal places
+ const parts = gasCostEth.split(".");
+ const dec =
+ parts.length > 1
+ ? parts[1].slice(0, 6).replace(/0+$/, "") || "0"
+ : "0";
+ const feeStr = parts[0] + "." + dec + " ETH";
+ $("confirm-fee-amount").textContent = feeStr;
+
+ const ethPrice = getPrice("ETH");
+ if (ethPrice) {
+ $("confirm-fee-usd").textContent = formatUsd(
+ parseFloat(gasCostEth) * ethPrice,
+ );
+ }
+ } catch (e) {
+ log.errorf("gas estimation failed:", e.message);
+ $("confirm-fee-amount").textContent = "Unable to estimate";
+ $("confirm-fee-usd").textContent = "";
+ }
}
function showPasswordModal() {
@@ -169,10 +304,23 @@ function init(ctx) {
);
const provider = getProvider(state.rpcUrl);
const connectedSigner = signer.connect(provider);
- const tx = await connectedSigner.sendTransaction({
- to: pendingTx.to,
- value: parseEther(pendingTx.amount),
- });
+
+ let tx;
+ if (pendingTx.token === "ETH") {
+ tx = await connectedSigner.sendTransaction({
+ to: pendingTx.to,
+ value: parseEther(pendingTx.amount),
+ });
+ } else {
+ const contract = new Contract(
+ pendingTx.token,
+ ERC20_ABI,
+ connectedSigner,
+ );
+ const decimals = await contract.decimals();
+ const amount = parseUnits(pendingTx.amount, decimals);
+ tx = await contract.transfer(pendingTx.to, amount);
+ }
// Disable send button immediately after broadcast
const sendBtn = $("btn-confirm-send");
diff --git a/src/popup/views/send.js b/src/popup/views/send.js
index efb4f5c..0749ceb 100644
--- a/src/popup/views/send.js
+++ b/src/popup/views/send.js
@@ -92,6 +92,16 @@ function init(_ctx) {
const token = state.selectedToken || $("send-token").value;
const addr = currentAddress();
+ let tokenSymbol = null;
+ let tokenBalance = null;
+ if (token !== "ETH") {
+ const tb = (addr.tokenBalances || []).find(
+ (t) => t.address.toLowerCase() === token.toLowerCase(),
+ );
+ tokenSymbol = tb ? tb.symbol : "?";
+ tokenBalance = tb ? tb.balance || "0" : "0";
+ }
+
ctx.showConfirmTx({
from: addr.address,
to: resolvedTo,
@@ -99,6 +109,8 @@ function init(_ctx) {
amount: amount,
token: token,
balance: addr.balance,
+ tokenSymbol: tokenSymbol,
+ tokenBalance: tokenBalance,
});
});