Send's Review accepted any amount parseFloat() read as above zero, so 1e-3, 1abc and amounts with too many decimal places reached ConfirmTx, which then refused them or failed to estimate the fee. isValidAmount() in src/shared/txValidation.js is now the one amount check, used by Send's Review and Max and by validateTransfer() for ConfirmTx: plain digits with at most one decimal point, above zero, nothing but zeros past the token's places. Zeros past those places are accepted, since the transfer encodes the amount exactly. Send now reads the token's decimal places before the ENS lookup, because the check needs them. Judgement call: ConfirmTx now also refuses zero, as Send already did. Model: opus-5-5
214 lines
6.4 KiB
JavaScript
214 lines
6.4 KiB
JavaScript
// The Send screen refuses every amount the confirmation screen would refuse,
|
|
// by the same check (https://git.eeqj.de/sneak/AutistMask/issues/532).
|
|
//
|
|
// Send used to accept anything parseFloat() read as above zero, so "1e-3",
|
|
// "1abc" and an amount with more decimal places than the token has went on to
|
|
// a confirmation screen that then refused them. Both screens now call
|
|
// isValidAmount() in src/shared/txValidation.js.
|
|
//
|
|
// Driven through the real Send review handler and the real confirmation
|
|
// screen, with only the node and the DOM stubbed.
|
|
|
|
"use strict";
|
|
|
|
jest.mock("../src/shared/log", () => ({
|
|
log: {
|
|
debugf: () => {},
|
|
infof: () => {},
|
|
warnf: () => {},
|
|
errorf: () => {},
|
|
},
|
|
debugFetch: jest.fn(),
|
|
setRuntimeDebug: () => {},
|
|
isDebug: () => false,
|
|
}));
|
|
|
|
// What the confirmation screen asks the node for.
|
|
const mockProvider = {
|
|
getFeeData: async () => ({
|
|
maxFeePerGas: 2000000000n,
|
|
gasPrice: 1000000000n,
|
|
}),
|
|
estimateGas: async () => 21000n,
|
|
getCode: async () => "0x",
|
|
getTransactionCount: async () => 1,
|
|
getBalance: async () => 0n,
|
|
};
|
|
|
|
jest.mock("../src/shared/balances", () => {
|
|
const actual = jest.requireActual("../src/shared/balances");
|
|
return { ...actual, getProvider: () => mockProvider };
|
|
});
|
|
|
|
// The confirmation screen's Etherscan label lookup 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,
|
|
addEventListener(name, fn) {
|
|
handlers.set(name, fn);
|
|
},
|
|
appendChild(child) {
|
|
return child;
|
|
},
|
|
querySelectorAll: () => [],
|
|
querySelector: () => null,
|
|
remove() {},
|
|
focus() {},
|
|
};
|
|
}
|
|
|
|
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 { state } = require("../src/shared/state");
|
|
const send = require("../src/popup/views/send");
|
|
const confirmTx = require("../src/popup/views/confirmTx");
|
|
|
|
const HOLDER = "0x" + "a".repeat(40);
|
|
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
|
// A token with 6 decimal places, as USDC has.
|
|
const TOKEN = "0x" + "d".repeat(40);
|
|
const TOKEN_BY_SYMBOL = { ETH: "ETH", TOK: TOKEN };
|
|
|
|
function el(id) {
|
|
return global.document.getElementById(id);
|
|
}
|
|
|
|
// Press Review on the Send screen with `amount` entered, for a send of `token`
|
|
// ("ETH" or TOKEN), and return what it handed the confirmation screen, or null
|
|
// when it handed nothing over.
|
|
async function review(amount, token) {
|
|
let handed = null;
|
|
send.init({ showConfirmTx: (info) => (handed = info) });
|
|
state.selectedToken = token;
|
|
el("send-to").value = RECIPIENT;
|
|
el("send-amount").value = amount;
|
|
await el("btn-send-review").handlers.get("click")();
|
|
return handed;
|
|
}
|
|
|
|
// Show the confirmation screen for the send Review hands over for 0.5, with
|
|
// `amount` in its place, and let its fee estimate settle.
|
|
async function confirm(amount, token) {
|
|
const txInfo = await review("0.5", token);
|
|
confirmTx.show({ ...txInfo, amount });
|
|
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
|
|
}
|
|
|
|
beforeEach(() => {
|
|
elements.clear();
|
|
state.wallets = [
|
|
{
|
|
name: "Wallet 1",
|
|
addresses: [
|
|
{
|
|
address: HOLDER,
|
|
balance: "1.0",
|
|
tokenBalances: [
|
|
{
|
|
address: TOKEN,
|
|
name: "Token",
|
|
symbol: "TOK",
|
|
decimals: 6,
|
|
balance: "250.0",
|
|
holders: 50000,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
];
|
|
state.selectedWallet = 0;
|
|
state.selectedAddress = 0;
|
|
state.trackedTokens = [
|
|
{ address: TOKEN, symbol: "TOK", name: "Token", decimals: 6 },
|
|
];
|
|
state.fraudContracts = [];
|
|
state.selectedToken = null;
|
|
state.currentView = null;
|
|
});
|
|
|
|
describe.each([
|
|
["1e-3", "ETH"],
|
|
["1e-3", "TOK"],
|
|
["1abc", "ETH"],
|
|
// 19 decimal places; ETH has 18.
|
|
["0.0000000000000000001", "ETH"],
|
|
// 7 decimal places; the token has 6.
|
|
["0.0000001", "TOK"],
|
|
["0", "ETH"],
|
|
["-1", "ETH"],
|
|
])("%s %s", (amount, symbol) => {
|
|
const token = TOKEN_BY_SYMBOL[symbol];
|
|
|
|
test("Send refuses it with a flash message and opens nothing", async () => {
|
|
expect(await review(amount, token)).toBe(null);
|
|
expect(el("flash-msg").textContent).toBe(
|
|
"Please enter a valid amount.",
|
|
);
|
|
});
|
|
|
|
test("the confirmation screen refuses it too", async () => {
|
|
await confirm(amount, token);
|
|
expect(el("confirm-errors").innerHTML).toContain(
|
|
"Please enter a valid amount to send.",
|
|
);
|
|
expect(el("btn-confirm-send").disabled).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe.each([
|
|
["0.5", "ETH"],
|
|
["0.5", "TOK"],
|
|
// Exactly the 6 decimal places the token has.
|
|
["0.000001", "TOK"],
|
|
// Only zeros past the token's 6 places, and past ETH's 18.
|
|
["1.0000000", "TOK"],
|
|
["0.5000000000000000000", "ETH"],
|
|
])("%s %s", (amount, symbol) => {
|
|
const token = TOKEN_BY_SYMBOL[symbol];
|
|
|
|
test("Send hands it to the confirmation screen", async () => {
|
|
expect((await review(amount, token)).amount).toBe(amount);
|
|
expect(el("flash-msg").textContent).toBe("");
|
|
});
|
|
|
|
test("the confirmation screen lets it be sent", async () => {
|
|
await confirm(amount, token);
|
|
expect(el("confirm-errors").innerHTML).toBe("");
|
|
expect(el("btn-confirm-send").disabled).toBe(false);
|
|
});
|
|
});
|