A transaction with no `to` showed a blank address, with a colour dot whose colour was `undefined`, on the wait, success and error screens, the transaction detail view and the history rows on Home, AddressDetail and AddressToken. The approval screen showed "(contract creation)". All of them now say "This transaction creates a new contract. It has no recipient." The three history lists draw a row's counterparty lines through one helper in helpers.js. A transaction with a real `to` is unchanged. The new test drives each screen and list both ways. Model: opus-5-5
316 lines
10 KiB
JavaScript
316 lines
10 KiB
JavaScript
// The recipient line of a contract creation
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/250).
|
|
//
|
|
// A transaction with no `to` creates a contract. The approval screen, the
|
|
// wait, success and error screens, the transaction detail view and the
|
|
// transaction history rows each say so in a sentence, where they used to show
|
|
// a blank line (an empty address, with a colour dot whose colour was
|
|
// `undefined`) or, on the approval screen, "(contract creation)". A
|
|
// transaction with a real `to` still shows that address.
|
|
//
|
|
// Driven against a minimal DOM stub in the shape
|
|
// tests/typedDataPermit.test.js uses.
|
|
|
|
jest.mock("../src/shared/log", () => ({
|
|
log: {
|
|
debugf: () => {},
|
|
infof: () => {},
|
|
warnf: () => {},
|
|
errorf: () => {},
|
|
},
|
|
// The transaction detail view fetches on-chain details after drawing; an
|
|
// answer that is not ok leaves the drawn lines as they are.
|
|
debugFetch: async () => ({ ok: false }),
|
|
setRuntimeDebug: () => {},
|
|
isDebug: () => false,
|
|
}));
|
|
|
|
// The wait screen polls for a receipt; this one never arrives.
|
|
jest.mock("../src/shared/balances", () => ({
|
|
getProvider: () => ({ getTransactionReceipt: () => new Promise(() => {}) }),
|
|
refreshBalances: () => {},
|
|
}));
|
|
|
|
// The history lists ask the explorer for their transactions and resolve ENS
|
|
// names for them; here the explorer answers with mockHistory and no name
|
|
// resolves.
|
|
let mockHistory = [];
|
|
jest.mock("../src/shared/transactions", () => ({
|
|
...jest.requireActual("../src/shared/transactions"),
|
|
fetchRecentTransactions: async () => mockHistory,
|
|
}));
|
|
jest.mock("../src/shared/ens", () => ({
|
|
...jest.requireActual("../src/shared/ens"),
|
|
resolveEnsNames: async () => new Map(),
|
|
}));
|
|
|
|
globalThis.chrome = {
|
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
|
};
|
|
|
|
const { state } = require("../src/shared/state");
|
|
const approval = require("../src/popup/views/approval");
|
|
const txStatus = require("../src/popup/views/txStatus");
|
|
const transactionDetail = require("../src/popup/views/transactionDetail");
|
|
const home = require("../src/popup/views/home");
|
|
const addressDetail = require("../src/popup/views/addressDetail");
|
|
const addressToken = require("../src/popup/views/addressToken");
|
|
|
|
const SENTENCE =
|
|
"This transaction creates a new contract. It has no recipient.";
|
|
|
|
const FROM = "0x0000000000000000000000000000000000000a11";
|
|
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
|
const TX_HASH =
|
|
"0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84";
|
|
// Init code for a contract creation's data.
|
|
const INIT_CODE = "0x600160005500";
|
|
|
|
function makeElement(id) {
|
|
const classes = new Set();
|
|
const el = {
|
|
id,
|
|
textContent: "",
|
|
value: "",
|
|
innerHTML: "",
|
|
disabled: false,
|
|
style: {},
|
|
dataset: {},
|
|
classList: {
|
|
add: (...names) => names.forEach((n) => classes.add(n)),
|
|
remove: (...names) => names.forEach((n) => classes.delete(n)),
|
|
contains: (n) => classes.has(n),
|
|
toggle: (n, force) => {
|
|
const on = force === undefined ? !classes.has(n) : force;
|
|
if (on) classes.add(n);
|
|
else classes.delete(n);
|
|
return on;
|
|
},
|
|
},
|
|
addEventListener: () => {},
|
|
querySelectorAll: () => [],
|
|
appendChild: () => {},
|
|
};
|
|
// Views reach for .parentElement to hide whole sections.
|
|
Object.defineProperty(el, "parentElement", {
|
|
get: () => node(id + "-parent"),
|
|
});
|
|
return el;
|
|
}
|
|
|
|
function makeDocument() {
|
|
const els = new Map();
|
|
return {
|
|
getElementById(id) {
|
|
// The debug banner is created on demand by helpers.js; absent
|
|
// is the state a non-debug, non-testnet popup is in.
|
|
if (id === "debug-banner") return null;
|
|
if (!els.has(id)) els.set(id, makeElement(id));
|
|
return els.get(id);
|
|
},
|
|
createElement: () => makeElement("created"),
|
|
body: { prepend: () => {} },
|
|
};
|
|
}
|
|
|
|
function node(id) {
|
|
return globalThis.document.getElementById(id);
|
|
}
|
|
|
|
// The line a transaction with a real `to` shows: that address, and nothing
|
|
// left over from an empty one.
|
|
function expectAddressLine(html) {
|
|
expect(html).toContain(RECIPIENT);
|
|
expect(html).not.toContain(SENTENCE);
|
|
expect(html).not.toContain("undefined");
|
|
}
|
|
|
|
beforeEach(() => {
|
|
globalThis.document = makeDocument();
|
|
globalThis.window = { location: { search: "" } };
|
|
state.wallets = [];
|
|
state.trackedTokens = [];
|
|
state.viewData = {};
|
|
state.viewStack = [];
|
|
state.currentView = null;
|
|
txStatus.init({ doRefreshAndRender: () => {} });
|
|
});
|
|
|
|
afterEach(() => {
|
|
txStatus.endWait();
|
|
});
|
|
|
|
// Open the transaction approval screen the way the popup does: the background
|
|
// hands over the populated transaction and show() draws it.
|
|
async function openTxApproval(to, data) {
|
|
globalThis.chrome.runtime = {
|
|
connect: () => ({ postMessage: () => {} }),
|
|
sendMessage: (msg, reply) => {
|
|
if (!reply) return;
|
|
if (msg.type !== "AUTISTMASK_GET_APPROVAL") return reply(null);
|
|
reply({
|
|
type: "tx",
|
|
hostname: "dapp.example",
|
|
isPhishingDomain: false,
|
|
approvedFrom: FROM,
|
|
approvedTx: {
|
|
type: 2,
|
|
from: FROM,
|
|
chainId: "0x1",
|
|
nonce: "0x7",
|
|
gasLimit: "0x5208",
|
|
maxPriorityFeePerGas: "0x3b9aca00",
|
|
maxFeePerGas: "0x77359400",
|
|
to,
|
|
value: "0x0",
|
|
data,
|
|
accessList: [],
|
|
},
|
|
});
|
|
},
|
|
};
|
|
approval.init({});
|
|
await approval.show(1);
|
|
}
|
|
|
|
describe("the transaction approval screen", () => {
|
|
test("a contract creation says so instead of naming a contract", async () => {
|
|
await openTxApproval(null, INIT_CODE);
|
|
expect(node("approve-tx-to").innerHTML).toBe(SENTENCE);
|
|
});
|
|
|
|
test("a transaction with a recipient shows its address", async () => {
|
|
await openTxApproval(RECIPIENT, "0x");
|
|
expectAddressLine(node("approve-tx-to").innerHTML);
|
|
});
|
|
});
|
|
|
|
// approval.js carries a contract creation to these screens with `to` as "".
|
|
describe("the wait, success and error screens", () => {
|
|
const creation = {
|
|
to: "",
|
|
amount: "0.0000",
|
|
token: "ETH",
|
|
tokenSymbol: null,
|
|
};
|
|
const transfer = { ...creation, to: RECIPIENT };
|
|
|
|
test("a contract creation says so on the wait screen", () => {
|
|
txStatus.showWait(creation, TX_HASH);
|
|
expect(node("wait-tx-to").innerHTML).toBe(SENTENCE);
|
|
});
|
|
|
|
test("a transaction with a recipient shows its address on the wait screen", () => {
|
|
txStatus.showWait(transfer, TX_HASH);
|
|
expectAddressLine(node("wait-tx-to").innerHTML);
|
|
});
|
|
|
|
test("a contract creation says so on the success and error screens", () => {
|
|
state.viewData = {
|
|
amount: "0.0000",
|
|
symbol: "ETH",
|
|
to: "",
|
|
hash: TX_HASH,
|
|
blockNumber: 1,
|
|
};
|
|
txStatus.renderSuccess();
|
|
expect(node("success-tx-to").innerHTML).toBe(SENTENCE);
|
|
|
|
txStatus.showError(creation, TX_HASH, "The transaction failed.");
|
|
expect(node("error-tx-to").innerHTML).toBe(SENTENCE);
|
|
});
|
|
|
|
test("a transaction with a recipient shows its address on the success and error screens", () => {
|
|
state.viewData = {
|
|
amount: "0.0050",
|
|
symbol: "ETH",
|
|
to: RECIPIENT,
|
|
hash: TX_HASH,
|
|
blockNumber: 1,
|
|
};
|
|
txStatus.renderSuccess();
|
|
expectAddressLine(node("success-tx-to").innerHTML);
|
|
|
|
txStatus.showError(transfer, TX_HASH, "The transaction failed.");
|
|
expectAddressLine(node("error-tx-to").innerHTML);
|
|
});
|
|
});
|
|
|
|
// A transaction FROM sent, as the history lists hold it. The explorer reports a
|
|
// contract creation with no `to`, which src/shared/transactions.js turns into
|
|
// `to: ""`.
|
|
function historyTx(to) {
|
|
return {
|
|
hash: TX_HASH,
|
|
from: FROM,
|
|
to,
|
|
value: "0.0000",
|
|
exactValue: "0.0",
|
|
rawAmount: "0",
|
|
rawUnit: "wei",
|
|
symbol: "ETH",
|
|
timestamp: 1790000000,
|
|
isError: false,
|
|
directionLabel: "Sent",
|
|
direction: "sent",
|
|
contractAddress: null,
|
|
};
|
|
}
|
|
|
|
// The detail view is opened with the transaction a history row holds.
|
|
describe("the transaction detail view", () => {
|
|
test("a contract creation says so", () => {
|
|
transactionDetail.show(historyTx(""));
|
|
expect(node("tx-detail-to").innerHTML).toBe(SENTENCE);
|
|
expect(node("tx-detail-type").textContent).toBe("Contract Creation");
|
|
});
|
|
|
|
test("a transaction with a recipient shows its address", () => {
|
|
transactionDetail.show(historyTx(RECIPIENT));
|
|
expectAddressLine(node("tx-detail-to").innerHTML);
|
|
});
|
|
});
|
|
|
|
// The same rows are drawn on Home, AddressDetail and AddressToken (for ETH).
|
|
describe.each([
|
|
["Home", "home-tx-list", () => home.render({})],
|
|
["AddressDetail", "tx-list", () => addressDetail.show()],
|
|
["AddressToken", "address-token-tx-list", () => addressToken.show()],
|
|
])("the transaction history on %s", (_name, listId, open) => {
|
|
async function rowsFor(tx) {
|
|
mockHistory = [tx];
|
|
open();
|
|
// The list is drawn once the history has been fetched.
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
return node(listId).innerHTML;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
state.wallets = [
|
|
{
|
|
name: "Main",
|
|
type: "key",
|
|
addresses: [{ address: FROM, balance: "0.0000" }],
|
|
},
|
|
];
|
|
state.selectedWallet = 0;
|
|
state.selectedAddress = 0;
|
|
state.selectedToken = "ETH";
|
|
});
|
|
|
|
test("a contract creation's row says so, with no colour dot and no address line", async () => {
|
|
const html = await rowsFor(historyTx(""));
|
|
expect(html).toContain(SENTENCE);
|
|
expect(html).not.toContain("background:");
|
|
expect(html).not.toContain("am-address");
|
|
expect(html).not.toContain("undefined");
|
|
});
|
|
|
|
test("a transaction with a recipient shows its colour dot and address", async () => {
|
|
const html = await rowsFor(historyTx(RECIPIENT));
|
|
expectAddressLine(html);
|
|
expect(html).toContain("background:#");
|
|
expect(html).toContain(`<div class="am-address">${RECIPIENT}</div>`);
|
|
});
|
|
});
|