bundledToken(address, chainId) in tokenList.js answers only for Ethereum Mainnet's chain id. Every reader of the bundled list now goes through it, with the chain id of the transaction, balance or history entry it draws. The token sources the screens build carry that chain id. The Uniswap decoder sets the transaction's chain id on them. fetchTokenBalances() takes the chain id of the network it asks about. Seven existing tests read the bundled list without naming a network. Each now names Ethereum Mainnet. Model: opus-5-5
158 lines
5.3 KiB
JavaScript
158 lines
5.3 KiB
JavaScript
// The approval screen for a site's Sepolia transfer of the contract at USDC's
|
|
// mainnet address, while Sepolia is active.
|
|
//
|
|
// The bundled token list holds Ethereum Mainnet contracts. The contract at the
|
|
// same address on Sepolia is another token, but the screen named it USDC and
|
|
// scaled its amount at USDC's decimals
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/632). The bundled list now names
|
|
// a token only on Ethereum Mainnet.
|
|
//
|
|
// Driven through the real approval screen, against the minimal DOM stub
|
|
// tests/approvalTokenNetwork.test.js uses.
|
|
|
|
globalThis.chrome = {
|
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
|
};
|
|
|
|
const { Interface } = require("ethers");
|
|
const { state } = require("../src/shared/state");
|
|
const { NETWORKS } = require("../src/shared/networks");
|
|
const approval = require("../src/popup/views/approval");
|
|
|
|
const FROM = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
|
|
const RECIPIENT = "0x711e691962E5c054b78071C789A0C8CE657377aD";
|
|
// USDC's address on Ethereum Mainnet, where the bundled list names it.
|
|
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
|
|
|
|
// FROM sends 1234500000 base units to RECIPIENT.
|
|
const TRANSFER_DATA = new Interface([
|
|
"function transfer(address to, uint256 amount)",
|
|
]).encodeFunctionData("transfer", [RECIPIENT, 1234500000n]);
|
|
|
|
function makeElement(id) {
|
|
const classes = new Set();
|
|
const el = {
|
|
id,
|
|
textContent: "",
|
|
value: "",
|
|
innerHTML: "",
|
|
disabled: false,
|
|
style: {},
|
|
dataset: {},
|
|
listeners: {},
|
|
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: (name, fn) => {
|
|
el.listeners[name] = el.listeners[name] || [];
|
|
el.listeners[name].push(fn);
|
|
},
|
|
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);
|
|
}
|
|
|
|
// One line of the decoded details: its label and what is drawn under it.
|
|
function decodedLine(label) {
|
|
return node("approve-tx-decoded-details")
|
|
.innerHTML.split('<div class="mb-2">')
|
|
.find((line) =>
|
|
line.startsWith(`<div class="text-muted">${label}</div>`),
|
|
);
|
|
}
|
|
|
|
// Open the approval screen, with Sepolia active, for the site's Sepolia
|
|
// transaction sending TRANSFER_DATA to the contract at USDC's address.
|
|
async function showSepoliaTransfer() {
|
|
globalThis.document = makeDocument();
|
|
globalThis.window = { location: { search: "" }, close: () => {} };
|
|
globalThis.chrome.runtime = {
|
|
connect: () => ({ postMessage: () => {} }),
|
|
sendMessage: (msg, reply) => {
|
|
if (!reply) return;
|
|
reply({
|
|
type: "tx",
|
|
origin: "https://dapp.example",
|
|
isPhishingDomain: false,
|
|
approvedFrom: FROM,
|
|
approvedTx: {
|
|
type: 2,
|
|
from: FROM,
|
|
chainId: NETWORKS.sepolia.chainId,
|
|
nonce: "0x7",
|
|
gasLimit: "0x30d40",
|
|
maxPriorityFeePerGas: "0x3b9aca00",
|
|
maxFeePerGas: "0x77359400",
|
|
to: USDC,
|
|
value: "0x0",
|
|
data: TRANSFER_DATA,
|
|
accessList: [],
|
|
},
|
|
});
|
|
},
|
|
};
|
|
state.networkId = "sepolia";
|
|
state.trackedTokens = [];
|
|
state.networkTrackedTokens = {};
|
|
state.activeAddress = FROM;
|
|
state.wallets = [
|
|
{
|
|
type: "hd",
|
|
name: "Wallet 1",
|
|
xpub: "xpub-wallet-1",
|
|
encryptedSecret: "encrypted-secret-1",
|
|
nextIndex: 1,
|
|
addresses: [
|
|
{ address: FROM, index: 0, balance: "0", tokenBalances: [] },
|
|
],
|
|
},
|
|
];
|
|
state.currentView = null;
|
|
approval.init({});
|
|
await approval.show(1);
|
|
}
|
|
|
|
test("names no token, and states the amount in base units", async () => {
|
|
await showSepoliaTransfer();
|
|
expect(state.currentView).toBe("approve-tx");
|
|
expect(node("approve-tx-to").innerHTML).not.toContain("USDC");
|
|
expect(decodedLine("Token")).toContain(
|
|
'<div class="font-bold">Unknown token</div>',
|
|
);
|
|
expect(decodedLine("Amount")).toContain(
|
|
'<div class="font-bold">1234500000 base units (decimals unknown)</div>',
|
|
);
|
|
});
|