Send accepted any amount parseFloat() read as above zero, so "1e-3", "1abc" and an amount with a nonzero digit past the token's decimal places reached ConfirmTx, which then refused them. isValidAmount() in src/shared/txValidation.js is now the one check: Review and Max on Send call it with the token's decimal places, and validateTransfer() calls it for ConfirmTx, which now also refuses a zero amount and a token amount with a nonzero digit past the token's places. Zeros past those places are accepted, since the amount encoded for the transfer is the same. 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);
|
|
});
|
|
});
|