feat: a "Max" button on the Send screen (closes #198)
check / check (push) Failing after 3s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s

Max fills in a token's whole balance, or for ETH the exact balance minus
the fee reserve the confirmation screen's balance check gates on. The
confirmation screen works a max ETH amount out again from its own fee
estimate and signs it with that estimate's fee fields, so a fee that rose
before signing cannot push amount plus fee above the balance.
validateTransfer() still gates every send, the check that ETH covers a
token send's fee included. Where there is nothing to fill in, a flash
message says why.

Model: opus-5-5
This commit is contained in:
2026-10-05 04:05:20 +00:00
parent cf7ca99215
commit a0f1e98323
9 changed files with 672 additions and 30 deletions
+50
View File
@@ -6,6 +6,7 @@ const {
FEE_UNAVAILABLE,
feeReserveWei,
feeEstimateWei,
maxEthAmount,
toFixedPoint,
validateTransfer,
} = require("../src/shared/txValidation");
@@ -306,6 +307,55 @@ describe("feeEstimateWei", () => {
});
});
// The amount the Send screen's Max fills in for ETH: the exact balance, as
// balances.js stores it, minus the fee reserve. Never the four-decimal balance
// the Send screen shows.
describe("maxEthAmount", () => {
// The Send screen shows this balance as 1.2345.
const BALANCE = "1.234567890123456789";
test("is the exact balance minus the fee, to the wei", () => {
expect(maxEthAmount(BALANCE, FEE)).toBe("1.234147890123456789");
expect(
parseEther(BALANCE) - parseEther(maxEthAmount(BALANCE, FEE)),
).toBe(FEE);
});
test("passes validateTransfer with exactly the fee left behind", () => {
const r = validateTransfer({
isErc20: false,
amount: maxEthAmount(BALANCE, FEE),
ethBalance: BALANCE,
feeStatus: FEE_KNOWN,
feeWei: FEE,
});
expect(r).toEqual({ canSend: true, codes: [] });
});
test("is one wei when the balance is one wei more than the fee", () => {
expect(maxEthAmount("0.000420000000000001", FEE)).toBe(
"0.000000000000000001",
);
});
test("is null when the balance does not cover the fee", () => {
expect(maxEthAmount("0.0001", FEE)).toBe(null);
expect(maxEthAmount("0.0", FEE)).toBe(null);
});
test("is null when the balance covers the fee and nothing more", () => {
expect(maxEthAmount("0.00042", FEE)).toBe(null);
});
test("is null on a balance or fee it cannot do exact arithmetic on", () => {
expect(maxEthAmount(undefined, FEE)).toBe(null);
expect(maxEthAmount("not a number", FEE)).toBe(null);
expect(maxEthAmount(BALANCE, null)).toBe(null);
expect(maxEthAmount(BALANCE, -1n)).toBe(null);
expect(maxEthAmount(BALANCE, 420000000000000)).toBe(null);
});
});
// Everything that is not a usable fee blocks exactly as FEE_UNAVAILABLE does.
// Each of these previously returned { canSend: true, codes: [] } — counting no
// fee at all, on a full-balance send, in the direction that lets money out.