Compare commits

..

1 Commits

Author SHA1 Message Date
user
f01a662000 feat: expand warning types for send confirmation
All checks were successful
check / check (push) Successful in 23s
- Combine MEW darklist (652) and CryptoScamDB (2043) into 2314 scam addresses
- Add null/burn address detection with permanent loss warning
- Add contract address detection warning (sending directly to contracts)
- Unify all warnings into single warnings element (sync + async)
- Zero-history warning now uses unified warning system

Closes #114
2026-02-28 16:11:02 -08:00
4 changed files with 1732 additions and 70 deletions

View File

@@ -599,31 +599,6 @@
Double-check the address before sending. Double-check the address before sending.
</div> </div>
</div> </div>
<div
id="confirm-contract-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 is a smart contract. Sending ETH
or tokens directly to a contract may result in permanent
loss of funds.
</div>
</div>
<div
id="confirm-burn-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: This is a known null/burn address. Funds sent
here are permanently destroyed and cannot be recovered.
</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

@@ -24,8 +24,8 @@ const { getSignerForAddress } = require("../../shared/wallet");
const { decryptWithPassword } = require("../../shared/vault"); 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, isNullOrBurnAddress } = require("../../shared/scamlist");
const { ERC20_ABI, isBurnAddress } = 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");
const txStatus = require("./txStatus"); const txStatus = require("./txStatus");
@@ -38,6 +38,28 @@ const EXT_ICON =
`</svg></span>`; `</svg></span>`;
let pendingTx = null; let pendingTx = null;
// Track active warnings so async checks can append without overwriting.
let activeWarnings = [];
function renderWarnings(el, warnings) {
activeWarnings = warnings.slice();
if (warnings.length > 0) {
el.innerHTML = warnings
.map(
(w) =>
`<div class="border border-border border-dashed p-2 mb-1 text-xs font-bold">WARNING: ${w}</div>`,
)
.join("");
el.classList.remove("hidden");
} else {
el.classList.add("hidden");
}
}
function appendWarning(el, message) {
activeWarnings.push(message);
renderWarnings(el, activeWarnings);
}
function restore() { function restore() {
const d = state.viewData; const d = state.viewData;
@@ -165,16 +187,16 @@ function show(txInfo) {
$("confirm-balance").textContent = valueWithUsd(bal + " ETH", balUsd); $("confirm-balance").textContent = valueWithUsd(bal + " ETH", balUsd);
} }
// Check for warnings (synchronous checks) // Check for warnings (synchronous checks first, async checks added later)
const warnings = []; const warnings = [];
if (isScamAddress(txInfo.to)) { if (isScamAddress(txInfo.to)) {
warnings.push( warnings.push(
"This address is on a known scam/fraud list. Do not send funds to this address.", "This address is on a known scam/fraud list. Do not send funds to this address.",
); );
} }
if (isBurnAddress(txInfo.to)) { if (isNullOrBurnAddress(txInfo.to)) {
warnings.push( warnings.push(
"This is a known null/burn address. Funds sent here are permanently destroyed and cannot be recovered.", "This is a null or burn address. Funds sent here will be permanently lost.",
); );
} }
if (txInfo.to.toLowerCase() === txInfo.from.toLowerCase()) { if (txInfo.to.toLowerCase() === txInfo.from.toLowerCase()) {
@@ -182,17 +204,7 @@ function show(txInfo) {
} }
const warningsEl = $("confirm-warnings"); const warningsEl = $("confirm-warnings");
if (warnings.length > 0) { renderWarnings(warningsEl, warnings);
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 // Check for errors
const errors = []; const errors = [];
@@ -248,14 +260,10 @@ function show(txInfo) {
state.viewData = { pendingTx: txInfo }; state.viewData = { pendingTx: txInfo };
showView("confirm-tx"); showView("confirm-tx");
// Reset async warnings to hidden (space always reserved, no layout shift) // Hide the legacy recipient warning element (warnings now unified)
$("confirm-recipient-warning").style.visibility = "hidden"; const legacyWarningEl = $("confirm-recipient-warning");
$("confirm-contract-warning").style.visibility = "hidden"; if (legacyWarningEl) {
$("confirm-burn-warning").style.visibility = "hidden"; legacyWarningEl.style.display = "none";
// Show burn warning via reserved element (in addition to inline warning)
if (isBurnAddress(txInfo.to)) {
$("confirm-burn-warning").style.visibility = "visible";
} }
estimateGas(txInfo); estimateGas(txInfo);
@@ -303,17 +311,24 @@ async function estimateGas(txInfo) {
} }
async function checkRecipientHistory(txInfo) { async function checkRecipientHistory(txInfo) {
const warningsEl = $("confirm-warnings");
try { try {
const provider = getProvider(state.rpcUrl); const provider = getProvider(state.rpcUrl);
const code = await provider.getCode(txInfo.to); const code = await provider.getCode(txInfo.to);
if (code && code !== "0x") { if (code && code !== "0x") {
// Recipient is a contract — warn the user // Recipient is a contract address — warn the user
$("confirm-contract-warning").style.visibility = "visible"; appendWarning(
warningsEl,
"The recipient is a contract address. Sending tokens directly to a contract may result in permanent loss of funds.",
);
return; return;
} }
const txCount = await provider.getTransactionCount(txInfo.to); const txCount = await provider.getTransactionCount(txInfo.to);
if (txCount === 0) { if (txCount === 0) {
$("confirm-recipient-warning").style.visibility = "visible"; appendWarning(
warningsEl,
"The recipient address has ZERO transaction history. This may indicate a fresh or unused address. Double-check the address before sending.",
);
} }
} catch (e) { } catch (e) {
log.errorf("recipient history check failed:", e.message); log.errorf("recipient history check failed:", e.message);

View File

@@ -20,19 +20,6 @@ const ERC20_ABI = [
"function approve(address spender, uint256 amount) returns (bool)", "function approve(address spender, uint256 amount) returns (bool)",
]; ];
// Known null/burn addresses that permanently destroy funds.
const BURN_ADDRESSES = new Set([
"0x0000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000001",
"0x000000000000000000000000000000000000dead",
"0xdead000000000000000000000000000000000000",
"0x00000000000000000000000000000000deadbeef",
]);
function isBurnAddress(address) {
return BURN_ADDRESSES.has(address.toLowerCase());
}
module.exports = { module.exports = {
DEBUG, DEBUG,
DEBUG_MNEMONIC, DEBUG_MNEMONIC,
@@ -41,6 +28,4 @@ module.exports = {
DEFAULT_BLOCKSCOUT_URL, DEFAULT_BLOCKSCOUT_URL,
BIP44_ETH_PATH, BIP44_ETH_PATH,
ERC20_ABI, ERC20_ABI,
BURN_ADDRESSES,
isBurnAddress,
}; };

File diff suppressed because it is too large Load Diff