Compare commits

...
Author SHA1 Message Date
clawbot f47d17a98f chore: the native token's label follows the active network (closes #372)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s
networks.js gives each network a nativeCurrency (ETH, SepoliaETH) and nothing
read it: every screen wrote a hardcoded ETH, so on Sepolia the balance, the
value and the fee all read ETH. A nativeCurrency() helper in the popup's
helpers.js returns the active network's, and every native balance, value, fee,
reserve, type line and history entry is labelled with it. The "ETH" that
selectedToken and txInfo.token hold is the native token's id and is unchanged.
The confirmation screen's not-enough-to-pay-the-fee sentence now names the
coin to add, so show() sets it.

Model: opus-5-5
2026-10-04 21:53:00 +00:00
15 changed files with 407 additions and 43 deletions
+8
View File
@@ -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 place. Users should never see the same value rendered differently on two
screens. 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, or in a
"Native ETH transfer" type line, Sepolia shows `SepoliaETH`. The swap lines keep
`ETH`, the router's own name for the native currency, and the ETH/USD price
line, the contract-recipient warning and the refusal of a fee above 1 ETH keep
their fixed wording.
**Specific Exception — Truncation:** On some non-critical display locations, we **Specific Exception — Truncation:** On some non-critical display locations, we
may truncate _a small number_ of characters from the middle of an address solely 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 due to display size constraints. Wherever possible, and, notably, **in all
+7
View File
@@ -45,6 +45,13 @@ but the review is broader than any of them.
# Completed Steps # 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, and the
transaction history now read `nativeCurrency`, which is `ETH` on mainnet and
`SepoliaETH` on Sepolia.
- 2026-10-04: A swap whose deadline is later than a JavaScript date can hold is - 2026-10-04: A swap whose deadline is later than a JavaScript date can hold is
decoded ([#437](https://git.eeqj.de/sneak/AutistMask/issues/437)). A date decoded ([#437](https://git.eeqj.de/sneak/AutistMask/issues/437)). A date
reaches only to 275760-09-13, so a later deadline, such as the `uint256` reaches only to 275760-09-13, so a later deadline, such as the `uint256`
+3 -4
View File
@@ -690,14 +690,13 @@
Your balance does not cover this amount plus the network Your balance does not cover this amount plus the network
fee. Please go back and send a smaller amount. fee. Please go back and send a smaller amount.
</div> </div>
<!-- Its sentence names the network's native token, so show()
in confirmTx.js sets it. -->
<div <div
id="confirm-gas-error" id="confirm-gas-error"
class="mb-2 border border-border border-dashed p-2 text-xs" class="mb-2 border border-border border-dashed p-2 text-xs"
style="visibility: hidden" style="visibility: hidden"
> ></div>
You do not have enough ETH to pay the network fee for this
transfer. Please add ETH to this address and try again.
</div>
<!-- Its sentence names why the fee could not be estimated, <!-- Its sentence names why the fee could not be estimated,
so show() in confirmTx.js sets it. --> so show() in confirmTx.js sets it. -->
<div <div
+2
View File
@@ -7,6 +7,7 @@ const {
addressTitle, addressTitle,
escapeHtml, escapeHtml,
displaySymbol, displaySymbol,
nativeCurrency,
renderAddressHtml, renderAddressHtml,
attachCopyHandlers, attachCopyHandlers,
goBack, goBack,
@@ -153,6 +154,7 @@ async function loadTransactions(address) {
const rawTxs = await fetchRecentTransactions( const rawTxs = await fetchRecentTransactions(
address, address,
state.blockscoutUrl, state.blockscoutUrl,
nativeCurrency(),
); );
const result = filterTransactions(rawTxs, { const result = filterTransactions(rawTxs, {
hideSpoofedSymbols: state.hideSpoofedSymbols, hideSpoofedSymbols: state.hideSpoofedSymbols,
+3 -1
View File
@@ -10,6 +10,7 @@ const {
addressTitle, addressTitle,
escapeHtml, escapeHtml,
displaySymbol, displaySymbol,
nativeCurrency,
balanceLine, balanceLine,
unknownableAmount, unknownableAmount,
renderAddressHtml, renderAddressHtml,
@@ -106,7 +107,7 @@ function show() {
let symbol, amount, price; let symbol, amount, price;
const knownToken = TOKEN_BY_ADDRESS.get(tokenId.toLowerCase()); const knownToken = TOKEN_BY_ADDRESS.get(tokenId.toLowerCase());
if (tokenId === "ETH") { if (tokenId === "ETH") {
symbol = "ETH"; symbol = nativeCurrency();
amount = parseFloat(addr.balance || "0"); amount = parseFloat(addr.balance || "0");
price = getPrice("ETH"); price = getPrice("ETH");
} else { } else {
@@ -226,6 +227,7 @@ async function loadTransactions(address, tokenId) {
const rawTxs = await fetchRecentTransactions( const rawTxs = await fetchRecentTransactions(
address, address,
state.blockscoutUrl, state.blockscoutUrl,
nativeCurrency(),
); );
const result = filterTransactions(rawTxs, { const result = filterTransactions(rawTxs, {
hideSpoofedSymbols: state.hideSpoofedSymbols, hideSpoofedSymbols: state.hideSpoofedSymbols,
+5 -1
View File
@@ -10,6 +10,7 @@ const {
attachCopyHandlers, attachCopyHandlers,
onViewLeave, onViewLeave,
formatFee, formatFee,
nativeCurrency,
} = require("./helpers"); } = require("./helpers");
const { state, saveState } = require("../../shared/state"); const { state, saveState } = require("../../shared/state");
const { networkByChainId } = require("../../shared/networks"); const { networkByChainId } = require("../../shared/networks");
@@ -330,7 +331,10 @@ function showTxApproval(details) {
const ethUsd = ethPrice ? parseFloat(ethValueFormatted) * ethPrice : null; const ethUsd = ethPrice ? parseFloat(ethValueFormatted) * ethPrice : null;
const usdStr = formatUsd(ethUsd); const usdStr = formatUsd(ethUsd);
$("approve-tx-value").textContent = $("approve-tx-value").textContent =
ethValueFormatted + " ETH" + (usdStr ? " (" + usdStr + ")" : ""); ethValueFormatted +
" " +
nativeCurrency() +
(usdStr ? " (" + usdStr + ")" : "");
showTxFee(approvedTx); showTxFee(approvedTx);
+23 -8
View File
@@ -11,6 +11,7 @@ const {
addressTitle, addressTitle,
escapeHtml, escapeHtml,
displaySymbol, displaySymbol,
nativeCurrency,
renderAddressHtml, renderAddressHtml,
attachCopyHandlers, attachCopyHandlers,
goBack, goBack,
@@ -90,7 +91,7 @@ function show(txInfo) {
// The raw symbol is the price-table key; the capped one is what the // The raw symbol is the price-table key; the capped one is what the
// screen says. Truncating before the lookup would silently drop the // screen says. Truncating before the lookup would silently drop the
// price of any token whose symbol is long enough to be capped. // price of any token whose symbol is long enough to be capped.
const rawSymbol = isErc20 ? txInfo.tokenSymbol || "?" : "ETH"; const rawSymbol = isErc20 ? txInfo.tokenSymbol || "?" : nativeCurrency();
const symbol = displaySymbol(rawSymbol); const symbol = displaySymbol(rawSymbol);
// Transaction type // Transaction type
@@ -98,7 +99,7 @@ function show(txInfo) {
$("confirm-type").textContent = $("confirm-type").textContent =
"ERC-20 token transfer (" + symbol + ")"; "ERC-20 token transfer (" + symbol + ")";
} else { } else {
$("confirm-type").textContent = "Native ETH transfer"; $("confirm-type").textContent = "Native " + symbol + " transfer";
} }
// Token contract section (ERC-20 only) // Token contract section (ERC-20 only)
@@ -162,7 +163,7 @@ function show(txInfo) {
const bal = txInfo.balance || "0"; const bal = txInfo.balance || "0";
const balUsd = ethPrice ? parseFloat(bal) * ethPrice : null; const balUsd = ethPrice ? parseFloat(bal) * ethPrice : null;
$("confirm-balance").textContent = valueWithUsd( $("confirm-balance").textContent = valueWithUsd(
truncateAmountNeverZero(bal) + " ETH", truncateAmountNeverZero(bal) + " " + symbol,
balUsd, balUsd,
); );
} }
@@ -197,6 +198,12 @@ function show(txInfo) {
// estimate landing later never moves anything. // estimate landing later never moves anything.
$("confirm-amount-fee-error").classList.toggle("hidden", isErc20); $("confirm-amount-fee-error").classList.toggle("hidden", isErc20);
$("confirm-gas-error").classList.toggle("hidden", !isErc20); $("confirm-gas-error").classList.toggle("hidden", !isErc20);
$("confirm-gas-error").textContent =
"You do not have enough " +
nativeCurrency() +
" to pay the network fee for this transfer. Please add " +
nativeCurrency() +
" to this address and try again.";
// The fee-unknown message names its cause, which is also known here. // The fee-unknown message names its cause, which is also known here.
// Without the token's scale estimateGas() cannot encode the transfer, so // Without the token's scale estimateGas() cannot encode the transfer, so
@@ -245,7 +252,9 @@ function show(txInfo) {
// touches already occupies its space, so re-running it never moves anything. // touches already occupies its space, so re-running it never moves anything.
function renderValidation(txInfo) { function renderValidation(txInfo) {
const isErc20 = txInfo.token !== "ETH"; const isErc20 = txInfo.token !== "ETH";
const symbol = isErc20 ? displaySymbol(txInfo.tokenSymbol || "?") : "ETH"; const symbol = isErc20
? displaySymbol(txInfo.tokenSymbol || "?")
: nativeCurrency();
const { canSend, codes } = validateTransfer({ const { canSend, codes } = validateTransfer({
isErc20, isErc20,
@@ -258,7 +267,7 @@ function renderValidation(txInfo) {
// Messages carrying the user's own numbers are built here; the fixed // Messages carrying the user's own numbers are built here; the fixed
// sentences live in the reserved elements in index.html, except the // sentences live in the reserved elements in index.html, except the
// fee-unknown one, which show() sets. // gas and fee-unknown ones, which show() sets.
const messages = []; const messages = [];
if (codes.includes(CODES.AMOUNT_INVALID)) { if (codes.includes(CODES.AMOUNT_INVALID)) {
messages.push("Please enter a valid amount to send."); messages.push("Please enter a valid amount to send.");
@@ -287,9 +296,13 @@ function renderValidation(txInfo) {
messages.push( messages.push(
"Insufficient balance. You have " + "Insufficient balance. You have " +
truncateAmountNeverZero(txInfo.balance || "0") + truncateAmountNeverZero(txInfo.balance || "0") +
" ETH but are trying to send " + " " +
symbol +
" but are trying to send " +
txInfo.amount + txInfo.amount +
" ETH.", " " +
symbol +
".",
); );
} }
@@ -382,7 +395,9 @@ async function estimateGas(txInfo) {
$("confirm-fee-reserve").textContent = $("confirm-fee-reserve").textContent =
"up to " + "up to " +
truncateAmountNeverZero(formatEther(gasCostWei)) + truncateAmountNeverZero(formatEther(gasCostWei)) +
" ETH reserved"; " " +
nativeCurrency() +
" reserved";
setVisible("confirm-fee-reserve", true); setVisible("confirm-fee-reserve", true);
} else { } else {
// No spread to report: either there is no estimate, or the node // No spread to report: either there is no estimate, or the node
+14 -2
View File
@@ -252,6 +252,14 @@ function unknownableAmount(balance) {
return Number.isFinite(n) ? n : null; return Number.isFinite(n) ? n : null;
} }
// The active network's native token symbol, `ETH` on mainnet and `SepoliaETH`
// on Sepolia, as src/shared/networks.js names it. Every screen labels a native
// amount with this. The "ETH" that state.selectedToken and txInfo.token hold
// is the native token's id, not its label, and stays "ETH" on every network.
function nativeCurrency() {
return currentNetwork().nativeCurrency;
}
// A network fee in wei as the confirmation and approval screens both show it: // A network fee in wei as the confirmation and approval screens both show it:
// the ETH figure through truncateAmountNeverZero(), then its USD value when the // the ETH figure through truncateAmountNeverZero(), then its USD value when the
// ETH price is known. The USD value is of the exact fee, not of the truncated // ETH price is known. The USD value is of the exact fee, not of the truncated
@@ -261,7 +269,10 @@ function formatFee(wei) {
const ethPrice = getPrice("ETH"); const ethPrice = getPrice("ETH");
const usd = ethPrice ? formatUsd(parseFloat(eth) * ethPrice) : ""; const usd = ethPrice ? formatUsd(parseFloat(eth) * ethPrice) : "";
return ( return (
truncateAmountNeverZero(eth) + " ETH" + (usd ? " (" + usd + ")" : "") truncateAmountNeverZero(eth) +
" " +
nativeCurrency() +
(usd ? " (" + usd + ")" : "")
); );
} }
@@ -303,7 +314,7 @@ function balanceLine(symbol, amount, price, tokenId) {
function balanceLinesForAddress(addr, trackedTokens, showZero) { function balanceLinesForAddress(addr, trackedTokens, showZero) {
let html = balanceLine( let html = balanceLine(
"ETH", nativeCurrency(),
parseFloat(addr.balance || "0"), parseFloat(addr.balance || "0"),
getPrice("ETH"), getPrice("ETH"),
"ETH", "ETH",
@@ -660,6 +671,7 @@ module.exports = {
balanceLinesForAddress, balanceLinesForAddress,
addressHoldsFunds, addressHoldsFunds,
unknownableAmount, unknownableAmount,
nativeCurrency,
formatFee, formatFee,
addressColor, addressColor,
addressDotHtml, addressDotHtml,
+7 -2
View File
@@ -10,6 +10,7 @@ const {
addressTitle, addressTitle,
escapeHtml, escapeHtml,
displaySymbol, displaySymbol,
nativeCurrency,
renderAddressHtml, renderAddressHtml,
attachCopyHandlers, attachCopyHandlers,
pushCurrentView, pushCurrentView,
@@ -68,7 +69,7 @@ function renderTotalValue() {
return; return;
} }
const ethBal = parseFloat(addr.balance || "0"); const ethBal = parseFloat(addr.balance || "0");
const ethStr = ethBal.toFixed(4) + " ETH"; const ethStr = ethBal.toFixed(4) + " " + nativeCurrency();
const ethUsd = ethPrice ? " (" + formatUsd(ethBal * ethPrice) + ")" : ""; const ethUsd = ethPrice ? " (" + formatUsd(ethBal * ethPrice) + ")" : "";
el.textContent = ethStr + ethUsd; el.textContent = ethStr + ethUsd;
@@ -182,7 +183,11 @@ async function loadHomeTxs(ctx) {
try { try {
const fetches = allAddresses.map((addr) => const fetches = allAddresses.map((addr) =>
fetchRecentTransactions(addr, state.blockscoutUrl), fetchRecentTransactions(
addr,
state.blockscoutUrl,
nativeCurrency(),
),
); );
const results = await Promise.all(fetches); const results = await Promise.all(fetches);
+5 -2
View File
@@ -5,6 +5,8 @@ const {
showFlash, showFlash,
addressTitle, addressTitle,
displaySymbol, displaySymbol,
escapeHtml,
nativeCurrency,
renderAddressHtml, renderAddressHtml,
attachCopyHandlers, attachCopyHandlers,
goBack, goBack,
@@ -124,7 +126,7 @@ function updateToValidation() {
function renderSendTokenSelect(addr) { function renderSendTokenSelect(addr) {
const sel = $("send-token"); const sel = $("send-token");
sel.innerHTML = '<option value="ETH">ETH</option>'; sel.innerHTML = `<option value="ETH">${escapeHtml(nativeCurrency())}</option>`;
const fraudSet = new Set( const fraudSet = new Set(
(state.fraudContracts || []).map((a) => a.toLowerCase()), (state.fraudContracts || []).map((a) => a.toLowerCase()),
); );
@@ -204,7 +206,8 @@ function updateSendBalance() {
$("send-balance").textContent = $("send-balance").textContent =
"Current balance: " + "Current balance: " +
truncateAmountNeverZero(addr.balance || "0") + truncateAmountNeverZero(addr.balance || "0") +
" ETH"; " " +
nativeCurrency();
} else { } else {
const symbol = resolveSymbol( const symbol = resolveSymbol(
token, token,
+6 -3
View File
@@ -18,6 +18,7 @@ const {
etherscanLinkHtml, etherscanLinkHtml,
explorerUrl, explorerUrl,
displaySymbol, displaySymbol,
nativeCurrency,
goBack, goBack,
} = require("./helpers"); } = require("./helpers");
const { state } = require("../../shared/state"); const { state } = require("../../shared/state");
@@ -41,8 +42,10 @@ function getTransactionType(tx) {
return "Token Approval"; return "Token Approval";
return "Contract Call"; return "Contract Call";
} }
if (tx.symbol && tx.symbol !== "ETH") return "ERC-20 Token Transfer"; if (tx.symbol && tx.symbol !== nativeCurrency()) {
return "Native ETH Transfer"; return "ERC-20 Token Transfer";
}
return "Native " + nativeCurrency() + " Transfer";
} }
function blockieHtml(address) { function blockieHtml(address) {
@@ -227,7 +230,7 @@ function populateOnChainDetails(txData) {
showDetailField( showDetailField(
"tx-detail-fee-section", "tx-detail-fee-section",
"tx-detail-fee", "tx-detail-fee",
feeEth + " ETH", feeEth + " " + nativeCurrency(),
); );
} }
+4 -3
View File
@@ -12,6 +12,7 @@ const {
etherscanLinkHtml, etherscanLinkHtml,
explorerUrl, explorerUrl,
displaySymbol, displaySymbol,
nativeCurrency,
clearViewStack, clearViewStack,
} = require("./helpers"); } = require("./helpers");
const { resolveTokenSymbol } = require("../../shared/approvalAmount"); const { resolveTokenSymbol } = require("../../shared/approvalAmount");
@@ -88,7 +89,7 @@ function startWait(txInfo, txHash, broadcastTime, pollNow) {
const symbol = const symbol =
txInfo.token === "ETH" txInfo.token === "ETH"
? "ETH" ? nativeCurrency()
: displaySymbol(txInfo.tokenSymbol || "?"); : displaySymbol(txInfo.tokenSymbol || "?");
$("wait-tx-summary").textContent = txInfo.amount + " " + symbol; $("wait-tx-summary").textContent = txInfo.amount + " " + symbol;
$("wait-tx-to").innerHTML = toAddressHtml(txInfo.to); $("wait-tx-to").innerHTML = toAddressHtml(txInfo.to);
@@ -223,7 +224,7 @@ function showSuccess(txInfo, txHash, blockNumber) {
const symbol = const symbol =
txInfo.token === "ETH" txInfo.token === "ETH"
? "ETH" ? nativeCurrency()
: displaySymbol(txInfo.tokenSymbol || "?"); : displaySymbol(txInfo.tokenSymbol || "?");
state.viewData = { state.viewData = {
amount: txInfo.amount, amount: txInfo.amount,
@@ -320,7 +321,7 @@ function showError(txInfo, txHash, message) {
const symbol = const symbol =
txInfo.token === "ETH" txInfo.token === "ETH"
? "ETH" ? nativeCurrency()
: displaySymbol(txInfo.tokenSymbol || "?"); : displaySymbol(txInfo.tokenSymbol || "?");
state.viewData = { state.viewData = {
amount: txInfo.amount, amount: txInfo.amount,
+13 -4
View File
@@ -28,7 +28,7 @@ function normalizeAddress(addr) {
return (addr || "").toLowerCase(); return (addr || "").toLowerCase();
} }
function parseTx(tx, addrLower) { function parseTx(tx, addrLower, nativeCurrency) {
const from = tx.from?.hash || ""; const from = tx.from?.hash || "";
const to = tx.to?.hash || ""; const to = tx.to?.hash || "";
const rawWei = tx.value || "0"; const rawWei = tx.value || "0";
@@ -36,7 +36,7 @@ function parseTx(tx, addrLower) {
const method = tx.method || null; const method = tx.method || null;
// For contract calls, produce a meaningful label instead of "0.0000 ETH" // For contract calls, produce a meaningful label instead of "0.0000 ETH"
let symbol = "ETH"; let symbol = nativeCurrency;
let value = formatTxValue(formatEther(rawWei)); let value = formatTxValue(formatEther(rawWei));
let exactValue = formatEther(rawWei); let exactValue = formatEther(rawWei);
let rawAmount = rawWei; let rawAmount = rawWei;
@@ -221,7 +221,14 @@ function mergeTransactions(txs, tokenTransfers) {
return merged; 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); log.debugf("fetchRecentTransactions", address);
const addrLower = normalizeAddress(address); const addrLower = normalizeAddress(address);
@@ -254,7 +261,9 @@ async function fetchRecentTransactions(address, blockscoutUrl, count = 25) {
const ttJson = ttResp.ok ? await ttResp.json() : {}; const ttJson = ttResp.ok ? await ttResp.json() : {};
const txs = mergeTransactions( 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)), (ttJson.items || []).map((tt) => parseTokenTransfer(tt, addrLower)),
); );
+245
View File
@@ -0,0 +1,245 @@
// 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.
"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() {},
};
}
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 { balanceLinesForAddress } = require("../src/popup/views/helpers");
const HOLDER = "0x" + "a".repeat(40);
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
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(
`<span>${symbol}</span><span>1.5000</span>`,
);
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 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 +
".",
);
});
});
+62 -13
View File
@@ -1219,7 +1219,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
test("queries only the two Blockscout endpoints for the address", async () => { test("queries only the two Blockscout endpoints for the address", async () => {
respondWith([], []); respondWith([], []);
await fetchRecentTransactions(VICTIM, BLOCKSCOUT); await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "ETH");
expect(debugFetch).toHaveBeenCalledTimes(2); expect(debugFetch).toHaveBeenCalledTimes(2);
const urls = debugFetch.mock.calls.map((c) => c[0]); const urls = debugFetch.mock.calls.map((c) => c[0]);
expect(urls).toContain( 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); expect(txs).toHaveLength(1);
const merged = txs[0]; const merged = txs[0];
// The received leg (the swap output) supplies the display amount. // 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).toHaveLength(1);
expect(txs[0].symbol).toBe("USDC"); expect(txs[0].symbol).toBe("USDC");
expect(txs[0].value).toBe("1500.5000"); expect(txs[0].value).toBe("1500.5000");
@@ -1346,7 +1346,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
}); });
respondWith([], [leg("1000000"), leg("2000000")]); respondWith([], [leg("1000000"), leg("2000000")]);
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT); const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "ETH");
expect(txs).toHaveLength(1); expect(txs).toHaveLength(1);
// Keyed by hash plus contract, so the later leg wins. // Keyed by hash plus contract, so the later leg wins.
expect(txs[0].exactValue).toBe("2.0"); 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"]); 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).toHaveLength(1);
expect(txs[0].symbol).toBe("USDC"); expect(txs[0].symbol).toBe("USDC");
expect(txs[0].exactValue).toBe("1.0"); expect(txs[0].exactValue).toBe("1.0");
@@ -1452,10 +1452,43 @@ describe("fetchRecentTransactions merge and dedup", () => {
}); });
respondWith([item(6), item(8), item(7)], []); 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]); 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 () => { test("the fake token transfer survives fetching and is then filtered", async () => {
respondWith( 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).toHaveLength(1);
expect(txs[0].contractAddress).toBe(FAKE_ETH_CONTRACT); expect(txs[0].contractAddress).toBe(FAKE_ETH_CONTRACT);
expect(txs[0].holders).toBe(0); expect(txs[0].holders).toBe(0);
@@ -1515,19 +1548,31 @@ describe("fetchRecentTransactions merge and dedup", () => {
test("an omitted holders_count parses to null", async () => { test("an omitted holders_count parses to null", async () => {
respondWith([], spamTransferWithToken(OMITTED)); respondWith([], spamTransferWithToken(OMITTED));
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT); const txs = await fetchRecentTransactions(
VICTIM,
BLOCKSCOUT,
"ETH",
);
expect(txs[0].holders).toBeNull(); expect(txs[0].holders).toBeNull();
}); });
test("a null holders_count parses to null", async () => { test("a null holders_count parses to null", async () => {
respondWith([], spamTransferWithToken(NULLED)); respondWith([], spamTransferWithToken(NULLED));
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT); const txs = await fetchRecentTransactions(
VICTIM,
BLOCKSCOUT,
"ETH",
);
expect(txs[0].holders).toBeNull(); expect(txs[0].holders).toBeNull();
}); });
test("the transfer survives the low-holder filter", async () => { test("the transfer survives the low-holder filter", async () => {
respondWith([], spamTransferWithToken(OMITTED)); respondWith([], spamTransferWithToken(OMITTED));
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT); const txs = await fetchRecentTransactions(
VICTIM,
BLOCKSCOUT,
"ETH",
);
expect(filterTransactions(txs, filters()).transactions).toEqual( expect(filterTransactions(txs, filters()).transactions).toEqual(
txs, txs,
); );
@@ -1539,7 +1584,11 @@ describe("fetchRecentTransactions merge and dedup", () => {
// holder count is the only rule that can catch it. // 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 () => { test('a reported holders_count of "0" still parses to 0 and is filtered', async () => {
respondWith([], spamTransferWithToken(ZERO)); respondWith([], spamTransferWithToken(ZERO));
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT); const txs = await fetchRecentTransactions(
VICTIM,
BLOCKSCOUT,
"ETH",
);
expect(txs[0].holders).toBe(0); expect(txs[0].holders).toBe(0);
expect(filterTransactions(txs, filters()).transactions).toEqual([]); expect(filterTransactions(txs, filters()).transactions).toEqual([]);
}); });
@@ -1555,7 +1604,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
}, },
})); }));
await expect( await expect(
fetchRecentTransactions(VICTIM, BLOCKSCOUT), fetchRecentTransactions(VICTIM, BLOCKSCOUT, "ETH"),
).resolves.toEqual([]); ).resolves.toEqual([]);
}); });