fix: every balance is cut to four decimals (closes #757)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

The balance lists and Home's ETH line rounded with toFixed(4), so 0.99996
ETH read 1.0000 there and 0.9999 on Send. Send's current balance and
ConfirmTx's balance line kept the nonzero floor, so 0.00004 USDC read
0.0000 in the lists and 0.00004 on Send. All four now cut the stored
decimal string with truncateAmount(), as the history rows do.
balanceLine() takes the stored balance string instead of a parsed number,
and escapes the quantity cell. The amounts, fees and allowances a screen
asks the user to approve keep the floor of truncateAmountNeverZero().

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-10 23:17:26 +00:00
1 parent 6d158bf1b1
commit b2e2de5cd5
15 files changed
+201 -87

No files matched your search

+16 -16
View File
@@ -6,8 +6,10 @@
// 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, every nonzero token holding
// kept, and the screens show them and the fee through that helper.
// 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.
//
// Driven through the real refreshBalances(), Send screen, confirmation screen
// and approval screen, with only the node, the explorer and the DOM stubbed: a
@@ -278,21 +280,21 @@ beforeEach(() => {
mockNode.feeData = { maxFeePerGas: 1n, gasPrice: 1n };
});
describe("an ETH balance below 0.000001 never renders as zero", () => {
describe("an ETH balance below 0.0001 reads 0.0000", () => {
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");
expect(text("send-balance")).toBe("Current balance: 0.0000 ETH");
});
test("on the confirmation screen", async () => {
await refreshWith(HALF_MICRO_ETH);
await confirmSend("0.0000001");
expect(text("confirm-balance")).toBe("0.0000005 ETH");
expect(text("confirm-balance")).toBe("0.0000 ETH");
});
test("while a balance above the floor keeps four decimals", async () => {
test("while a larger balance is cut to four decimals", async () => {
await refreshWith(1234567890000000000n);
await confirmSend("0.1");
expect(text("confirm-balance")).toBe("1.2345 ETH");
@@ -302,7 +304,7 @@ describe("an ETH balance below 0.000001 never renders as zero", () => {
// 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 never renders as zero", () => {
describe("a tracked token holding below 0.000001 reads 0.0000", () => {
beforeEach(() => {
state.trackedTokens = [
{ address: TOKEN, symbol: "TOK", name: "Token", decimals: 18 },
@@ -313,13 +315,13 @@ describe("a tracked token holding below 0.000001 never renders as zero", () => {
await refreshWith(10n ** 18n, [tokenRow(HALF_MICRO_ETH)]);
state.selectedToken = TOKEN;
send.updateSendBalance();
expect(text("send-balance")).toBe("Current balance: 0.0000005 TOK");
expect(text("send-balance")).toBe("Current balance: 0.0000 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.0000005 TOK");
expect(text("confirm-balance")).toBe("0.0000 TOK");
expect(errors()).toBe("");
expect(balanceError()).toBe("");
await confirmSend("0.0000006", TOKEN);
@@ -347,11 +349,9 @@ describe("a tracked token holding below 0.000001 never renders as zero", () => {
await refreshWith(10n ** 18n, [tokenRow(1n, { decimals: "24" })]);
state.selectedToken = TOKEN;
send.updateSendBalance();
expect(text("send-balance")).toBe(
"Current balance: 0.000000000000000000000001 TOK",
);
expect(text("send-balance")).toBe("Current balance: 0.0000 TOK");
await confirmSend("0.000000000000000001", TOKEN);
expect(text("confirm-balance")).toBe("0.000000000000000000000001 TOK");
expect(text("confirm-balance")).toBe("0.0000 TOK");
expect(balanceError()).toContain(
"Your TOK balance does not cover this amount.",
);
@@ -363,20 +363,20 @@ describe("a tracked token holding below 0.000001 never renders as zero", () => {
// 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 never renders as zero", () => {
describe("an untracked token holding below 0.000001 reads 0.0000", () => {
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.0000005 TOK");
expect(text("send-balance")).toBe("Current balance: 0.0000 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.0000005 TOK");
expect(text("confirm-balance")).toBe("0.0000 TOK");
expect(errors()).toBe("");
expect(balanceError()).toBe("");
await confirmSend("0.0000006", TOKEN);