Author SHA1 Message Date
sneak 3486d46998 docs: TODO.md Next Step names open milestone work, not tagging (closes #771)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
Tags and releases are the owner's call; no repo document tells him when
to tag.

Model: opus-5-5
2026-11-11 01:06:11 +00:00
clawbot 02f14143eb docs: TODO.md holds open work only (closes #769)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
Removes the Completed Steps work log and the v0.1.0 tagging date from
Status. Every open item the log mentioned is a tracker issue. The
Workflow bullet that named the removed section now says to record no
finished work in this file.

Model: opus-5-5
2026-11-11 02:04:13 +01:00
clawbot b87c03be11 fix: build hashes and recovery-phrase entropy come from ethers (closes #758)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
build.js and script/lib/package.js hash with sha256 from ethers, with the
0x prefix stripped so the build receipt and SHA256SUMS keep bare lowercase
hex. generateMnemonic() takes its 16 bytes of entropy from randomBytes in
ethers. The README Crypto policy no longer lists these as exceptions.

Model: opus-5-5
2026-11-11 00:42:18 +01:00
17 changed files with 91 additions and 2104 deletions

No files matched your search

+6 -7
View File
@@ -1191,8 +1191,8 @@ These helpers write one kind of value on every screen that shows it:
Most screens show token and ETH amounts with four decimal places. History rows Most screens show token and ETH amounts with four decimal places. History rows
cut amounts to four decimals and never round up (`truncateAmount()` in cut amounts to four decimals and never round up (`truncateAmount()` in
`src/shared/amountDisplay.js`). The balance lists and Home's ETH balance cut `src/shared/amountDisplay.js`). The balance lists and Home's ETH balance round
them the same way. The screens that state what is being authorized cut as the to four places. The screens that state what is being authorized cut as the
history does, and extend an amount below 0.0001 to its first nonzero digit history does, and extend an amount below 0.0001 to its first nonzero digit
(`truncateAmountNeverZero()`). So a nonzero amount never reads as zero there. (`truncateAmountNeverZero()`). So a nonzero amount never reads as zero there.
Those screens are: Those screens are:
@@ -1647,11 +1647,10 @@ The development dependencies ship nowhere:
### Crypto policy ### Crypto policy
Hashing, key derivation, signing and encryption go through `ethers` or In the extension, hashing, key derivation, signing and encryption go through
`libsodium-wrappers-sumo`. No primitive is implemented in this repository. A new `ethers` or `libsodium-wrappers-sumo`. A new recovery phrase takes its random
recovery phrase draws its random bytes from the browser's bytes from `ethers`. `build.js` and `script/lib/package.js` also hash files with
`crypto.getRandomValues()`. The build tooling, `build.js` and `script/lib/`, `ethers`. No primitive is implemented in this repository.
hashes files with Node's `crypto`; none of it ships in the extension.
### Content Security Policy ### Content Security Policy
+6 -1938
View File
File diff suppressed because it is too large. Load diff
+3 -5
View File
@@ -1,8 +1,8 @@
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const crypto = require("crypto");
const { execSync } = require("child_process"); const { execSync } = require("child_process");
const esbuild = require("esbuild"); const esbuild = require("esbuild");
const { sha256 } = require("ethers");
const { resolveVersion } = require("./script/lib/version"); const { resolveVersion } = require("./script/lib/version");
const { const {
BACKGROUND_ENTRY_PREFIX, BACKGROUND_ENTRY_PREFIX,
@@ -314,10 +314,8 @@ function copyIcons(distDir) {
} }
function sha256File(absPath) { function sha256File(absPath) {
return crypto // ethers prefixes the hex with 0x; the receipt holds bare hex.
.createHash("sha256") return sha256(fs.readFileSync(absPath)).slice(2);
.update(fs.readFileSync(absPath))
.digest("hex");
} }
// Write the receipt for the files this build emitted. Deliberately records no // Write the receipt for the files this build emitted. Deliberately records no
+9 -3
View File
@@ -25,9 +25,9 @@
"use strict"; "use strict";
const crypto = require("crypto");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const ethers = require("ethers");
const { readZip, writeZip } = require("./zip"); const { readZip, writeZip } = require("./zip");
const { resolveVersion } = require("./version"); const { resolveVersion } = require("./version");
@@ -60,8 +60,9 @@ function fail(message) {
throw new Error(message); throw new Error(message);
} }
// Lowercase hex without the 0x prefix ethers adds, as sha256sum prints it.
function sha256(buf) { function sha256(buf) {
return crypto.createHash("sha256").update(buf).digest("hex"); return ethers.sha256(buf).slice(2);
} }
// Every regular file under dir, as archive-root-relative forward-slashed // Every regular file under dir, as archive-root-relative forward-slashed
@@ -291,4 +292,9 @@ if (require.main === module) {
} }
} }
module.exports = { checkSelfContained, htmlReferences, manifestReferences }; module.exports = {
checkSelfContained,
htmlReferences,
manifestReferences,
sha256,
};
+5 -6
View File
@@ -159,10 +159,10 @@ function renderBalance() {
const addr = const addr =
state.wallets[state.selectedWallet].addresses[state.selectedAddress]; state.wallets[state.selectedWallet].addresses[state.selectedAddress];
const tokenId = state.selectedToken; const tokenId = state.selectedToken;
let balance, price; let amount, price;
if (tokenId === "ETH") { if (tokenId === "ETH") {
// null when no refresh has read the balance. // null when no refresh has read the balance.
balance = addr.balance; amount = unknownableAmount(addr.balance);
price = getPrice("ETH"); price = getPrice("ETH");
} else { } else {
const id = tokenId.toLowerCase(); const id = tokenId.toLowerCase();
@@ -175,13 +175,12 @@ function renderBalance() {
// null when the scale is unknown, or when no refresh has read the // null when the scale is unknown, or when no refresh has read the
// token's balance: no quantity to show, and none to price. // token's balance: no quantity to show, and none to price.
// balanceLine() states that rather than printing 0.0000. // balanceLine() states that rather than printing 0.0000.
if (tb) balance = tb.balance; if (tb) amount = unknownableAmount(tb.balance);
else balance = tracked && tracked.balanceNotFetched ? null : "0"; else amount = tracked && tracked.balanceNotFetched ? null : 0;
price = getPrice(tokenId); price = getPrice(tokenId);
} }
// USD total for this token only // USD total for this token only
const amount = unknownableAmount(balance);
const usdVal = price && amount !== null ? amount * price : null; const usdVal = price && amount !== null ? amount * price : null;
const usdStr = formatUsd(usdVal); const usdStr = formatUsd(usdVal);
$("address-token-usd-total").innerHTML = escapeHtml(usdStr) || " "; $("address-token-usd-total").innerHTML = escapeHtml(usdStr) || " ";
@@ -189,7 +188,7 @@ function renderBalance() {
// Single token balance line (no tokenId — not clickable here) // Single token balance line (no tokenId — not clickable here)
$("address-token-balance").innerHTML = balanceLine( $("address-token-balance").innerHTML = balanceLine(
currentSymbol, currentSymbol,
balance, amount,
price, price,
); );
} }
+2 -2
View File
@@ -79,8 +79,8 @@ function recoveryPathText(wallet) {
// "Holds" means ETH or any ERC-20 the wallet knows about — an address with no // "Holds" means ETH or any ERC-20 the wallet knows about — an address with no
// ETH and a five-figure stablecoin position must not get the blank line on // ETH and a five-figure stablecoin position must not get the blank line on
// the one screen whose job is to warn. The sentence names no figure of its // the one screen whose job is to warn. The sentence names no figure of its
// own: the rendered lines cut to four decimals, so a sentence built from their // own: the rendered lines round to four decimals, so a sentence built from a
// figure would report "0.0000 ETH" for an address holding real money. // rounded number would report "0.0000 ETH" for an address holding real money.
// The lines below it carry the amounts, in the same format as Home and // The lines below it carry the amounts, in the same format as Home and
// AddressDetail, followed by the USD total when there is one to give — no // AddressDetail, followed by the USD total when there is one to give — no
// total line at all on testnet, before the first price fetch or while no // total line at all on testnet, before the first price fetch or while no
+11 -14
View File
@@ -18,7 +18,6 @@
const { formatEther } = require("ethers"); const { formatEther } = require("ethers");
const makeBlockie = require("ethereum-blockies-base64"); const makeBlockie = require("ethereum-blockies-base64");
const { const {
truncateAmount,
truncateAmountNeverZero, truncateAmountNeverZero,
isBelowOneMillionth, isBelowOneMillionth,
} = require("../../shared/amountDisplay"); } = require("../../shared/amountDisplay");
@@ -374,15 +373,13 @@ function formatFee(wei, symbol, chainId) {
// the row that issue #307 was reported against: every screen that lists a // the row that issue #307 was reported against: every screen that lists a
// holding renders through here. // holding renders through here.
// //
// `balance` is the stored balance, a decimal string. It is null for a holding // `amount` is null for a holding whose scale nothing knows
// whose scale nothing knows
// (https://git.eeqj.de/sneak/AutistMask/issues/349), and for a balance no // (https://git.eeqj.de/sneak/AutistMask/issues/349), and for a balance no
// refresh has read. There is no quantity to print for it and no fiat value to // refresh has read. There is no quantity to print for it and no fiat value to
// derive from one, and printing 0.0000 for a real holding is the failure this // derive from one, and printing 0.0000 for a real holding is the failure this
// whole rule exists to prevent, so the row says so instead. // whole rule exists to prevent, so the row says so instead.
function balanceLine(symbol, balance, price, tokenId) { function balanceLine(symbol, amount, price, tokenId) {
const amount = unknownableAmount(balance); const qty = amount === null ? "quantity unknown" : amount.toFixed(4);
const qty = amount === null ? "quantity unknown" : truncateAmount(balance);
const usd = const usd =
price && amount !== null price && amount !== null
? escapeHtml(formatUsd(amount * price)) || " " ? escapeHtml(formatUsd(amount * price)) || " "
@@ -397,7 +394,7 @@ function balanceLine(symbol, balance, price, tokenId) {
`<div class="flex text-xs${clickClass}"${tokenAttr}>` + `<div class="flex text-xs${clickClass}"${tokenAttr}>` +
`<span class="flex justify-between w-[42ch] max-w-full">` + `<span class="flex justify-between w-[42ch] max-w-full">` +
`<span>${escapeHtml(displaySymbol(symbol))}</span>` + `<span>${escapeHtml(displaySymbol(symbol))}</span>` +
`<span>${escapeHtml(qty)}</span>` + `<span>${qty}</span>` +
`</span>` + `</span>` +
`<span class="text-right text-muted flex-1">${usd}</span>` + `<span class="text-right text-muted flex-1">${usd}</span>` +
`</div>` `</div>`
@@ -423,7 +420,7 @@ function markTrackedTokensNotFetched() {
function balanceLinesForAddress(addr, trackedTokens, showZero) { function balanceLinesForAddress(addr, trackedTokens, showZero) {
let html = balanceLine( let html = balanceLine(
nativeCurrency(), nativeCurrency(),
addr.balance, unknownableAmount(addr.balance),
getPrice("ETH"), getPrice("ETH"),
"ETH", "ETH",
); );
@@ -441,7 +438,7 @@ function balanceLinesForAddress(addr, trackedTokens, showZero) {
if (bal === 0 && !showZero) continue; if (bal === 0 && !showZero) continue;
html += balanceLine( html += balanceLine(
t.symbol, t.symbol,
t.balance, bal,
getPrice(t.address), getPrice(t.address),
t.address.toLowerCase(), t.address.toLowerCase(),
); );
@@ -459,7 +456,7 @@ function balanceLinesForAddress(addr, trackedTokens, showZero) {
} }
html += balanceLine( html += balanceLine(
t.symbol, t.symbol,
t.balanceNotFetched ? null : "0", t.balanceNotFetched ? null : 0,
getPrice(t.address), getPrice(t.address),
t.address.toLowerCase(), t.address.toLowerCase(),
); );
@@ -470,10 +467,10 @@ function balanceLinesForAddress(addr, trackedTokens, showZero) {
// Whether an address holds anything at all: ETH or any ERC-20 the wallet // Whether an address holds anything at all: ETH or any ERC-20 the wallet
// knows about, except a token holding below 0.000001, which the balance list // knows about, except a token holding below 0.000001, which the balance list
// under the remove-address warning leaves out too. Deliberately exact — the // under the remove-address warning leaves out too. Deliberately unrounded —
// rendered lines cut to four decimals, so a dust balance displays as 0.0000 // the rendered lines round to four decimals, so a dust balance displays as
// while still being real money at a real address. Callers that warn about // 0.0000 while still being real money at a real address. Callers that warn
// holdings must ask this, not the rendered figure. // about holdings must ask this, not the rendered figure.
function addressHoldsFunds(addr) { function addressHoldsFunds(addr) {
if (!addr) return false; if (!addr) return false;
// A null or absent ETH balance reads as unknown, so the address may hold // A null or absent ETH balance reads as unknown, so the address may hold
+1 -2
View File
@@ -31,7 +31,6 @@ const {
resetSendValidation, resetSendValidation,
} = require("./send"); } = require("./send");
const { deriveAddressFromXpub } = require("../../shared/wallet"); const { deriveAddressFromXpub } = require("../../shared/wallet");
const { truncateAmount } = require("../../shared/amountDisplay");
const { canRemoveAddress } = require("../../shared/walletDelete"); const { canRemoveAddress } = require("../../shared/walletDelete");
const { const {
walletDefect, walletDefect,
@@ -82,7 +81,7 @@ function renderTotalValue() {
if (ethBal === null) { if (ethBal === null) {
el.textContent = "unknown (" + nativeCurrency() + ")"; el.textContent = "unknown (" + nativeCurrency() + ")";
} else { } else {
const ethStr = truncateAmount(addr.balance) + " " + nativeCurrency(); const ethStr = ethBal.toFixed(4) + " " + nativeCurrency();
const ethUsd = ethPrice const ethUsd = ethPrice
? " (" + formatUsd(ethBal * ethPrice) + ")" ? " (" + formatUsd(ethBal * ethPrice) + ")"
: ""; : "";
+2 -5
View File
@@ -1,7 +1,7 @@
// Wallet operations: mnemonic generation, HD derivation, signing. // Wallet operations: mnemonic generation, HD derivation, signing.
// All crypto delegated to ethers.js. // All crypto delegated to ethers.js.
const { Mnemonic, HDNodeWallet, Wallet } = require("ethers"); const { Mnemonic, HDNodeWallet, Wallet, randomBytes } = require("ethers");
const { DEBUG, DEBUG_MNEMONIC, BIP44_ETH_PATH } = require("./constants"); const { DEBUG, DEBUG_MNEMONIC, BIP44_ETH_PATH } = require("./constants");
function generateMnemonic() { function generateMnemonic() {
@@ -10,10 +10,7 @@ function generateMnemonic() {
// settings toggle drives, which would let a user of a release build turn // settings toggle drives, which would let a user of a release build turn
// the hardcoded, publicly known test phrase back on for real wallets. // the hardcoded, publicly known test phrase back on for real wallets.
if (DEBUG) return DEBUG_MNEMONIC; if (DEBUG) return DEBUG_MNEMONIC;
const m = Mnemonic.fromEntropy( return Mnemonic.fromEntropy(randomBytes(16)).phrase;
globalThis.crypto.getRandomValues(new Uint8Array(16)),
);
return m.phrase;
} }
// Every extended key (xprv or xpub) entering the app goes through this. // Every extended key (xprv or xpub) entering the app goes through this.
+15
View File
@@ -25,6 +25,10 @@ const { ERC20_ABI } = require("../src/shared/constants");
const { state } = require("../src/shared/state"); const { state } = require("../src/shared/state");
const { decodeCalldata } = require("../src/shared/calldataDecoder"); const { decodeCalldata } = require("../src/shared/calldataDecoder");
const uniswap = require("../src/shared/uniswap"); const uniswap = require("../src/shared/uniswap");
const {
truncateAmount,
truncateAmountNeverZero,
} = require("../src/shared/amountDisplay");
const iface = new Interface(ERC20_ABI); const iface = new Interface(ERC20_ABI);
@@ -179,4 +183,15 @@ describe("the four-decimal rule is otherwise unchanged", () => {
test("a genuine zero still renders as zero", () => { test("a genuine zero still renders as zero", () => {
expect(amount(transferData(0n), DAI).rawValue).toBe("0.0000"); expect(amount(transferData(0n), DAI).rawValue).toBe("0.0000");
}); });
// The three truncators now share one module. The floor is a policy of the
// approval and confirmation screens only: the history and balance lists
// keep plain truncation, because the transaction detail view is the
// authoritative record and already shows exact precision.
test("the list rule stays unfloored", () => {
expect(truncateAmount("0.000000000000000001")).toBe("0.0000");
expect(truncateAmountNeverZero("0.000000000000000001")).toBe(
"0.000000000000000001",
);
});
}); });
+5 -5
View File
@@ -41,13 +41,13 @@ describe("balanceLine", () => {
// this payload before its id attribute, so an assertion about the // this payload before its id attribute, so an assertion about the
// rest of the payload would pass on the cap and say nothing about // rest of the payload would pass on the cap and say nothing about
// the escape. // the escape.
const html = balanceLine(HOSTILE_SYMBOL, "1", null, null); const html = balanceLine(HOSTILE_SYMBOL, 1, null, null);
expect(html).not.toContain("<iframe"); expect(html).not.toContain("<iframe");
expect(html).toContain("&lt;iframe"); expect(html).toContain("&lt;iframe");
}); });
test("caps the symbol before rendering it", () => { test("caps the symbol before rendering it", () => {
const html = balanceLine("A".repeat(4096), "1", null, null); const html = balanceLine("A".repeat(4096), 1, null, null);
expect(html).toContain("A".repeat(MAX_SYMBOL_LENGTH - 1) + "…"); expect(html).toContain("A".repeat(MAX_SYMBOL_LENGTH - 1) + "…");
expect(html).not.toContain("A".repeat(MAX_SYMBOL_LENGTH + 1)); expect(html).not.toContain("A".repeat(MAX_SYMBOL_LENGTH + 1));
}); });
@@ -55,13 +55,13 @@ describe("balanceLine", () => {
// The token id lands inside data-token="...", so a quote in it is a // The token id lands inside data-token="...", so a quote in it is a
// way out of the attribute and into a new one. // way out of the attribute and into a new one.
test("keeps a quote-bearing token id inside its attribute", () => { test("keeps a quote-bearing token id inside its attribute", () => {
const html = balanceLine("TKN", "1", null, '" onclick="alert(1)'); const html = balanceLine("TKN", 1, null, '" onclick="alert(1)');
expect(html).not.toContain('onclick="'); expect(html).not.toContain('onclick="');
expect(html).toContain('data-token="&quot; onclick=&quot;alert(1)"'); expect(html).toContain('data-token="&quot; onclick=&quot;alert(1)"');
}); });
test("renders an ordinary holding unchanged", () => { test("renders an ordinary holding unchanged", () => {
const html = balanceLine("USDC", "1.5", null, "0xabc"); const html = balanceLine("USDC", 1.5, null, "0xabc");
expect(html).toContain("<span>USDC</span>"); expect(html).toContain("<span>USDC</span>");
expect(html).toContain("<span>1.5000</span>"); expect(html).toContain("<span>1.5000</span>");
expect(html).toContain('data-token="0xabc"'); expect(html).toContain('data-token="0xabc"');
@@ -69,7 +69,7 @@ describe("balanceLine", () => {
// formatUsd() writes a value under a cent as "< $0.01". // formatUsd() writes a value under a cent as "< $0.01".
test("escapes the USD value along with the symbol", () => { test("escapes the USD value along with the symbol", () => {
const html = balanceLine("USDC", "0.001", 1, null); const html = balanceLine("USDC", 0.001, 1, null);
expect(html).toContain("&lt; $0.01"); expect(html).toContain("&lt; $0.01");
expect(html).not.toContain("< $0.01"); expect(html).not.toContain("< $0.01");
}); });
-77
View File
@@ -1,77 +0,0 @@
// The balance lists and Home's ETH line show a balance as the history rows do
// (README.md, Amounts): cut to four decimal places, never rounded up
// (https://git.eeqj.de/sneak/AutistMask/issues/757).
//
// Driven through the real popup over a stored profile on Ethereum Mainnet.
const {
ADDRESS,
bootPopup,
cleanupPopup,
unversionedValidProfile,
} = require("./support/popupBoot");
const { balanceLine } = require("../src/popup/views/helpers");
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
afterEach(() => {
cleanupPopup();
});
// The quantity cell of a balance row.
function quantity(row) {
return row.match(/<span>([^<]*)<\/span><\/span>/)[1];
}
// The popup on Home over one address holding `eth` ETH and `usdc` USDC.
function bootHolding(eth, usdc) {
const profile = unversionedValidProfile({
trackedTokens: [{ address: USDC, symbol: "USDC", decimals: 6 }],
});
profile.wallets[0].addresses[0] = {
address: ADDRESS,
balance: eth,
tokenBalances: [
{ address: USDC, symbol: "USDC", decimals: 6, balance: usdc },
],
};
return bootPopup(profile);
}
// The balance row for `token` in Home's wallet list.
function homeRow(page, token) {
return page
.node("wallet-list")
.innerHTML.split("<div ")
.find((row) => row.includes('data-token="' + token + '"'));
}
describe("balanceLine", () => {
test("cuts 0.99996 to 0.9999, never rounding it up to 1.0000", () => {
expect(quantity(balanceLine("ETH", "0.99996", null, "ETH"))).toBe(
"0.9999",
);
});
test("cuts 0.00004 to 0.0000", () => {
expect(quantity(balanceLine("USDC", "0.00004", null, USDC))).toBe(
"0.0000",
);
});
});
describe("Home", () => {
test("the ETH line and the balance list read 0.99996 ETH as 0.9999", async () => {
const page = await bootHolding("0.99996", "0.00004");
expect(page.visibleViews()).toEqual(["main"]);
expect(page.text("total-value")).toBe("0.9999 ETH");
expect(quantity(homeRow(page, "ETH"))).toBe("0.9999");
});
test("the balance list reads 0.00004 USDC as 0.0000", async () => {
const page = await bootHolding("0.99996", "0.00004");
expect(quantity(homeRow(page, USDC.toLowerCase()))).toBe("0.0000");
});
});
+2 -2
View File
@@ -4,8 +4,8 @@
// the copy is the substance and is tested as such. Two things it must not // the copy is the substance and is tested as such. Two things it must not
// get wrong: what it takes to get the address back — the app refuses both // get wrong: what it takes to get the address back — the app refuses both
// obvious routes — and what counts as holding something, which is any // obvious routes — and what counts as holding something, which is any
// ERC-20 as well as ETH, at any size, including a balance below the four // ERC-20 as well as ETH, at any size, including a balance that rounds to
// decimals the balance lines render. The DOM behaviour // zero at the four decimals the balance lines render. The DOM behaviour
// around them is driven against the real popup by tests/e2e/run.js. // around them is driven against the real popup by tests/e2e/run.js.
// helpers.js pulls in state.js, which reads chrome.storage.local at load. // helpers.js pulls in state.js, which reads chrome.storage.local at load.
+9 -9
View File
@@ -829,11 +829,12 @@ test("a common-token quick-pick fills in the contract address (#150)", async (en
await visible(env.page, "#view-address"); await visible(env.page, "#view-address");
}); });
// The native amount as the transaction list writes it (cut to four decimals, // The native amount as the transaction list writes it (four decimals) and
// quantity() below) and as the detail screen writes it (full precision). Both // as the detail screen writes it (full precision). Both are rendered here
// are rendered here from the fixture rather than read off the screen, so the // from the fixture rather than read off the screen, so the assertions
// assertions compare against the wei the stub served. // compare against the wei the stub served.
const NATIVE_ROW_TEXT = quantity(STUB_NATIVE_VALUE_WEI) + " ETH"; const NATIVE_ROW_TEXT =
parseFloat(formatEther(STUB_NATIVE_VALUE_WEI)).toFixed(4) + " ETH";
const NATIVE_DETAIL_TEXT = formatEther(STUB_NATIVE_VALUE_WEI) + " ETH"; const NATIVE_DETAIL_TEXT = formatEther(STUB_NATIVE_VALUE_WEI) + " ETH";
test("the native ETH transaction detail still renders (#151)", async (env) => { test("the native ETH transaction detail still renders (#151)", async (env) => {
@@ -2723,11 +2724,10 @@ async function goToConfirm(page, send) {
await assertAddressesFit(page, "the confirmation screen"); await assertAddressesFit(page, "the confirmation screen");
} }
// An amount in wei as the main view's balance lines and the transaction list // A balance as the main view renders it: balanceLinesForAddress() writes
// write it: cut to four decimal places, never rounded, as feeEth() does. // every quantity with four decimal places.
function quantity(wei) { function quantity(wei) {
const [whole, frac = ""] = formatEther(wei).split("."); return parseFloat(formatEther(wei)).toFixed(4);
return whole + "." + (frac + "0000").slice(0, 4);
} }
// Wait on the main view until a changed balance fixture has been picked up. // Wait on the main view until a changed balance fixture has been picked up.
+13 -2
View File
@@ -1,4 +1,5 @@
// The release archives: the zip container, and the self-containment rule. // The release archives: the zip container, the self-containment rule, and the
// digest format in release/SHA256SUMS.
// //
// script/package produces one archive per browser and script/lib/package.js // script/package produces one archive per browser and script/lib/package.js
// decides what goes in it. The trap that rule exists for is real and specific: // decides what goes in it. The trap that rule exists for is real and specific:
@@ -11,12 +12,14 @@
// So the packager resolves every reference in the manifest and in every HTML // So the packager resolves every reference in the manifest and in every HTML
// document, and fails on any that leaves the extension root. These are the // document, and fails on any that leaves the extension root. These are the
// cases for that, plus the archive format itself — new code, and the thing the // cases for that, plus the archive format itself — new code, and the thing the
// artifact is made of. // artifact is made of. One more case checks the digest format in
// release/SHA256SUMS.
const { const {
checkSelfContained, checkSelfContained,
htmlReferences, htmlReferences,
manifestReferences, manifestReferences,
sha256,
} = require("../script/lib/package"); } = require("../script/lib/package");
const { readZip, writeZip } = require("../script/lib/zip"); const { readZip, writeZip } = require("../script/lib/zip");
@@ -142,6 +145,14 @@ describe("archive self-containment", () => {
}); });
}); });
// release/SHA256SUMS is checked with `sha256sum -c`, which needs the digest
// as bare lowercase hex. The expected value is the FIPS 180-2 digest of "abc".
test("sha256 prints the digest as sha256sum does", () => {
expect(sha256(Buffer.from("abc"))).toBe(
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
);
});
describe("the zip container", () => { describe("the zip container", () => {
const files = [ const files = [
{ name: "manifest.json", data: Buffer.from('{"a":1}') }, { name: "manifest.json", data: Buffer.from('{"a":1}') },
-25
View File
@@ -1361,31 +1361,6 @@ describe("fetchRecentTransactions merge and dedup", () => {
expect(txs[0].holders).toBe(3500000); expect(txs[0].holders).toBe(3500000);
}); });
// README.md, Amounts: a history row cuts to four decimals, with no floor.
test("a token transfer below 0.0001 reads 0.0000", async () => {
respondWith(
[],
[
{
transaction_hash: "0x" + "4".repeat(64),
block_number: 21000040,
timestamp: TS,
from: { hash: ORDINARY_PEER },
to: { hash: VICTIM },
total: { value: "40", decimals: "6" },
token: {
symbol: "USDC",
address_hash: USDC_CONTRACT,
holders_count: "3500000",
},
},
],
);
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
expect(txs[0].value).toBe("0.0000");
});
test("two transfers of the same token in one transaction collapse to one entry", async () => { test("two transfers of the same token in one transaction collapse to one entry", async () => {
const hash = "0x" + "3".repeat(64); const hash = "0x" + "3".repeat(64);
const leg = (value) => ({ const leg = (value) => ({
+2 -2
View File
@@ -96,7 +96,7 @@ afterEach(() => {
describe("balanceLine", () => { describe("balanceLine", () => {
test("an unknown quantity and a zero one render differently", () => { test("an unknown quantity and a zero one render differently", () => {
const unknown = balanceLine("NOVEL", null, null, NOVEL); const unknown = balanceLine("NOVEL", null, null, NOVEL);
const zero = balanceLine("NOVEL", "0", null, NOVEL); const zero = balanceLine("NOVEL", 0, null, NOVEL);
expect(unknown).not.toBe(zero); expect(unknown).not.toBe(zero);
expect(quantities(unknown)).toEqual(["NOVEL", "quantity unknown"]); expect(quantities(unknown)).toEqual(["NOVEL", "quantity unknown"]);
expect(quantities(zero)).toEqual(["NOVEL", "0.0000"]); expect(quantities(zero)).toEqual(["NOVEL", "0.0000"]);
@@ -105,7 +105,7 @@ describe("balanceLine", () => {
test("an unknown quantity produces no fiat figure, a zero one does", () => { test("an unknown quantity produces no fiat figure, a zero one does", () => {
prices[NOVEL] = 3; prices[NOVEL] = 3;
const unknown = balanceLine("NOVEL", null, 3, NOVEL); const unknown = balanceLine("NOVEL", null, 3, NOVEL);
const zero = balanceLine("NOVEL", "0", 3, NOVEL); const zero = balanceLine("NOVEL", 0, 3, NOVEL);
// A price times an unknown quantity is not $0.00: that is the same // A price times an unknown quantity is not $0.00: that is the same
// claim of "nothing here" the quantity cell just refused to make. // claim of "nothing here" the quantity cell just refused to make.
expect(unknown).toContain( expect(unknown).toContain(