feat: a "Max" button on the Send screen (closes #198)
Max fills in a token's balance, cut down to the 18 decimal places the confirmation screen accepts, or for ETH the exact balance minus the fee reserve the confirmation screen's balance check gates on. An ETH fee estimate that finishes after the Send screen was left, or its address, holding, recipient or amount changed, fills nothing in. 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:
@@ -1,4 +1,4 @@
|
||||
// Balance arithmetic for the transaction confirmation screen.
|
||||
// Balance arithmetic for the Send and transaction confirmation screens.
|
||||
//
|
||||
// Pure: no DOM, no network, no state. Everything is exact integer math on
|
||||
// 18-decimal fixed point (wei for ETH), so it can be unit tested directly
|
||||
@@ -10,7 +10,7 @@
|
||||
// the token balance arrive as human decimal strings, so comparing them at a
|
||||
// common scale is exact.
|
||||
|
||||
const { parseUnits } = require("ethers");
|
||||
const { parseUnits, formatEther } = require("ethers");
|
||||
|
||||
const SCALE_DECIMALS = 18;
|
||||
|
||||
@@ -87,6 +87,28 @@ function toFixedPoint(value) {
|
||||
}
|
||||
}
|
||||
|
||||
// The most ETH a send can carry: the exact balance minus the fee reserve from
|
||||
// feeReserveWei(), as a decimal string, so validateTransfer() passes it with
|
||||
// exactly that reserve left behind. `ethBalance` is the exact decimal string
|
||||
// balances.js stores, never a rounded one. Null when the balance does not
|
||||
// leave anything to send once the fee is paid, or when either input is
|
||||
// unusable.
|
||||
function maxEthAmount(ethBalance, feeWei) {
|
||||
const balanceWei = toFixedPoint(ethBalance);
|
||||
if (balanceWei === null) return null;
|
||||
if (typeof feeWei !== "bigint" || feeWei < 0n) return null;
|
||||
const amountWei = balanceWei - feeWei;
|
||||
if (amountWei <= 0n) return null;
|
||||
return formatEther(amountWei);
|
||||
}
|
||||
|
||||
// The most of a token a send can carry: its balance cut down, never rounded
|
||||
// up, to the 18 places (SCALE_DECIMALS) an amount may have. A token can
|
||||
// declare more than 18 decimals, and its balance is stored with all of them.
|
||||
function maxTokenAmount(tokenBalance) {
|
||||
return tokenBalance.replace(/(\.\d{18})\d+$/, "$1");
|
||||
}
|
||||
|
||||
// Validate a pending transfer against the balances that must cover it.
|
||||
//
|
||||
// isErc20 — token transfer rather than a native ETH transfer
|
||||
@@ -139,13 +161,12 @@ function validateTransfer({
|
||||
const feeFp = known ? feeWei : null;
|
||||
|
||||
if (isErc20) {
|
||||
// A token can declare more than 18 decimals, and its balance is
|
||||
// stored with all of them. Only the first 18 places (SCALE_DECIMALS)
|
||||
// are read: an amount with more was refused above, so the places
|
||||
// after them cannot decide whether the amount fits.
|
||||
// Only the first 18 places of the balance are read: an amount with
|
||||
// more was refused above, so the places after them cannot decide
|
||||
// whether the amount fits.
|
||||
const tokenText =
|
||||
typeof tokenBalance === "string"
|
||||
? tokenBalance.replace(/(\.\d{18})\d+$/, "$1")
|
||||
? maxTokenAmount(tokenBalance)
|
||||
: tokenBalance;
|
||||
const tokenFp = toFixedPoint(tokenText) ?? 0n;
|
||||
if (amountFp > tokenFp) codes.push(CODES.INSUFFICIENT_TOKEN);
|
||||
@@ -174,6 +195,8 @@ module.exports = {
|
||||
SCALE_DECIMALS,
|
||||
feeReserveWei,
|
||||
feeEstimateWei,
|
||||
maxEthAmount,
|
||||
maxTokenAmount,
|
||||
toFixedPoint,
|
||||
validateTransfer,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user