chore: the native token's label follows the active network (closes #372)
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
This commit is contained in:
@@ -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, 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
|
||||
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
|
||||
|
||||
@@ -45,6 +45,13 @@ 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, 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
|
||||
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`
|
||||
|
||||
@@ -690,14 +690,13 @@
|
||||
Your balance does not cover this amount plus the network
|
||||
fee. Please go back and send a smaller amount.
|
||||
</div>
|
||||
<!-- Its sentence names the network's native token, so show()
|
||||
in confirmTx.js sets it. -->
|
||||
<div
|
||||
id="confirm-gas-error"
|
||||
class="mb-2 border border-border border-dashed p-2 text-xs"
|
||||
style="visibility: hidden"
|
||||
>
|
||||
You do not have enough ETH to pay the network fee for this
|
||||
transfer. Please add ETH to this address and try again.
|
||||
</div>
|
||||
></div>
|
||||
<!-- Its sentence names why the fee could not be estimated,
|
||||
so show() in confirmTx.js sets it. -->
|
||||
<div
|
||||
|
||||
@@ -7,6 +7,7 @@ const {
|
||||
addressTitle,
|
||||
escapeHtml,
|
||||
displaySymbol,
|
||||
nativeCurrency,
|
||||
renderAddressHtml,
|
||||
attachCopyHandlers,
|
||||
goBack,
|
||||
@@ -153,6 +154,7 @@ async function loadTransactions(address) {
|
||||
const rawTxs = await fetchRecentTransactions(
|
||||
address,
|
||||
state.blockscoutUrl,
|
||||
nativeCurrency(),
|
||||
);
|
||||
const result = filterTransactions(rawTxs, {
|
||||
hideSpoofedSymbols: state.hideSpoofedSymbols,
|
||||
|
||||
@@ -10,6 +10,7 @@ const {
|
||||
addressTitle,
|
||||
escapeHtml,
|
||||
displaySymbol,
|
||||
nativeCurrency,
|
||||
balanceLine,
|
||||
unknownableAmount,
|
||||
renderAddressHtml,
|
||||
@@ -106,7 +107,7 @@ function show() {
|
||||
let symbol, amount, price;
|
||||
const knownToken = TOKEN_BY_ADDRESS.get(tokenId.toLowerCase());
|
||||
if (tokenId === "ETH") {
|
||||
symbol = "ETH";
|
||||
symbol = nativeCurrency();
|
||||
amount = parseFloat(addr.balance || "0");
|
||||
price = getPrice("ETH");
|
||||
} else {
|
||||
@@ -226,6 +227,7 @@ async function loadTransactions(address, tokenId) {
|
||||
const rawTxs = await fetchRecentTransactions(
|
||||
address,
|
||||
state.blockscoutUrl,
|
||||
nativeCurrency(),
|
||||
);
|
||||
const result = filterTransactions(rawTxs, {
|
||||
hideSpoofedSymbols: state.hideSpoofedSymbols,
|
||||
|
||||
@@ -10,6 +10,7 @@ const {
|
||||
attachCopyHandlers,
|
||||
onViewLeave,
|
||||
formatFee,
|
||||
nativeCurrency,
|
||||
} = require("./helpers");
|
||||
const { state, saveState } = require("../../shared/state");
|
||||
const { networkByChainId } = require("../../shared/networks");
|
||||
@@ -330,7 +331,10 @@ function showTxApproval(details) {
|
||||
const ethUsd = ethPrice ? parseFloat(ethValueFormatted) * ethPrice : null;
|
||||
const usdStr = formatUsd(ethUsd);
|
||||
$("approve-tx-value").textContent =
|
||||
ethValueFormatted + " ETH" + (usdStr ? " (" + usdStr + ")" : "");
|
||||
ethValueFormatted +
|
||||
" " +
|
||||
nativeCurrency() +
|
||||
(usdStr ? " (" + usdStr + ")" : "");
|
||||
|
||||
showTxFee(approvedTx);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ const {
|
||||
addressTitle,
|
||||
escapeHtml,
|
||||
displaySymbol,
|
||||
nativeCurrency,
|
||||
renderAddressHtml,
|
||||
attachCopyHandlers,
|
||||
goBack,
|
||||
@@ -90,7 +91,7 @@ function show(txInfo) {
|
||||
// The raw symbol is the price-table key; the capped one is what the
|
||||
// screen says. Truncating before the lookup would silently drop the
|
||||
// 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);
|
||||
|
||||
// Transaction type
|
||||
@@ -98,7 +99,7 @@ function show(txInfo) {
|
||||
$("confirm-type").textContent =
|
||||
"ERC-20 token transfer (" + symbol + ")";
|
||||
} else {
|
||||
$("confirm-type").textContent = "Native ETH transfer";
|
||||
$("confirm-type").textContent = "Native " + symbol + " transfer";
|
||||
}
|
||||
|
||||
// Token contract section (ERC-20 only)
|
||||
@@ -162,7 +163,7 @@ function show(txInfo) {
|
||||
const bal = txInfo.balance || "0";
|
||||
const balUsd = ethPrice ? parseFloat(bal) * ethPrice : null;
|
||||
$("confirm-balance").textContent = valueWithUsd(
|
||||
truncateAmountNeverZero(bal) + " ETH",
|
||||
truncateAmountNeverZero(bal) + " " + symbol,
|
||||
balUsd,
|
||||
);
|
||||
}
|
||||
@@ -197,6 +198,12 @@ function show(txInfo) {
|
||||
// estimate landing later never moves anything.
|
||||
$("confirm-amount-fee-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.
|
||||
// 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.
|
||||
function renderValidation(txInfo) {
|
||||
const isErc20 = txInfo.token !== "ETH";
|
||||
const symbol = isErc20 ? displaySymbol(txInfo.tokenSymbol || "?") : "ETH";
|
||||
const symbol = isErc20
|
||||
? displaySymbol(txInfo.tokenSymbol || "?")
|
||||
: nativeCurrency();
|
||||
|
||||
const { canSend, codes } = validateTransfer({
|
||||
isErc20,
|
||||
@@ -258,7 +267,7 @@ function renderValidation(txInfo) {
|
||||
|
||||
// Messages carrying the user's own numbers are built here; the fixed
|
||||
// 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 = [];
|
||||
if (codes.includes(CODES.AMOUNT_INVALID)) {
|
||||
messages.push("Please enter a valid amount to send.");
|
||||
@@ -287,9 +296,13 @@ function renderValidation(txInfo) {
|
||||
messages.push(
|
||||
"Insufficient balance. You have " +
|
||||
truncateAmountNeverZero(txInfo.balance || "0") +
|
||||
" ETH but are trying to send " +
|
||||
" " +
|
||||
symbol +
|
||||
" but are trying to send " +
|
||||
txInfo.amount +
|
||||
" ETH.",
|
||||
" " +
|
||||
symbol +
|
||||
".",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -382,7 +395,9 @@ async function estimateGas(txInfo) {
|
||||
$("confirm-fee-reserve").textContent =
|
||||
"up to " +
|
||||
truncateAmountNeverZero(formatEther(gasCostWei)) +
|
||||
" ETH reserved";
|
||||
" " +
|
||||
nativeCurrency() +
|
||||
" reserved";
|
||||
setVisible("confirm-fee-reserve", true);
|
||||
} else {
|
||||
// No spread to report: either there is no estimate, or the node
|
||||
|
||||
@@ -252,6 +252,14 @@ function unknownableAmount(balance) {
|
||||
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:
|
||||
// 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
|
||||
@@ -261,7 +269,10 @@ function formatFee(wei) {
|
||||
const ethPrice = getPrice("ETH");
|
||||
const usd = ethPrice ? formatUsd(parseFloat(eth) * ethPrice) : "";
|
||||
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) {
|
||||
let html = balanceLine(
|
||||
"ETH",
|
||||
nativeCurrency(),
|
||||
parseFloat(addr.balance || "0"),
|
||||
getPrice("ETH"),
|
||||
"ETH",
|
||||
@@ -660,6 +671,7 @@ module.exports = {
|
||||
balanceLinesForAddress,
|
||||
addressHoldsFunds,
|
||||
unknownableAmount,
|
||||
nativeCurrency,
|
||||
formatFee,
|
||||
addressColor,
|
||||
addressDotHtml,
|
||||
|
||||
@@ -10,6 +10,7 @@ const {
|
||||
addressTitle,
|
||||
escapeHtml,
|
||||
displaySymbol,
|
||||
nativeCurrency,
|
||||
renderAddressHtml,
|
||||
attachCopyHandlers,
|
||||
pushCurrentView,
|
||||
@@ -68,7 +69,7 @@ function renderTotalValue() {
|
||||
return;
|
||||
}
|
||||
const ethBal = parseFloat(addr.balance || "0");
|
||||
const ethStr = ethBal.toFixed(4) + " ETH";
|
||||
const ethStr = ethBal.toFixed(4) + " " + nativeCurrency();
|
||||
const ethUsd = ethPrice ? " (" + formatUsd(ethBal * ethPrice) + ")" : "";
|
||||
el.textContent = ethStr + ethUsd;
|
||||
|
||||
@@ -182,7 +183,11 @@ async function loadHomeTxs(ctx) {
|
||||
|
||||
try {
|
||||
const fetches = allAddresses.map((addr) =>
|
||||
fetchRecentTransactions(addr, state.blockscoutUrl),
|
||||
fetchRecentTransactions(
|
||||
addr,
|
||||
state.blockscoutUrl,
|
||||
nativeCurrency(),
|
||||
),
|
||||
);
|
||||
const results = await Promise.all(fetches);
|
||||
|
||||
|
||||
@@ -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 = '<option value="ETH">ETH</option>';
|
||||
sel.innerHTML = `<option value="ETH">${escapeHtml(nativeCurrency())}</option>`;
|
||||
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,
|
||||
|
||||
@@ -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";
|
||||
if (tx.symbol && tx.symbol !== nativeCurrency()) {
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
|
||||
|
||||
@@ -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
@@ -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([]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user