networks.js gives each network a nativeCurrency (ETH, SepoliaETH) and nothing read it: every screen wrote ETH. A nativeCurrency() helper returns the active network's, and every native balance, value, fee, reserve, type line, history entry, the contract-recipient warning and the fee-limit refusal use it. The "ETH" that selectedToken and txInfo.token hold is the native token's id and is unchanged. A token reporting any network's nativeCurrency is a spoof, as one reporting ETH already was, and the transaction detail screen calls an entry a token transfer when it has a token contract, not by its symbol. Model: opus-5-5
324 lines
10 KiB
JavaScript
324 lines
10 KiB
JavaScript
// The native token's label on the screens that show a native amount.
|
|
//
|
|
// src/shared/networks.js gives each network a nativeCurrency, `ETH` on mainnet
|
|
// and `SepoliaETH` on Sepolia, and nothing read it: every screen wrote a
|
|
// hardcoded "ETH", so on Sepolia the balance, the value and the fee all read
|
|
// ETH (https://git.eeqj.de/sneak/AutistMask/issues/372). Each line is asserted
|
|
// on both networks, through the real Send, confirmation and approval screens,
|
|
// with only the node and the DOM stubbed. So is that a token cannot pass for
|
|
// the native token by reporting its label.
|
|
|
|
"use strict";
|
|
|
|
jest.mock("ethers", () => {
|
|
const actual = jest.requireActual("ethers");
|
|
class StubProvider {
|
|
async lookupAddress() {
|
|
return null;
|
|
}
|
|
// 10 gwei expected, 20 gwei reserved per gas.
|
|
async getFeeData() {
|
|
return { maxFeePerGas: 20000000000n, gasPrice: 10000000000n };
|
|
}
|
|
async estimateGas() {
|
|
return 21000n;
|
|
}
|
|
async getCode() {
|
|
return "0x";
|
|
}
|
|
async getTransactionCount() {
|
|
return 1;
|
|
}
|
|
}
|
|
return {
|
|
...actual,
|
|
JsonRpcProvider: StubProvider,
|
|
Network: { from: () => ({}) },
|
|
};
|
|
});
|
|
|
|
jest.mock("../src/shared/log", () => ({
|
|
log: {
|
|
debugf: () => {},
|
|
infof: () => {},
|
|
warnf: () => {},
|
|
errorf: () => {},
|
|
},
|
|
debugFetch: jest.fn(async () => ({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ items: [] }),
|
|
})),
|
|
urlOrigin: () => "",
|
|
setRuntimeDebug: () => {},
|
|
isDebug: () => false,
|
|
}));
|
|
|
|
global.fetch = jest.fn(() => {
|
|
throw new Error("tests must not perform network requests");
|
|
});
|
|
|
|
// The approval the background hands the approval screen. Set per test.
|
|
let approvalDetails = null;
|
|
|
|
const { makeStorageStub } = require("./support/storageStub");
|
|
global.chrome = {
|
|
storage: makeStorageStub(),
|
|
runtime: {
|
|
connect: () => ({
|
|
postMessage() {},
|
|
disconnect() {},
|
|
onDisconnect: { addListener() {} },
|
|
}),
|
|
sendMessage(message, callback) {
|
|
callback(
|
|
message.type === "AUTISTMASK_GET_APPROVAL"
|
|
? approvalDetails
|
|
: undefined,
|
|
);
|
|
},
|
|
},
|
|
};
|
|
|
|
// 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() {},
|
|
// Views reach for .parentElement to hide whole sections.
|
|
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 { state } = require("../src/shared/state");
|
|
const { NETWORKS } = require("../src/shared/networks");
|
|
const { clearPrices } = require("../src/shared/prices");
|
|
const send = require("../src/popup/views/send");
|
|
const confirmTx = require("../src/popup/views/confirmTx");
|
|
const approval = require("../src/popup/views/approval");
|
|
const transactionDetail = require("../src/popup/views/transactionDetail");
|
|
const { balanceLinesForAddress } = require("../src/popup/views/helpers");
|
|
const { filterTransactions } = require("../src/shared/transactions");
|
|
|
|
const HOLDER = "0x" + "a".repeat(40);
|
|
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
|
// A token contract that is not in the bundled token list.
|
|
const TOKEN_CONTRACT = "0xd05339f9ea5ab9d9f03b9d57f671d2abd1f55c82";
|
|
|
|
function text(id) {
|
|
return global.document.getElementById(id).textContent;
|
|
}
|
|
|
|
// Press Review on the Send screen for a native send of `amount`, and show the
|
|
// confirmation screen it leads to with its fee estimate settled.
|
|
async function confirmSend(amount) {
|
|
let txInfo = null;
|
|
send.init({ showConfirmTx: (info) => (txInfo = info) });
|
|
state.selectedToken = "ETH";
|
|
global.document.getElementById("send-to").value = RECIPIENT;
|
|
global.document.getElementById("send-amount").value = amount;
|
|
await global.document
|
|
.getElementById("btn-send-review")
|
|
.handlers.get("click")();
|
|
confirmTx.show(txInfo);
|
|
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
|
|
}
|
|
|
|
// The approval screen for a dApp transaction sending 0.01 of the native token
|
|
// with 21000 gas at up to 20 gwei, on the active network.
|
|
async function approveTx() {
|
|
approvalDetails = {
|
|
type: "tx",
|
|
origin: "https://dapp.example",
|
|
approvedFrom: HOLDER,
|
|
approvedTx: {
|
|
to: RECIPIENT,
|
|
value: "10000000000000000",
|
|
data: "0x",
|
|
chainId: NETWORKS[state.networkId].chainId,
|
|
gasLimit: "21000",
|
|
maxFeePerGas: "20000000000",
|
|
nonce: 0,
|
|
},
|
|
};
|
|
await approval.show("1");
|
|
}
|
|
|
|
describe.each([
|
|
["mainnet", "ETH"],
|
|
["sepolia", "SepoliaETH"],
|
|
])("on %s the native token reads %s", (networkId, symbol) => {
|
|
beforeEach(() => {
|
|
elements.clear();
|
|
clearPrices();
|
|
state.networkId = networkId;
|
|
state.wallets = [
|
|
{
|
|
name: "Wallet 1",
|
|
addresses: [{ address: HOLDER, balance: "1.5" }],
|
|
},
|
|
];
|
|
state.selectedWallet = 0;
|
|
state.selectedAddress = 0;
|
|
state.trackedTokens = [];
|
|
state.fraudContracts = [];
|
|
state.currentView = null;
|
|
});
|
|
|
|
test("the balance", async () => {
|
|
const addr = state.wallets[0].addresses[0];
|
|
expect(balanceLinesForAddress(addr, [], false)).toContain(
|
|
`<span>${symbol}</span><span>1.5000</span>`,
|
|
);
|
|
state.selectedToken = "ETH";
|
|
send.updateSendBalance();
|
|
expect(text("send-balance")).toBe("Current balance: 1.5000 " + symbol);
|
|
await confirmSend("0.1");
|
|
expect(text("confirm-balance")).toBe("1.5000 " + symbol);
|
|
});
|
|
|
|
test("the value", async () => {
|
|
await confirmSend("0.1");
|
|
expect(text("confirm-type")).toBe("Native " + symbol + " transfer");
|
|
expect(text("confirm-amount")).toBe("0.1 " + symbol);
|
|
await approveTx();
|
|
expect(text("approve-tx-value")).toBe("0.0100 " + symbol);
|
|
});
|
|
|
|
test("the fee", async () => {
|
|
await confirmSend("0.1");
|
|
// 21000 gas at 10 gwei expected, at 20 gwei reserved.
|
|
expect(text("confirm-fee-amount")).toBe("~0.0002 " + symbol);
|
|
expect(text("confirm-fee-reserve")).toBe(
|
|
"up to 0.0004 " + symbol + " reserved",
|
|
);
|
|
expect(text("confirm-gas-error")).toContain(
|
|
"You do not have enough " + symbol + " to pay the network fee",
|
|
);
|
|
await approveTx();
|
|
expect(text("approve-tx-fee")).toBe("0.0004 " + symbol);
|
|
});
|
|
|
|
test("the contract-recipient warning", async () => {
|
|
await confirmSend("0.1");
|
|
expect(text("confirm-contract-warning")).toContain(
|
|
"Sending " + symbol + " or tokens directly to a contract",
|
|
);
|
|
});
|
|
|
|
// A token reports whatever symbol it likes. One reporting the label the
|
|
// wallet shows its native token under, on this network or any other, is
|
|
// a fake, exactly as one reporting `ETH` always was.
|
|
test.each(["ETH", symbol])(
|
|
"a token claiming %s is dropped from the history and the Send selector",
|
|
(claim) => {
|
|
const result = filterTransactions(
|
|
[
|
|
{
|
|
hash: "0x" + "1".repeat(64),
|
|
symbol: claim,
|
|
contractAddress: TOKEN_CONTRACT,
|
|
holders: 900000,
|
|
valueGwei: null,
|
|
isContractCall: false,
|
|
},
|
|
],
|
|
{ hideSpoofedSymbols: true },
|
|
);
|
|
expect(result.transactions).toEqual([]);
|
|
expect(result.newFraudContracts).toEqual([TOKEN_CONTRACT]);
|
|
|
|
send.renderSendTokenSelect({
|
|
address: HOLDER,
|
|
tokenBalances: [
|
|
{
|
|
address: TOKEN_CONTRACT,
|
|
symbol: claim,
|
|
decimals: 18,
|
|
balance: "5",
|
|
holders: 900000,
|
|
},
|
|
],
|
|
});
|
|
expect(
|
|
global.document.getElementById("send-token").children,
|
|
).toEqual([]);
|
|
},
|
|
);
|
|
|
|
// The detail screen tells the two apart by the token contract, which only
|
|
// a token transfer has, so a token reporting the native label still reads
|
|
// as a token transfer.
|
|
test("the transaction detail screen's type line", () => {
|
|
const entry = {
|
|
hash: "0x" + "2".repeat(64),
|
|
from: RECIPIENT,
|
|
to: HOLDER,
|
|
value: "1.0000",
|
|
exactValue: "1.0",
|
|
symbol,
|
|
timestamp: 1790000000,
|
|
isError: false,
|
|
direction: "received",
|
|
directionLabel: "Received",
|
|
};
|
|
transactionDetail.show({ ...entry, contractAddress: null });
|
|
expect(text("tx-detail-type")).toBe("Native " + symbol + " Transfer");
|
|
transactionDetail.show({ ...entry, contractAddress: TOKEN_CONTRACT });
|
|
expect(text("tx-detail-type")).toBe("ERC-20 Token Transfer");
|
|
});
|
|
|
|
test("the insufficient-balance error", async () => {
|
|
await confirmSend("2");
|
|
expect(
|
|
global.document.getElementById("confirm-errors").innerHTML,
|
|
).toContain(
|
|
"You have 1.5000 " +
|
|
symbol +
|
|
" but are trying to send 2 " +
|
|
symbol +
|
|
".",
|
|
);
|
|
});
|
|
});
|