Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb7c75101f |
No files matched your search
@@ -1191,23 +1191,15 @@ 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
|
||||
cut amounts to four decimals and never round up (`truncateAmount()` in
|
||||
`src/shared/amountDisplay.js`). These balances are cut the same way:
|
||||
|
||||
- the balance lists;
|
||||
- Home's ETH balance;
|
||||
- Send's current balance;
|
||||
- ConfirmTx's balance line.
|
||||
|
||||
So a balance below 0.0001 reads `0.0000` there.
|
||||
|
||||
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
|
||||
`src/shared/amountDisplay.js`). The balance lists and Home's ETH balance cut
|
||||
them the same way. 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
|
||||
(`truncateAmountNeverZero()`). So a nonzero amount never reads as zero there.
|
||||
Those screens are:
|
||||
|
||||
- TxApproval;
|
||||
- SignApproval's Permit warning;
|
||||
- ConfirmTx's amount and network fee;
|
||||
- ConfirmTx, and Send's current balance;
|
||||
- WaitTx, SuccessTx and ErrorTx.
|
||||
|
||||
TransactionDetail shows the exact amount and its base units.
|
||||
@@ -1655,10 +1647,11 @@ The development dependencies ship nowhere:
|
||||
|
||||
### Crypto policy
|
||||
|
||||
In the extension, hashing, key derivation, signing and encryption go through
|
||||
`ethers` or `libsodium-wrappers-sumo`. A new recovery phrase takes its random
|
||||
bytes from `ethers`. `build.js` and `script/lib/package.js` also hash files with
|
||||
`ethers`. No primitive is implemented in this repository.
|
||||
Hashing, key derivation, signing and encryption go through `ethers` or
|
||||
`libsodium-wrappers-sumo`. No primitive is implemented in this repository. A new
|
||||
recovery phrase draws its random bytes from the browser's
|
||||
`crypto.getRandomValues()`. The build tooling, `build.js` and `script/lib/`,
|
||||
hashes files with Node's `crypto`; none of it ships in the extension.
|
||||
|
||||
### Content Security Policy
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
const { execSync } = require("child_process");
|
||||
const esbuild = require("esbuild");
|
||||
const { sha256 } = require("ethers");
|
||||
const { resolveVersion } = require("./script/lib/version");
|
||||
const {
|
||||
BACKGROUND_ENTRY_PREFIX,
|
||||
@@ -314,8 +314,10 @@ function copyIcons(distDir) {
|
||||
}
|
||||
|
||||
function sha256File(absPath) {
|
||||
// ethers prefixes the hex with 0x; the receipt holds bare hex.
|
||||
return sha256(fs.readFileSync(absPath)).slice(2);
|
||||
return crypto
|
||||
.createHash("sha256")
|
||||
.update(fs.readFileSync(absPath))
|
||||
.digest("hex");
|
||||
}
|
||||
|
||||
// Write the receipt for the files this build emitted. Deliberately records no
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ethers = require("ethers");
|
||||
|
||||
const { readZip, writeZip } = require("./zip");
|
||||
const { resolveVersion } = require("./version");
|
||||
@@ -60,9 +60,8 @@ function fail(message) {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
// Lowercase hex without the 0x prefix ethers adds, as sha256sum prints it.
|
||||
function sha256(buf) {
|
||||
return ethers.sha256(buf).slice(2);
|
||||
return crypto.createHash("sha256").update(buf).digest("hex");
|
||||
}
|
||||
|
||||
// Every regular file under dir, as archive-root-relative forward-slashed
|
||||
@@ -292,9 +291,4 @@ if (require.main === module) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkSelfContained,
|
||||
htmlReferences,
|
||||
manifestReferences,
|
||||
sha256,
|
||||
};
|
||||
module.exports = { checkSelfContained, htmlReferences, manifestReferences };
|
||||
@@ -43,12 +43,9 @@ const {
|
||||
transferAmountUnits,
|
||||
} = require("../../shared/transferAmount");
|
||||
const { assertWithinCeilings } = require("../../shared/approvalVerify");
|
||||
// The amount and the fee reserve keep the nonzero floor, as the approval
|
||||
// screen's amounts do. The balance line is cut as the balance lists cut it.
|
||||
const {
|
||||
truncateAmount,
|
||||
truncateAmountNeverZero,
|
||||
} = require("../../shared/amountDisplay");
|
||||
// The amount, the balance line and the fee reserve go through it, as the
|
||||
// approval screen's amounts do.
|
||||
const { truncateAmountNeverZero } = require("../../shared/amountDisplay");
|
||||
const {
|
||||
CODES,
|
||||
FEE_PENDING,
|
||||
@@ -163,7 +160,7 @@ function renderBalance(txInfo) {
|
||||
const price = getPrice(txInfo.token, txInfo.chainId);
|
||||
const balUsd = price ? parseFloat(bal) * price : null;
|
||||
$("confirm-balance").textContent = valueWithUsd(
|
||||
truncateAmount(bal) + " " + symbol,
|
||||
truncateAmountNeverZero(bal) + " " + symbol,
|
||||
balUsd,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -378,8 +378,8 @@ function formatFee(wei, symbol, chainId) {
|
||||
// whose scale nothing knows
|
||||
// (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
|
||||
// derive from one. A holding whose amount is unknown is shown as
|
||||
// `quantity unknown` rather than as a zero.
|
||||
// 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.
|
||||
function balanceLine(symbol, balance, price, tokenId) {
|
||||
const amount = unknownableAmount(balance);
|
||||
const qty = amount === null ? "quantity unknown" : truncateAmount(balance);
|
||||
|
||||
@@ -24,7 +24,7 @@ const { resolveSymbol } = require("../../shared/tokenList");
|
||||
const { isLowHolderCount } = require("../../shared/holders");
|
||||
const { isSpoofedSymbol } = require("../../shared/symbolSpoof");
|
||||
const {
|
||||
truncateAmount,
|
||||
truncateAmountNeverZero,
|
||||
isBelowOneMillionth,
|
||||
} = require("../../shared/amountDisplay");
|
||||
const {
|
||||
@@ -239,7 +239,7 @@ function updateSendBalance() {
|
||||
addr.balance == null
|
||||
? "Current balance: unknown (" + nativeCurrency() + ")"
|
||||
: "Current balance: " +
|
||||
truncateAmount(addr.balance) +
|
||||
truncateAmountNeverZero(addr.balance) +
|
||||
" " +
|
||||
nativeCurrency();
|
||||
} else {
|
||||
@@ -258,15 +258,18 @@ function updateSendBalance() {
|
||||
$("send-balance").textContent =
|
||||
bal == null
|
||||
? "Current balance: unknown (" + symbol + ")"
|
||||
: "Current balance: " + truncateAmount(bal) + " " + symbol;
|
||||
: "Current balance: " +
|
||||
truncateAmountNeverZero(bal) +
|
||||
" " +
|
||||
symbol;
|
||||
}
|
||||
}
|
||||
|
||||
// Fill the amount field with the most the selected holding can send: a
|
||||
// token's whole balance (cut to 18 decimal places), or for ETH the exact
|
||||
// balance minus the fee reserve the confirmation screen checks against, never
|
||||
// the balance the screen shows, which is cut to four decimals. Where there is
|
||||
// nothing to fill in, a flash message says why.
|
||||
// the rounded balance the screen shows. Where there is nothing to fill in, a
|
||||
// flash message says why.
|
||||
async function fillMaxAmount() {
|
||||
const addr = currentAddress();
|
||||
if (!addr) return;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Wallet operations: mnemonic generation, HD derivation, signing.
|
||||
// All crypto delegated to ethers.js.
|
||||
|
||||
const { Mnemonic, HDNodeWallet, Wallet, randomBytes } = require("ethers");
|
||||
const { Mnemonic, HDNodeWallet, Wallet } = require("ethers");
|
||||
const { DEBUG, DEBUG_MNEMONIC, BIP44_ETH_PATH } = require("./constants");
|
||||
|
||||
function generateMnemonic() {
|
||||
@@ -10,7 +10,10 @@ function generateMnemonic() {
|
||||
// settings toggle drives, which would let a user of a release build turn
|
||||
// the hardcoded, publicly known test phrase back on for real wallets.
|
||||
if (DEBUG) return DEBUG_MNEMONIC;
|
||||
return Mnemonic.fromEntropy(randomBytes(16)).phrase;
|
||||
const m = Mnemonic.fromEntropy(
|
||||
globalThis.crypto.getRandomValues(new Uint8Array(16)),
|
||||
);
|
||||
return m.phrase;
|
||||
}
|
||||
|
||||
// Every extended key (xprv or xpub) entering the app goes through this.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// The balance lists, Home's ETH line and Send's current balance 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).
|
||||
// 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.
|
||||
|
||||
@@ -75,17 +75,3 @@ describe("Home", () => {
|
||||
expect(quantity(homeRow(page, USDC.toLowerCase()))).toBe("0.0000");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Send", () => {
|
||||
test("the current balance reads 0.00004 USDC as 0.0000", async () => {
|
||||
const page = await bootHolding("0.99996", "0.00004");
|
||||
await page.click("btn-main-send");
|
||||
// USDC chosen in the dropdown, as the user does.
|
||||
const dropdown = page.node("send-token");
|
||||
dropdown.value = USDC;
|
||||
for (const fn of dropdown.listeners.change) await fn();
|
||||
|
||||
expect(page.visibleViews()).toEqual(["send"]);
|
||||
expect(page.text("send-balance")).toBe("Current balance: 0.0000 USDC");
|
||||
});
|
||||
});
|
||||
@@ -165,9 +165,9 @@ describe("the balance warning on the removal confirmation", () => {
|
||||
});
|
||||
|
||||
// The sentence must not assert an amount, because any amount it could
|
||||
// assert has been cut to four decimals: "This address holds 0.0000 ETH." is
|
||||
// what the cut form produces for an address that holds real money.
|
||||
test("the warning sentence asserts no cut amount", () => {
|
||||
// assert has been rounded: "This address holds 0.0000 ETH." is what the
|
||||
// rounded form produces for an address that holds real money.
|
||||
test("the warning sentence asserts no rounded amount", () => {
|
||||
for (const addr of [DUST, ETH_ONLY, TOKEN_ONLY]) {
|
||||
expect(balanceWarningHtml(addr)).not.toMatch(
|
||||
/holds [\d.]+ (ETH|USDC)/,
|
||||
|
||||
+2
-13
@@ -1,5 +1,4 @@
|
||||
// The release archives: the zip container, the self-containment rule, and the
|
||||
// digest format in release/SHA256SUMS.
|
||||
// The release archives: the zip container, and the self-containment rule.
|
||||
//
|
||||
// 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:
|
||||
@@ -12,14 +11,12 @@
|
||||
// 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
|
||||
// cases for that, plus the archive format itself — new code, and the thing the
|
||||
// artifact is made of. One more case checks the digest format in
|
||||
// release/SHA256SUMS.
|
||||
// artifact is made of.
|
||||
|
||||
const {
|
||||
checkSelfContained,
|
||||
htmlReferences,
|
||||
manifestReferences,
|
||||
sha256,
|
||||
} = require("../script/lib/package");
|
||||
const { readZip, writeZip } = require("../script/lib/zip");
|
||||
|
||||
@@ -145,14 +142,6 @@ 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", () => {
|
||||
const files = [
|
||||
{ name: "manifest.json", data: Buffer.from('{"a":1}') },
|
||||
|
||||
@@ -6,10 +6,8 @@
|
||||
// balances and the fee were each cut to six decimal places, a rule of their
|
||||
// own, and a token holding cut to zero was dropped, while the approval screen
|
||||
// showed the same fee through src/shared/amountDisplay.js with the nonzero
|
||||
// floor. The balances are now stored exactly, and every nonzero token holding
|
||||
// is kept. The fee lines show the fee through that helper with the floor. The
|
||||
// balance lines cut a balance to four decimals, as the balance lists do, so a
|
||||
// balance below 0.0001 reads 0.0000.
|
||||
// floor. The balances are now stored exactly, every nonzero token holding
|
||||
// kept, and the screens show them and the fee through that helper.
|
||||
//
|
||||
// Driven through the real refreshBalances(), Send screen, confirmation screen
|
||||
// and approval screen, with only the node, the explorer and the DOM stubbed: a
|
||||
@@ -280,22 +278,21 @@ beforeEach(() => {
|
||||
mockNode.feeData = { maxFeePerGas: 1n, gasPrice: 1n };
|
||||
});
|
||||
|
||||
describe("an ETH balance below 0.0001 reads 0.0000", () => {
|
||||
describe("an ETH balance below 0.000001 never renders as zero", () => {
|
||||
test("on the Send screen", async () => {
|
||||
await refreshWith(HALF_MICRO_ETH);
|
||||
state.selectedToken = "ETH";
|
||||
send.updateSendBalance();
|
||||
expect(text("send-balance")).toBe("Current balance: 0.0000 ETH");
|
||||
expect(state.wallets[0].addresses[0].balance).toBe("0.0000005");
|
||||
expect(text("send-balance")).toBe("Current balance: 0.0000005 ETH");
|
||||
});
|
||||
|
||||
test("on the confirmation screen", async () => {
|
||||
await refreshWith(HALF_MICRO_ETH);
|
||||
await confirmSend("0.0000001");
|
||||
expect(text("confirm-balance")).toBe("0.0000 ETH");
|
||||
expect(text("confirm-balance")).toBe("0.0000005 ETH");
|
||||
});
|
||||
|
||||
test("while a larger balance is cut to four decimals", async () => {
|
||||
test("while a balance above the floor keeps four decimals", async () => {
|
||||
await refreshWith(1234567890000000000n);
|
||||
await confirmSend("0.1");
|
||||
expect(text("confirm-balance")).toBe("1.2345 ETH");
|
||||
@@ -305,7 +302,7 @@ describe("an ETH balance below 0.0001 reads 0.0000", () => {
|
||||
// A token the user tracks stays on the balance list when the explorer's row is
|
||||
// dropped, so a holding of it below 0.000001 reached these screens as zero, and
|
||||
// the send was checked against zero.
|
||||
describe("a tracked token holding below 0.000001 reads 0.0000", () => {
|
||||
describe("a tracked token holding below 0.000001 never renders as zero", () => {
|
||||
beforeEach(() => {
|
||||
state.trackedTokens = [
|
||||
{ address: TOKEN, symbol: "TOK", name: "Token", decimals: 18 },
|
||||
@@ -316,17 +313,13 @@ describe("a tracked token holding below 0.000001 reads 0.0000", () => {
|
||||
await refreshWith(10n ** 18n, [tokenRow(HALF_MICRO_ETH)]);
|
||||
state.selectedToken = TOKEN;
|
||||
send.updateSendBalance();
|
||||
expect(text("send-balance")).toBe("Current balance: 0.0000 TOK");
|
||||
const stored = state.wallets[0].addresses[0].tokenBalances.find(
|
||||
(t) => t.address === TOKEN,
|
||||
);
|
||||
expect(stored.balance).toBe("0.0000005");
|
||||
expect(text("send-balance")).toBe("Current balance: 0.0000005 TOK");
|
||||
});
|
||||
|
||||
test("on the confirmation screen, which checks the send against it", async () => {
|
||||
await refreshWith(10n ** 18n, [tokenRow(HALF_MICRO_ETH)]);
|
||||
await confirmSend("0.0000005", TOKEN);
|
||||
expect(text("confirm-balance")).toBe("0.0000 TOK");
|
||||
expect(text("confirm-balance")).toBe("0.0000005 TOK");
|
||||
expect(errors()).toBe("");
|
||||
expect(balanceError()).toBe("");
|
||||
await confirmSend("0.0000006", TOKEN);
|
||||
@@ -354,9 +347,11 @@ describe("a tracked token holding below 0.000001 reads 0.0000", () => {
|
||||
await refreshWith(10n ** 18n, [tokenRow(1n, { decimals: "24" })]);
|
||||
state.selectedToken = TOKEN;
|
||||
send.updateSendBalance();
|
||||
expect(text("send-balance")).toBe("Current balance: 0.0000 TOK");
|
||||
expect(text("send-balance")).toBe(
|
||||
"Current balance: 0.000000000000000000000001 TOK",
|
||||
);
|
||||
await confirmSend("0.000000000000000001", TOKEN);
|
||||
expect(text("confirm-balance")).toBe("0.0000 TOK");
|
||||
expect(text("confirm-balance")).toBe("0.000000000000000000000001 TOK");
|
||||
expect(balanceError()).toContain(
|
||||
"Your TOK balance does not cover this amount.",
|
||||
);
|
||||
@@ -368,20 +363,20 @@ describe("a tracked token holding below 0.000001 reads 0.0000", () => {
|
||||
// selected while its own screen is open: after sending 2 of a 2.0000003
|
||||
// holding, the user is back on that screen, and Send read the missing row as
|
||||
// zero.
|
||||
describe("an untracked token holding below 0.000001 reads 0.0000", () => {
|
||||
describe("an untracked token holding below 0.000001 never renders as zero", () => {
|
||||
const row = () => tokenRow(HALF_MICRO_ETH, { holders_count: "50000" });
|
||||
|
||||
test("on the Send screen", async () => {
|
||||
await refreshWith(10n ** 18n, [row()]);
|
||||
state.selectedToken = TOKEN;
|
||||
send.updateSendBalance();
|
||||
expect(text("send-balance")).toBe("Current balance: 0.0000 TOK");
|
||||
expect(text("send-balance")).toBe("Current balance: 0.0000005 TOK");
|
||||
});
|
||||
|
||||
test("on the confirmation screen, which checks the send against it", async () => {
|
||||
await refreshWith(10n ** 18n, [row()]);
|
||||
await confirmSend("0.0000005", TOKEN);
|
||||
expect(text("confirm-balance")).toBe("0.0000 TOK");
|
||||
expect(text("confirm-balance")).toBe("0.0000005 TOK");
|
||||
expect(errors()).toBe("");
|
||||
expect(balanceError()).toBe("");
|
||||
await confirmSend("0.0000006", TOKEN);
|
||||
|
||||
Reference in new issue
Block a user