fix: the recipient warning on ConfirmTx says what it checked and skips addresses holding ETH (closes #528) #571
@@ -1020,7 +1020,7 @@ on the approval screen, the amount on the wait, success and error screens, the
|
|||||||
transaction history and the transaction detail screen, and the refusal of a fee
|
transaction history and the transaction detail screen, and the refusal of a fee
|
||||||
above 1 ETH. A chain id that names no network reads `ETH`. Wherever this
|
above 1 ETH. A chain id that names no network reads `ETH`. Wherever this
|
||||||
document shows ETH as the label of a native balance, value or fee, in a "Native
|
document shows ETH as the label of a native balance, value or fee, in a "Native
|
||||||
ETH transfer" type line, in the contract-recipient warning or in that refusal,
|
ETH transfer" type line, in the two recipient warnings or in that refusal,
|
||||||
Sepolia shows `SepoliaETH`. The swap lines keep `ETH`, the router's own name for
|
Sepolia shows `SepoliaETH`. The swap lines keep `ETH`, the router's own name for
|
||||||
the native currency, and the ETH/USD price line, shown on mainnet only, keeps
|
the native currency, and the ETH/USD price line, shown on mainnet only, keeps
|
||||||
its fixed wording.
|
its fixed wording.
|
||||||
@@ -1730,8 +1730,10 @@ view would leave a wallet one click from deletion.
|
|||||||
reserved either way
|
reserved either way
|
||||||
- Warnings: inline warnings from the local checks (scam address, self-send)
|
- Warnings: inline warnings from the local checks (scam address, self-send)
|
||||||
plus four reserved warning boxes made visible by the async checks —
|
plus four reserved warning boxes made visible by the async checks —
|
||||||
recipient with no transaction history, recipient is a contract, burn
|
recipient that has never sent a transaction and holds no ETH, recipient is
|
||||||
address, and an Etherscan phishing/scam label
|
a contract, burn address, and an Etherscan phishing/scam label. An address
|
||||||
|
that has received ETH but never sent a transaction still holds that ETH,
|
||||||
|
so it gets no warning
|
||||||
- Errors (insufficient balance), plus three reserved error boxes — the
|
- Errors (insufficient balance), plus three reserved error boxes — the
|
||||||
amount plus the fee exceeds the balance (ETH transfers), not enough ETH to
|
amount plus the fee exceeds the balance (ETH transfers), not enough ETH to
|
||||||
pay the fee for the transfer (ERC-20 transfers), and the fee could not be
|
pay the fee for the transfer (ERC-20 transfers), and the fee could not be
|
||||||
|
|||||||
+6
-11
@@ -634,17 +634,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="confirm-warnings" class="mb-2 invisible"></div>
|
<div id="confirm-warnings" class="mb-2 invisible"></div>
|
||||||
<div id="confirm-recipient-warning" class="mb-2 invisible">
|
<!-- These two sentences name the network's native token, so
|
||||||
<div
|
show() in confirmTx.js sets them. -->
|
||||||
class="border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
|
<div
|
||||||
>
|
id="confirm-recipient-warning"
|
||||||
WARNING: The recipient address has ZERO transaction
|
class="mb-2 border border-red-500 border-dashed p-2 text-xs font-bold text-red-500 invisible"
|
||||||
history. This may indicate a fresh or unused address.
|
></div>
|
||||||
Double-check the address before sending.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Its sentence names the network's native token, so show()
|
|
||||||
in confirmTx.js sets it. -->
|
|
||||||
<div
|
<div
|
||||||
id="confirm-contract-warning"
|
id="confirm-contract-warning"
|
||||||
class="mb-2 border border-red-500 border-dashed p-2 text-xs font-bold text-red-500 invisible"
|
class="mb-2 border border-red-500 border-dashed p-2 text-xs font-bold text-red-500 invisible"
|
||||||
|
|||||||
@@ -217,7 +217,14 @@ function show(txInfo) {
|
|||||||
nativeCurrency() +
|
nativeCurrency() +
|
||||||
" to this address and try again.";
|
" to this address and try again.";
|
||||||
|
|
||||||
// Shown later, once checkRecipientHistory() finds a contract.
|
// Both shown later, by checkRecipientHistory(). The first sentence says
|
||||||
|
// exactly what getFullWarnings() checks.
|
||||||
|
$("confirm-recipient-warning").textContent =
|
||||||
|
"WARNING: The recipient address has never sent a transaction and" +
|
||||||
|
" holds no " +
|
||||||
|
nativeCurrency() +
|
||||||
|
". This may indicate a fresh or unused address. Double-check the" +
|
||||||
|
" address before sending.";
|
||||||
$("confirm-contract-warning").textContent =
|
$("confirm-contract-warning").textContent =
|
||||||
"WARNING: The recipient is a smart contract. Sending " +
|
"WARNING: The recipient is a smart contract. Sending " +
|
||||||
nativeCurrency() +
|
nativeCurrency() +
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ function getLocalWarnings(address, options = {}) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check an address against local lists AND via RPC queries.
|
* Check an address against local lists AND via RPC queries.
|
||||||
* Async — performs network calls to check contract status and tx history.
|
* Async — performs network calls to check contract status, tx count and
|
||||||
|
* balance.
|
||||||
*
|
*
|
||||||
* @param {string} address - The target address to check.
|
* @param {string} address - The target address to check.
|
||||||
* @param {object} provider - An ethers.js provider instance.
|
* @param {object} provider - An ethers.js provider instance.
|
||||||
@@ -78,21 +79,30 @@ async function getFullWarnings(address, provider, options = {}) {
|
|||||||
log.errorf("contract check failed:", e.shortMessage || e.message);
|
log.errorf("contract check failed:", e.shortMessage || e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip tx count check for contracts — they may legitimately have
|
// The transaction count is the number of transactions the address has
|
||||||
// zero inbound EOA transactions.
|
// sent, so an address that has only ever received has a count of zero.
|
||||||
|
// Native token it received stays in its balance until it sends a
|
||||||
|
// transaction, so the warning also needs a zero balance. A contract is
|
||||||
|
// skipped: it has its own warning above.
|
||||||
if (!isContract) {
|
if (!isContract) {
|
||||||
try {
|
try {
|
||||||
const txCount = await provider.getTransactionCount(address);
|
const txCount = await provider.getTransactionCount(address);
|
||||||
if (txCount === 0) {
|
if (txCount === 0) {
|
||||||
warnings.push({
|
const balance = await provider.getBalance(address);
|
||||||
type: "new-address",
|
if (balance === 0n) {
|
||||||
message:
|
warnings.push({
|
||||||
"This address has never sent a transaction. Double-check it is correct.",
|
type: "new-address",
|
||||||
severity: "info",
|
message:
|
||||||
});
|
"This address has never sent a transaction and holds none of the network's native token. Double-check it is correct.",
|
||||||
|
severity: "info",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.errorf("tx count check failed:", e.shortMessage || e.message);
|
log.errorf(
|
||||||
|
"tx count or balance check failed:",
|
||||||
|
e.shortMessage || e.message,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// The confirmation screen's warning for a recipient that has never sent a
|
||||||
|
// transaction (https://git.eeqj.de/sneak/AutistMask/issues/528).
|
||||||
|
//
|
||||||
|
// The node's transaction count for an address is the number of transactions
|
||||||
|
// it has sent, so an address that only ever receives, such as a savings
|
||||||
|
// address or an exchange deposit address, has a count of zero. The warning
|
||||||
|
// used to fire on that count alone. It now also needs a zero balance: ETH an
|
||||||
|
// address received stays in its balance until it sends a transaction.
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { getFullWarnings } = require("../src/shared/addressWarnings");
|
||||||
|
|
||||||
|
global.fetch = jest.fn(async () => {
|
||||||
|
throw new Error("tests must not perform network requests");
|
||||||
|
});
|
||||||
|
|
||||||
|
const RECIPIENT = "0x" + "b".repeat(40);
|
||||||
|
|
||||||
|
// The warning types getFullWarnings() returns for RECIPIENT when the node
|
||||||
|
// reports it as a regular address with `txCount` sent transactions and a
|
||||||
|
// balance of `balance` wei.
|
||||||
|
async function warningTypes(txCount, balance) {
|
||||||
|
const provider = {
|
||||||
|
getCode: async () => "0x",
|
||||||
|
getTransactionCount: async () => txCount,
|
||||||
|
getBalance: async () => balance,
|
||||||
|
};
|
||||||
|
const warnings = await getFullWarnings(RECIPIENT, provider);
|
||||||
|
return warnings.map((w) => w.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
test("a recipient that has received 0.5 ETH and never sent a transaction gets no warning", async () => {
|
||||||
|
expect(await warningTypes(0, 500000000000000000n)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a recipient that has never sent a transaction and holds no ETH gets the warning", async () => {
|
||||||
|
expect(await warningTypes(0, 0n)).toEqual(["new-address"]);
|
||||||
|
});
|
||||||
@@ -267,6 +267,13 @@ describe.each([
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("the warning for a recipient that has never sent a transaction", async () => {
|
||||||
|
await confirmSend("0.1");
|
||||||
|
expect(text("confirm-recipient-warning")).toContain(
|
||||||
|
"has never sent a transaction and holds no " + symbol + ".",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// A token reports whatever symbol it likes. One reporting the label the
|
// A token reports whatever symbol it likes. One reporting the label the
|
||||||
// wallet shows its native token under, on this network or any other, is
|
// wallet shows its native token under, on this network or any other, is
|
||||||
// a fake, exactly as one reporting `ETH` always was.
|
// a fake, exactly as one reporting `ETH` always was.
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ test("ethers puts the URL in the error message, not in the short message", async
|
|||||||
test("the recipient checks before a send", async () => {
|
test("the recipient checks before a send", async () => {
|
||||||
await getFullWarnings(ADDRESS, getProvider(RPC_URL, "mainnet"));
|
await getFullWarnings(ADDRESS, getProvider(RPC_URL, "mainnet"));
|
||||||
expectFailureLoggedWithoutKey("contract check failed");
|
expectFailureLoggedWithoutKey("contract check failed");
|
||||||
expectFailureLoggedWithoutKey("tx count check failed");
|
expectFailureLoggedWithoutKey("tx count or balance check failed");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("the ENS reverse lookup", async () => {
|
test("the ENS reverse lookup", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user