Compare commits

..
1 Commits
Author SHA1 Message Date
clawbot 9a2910358d chore: the native token's label follows the network (closes #372)
check / check (push) Failing after 3s
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 ETH. The wallet's balances and the Send and
confirmation screens now use the active network's. A transaction's figures use
the network its chain id names, through nativeCurrencyByChainId(): the
approval value and fee, the wait, success and error screens, history entries,
the detail screen and the fee-limit refusal, so a site switching networks
cannot make one read as another network's coin. The "ETH" that selectedToken
and txInfo.token hold is the native token's id and is unchanged. A token
reporting any network's nativeCurrency is a spoof, and the detail screen calls
an entry a token transfer when it has a token contract.

Model: opus-5-5
2026-10-04 22:57:36 +00:00
15 changed files with 290 additions and 92 deletions
+13 -7
View File
@@ -846,13 +846,19 @@ 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.
The native token's label is a network's `nativeCurrency` in
`src/shared/networks.js`: `ETH` on mainnet, `SepoliaETH` on Sepolia. The
wallet's balances and the Send and confirmation screens, which send on the
active network, use the active network's. A transaction's figures use the one of
the network its chain id names, whichever network is active: the value and fee
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
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
ETH transfer" type line, in the contract-recipient warning or in that refusal,
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
+10 -9
View File
@@ -45,17 +45,18 @@ but the review is broader than any of them.
# Completed Steps
- 2026-10-04: The native token's label follows the active network
- 2026-10-04: The native token's label follows the 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.
so on Sepolia the balance, the value and the fee all read `ETH`. Every native
figure now reads `nativeCurrency`, which is `ETH` on mainnet and `SepoliaETH`
on Sepolia: the balance lists, Send and confirmation screens and the
contract-recipient warning the active network's; the approval, wait, success,
error and transaction detail screens, the transaction history and the refusal
of a fee above the limit that of the network the transaction's chain id names.
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
+2 -3
View File
@@ -7,13 +7,12 @@ const {
addressTitle,
escapeHtml,
displaySymbol,
nativeCurrency,
renderAddressHtml,
attachCopyHandlers,
goBack,
pushCurrentView,
} = require("./helpers");
const { state, saveState } = require("../../shared/state");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { formatAddressTotal, getAddressValue } = require("../../shared/prices");
const {
fetchRecentTransactions,
@@ -154,7 +153,7 @@ async function loadTransactions(address) {
const rawTxs = await fetchRecentTransactions(
address,
state.blockscoutUrl,
nativeCurrency(),
currentNetwork().chainId,
);
const result = filterTransactions(rawTxs, {
hideSpoofedSymbols: state.hideSpoofedSymbols,
+2 -2
View File
@@ -18,7 +18,7 @@ const {
goBack,
pushCurrentView,
} = require("./helpers");
const { state, saveState } = require("../../shared/state");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { TOKEN_BY_ADDRESS, resolveSymbol } = require("../../shared/tokenList");
const { formatUsd, getPrice } = require("../../shared/prices");
const {
@@ -227,7 +227,7 @@ async function loadTransactions(address, tokenId) {
const rawTxs = await fetchRecentTransactions(
address,
state.blockscoutUrl,
nativeCurrency(),
currentNetwork().chainId,
);
const result = filterTransactions(rawTxs, {
hideSpoofedSymbols: state.hideSpoofedSymbols,
+16 -5
View File
@@ -10,10 +10,12 @@ const {
attachCopyHandlers,
onViewLeave,
formatFee,
nativeCurrency,
} = require("./helpers");
const { state, saveState } = require("../../shared/state");
const { networkByChainId } = require("../../shared/networks");
const {
networkByChainId,
nativeCurrencyByChainId,
} = require("../../shared/networks");
const {
formatEther,
formatUnits,
@@ -220,8 +222,12 @@ function showTxFee(approvedTx) {
const gasLimit = BigInt(approvedTx.gasLimit);
const feePerGas = BigInt(approvedTx.maxFeePerGas || approvedTx.gasPrice);
// Through formatFee(), as the confirmation screen's fee is, so the same
// fee reads the same on both.
$("approve-tx-fee").textContent = formatFee(gasLimit * feePerGas);
// fee reads the same on both. In the native currency of the network shown
// above, as the value is.
$("approve-tx-fee").textContent = formatFee(
gasLimit * feePerGas,
nativeCurrencyByChainId(approvedTx.chainId),
);
let detail =
gasLimit.toString() +
@@ -265,6 +271,7 @@ function showTxApproval(details) {
amount: formatTxValue(ethValue),
token: "ETH",
tokenSymbol: null,
chainId: approvedTx.chainId,
};
// If this is an ERC-20 call, try to extract the real recipient and amount
@@ -330,10 +337,14 @@ function showTxApproval(details) {
const ethPrice = getPrice("ETH");
const ethUsd = ethPrice ? parseFloat(ethValueFormatted) * ethPrice : null;
const usdStr = formatUsd(ethUsd);
// In the native currency of the network the transaction is for, which the
// Network line names, not the active network's: a site can switch the
// active network after this transaction is prepared and back before it is
// signed.
$("approve-tx-value").textContent =
ethValueFormatted +
" " +
nativeCurrency() +
nativeCurrencyByChainId(approvedTx.chainId) +
(usdStr ? " (" + usdStr + ")" : "");
showTxFee(approvedTx);
+11 -3
View File
@@ -18,7 +18,7 @@ const {
onViewLeave,
formatFee,
} = require("./helpers");
const { state } = require("../../shared/state");
const { state, currentNetwork } = require("../../shared/state");
const { getSignerForAddress } = require("../../shared/wallet");
const { decryptWithPassword } = require("../../shared/vault");
const { formatUsd, getPrice } = require("../../shared/prices");
@@ -398,7 +398,8 @@ async function estimateGas(txInfo) {
// The fee line goes through formatFee(), as the approval screen's
// does, so the same fee reads the same on both.
if (estimateWei !== null && estimateWei < gasCostWei) {
$("confirm-fee-amount").textContent = "~" + formatFee(estimateWei);
$("confirm-fee-amount").textContent =
"~" + formatFee(estimateWei, nativeCurrency());
$("confirm-fee-reserve").textContent =
"up to " +
truncateAmountNeverZero(formatEther(gasCostWei)) +
@@ -410,7 +411,10 @@ async function estimateGas(txInfo) {
// No spread to report: either there is no estimate, or the node
// quotes a gas price at or above maxFeePerGas, so the expected
// cost is not below the reserve. Show the reserve alone.
$("confirm-fee-amount").textContent = formatFee(gasCostWei);
$("confirm-fee-amount").textContent = formatFee(
gasCostWei,
nativeCurrency(),
);
setVisible("confirm-fee-reserve", false);
}
feeStatus = FEE_KNOWN;
@@ -530,6 +534,10 @@ function init(_ctx) {
$("btn-confirm-send").disabled = true;
$("btn-confirm-send").classList.add("text-muted");
// The network it is sent on. The wait, success and error screens
// label its amount by this, not by the network active when they draw.
pendingTx.chainId = currentNetwork().chainId;
let tx;
try {
const signer = getSignerForAddress(
+12 -8
View File
@@ -268,25 +268,29 @@ function unknownableAmount(balance) {
}
// 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.
// on Sepolia, as src/shared/networks.js names it. The wallet's balances and
// the Send and confirmation screens, which send on the active network, label a
// native amount with this; a transaction already made or requested is labelled
// by its own chain id, through nativeCurrencyByChainId() in networks.js. 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:
// 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
// figure.
function formatFee(wei) {
// the ETH figure through truncateAmountNeverZero() and labelled `symbol`, the
// native currency of the network the fee is paid on, then its USD value when
// the ETH price is known. The USD value is of the exact fee, not of the
// truncated figure.
function formatFee(wei, symbol) {
const eth = formatEther(wei);
const ethPrice = getPrice("ETH");
const usd = ethPrice ? formatUsd(parseFloat(eth) * ethPrice) : "";
return (
truncateAmountNeverZero(eth) +
" " +
nativeCurrency() +
symbol +
(usd ? " (" + usd + ")" : "")
);
}
+7 -2
View File
@@ -15,7 +15,12 @@ const {
attachCopyHandlers,
pushCurrentView,
} = require("./helpers");
const { state, saveState, currentAddress } = require("../../shared/state");
const {
state,
saveState,
currentAddress,
currentNetwork,
} = require("../../shared/state");
const { notify } = require("../../shared/browserApi");
const {
updateSendBalance,
@@ -186,7 +191,7 @@ async function loadHomeTxs(ctx) {
fetchRecentTransactions(
addr,
state.blockscoutUrl,
nativeCurrency(),
currentNetwork().chainId,
),
);
const results = await Promise.all(fetches);
+12 -7
View File
@@ -18,10 +18,10 @@ const {
etherscanLinkHtml,
explorerUrl,
displaySymbol,
nativeCurrency,
goBack,
} = require("./helpers");
const { state } = require("../../shared/state");
const { nativeCurrencyByChainId } = require("../../shared/networks");
const { formatEther, formatUnits } = require("ethers");
const makeBlockie = require("ethereum-blockies-base64");
const { log, debugFetch } = require("../../shared/log");
@@ -45,7 +45,7 @@ function getTransactionType(tx) {
// 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";
return "Native " + nativeCurrencyByChainId(tx.chainId) + " Transfer";
}
function blockieHtml(address) {
@@ -87,6 +87,11 @@ function show(tx) {
isContractCall: tx.isContractCall || false,
method: tx.method || null,
contractAddress: tx.contractAddress || null,
// The network the history entry was read from. The type line and
// the fee are in its native currency, not the active network's:
// a site can switch the active network before a later popup
// shows this screen again.
chainId: tx.chainId,
},
};
render();
@@ -182,7 +187,7 @@ function render() {
if (el) el.classList.add("hidden");
}
loadFullTxDetails(tx.hash, tx.to);
loadFullTxDetails(tx.hash, tx.to, tx.chainId);
const isoStr = isoDate(tx.timestamp);
$("tx-detail-time").innerHTML =
@@ -200,7 +205,7 @@ function showDetailField(sectionId, contentId, value) {
section.classList.remove("hidden");
}
function populateOnChainDetails(txData) {
function populateOnChainDetails(txData, chainId) {
// Block number
if (txData.block_number != null) {
const blockLink = explorerUrl("block", String(txData.block_number));
@@ -230,7 +235,7 @@ function populateOnChainDetails(txData) {
showDetailField(
"tx-detail-fee-section",
"tx-detail-fee",
feeEth + " " + nativeCurrency(),
feeEth + " " + nativeCurrencyByChainId(chainId),
);
}
@@ -290,7 +295,7 @@ function populateOnChainDetails(txData) {
}
}
async function loadFullTxDetails(txHash, toAddress) {
async function loadFullTxDetails(txHash, toAddress, chainId) {
const section = $("tx-detail-calldata-section");
const actionEl = $("tx-detail-calldata-action");
const detailsEl = $("tx-detail-calldata-details");
@@ -307,7 +312,7 @@ async function loadFullTxDetails(txHash, toAddress) {
const txData = await resp.json();
// Populate on-chain detail fields (block, nonce, gas, fee)
populateOnChainDetails(txData);
populateOnChainDetails(txData, chainId);
const inputData = txData.raw_input || txData.input || null;
if (!inputData || inputData === "0x") return;
+12 -7
View File
@@ -12,11 +12,11 @@ const {
etherscanLinkHtml,
explorerUrl,
displaySymbol,
nativeCurrency,
clearViewStack,
} = require("./helpers");
const { resolveTokenSymbol } = require("../../shared/approvalAmount");
const { state } = require("../../shared/state");
const { nativeCurrencyByChainId } = require("../../shared/networks");
const { getProvider } = require("../../shared/balances");
const { log } = require("../../shared/log");
@@ -87,9 +87,13 @@ function startWait(txInfo, txHash, broadcastTime, pollNow) {
endWait();
const id = waitId;
// A native amount, here and on the success and error screens, is in the
// native currency of txInfo.chainId, the network the transaction was sent
// on, not the active network's: a site can switch the active network
// while this screen is open or before a later popup resumes it.
const symbol =
txInfo.token === "ETH"
? nativeCurrency()
? nativeCurrencyByChainId(txInfo.chainId)
: displaySymbol(txInfo.tokenSymbol || "?");
$("wait-tx-summary").textContent = txInfo.amount + " " + symbol;
$("wait-tx-to").innerHTML = toAddressHtml(txInfo.to);
@@ -194,9 +198,10 @@ function showWait(txInfo, txHash) {
// an object merely missing one of them throws a TypeError out of
// restoreView() — which init() does not guard, skipping the rest of popup
// init and leaving wait-tx on screen with no back control. A non-numeric
// broadcastTime leaves an unexitable wait counting "NaNs". txInfo.token and
// txInfo.tokenSymbol are deliberately unchecked: they are compared and
// coalesced rather than dereferenced, and tokenSymbol is null for ETH.
// broadcastTime leaves an unexitable wait counting "NaNs". txInfo.token,
// txInfo.tokenSymbol and txInfo.chainId are deliberately unchecked: they are
// compared and coalesced rather than dereferenced, and tokenSymbol is null for
// ETH.
function restoreWait() {
const d = state.viewData;
if (!d || !d.pendingWait) return false;
@@ -224,7 +229,7 @@ function showSuccess(txInfo, txHash, blockNumber) {
const symbol =
txInfo.token === "ETH"
? nativeCurrency()
? nativeCurrencyByChainId(txInfo.chainId)
: displaySymbol(txInfo.tokenSymbol || "?");
state.viewData = {
amount: txInfo.amount,
@@ -321,7 +326,7 @@ function showError(txInfo, txHash, message) {
const symbol =
txInfo.token === "ETH"
? nativeCurrency()
? nativeCurrencyByChainId(txInfo.chainId)
: displaySymbol(txInfo.tokenSymbol || "?");
state.viewData = {
amount: txInfo.amount,
+5 -7
View File
@@ -58,7 +58,7 @@ const {
verifyMessage,
verifyTypedData,
} = require("ethers");
const { networkByChainId } = require("./networks");
const { nativeCurrencyByChainId } = require("./networks");
// The only transaction types this wallet signs: legacy, EIP-2930 and
// EIP-1559. populateTransaction() produces nothing else, so nothing else can
@@ -410,12 +410,10 @@ function assertWithinCeilings(tx) {
}
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";
// transaction is for. Every caller's transaction names it.
const nativeCurrency = nativeCurrencyByChainId(
present(tx.chainId) ? toQuantity(tx.chainId) : null,
);
throw refuse(
"This transaction would allow a network fee of up to " +
formatEther(gasLimit * price) +
+10
View File
@@ -76,6 +76,15 @@ function networkByChainId(chainId) {
return null;
}
// The native currency of the network with this chain id. A transaction's
// value and fee are labelled with the one of the chain the transaction is on,
// which need not be the active network. `ETH` when the chain id is missing or
// no network here has it.
function nativeCurrencyByChainId(chainId) {
const network = networkByChainId(chainId);
return network ? network.nativeCurrency : "ETH";
}
// Build a block explorer link for the given path type and value.
// type: "address" | "tx" | "token" | "block"
function explorerLink(network, type, value) {
@@ -89,5 +98,6 @@ module.exports = {
isKnownNetworkId,
networkById,
networkByChainId,
nativeCurrencyByChainId,
explorerLink,
};
+13 -9
View File
@@ -11,6 +11,7 @@ const { log, debugFetch } = require("./log");
const { TOKEN_BY_ADDRESS } = require("./tokenList");
const { parseHoldersCount, isLowHolderCount } = require("./holders");
const { isSpoofedSymbol } = require("./symbolSpoof");
const { nativeCurrencyByChainId } = require("./networks");
// The uint8 test every scale in this wallet goes through. Shared, not copied:
// a scale is either reported or it is unknown, and "unknown" must mean the
// same thing here as it does on the screens that refuse to format one.
@@ -28,7 +29,7 @@ function normalizeAddress(addr) {
return (addr || "").toLowerCase();
}
function parseTx(tx, addrLower, nativeCurrency) {
function parseTx(tx, addrLower, chainId) {
const from = tx.from?.hash || "";
const to = tx.to?.hash || "";
const rawWei = tx.value || "0";
@@ -36,7 +37,7 @@ function parseTx(tx, addrLower, nativeCurrency) {
const method = tx.method || null;
// For contract calls, produce a meaningful label instead of "0.0000 ETH"
let symbol = nativeCurrency;
let symbol = nativeCurrencyByChainId(chainId);
let value = formatTxValue(formatEther(rawWei));
let exactValue = formatEther(rawWei);
let rawAmount = rawWei;
@@ -90,10 +91,11 @@ function parseTx(tx, addrLower, nativeCurrency) {
holders: null,
isContractCall: toIsContract,
method: method,
chainId: chainId,
};
}
function parseTokenTransfer(tt, addrLower) {
function parseTokenTransfer(tt, addrLower, chainId) {
const from = tt.from?.hash || "";
const to = tt.to?.hash || "";
// The explorer's own answer, or null. Never a default: a transfer of
@@ -139,6 +141,7 @@ function parseTokenTransfer(tt, addrLower) {
// low-holder filter declines to judge a null, so a legitimate token
// is not hidden because a field went missing upstream.
holders: parseHoldersCount(tt.token?.holders_count),
chainId: chainId,
};
}
@@ -221,12 +224,13 @@ function mergeTransactions(txs, tokenTransfers) {
return merged;
}
// `nativeCurrency` is the active network's native token symbol from
// networks.js (`ETH`, `SepoliaETH`), which a native entry is labelled with.
// `chainId` is the chain id of the network `blockscoutUrl` serves. Every entry
// carries it, and a native entry is labelled with that network's
// `nativeCurrency` from networks.js (`ETH`, `SepoliaETH`).
async function fetchRecentTransactions(
address,
blockscoutUrl,
nativeCurrency,
chainId,
count = 25,
) {
log.debugf("fetchRecentTransactions", address);
@@ -261,10 +265,10 @@ async function fetchRecentTransactions(
const ttJson = ttResp.ok ? await ttResp.json() : {};
const txs = mergeTransactions(
(txJson.items || []).map((tx) =>
parseTx(tx, addrLower, nativeCurrency),
(txJson.items || []).map((tx) => parseTx(tx, addrLower, chainId)),
(ttJson.items || []).map((tt) =>
parseTokenTransfer(tt, addrLower, chainId),
),
(ttJson.items || []).map((tt) => parseTokenTransfer(tt, addrLower)),
);
const result = txs.slice(0, count);
+144 -6
View File
@@ -6,7 +6,8 @@
// 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.
// the native token by reporting its label, and that a transaction's figures
// carry its own network's label when another network is active.
"use strict";
@@ -29,6 +30,9 @@ jest.mock("ethers", () => {
async getTransactionCount() {
return 1;
}
async getTransactionReceipt() {
return { blockNumber: 21000000 };
}
}
return {
...actual,
@@ -54,6 +58,21 @@ jest.mock("../src/shared/log", () => ({
isDebug: () => false,
}));
// Signing a send succeeds without a key, and sending answers with a hash.
jest.mock("../src/shared/vault", () => ({
...jest.requireActual("../src/shared/vault"),
decryptWithPassword: async () => "secret",
}));
jest.mock("../src/shared/wallet", () => ({
...jest.requireActual("../src/shared/wallet"),
getSignerForAddress: () => ({
connect: () => ({
populateTransaction: async (request) => request,
sendTransaction: async () => ({ hash: "0x" + "3".repeat(64) }),
}),
}),
}));
global.fetch = jest.fn(() => {
throw new Error("tests must not perform network requests");
});
@@ -137,8 +156,10 @@ 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 txStatus = require("../src/popup/views/txStatus");
const { balanceLinesForAddress } = require("../src/popup/views/helpers");
const { filterTransactions } = require("../src/shared/transactions");
const { debugFetch } = require("../src/shared/log");
const HOLDER = "0x" + "a".repeat(40);
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
@@ -165,8 +186,8 @@ async function confirmSend(amount) {
}
// 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() {
// with 21000 gas at up to 20 gwei, on the network with `chainId`.
async function approveTx(chainId) {
approvalDetails = {
type: "tx",
origin: "https://dapp.example",
@@ -175,7 +196,7 @@ async function approveTx() {
to: RECIPIENT,
value: "10000000000000000",
data: "0x",
chainId: NETWORKS[state.networkId].chainId,
chainId,
gasLimit: "21000",
maxFeePerGas: "20000000000",
nonce: 0,
@@ -221,7 +242,7 @@ describe.each([
await confirmSend("0.1");
expect(text("confirm-type")).toBe("Native " + symbol + " transfer");
expect(text("confirm-amount")).toBe("0.1 " + symbol);
await approveTx();
await approveTx(NETWORKS[networkId].chainId);
expect(text("approve-tx-value")).toBe("0.0100 " + symbol);
});
@@ -235,7 +256,7 @@ describe.each([
expect(text("confirm-gas-error")).toContain(
"You do not have enough " + symbol + " to pay the network fee",
);
await approveTx();
await approveTx(NETWORKS[networkId].chainId);
expect(text("approve-tx-fee")).toBe("0.0004 " + symbol);
});
@@ -301,6 +322,7 @@ describe.each([
isError: false,
direction: "received",
directionLabel: "Received",
chainId: NETWORKS[networkId].chainId,
};
transactionDetail.show({ ...entry, contractAddress: null });
expect(text("tx-detail-type")).toBe("Native " + symbol + " Transfer");
@@ -321,3 +343,119 @@ describe.each([
);
});
});
// A transaction's value and fee are in the native currency of the network the
// transaction is on, which need not be the active one. A site can switch the
// active network after its transaction is prepared and back before it is
// signed, and a popup opened after a switch shows a sent or listed transaction
// again. The wallet's balances follow the active network; these do not.
describe.each([
["mainnet", "sepolia", "ETH"],
["sepolia", "mainnet", "SepoliaETH"],
])(
"a %s transaction shown with %s active reads %s",
(txNetworkId, activeNetworkId, symbol) => {
const chainId = NETWORKS[txNetworkId].chainId;
const hash = "0x" + "3".repeat(64);
beforeEach(() => {
elements.clear();
clearPrices();
state.networkId = activeNetworkId;
state.wallets = [
{
name: "Wallet 1",
addresses: [{ address: HOLDER, balance: "1.5" }],
},
];
state.selectedWallet = 0;
state.selectedAddress = 0;
state.trackedTokens = [];
state.fraudContracts = [];
state.currentView = null;
txStatus.init({ doRefreshAndRender() {} });
});
afterEach(() => {
txStatus.endWait();
});
test("the approval screen's value and fee", async () => {
await approveTx(chainId);
expect(text("approve-tx-network")).toBe(NETWORKS[txNetworkId].name);
expect(text("approve-tx-value")).toBe("0.0100 " + symbol);
expect(text("approve-tx-fee")).toBe("0.0004 " + symbol);
});
test("the wait, success and error screens", async () => {
const txInfo = {
from: HOLDER,
to: RECIPIENT,
amount: "0.0100",
token: "ETH",
tokenSymbol: null,
chainId,
};
txStatus.showWait(txInfo, hash);
expect(text("wait-tx-summary")).toBe("0.0100 " + symbol);
txStatus.showError(txInfo, hash, "Failed.");
expect(text("error-tx-summary")).toBe("0.0100 " + symbol);
// A later popup resumes the wait, and the receipt is there.
state.viewData = {
pendingWait: { txInfo, hash, broadcastTime: Date.now() },
};
txStatus.restoreWait();
for (let i = 0; i < 10; i++) {
await new Promise((r) => setTimeout(r, 0));
}
expect(text("success-tx-summary")).toBe("0.0100 " + symbol);
});
// Sent from the Send screen on the transaction's network, then
// resumed by a popup that opens after the active network changed.
test("the wait screen after a send", async () => {
state.networkId = txNetworkId;
await confirmSend("0.1");
confirmTx.init({});
global.document.getElementById("confirm-tx-password").value = "pw";
await global.document
.getElementById("btn-confirm-send")
.handlers.get("click")();
expect(text("wait-tx-summary")).toBe("0.1 " + symbol);
state.networkId = activeNetworkId;
txStatus.restoreWait();
expect(text("wait-tx-summary")).toBe("0.1 " + symbol);
});
test("the transaction detail screen's type line and fee", async () => {
debugFetch.mockImplementationOnce(async () => ({
ok: true,
status: 200,
json: async () => ({ fee: { value: "21000000000000" } }),
}));
transactionDetail.show({
hash,
from: RECIPIENT,
to: HOLDER,
value: "1.0000",
exactValue: "1.0",
symbol,
timestamp: 1790000000,
isError: false,
direction: "received",
directionLabel: "Received",
contractAddress: null,
chainId,
});
expect(text("tx-detail-type")).toBe(
"Native " + symbol + " Transfer",
);
for (let i = 0; i < 10; i++) {
await new Promise((r) => setTimeout(r, 0));
}
expect(
global.document.getElementById("tx-detail-fee").innerHTML,
).toContain("0.000021 " + symbol);
});
},
);
+21 -17
View File
@@ -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, "ETH");
await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
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, "ETH");
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
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, "ETH");
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
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, "ETH");
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
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, "ETH");
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
expect(txs.map((t) => t.symbol).sort()).toEqual(["USDC", "WETH"]);
});
@@ -1427,12 +1427,13 @@ describe("fetchRecentTransactions merge and dedup", () => {
],
);
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "ETH");
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
expect(txs).toHaveLength(1);
expect(txs[0].symbol).toBe("USDC");
expect(txs[0].exactValue).toBe("1.0");
expect(txs[0].direction).toBe("sent");
expect(txs[0].contractAddress).toBe(USDC_CONTRACT);
expect(txs[0].chainId).toBe("0x1");
// The surviving row is the token row, and the filters keep it.
const kept = filterTransactions(txs, filters()).transactions;
expect(kept).toHaveLength(1);
@@ -1452,13 +1453,14 @@ describe("fetchRecentTransactions merge and dedup", () => {
});
respondWith([item(6), item(8), item(7)], []);
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "ETH", 2);
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1", 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 () => {
// the chain id of the network the explorer serves. Every entry carries
// it, and a native entry is labelled with that network's nativeCurrency.
test("a native entry is labelled by the chain id handed in", async () => {
respondWith(
[
{
@@ -1477,16 +1479,18 @@ describe("fetchRecentTransactions merge and dedup", () => {
const sepolia = await fetchRecentTransactions(
VICTIM,
BLOCKSCOUT,
"SepoliaETH",
"0xaa36a7",
);
expect(sepolia[0].symbol).toBe("SepoliaETH");
expect(sepolia[0].value).toBe("0.0100");
expect(sepolia[0].chainId).toBe("0xaa36a7");
const mainnet = await fetchRecentTransactions(
VICTIM,
BLOCKSCOUT,
"ETH",
"0x1",
);
expect(mainnet[0].symbol).toBe("ETH");
expect(mainnet[0].chainId).toBe("0x1");
});
test("the fake token transfer survives fetching and is then filtered", async () => {
@@ -1509,7 +1513,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
],
);
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "ETH");
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
expect(txs).toHaveLength(1);
expect(txs[0].contractAddress).toBe(FAKE_ETH_CONTRACT);
expect(txs[0].holders).toBe(0);
@@ -1551,7 +1555,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
const txs = await fetchRecentTransactions(
VICTIM,
BLOCKSCOUT,
"ETH",
"0x1",
);
expect(txs[0].holders).toBeNull();
});
@@ -1561,7 +1565,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
const txs = await fetchRecentTransactions(
VICTIM,
BLOCKSCOUT,
"ETH",
"0x1",
);
expect(txs[0].holders).toBeNull();
});
@@ -1571,7 +1575,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
const txs = await fetchRecentTransactions(
VICTIM,
BLOCKSCOUT,
"ETH",
"0x1",
);
expect(filterTransactions(txs, filters()).transactions).toEqual(
txs,
@@ -1587,7 +1591,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
const txs = await fetchRecentTransactions(
VICTIM,
BLOCKSCOUT,
"ETH",
"0x1",
);
expect(txs[0].holders).toBe(0);
expect(filterTransactions(txs, filters()).transactions).toEqual([]);
@@ -1604,7 +1608,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
},
}));
await expect(
fetchRecentTransactions(VICTIM, BLOCKSCOUT, "ETH"),
fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1"),
).resolves.toEqual([]);
});