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); });