Compare commits

..

2 Commits

Author SHA1 Message Date
user
b799686cd4 fix: zero-tx warning layout shift and contract address false positive
All checks were successful
check / check (push) Successful in 22s
- Reserve space for the warning upfront using visibility:hidden instead
  of display:none, preventing layout shift per README policy
- Move warning HTML to index.html as a static element rather than
  injecting dynamically
- Skip warning for contract addresses (check getCode first) since
  getTransactionCount only returns outgoing tx nonce
- Collapse reserved space when warning is not needed (address has
  history, is a contract, or on RPC error)
2026-02-28 14:18:28 -08:00
user
9e177f04a4 feat: show red warning when sending to address with zero tx history
On the confirm-tx view, asynchronously check the recipient address
transaction count via getTransactionCount(). If zero, display a
prominent red warning advising the user to double-check the address.

Closes #82
2026-02-28 14:18:28 -08:00
3 changed files with 40 additions and 51 deletions

View File

@@ -577,6 +577,19 @@
<div id="confirm-fee-amount" class="text-xs"></div> <div id="confirm-fee-amount" class="text-xs"></div>
</div> </div>
<div id="confirm-warnings" class="mb-2 hidden"></div> <div id="confirm-warnings" class="mb-2 hidden"></div>
<div
id="confirm-recipient-warning"
class="mb-2"
style="visibility: hidden"
>
<div
class="border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
>
WARNING: The recipient address has ZERO transaction
history. This may indicate a fresh or unused address.
Double-check the address before sending.
</div>
</div>
<div <div
id="confirm-errors" id="confirm-errors"
class="mb-2 border border-border border-dashed p-2 hidden" class="mb-2 border border-border border-dashed p-2 hidden"

View File

@@ -25,7 +25,6 @@ const { decryptWithPassword } = require("../../shared/vault");
const { formatUsd, getPrice } = require("../../shared/prices"); const { formatUsd, getPrice } = require("../../shared/prices");
const { getProvider } = require("../../shared/balances"); const { getProvider } = require("../../shared/balances");
const { isScamAddress } = require("../../shared/scamlist"); const { isScamAddress } = require("../../shared/scamlist");
const { hasTransactionHistory } = require("../../shared/transactions");
const { ERC20_ABI } = require("../../shared/constants"); const { ERC20_ABI } = require("../../shared/constants");
const { log } = require("../../shared/log"); const { log } = require("../../shared/log");
const makeBlockie = require("ethereum-blockies-base64"); const makeBlockie = require("ethereum-blockies-base64");
@@ -244,6 +243,12 @@ function show(txInfo) {
state.viewData = { pendingTx: txInfo }; state.viewData = { pendingTx: txInfo };
showView("confirm-tx"); showView("confirm-tx");
// Reset recipient warning: reserve space (visibility:hidden) while
// the async check runs, preventing layout shift per README policy.
const recipientWarning = $("confirm-recipient-warning");
recipientWarning.style.display = "";
recipientWarning.style.visibility = "hidden";
estimateGas(txInfo); estimateGas(txInfo);
checkRecipientHistory(txInfo); checkRecipientHistory(txInfo);
} }
@@ -289,27 +294,30 @@ async function estimateGas(txInfo) {
} }
async function checkRecipientHistory(txInfo) { async function checkRecipientHistory(txInfo) {
const el = $("confirm-recipient-warning");
try { try {
const hasHistory = await hasTransactionHistory( const provider = getProvider(state.rpcUrl);
txInfo.to, // Skip warning for contract addresses — they may legitimately
state.blockscoutUrl, // have zero outgoing transactions (getTransactionCount returns
); // the nonce, i.e. sent-tx count only).
if (hasHistory === false) { const code = await provider.getCode(txInfo.to);
const warningsEl = $("confirm-warnings"); if (code && code !== "0x") {
const warningDiv = document.createElement("div"); // Contract address — hide the reserved space entirely
warningDiv.className = el.style.display = "none";
"border border-dashed p-2 mb-1 text-xs font-bold"; return;
warningDiv.style.color = "#dc2626"; }
warningDiv.style.borderColor = "#dc2626"; const txCount = await provider.getTransactionCount(txInfo.to);
warningDiv.textContent = if (txCount === 0) {
"WARNING: This address has ZERO transaction history on-chain. " + el.style.visibility = "visible";
"It has never sent or received any transactions. " + } else {
"Double-check the address before sending."; // Address has history — collapse the reserved space
warningsEl.appendChild(warningDiv); el.style.display = "none";
warningsEl.classList.remove("hidden");
} }
} catch (e) { } catch (e) {
log.errorf("recipient history check failed:", e.message); log.errorf("recipient history check failed:", e.message);
// On error, collapse the reserved space rather than showing a
// false warning or leaving an empty gap
el.style.display = "none";
} }
} }

View File

@@ -251,36 +251,4 @@ function filterTransactions(txs, filters = {}) {
return { transactions: filtered, newFraudContracts: newFraud }; return { transactions: filtered, newFraudContracts: newFraud };
} }
async function hasTransactionHistory(address, blockscoutUrl) { module.exports = { fetchRecentTransactions, filterTransactions };
try {
const resp = await debugFetch(blockscoutUrl + "/addresses/" + address);
if (!resp.ok) {
// If Blockscout returns 404, the address has never been seen on-chain.
if (resp.status === 404) return false;
log.errorf(
"blockscout address check:",
resp.status,
resp.statusText,
);
return null; // unknown
}
const data = await resp.json();
// Blockscout v2 address endpoint returns tx counts.
// An address with no history may still exist (e.g. received ETH once
// but shows 0 outgoing). We check both transactions_count and
// token_transfers_count to be thorough.
const txCount =
(parseInt(data.transactions_count, 10) || 0) +
(parseInt(data.token_transfers_count, 10) || 0);
return txCount > 0;
} catch (e) {
log.errorf("hasTransactionHistory error:", e.message);
return null; // unknown, don't block the user
}
}
module.exports = {
fetchRecentTransactions,
filterTransactions,
hasTransactionHistory,
};