Compare commits
1
Commits
next
...
1df0ebb493
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1df0ebb493 |
@@ -889,11 +889,16 @@ shows as `0.9999`, never rounded up.
|
|||||||
The rule and its exception live in `src/shared/amountDisplay.js` as
|
The rule and its exception live in `src/shared/amountDisplay.js` as
|
||||||
`truncateAmount()` and `truncateAmountNeverZero()`. Everything the approval and
|
`truncateAmount()` and `truncateAmountNeverZero()`. Everything the approval and
|
||||||
confirmation screens display goes through the floored one — the ERC-20 amount,
|
confirmation screens display goes through the floored one — the ERC-20 amount,
|
||||||
the ETH value and max fee (`src/popup/views/approval.js`), and the swap's
|
the ETH value and max fee (`src/popup/views/approval.js`), the swap's `Amount`
|
||||||
`Amount` and `Min. received` lines (`src/shared/uniswap.js`). The history and
|
and `Min. received` lines (`src/shared/uniswap.js`), the Send screen's
|
||||||
balance lists (`src/shared/transactions.js`) use the unfloored one: the
|
`Current balance` (`src/popup/views/send.js`), and the balance and network fee
|
||||||
transaction detail view is the authoritative record and already shows exact
|
on the confirmation screen for the wallet's own send
|
||||||
precision. The 4-decimal rule is unchanged everywhere else, including for
|
(`src/popup/views/confirmTx.js`), so a fee reads the same there as on the
|
||||||
|
approval screen. For this the ETH balance is stored exactly. A token balance is
|
||||||
|
stored to six decimal places, and a holding below 0.000001 is not listed at all.
|
||||||
|
The history and balance lists (`src/shared/transactions.js`) use the unfloored
|
||||||
|
one: the transaction detail view is the authoritative record and already shows
|
||||||
|
exact precision. The 4-decimal rule is unchanged everywhere else, including for
|
||||||
amounts at or above the floor on the approval screens.
|
amounts at or above the floor on the approval screens.
|
||||||
|
|
||||||
The floor applies only where the token's scale is known. Where it is not, the
|
The floor applies only where the token's scale is known. Where it is not, the
|
||||||
|
|||||||
@@ -45,6 +45,18 @@ but the review is broader than any of them.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-04: The Send and confirmation screens no longer show an ETH balance or
|
||||||
|
a network fee below 0.000001 as `0.0`
|
||||||
|
([#343](https://git.eeqj.de/sneak/AutistMask/issues/343)). The stored ETH
|
||||||
|
balance (`src/shared/balances.js`) and the confirmation screen's fee were each
|
||||||
|
cut to six decimal places by a rule of their own. The ETH balance is now
|
||||||
|
stored exactly, and the Send screen's `Current balance`, and the confirmation
|
||||||
|
screen's balance, fee, reserve and insufficient-balance messages, go through
|
||||||
|
`truncateAmountNeverZero()` in `src/shared/amountDisplay.js`, the helper the
|
||||||
|
approval screen already used, so the same fee reads the same on both screens.
|
||||||
|
Token balances are still stored to six decimal places: that cut is also what
|
||||||
|
leaves a holding below 0.000001 off the balance list, and keeps the stored
|
||||||
|
string within the 18 decimals the balance check reads.
|
||||||
- 2026-10-03: The typed-data signing screen warns for a token permission, and
|
- 2026-10-03: The typed-data signing screen warns for a token permission, and
|
||||||
names the primary type ethers signs
|
names the primary type ethers signs
|
||||||
([#400](https://git.eeqj.de/sneak/AutistMask/issues/400)). A Permit or Permit2
|
([#400](https://git.eeqj.de/sneak/AutistMask/issues/400)). A Permit or Permit2
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ const {
|
|||||||
transferAmountUnits,
|
transferAmountUnits,
|
||||||
} = require("../../shared/transferAmount");
|
} = require("../../shared/transferAmount");
|
||||||
const { assertWithinCeilings } = require("../../shared/approvalVerify");
|
const { assertWithinCeilings } = require("../../shared/approvalVerify");
|
||||||
|
// Every balance and fee this screen shows goes through it, as the approval
|
||||||
|
// screen's do, so the same value reads the same on both.
|
||||||
|
const { truncateAmountNeverZero } = require("../../shared/amountDisplay");
|
||||||
const {
|
const {
|
||||||
CODES,
|
CODES,
|
||||||
FEE_PENDING,
|
FEE_PENDING,
|
||||||
@@ -150,11 +153,17 @@ function show(txInfo) {
|
|||||||
$("confirm-balance").textContent =
|
$("confirm-balance").textContent =
|
||||||
bal == null
|
bal == null
|
||||||
? "unknown (" + symbol + ")"
|
? "unknown (" + symbol + ")"
|
||||||
: valueWithUsd(bal + " " + symbol, balUsd);
|
: valueWithUsd(
|
||||||
|
truncateAmountNeverZero(bal) + " " + symbol,
|
||||||
|
balUsd,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const bal = txInfo.balance || "0";
|
const bal = txInfo.balance || "0";
|
||||||
const balUsd = ethPrice ? parseFloat(bal) * ethPrice : null;
|
const balUsd = ethPrice ? parseFloat(bal) * ethPrice : null;
|
||||||
$("confirm-balance").textContent = valueWithUsd(bal + " ETH", balUsd);
|
$("confirm-balance").textContent = valueWithUsd(
|
||||||
|
truncateAmountNeverZero(bal) + " ETH",
|
||||||
|
balUsd,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for warnings (synchronous local checks)
|
// Check for warnings (synchronous local checks)
|
||||||
@@ -249,7 +258,7 @@ function renderValidation(txInfo) {
|
|||||||
: "Insufficient " +
|
: "Insufficient " +
|
||||||
symbol +
|
symbol +
|
||||||
" balance. You have " +
|
" balance. You have " +
|
||||||
txInfo.tokenBalance +
|
truncateAmountNeverZero(txInfo.tokenBalance) +
|
||||||
" " +
|
" " +
|
||||||
symbol +
|
symbol +
|
||||||
" but are trying to send " +
|
" but are trying to send " +
|
||||||
@@ -262,7 +271,7 @@ function renderValidation(txInfo) {
|
|||||||
if (codes.includes(CODES.INSUFFICIENT_ETH)) {
|
if (codes.includes(CODES.INSUFFICIENT_ETH)) {
|
||||||
messages.push(
|
messages.push(
|
||||||
"Insufficient balance. You have " +
|
"Insufficient balance. You have " +
|
||||||
txInfo.balance +
|
truncateAmountNeverZero(txInfo.balance || "0") +
|
||||||
" ETH but are trying to send " +
|
" ETH but are trying to send " +
|
||||||
txInfo.amount +
|
txInfo.amount +
|
||||||
" ETH.",
|
" ETH.",
|
||||||
@@ -305,14 +314,6 @@ function setVisible(id, visible) {
|
|||||||
$(id).style.visibility = visible ? "visible" : "hidden";
|
$(id).style.visibility = visible ? "visible" : "hidden";
|
||||||
}
|
}
|
||||||
|
|
||||||
// A fee in wei as an ETH string, truncated to 6 decimal places.
|
|
||||||
function formatFeeEth(wei) {
|
|
||||||
const parts = formatEther(wei).split(".");
|
|
||||||
const dec =
|
|
||||||
parts.length > 1 ? parts[1].slice(0, 6).replace(/0+$/, "") || "0" : "0";
|
|
||||||
return parts[0] + "." + dec + " ETH";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function estimateGas(txInfo) {
|
async function estimateGas(txInfo) {
|
||||||
try {
|
try {
|
||||||
const provider = getProvider(state.rpcUrl, state.networkId);
|
const provider = getProvider(state.rpcUrl, state.networkId);
|
||||||
@@ -365,18 +366,22 @@ async function estimateGas(txInfo) {
|
|||||||
|
|
||||||
if (estimateWei !== null && estimateWei < gasCostWei) {
|
if (estimateWei !== null && estimateWei < gasCostWei) {
|
||||||
$("confirm-fee-amount").textContent = valueWithUsd(
|
$("confirm-fee-amount").textContent = valueWithUsd(
|
||||||
"~" + formatFeeEth(estimateWei),
|
"~" +
|
||||||
|
truncateAmountNeverZero(formatEther(estimateWei)) +
|
||||||
|
" ETH",
|
||||||
usd(estimateWei),
|
usd(estimateWei),
|
||||||
);
|
);
|
||||||
$("confirm-fee-reserve").textContent =
|
$("confirm-fee-reserve").textContent =
|
||||||
"up to " + formatFeeEth(gasCostWei) + " reserved";
|
"up to " +
|
||||||
|
truncateAmountNeverZero(formatEther(gasCostWei)) +
|
||||||
|
" ETH reserved";
|
||||||
setVisible("confirm-fee-reserve", true);
|
setVisible("confirm-fee-reserve", true);
|
||||||
} else {
|
} else {
|
||||||
// No spread to report: either there is no estimate, or the node
|
// No spread to report: either there is no estimate, or the node
|
||||||
// quotes a gas price at or above maxFeePerGas, so the expected
|
// quotes a gas price at or above maxFeePerGas, so the expected
|
||||||
// cost is not below the reserve. Show the reserve alone.
|
// cost is not below the reserve. Show the reserve alone.
|
||||||
$("confirm-fee-amount").textContent = valueWithUsd(
|
$("confirm-fee-amount").textContent = valueWithUsd(
|
||||||
formatFeeEth(gasCostWei),
|
truncateAmountNeverZero(formatEther(gasCostWei)) + " ETH",
|
||||||
usd(gasCostWei),
|
usd(gasCostWei),
|
||||||
);
|
);
|
||||||
setVisible("confirm-fee-reserve", false);
|
setVisible("confirm-fee-reserve", false);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const { resolveTokenDecimals } = require("../../shared/approvalAmount");
|
|||||||
const { resolveSymbol } = require("../../shared/tokenList");
|
const { resolveSymbol } = require("../../shared/tokenList");
|
||||||
const { isLowHolderCount } = require("../../shared/holders");
|
const { isLowHolderCount } = require("../../shared/holders");
|
||||||
const { isSpoofedSymbol } = require("../../shared/symbolSpoof");
|
const { isSpoofedSymbol } = require("../../shared/symbolSpoof");
|
||||||
|
const { truncateAmountNeverZero } = require("../../shared/amountDisplay");
|
||||||
const { getAddress } = require("ethers");
|
const { getAddress } = require("ethers");
|
||||||
|
|
||||||
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
||||||
@@ -150,7 +151,9 @@ function updateSendBalance() {
|
|||||||
const token = state.selectedToken || $("send-token").value;
|
const token = state.selectedToken || $("send-token").value;
|
||||||
if (token === "ETH") {
|
if (token === "ETH") {
|
||||||
$("send-balance").textContent =
|
$("send-balance").textContent =
|
||||||
"Current balance: " + (addr.balance || "0") + " ETH";
|
"Current balance: " +
|
||||||
|
truncateAmountNeverZero(addr.balance || "0") +
|
||||||
|
" ETH";
|
||||||
} else {
|
} else {
|
||||||
const tb = (addr.tokenBalances || []).find(
|
const tb = (addr.tokenBalances || []).find(
|
||||||
(t) => t.address.toLowerCase() === token.toLowerCase(),
|
(t) => t.address.toLowerCase() === token.toLowerCase(),
|
||||||
@@ -167,7 +170,10 @@ function updateSendBalance() {
|
|||||||
$("send-balance").textContent =
|
$("send-balance").textContent =
|
||||||
bal == null
|
bal == null
|
||||||
? "Current balance: unknown (" + symbol + ")"
|
? "Current balance: unknown (" + symbol + ")"
|
||||||
: "Current balance: " + bal + " " + symbol;
|
: "Current balance: " +
|
||||||
|
truncateAmountNeverZero(bal) +
|
||||||
|
" " +
|
||||||
|
symbol;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-9
@@ -52,14 +52,12 @@ function requireNetworkId(networkId) {
|
|||||||
return net;
|
return net;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBalance(wei) {
|
// A token balance cut to six decimal places. Two things rely on the cut: a
|
||||||
const eth = formatEther(wei);
|
// holding below 0.000001 comes out as "0.0" and is left off the balance list as
|
||||||
const parts = eth.split(".");
|
// dust, and the stored string never carries more decimals than the balance
|
||||||
if (parts.length === 1) return eth + ".0";
|
// check on the confirmation screen can read (18, in txValidation.js), whatever
|
||||||
const dec = parts[1].slice(0, 6).replace(/0+$/, "") || "0";
|
// scale the token declares. Screens truncate it again for display, through
|
||||||
return parts[0] + "." + dec;
|
// src/shared/amountDisplay.js.
|
||||||
}
|
|
||||||
|
|
||||||
function formatTokenBalance(raw, decimals) {
|
function formatTokenBalance(raw, decimals) {
|
||||||
const val = formatUnits(raw, decimals);
|
const val = formatUnits(raw, decimals);
|
||||||
const parts = val.split(".");
|
const parts = val.split(".");
|
||||||
@@ -221,7 +219,10 @@ async function refreshBalances(
|
|||||||
provider
|
provider
|
||||||
.getBalance(addr.address)
|
.getBalance(addr.address)
|
||||||
.then((bal) => {
|
.then((bal) => {
|
||||||
addr.balance = formatBalance(bal);
|
// Exact, never cut: the screens truncate for display
|
||||||
|
// themselves, and a cut here stores a small nonzero
|
||||||
|
// balance as zero.
|
||||||
|
addr.balance = formatEther(bal);
|
||||||
log.debugf("ETH balance", addr.address, addr.balance);
|
log.debugf("ETH balance", addr.address, addr.balance);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
+12
-12
@@ -1414,9 +1414,10 @@ test("a rejected dust threshold shifts no layout (#233)", async (env) => {
|
|||||||
// on opposite sides of the reserve while sitting on the same side of the
|
// on opposite sides of the reserve while sitting on the same side of the
|
||||||
// estimate.
|
// estimate.
|
||||||
|
|
||||||
// The balance the funded fixture serves, and the amounts sent against it.
|
// The balance the funded fixture serves, as the Send and confirmation screens
|
||||||
|
// show it, and the amounts sent against it.
|
||||||
const FUNDED_ETH_WEI = 10n ** 18n;
|
const FUNDED_ETH_WEI = 10n ** 18n;
|
||||||
const FUNDED_ETH_TEXT = "1.0";
|
const FUNDED_ETH_TEXT = "1.0000";
|
||||||
const COMFORTABLE_AMOUNT = "0.1";
|
const COMFORTABLE_AMOUNT = "0.1";
|
||||||
const OVER_BALANCE_AMOUNT = "2.0";
|
const OVER_BALANCE_AMOUNT = "2.0";
|
||||||
|
|
||||||
@@ -1430,7 +1431,7 @@ const GAP_AMOUNT = formatEther(FUNDED_ETH_WEI - FEE_ESTIMATE_WEI);
|
|||||||
// fee test: it covers the expected cost to the wei and falls short of the
|
// fee test: it covers the expected cost to the wei and falls short of the
|
||||||
// reserve, so the same swap flips this assertion too — through a different
|
// reserve, so the same swap flips this assertion too — through a different
|
||||||
// balance and a different message than the ETH path uses.
|
// balance and a different message than the ETH path uses.
|
||||||
const TOKEN_BALANCE_TEXT = "1.5";
|
const TOKEN_BALANCE_TEXT = "1.5000";
|
||||||
const TOKEN_AMOUNT = "0.25";
|
const TOKEN_AMOUNT = "0.25";
|
||||||
const OVER_TOKEN_AMOUNT = "9.0";
|
const OVER_TOKEN_AMOUNT = "9.0";
|
||||||
const FEE_ONLY_ETH_WEI = FEE_ESTIMATE_WEI;
|
const FEE_ONLY_ETH_WEI = FEE_ESTIMATE_WEI;
|
||||||
@@ -1439,16 +1440,15 @@ function toHexWei(wei) {
|
|||||||
return "0x" + wei.toString(16);
|
return "0x" + wei.toString(16);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A fee in wei as the confirmation screen writes it. Deliberately a second
|
// A fee in wei as the confirmation screen writes it: truncated to four decimal
|
||||||
// implementation of formatFeeEth() from src/popup/views/confirmTx.js rather
|
// places (README.md, Display Consistency). Deliberately a second
|
||||||
// than an import of it: that module pulls in the whole popup and cannot be
|
// implementation rather than an import of src/shared/amountDisplay.js:
|
||||||
// required outside a browser, and asserting against an independent rendering
|
// asserting against an independent rendering is stronger than asserting a
|
||||||
// is stronger than asserting a function equals itself.
|
// function equals itself. The fixture's fees are above 0.0001 ETH, so the
|
||||||
|
// nonzero floor never applies here.
|
||||||
function feeEth(wei) {
|
function feeEth(wei) {
|
||||||
const parts = formatEther(wei).split(".");
|
const [whole, frac = ""] = formatEther(wei).split(".");
|
||||||
const dec =
|
return whole + "." + (frac + "0000").slice(0, 4) + " ETH";
|
||||||
parts.length > 1 ? parts[1].slice(0, 6).replace(/0+$/, "") || "0" : "0";
|
|
||||||
return parts[0] + "." + dec + " ETH";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// What the confirmation screen is showing right now, read out of the DOM in
|
// What the confirmation screen is showing right now, read out of the DOM in
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
// The balance and fee lines of the Send and confirmation screens, and the fee
|
||||||
|
// line they must share with the approval screen.
|
||||||
|
//
|
||||||
|
// An ETH balance or a fee below 0.000001 rendered as `0.0` on these screens
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/343): the stored balance and the
|
||||||
|
// fee were each cut to six decimal places, a rule of their own, while the
|
||||||
|
// approval screen showed the same fee through src/shared/amountDisplay.js with
|
||||||
|
// the nonzero floor. Both now go through that one helper.
|
||||||
|
//
|
||||||
|
// Driven through the real refreshBalances(), Send screen, confirmation screen
|
||||||
|
// and approval screen, with only the node and the DOM stubbed: a balance
|
||||||
|
// written onto state by hand would skip the place the cut happened.
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// What the stub node answers. Each test sets what it needs.
|
||||||
|
const mockNode = {
|
||||||
|
balanceWei: 0n,
|
||||||
|
feeData: { maxFeePerGas: 1n, gasPrice: 1n },
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock("ethers", () => {
|
||||||
|
const actual = jest.requireActual("ethers");
|
||||||
|
class StubProvider {
|
||||||
|
async getBalance() {
|
||||||
|
return mockNode.balanceWei;
|
||||||
|
}
|
||||||
|
async lookupAddress() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async getFeeData() {
|
||||||
|
return mockNode.feeData;
|
||||||
|
}
|
||||||
|
async estimateGas() {
|
||||||
|
return 21000n;
|
||||||
|
}
|
||||||
|
async getCode() {
|
||||||
|
return "0x";
|
||||||
|
}
|
||||||
|
async getTransactionCount() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
JsonRpcProvider: StubProvider,
|
||||||
|
Network: { from: () => ({}) },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock("../src/shared/log", () => ({
|
||||||
|
log: {
|
||||||
|
debugf: () => {},
|
||||||
|
infof: () => {},
|
||||||
|
warnf: () => {},
|
||||||
|
errorf: () => {},
|
||||||
|
},
|
||||||
|
// The explorer's token list, which refreshBalances() also fetches: empty.
|
||||||
|
debugFetch: jest.fn(async () => ({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => [],
|
||||||
|
})),
|
||||||
|
setRuntimeDebug: () => {},
|
||||||
|
isDebug: () => false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// The confirmation screen's Etherscan label lookup is the only fetch() these
|
||||||
|
// screens make; it fails, as it does offline.
|
||||||
|
global.fetch = jest.fn(() => {
|
||||||
|
throw new Error("tests must not perform network requests");
|
||||||
|
});
|
||||||
|
|
||||||
|
// The approval the background hands the approval screen. Set per test.
|
||||||
|
let approvalDetails = null;
|
||||||
|
|
||||||
|
const { makeStorageStub } = require("./support/storageStub");
|
||||||
|
global.chrome = {
|
||||||
|
storage: makeStorageStub(),
|
||||||
|
runtime: {
|
||||||
|
connect: () => ({
|
||||||
|
postMessage() {},
|
||||||
|
disconnect() {},
|
||||||
|
onDisconnect: { addListener() {} },
|
||||||
|
}),
|
||||||
|
sendMessage(message, callback) {
|
||||||
|
callback(
|
||||||
|
message.type === "AUTISTMASK_GET_APPROVAL"
|
||||||
|
? approvalDetails
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// A stub DOM: every id resolves to a recording element.
|
||||||
|
const elements = new Map();
|
||||||
|
|
||||||
|
function makeEl(id) {
|
||||||
|
const handlers = new Map();
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
textContent: "",
|
||||||
|
innerHTML: "",
|
||||||
|
value: "",
|
||||||
|
disabled: false,
|
||||||
|
style: {},
|
||||||
|
dataset: {},
|
||||||
|
classList: {
|
||||||
|
add() {},
|
||||||
|
remove() {},
|
||||||
|
toggle() {},
|
||||||
|
contains: () => false,
|
||||||
|
},
|
||||||
|
handlers,
|
||||||
|
addEventListener(name, fn) {
|
||||||
|
handlers.set(name, fn);
|
||||||
|
},
|
||||||
|
appendChild(child) {
|
||||||
|
return child;
|
||||||
|
},
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
querySelector: () => null,
|
||||||
|
remove() {},
|
||||||
|
focus() {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
global.document = {
|
||||||
|
getElementById(id) {
|
||||||
|
if (!elements.has(id)) elements.set(id, makeEl(id));
|
||||||
|
return elements.get(id);
|
||||||
|
},
|
||||||
|
createElement: (tag) => makeEl(tag),
|
||||||
|
body: { prepend() {}, appendChild() {} },
|
||||||
|
addEventListener() {},
|
||||||
|
};
|
||||||
|
global.navigator = { clipboard: { writeText() {} } };
|
||||||
|
|
||||||
|
const { refreshBalances } = require("../src/shared/balances");
|
||||||
|
const { state } = require("../src/shared/state");
|
||||||
|
const send = require("../src/popup/views/send");
|
||||||
|
const confirmTx = require("../src/popup/views/confirmTx");
|
||||||
|
const approval = require("../src/popup/views/approval");
|
||||||
|
|
||||||
|
const HOLDER = "0x" + "a".repeat(40);
|
||||||
|
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
||||||
|
|
||||||
|
// 0.0000005 ETH.
|
||||||
|
const HALF_MICRO_ETH = 500000000000n;
|
||||||
|
|
||||||
|
function text(id) {
|
||||||
|
return global.document.getElementById(id).textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ETH balance the node reports, fetched and stored exactly where the popup
|
||||||
|
// stores it.
|
||||||
|
async function refreshWith(balanceWei) {
|
||||||
|
mockNode.balanceWei = balanceWei;
|
||||||
|
state.wallets = [{ name: "Wallet 1", addresses: [{ address: HOLDER }] }];
|
||||||
|
state.selectedWallet = 0;
|
||||||
|
state.selectedAddress = 0;
|
||||||
|
await refreshBalances(
|
||||||
|
state.wallets,
|
||||||
|
"https://rpc.example.invalid",
|
||||||
|
"https://blockscout.example/api/v2",
|
||||||
|
[],
|
||||||
|
"mainnet",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Press Review on the Send screen for an ETH send, and show the confirmation
|
||||||
|
// screen it leads to with its fee estimate settled.
|
||||||
|
async function confirmEthSend(amount) {
|
||||||
|
let txInfo = null;
|
||||||
|
send.init({ showConfirmTx: (info) => (txInfo = info) });
|
||||||
|
state.selectedToken = "ETH";
|
||||||
|
global.document.getElementById("send-to").value = RECIPIENT;
|
||||||
|
global.document.getElementById("send-amount").value = amount;
|
||||||
|
await global.document
|
||||||
|
.getElementById("btn-send-review")
|
||||||
|
.handlers.get("click")();
|
||||||
|
confirmTx.show(txInfo);
|
||||||
|
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The approval screen for a dApp transaction of 21000 gas, the gas the stub
|
||||||
|
// node estimates for the send above.
|
||||||
|
async function approveTxWithFeePerGas(maxFeePerGas) {
|
||||||
|
approvalDetails = {
|
||||||
|
type: "tx",
|
||||||
|
hostname: "dapp.example",
|
||||||
|
approvedFrom: HOLDER,
|
||||||
|
approvedTx: {
|
||||||
|
to: RECIPIENT,
|
||||||
|
value: "0",
|
||||||
|
data: "0x",
|
||||||
|
chainId: "0x1",
|
||||||
|
gasLimit: "21000",
|
||||||
|
maxFeePerGas: String(maxFeePerGas),
|
||||||
|
nonce: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await approval.show("1");
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
elements.clear();
|
||||||
|
state.selectedToken = null;
|
||||||
|
state.trackedTokens = [];
|
||||||
|
state.fraudContracts = [];
|
||||||
|
state.currentView = null;
|
||||||
|
mockNode.feeData = { maxFeePerGas: 1n, gasPrice: 1n };
|
||||||
|
});
|
||||||
|
|
||||||
|
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.0000005 ETH");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("on the confirmation screen", async () => {
|
||||||
|
await refreshWith(HALF_MICRO_ETH);
|
||||||
|
await confirmEthSend("0.0000001");
|
||||||
|
expect(text("confirm-balance")).toBe("0.0000005 ETH");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("while a balance above the floor keeps four decimals", async () => {
|
||||||
|
await refreshWith(1234567890000000000n);
|
||||||
|
await confirmEthSend("0.1");
|
||||||
|
expect(text("confirm-balance")).toBe("1.2345 ETH");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("a fee below 0.000001 ETH never renders as zero", () => {
|
||||||
|
test("when the estimate and the reserve are the same", async () => {
|
||||||
|
await refreshWith(10n ** 18n);
|
||||||
|
await confirmEthSend("0.1");
|
||||||
|
// 21000 gas at 1 wei is 0.000000000000021 ETH, shown to its first
|
||||||
|
// significant digit.
|
||||||
|
expect(text("confirm-fee-amount")).toBe("0.00000000000002 ETH");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("when they differ, on both lines", async () => {
|
||||||
|
mockNode.feeData = { maxFeePerGas: 2n, gasPrice: 1n };
|
||||||
|
await refreshWith(10n ** 18n);
|
||||||
|
await confirmEthSend("0.1");
|
||||||
|
expect(text("confirm-fee-amount")).toBe("~0.00000000000002 ETH");
|
||||||
|
expect(text("confirm-fee-reserve")).toBe(
|
||||||
|
"up to 0.00000000000004 ETH reserved",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// The confirmation screen shows the reserve alone when the node quotes no
|
||||||
|
// cheaper estimate, and that reserve is the same gas limit times maximum fee
|
||||||
|
// per gas that the approval screen calls the max fee.
|
||||||
|
describe("the same fee reads the same on the confirmation and approval screens", () => {
|
||||||
|
test.each([
|
||||||
|
["below the floor", 1n],
|
||||||
|
["with more than four decimals", 58823529411n],
|
||||||
|
])("%s", async (_label, feePerGas) => {
|
||||||
|
mockNode.feeData = { maxFeePerGas: feePerGas, gasPrice: feePerGas };
|
||||||
|
await refreshWith(10n ** 18n);
|
||||||
|
await confirmEthSend("0.1");
|
||||||
|
const onConfirm = text("confirm-fee-amount");
|
||||||
|
await approveTxWithFeePerGas(feePerGas);
|
||||||
|
expect(text("approve-tx-fee")).toBe(onConfirm);
|
||||||
|
expect(onConfirm).toMatch(/[1-9]/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -389,7 +389,7 @@ describe("a scale the explorer's own rows disagree about", () => {
|
|||||||
expect(txInfo.tokenBalance).toBe("5.0");
|
expect(txInfo.tokenBalance).toBe("5.0");
|
||||||
confirmTx.show(txInfo);
|
confirmTx.show(txInfo);
|
||||||
await settle();
|
await settle();
|
||||||
expect(text("confirm-balance")).toBe("5.0 NOVEL");
|
expect(text("confirm-balance")).toBe("5.0000 NOVEL");
|
||||||
expect(errors()).toBe("");
|
expect(errors()).toBe("");
|
||||||
expect(sendDisabled()).toBe(false);
|
expect(sendDisabled()).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -431,7 +431,7 @@ describe("the confirmation screen tells an unknown balance from a zero one", ()
|
|||||||
const zero = await render("0.0");
|
const zero = await render("0.0");
|
||||||
expect(unknown.balance).not.toBe(zero.balance);
|
expect(unknown.balance).not.toBe(zero.balance);
|
||||||
expect(unknown.balance).toBe("unknown (NOVEL)");
|
expect(unknown.balance).toBe("unknown (NOVEL)");
|
||||||
expect(zero.balance).toBe("0.0 NOVEL");
|
expect(zero.balance).toBe("0.0000 NOVEL");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Both hit INSUFFICIENT_TOKEN — an unknown balance is treated as nothing to
|
// Both hit INSUFFICIENT_TOKEN — an unknown balance is treated as nothing to
|
||||||
@@ -443,7 +443,7 @@ describe("the confirmation screen tells an unknown balance from a zero one", ()
|
|||||||
expect(unknown.errors).not.toBe(zero.errors);
|
expect(unknown.errors).not.toBe(zero.errors);
|
||||||
expect(unknown.errors).toContain("This token's balance is unknown");
|
expect(unknown.errors).toContain("This token's balance is unknown");
|
||||||
expect(unknown.errors).not.toContain("You have");
|
expect(unknown.errors).not.toContain("You have");
|
||||||
expect(zero.errors).toContain("You have 0.0 NOVEL");
|
expect(zero.errors).toContain("You have 0.0000 NOVEL");
|
||||||
expect(zero.errors).not.toContain("balance is unknown");
|
expect(zero.errors).not.toContain("balance is unknown");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user