networks.js gives each network a nativeCurrency (ETH, SepoliaETH) and nothing read it: every screen wrote a hardcoded ETH, so on Sepolia the balance, the value and the fee all read ETH. A nativeCurrency() helper in the popup's helpers.js returns the active network's, and every native balance, value, fee, reserve, type line and history entry is labelled with it. The "ETH" that selectedToken and txInfo.token hold is the native token's id and is unchanged. The confirmation screen's not-enough-to-pay-the-fee sentence now names the coin to add, so show() sets it. Model: opus-5-5
246 lines
7.3 KiB
JavaScript
246 lines
7.3 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.
|
|
|
|
"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() {},
|
|
};
|
|
}
|
|
|
|
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 { balanceLinesForAddress } = require("../src/popup/views/helpers");
|
|
|
|
const HOLDER = "0x" + "a".repeat(40);
|
|
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
|
|
|
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 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 +
|
|
".",
|
|
);
|
|
});
|
|
});
|