fix: the wallet's own send shows its amount to four decimal places (closes #522)
ConfirmTx and the wait, success and error screens printed the text from Send's amount field as typed or as Max filled it in: `0.5 ETH`, or `1.234147890123456789 ETH` after Max, where TxApproval shows a site's send as `0.5000 ETH`. ConfirmTx now shows the amount through truncateAmountNeverZero(), as TxApproval does, and hands the status screens a copy carrying that text. The transaction is still signed for the exact amount, which stays on the pending transaction, and the USD value is priced from it. A saved confirmation screen whose amount is not text is no longer redrawn, since the amount is now split. Model: opus-5-5
This commit is contained in:
@@ -1082,19 +1082,21 @@ 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
|
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
|
approval screen states base units instead of a quantity — see Unknown token
|
||||||
@@ -1757,7 +1759,9 @@ 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
|
||||||
|
places with the nonzero floor, as on TxApproval (see Display Consistency),
|
||||||
|
however many places Send's amount field held. An ETH amount Send's "Max"
|
||||||
filled in is worked out again from this screen's own fee estimate when it
|
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
|
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
|
with that estimate's fee fields, so a fee fetched again at signing cannot
|
||||||
|
|||||||
@@ -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))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -101,7 +101,9 @@ function valueWithUsd(text, usdAmount) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The Amount line, with its USD value. A max ETH send's line is drawn again
|
// 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.
|
// once its amount is re-derived from the fee estimate. Only the text on screen
|
||||||
|
// is cut to four decimals: the USD value is priced from the exact amount, and
|
||||||
|
// txInfo.amount, which is what gets signed, is left as it is.
|
||||||
function renderAmount(txInfo) {
|
function renderAmount(txInfo) {
|
||||||
const isErc20 = txInfo.token !== "ETH";
|
const isErc20 = txInfo.token !== "ETH";
|
||||||
const rawSymbol = isErc20
|
const rawSymbol = isErc20
|
||||||
@@ -110,7 +112,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),
|
truncateAmountNeverZero(txInfo.amount) + " " + displaySymbol(rawSymbol),
|
||||||
amountUsd,
|
amountUsd,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -698,6 +700,13 @@ 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: truncateAmountNeverZero(pendingTx.amount),
|
||||||
|
};
|
||||||
let tx;
|
let tx;
|
||||||
try {
|
try {
|
||||||
const signer = getSignerForAddress(wallet, addr, decryptedSecret);
|
const signer = getSignerForAddress(wallet, addr, decryptedSecret);
|
||||||
@@ -714,7 +723,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 +739,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");
|
||||||
|
|||||||
@@ -0,0 +1,387 @@
|
|||||||
|
// 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"));
|
||||||
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
@@ -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),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user