fix: the wallet's own send shows its amount to four decimal places (closes #522)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

ConfirmTx and the wait, success and error screens printed Send's amount field
as it stood (`.5 ETH`, or every digit Max filled in); TxApproval shows
`0.5000 ETH`. ConfirmTx now writes the amount as TxApproval does: parsed at the
scale it is encoded with, written back as plain decimal text and cut by
truncateAmountNeverZero(), and hands the status screens a copy carrying that
text. A token whose scale is unknown, which cannot be sent, has the field's
text cut the same way. The transaction is still signed for the exact amount,
which also prices the USD value. A saved confirmation screen whose amount is
not text is no longer redrawn.

Model: opus-5-5
This commit is contained in:
2026-10-19 15:07:27 +00:00
parent 3db066413e
commit 02c12d664c
9 changed files with 527 additions and 50 deletions
+39 -27
View File
@@ -1082,23 +1082,29 @@ The rule and its exception live in `src/shared/amountDisplay.js` as
confirmation screens display goes through the floored one — the ERC-20 amount, confirmation screens display goes through the floored one — the ERC-20 amount,
the ETH value and max fee (`src/popup/views/approval.js`), the swap's `Amount` the ETH value and max fee (`src/popup/views/approval.js`), the swap's `Amount`
and `Min. received` lines (`src/shared/uniswap.js`), the Send screen's and `Min. received` lines (`src/shared/uniswap.js`), the Send screen's
`Current balance` (`src/popup/views/send.js`), and the balance and network fee `Current balance` (`src/popup/views/send.js`), and the amount, balance and
on the confirmation screen for the wallet's own send network fee on the confirmation screen for the wallet's own send
(`src/popup/views/confirmTx.js`). Both screens render a network fee through (`src/popup/views/confirmTx.js`), which hands the wait, success and error
`formatFee()` in `src/popup/views/helpers.js`, which prices the exact fee in USD screens the amount as it showed it. The transaction is still signed for the
rather than its truncated figure, so the same fee reads the same on both, USD exact amount, and its USD value is priced from it. Both screens render a network
value included. Balances are stored exactly (`src/shared/balances.js`), whatever fee through `formatFee()` in `src/popup/views/helpers.js`, which prices the
decimals a token declares, so a balance below the floor reaches these screens as exact fee in USD rather than its truncated figure, so the same fee reads the
it is. The history list (`src/shared/transactions.js`) uses the unfloored one: same on both, USD value included. Balances are stored exactly
the transaction detail view is the authoritative record and already shows exact (`src/shared/balances.js`), whatever decimals a token declares, so a balance
precision. The balance lists use neither: they round to four places with below the floor reaches these screens as it is. The history list
`toFixed(4)` (`balanceLine()` in `src/popup/views/helpers.js`). The 4-decimal (`src/shared/transactions.js`) uses the unfloored one: the transaction detail
rule is unchanged everywhere else, including for amounts at or above the floor view is the authoritative record and already shows exact precision. The balance
on the approval screens. lists use neither: they round to four places with `toFixed(4)` (`balanceLine()`
in `src/popup/views/helpers.js`). The 4-decimal rule is unchanged everywhere
else, including for amounts at or above the floor on the approval screens.
The floor applies only where the token's scale is known. Where it is not, the On the approval screen and the wait, success and error screens that carry its
approval screen states base units instead of a quantity — see Unknown token amount forward, the floor applies only where the token's scale is known. Where
scale below — and no truncation happens at all. it is not, they state base units instead of a quantity — see Unknown token scale
below — and no truncation happens at all. On ConfirmTx and the wait, success and
error screens that follow it, the amount of a token whose scale is unknown,
which cannot be sent, is the text from Send's amount field cut to four decimals
with the same floor (`.5` reads `.5000`).
**Specific Exception — unknown token scale:** Calldata carries base units and no **Specific Exception — unknown token scale:** Calldata carries base units and no
scale, so every amount on the dApp approval screen needs the token's `decimals`. scale, so every amount on the dApp approval screen needs the token's `decimals`.
@@ -1763,14 +1769,18 @@ view would leave a wallet one click from deletion.
- Token contract: full address + etherscan link (ERC-20 only) - Token contract: full address + etherscan link (ERC-20 only)
- From: blockie + color dot + full address + etherscan link + wallet title - From: blockie + color dot + full address + etherscan link + wallet title
- To: blockie + color dot + full address + etherscan link + ENS name - To: blockie + color dot + full address + etherscan link + ENS name
- Amount: value + symbol (USD in parentheses). An ETH amount Send's "Max" - Amount: value + symbol (USD in parentheses), the value to four decimal
filled in is worked out again from this screen's own fee estimate when it places with the nonzero floor, as on TxApproval (see Display Consistency),
arrives, as the balance minus the reserve, and the transaction is signed however Send's amount field wrote it (`.5` and `0.5` both read `0.5000`).
with that estimate's fee fields, so a fee fetched again at signing cannot For a token whose scale is unknown, which cannot be sent, the value is the
exceed what the amount leaves behind. The address keeps whatever part of text from Send's amount field cut the same way (`.5` reads `.5000`). An
the reserve the transaction does not use. If the balance could not be read ETH amount Send's "Max" filled in is worked out again from this screen's
or no longer covers the fee, the amount is left as it was and one of the own fee estimate when it arrives, as the balance minus the reserve, and
errors below blocks the send the transaction is signed with that estimate's fee fields, so a fee
fetched again at signing cannot exceed what the amount leaves behind. The
address keeps whatever part of the reserve the transaction does not use.
If the balance could not be read or no longer covers the fee, the amount
is left as it was and one of the errors below blocks the send
- Your balance: the From address's balance on the network its transaction - Your balance: the From address's balance on the network its transaction
was reviewed on, read from that network's RPC endpoint each time the was reviewed on, read from that network's RPC endpoint each time the
screen is shown, so a screen the popup reopens onto states and checks that screen is shown, so a screen the popup reopens onto states and checks that
@@ -2350,9 +2360,11 @@ view would leave a wallet one click from deletion.
- From: color dot + full address + etherscan link - From: color dot + full address + etherscan link
- Contract: color dot + full address + etherscan link, token symbol label if - Contract: color dot + full address + etherscan link, token symbol label if
known; for a contract creation, the same sentence as on WaitTx known; for a contract creation, the same sentence as on WaitTx
- Value: amount in ETH (4 decimal places, USD in parentheses) - Value: amount in ETH (4 decimal places with the nonzero floor, USD in
- Network fee (max): gas limit × fee per gas in ETH (4 decimal places, USD parentheses)
in parentheses), with the gas limit and the fee per gas in gwei below it - Network fee (max): gas limit × fee per gas in ETH (4 decimal places with
the nonzero floor, USD in parentheses), with the gas limit and the fee per
gas in gwei below it
- Network and nonce - Network and nonce
- Raw data: full calldata displayed inline (shown if present) - Raw data: full calldata displayed inline (shown if present)
- Password input and an error line - Password input and an error line
+6 -4
View File
@@ -127,16 +127,18 @@ function isRenderableDecoded(value) {
} }
// The pending transaction confirmTx.show() renders: `token` reaches // The pending transaction confirmTx.show() renders: `token` reaches
// renderAddressHtml() when it is not "ETH", and `from`/`to` reach // renderAddressHtml() when it is not "ETH", `from`/`to` reach
// addressTitle(), makeBlockie() and getLocalWarnings(). `chainId`, which // addressTitle(), makeBlockie() and getLocalWarnings(), and `amount` reaches
// show() fills in when it is absent, picks the network whose explorer the // truncateAmountNeverZero(), which splits it. `chainId`, which show() fills in
// address links open, so it has to name one. // when it is absent, picks the network whose explorer the address links open,
// so it has to name one.
function isRenderablePendingTx(value) { function isRenderablePendingTx(value) {
return ( return (
isRecord(value) && isRecord(value) &&
isText(value.token) && isText(value.token) &&
isAddressText(value.from) && isAddressText(value.from) &&
isAddressText(value.to) && isAddressText(value.to) &&
isText(value.amount) &&
(!value.chainId || SUPPORTED_CHAIN_IDS.has(value.chainId)) (!value.chainId || SUPPORTED_CHAIN_IDS.has(value.chainId))
); );
} }
+35 -7
View File
@@ -45,8 +45,8 @@ const {
transferAmountUnits, transferAmountUnits,
} = require("../../shared/transferAmount"); } = require("../../shared/transferAmount");
const { assertWithinCeilings } = require("../../shared/approvalVerify"); const { assertWithinCeilings } = require("../../shared/approvalVerify");
// The balance line and the fee reserve go through it, as the approval screen's // The amount, the balance line and the fee reserve go through it, as the
// amounts do. // approval screen's amounts do.
const { truncateAmountNeverZero } = require("../../shared/amountDisplay"); const { truncateAmountNeverZero } = require("../../shared/amountDisplay");
const { const {
CODES, CODES,
@@ -100,8 +100,32 @@ function valueWithUsd(text, usdAmount) {
return text; return text;
} }
// The Amount line, with its USD value. A max ETH send's line is drawn again // The amount as this screen and the wait, success and error screens show it,
// once its amount is re-derived from the fee estimate. // written as the approval screen writes a site's amount: the amount being
// signed, parsed at the scale it is encoded with and written back as plain
// decimal text, then cut to four decimals with the nonzero floor. So `.5` and
// `00.5` read `0.5000`, as `0.5` does, except for a token whose scale is
// unknown, which cannot be sent: its amount is cut as typed, so `.5` reads
// `.5000`. txInfo.amount, which is what gets signed, is left as it is.
function shownAmount(txInfo) {
let exact = txInfo.amount;
try {
if (txInfo.token === "ETH") {
exact = formatEther(parseEther(txInfo.amount));
} else {
const decimals = displayedDecimals(txInfo.tokenDecimals);
exact = formatUnits(parseUnits(txInfo.amount, decimals), decimals);
}
} catch {
// A token whose scale is unknown, which is never signed, or an amount
// the amount check refuses: cut as Send's amount field held it.
}
return truncateAmountNeverZero(exact);
}
// The Amount line, with its USD value, which is priced from the exact amount.
// A max ETH send's line is drawn again once its amount is re-derived from the
// fee estimate.
function renderAmount(txInfo) { function renderAmount(txInfo) {
const isErc20 = txInfo.token !== "ETH"; const isErc20 = txInfo.token !== "ETH";
const rawSymbol = isErc20 const rawSymbol = isErc20
@@ -110,7 +134,7 @@ function renderAmount(txInfo) {
const price = getPrice(isErc20 ? rawSymbol : "ETH", txInfo.chainId); const price = getPrice(isErc20 ? rawSymbol : "ETH", txInfo.chainId);
const amountUsd = price ? parseFloat(txInfo.amount) * price : null; const amountUsd = price ? parseFloat(txInfo.amount) * price : null;
$("confirm-amount").textContent = valueWithUsd( $("confirm-amount").textContent = valueWithUsd(
txInfo.amount + " " + displaySymbol(rawSymbol), shownAmount(txInfo) + " " + displaySymbol(rawSymbol),
amountUsd, amountUsd,
); );
} }
@@ -698,6 +722,10 @@ function init(_ctx) {
$("btn-confirm-send").disabled = true; $("btn-confirm-send").disabled = true;
$("btn-confirm-send").classList.add("text-muted"); $("btn-confirm-send").classList.add("text-muted");
// The wait, success and error screens show the amount as this screen
// does, so they are handed a copy carrying that text. pendingTx keeps
// the exact amount, which is the one signed.
const shownTx = { ...pendingTx, amount: shownAmount(pendingTx) };
let tx; let tx;
try { try {
const signer = getSignerForAddress(wallet, addr, decryptedSecret); const signer = getSignerForAddress(wallet, addr, decryptedSecret);
@@ -714,7 +742,7 @@ function init(_ctx) {
// Note: JS strings are immutable; this nulls the reference but // Note: JS strings are immutable; this nulls the reference but
// the original string may persist in memory until GC. // the original string may persist in memory until GC.
decryptedSecret = null; decryptedSecret = null;
txStatus.showWait(pendingTx, tx.hash); txStatus.showWait(shownTx, tx.hash);
} catch (e) { } catch (e) {
decryptedSecret = null; decryptedSecret = null;
// A gas limit or fee over its bound is refused before anything is // A gas limit or fee over its bound is refused before anything is
@@ -730,7 +758,7 @@ function init(_ctx) {
return; return;
} }
const hash = tx ? tx.hash : null; const hash = tx ? tx.hash : null;
txStatus.showError(pendingTx, hash, e.shortMessage || e.message); txStatus.showError(shownTx, hash, e.shortMessage || e.message);
} finally { } finally {
$("btn-confirm-send").disabled = false; $("btn-confirm-send").disabled = false;
$("btn-confirm-send").classList.remove("text-muted"); $("btn-confirm-send").classList.remove("text-muted");
+415
View File
@@ -0,0 +1,415 @@
// The amount of the wallet's own send on the confirmation, wait, success and
// error screens (https://git.eeqj.de/sneak/AutistMask/issues/522).
//
// These screens printed the text from Send's amount field as it was typed or
// filled in: `0.5 ETH`, or `1.234147890123456789 ETH` after Max, where the
// approval screen shows a site's send as `0.5000 ETH`. They now show it to four
// decimal places with the nonzero floor, as the approval screen does, and the
// transaction is still signed for the exact amount.
//
// Driven through the real Send screen, confirmation screen, Sign & Send and
// wait screen, with only the node, the explorer and the DOM stubbed.
"use strict";
// What the stub node answers, and the signed transactions it was handed. The
// token's own decimals() answers 6, as the explorer does.
const mockNode = {
balanceWei: 0n,
tokenUnits: 0n,
tokenDecimals: 6n,
broadcast: [],
broadcastError: null,
receipt: null,
};
// The token rows the stub explorer reports for the address.
const mockExplorer = { items: [] };
jest.mock("ethers", () => {
const actual = jest.requireActual("ethers");
// The selector of decimals(); every other contract call is balanceOf().
const DECIMALS = "0x313ce567";
class StubProvider {
async getBalance() {
return mockNode.balanceWei;
}
async call(tx) {
return actual.AbiCoder.defaultAbiCoder().encode(
["uint256"],
[
tx.data.startsWith(DECIMALS)
? mockNode.tokenDecimals
: mockNode.tokenUnits,
],
);
}
async lookupAddress() {
return null;
}
// 20 gwei reserved per gas.
async getFeeData() {
return {
maxFeePerGas: 20000000000n,
maxPriorityFeePerGas: 1000000000n,
gasPrice: 10000000000n,
};
}
async estimateGas() {
return 21000n;
}
async getCode() {
return "0x";
}
async getTransactionCount() {
return 1;
}
async getNetwork() {
return { chainId: 1n };
}
async broadcastTransaction(signed) {
if (mockNode.broadcastError) throw mockNode.broadcastError;
mockNode.broadcast.push(signed);
return { hash: "0x" + "ab".repeat(32) };
}
async getTransactionReceipt() {
return mockNode.receipt;
}
}
return {
...actual,
JsonRpcProvider: StubProvider,
Network: { from: () => ({}) },
};
});
jest.mock("../src/shared/log", () => ({
log: {
debugf: () => {},
infof: () => {},
warnf: () => {},
errorf: () => {},
},
// The explorer's token list, which refreshBalances() also fetches.
debugFetch: jest.fn(async () => ({
ok: true,
status: 200,
json: async () => mockExplorer.items,
})),
urlOrigin: () => "",
setRuntimeDebug: () => {},
isDebug: () => false,
}));
// The confirmation screen's Etherscan label lookup is the only fetch() these
// screens make; it fails, as it does offline.
global.fetch = jest.fn(() => {
throw new Error("tests must not perform network requests");
});
const { makeStorageStub } = require("./support/storageStub");
global.chrome = { storage: makeStorageStub(), runtime: { sendMessage() {} } };
// A stub DOM: every id resolves to a recording element.
const elements = new Map();
function makeEl(id) {
const handlers = new Map();
return {
id,
textContent: "",
innerHTML: "",
value: "",
disabled: false,
style: {},
dataset: {},
classList: {
add() {},
remove() {},
toggle() {},
contains: () => false,
},
handlers,
children: [],
addEventListener(name, fn) {
handlers.set(name, fn);
},
appendChild(child) {
this.children.push(child);
return child;
},
querySelectorAll: () => [],
querySelector: () => null,
remove() {},
focus() {},
// The success screen hides whole sections through .parentElement.
get parentElement() {
return global.document.getElementById(id + "-parent");
},
};
}
global.document = {
getElementById(id) {
if (!elements.has(id)) elements.set(id, makeEl(id));
return elements.get(id);
},
createElement: (tag) => makeEl(tag),
body: { prepend() {}, appendChild() {} },
addEventListener() {},
};
global.navigator = { clipboard: { writeText() {} } };
const {
Interface,
Transaction,
Wallet,
getAddress,
parseEther,
} = require("ethers");
const { refreshBalances } = require("../src/shared/balances");
const { encryptWithPassword } = require("../src/shared/vault");
const { state } = require("../src/shared/state");
const { clearPrices } = require("../src/shared/prices");
const send = require("../src/popup/views/send");
const confirmTx = require("../src/popup/views/confirmTx");
const txStatus = require("../src/popup/views/txStatus");
const PRIVATE_KEY = "0x" + "11".repeat(32);
const HOLDER = new Wallet(PRIVATE_KEY).address;
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
const PASSWORD = "correct horse battery staple";
const GWEI = 1000000000n;
// 1.234567890123456789 ETH.
const BALANCE_WEI = 1234567890123456789n;
// A token the bundled list does not know, with enough holders to be listed,
// at 6 decimal places.
const TOKEN = "0x" + "d".repeat(40);
const transferIface = new Interface([
"function transfer(address to, uint256 amount)",
]);
let encryptedSecret;
// The confirmation screen Review leads to, once shown.
let confirmed = null;
function el(id) {
return global.document.getElementById(id);
}
function text(id) {
return el(id).textContent;
}
// show() reads the balance and then estimates the fee without awaiting
// either; this lets both settle.
async function settle() {
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
}
// The address holds BALANCE_WEI and, when `tokenUnits` is given, that many
// base units of TOKEN, fetched and stored where the popup stores them. Send
// is then open for `token` ("ETH" or TOKEN) with the recipient entered.
async function openSend(token, tokenUnits = null) {
mockNode.balanceWei = BALANCE_WEI;
mockNode.tokenUnits = tokenUnits ?? 0n;
mockExplorer.items =
tokenUnits === null
? []
: [
{
value: String(tokenUnits),
token: {
type: "ERC-20",
address_hash: TOKEN,
symbol: "TOK",
name: "Token",
decimals: "6",
holders_count: "50000",
},
},
];
state.wallets = [
{
type: "key",
name: "Wallet 1",
encryptedSecret,
addresses: [{ address: HOLDER }],
},
];
state.selectedWallet = 0;
state.selectedAddress = 0;
await refreshBalances(
state.wallets,
"https://rpc.example.invalid",
"https://blockscout.example/api/v2",
state.trackedTokens,
"mainnet",
);
send.init({
showConfirmTx: (info) => (confirmed = info),
pageClosed: new AbortController().signal,
});
confirmTx.init({});
send.resetSendValidation();
state.currentView = "send";
state.selectedToken = token;
el("send-to").value = RECIPIENT;
el("send-amount").value = "";
}
// Press Review and show the confirmation screen with its fee estimate settled.
async function review() {
await el("btn-send-review").handlers.get("click")();
confirmTx.show(confirmed);
await settle();
}
async function signAndSend() {
el("confirm-tx-password").value = PASSWORD;
await el("btn-confirm-send").handlers.get("click")();
}
// The one transaction the stub node was handed.
function broadcastTx() {
expect(mockNode.broadcast).toHaveLength(1);
return Transaction.from(mockNode.broadcast[0]);
}
beforeAll(async () => {
encryptedSecret = await encryptWithPassword(PRIVATE_KEY, PASSWORD);
});
beforeEach(() => {
elements.clear();
clearPrices();
confirmed = null;
state.selectedToken = null;
state.trackedTokens = [];
state.fraudContracts = [];
state.currentView = null;
mockNode.broadcast = [];
mockNode.broadcastError = null;
mockNode.receipt = null;
txStatus.init({
doRefreshAndRender() {},
pageClosed: new AbortController().signal,
});
});
afterEach(() => {
txStatus.endWait();
});
describe("the wallet's own send shows its amount to four decimal places", () => {
test("for a short amount", async () => {
await openSend("ETH");
el("send-amount").value = "0.5";
await review();
expect(text("confirm-amount")).toBe("0.5000 ETH");
await signAndSend();
expect(text("wait-tx-summary")).toBe("0.5000 ETH");
expect(broadcastTx().value).toBe(parseEther("0.5"));
});
// Send accepts both. The approval screen writes the same value `0.5000`.
test.each([".5", "00.5"])("for an amount typed as %s", async (typed) => {
await openSend("ETH");
el("send-amount").value = typed;
await review();
expect(text("confirm-amount")).toBe("0.5000 ETH");
await signAndSend();
expect(text("wait-tx-summary")).toBe("0.5000 ETH");
expect(broadcastTx().value).toBe(parseEther("0.5"));
});
test("for a token amount typed as .5", async () => {
// 100 TOK.
await openSend(TOKEN, 100000000n);
el("send-amount").value = ".5";
await review();
expect(text("confirm-amount")).toBe("0.5000 TOK");
await signAndSend();
expect(text("wait-tx-summary")).toBe("0.5000 TOK");
const [, amount] = transferIface.decodeFunctionData(
"transfer",
broadcastTx().data,
);
expect(amount).toBe(500000n);
});
test("for a Max amount with many decimals", async () => {
await openSend("ETH");
await el("btn-send-max").handlers.get("click")();
// The balance minus 21000 gas at 20 gwei.
expect(el("send-amount").value).toBe("1.234147890123456789");
await review();
expect(text("confirm-amount")).toBe("1.2341 ETH");
await signAndSend();
expect(text("wait-tx-summary")).toBe("1.2341 ETH");
expect(broadcastTx().value).toBe(BALANCE_WEI - 21000n * 20n * GWEI);
});
// Cut to four decimals it would read 0.0000, so it is shown to its first
// significant digit instead, and signed in full.
test("for a tiny nonzero amount", async () => {
await openSend("ETH");
el("send-amount").value = "0.000012345";
await review();
expect(text("confirm-amount")).toBe("0.00001 ETH");
await signAndSend();
expect(text("wait-tx-summary")).toBe("0.00001 ETH");
expect(broadcastTx().value).toBe(parseEther("0.000012345"));
});
test("for a token amount", async () => {
// 100 TOK.
await openSend(TOKEN, 100000000n);
el("send-amount").value = "12.345678";
await review();
expect(text("confirm-amount")).toBe("12.3456 TOK");
await signAndSend();
expect(text("wait-tx-summary")).toBe("12.3456 TOK");
const tx = broadcastTx();
expect(tx.to).toBe(getAddress(TOKEN));
const [to, amount] = transferIface.decodeFunctionData(
"transfer",
tx.data,
);
expect(to).toBe(RECIPIENT);
expect(amount).toBe(12345678n);
});
test("on the success screen", async () => {
await openSend("ETH");
el("send-amount").value = "0.5";
await review();
await signAndSend();
// A popup opened later resumes the wait, and the receipt is there.
mockNode.receipt = { blockNumber: 21000000 };
txStatus.restoreWait();
await settle();
expect(state.currentView).toBe("success-tx");
expect(text("success-tx-summary")).toBe("0.5000 ETH");
});
test("on the error screen", async () => {
await openSend("ETH");
el("send-amount").value = "0.5";
await review();
mockNode.broadcastError = new Error("the node refused it");
await signAndSend();
expect(state.currentView).toBe("error-tx");
expect(text("error-tx-summary")).toBe("0.5000 ETH");
});
});
+2 -2
View File
@@ -333,7 +333,7 @@ function expectNativeTransferOn(networkId) {
"Native " + symbol + " transfer", "Native " + symbol + " transfer",
); );
expect(el("confirm-amount").textContent).toBe( expect(el("confirm-amount").textContent).toBe(
"1 " + symbol + usdOn(networkId, "$2,000.00"), "1.0000 " + symbol + usdOn(networkId, "$2,000.00"),
); );
expect(el("confirm-balance").textContent).toBe( expect(el("confirm-balance").textContent).toBe(
"5.0000 " + symbol + usdOn(networkId, "$10,000.00"), "5.0000 " + symbol + usdOn(networkId, "$10,000.00"),
@@ -649,7 +649,7 @@ describe.each([
`href="${explorerUrl}/address/${TOKEN}"`, `href="${explorerUrl}/address/${TOKEN}"`,
); );
expect(el("confirm-amount").textContent).toBe( expect(el("confirm-amount").textContent).toBe(
"10 USDC" + usdOn(reviewedOn, "$10.00"), "10.0000 USDC" + usdOn(reviewedOn, "$10.00"),
); );
expect(el("confirm-balance").textContent).toBe( expect(el("confirm-balance").textContent).toBe(
"50.0000 USDC" + usdOn(reviewedOn, "$50.00"), "50.0000 USDC" + usdOn(reviewedOn, "$50.00"),
+2 -1
View File
@@ -2915,9 +2915,10 @@ async function goToTokenConfirm(env) {
amount: TOKEN_AMOUNT, amount: TOKEN_AMOUNT,
}); });
await waitForEstimate(env.page); await waitForEstimate(env.page);
// TOKEN_AMOUNT to four decimal places (README.md, Display Consistency).
const shown = await shownAmount(env.page); const shown = await shownAmount(env.page);
assert( assert(
shown === TOKEN_AMOUNT + " " + STUB_TOKEN.symbol, shown === "0.2500 " + STUB_TOKEN.symbol,
"the confirmation screen is not showing the amount that was entered: " + "the confirmation screen is not showing the amount that was entered: " +
JSON.stringify(shown), JSON.stringify(shown),
); );
+3 -3
View File
@@ -245,7 +245,7 @@ describe.each([
test("the value", async () => { test("the value", async () => {
await confirmSend("0.1"); await confirmSend("0.1");
expect(text("confirm-type")).toBe("Native " + symbol + " transfer"); expect(text("confirm-type")).toBe("Native " + symbol + " transfer");
expect(text("confirm-amount")).toBe("0.1 " + symbol); expect(text("confirm-amount")).toBe("0.1000 " + symbol);
await approveTx(NETWORKS[networkId].chainId); await approveTx(NETWORKS[networkId].chainId);
expect(text("approve-tx-value")).toBe("0.0100 " + symbol); expect(text("approve-tx-value")).toBe("0.0100 " + symbol);
}); });
@@ -429,10 +429,10 @@ describe.each([
await global.document await global.document
.getElementById("btn-confirm-send") .getElementById("btn-confirm-send")
.handlers.get("click")(); .handlers.get("click")();
expect(text("wait-tx-summary")).toBe("0.1 " + symbol); expect(text("wait-tx-summary")).toBe("0.1000 " + symbol);
state.networkId = activeNetworkId; state.networkId = activeNetworkId;
txStatus.restoreWait(); txStatus.restoreWait();
expect(text("wait-tx-summary")).toBe("0.1 " + symbol); expect(text("wait-tx-summary")).toBe("0.1000 " + symbol);
}); });
test("the transaction detail screen's type line and fee", async () => { test("the transaction detail screen's type line and fee", async () => {
+13
View File
@@ -477,6 +477,19 @@ const CONTRACT = [
views: ["confirm-tx"], views: ["confirm-tx"],
restored: false, restored: false,
}, },
// The amount line splits the amount to cut it to four decimals.
{
value: {
pendingTx: {
token: "ETH",
from: ADDRESS,
to: ADDRESS,
amount: 42,
},
},
views: ["confirm-tx"],
restored: false,
},
// A chain id naming no network has no explorer to link to. // A chain id naming no network has no explorer to link to.
{ {
value: { value: {
+12 -6
View File
@@ -306,7 +306,8 @@ describe("Max on an ETH send", () => {
openSend(); openSend();
await pressMax(); await pressMax();
await review(); await review();
expect(text("confirm-amount")).toBe(maxAfter(20n * GWEI) + " ETH"); expect(confirmed.amount).toBe(maxAfter(20n * GWEI));
expect(text("confirm-amount")).toBe("1.2341 ETH");
expect(el("confirm-errors").innerHTML).toBe(""); expect(el("confirm-errors").innerHTML).toBe("");
expect(el("confirm-amount-fee-error").style.visibility).toBe("hidden"); expect(el("confirm-amount-fee-error").style.visibility).toBe("hidden");
expect(canSend()).toBe(true); expect(canSend()).toBe(true);
@@ -322,14 +323,16 @@ describe("Max on an ETH send", () => {
// Kept, the Send screen's amount would be refused for want of funds. // Kept, the Send screen's amount would be refused for want of funds.
mockNode.feeData = fees(30n * GWEI); mockNode.feeData = fees(30n * GWEI);
await review(); await review();
expect(text("confirm-amount")).toBe(maxAfter(30n * GWEI) + " ETH"); expect(confirmed.amount).toBe(maxAfter(30n * GWEI));
expect(text("confirm-amount")).toBe("1.2339 ETH");
expect(canSend()).toBe(true); expect(canSend()).toBe(true);
// And falls when the screen is shown again, as on reopening the popup. // And falls when the screen is shown again, as on reopening the popup.
mockNode.feeData = fees(10n * GWEI); mockNode.feeData = fees(10n * GWEI);
confirmTx.restore(); confirmTx.restore();
await settle(); await settle();
expect(text("confirm-amount")).toBe(maxAfter(10n * GWEI) + " ETH"); expect(confirmed.amount).toBe(maxAfter(10n * GWEI));
expect(text("confirm-amount")).toBe("1.2343 ETH");
expect(canSend()).toBe(true); expect(canSend()).toBe(true);
}); });
@@ -526,7 +529,8 @@ describe("Max on an ETH send", () => {
el("send-amount").handlers.get("input")(); el("send-amount").handlers.get("input")();
mockNode.feeData = fees(30n * GWEI); mockNode.feeData = fees(30n * GWEI);
await review(); await review();
expect(text("confirm-amount")).toBe("0.5 ETH"); expect(confirmed.amount).toBe("0.5");
expect(text("confirm-amount")).toBe("0.5000 ETH");
}); });
}); });
@@ -540,7 +544,8 @@ describe("Max on a token send", () => {
await pressMax(); await pressMax();
expect(el("send-amount").value).toBe("1234.567890123456789012"); expect(el("send-amount").value).toBe("1234.567890123456789012");
await review(); await review();
expect(text("confirm-amount")).toBe("1234.567890123456789012 TOK"); expect(confirmed.amount).toBe("1234.567890123456789012");
expect(text("confirm-amount")).toBe("1234.5678 TOK");
expect(canSend()).toBe(true); expect(canSend()).toBe(true);
}); });
@@ -553,7 +558,8 @@ describe("Max on a token send", () => {
await pressMax(); await pressMax();
expect(el("send-amount").value).toBe("1234.567890123456789012"); expect(el("send-amount").value).toBe("1234.567890123456789012");
await review(); await review();
expect(text("confirm-amount")).toBe("1234.567890123456789012 TOK"); expect(confirmed.amount).toBe("1234.567890123456789012");
expect(text("confirm-amount")).toBe("1234.5678 TOK");
expect(canSend()).toBe(true); expect(canSend()).toBe(true);
}); });