Compare commits
2
Commits
7de9dd26fe
...
c32e44d6c6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c32e44d6c6 | ||
|
|
6fece80afd |
@@ -840,10 +840,12 @@ something when you click it.
|
|||||||
The same data must be formatted identically everywhere it appears. Token and ETH
|
The same data must be formatted identically everywhere it appears. Token and ETH
|
||||||
amounts are displayed with exactly 4 decimal places (e.g. "1.0500 ETH", "17.1900
|
amounts are displayed with exactly 4 decimal places (e.g. "1.0500 ETH", "17.1900
|
||||||
USDT") in balance lists, transaction lists, send confirmations, and approval
|
USDT") in balance lists, transaction lists, send confirmations, and approval
|
||||||
screens. Timestamps include both an ISO datetime and a humanized relative age
|
screens. A transaction's time includes both an ISO datetime and a humanized
|
||||||
wherever shown. If a formatting rule applies in one place, it applies in every
|
relative age, written by `isoDate()` and `timeAgo()` in
|
||||||
place. Users should never see the same value rendered differently on two
|
`src/popup/views/helpers.js` on every screen that shows one; the ISO datetime is
|
||||||
screens.
|
in UTC when the UTC Timestamps setting is on. 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 a network's `nativeCurrency` in
|
The native token's label is a network's `nativeCurrency` in
|
||||||
`src/shared/networks.js`: `ETH` on mainnet, `SepoliaETH` on Sepolia. The
|
`src/shared/networks.js`: `ETH` on mainnet, `SepoliaETH` on Sepolia. The
|
||||||
|
|||||||
@@ -45,6 +45,31 @@ but the review is broader than any of them.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-05: A Chrome end-to-end test that fails no longer takes later tests
|
||||||
|
down with it ([#318](https://git.eeqj.de/sneak/AutistMask/issues/318)). Each
|
||||||
|
test that turns a fixture switch on for itself alone (a held or failing gas
|
||||||
|
estimate, a seeded native transfer or receipt, a token's lying `decimals()` or
|
||||||
|
markup symbol) turns it off again in a `finally`, and the two tests that drive
|
||||||
|
the popup's own send end on the address screen whether they pass or not,
|
||||||
|
reopening the popup to leave a wait for a receipt. The lying-`decimals()` test
|
||||||
|
checks that nothing was broadcast as soon as the send ends, before it waits
|
||||||
|
for the failure screen, so a broadcast fails it in seconds rather than after a
|
||||||
|
60-second wait. The fixture's `decimals()` override tells 0 from no override,
|
||||||
|
so a token with no decimal places can be fixtured.
|
||||||
|
|
||||||
|
- 2026-10-05: Dead code removed and copied view helpers shared
|
||||||
|
([#168](https://git.eeqj.de/sneak/AutistMask/issues/168)). AddressDetail and
|
||||||
|
AddressToken each defined their own `isoDate()` and `timeAgo()` in place of
|
||||||
|
the ones in `src/popup/views/helpers.js`, so a fix to the shared pair would
|
||||||
|
not have reached them. The copies were identical; every screen now uses the
|
||||||
|
shared pair. `blockieHtml()` and `tokenLabel()`, each defined twice, live in
|
||||||
|
`helpers.js` too. Removed as never called: `explorerLink()` (the views build
|
||||||
|
explorer links with `explorerUrl()`), `ETHEREUM_SEPOLIA_CHAIN_ID` (the chain
|
||||||
|
id lives in `src/shared/networks.js`), and `getWalletValue()` and
|
||||||
|
`getTotalValue()`: Home's "Total:" is the active address's total, as
|
||||||
|
`README.md` says. `addressColor()` and `etherscanAddressUrl()` are no longer
|
||||||
|
exported. Nothing the user sees changed.
|
||||||
|
|
||||||
- 2026-10-05: A prompt raised while another approval window has focus opens a
|
- 2026-10-05: A prompt raised while another approval window has focus opens a
|
||||||
window of its own ([#290](https://git.eeqj.de/sneak/AutistMask/issues/290)).
|
window of its own ([#290](https://git.eeqj.de/sneak/AutistMask/issues/290)).
|
||||||
The background centred each approval window on the last focused window, which
|
The background centred each approval window on the last focused window, which
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ const {
|
|||||||
attachCopyHandlers,
|
attachCopyHandlers,
|
||||||
goBack,
|
goBack,
|
||||||
pushCurrentView,
|
pushCurrentView,
|
||||||
|
isoDate,
|
||||||
|
timeAgo,
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||||||
const { formatAddressTotal, getAddressValue } = require("../../shared/prices");
|
const { formatAddressTotal, getAddressValue } = require("../../shared/prices");
|
||||||
@@ -88,62 +90,6 @@ function show() {
|
|||||||
loadTransactions(addr.address);
|
loadTransactions(addr.address);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isoDate(timestamp) {
|
|
||||||
const d = new Date(timestamp * 1000);
|
|
||||||
const pad = (n) => String(n).padStart(2, "0");
|
|
||||||
if (state.utcTimestamps) {
|
|
||||||
return (
|
|
||||||
d.getUTCFullYear() +
|
|
||||||
"-" +
|
|
||||||
pad(d.getUTCMonth() + 1) +
|
|
||||||
"-" +
|
|
||||||
pad(d.getUTCDate()) +
|
|
||||||
"T" +
|
|
||||||
pad(d.getUTCHours()) +
|
|
||||||
":" +
|
|
||||||
pad(d.getUTCMinutes()) +
|
|
||||||
":" +
|
|
||||||
pad(d.getUTCSeconds()) +
|
|
||||||
"Z"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const offsetMin = -d.getTimezoneOffset();
|
|
||||||
const sign = offsetMin >= 0 ? "+" : "-";
|
|
||||||
const absOff = Math.abs(offsetMin);
|
|
||||||
const tzStr = sign + pad(Math.floor(absOff / 60)) + ":" + pad(absOff % 60);
|
|
||||||
return (
|
|
||||||
d.getFullYear() +
|
|
||||||
"-" +
|
|
||||||
pad(d.getMonth() + 1) +
|
|
||||||
"-" +
|
|
||||||
pad(d.getDate()) +
|
|
||||||
"T" +
|
|
||||||
pad(d.getHours()) +
|
|
||||||
":" +
|
|
||||||
pad(d.getMinutes()) +
|
|
||||||
":" +
|
|
||||||
pad(d.getSeconds()) +
|
|
||||||
tzStr
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function timeAgo(timestamp) {
|
|
||||||
const seconds = Math.floor(Date.now() / 1000 - timestamp);
|
|
||||||
if (seconds < 60) return seconds + " seconds ago";
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
if (minutes < 60)
|
|
||||||
return minutes + " minute" + (minutes !== 1 ? "s" : "") + " ago";
|
|
||||||
const hours = Math.floor(minutes / 60);
|
|
||||||
if (hours < 24) return hours + " hour" + (hours !== 1 ? "s" : "") + " ago";
|
|
||||||
const days = Math.floor(hours / 24);
|
|
||||||
if (days < 30) return days + " day" + (days !== 1 ? "s" : "") + " ago";
|
|
||||||
const months = Math.floor(days / 30);
|
|
||||||
if (months < 12)
|
|
||||||
return months + " month" + (months !== 1 ? "s" : "") + " ago";
|
|
||||||
const years = Math.floor(days / 365);
|
|
||||||
return years + " year" + (years !== 1 ? "s" : "") + " ago";
|
|
||||||
}
|
|
||||||
|
|
||||||
let loadedTxs = [];
|
let loadedTxs = [];
|
||||||
|
|
||||||
let ensNameMap = new Map();
|
let ensNameMap = new Map();
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const {
|
|||||||
attachCopyHandlers,
|
attachCopyHandlers,
|
||||||
goBack,
|
goBack,
|
||||||
pushCurrentView,
|
pushCurrentView,
|
||||||
|
isoDate,
|
||||||
|
timeAgo,
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||||||
const { TOKEN_BY_ADDRESS, resolveSymbol } = require("../../shared/tokenList");
|
const { TOKEN_BY_ADDRESS, resolveSymbol } = require("../../shared/tokenList");
|
||||||
@@ -37,62 +39,6 @@ const { walletDefect } = require("../../shared/walletDefects");
|
|||||||
|
|
||||||
let ctx;
|
let ctx;
|
||||||
|
|
||||||
function isoDate(timestamp) {
|
|
||||||
const d = new Date(timestamp * 1000);
|
|
||||||
const pad = (n) => String(n).padStart(2, "0");
|
|
||||||
if (state.utcTimestamps) {
|
|
||||||
return (
|
|
||||||
d.getUTCFullYear() +
|
|
||||||
"-" +
|
|
||||||
pad(d.getUTCMonth() + 1) +
|
|
||||||
"-" +
|
|
||||||
pad(d.getUTCDate()) +
|
|
||||||
"T" +
|
|
||||||
pad(d.getUTCHours()) +
|
|
||||||
":" +
|
|
||||||
pad(d.getUTCMinutes()) +
|
|
||||||
":" +
|
|
||||||
pad(d.getUTCSeconds()) +
|
|
||||||
"Z"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const offsetMin = -d.getTimezoneOffset();
|
|
||||||
const sign = offsetMin >= 0 ? "+" : "-";
|
|
||||||
const absOff = Math.abs(offsetMin);
|
|
||||||
const tzStr = sign + pad(Math.floor(absOff / 60)) + ":" + pad(absOff % 60);
|
|
||||||
return (
|
|
||||||
d.getFullYear() +
|
|
||||||
"-" +
|
|
||||||
pad(d.getMonth() + 1) +
|
|
||||||
"-" +
|
|
||||||
pad(d.getDate()) +
|
|
||||||
"T" +
|
|
||||||
pad(d.getHours()) +
|
|
||||||
":" +
|
|
||||||
pad(d.getMinutes()) +
|
|
||||||
":" +
|
|
||||||
pad(d.getSeconds()) +
|
|
||||||
tzStr
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function timeAgo(timestamp) {
|
|
||||||
const seconds = Math.floor(Date.now() / 1000 - timestamp);
|
|
||||||
if (seconds < 60) return seconds + " seconds ago";
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
if (minutes < 60)
|
|
||||||
return minutes + " minute" + (minutes !== 1 ? "s" : "") + " ago";
|
|
||||||
const hours = Math.floor(minutes / 60);
|
|
||||||
if (hours < 24) return hours + " hour" + (hours !== 1 ? "s" : "") + " ago";
|
|
||||||
const days = Math.floor(hours / 24);
|
|
||||||
if (days < 30) return days + " day" + (days !== 1 ? "s" : "") + " ago";
|
|
||||||
const months = Math.floor(days / 30);
|
|
||||||
if (months < 12)
|
|
||||||
return months + " month" + (months !== 1 ? "s" : "") + " ago";
|
|
||||||
const years = Math.floor(days / 365);
|
|
||||||
return years + " year" + (years !== 1 ? "s" : "") + " ago";
|
|
||||||
}
|
|
||||||
|
|
||||||
let loadedTxs = [];
|
let loadedTxs = [];
|
||||||
let ensNameMap = new Map();
|
let ensNameMap = new Map();
|
||||||
let currentSymbol = null;
|
let currentSymbol = null;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const {
|
|||||||
attachCopyHandlers,
|
attachCopyHandlers,
|
||||||
onViewLeave,
|
onViewLeave,
|
||||||
formatFee,
|
formatFee,
|
||||||
|
tokenLabel,
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { state, saveState } = require("../../shared/state");
|
const { state, saveState } = require("../../shared/state");
|
||||||
const {
|
const {
|
||||||
@@ -73,17 +74,6 @@ function tokenAmountText(rawAmount, decimals, symbol) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// The symbol shown for a token line, resolved from the bundled list, the
|
|
||||||
// tokens the user tracks, and the explorer's report — the same chain the
|
|
||||||
// amount line's scale comes from. Null when no source names one, so the token
|
|
||||||
// lines keep saying `Unknown token` for a token nothing knows.
|
|
||||||
function tokenLabel(address) {
|
|
||||||
return resolveTokenSymbol(address, {
|
|
||||||
trackedTokens: state.trackedTokens,
|
|
||||||
wallets: state.wallets,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to decode calldata using known ABIs.
|
// Try to decode calldata using known ABIs.
|
||||||
// Returns { name, description, details } or null.
|
// Returns { name, description, details } or null.
|
||||||
function decodeCalldata(data, toAddress) {
|
function decodeCalldata(data, toAddress) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const {
|
|||||||
displaySymbol,
|
displaySymbol,
|
||||||
nativeCurrency,
|
nativeCurrency,
|
||||||
renderAddressHtml,
|
renderAddressHtml,
|
||||||
|
blockieHtml,
|
||||||
attachCopyHandlers,
|
attachCopyHandlers,
|
||||||
goBack,
|
goBack,
|
||||||
onViewLeave,
|
onViewLeave,
|
||||||
@@ -47,7 +48,6 @@ const {
|
|||||||
validateTransfer,
|
validateTransfer,
|
||||||
} = require("../../shared/txValidation");
|
} = require("../../shared/txValidation");
|
||||||
const { log } = require("../../shared/log");
|
const { log } = require("../../shared/log");
|
||||||
const makeBlockie = require("ethereum-blockies-base64");
|
|
||||||
const txStatus = require("./txStatus");
|
const txStatus = require("./txStatus");
|
||||||
|
|
||||||
let pendingTx = null;
|
let pendingTx = null;
|
||||||
@@ -67,11 +67,6 @@ function restore() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function blockieHtml(address) {
|
|
||||||
const src = makeBlockie(address);
|
|
||||||
return `<img src="${escapeHtml(src)}" width="48" height="48" style="image-rendering:pixelated;border-radius:50%;display:inline-block">`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmAddressHtml(address, ensName, title) {
|
function confirmAddressHtml(address, ensName, title) {
|
||||||
const blockie = blockieHtml(address);
|
const blockie = blockieHtml(address);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -13,10 +13,12 @@
|
|||||||
// reasoning behind it are; it is re-exported below so views keep importing
|
// reasoning behind it are; it is re-exported below so views keep importing
|
||||||
// it from here.
|
// it from here.
|
||||||
const { formatEther } = require("ethers");
|
const { formatEther } = require("ethers");
|
||||||
|
const makeBlockie = require("ethereum-blockies-base64");
|
||||||
const {
|
const {
|
||||||
truncateAmountNeverZero,
|
truncateAmountNeverZero,
|
||||||
isBelowOneMillionth,
|
isBelowOneMillionth,
|
||||||
} = require("../../shared/amountDisplay");
|
} = require("../../shared/amountDisplay");
|
||||||
|
const { resolveTokenSymbol } = require("../../shared/approvalAmount");
|
||||||
const { DEBUG } = require("../../shared/constants");
|
const { DEBUG } = require("../../shared/constants");
|
||||||
const { escapeHtml } = require("../../shared/html");
|
const { escapeHtml } = require("../../shared/html");
|
||||||
const { isDebug } = require("../../shared/log");
|
const { isDebug } = require("../../shared/log");
|
||||||
@@ -278,6 +280,17 @@ function nativeCurrency() {
|
|||||||
return currentNetwork().nativeCurrency;
|
return currentNetwork().nativeCurrency;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The symbol shown for a token line, resolved from the bundled list, the
|
||||||
|
// tokens the user tracks, and the explorer's report — the same chain the
|
||||||
|
// amount line's scale comes from. Null when no source names one, so the token
|
||||||
|
// lines keep saying `Unknown token` for a token nothing knows.
|
||||||
|
function tokenLabel(address) {
|
||||||
|
return resolveTokenSymbol(address, {
|
||||||
|
trackedTokens: state.trackedTokens,
|
||||||
|
wallets: state.wallets,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// A network fee in wei as the confirmation and approval screens both show it:
|
// A network fee in wei as the confirmation and approval screens both show it:
|
||||||
// the ETH figure through truncateAmountNeverZero() and labelled `symbol`, the
|
// the ETH figure through truncateAmountNeverZero() and labelled `symbol`, the
|
||||||
// native currency of the network the fee is paid on, then its USD value when
|
// native currency of the network the fee is paid on, then its USD value when
|
||||||
@@ -446,6 +459,11 @@ function addressDotHtml(address) {
|
|||||||
return `<span style="width:8px;height:8px;border-radius:50%;display:inline-block;background:${color};margin-right:4px;vertical-align:middle;flex-shrink:0;"></span>`;
|
return `<span style="width:8px;height:8px;border-radius:50%;display:inline-block;background:${color};margin-right:4px;vertical-align:middle;flex-shrink:0;"></span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function blockieHtml(address) {
|
||||||
|
const src = makeBlockie(address);
|
||||||
|
return `<img src="${escapeHtml(src)}" width="48" height="48" style="image-rendering:pixelated;border-radius:50%;display:inline-block">`;
|
||||||
|
}
|
||||||
|
|
||||||
// Look up an address across all wallets and return its title
|
// Look up an address across all wallets and return its title
|
||||||
// (e.g. "Address 1.2") or null if it's not one of ours.
|
// (e.g. "Address 1.2") or null if it's not one of ours.
|
||||||
function addressTitle(address, wallets) {
|
function addressTitle(address, wallets) {
|
||||||
@@ -492,6 +510,9 @@ function formatAddressHtml(address, ensName, maxLen, title) {
|
|||||||
return renderAddressHtml(address, { title, ensName, maxLen });
|
return renderAddressHtml(address, { title, ensName, maxLen });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A transaction's time as every screen shows it (README, Display
|
||||||
|
// Consistency): the ISO datetime, in UTC when the UTC Timestamps setting is
|
||||||
|
// on, and the relative age. Views import these two; they keep no copies.
|
||||||
function isoDate(timestamp) {
|
function isoDate(timestamp) {
|
||||||
const d = new Date(timestamp * 1000);
|
const d = new Date(timestamp * 1000);
|
||||||
const pad = (n) => String(n).padStart(2, "0");
|
const pad = (n) => String(n).padStart(2, "0");
|
||||||
@@ -691,9 +712,10 @@ module.exports = {
|
|||||||
addressHoldsFunds,
|
addressHoldsFunds,
|
||||||
unknownableAmount,
|
unknownableAmount,
|
||||||
nativeCurrency,
|
nativeCurrency,
|
||||||
|
tokenLabel,
|
||||||
formatFee,
|
formatFee,
|
||||||
addressColor,
|
|
||||||
addressDotHtml,
|
addressDotHtml,
|
||||||
|
blockieHtml,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
displaySymbol,
|
displaySymbol,
|
||||||
addressTitle,
|
addressTitle,
|
||||||
@@ -703,7 +725,6 @@ module.exports = {
|
|||||||
renderAddressHtml,
|
renderAddressHtml,
|
||||||
copyableHtml,
|
copyableHtml,
|
||||||
attachCopyHandlers,
|
attachCopyHandlers,
|
||||||
etherscanAddressUrl,
|
|
||||||
etherscanLinkHtml,
|
etherscanLinkHtml,
|
||||||
explorerUrl,
|
explorerUrl,
|
||||||
EXT_ICON,
|
EXT_ICON,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function isTracked(address) {
|
|||||||
return state.trackedTokens.some((t) => t.address.toLowerCase() === lower);
|
return state.trackedTokens.some((t) => t.address.toLowerCase() === lower);
|
||||||
}
|
}
|
||||||
|
|
||||||
function tokenLabel(t) {
|
function nameAndSymbol(t) {
|
||||||
return t.name ? t.name + " (" + t.symbol + ")" : t.symbol;
|
return t.name ? t.name + " (" + t.symbol + ")" : t.symbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ function renderDropdown() {
|
|||||||
let html = '<option value="">-- select --</option>';
|
let html = '<option value="">-- select --</option>';
|
||||||
for (const t of tokens) {
|
for (const t of tokens) {
|
||||||
const tracked = isTracked(t.address);
|
const tracked = isTracked(t.address);
|
||||||
const label = tokenLabel(t) + (tracked ? " (tracked)" : "");
|
const label = nameAndSymbol(t) + (tracked ? " (tracked)" : "");
|
||||||
html +=
|
html +=
|
||||||
`<option value="${escapeHtml(t.address)}"` +
|
`<option value="${escapeHtml(t.address)}"` +
|
||||||
` data-symbol="${escapeHtml(t.symbol)}"` +
|
` data-symbol="${escapeHtml(t.symbol)}"` +
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const {
|
|||||||
isoDate,
|
isoDate,
|
||||||
timeAgo,
|
timeAgo,
|
||||||
renderAddressHtml,
|
renderAddressHtml,
|
||||||
|
blockieHtml,
|
||||||
attachCopyHandlers,
|
attachCopyHandlers,
|
||||||
copyableHtml,
|
copyableHtml,
|
||||||
etherscanLinkHtml,
|
etherscanLinkHtml,
|
||||||
@@ -23,7 +24,6 @@ const {
|
|||||||
const { state } = require("../../shared/state");
|
const { state } = require("../../shared/state");
|
||||||
const { nativeCurrencyByChainId } = require("../../shared/networks");
|
const { nativeCurrencyByChainId } = require("../../shared/networks");
|
||||||
const { formatEther, formatUnits } = require("ethers");
|
const { formatEther, formatUnits } = require("ethers");
|
||||||
const makeBlockie = require("ethereum-blockies-base64");
|
|
||||||
const { log, debugFetch } = require("../../shared/log");
|
const { log, debugFetch } = require("../../shared/log");
|
||||||
const { decodeCalldata } = require("./approval");
|
const { decodeCalldata } = require("./approval");
|
||||||
|
|
||||||
@@ -48,11 +48,6 @@ function getTransactionType(tx) {
|
|||||||
return "Native " + nativeCurrencyByChainId(tx.chainId) + " Transfer";
|
return "Native " + nativeCurrencyByChainId(tx.chainId) + " Transfer";
|
||||||
}
|
}
|
||||||
|
|
||||||
function blockieHtml(address) {
|
|
||||||
const src = makeBlockie(address);
|
|
||||||
return `<img src="${escapeHtml(src)}" width="48" height="48" style="image-rendering:pixelated;border-radius:50%;display:inline-block">`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function txAddressHtml(address, ensName, title) {
|
function txAddressHtml(address, ensName, title) {
|
||||||
const blockie = blockieHtml(address);
|
const blockie = blockieHtml(address);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ const {
|
|||||||
explorerUrl,
|
explorerUrl,
|
||||||
displaySymbol,
|
displaySymbol,
|
||||||
clearViewStack,
|
clearViewStack,
|
||||||
|
tokenLabel,
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { resolveTokenSymbol } = require("../../shared/approvalAmount");
|
|
||||||
const { state } = require("../../shared/state");
|
const { state } = require("../../shared/state");
|
||||||
const { nativeCurrencyByChainId } = require("../../shared/networks");
|
const { nativeCurrencyByChainId } = require("../../shared/networks");
|
||||||
const { getProvider } = require("../../shared/balances");
|
const { getProvider } = require("../../shared/balances");
|
||||||
@@ -243,17 +243,6 @@ function showSuccess(txInfo, txHash, blockNumber) {
|
|||||||
ctx.doRefreshAndRender();
|
ctx.doRefreshAndRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The symbol shown for a decoded token line, resolved from the bundled list,
|
|
||||||
// the tokens the user tracks, and the explorer's report — the same chain the
|
|
||||||
// approval screen uses. Null when no source names one, so the line keeps
|
|
||||||
// saying `Unknown token`.
|
|
||||||
function tokenLabel(address) {
|
|
||||||
return resolveTokenSymbol(address, {
|
|
||||||
trackedTokens: state.trackedTokens,
|
|
||||||
wallets: state.wallets,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodedDetailsHtml(decoded) {
|
function decodedDetailsHtml(decoded) {
|
||||||
if (!decoded || !decoded.details) return "";
|
if (!decoded || !decoded.details) return "";
|
||||||
let html = `<div class="border border-border border-dashed p-2 mb-3">`;
|
let html = `<div class="border border-border border-dashed p-2 mb-3">`;
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ const DEBUG_MNEMONIC = DEBUG
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
const ETHEREUM_MAINNET_CHAIN_ID = "0x1";
|
const ETHEREUM_MAINNET_CHAIN_ID = "0x1";
|
||||||
const ETHEREUM_SEPOLIA_CHAIN_ID = "0xaa36a7";
|
|
||||||
|
|
||||||
const DEFAULT_RPC_URL = "https://ethereum-rpc.publicnode.com";
|
const DEFAULT_RPC_URL = "https://ethereum-rpc.publicnode.com";
|
||||||
|
|
||||||
@@ -69,7 +68,6 @@ module.exports = {
|
|||||||
BUILD_DEBUG_MARKER,
|
BUILD_DEBUG_MARKER,
|
||||||
DEBUG_MNEMONIC,
|
DEBUG_MNEMONIC,
|
||||||
ETHEREUM_MAINNET_CHAIN_ID,
|
ETHEREUM_MAINNET_CHAIN_ID,
|
||||||
ETHEREUM_SEPOLIA_CHAIN_ID,
|
|
||||||
DEFAULT_RPC_URL,
|
DEFAULT_RPC_URL,
|
||||||
DEFAULT_BLOCKSCOUT_URL,
|
DEFAULT_BLOCKSCOUT_URL,
|
||||||
BIP44_ETH_PATH,
|
BIP44_ETH_PATH,
|
||||||
|
|||||||
@@ -85,12 +85,6 @@ function nativeCurrencyByChainId(chainId) {
|
|||||||
return network ? network.nativeCurrency : "ETH";
|
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) {
|
|
||||||
return `${network.explorerUrl}/${type}/${value}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
NETWORKS,
|
NETWORKS,
|
||||||
SUPPORTED_CHAIN_IDS,
|
SUPPORTED_CHAIN_IDS,
|
||||||
@@ -99,5 +93,4 @@ module.exports = {
|
|||||||
networkById,
|
networkById,
|
||||||
networkByChainId,
|
networkByChainId,
|
||||||
nativeCurrencyByChainId,
|
nativeCurrencyByChainId,
|
||||||
explorerLink,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -104,27 +104,6 @@ function getAddressValue(addr) {
|
|||||||
return { usd, partial };
|
return { usd, partial };
|
||||||
}
|
}
|
||||||
|
|
||||||
// The same pair for a whole wallet, and for every wallet at once. One
|
|
||||||
// unpriced holding anywhere makes the sum a floor, so partial carries up.
|
|
||||||
function getWalletValue(wallet) {
|
|
||||||
return sumValues(wallet.addresses.map(getAddressValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTotalValue(wallets) {
|
|
||||||
return sumValues(wallets.map(getWalletValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
function sumValues(values) {
|
|
||||||
let usd = null;
|
|
||||||
let partial = false;
|
|
||||||
for (const value of values) {
|
|
||||||
if (value.usd === null) continue;
|
|
||||||
usd = (usd === null ? 0 : usd) + value.usd;
|
|
||||||
partial = partial || value.partial;
|
|
||||||
}
|
|
||||||
return { usd, partial };
|
|
||||||
}
|
|
||||||
|
|
||||||
// The one rendering of an address total, so no screen says it differently.
|
// The one rendering of an address total, so no screen says it differently.
|
||||||
//
|
//
|
||||||
// A partial total is shown and named as partial: the figure is the ETH and
|
// A partial total is shown and named as partial: the figure is the ETH and
|
||||||
@@ -149,6 +128,4 @@ module.exports = {
|
|||||||
formatUsd,
|
formatUsd,
|
||||||
formatAddressTotal,
|
formatAddressTotal,
|
||||||
getAddressValue,
|
getAddressValue,
|
||||||
getWalletValue,
|
|
||||||
getTotalValue,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ const {
|
|||||||
prices,
|
prices,
|
||||||
clearPrices,
|
clearPrices,
|
||||||
getAddressValue,
|
getAddressValue,
|
||||||
getWalletValue,
|
|
||||||
getTotalValue,
|
|
||||||
formatAddressTotal,
|
formatAddressTotal,
|
||||||
} = require("../src/shared/prices");
|
} = require("../src/shared/prices");
|
||||||
const { state } = require("../src/shared/state");
|
const { state } = require("../src/shared/state");
|
||||||
@@ -136,17 +134,6 @@ describe("the value of an address, and whether it is the whole value", () => {
|
|||||||
partial: false,
|
partial: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("one unpriced holding makes a wallet and the grand total partial", () => {
|
|
||||||
const wallet = { addresses: [FULLY_PRICED, UNPRICED_ONLY] };
|
|
||||||
expect(getWalletValue(wallet)).toEqual({ usd: 5500, partial: true });
|
|
||||||
expect(getTotalValue([wallet])).toEqual({ usd: 5500, partial: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a wallet of fully priced addresses stays complete", () => {
|
|
||||||
const wallet = { addresses: [FULLY_PRICED, EMPTY] };
|
|
||||||
expect(getWalletValue(wallet)).toEqual({ usd: 5500, partial: false });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("how that value is written on screen", () => {
|
describe("how that value is written on screen", () => {
|
||||||
|
|||||||
@@ -256,14 +256,15 @@ const SELECTOR_DECIMALS = "0x313ce567";
|
|||||||
// Blockscout, which is exactly the disagreement the wallet must refuse to
|
// Blockscout, which is exactly the disagreement the wallet must refuse to
|
||||||
// sign over. It is read at request time, so a test flips it on the options
|
// sign over. It is read at request time, so a test flips it on the options
|
||||||
// object the route was registered with — after the confirmation screen has
|
// object the route was registered with — after the confirmation screen has
|
||||||
// been built — without re-registering anything.
|
// been built — without re-registering anything. Only null or undefined means
|
||||||
|
// no override: 0 is a token with no decimal places, and is answered as one.
|
||||||
function ethCallResult(req, opts) {
|
function ethCallResult(req, opts) {
|
||||||
const call = Array.isArray(req.params) ? req.params[0] : null;
|
const call = Array.isArray(req.params) ? req.params[0] : null;
|
||||||
if (!call || typeof call !== "object") return ZERO_WORD;
|
if (!call || typeof call !== "object") return ZERO_WORD;
|
||||||
const data = String(call.data || call.input || "").toLowerCase();
|
const data = String(call.data || call.input || "").toLowerCase();
|
||||||
const to = String(call.to || "").toLowerCase();
|
const to = String(call.to || "").toLowerCase();
|
||||||
if (data.startsWith(SELECTOR_DECIMALS) && to === STUB_TOKEN.address) {
|
if (data.startsWith(SELECTOR_DECIMALS) && to === STUB_TOKEN.address) {
|
||||||
return word(opts.tokenDecimalsOverride || STUB_TOKEN.decimals);
|
return word(opts.tokenDecimalsOverride ?? STUB_TOKEN.decimals);
|
||||||
}
|
}
|
||||||
return ZERO_WORD;
|
return ZERO_WORD;
|
||||||
}
|
}
|
||||||
@@ -597,9 +598,10 @@ function traceEnabled(raw) {
|
|||||||
* eth_estimateGas until this is cleared again.
|
* eth_estimateGas until this is cleared again.
|
||||||
* @param {string[]} [opts.broadcastTransactions] every raw signed
|
* @param {string[]} [opts.broadcastTransactions] every raw signed
|
||||||
* transaction handed to eth_sendRawTransaction, appended in order.
|
* transaction handed to eth_sendRawTransaction, appended in order.
|
||||||
* @param {string} [opts.tokenDecimalsOverride] what decimals() answers for
|
* @param {number|string|null} [opts.tokenDecimalsOverride] the scale
|
||||||
* the stub token, in place of the value Blockscout reports for it. This is
|
* decimals() answers for the stub token, in place of the value Blockscout
|
||||||
* the token that lies about its scale; read at request time.
|
* reports for it; null for none, while 0 is a scale like any other. This
|
||||||
|
* is the token that lies about its scale; read at request time.
|
||||||
* @param {string} [opts.tokenSymbolOverride] what the explorer reports as
|
* @param {string} [opts.tokenSymbolOverride] what the explorer reports as
|
||||||
* the stub token's symbol, in place of "E2E". This is the token whose
|
* the stub token's symbol, in place of "E2E". This is the token whose
|
||||||
* symbol is markup; read at request time.
|
* symbol is markup; read at request time.
|
||||||
|
|||||||
+100
-33
@@ -352,6 +352,7 @@ test("the native ETH transaction detail still renders (#151)", async (env) => {
|
|||||||
// the assertions below are as much about that row staying hidden as
|
// the assertions below are as much about that row staying hidden as
|
||||||
// about the screen coming up.
|
// about the screen coming up.
|
||||||
env.routeOpts.seedNativeTransfer = true;
|
env.routeOpts.seedNativeTransfer = true;
|
||||||
|
try {
|
||||||
await env.page.reload();
|
await env.page.reload();
|
||||||
await openAddressDetail(env.page);
|
await openAddressDetail(env.page);
|
||||||
|
|
||||||
@@ -368,7 +369,9 @@ test("the native ETH transaction detail still renders (#151)", async (env) => {
|
|||||||
"the native transaction detail shows the wrong hash: " + hash,
|
"the native transaction detail shows the wrong hash: " + hash,
|
||||||
);
|
);
|
||||||
|
|
||||||
const type = (await env.page.locator("#tx-detail-type").innerText()).trim();
|
const type = (
|
||||||
|
await env.page.locator("#tx-detail-type").innerText()
|
||||||
|
).trim();
|
||||||
assert(
|
assert(
|
||||||
type === "Native ETH Transfer",
|
type === "Native ETH Transfer",
|
||||||
"the native transaction was classified " + JSON.stringify(type),
|
"the native transaction was classified " + JSON.stringify(type),
|
||||||
@@ -393,13 +396,15 @@ test("the native ETH transaction detail still renders (#151)", async (env) => {
|
|||||||
|
|
||||||
assert(
|
assert(
|
||||||
!(await env.page.isVisible("#tx-detail-token-contract-section")),
|
!(await env.page.isVisible("#tx-detail-token-contract-section")),
|
||||||
"the token contract row is showing on a transfer that has no token " +
|
"the token contract row is showing on a transfer that has no " +
|
||||||
"contract",
|
"token contract",
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
// Back to one seeded transaction for everything after this: the tests
|
// Back to one seeded transaction for everything after this: the
|
||||||
// below were written against a list holding the token transfer alone.
|
// tests below were written against a list holding the token transfer
|
||||||
|
// alone.
|
||||||
env.routeOpts.seedNativeTransfer = false;
|
env.routeOpts.seedNativeTransfer = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("tap-to-copy on the transaction detail screen copies the address (#151)", async (env) => {
|
test("tap-to-copy on the transaction detail screen copies the address (#151)", async (env) => {
|
||||||
@@ -1771,6 +1776,7 @@ test("ConfirmTx blocks sending while the fee estimate is pending (#238)", async
|
|||||||
expectToken: true,
|
expectToken: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Released by the next test, which watches the estimate land.
|
||||||
env.routeOpts.holdGasEstimate = true;
|
env.routeOpts.holdGasEstimate = true;
|
||||||
await goToConfirm(env.page, {
|
await goToConfirm(env.page, {
|
||||||
token: "ETH",
|
token: "ETH",
|
||||||
@@ -1933,6 +1939,7 @@ test("ConfirmTx refuses to send when the fee estimate fails, with its own messag
|
|||||||
);
|
);
|
||||||
env.routeOpts.failGasEstimate = true;
|
env.routeOpts.failGasEstimate = true;
|
||||||
env.routeOpts.holdGasEstimate = true;
|
env.routeOpts.holdGasEstimate = true;
|
||||||
|
try {
|
||||||
await goToConfirm(env.page, {
|
await goToConfirm(env.page, {
|
||||||
token: "ETH",
|
token: "ETH",
|
||||||
balance: FUNDED_ETH_TEXT + " ETH",
|
balance: FUNDED_ETH_TEXT + " ETH",
|
||||||
@@ -1949,10 +1956,10 @@ test("ConfirmTx refuses to send when the fee estimate fails, with its own messag
|
|||||||
await waitForEstimate(env.page);
|
await waitForEstimate(env.page);
|
||||||
|
|
||||||
const st = await confirmState(env.page);
|
const st = await confirmState(env.page);
|
||||||
env.routeOpts.failGasEstimate = false;
|
|
||||||
assert(
|
assert(
|
||||||
st.fee === "Unable to estimate",
|
st.fee === "Unable to estimate",
|
||||||
"the fee line does not report the failure: " + JSON.stringify(st.fee),
|
"the fee line does not report the failure: " +
|
||||||
|
JSON.stringify(st.fee),
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
!st.reserveShown,
|
!st.reserveShown,
|
||||||
@@ -1978,10 +1985,15 @@ test("ConfirmTx refuses to send when the fee estimate fails, with its own messag
|
|||||||
st.height +
|
st.height +
|
||||||
"px",
|
"px",
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
env.routeOpts.failGasEstimate = false;
|
||||||
|
env.routeOpts.holdGasEstimate = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ConfirmTx drives the ERC-20 path from pending to funded (#238)", async (env) => {
|
test("ConfirmTx drives the ERC-20 path from pending to funded (#238)", async (env) => {
|
||||||
env.routeOpts.holdGasEstimate = true;
|
env.routeOpts.holdGasEstimate = true;
|
||||||
|
try {
|
||||||
await goToConfirm(env.page, {
|
await goToConfirm(env.page, {
|
||||||
token: STUB_TOKEN.address,
|
token: STUB_TOKEN.address,
|
||||||
balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
|
balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
|
||||||
@@ -1990,9 +2002,12 @@ test("ConfirmTx drives the ERC-20 path from pending to funded (#238)", async (en
|
|||||||
|
|
||||||
const pending = await confirmState(env.page);
|
const pending = await confirmState(env.page);
|
||||||
env.erc20PendingHeight = pending.height;
|
env.erc20PendingHeight = pending.height;
|
||||||
console.log("# confirm-tx ERC-20 view height: " + pending.height + "px");
|
console.log(
|
||||||
|
"# confirm-tx ERC-20 view height: " + pending.height + "px",
|
||||||
|
);
|
||||||
assert(
|
assert(
|
||||||
pending.type === "ERC-20 token transfer (" + STUB_TOKEN.symbol + ")",
|
pending.type ===
|
||||||
|
"ERC-20 token transfer (" + STUB_TOKEN.symbol + ")",
|
||||||
"unexpected transaction type: " + JSON.stringify(pending.type),
|
"unexpected transaction type: " + JSON.stringify(pending.type),
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
@@ -2020,7 +2035,10 @@ test("ConfirmTx drives the ERC-20 path from pending to funded (#238)", async (en
|
|||||||
"the ERC-20 fee block does not quote the reserve: " +
|
"the ERC-20 fee block does not quote the reserve: " +
|
||||||
JSON.stringify(st.reserve),
|
JSON.stringify(st.reserve),
|
||||||
);
|
);
|
||||||
assert(!st.sendDisabled, "Send is disabled for a funded ERC-20 transfer");
|
assert(
|
||||||
|
!st.sendDisabled,
|
||||||
|
"Send is disabled for a funded ERC-20 transfer",
|
||||||
|
);
|
||||||
assert(
|
assert(
|
||||||
st.height === pending.height,
|
st.height === pending.height,
|
||||||
"the ERC-20 view changed height when the estimate landed: " +
|
"the ERC-20 view changed height when the estimate landed: " +
|
||||||
@@ -2029,6 +2047,9 @@ test("ConfirmTx drives the ERC-20 path from pending to funded (#238)", async (en
|
|||||||
st.height +
|
st.height +
|
||||||
"px",
|
"px",
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
env.routeOpts.holdGasEstimate = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ConfirmTx refuses an ERC-20 send that exceeds the token balance (#238)", async (env) => {
|
test("ConfirmTx refuses an ERC-20 send that exceeds the token balance (#238)", async (env) => {
|
||||||
@@ -2130,6 +2151,7 @@ test("ConfirmTx reports a failed ERC-20 estimate as unknown, not as a fee proble
|
|||||||
/gas estimation failed/,
|
/gas estimation failed/,
|
||||||
);
|
);
|
||||||
env.routeOpts.failGasEstimate = true;
|
env.routeOpts.failGasEstimate = true;
|
||||||
|
try {
|
||||||
await goToConfirm(env.page, {
|
await goToConfirm(env.page, {
|
||||||
token: STUB_TOKEN.address,
|
token: STUB_TOKEN.address,
|
||||||
balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
|
balance: TOKEN_BALANCE_TEXT + " " + STUB_TOKEN.symbol,
|
||||||
@@ -2138,7 +2160,6 @@ test("ConfirmTx reports a failed ERC-20 estimate as unknown, not as a fee proble
|
|||||||
await waitForEstimate(env.page);
|
await waitForEstimate(env.page);
|
||||||
|
|
||||||
const st = await confirmState(env.page);
|
const st = await confirmState(env.page);
|
||||||
env.routeOpts.failGasEstimate = false;
|
|
||||||
assert(
|
assert(
|
||||||
st.fee === "Unable to estimate",
|
st.fee === "Unable to estimate",
|
||||||
"the ERC-20 fee line does not report the failure: " +
|
"the ERC-20 fee line does not report the failure: " +
|
||||||
@@ -2164,6 +2185,9 @@ test("ConfirmTx reports a failed ERC-20 estimate as unknown, not as a fee proble
|
|||||||
st.height +
|
st.height +
|
||||||
"px",
|
"px",
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
env.routeOpts.failGasEstimate = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------- the popup's own send, end to end (#305)
|
// ------------------------- the popup's own send, end to end (#305)
|
||||||
@@ -2220,6 +2244,34 @@ async function fillPasswordAndSend(page) {
|
|||||||
await page.click("#btn-confirm-send");
|
await page.click("#btn-confirm-send");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Back to the address screen from wherever a send stopped, which is where a
|
||||||
|
// passing send test leaves the popup for the next one. The success and failure
|
||||||
|
// screens are left by their Done button. The wait for a receipt has no button
|
||||||
|
// and asks only every ten seconds; a reopened popup resumes it and asks at
|
||||||
|
// once, and seedReceipt has the stub node confirm the transaction.
|
||||||
|
async function backToAddressAfterSend(env) {
|
||||||
|
if (await env.page.isVisible("#view-wait-tx")) {
|
||||||
|
env.routeOpts.seedReceipt = true;
|
||||||
|
await waitForPersisted(
|
||||||
|
env.page,
|
||||||
|
"currentView",
|
||||||
|
"wait-tx",
|
||||||
|
"before closing the popup",
|
||||||
|
);
|
||||||
|
await env.page.close();
|
||||||
|
env.page = await openPopup(env.ctx, env.popupUrl);
|
||||||
|
await visible(env.page, "#view-success-tx");
|
||||||
|
env.routeOpts.seedReceipt = false;
|
||||||
|
}
|
||||||
|
for (const done of ["#btn-success-tx-done", "#btn-error-tx-done"]) {
|
||||||
|
if (await env.page.isVisible(done)) {
|
||||||
|
await env.page.click(done);
|
||||||
|
await visible(env.page, "#view-address");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await backToAddress(env.page);
|
||||||
|
}
|
||||||
|
|
||||||
async function goToTokenConfirm(env) {
|
async function goToTokenConfirm(env) {
|
||||||
await goToConfirm(env.page, {
|
await goToConfirm(env.page, {
|
||||||
token: STUB_TOKEN.address,
|
token: STUB_TOKEN.address,
|
||||||
@@ -2240,13 +2292,14 @@ test("the popup's own ERC-20 send broadcasts the amount it displayed (#305)", as
|
|||||||
// The previous test left the ETH balance at the fee-only fixture, which
|
// The previous test left the ETH balance at the fee-only fixture, which
|
||||||
// blocks sending outright; this one has to be able to press Send.
|
// blocks sending outright; this one has to be able to press Send.
|
||||||
env.routeOpts.ethBalanceWei = toHexWei(FUNDED_ETH_WEI);
|
env.routeOpts.ethBalanceWei = toHexWei(FUNDED_ETH_WEI);
|
||||||
|
// Confirm the transaction once it is broadcast, so the wait screen
|
||||||
|
// resolves to the success view instead of polling for the rest of the run.
|
||||||
|
env.routeOpts.seedReceipt = true;
|
||||||
|
try {
|
||||||
await settleOnMain(env, { ethWei: FUNDED_ETH_WEI, expectToken: true });
|
await settleOnMain(env, { ethWei: FUNDED_ETH_WEI, expectToken: true });
|
||||||
const shown = await goToTokenConfirm(env);
|
const shown = await goToTokenConfirm(env);
|
||||||
|
|
||||||
const before = env.routeOpts.broadcastTransactions.length;
|
const before = env.routeOpts.broadcastTransactions.length;
|
||||||
// Confirm the transaction once it is broadcast, so the wait screen
|
|
||||||
// resolves to the success view instead of polling for the rest of the run.
|
|
||||||
env.routeOpts.seedReceipt = true;
|
|
||||||
await fillPasswordAndSend(env.page);
|
await fillPasswordAndSend(env.page);
|
||||||
await visible(env.page, "#view-wait-tx", 60000);
|
await visible(env.page, "#view-wait-tx", 60000);
|
||||||
|
|
||||||
@@ -2260,9 +2313,9 @@ test("the popup's own ERC-20 send broadcasts the amount it displayed (#305)", as
|
|||||||
broadcast[broadcast.length - 1],
|
broadcast[broadcast.length - 1],
|
||||||
);
|
);
|
||||||
|
|
||||||
// The measurement, printed on every run: the amount the user read, and
|
// The measurement, printed on every run: the amount the user read,
|
||||||
// what the signed bytes mean at each of the two candidate scales. Under
|
// and what the signed bytes mean at each of the two candidate scales.
|
||||||
// the defect these three lines disagree.
|
// Under the defect these three lines disagree.
|
||||||
console.log(
|
console.log(
|
||||||
"# erc-20 send artifact: displayed=" +
|
"# erc-20 send artifact: displayed=" +
|
||||||
JSON.stringify(shown) +
|
JSON.stringify(shown) +
|
||||||
@@ -2318,29 +2371,38 @@ test("the popup's own ERC-20 send broadcasts the amount it displayed (#305)", as
|
|||||||
);
|
);
|
||||||
|
|
||||||
await visible(env.page, "#view-success-tx", 60000);
|
await visible(env.page, "#view-success-tx", 60000);
|
||||||
await env.page.click("#btn-success-tx-done");
|
} finally {
|
||||||
await visible(env.page, "#view-address");
|
|
||||||
env.routeOpts.seedReceipt = false;
|
env.routeOpts.seedReceipt = false;
|
||||||
|
await backToAddressAfterSend(env);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("a token that lies about decimals() at signing time broadcasts nothing (#305)", async (env) => {
|
test("a token that lies about decimals() at signing time broadcasts nothing (#305)", async (env) => {
|
||||||
|
try {
|
||||||
const shown = await goToTokenConfirm(env);
|
const shown = await goToTokenConfirm(env);
|
||||||
|
|
||||||
// Only now, with the screen already built and its estimate already taken
|
// Only now, with the screen already built and its estimate already
|
||||||
// at the explorer's scale, does the contract start answering differently.
|
// taken at the explorer's scale, does the contract start answering
|
||||||
// This is the whole shape of the defect: a value read at signing time that
|
// differently. This is the whole shape of the defect: a value read at
|
||||||
// nothing on screen was ever derived from.
|
// signing time that nothing on screen was ever derived from.
|
||||||
env.routeOpts.tokenDecimalsOverride = LYING_DECIMALS;
|
env.routeOpts.tokenDecimalsOverride = LYING_DECIMALS;
|
||||||
const before = env.routeOpts.broadcastTransactions.length;
|
const before = env.routeOpts.broadcastTransactions.length;
|
||||||
await fillPasswordAndSend(env.page);
|
await fillPasswordAndSend(env.page);
|
||||||
await visible(env.page, "#view-error-tx", 60000);
|
|
||||||
env.routeOpts.tokenDecimalsOverride = null;
|
|
||||||
|
|
||||||
|
// The count is asserted as soon as the send has ended either way, and
|
||||||
|
// the screen only after it: a transfer that got through shows as the
|
||||||
|
// wait for its receipt, never as the failure screen expected here.
|
||||||
|
await visible(
|
||||||
|
env.page,
|
||||||
|
"#view-wait-tx:not(.hidden), #view-error-tx:not(.hidden)",
|
||||||
|
60000,
|
||||||
|
);
|
||||||
assert(
|
assert(
|
||||||
env.routeOpts.broadcastTransactions.length === before,
|
env.routeOpts.broadcastTransactions.length === before,
|
||||||
"a transfer encoded against a contract that contradicts the " +
|
"a transfer encoded against a contract that contradicts the " +
|
||||||
"confirmation screen still reached the RPC (#305)",
|
"confirmation screen still reached the RPC (#305)",
|
||||||
);
|
);
|
||||||
|
await visible(env.page, "#view-error-tx");
|
||||||
|
|
||||||
const message = (
|
const message = (
|
||||||
await env.page.locator("#error-tx-message").innerText()
|
await env.page.locator("#error-tx-message").innerText()
|
||||||
@@ -2363,9 +2425,10 @@ test("a token that lies about decimals() at signing time broadcasts nothing (#30
|
|||||||
/^[A-Z].*\.$/s.test(message),
|
/^[A-Z].*\.$/s.test(message),
|
||||||
"the refusal is not a full sentence: " + JSON.stringify(message),
|
"the refusal is not a full sentence: " + JSON.stringify(message),
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
await env.page.click("#btn-error-tx-done");
|
env.routeOpts.tokenDecimalsOverride = null;
|
||||||
await visible(env.page, "#view-address");
|
await backToAddressAfterSend(env);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------------------------- hostile token symbol (#307)
|
// ------------------------------------------- hostile token symbol (#307)
|
||||||
@@ -2437,6 +2500,7 @@ test("a token whose symbol() returns markup renders as text (#307)", async (env)
|
|||||||
"# stub token symbol() now returns: " + JSON.stringify(HOSTILE_SYMBOL),
|
"# stub token symbol() now returns: " + JSON.stringify(HOSTILE_SYMBOL),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
// Close and reopen so the refresh that runs on open fetches balances
|
// Close and reopen so the refresh that runs on open fetches balances
|
||||||
// with the hostile symbol in them.
|
// with the hostile symbol in them.
|
||||||
await reopenPopup(env, "address");
|
await reopenPopup(env, "address");
|
||||||
@@ -2499,12 +2563,15 @@ test("a token whose symbol() returns markup renders as text (#307)", async (env)
|
|||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
!st.rowText.includes("z-index"),
|
!st.rowText.includes("z-index"),
|
||||||
"the uncapped symbol reached the screen: " + JSON.stringify(st.rowText),
|
"the uncapped symbol reached the screen: " +
|
||||||
|
JSON.stringify(st.rowText),
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
// Put the fixture back before the next test reads it, and let the
|
// Put the fixture back before the next test reads it.
|
||||||
// stored balances be rewritten with the honest symbol.
|
|
||||||
env.routeOpts.tokenSymbolOverride = null;
|
env.routeOpts.tokenSymbolOverride = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let the stored balances be rewritten with the honest symbol.
|
||||||
await reopenPopup(env, "main");
|
await reopenPopup(env, "main");
|
||||||
await env.page.waitForFunction(
|
await env.page.waitForFunction(
|
||||||
(addr) => {
|
(addr) => {
|
||||||
|
|||||||
@@ -253,8 +253,6 @@ async function bootPopup(stored, options) {
|
|||||||
formatUsd: () => "",
|
formatUsd: () => "",
|
||||||
formatAddressTotal: () => "",
|
formatAddressTotal: () => "",
|
||||||
getAddressValue: () => ({ usd: null, partial: false }),
|
getAddressValue: () => ({ usd: null, partial: false }),
|
||||||
getWalletValue: () => ({ usd: null, partial: false }),
|
|
||||||
getTotalValue: () => ({ usd: null, partial: false }),
|
|
||||||
}));
|
}));
|
||||||
jest.doMock("../../src/shared/balances", () => ({
|
jest.doMock("../../src/shared/balances", () => ({
|
||||||
fetchTokenBalances: jest.fn(async () => []),
|
fetchTokenBalances: jest.fn(async () => []),
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
// A transaction's time is written by isoDate() and timeAgo() in
|
||||||
|
// src/popup/views/helpers.js on every screen that shows one (README, Display
|
||||||
|
// Consistency; https://git.eeqj.de/sneak/AutistMask/issues/168). AddressDetail
|
||||||
|
// and AddressToken used to define their own copies, so a fix to the shared pair
|
||||||
|
// would not have reached them.
|
||||||
|
//
|
||||||
|
// The pair is replaced before the views are loaded, because a view takes it
|
||||||
|
// when it loads. A view that writes the time with a copy of its own shows the
|
||||||
|
// real time instead of the replacement.
|
||||||
|
//
|
||||||
|
// Driven against a minimal DOM stub in the shape
|
||||||
|
// tests/contractCreation.test.js uses.
|
||||||
|
|
||||||
|
jest.mock("../src/shared/log", () => ({
|
||||||
|
log: {
|
||||||
|
debugf: () => {},
|
||||||
|
infof: () => {},
|
||||||
|
warnf: () => {},
|
||||||
|
errorf: () => {},
|
||||||
|
},
|
||||||
|
// The transaction detail view fetches on-chain details after drawing; an
|
||||||
|
// answer that is not ok leaves the drawn lines as they are.
|
||||||
|
debugFetch: async () => ({ ok: false }),
|
||||||
|
setRuntimeDebug: () => {},
|
||||||
|
isDebug: () => false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// The history lists ask the explorer for their transactions and resolve ENS
|
||||||
|
// names for them; here the explorer answers with mockHistory and no name
|
||||||
|
// resolves.
|
||||||
|
let mockHistory = [];
|
||||||
|
jest.mock("../src/shared/transactions", () => ({
|
||||||
|
...jest.requireActual("../src/shared/transactions"),
|
||||||
|
fetchRecentTransactions: async () => mockHistory,
|
||||||
|
}));
|
||||||
|
jest.mock("../src/shared/ens", () => ({
|
||||||
|
...jest.requireActual("../src/shared/ens"),
|
||||||
|
resolveEnsNames: async () => new Map(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
globalThis.chrome = {
|
||||||
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const helpers = require("../src/popup/views/helpers");
|
||||||
|
jest.spyOn(helpers, "isoDate").mockReturnValue("SHARED-ISO-DATE");
|
||||||
|
jest.spyOn(helpers, "timeAgo").mockReturnValue("SHARED-TIME-AGO");
|
||||||
|
|
||||||
|
const { state } = require("../src/shared/state");
|
||||||
|
const addressDetail = require("../src/popup/views/addressDetail");
|
||||||
|
const addressToken = require("../src/popup/views/addressToken");
|
||||||
|
const transactionDetail = require("../src/popup/views/transactionDetail");
|
||||||
|
|
||||||
|
const FROM = "0x0000000000000000000000000000000000000a11";
|
||||||
|
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
||||||
|
|
||||||
|
function makeElement(id) {
|
||||||
|
const el = {
|
||||||
|
id,
|
||||||
|
textContent: "",
|
||||||
|
value: "",
|
||||||
|
innerHTML: "",
|
||||||
|
style: {},
|
||||||
|
dataset: {},
|
||||||
|
classList: {
|
||||||
|
add: () => {},
|
||||||
|
remove: () => {},
|
||||||
|
contains: () => false,
|
||||||
|
toggle: () => false,
|
||||||
|
},
|
||||||
|
addEventListener: () => {},
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
appendChild: () => {},
|
||||||
|
};
|
||||||
|
// Views reach for .parentElement to hide whole sections.
|
||||||
|
Object.defineProperty(el, "parentElement", {
|
||||||
|
get: () => node(id + "-parent"),
|
||||||
|
});
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDocument() {
|
||||||
|
const els = new Map();
|
||||||
|
return {
|
||||||
|
getElementById(id) {
|
||||||
|
// The debug banner is created on demand by helpers.js; absent
|
||||||
|
// is the state a non-debug, non-testnet popup is in.
|
||||||
|
if (id === "debug-banner") return null;
|
||||||
|
if (!els.has(id)) els.set(id, makeElement(id));
|
||||||
|
return els.get(id);
|
||||||
|
},
|
||||||
|
createElement: () => makeElement("created"),
|
||||||
|
body: { prepend: () => {} },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function node(id) {
|
||||||
|
return globalThis.document.getElementById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A transaction FROM sent, as the history lists hold it.
|
||||||
|
function historyTx() {
|
||||||
|
return {
|
||||||
|
hash: "0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84",
|
||||||
|
from: FROM,
|
||||||
|
to: RECIPIENT,
|
||||||
|
value: "0.0000",
|
||||||
|
exactValue: "0.0",
|
||||||
|
rawAmount: "0",
|
||||||
|
rawUnit: "wei",
|
||||||
|
symbol: "ETH",
|
||||||
|
timestamp: 1790000000,
|
||||||
|
isError: false,
|
||||||
|
directionLabel: "Sent",
|
||||||
|
direction: "sent",
|
||||||
|
contractAddress: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
globalThis.document = makeDocument();
|
||||||
|
globalThis.window = { location: { search: "" } };
|
||||||
|
state.wallets = [
|
||||||
|
{
|
||||||
|
name: "Main",
|
||||||
|
type: "key",
|
||||||
|
addresses: [{ address: FROM, balance: "0.0000" }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
state.trackedTokens = [];
|
||||||
|
state.viewData = {};
|
||||||
|
state.viewStack = [];
|
||||||
|
state.currentView = null;
|
||||||
|
state.selectedWallet = 0;
|
||||||
|
state.selectedAddress = 0;
|
||||||
|
state.selectedToken = "ETH";
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
["AddressDetail", "tx-list", () => addressDetail.show()],
|
||||||
|
["AddressToken", "address-token-tx-list", () => addressToken.show()],
|
||||||
|
])("a transaction history row on %s", (_name, listId, open) => {
|
||||||
|
test("shows the time written by the shared isoDate() and timeAgo()", async () => {
|
||||||
|
mockHistory = [historyTx()];
|
||||||
|
open();
|
||||||
|
// The list is drawn once the history has been fetched.
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
const html = node(listId).innerHTML;
|
||||||
|
expect(html).toContain('title="SHARED-ISO-DATE"');
|
||||||
|
expect(html).toContain(">SHARED-TIME-AGO<");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the transaction detail view shows the time written by the shared isoDate() and timeAgo()", () => {
|
||||||
|
transactionDetail.show(historyTx());
|
||||||
|
const html = node("tx-detail-time").innerHTML;
|
||||||
|
expect(html).toContain("SHARED-ISO-DATE");
|
||||||
|
expect(html).toContain("(SHARED-TIME-AGO)");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user