From cab7cb02b6bb91a9e6fda523ea1ddbea41f11206 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Mon, 19 Oct 2026 17:23:02 +0200 Subject: [PATCH] fix: the wallet's own send shows its amount to four decimal places (closes #522) 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 --- README.md | 66 +++-- src/popup/viewRouter.js | 10 +- src/popup/views/confirmTx.js | 42 ++- tests/confirmTxAmount.test.js | 415 +++++++++++++++++++++++++++ tests/confirmTxNetworkSwitch.test.js | 4 +- tests/e2e/run.js | 3 +- tests/nativeCurrency.test.js | 6 +- tests/persistedFieldContract.test.js | 13 + tests/sendMax.test.js | 18 +- 9 files changed, 527 insertions(+), 50 deletions(-) create mode 100644 tests/confirmTxAmount.test.js diff --git a/README.md b/README.md index 2bee4fd..24e1d92 100644 --- a/README.md +++ b/README.md @@ -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, 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 -`Current balance` (`src/popup/views/send.js`), and the balance and network fee -on the confirmation screen for the wallet's own send -(`src/popup/views/confirmTx.js`). Both screens render a network fee through -`formatFee()` in `src/popup/views/helpers.js`, which prices the exact fee in USD -rather than its truncated figure, so the same fee reads the same on both, USD -value included. Balances are stored exactly (`src/shared/balances.js`), whatever -decimals a token declares, so a balance below the floor reaches these screens as -it is. The history list (`src/shared/transactions.js`) uses the unfloored one: -the transaction detail view is the authoritative record and already shows exact -precision. The balance 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. +`Current balance` (`src/popup/views/send.js`), and the amount, balance and +network fee on the confirmation screen for the wallet's own send +(`src/popup/views/confirmTx.js`), which hands the wait, success and error +screens the amount as it showed it. The transaction is still signed for the +exact amount, and its USD value is priced from it. Both screens render a network +fee through `formatFee()` in `src/popup/views/helpers.js`, which prices the +exact fee in USD rather than its truncated figure, so the same fee reads the +same on both, USD value included. Balances are stored exactly +(`src/shared/balances.js`), whatever decimals a token declares, so a balance +below the floor reaches these screens as it is. The history list +(`src/shared/transactions.js`) uses the unfloored one: the transaction detail +view is the authoritative record and already shows exact precision. The balance +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 -approval screen states base units instead of a quantity — see Unknown token -scale below — and no truncation happens at all. +On the approval screen and the wait, success and error screens that carry its +amount forward, the floor applies only where the token's scale is known. Where +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 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) - From: blockie + color dot + full address + etherscan link + wallet title - To: blockie + color dot + full address + etherscan link + ENS name - - Amount: value + symbol (USD in parentheses). An ETH amount Send's "Max" - filled in is worked out again from this screen's own fee estimate when it - arrives, as the balance minus the reserve, and 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 + - Amount: value + symbol (USD in parentheses), the value to four decimal + places with the nonzero floor, as on TxApproval (see Display Consistency), + however Send's amount field wrote it (`.5` and `0.5` both read `0.5000`). + For a token whose scale is unknown, which cannot be sent, the value is the + text from Send's amount field cut the same way (`.5` reads `.5000`). An + ETH amount Send's "Max" filled in is worked out again from this screen's + own fee estimate when it arrives, as the balance minus the reserve, and + 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 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 @@ -2350,9 +2360,11 @@ view would leave a wallet one click from deletion. - From: color dot + full address + etherscan link - Contract: color dot + full address + etherscan link, token symbol label if known; for a contract creation, the same sentence as on WaitTx - - Value: amount in ETH (4 decimal places, USD in parentheses) - - Network fee (max): gas limit × fee per gas in ETH (4 decimal places, USD - in parentheses), with the gas limit and the fee per gas in gwei below it + - Value: amount in ETH (4 decimal places with the nonzero floor, USD in + parentheses) + - 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 - Raw data: full calldata displayed inline (shown if present) - Password input and an error line diff --git a/src/popup/viewRouter.js b/src/popup/viewRouter.js index 5f1d7bf..46bda0d 100644 --- a/src/popup/viewRouter.js +++ b/src/popup/viewRouter.js @@ -127,16 +127,18 @@ function isRenderableDecoded(value) { } // The pending transaction confirmTx.show() renders: `token` reaches -// renderAddressHtml() when it is not "ETH", and `from`/`to` reach -// addressTitle(), makeBlockie() and getLocalWarnings(). `chainId`, which -// show() fills in when it is absent, picks the network whose explorer the -// address links open, so it has to name one. +// renderAddressHtml() when it is not "ETH", `from`/`to` reach +// addressTitle(), makeBlockie() and getLocalWarnings(), and `amount` reaches +// truncateAmountNeverZero(), which splits it. `chainId`, which show() fills in +// when it is absent, picks the network whose explorer the address links open, +// so it has to name one. function isRenderablePendingTx(value) { return ( isRecord(value) && isText(value.token) && isAddressText(value.from) && isAddressText(value.to) && + isText(value.amount) && (!value.chainId || SUPPORTED_CHAIN_IDS.has(value.chainId)) ); } diff --git a/src/popup/views/confirmTx.js b/src/popup/views/confirmTx.js index a0b1715..aaaf5b8 100644 --- a/src/popup/views/confirmTx.js +++ b/src/popup/views/confirmTx.js @@ -45,8 +45,8 @@ const { transferAmountUnits, } = require("../../shared/transferAmount"); const { assertWithinCeilings } = require("../../shared/approvalVerify"); -// The balance line and the fee reserve go through it, as the approval screen's -// amounts do. +// The amount, the balance line and the fee reserve go through it, as the +// approval screen's amounts do. const { truncateAmountNeverZero } = require("../../shared/amountDisplay"); const { CODES, @@ -100,8 +100,32 @@ function valueWithUsd(text, usdAmount) { return text; } -// The Amount line, with its USD value. A max ETH send's line is drawn again -// once its amount is re-derived from the fee estimate. +// The amount as this screen and the wait, success and error screens show it, +// 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) { const isErc20 = txInfo.token !== "ETH"; const rawSymbol = isErc20 @@ -110,7 +134,7 @@ function renderAmount(txInfo) { const price = getPrice(isErc20 ? rawSymbol : "ETH", txInfo.chainId); const amountUsd = price ? parseFloat(txInfo.amount) * price : null; $("confirm-amount").textContent = valueWithUsd( - txInfo.amount + " " + displaySymbol(rawSymbol), + shownAmount(txInfo) + " " + displaySymbol(rawSymbol), amountUsd, ); } @@ -698,6 +722,10 @@ function init(_ctx) { $("btn-confirm-send").disabled = true; $("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; try { const signer = getSignerForAddress(wallet, addr, decryptedSecret); @@ -714,7 +742,7 @@ function init(_ctx) { // Note: JS strings are immutable; this nulls the reference but // the original string may persist in memory until GC. decryptedSecret = null; - txStatus.showWait(pendingTx, tx.hash); + txStatus.showWait(shownTx, tx.hash); } catch (e) { decryptedSecret = null; // A gas limit or fee over its bound is refused before anything is @@ -730,7 +758,7 @@ function init(_ctx) { return; } const hash = tx ? tx.hash : null; - txStatus.showError(pendingTx, hash, e.shortMessage || e.message); + txStatus.showError(shownTx, hash, e.shortMessage || e.message); } finally { $("btn-confirm-send").disabled = false; $("btn-confirm-send").classList.remove("text-muted"); diff --git a/tests/confirmTxAmount.test.js b/tests/confirmTxAmount.test.js new file mode 100644 index 0000000..80b0822 --- /dev/null +++ b/tests/confirmTxAmount.test.js @@ -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"); + }); +}); diff --git a/tests/confirmTxNetworkSwitch.test.js b/tests/confirmTxNetworkSwitch.test.js index 6a998a7..337849f 100644 --- a/tests/confirmTxNetworkSwitch.test.js +++ b/tests/confirmTxNetworkSwitch.test.js @@ -333,7 +333,7 @@ function expectNativeTransferOn(networkId) { "Native " + symbol + " transfer", ); 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( "5.0000 " + symbol + usdOn(networkId, "$10,000.00"), @@ -649,7 +649,7 @@ describe.each([ `href="${explorerUrl}/address/${TOKEN}"`, ); 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( "50.0000 USDC" + usdOn(reviewedOn, "$50.00"), diff --git a/tests/e2e/run.js b/tests/e2e/run.js index 898ec7b..56e2e4e 100644 --- a/tests/e2e/run.js +++ b/tests/e2e/run.js @@ -2915,9 +2915,10 @@ async function goToTokenConfirm(env) { amount: TOKEN_AMOUNT, }); await waitForEstimate(env.page); + // TOKEN_AMOUNT to four decimal places (README.md, Display Consistency). const shown = await shownAmount(env.page); assert( - shown === TOKEN_AMOUNT + " " + STUB_TOKEN.symbol, + shown === "0.2500 " + STUB_TOKEN.symbol, "the confirmation screen is not showing the amount that was entered: " + JSON.stringify(shown), ); diff --git a/tests/nativeCurrency.test.js b/tests/nativeCurrency.test.js index 42bfd1a..ae31109 100644 --- a/tests/nativeCurrency.test.js +++ b/tests/nativeCurrency.test.js @@ -245,7 +245,7 @@ describe.each([ test("the value", async () => { await confirmSend("0.1"); 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); expect(text("approve-tx-value")).toBe("0.0100 " + symbol); }); @@ -429,10 +429,10 @@ describe.each([ await global.document .getElementById("btn-confirm-send") .handlers.get("click")(); - expect(text("wait-tx-summary")).toBe("0.1 " + symbol); + expect(text("wait-tx-summary")).toBe("0.1000 " + symbol); state.networkId = activeNetworkId; 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 () => { diff --git a/tests/persistedFieldContract.test.js b/tests/persistedFieldContract.test.js index 977507c..cabeaca 100644 --- a/tests/persistedFieldContract.test.js +++ b/tests/persistedFieldContract.test.js @@ -477,6 +477,19 @@ const CONTRACT = [ views: ["confirm-tx"], 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. { value: { diff --git a/tests/sendMax.test.js b/tests/sendMax.test.js index 997e43e..af8943d 100644 --- a/tests/sendMax.test.js +++ b/tests/sendMax.test.js @@ -306,7 +306,8 @@ describe("Max on an ETH send", () => { openSend(); await pressMax(); 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-amount-fee-error").style.visibility).toBe("hidden"); 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. mockNode.feeData = fees(30n * GWEI); 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); // And falls when the screen is shown again, as on reopening the popup. mockNode.feeData = fees(10n * GWEI); confirmTx.restore(); 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); }); @@ -526,7 +529,8 @@ describe("Max on an ETH send", () => { el("send-amount").handlers.get("input")(); mockNode.feeData = fees(30n * GWEI); 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(); expect(el("send-amount").value).toBe("1234.567890123456789012"); 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); }); @@ -553,7 +558,8 @@ describe("Max on a token send", () => { await pressMax(); expect(el("send-amount").value).toBe("1234.567890123456789012"); 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); });