1 Commits
Author SHA1 Message Date
sneak 75a24b59e7 fix: Send refuses every amount the confirmation screen would refuse (closes #532)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
Send accepted any amount parseFloat() read as above zero, so "1e-3",
"1abc" and an amount with a nonzero digit past the token's decimal places
reached ConfirmTx, which then refused them. isValidAmount() in
src/shared/txValidation.js is now the one check: Review and Max on Send
call it with the token's decimal places, and validateTransfer() calls it
for ConfirmTx, which now also refuses a zero amount and a token amount
with a nonzero digit past the token's places. Zeros past those places
are accepted, since the amount encoded for the transfer is the same.

Model: opus-5-5
2026-10-18 19:34:11 +00:00
5 changed files with 33 additions and 11 deletions
+6 -3
View File
@@ -1687,9 +1687,12 @@ view would leave a wallet one click from deletion.
- "Review" (valid inputs, ENS resolved) → **ConfirmTx**
- "Review" with an unresolvable ENS name or an invalid amount → flash
message, no screen change. An amount is valid when it is a number above
zero written as digits with at most one decimal point, with no more
decimal places than the token has and never more than 18 (ETH's own),
which is also the check ConfirmTx makes before anything can be sent
zero, written as digits with at most one decimal point, with nothing but
zeros after the token's last decimal place. ETH has 18 places, and a token
with more, or with an unknown number, is held to 18. Zeros past those
places leave the amount unchanged, so `1.0000000` of a 6-place token
passes and is sent as exactly 1. ConfirmTx makes the same check before
anything can be sent
- "Back" → previous screen (Home, AddressDetail, or AddressToken)
#### ConfirmTx (`confirm-tx`)
+10 -7
View File
@@ -89,13 +89,16 @@ function toFixedPoint(value) {
}
// Whether `amount` can be sent: a number above zero, written as digits with
// at most one decimal point ("0.5", "12", ".5"), with no more decimal places
// than `tokenDecimals`, a token's own, and never more than the 18 the
// arithmetic here works in, which are ETH's. ETH, and a token whose decimal
// places are unknown, pass null and are held to the 18 (the confirmation
// screen refuses to send such a token anyway). Refuses "1e-3", "1abc", "0"
// and "-1". The Send screen checks it before the confirmation screen opens,
// and validateTransfer() checks it again there.
// at most one decimal point ("0.5", "12", ".5"), with nothing but zeros after
// the token's last decimal place, `tokenDecimals`, and nothing but zeros after
// the 18th, the places the arithmetic here works in, which are ETH's. ETH, and
// a token whose decimal places are unknown, pass null and are held to the 18
// (the confirmation screen refuses to send such a token anyway). Zeros past
// those places are accepted because parseUnits() drops them here and again
// when the transfer is encoded, so "1.0000000" of a 6-place token is exactly
// 1 everywhere. Refuses "1e-3", "1abc", "0" and "-1". The Send screen checks
// it before the confirmation screen opens, and validateTransfer() checks it
// again there.
function isValidAmount(amount, tokenDecimals = null) {
if (typeof amount !== "string") return false;
const places = Math.min(tokenDecimals ?? SCALE_DECIMALS, SCALE_DECIMALS);
+3
View File
@@ -194,6 +194,9 @@ describe.each([
["0.5", "TOK"],
// Exactly the 6 decimal places the token has.
["0.000001", "TOK"],
// Only zeros past the token's 6 places, and past ETH's 18.
["1.0000000", "TOK"],
["0.5000000000000000000", "ETH"],
])("%s %s", (amount, symbol) => {
const token = TOKEN_BY_SYMBOL[symbol];
+6
View File
@@ -116,6 +116,12 @@ describe("transferAmountUnits", () => {
expect(() => transferAmountUnits("0.0000001", 6, 6n)).toThrow();
});
test("encodes zeros past the token's scale as the amount typed", () => {
// The Send screen accepts "1.0000000" for a 6-place token, so the
// transfer must move exactly 1 of it.
expect(transferAmountUnits("1.0000000", 6, 6n)).toBe(1000000n);
});
test("every refusal is a full sentence", () => {
const messages = [];
for (const args of [
+8 -1
View File
@@ -54,13 +54,20 @@ describe("isValidAmount", () => {
expect(isValidAmount(undefined)).toBe(false);
});
test("refuses more decimal places than the token has", () => {
test("refuses a digit other than 0 past the token's places", () => {
expect(isValidAmount("0.000001", 6)).toBe(true);
expect(isValidAmount("0.0000001", 6)).toBe(false);
expect(isValidAmount("5", 0)).toBe(true);
expect(isValidAmount("0.5", 0)).toBe(false);
});
test("accepts zeros past the token's places, which leave the amount unchanged", () => {
expect(isValidAmount("1.0000000", 6)).toBe(true);
expect(isValidAmount("5.0", 0)).toBe(true);
expect(isValidAmount("1.0000000000000000000")).toBe(true);
expect(isValidAmount("0.0000000", 6)).toBe(false);
});
test("holds ETH, a token of unknown places, and any token to 18 places", () => {
const nineteen = "0.0000000000000000001";
expect(isValidAmount(nineteen)).toBe(false);