feat: expand confirm-tx warnings — closes #114
- Refactor address warnings into src/shared/addressWarnings.js module - getLocalWarnings(address, options): sync checks against local lists - getFullWarnings(address, provider, options): async local + RPC checks - Expand scam address list from 652 to 2417 addresses - Added EtherScamDB (MIT) as additional source - Update confirmTx.js to use the new addressWarnings module
This commit is contained in:
@@ -605,6 +605,31 @@
|
|||||||
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"
|
class="mb-2 border border-border border-dashed p-2"
|
||||||
|
|||||||
@@ -25,8 +25,11 @@ 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 {
|
||||||
const { ERC20_ABI } = require("../../shared/constants");
|
getLocalWarnings,
|
||||||
|
getFullWarnings,
|
||||||
|
} = require("../../shared/addressWarnings");
|
||||||
|
const { ERC20_ABI, isBurnAddress } = 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");
|
||||||
@@ -167,23 +170,17 @@ function show(txInfo) {
|
|||||||
$("confirm-balance").textContent = valueWithUsd(bal + " ETH", balUsd);
|
$("confirm-balance").textContent = valueWithUsd(bal + " ETH", balUsd);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for warnings
|
// Check for warnings (synchronous local checks)
|
||||||
const warnings = [];
|
const localWarnings = getLocalWarnings(txInfo.to, {
|
||||||
if (isScamAddress(txInfo.to)) {
|
fromAddress: txInfo.from,
|
||||||
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");
|
const warningsEl = $("confirm-warnings");
|
||||||
if (warnings.length > 0) {
|
if (localWarnings.length > 0) {
|
||||||
warningsEl.innerHTML = warnings
|
warningsEl.innerHTML = localWarnings
|
||||||
.map(
|
.map(
|
||||||
(w) =>
|
(w) =>
|
||||||
`<div class="border border-border border-dashed p-2 mb-1 text-xs font-bold">WARNING: ${w}</div>`,
|
`<div class="border border-border border-dashed p-2 mb-1 text-xs font-bold">WARNING: ${w.message}</div>`,
|
||||||
)
|
)
|
||||||
.join("");
|
.join("");
|
||||||
warningsEl.style.visibility = "visible";
|
warningsEl.style.visibility = "visible";
|
||||||
@@ -247,8 +244,15 @@ function show(txInfo) {
|
|||||||
state.viewData = { pendingTx: txInfo };
|
state.viewData = { pendingTx: txInfo };
|
||||||
showView("confirm-tx");
|
showView("confirm-tx");
|
||||||
|
|
||||||
// Reset recipient warning to hidden (space always reserved, no layout shift)
|
// Reset async warnings to hidden (space always reserved, no layout shift)
|
||||||
$("confirm-recipient-warning").style.visibility = "hidden";
|
$("confirm-recipient-warning").style.visibility = "hidden";
|
||||||
|
$("confirm-contract-warning").style.visibility = "hidden";
|
||||||
|
$("confirm-burn-warning").style.visibility = "hidden";
|
||||||
|
|
||||||
|
// 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);
|
||||||
checkRecipientHistory(txInfo);
|
checkRecipientHistory(txInfo);
|
||||||
@@ -295,19 +299,18 @@ async function estimateGas(txInfo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkRecipientHistory(txInfo) {
|
async function checkRecipientHistory(txInfo) {
|
||||||
const el = $("confirm-recipient-warning");
|
|
||||||
try {
|
try {
|
||||||
const provider = getProvider(state.rpcUrl);
|
const provider = getProvider(state.rpcUrl);
|
||||||
// Skip warning for contract addresses — they may legitimately
|
const asyncWarnings = await getFullWarnings(txInfo.to, provider, {
|
||||||
// have zero outgoing transactions (getTransactionCount returns
|
fromAddress: txInfo.from,
|
||||||
// the nonce, i.e. sent-tx count only).
|
});
|
||||||
const code = await provider.getCode(txInfo.to);
|
for (const w of asyncWarnings) {
|
||||||
if (code && code !== "0x") {
|
if (w.type === "contract") {
|
||||||
return;
|
$("confirm-contract-warning").style.visibility = "visible";
|
||||||
}
|
}
|
||||||
const txCount = await provider.getTransactionCount(txInfo.to);
|
if (w.type === "new-address") {
|
||||||
if (txCount === 0) {
|
$("confirm-recipient-warning").style.visibility = "visible";
|
||||||
el.style.visibility = "visible";
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.errorf("recipient history check failed:", e.message);
|
log.errorf("recipient history check failed:", e.message);
|
||||||
|
|||||||
98
src/shared/addressWarnings.js
Normal file
98
src/shared/addressWarnings.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// Address warning module.
|
||||||
|
// Provides local and async (RPC-based) warning checks for Ethereum addresses.
|
||||||
|
// Returns arrays of {type, message, severity} objects.
|
||||||
|
|
||||||
|
const { isScamAddress } = require("./scamlist");
|
||||||
|
const { isBurnAddress } = require("./constants");
|
||||||
|
const { log } = require("./log");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check an address against local-only lists (scam, burn, self-send).
|
||||||
|
* Synchronous — no network calls.
|
||||||
|
*
|
||||||
|
* @param {string} address - The target address to check.
|
||||||
|
* @param {object} [options] - Optional context.
|
||||||
|
* @param {string} [options.fromAddress] - Sender address (for self-send check).
|
||||||
|
* @returns {Array<{type: string, message: string, severity: string}>}
|
||||||
|
*/
|
||||||
|
function getLocalWarnings(address, options = {}) {
|
||||||
|
const warnings = [];
|
||||||
|
const addr = address.toLowerCase();
|
||||||
|
|
||||||
|
if (isScamAddress(addr)) {
|
||||||
|
warnings.push({
|
||||||
|
type: "scam",
|
||||||
|
message:
|
||||||
|
"This address is on a known scam/fraud list. Do not send funds to this address.",
|
||||||
|
severity: "critical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBurnAddress(addr)) {
|
||||||
|
warnings.push({
|
||||||
|
type: "burn",
|
||||||
|
message:
|
||||||
|
"This is a known null/burn address. Funds sent here are permanently destroyed and cannot be recovered.",
|
||||||
|
severity: "critical",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.fromAddress && addr === options.fromAddress.toLowerCase()) {
|
||||||
|
warnings.push({
|
||||||
|
type: "self-send",
|
||||||
|
message: "You are sending to your own address.",
|
||||||
|
severity: "warning",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check an address against local lists AND via RPC queries.
|
||||||
|
* Async — performs network calls to check contract status and tx history.
|
||||||
|
*
|
||||||
|
* @param {string} address - The target address to check.
|
||||||
|
* @param {object} provider - An ethers.js provider instance.
|
||||||
|
* @param {object} [options] - Optional context.
|
||||||
|
* @param {string} [options.fromAddress] - Sender address (for self-send check).
|
||||||
|
* @returns {Promise<Array<{type: string, message: string, severity: string}>>}
|
||||||
|
*/
|
||||||
|
async function getFullWarnings(address, provider, options = {}) {
|
||||||
|
const warnings = getLocalWarnings(address, options);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const code = await provider.getCode(address);
|
||||||
|
if (code && code !== "0x") {
|
||||||
|
warnings.push({
|
||||||
|
type: "contract",
|
||||||
|
message:
|
||||||
|
"This address is a smart contract, not a regular wallet.",
|
||||||
|
severity: "warning",
|
||||||
|
});
|
||||||
|
// If it's a contract, skip the tx count check — contracts
|
||||||
|
// may legitimately have zero inbound EOA transactions.
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.errorf("contract check failed:", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const txCount = await provider.getTransactionCount(address);
|
||||||
|
if (txCount === 0) {
|
||||||
|
warnings.push({
|
||||||
|
type: "new-address",
|
||||||
|
message:
|
||||||
|
"This address has never sent a transaction. Double-check it is correct.",
|
||||||
|
severity: "info",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.errorf("tx count check failed:", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getLocalWarnings, getFullWarnings };
|
||||||
@@ -20,6 +20,19 @@ 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,
|
||||||
@@ -28,4 +41,6 @@ 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
Reference in New Issue
Block a user