fix: count the network fee in the confirm-screen balance check (closes #154)
Some checks failed
check / check (push) Has been cancelled

The Send button was enabled whenever the amount alone fit the balance, so
a max-value ETH send passed the confirmation screen and failed at
broadcast, after the user had committed to it.

The arithmetic moves into src/shared/txValidation.js as a pure function
over 18-decimal fixed point: native ETH now requires amount + fee <=
balance, and an ERC-20 transfer requires the ETH balance to cover the fee
on top of the token check, reported as its own error. Validation re-runs
when the async estimate resolves; Send stays disabled while the estimate
is pending and when it fails, so an unknown fee is never treated as zero.
The fee messages are static elements that already reserve their space, so
nothing moves when the estimate lands.

The fee reserved is the one the node will actually require. The send pins
no fee fields, so ethers broadcasts a type-2 transaction and the node
validates it against value + gasLimit * maxFeePerGas; gating on gasPrice
would under-reserve by roughly gasLimit * baseFee and let through exactly
the broadcast failure this change exists to prevent. feeReserveWei()
derives that reserve, falling back to gasPrice only where no type-2
pricing exists. The reserve is read when the screen opens and the
broadcast derives its own, so a base fee that roughly doubles while the
user is at the password field can still outrun it: this turns a
deterministic failure on every max-value send into a rare one, not into
none.

The fee line shows both numbers rather than one - what the transfer is
expected to cost, and below it the larger amount reserved until it
confirms. Quoting only the reserve overstates the typical mainnet cost by
roughly double on every send; quoting only the estimate contradicts the
gate. The second line holds its space from the first paint.

validateTransfer() fails closed: a feeWei that is not a non-negative
bigint under FEE_KNOWN, any unrecognised feeStatus, and a negative amount
all block, rather than counting as a fee of zero or as an amount that
passes every comparison trivially.
This commit is contained in:
clawbot
2026-08-11 12:19:52 +00:00
parent 12acf4dc8c
commit ef82c62912
7 changed files with 745 additions and 63 deletions

View File

@@ -575,16 +575,26 @@ screen, including ExportPrivKey, falls back to Home.
- To: blockie + color dot + full address + etherscan link + ENS name
- Amount: value + symbol (USD in parentheses)
- Your balance: value + symbol (USD in parentheses)
- Estimated network fee: "Estimating..." then the ETH amount (USD in
parentheses) or "Unable to estimate", fetched async
- Network fee: "Estimating..." then two lines, or "Unable to estimate",
fetched async. The first line is what the transfer is expected to cost,
`gasLimit * gasPrice` (USD in parentheses); the second is the
`gasLimit * maxFeePerGas` reserve the node requires, which is what the
balance check gates on. The second line is omitted on a network with no
type-2 pricing, where the two are the same number, but its space is
reserved either way
- Warnings: inline warnings from the local checks (scam address, self-send)
plus four reserved warning boxes made visible by the async checks —
recipient with no transaction history, recipient is a contract, burn
address, and an Etherscan phishing/scam label
- Errors (insufficient balance)
- Errors (insufficient balance), plus three reserved error boxes — the
amount plus the fee exceeds the balance (ETH transfers), not enough ETH to
pay the fee for the transfer (ERC-20 transfers), and the fee could not be
estimated. The first two are mutually exclusive per transfer type, so only
the applicable one holds space
- Password: an inline field on this screen, not a modal, with its own error
line
- "Sign & Send" button (disabled if errors)
- "Sign & Send" button (disabled if errors, and while the network fee
estimate is pending or unavailable)
- **Transitions**:
- "Sign & Send" (correct password) → broadcast tx → **WaitTx**
- "Sign & Send" (correct password) → broadcast fails → **ErrorTx**