Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a2910358d |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 + ")" : "")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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) +
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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([]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user