diff --git a/README.md b/README.md
index 57016b4..fcfcc93 100644
--- a/README.md
+++ b/README.md
@@ -846,6 +846,14 @@ wherever shown. If a formatting rule applies in one place, it applies in every
place. Users should never see the same value rendered differently on two
screens.
+The native token's label is the active network's `nativeCurrency` in
+`src/shared/networks.js`: `ETH` on mainnet, `SepoliaETH` on Sepolia. Wherever
+this 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 the
+refusal of a fee above 1 ETH, 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 its fixed wording.
+
**Specific Exception — Truncation:** On some non-critical display locations, we
may truncate _a small number_ of characters from the middle of an address solely
due to display size constraints. Wherever possible, and, notably, **in all
@@ -1106,7 +1114,8 @@ balance is nonzero and it is in the bundled known-token list, is tracked by the
user, or has 1,000 or more holders; a token claiming a symbol from the bundled
list from any other contract address is always dropped, and so is any token
claiming a symbol that belongs to the native asset and therefore has no
-legitimate contract at all (`"ETH"`). That filter is unconditional — the "Hide
+legitimate contract at all (`"ETH"`, and every network's `nativeCurrency`, such
+as `"SepoliaETH"`, on every network). That filter is unconditional — the "Hide
tokens with fewer than 1,000 holders" setting governs the transaction history
and the send-screen token selector, not this list. `fetchTokenBalances()` stores
every nonzero holding of a token it admits, however small, but a holding below
@@ -1589,7 +1598,9 @@ view would leave a wallet one click from deletion.
- "Transaction" heading, "Back" button
- Transaction hash: full hash (tap to copy) + etherscan link
- Type: transaction classification — one of: Native ETH Transfer, ERC-20
- Token Transfer, Swap, Token Approval, Contract Call, Contract Creation
+ Token Transfer, Swap, Token Approval, Contract Call, Contract Creation. A
+ transfer with a token contract is an ERC-20 Token Transfer whatever symbol
+ the token reports.
- Status: "Success" or "Failed"
- From: blockie + color dot + full address (tap to copy) + etherscan link;
ENS name if available
@@ -2362,7 +2373,8 @@ indexes it as a real token transfer.
act on and what the user believes they own rather than what the history
displays. All three surfaces read the rule from `src/shared/symbolSpoof.js`,
so they cannot answer the question differently. A symbol the list maps to no
- contract at all — `"ETH"`, the native asset, is the only one — may be borne by
+ contract at all — the native asset's labels: `"ETH"` and every network's
+ `nativeCurrency`, such as `"SepoliaETH"`, on every network — may be borne by
no contract, so every ERC-20 claiming it is a spoof on all three. The user's
real ETH balance is not an ERC-20 and is read over RPC, so the rule never sees
it.
diff --git a/TODO.md b/TODO.md
index fa65fb0..ecb02ec 100644
--- a/TODO.md
+++ b/TODO.md
@@ -45,6 +45,17 @@ but the review is broader than any of them.
# Completed Steps
+- 2026-10-04: The native token's label follows the active network
+ ([#372](https://git.eeqj.de/sneak/AutistMask/issues/372)). `networks.js` gives
+ each network a `nativeCurrency` and nothing read it: every screen wrote `ETH`,
+ so on Sepolia the balance, the value and the fee all read `ETH`. The balance
+ lists, Send, confirmation, approval, wait, success and error screens, the
+ transaction history, the contract-recipient warning and the refusal of a fee
+ above the limit now read `nativeCurrency`, which is `ETH` on mainnet and
+ `SepoliaETH` on Sepolia. A token claiming any network's `nativeCurrency` is
+ dropped as a fake, as one claiming `ETH` already was, and the transaction
+ detail screen calls an entry a token transfer when it has a token contract,
+ not by its symbol.
- 2026-10-04: A popup that is already open when the stored profile becomes
unreadable moves to the recovery screen
([#373](https://git.eeqj.de/sneak/AutistMask/issues/373)). It used to stay on
diff --git a/src/popup/index.html b/src/popup/index.html
index 208f185..83635ee 100644
--- a/src/popup/index.html
+++ b/src/popup/index.html
@@ -640,19 +640,13 @@
Double-check the address before sending.
+
-
- WARNING: The recipient is a smart contract. Sending ETH
- or tokens directly to a contract may result in permanent
- loss of funds.
-
-
+ >
+
- You do not have enough ETH to pay the network fee for this
- transfer. Please add ETH to this address and try again.
-
+ >
- fetchRecentTransactions(addr, state.blockscoutUrl),
+ fetchRecentTransactions(
+ addr,
+ state.blockscoutUrl,
+ nativeCurrency(),
+ ),
);
const results = await Promise.all(fetches);
diff --git a/src/popup/views/send.js b/src/popup/views/send.js
index 779ba6d..6b1133d 100644
--- a/src/popup/views/send.js
+++ b/src/popup/views/send.js
@@ -5,6 +5,8 @@ const {
showFlash,
addressTitle,
displaySymbol,
+ escapeHtml,
+ nativeCurrency,
renderAddressHtml,
attachCopyHandlers,
goBack,
@@ -124,7 +126,7 @@ function updateToValidation() {
function renderSendTokenSelect(addr) {
const sel = $("send-token");
- sel.innerHTML = '';
+ sel.innerHTML = ``;
const fraudSet = new Set(
(state.fraudContracts || []).map((a) => a.toLowerCase()),
);
@@ -204,7 +206,8 @@ function updateSendBalance() {
$("send-balance").textContent =
"Current balance: " +
truncateAmountNeverZero(addr.balance || "0") +
- " ETH";
+ " " +
+ nativeCurrency();
} else {
const symbol = resolveSymbol(
token,
diff --git a/src/popup/views/transactionDetail.js b/src/popup/views/transactionDetail.js
index f13eb10..2e9a9a9 100644
--- a/src/popup/views/transactionDetail.js
+++ b/src/popup/views/transactionDetail.js
@@ -18,6 +18,7 @@ const {
etherscanLinkHtml,
explorerUrl,
displaySymbol,
+ nativeCurrency,
goBack,
} = require("./helpers");
const { state } = require("../../shared/state");
@@ -41,8 +42,10 @@ function getTransactionType(tx) {
return "Token Approval";
return "Contract Call";
}
- if (tx.symbol && tx.symbol !== "ETH") return "ERC-20 Token Transfer";
- return "Native ETH Transfer";
+ // By the token contract, not the symbol: a token chooses its own symbol
+ // and can report the native token's, but only a token transfer has one.
+ if (tx.contractAddress) return "ERC-20 Token Transfer";
+ return "Native " + nativeCurrency() + " Transfer";
}
function blockieHtml(address) {
@@ -227,7 +230,7 @@ function populateOnChainDetails(txData) {
showDetailField(
"tx-detail-fee-section",
"tx-detail-fee",
- feeEth + " ETH",
+ feeEth + " " + nativeCurrency(),
);
}
diff --git a/src/popup/views/txStatus.js b/src/popup/views/txStatus.js
index 56537f4..6adb14a 100644
--- a/src/popup/views/txStatus.js
+++ b/src/popup/views/txStatus.js
@@ -12,6 +12,7 @@ const {
etherscanLinkHtml,
explorerUrl,
displaySymbol,
+ nativeCurrency,
clearViewStack,
} = require("./helpers");
const { resolveTokenSymbol } = require("../../shared/approvalAmount");
@@ -88,7 +89,7 @@ function startWait(txInfo, txHash, broadcastTime, pollNow) {
const symbol =
txInfo.token === "ETH"
- ? "ETH"
+ ? nativeCurrency()
: displaySymbol(txInfo.tokenSymbol || "?");
$("wait-tx-summary").textContent = txInfo.amount + " " + symbol;
$("wait-tx-to").innerHTML = toAddressHtml(txInfo.to);
@@ -223,7 +224,7 @@ function showSuccess(txInfo, txHash, blockNumber) {
const symbol =
txInfo.token === "ETH"
- ? "ETH"
+ ? nativeCurrency()
: displaySymbol(txInfo.tokenSymbol || "?");
state.viewData = {
amount: txInfo.amount,
@@ -320,7 +321,7 @@ function showError(txInfo, txHash, message) {
const symbol =
txInfo.token === "ETH"
- ? "ETH"
+ ? nativeCurrency()
: displaySymbol(txInfo.tokenSymbol || "?");
state.viewData = {
amount: txInfo.amount,
diff --git a/src/shared/approvalVerify.js b/src/shared/approvalVerify.js
index a642f7e..d63a999 100644
--- a/src/shared/approvalVerify.js
+++ b/src/shared/approvalVerify.js
@@ -54,9 +54,11 @@ const {
formatEther,
getAddress,
getBytes,
+ toQuantity,
verifyMessage,
verifyTypedData,
} = require("ethers");
+const { networkByChainId } = require("./networks");
// The only transaction types this wallet signs: legacy, EIP-2930 and
// EIP-1559. populateTransaction() produces nothing else, so nothing else can
@@ -407,12 +409,23 @@ function assertWithinCeilings(tx) {
price = normalizeQuantity(tx.gasPrice, "gas price");
}
if (price !== null && gasLimit * price > MAX_TOTAL_FEE) {
+ // The fee is paid in the native currency of the network the
+ // transaction is for. Every caller's transaction names it; one
+ // that does not, or names a network not in networks.js, says ETH.
+ const network = present(tx.chainId)
+ ? networkByChainId(toQuantity(tx.chainId))
+ : null;
+ const nativeCurrency = network ? network.nativeCurrency : "ETH";
throw refuse(
"This transaction would allow a network fee of up to " +
formatEther(gasLimit * price) +
- " ETH, which is more than the " +
+ " " +
+ nativeCurrency +
+ ", which is more than the " +
formatEther(MAX_TOTAL_FEE) +
- " ETH this wallet will sign for.",
+ " " +
+ nativeCurrency +
+ " this wallet will sign for.",
);
}
}
diff --git a/src/shared/symbolSpoof.js b/src/shared/symbolSpoof.js
index 5caea3d..c2d5947 100644
--- a/src/shared/symbolSpoof.js
+++ b/src/shared/symbolSpoof.js
@@ -11,8 +11,8 @@
// KNOWN_SYMBOLS maps a symbol to the set of lowercased contract addresses
// that may bear it, or to null. Null means the symbol belongs to the native
// asset, which has no contract at all, so no contract may bear it and every
-// one that does is a spoof. "ETH" is the only such entry today; the rule is
-// written so that a second one needs no change here or at any call site.
+// one that does is a spoof. "ETH" is one such entry, and every network's
+// `nativeCurrency` in networks.js (`SepoliaETH`) is another, on every network.
//
// The value is a set because a ticker is not unique: seven symbols in the
// bundled list belong to two real contracts each, and answering with one of
diff --git a/src/shared/tokenList.js b/src/shared/tokenList.js
index a960105..7acec67 100644
--- a/src/shared/tokenList.js
+++ b/src/shared/tokenList.js
@@ -6,6 +6,7 @@
// 511 tokens.
const { debugFetch } = require("./log");
+const { NETWORKS } = require("./networks");
const COINDESK_API = "https://data-api.coindesk.com/index/cc/v1/latest/tick";
@@ -3610,7 +3611,9 @@ for (const t of TOKENS) {
// Build a map of symbol (uppercased) -> the set of contract addresses
// (lowercased) that legitimately bear it. Used for spoofed-symbol detection.
// "ETH" maps to null: the native asset has no contract, so no contract may
-// bear its symbol.
+// bear its symbol. So does every network's `nativeCurrency` in networks.js
+// (`SepoliaETH`), on every network, since that is the label the wallet shows
+// its native asset under on that network.
//
// The value is a set and not a single address because tickers are not unique
// and the list above proves it: seven of these 512 tokens share a symbol with
@@ -3624,6 +3627,9 @@ for (const t of TOKENS) {
// loosen the rule, because a contract outside the set is still a spoof.
const KNOWN_SYMBOLS = new Map();
KNOWN_SYMBOLS.set("ETH", null);
+for (const network of Object.values(NETWORKS)) {
+ KNOWN_SYMBOLS.set(network.nativeCurrency.toUpperCase(), null);
+}
for (const t of TOKENS) {
const upper = t.symbol.toUpperCase();
if (!KNOWN_SYMBOLS.has(upper)) {
diff --git a/src/shared/transactions.js b/src/shared/transactions.js
index 3ae38da..7e85aa2 100644
--- a/src/shared/transactions.js
+++ b/src/shared/transactions.js
@@ -28,7 +28,7 @@ function normalizeAddress(addr) {
return (addr || "").toLowerCase();
}
-function parseTx(tx, addrLower) {
+function parseTx(tx, addrLower, nativeCurrency) {
const from = tx.from?.hash || "";
const to = tx.to?.hash || "";
const rawWei = tx.value || "0";
@@ -36,7 +36,7 @@ function parseTx(tx, addrLower) {
const method = tx.method || null;
// For contract calls, produce a meaningful label instead of "0.0000 ETH"
- let symbol = "ETH";
+ let symbol = nativeCurrency;
let value = formatTxValue(formatEther(rawWei));
let exactValue = formatEther(rawWei);
let rawAmount = rawWei;
@@ -221,7 +221,14 @@ function mergeTransactions(txs, tokenTransfers) {
return merged;
}
-async function fetchRecentTransactions(address, blockscoutUrl, count = 25) {
+// `nativeCurrency` is the active network's native token symbol from
+// networks.js (`ETH`, `SepoliaETH`), which a native entry is labelled with.
+async function fetchRecentTransactions(
+ address,
+ blockscoutUrl,
+ nativeCurrency,
+ count = 25,
+) {
log.debugf("fetchRecentTransactions", address);
const addrLower = normalizeAddress(address);
@@ -254,7 +261,9 @@ async function fetchRecentTransactions(address, blockscoutUrl, count = 25) {
const ttJson = ttResp.ok ? await ttResp.json() : {};
const txs = mergeTransactions(
- (txJson.items || []).map((tx) => parseTx(tx, addrLower)),
+ (txJson.items || []).map((tx) =>
+ parseTx(tx, addrLower, nativeCurrency),
+ ),
(ttJson.items || []).map((tt) => parseTokenTransfer(tt, addrLower)),
);
diff --git a/tests/approvalVerify.test.js b/tests/approvalVerify.test.js
index 1e0ba83..6b2a13c 100644
--- a/tests/approvalVerify.test.js
+++ b/tests/approvalVerify.test.js
@@ -599,6 +599,24 @@ describe("verifySignedTx field comparison", () => {
expect(e.message).toContain("1.0 ETH");
}
});
+
+ // The fee is in the native currency of the network the transaction is
+ // for, whether its chain id is the hex string the background prepares
+ // or the number ethers parses from a signed transaction.
+ test.each([
+ ["0x1", "ETH"],
+ [1n, "ETH"],
+ ["0xaa36a7", "SepoliaETH"],
+ [11155111n, "SepoliaETH"],
+ ])("the refusal on chain %p names %s", (chainId, nativeCurrency) => {
+ expect(() => assertWithinCeilings({ ...OVER, chainId })).toThrow(
+ "up to 3000.0 " +
+ nativeCurrency +
+ ", which is more than the 1.0 " +
+ nativeCurrency +
+ " this wallet",
+ );
+ });
});
test("every field mismatch is a refusal, not a warning", async () => {
diff --git a/tests/nativeCurrency.test.js b/tests/nativeCurrency.test.js
new file mode 100644
index 0000000..9b95193
--- /dev/null
+++ b/tests/nativeCurrency.test.js
@@ -0,0 +1,323 @@
+// The native token's label on the screens that show a native amount.
+//
+// src/shared/networks.js gives each network a nativeCurrency, `ETH` on mainnet
+// and `SepoliaETH` on Sepolia, and nothing read it: every screen wrote a
+// hardcoded "ETH", so on Sepolia the balance, the value and the fee all read
+// ETH (https://git.eeqj.de/sneak/AutistMask/issues/372). Each line is asserted
+// on both networks, through the real Send, confirmation and approval screens,
+// with only the node and the DOM stubbed. So is that a token cannot pass for
+// the native token by reporting its label.
+
+"use strict";
+
+jest.mock("ethers", () => {
+ const actual = jest.requireActual("ethers");
+ class StubProvider {
+ async lookupAddress() {
+ return null;
+ }
+ // 10 gwei expected, 20 gwei reserved per gas.
+ async getFeeData() {
+ return { maxFeePerGas: 20000000000n, gasPrice: 10000000000n };
+ }
+ async estimateGas() {
+ return 21000n;
+ }
+ async getCode() {
+ return "0x";
+ }
+ async getTransactionCount() {
+ return 1;
+ }
+ }
+ return {
+ ...actual,
+ JsonRpcProvider: StubProvider,
+ Network: { from: () => ({}) },
+ };
+});
+
+jest.mock("../src/shared/log", () => ({
+ log: {
+ debugf: () => {},
+ infof: () => {},
+ warnf: () => {},
+ errorf: () => {},
+ },
+ debugFetch: jest.fn(async () => ({
+ ok: true,
+ status: 200,
+ json: async () => ({ items: [] }),
+ })),
+ urlOrigin: () => "",
+ setRuntimeDebug: () => {},
+ isDebug: () => false,
+}));
+
+global.fetch = jest.fn(() => {
+ throw new Error("tests must not perform network requests");
+});
+
+// The approval the background hands the approval screen. Set per test.
+let approvalDetails = null;
+
+const { makeStorageStub } = require("./support/storageStub");
+global.chrome = {
+ storage: makeStorageStub(),
+ runtime: {
+ connect: () => ({
+ postMessage() {},
+ disconnect() {},
+ onDisconnect: { addListener() {} },
+ }),
+ sendMessage(message, callback) {
+ callback(
+ message.type === "AUTISTMASK_GET_APPROVAL"
+ ? approvalDetails
+ : undefined,
+ );
+ },
+ },
+};
+
+// A stub DOM: every id resolves to a recording element.
+const elements = new Map();
+
+function makeEl(id) {
+ const handlers = new Map();
+ return {
+ id,
+ textContent: "",
+ innerHTML: "",
+ value: "",
+ disabled: false,
+ style: {},
+ dataset: {},
+ classList: {
+ add() {},
+ remove() {},
+ toggle() {},
+ contains: () => false,
+ },
+ handlers,
+ children: [],
+ addEventListener(name, fn) {
+ handlers.set(name, fn);
+ },
+ appendChild(child) {
+ this.children.push(child);
+ return child;
+ },
+ querySelectorAll: () => [],
+ querySelector: () => null,
+ remove() {},
+ focus() {},
+ // Views reach for .parentElement to hide whole sections.
+ get parentElement() {
+ return global.document.getElementById(id + "-parent");
+ },
+ };
+}
+
+global.document = {
+ getElementById(id) {
+ if (!elements.has(id)) elements.set(id, makeEl(id));
+ return elements.get(id);
+ },
+ createElement: (tag) => makeEl(tag),
+ body: { prepend() {}, appendChild() {} },
+ addEventListener() {},
+};
+global.navigator = { clipboard: { writeText() {} } };
+
+const { state } = require("../src/shared/state");
+const { NETWORKS } = require("../src/shared/networks");
+const { clearPrices } = require("../src/shared/prices");
+const send = require("../src/popup/views/send");
+const confirmTx = require("../src/popup/views/confirmTx");
+const approval = require("../src/popup/views/approval");
+const transactionDetail = require("../src/popup/views/transactionDetail");
+const { balanceLinesForAddress } = require("../src/popup/views/helpers");
+const { filterTransactions } = require("../src/shared/transactions");
+
+const HOLDER = "0x" + "a".repeat(40);
+const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
+// A token contract that is not in the bundled token list.
+const TOKEN_CONTRACT = "0xd05339f9ea5ab9d9f03b9d57f671d2abd1f55c82";
+
+function text(id) {
+ return global.document.getElementById(id).textContent;
+}
+
+// Press Review on the Send screen for a native send of `amount`, and show the
+// confirmation screen it leads to with its fee estimate settled.
+async function confirmSend(amount) {
+ let txInfo = null;
+ send.init({ showConfirmTx: (info) => (txInfo = info) });
+ state.selectedToken = "ETH";
+ global.document.getElementById("send-to").value = RECIPIENT;
+ global.document.getElementById("send-amount").value = amount;
+ await global.document
+ .getElementById("btn-send-review")
+ .handlers.get("click")();
+ confirmTx.show(txInfo);
+ for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
+}
+
+// The approval screen for a dApp transaction sending 0.01 of the native token
+// with 21000 gas at up to 20 gwei, on the active network.
+async function approveTx() {
+ approvalDetails = {
+ type: "tx",
+ origin: "https://dapp.example",
+ approvedFrom: HOLDER,
+ approvedTx: {
+ to: RECIPIENT,
+ value: "10000000000000000",
+ data: "0x",
+ chainId: NETWORKS[state.networkId].chainId,
+ gasLimit: "21000",
+ maxFeePerGas: "20000000000",
+ nonce: 0,
+ },
+ };
+ await approval.show("1");
+}
+
+describe.each([
+ ["mainnet", "ETH"],
+ ["sepolia", "SepoliaETH"],
+])("on %s the native token reads %s", (networkId, symbol) => {
+ beforeEach(() => {
+ elements.clear();
+ clearPrices();
+ state.networkId = networkId;
+ state.wallets = [
+ {
+ name: "Wallet 1",
+ addresses: [{ address: HOLDER, balance: "1.5" }],
+ },
+ ];
+ state.selectedWallet = 0;
+ state.selectedAddress = 0;
+ state.trackedTokens = [];
+ state.fraudContracts = [];
+ state.currentView = null;
+ });
+
+ test("the balance", async () => {
+ const addr = state.wallets[0].addresses[0];
+ expect(balanceLinesForAddress(addr, [], false)).toContain(
+ `${symbol}1.5000`,
+ );
+ state.selectedToken = "ETH";
+ send.updateSendBalance();
+ expect(text("send-balance")).toBe("Current balance: 1.5000 " + symbol);
+ await confirmSend("0.1");
+ expect(text("confirm-balance")).toBe("1.5000 " + symbol);
+ });
+
+ test("the value", async () => {
+ await confirmSend("0.1");
+ expect(text("confirm-type")).toBe("Native " + symbol + " transfer");
+ expect(text("confirm-amount")).toBe("0.1 " + symbol);
+ await approveTx();
+ expect(text("approve-tx-value")).toBe("0.0100 " + symbol);
+ });
+
+ test("the fee", async () => {
+ await confirmSend("0.1");
+ // 21000 gas at 10 gwei expected, at 20 gwei reserved.
+ expect(text("confirm-fee-amount")).toBe("~0.0002 " + symbol);
+ expect(text("confirm-fee-reserve")).toBe(
+ "up to 0.0004 " + symbol + " reserved",
+ );
+ expect(text("confirm-gas-error")).toContain(
+ "You do not have enough " + symbol + " to pay the network fee",
+ );
+ await approveTx();
+ expect(text("approve-tx-fee")).toBe("0.0004 " + symbol);
+ });
+
+ test("the contract-recipient warning", async () => {
+ await confirmSend("0.1");
+ expect(text("confirm-contract-warning")).toContain(
+ "Sending " + symbol + " or tokens directly to a contract",
+ );
+ });
+
+ // 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
+ // a fake, exactly as one reporting `ETH` always was.
+ test.each(["ETH", symbol])(
+ "a token claiming %s is dropped from the history and the Send selector",
+ (claim) => {
+ const result = filterTransactions(
+ [
+ {
+ hash: "0x" + "1".repeat(64),
+ symbol: claim,
+ contractAddress: TOKEN_CONTRACT,
+ holders: 900000,
+ valueGwei: null,
+ isContractCall: false,
+ },
+ ],
+ { hideSpoofedSymbols: true },
+ );
+ expect(result.transactions).toEqual([]);
+ expect(result.newFraudContracts).toEqual([TOKEN_CONTRACT]);
+
+ send.renderSendTokenSelect({
+ address: HOLDER,
+ tokenBalances: [
+ {
+ address: TOKEN_CONTRACT,
+ symbol: claim,
+ decimals: 18,
+ balance: "5",
+ holders: 900000,
+ },
+ ],
+ });
+ expect(
+ global.document.getElementById("send-token").children,
+ ).toEqual([]);
+ },
+ );
+
+ // The detail screen tells the two apart by the token contract, which only
+ // a token transfer has, so a token reporting the native label still reads
+ // as a token transfer.
+ test("the transaction detail screen's type line", () => {
+ const entry = {
+ hash: "0x" + "2".repeat(64),
+ from: RECIPIENT,
+ to: HOLDER,
+ value: "1.0000",
+ exactValue: "1.0",
+ symbol,
+ timestamp: 1790000000,
+ isError: false,
+ direction: "received",
+ directionLabel: "Received",
+ };
+ transactionDetail.show({ ...entry, contractAddress: null });
+ expect(text("tx-detail-type")).toBe("Native " + symbol + " Transfer");
+ transactionDetail.show({ ...entry, contractAddress: TOKEN_CONTRACT });
+ expect(text("tx-detail-type")).toBe("ERC-20 Token Transfer");
+ });
+
+ test("the insufficient-balance error", async () => {
+ await confirmSend("2");
+ expect(
+ global.document.getElementById("confirm-errors").innerHTML,
+ ).toContain(
+ "You have 1.5000 " +
+ symbol +
+ " but are trying to send 2 " +
+ symbol +
+ ".",
+ );
+ });
+});
diff --git a/tests/transactions.test.js b/tests/transactions.test.js
index e1353ba..36d8815 100644
--- a/tests/transactions.test.js
+++ b/tests/transactions.test.js
@@ -1219,7 +1219,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
test("queries only the two Blockscout endpoints for the address", async () => {
respondWith([], []);
- await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
+ await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "ETH");
expect(debugFetch).toHaveBeenCalledTimes(2);
const urls = debugFetch.mock.calls.map((c) => c[0]);
expect(urls).toContain(
@@ -1283,7 +1283,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
],
);
- const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
+ const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "ETH");
expect(txs).toHaveLength(1);
const merged = txs[0];
// The received leg (the swap output) supplies the display amount.
@@ -1320,7 +1320,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
],
);
- const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
+ const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "ETH");
expect(txs).toHaveLength(1);
expect(txs[0].symbol).toBe("USDC");
expect(txs[0].value).toBe("1500.5000");
@@ -1346,7 +1346,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
});
respondWith([], [leg("1000000"), leg("2000000")]);
- const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
+ const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "ETH");
expect(txs).toHaveLength(1);
// Keyed by hash plus contract, so the later leg wins.
expect(txs[0].exactValue).toBe("2.0");
@@ -1386,7 +1386,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
],
);
- const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
+ const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "ETH");
expect(txs.map((t) => t.symbol).sort()).toEqual(["USDC", "WETH"]);
});
@@ -1427,7 +1427,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
],
);
- const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
+ const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "ETH");
expect(txs).toHaveLength(1);
expect(txs[0].symbol).toBe("USDC");
expect(txs[0].exactValue).toBe("1.0");
@@ -1452,10 +1452,43 @@ describe("fetchRecentTransactions merge and dedup", () => {
});
respondWith([item(6), item(8), item(7)], []);
- const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, 2);
+ const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "ETH", 2);
expect(txs.map((t) => t.blockNumber)).toEqual([21000008, 21000007]);
});
+ // https://git.eeqj.de/sneak/AutistMask/issues/372: the caller hands in
+ // the active network's nativeCurrency, and a native entry carries it.
+ test("a native entry is labelled with the native token symbol handed in", async () => {
+ respondWith(
+ [
+ {
+ hash: "0x" + "a".repeat(64),
+ block_number: 21000090,
+ timestamp: TS,
+ from: { hash: ORDINARY_PEER },
+ to: { hash: VICTIM, is_contract: false },
+ value: "10000000000000000",
+ method: null,
+ status: "ok",
+ },
+ ],
+ [],
+ );
+ const sepolia = await fetchRecentTransactions(
+ VICTIM,
+ BLOCKSCOUT,
+ "SepoliaETH",
+ );
+ expect(sepolia[0].symbol).toBe("SepoliaETH");
+ expect(sepolia[0].value).toBe("0.0100");
+ const mainnet = await fetchRecentTransactions(
+ VICTIM,
+ BLOCKSCOUT,
+ "ETH",
+ );
+ expect(mainnet[0].symbol).toBe("ETH");
+ });
+
test("the fake token transfer survives fetching and is then filtered", async () => {
respondWith(
[],
@@ -1476,7 +1509,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
],
);
- const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
+ const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "ETH");
expect(txs).toHaveLength(1);
expect(txs[0].contractAddress).toBe(FAKE_ETH_CONTRACT);
expect(txs[0].holders).toBe(0);
@@ -1515,19 +1548,31 @@ describe("fetchRecentTransactions merge and dedup", () => {
test("an omitted holders_count parses to null", async () => {
respondWith([], spamTransferWithToken(OMITTED));
- const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
+ const txs = await fetchRecentTransactions(
+ VICTIM,
+ BLOCKSCOUT,
+ "ETH",
+ );
expect(txs[0].holders).toBeNull();
});
test("a null holders_count parses to null", async () => {
respondWith([], spamTransferWithToken(NULLED));
- const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
+ const txs = await fetchRecentTransactions(
+ VICTIM,
+ BLOCKSCOUT,
+ "ETH",
+ );
expect(txs[0].holders).toBeNull();
});
test("the transfer survives the low-holder filter", async () => {
respondWith([], spamTransferWithToken(OMITTED));
- const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
+ const txs = await fetchRecentTransactions(
+ VICTIM,
+ BLOCKSCOUT,
+ "ETH",
+ );
expect(filterTransactions(txs, filters()).transactions).toEqual(
txs,
);
@@ -1539,7 +1584,11 @@ describe("fetchRecentTransactions merge and dedup", () => {
// holder count is the only rule that can catch it.
test('a reported holders_count of "0" still parses to 0 and is filtered', async () => {
respondWith([], spamTransferWithToken(ZERO));
- const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT);
+ const txs = await fetchRecentTransactions(
+ VICTIM,
+ BLOCKSCOUT,
+ "ETH",
+ );
expect(txs[0].holders).toBe(0);
expect(filterTransactions(txs, filters()).transactions).toEqual([]);
});
@@ -1555,7 +1604,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
},
}));
await expect(
- fetchRecentTransactions(VICTIM, BLOCKSCOUT),
+ fetchRecentTransactions(VICTIM, BLOCKSCOUT, "ETH"),
).resolves.toEqual([]);
});