|
|
|
@@ -0,0 +1,176 @@
|
|
|
|
|
// The decoded details on the transaction detail screen of a token transfer
|
|
|
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/515).
|
|
|
|
|
//
|
|
|
|
|
// A history row for a token transfer holds the transfer's recipient as its
|
|
|
|
|
// `to`, not the contract the transaction was sent to. The screen used to decode
|
|
|
|
|
// the transaction's data against that `to`, so its Token line named the
|
|
|
|
|
// recipient, and the amount read "decimals unknown" for a token the wallet
|
|
|
|
|
// tracks. It decodes against the address the explorer reports the transaction
|
|
|
|
|
// was sent to.
|
|
|
|
|
//
|
|
|
|
|
// The row is the one the real history parser makes from the explorer's answer,
|
|
|
|
|
// opened through transactionDetail.show(), which is what a click on the row
|
|
|
|
|
// calls. Driven against a minimal DOM stub in the shape
|
|
|
|
|
// tests/contractCreation.test.js uses.
|
|
|
|
|
|
|
|
|
|
const { Interface } = require("ethers");
|
|
|
|
|
|
|
|
|
|
jest.mock("../src/shared/log", () => ({
|
|
|
|
|
log: {
|
|
|
|
|
debugf: () => {},
|
|
|
|
|
infof: () => {},
|
|
|
|
|
warnf: () => {},
|
|
|
|
|
errorf: () => {},
|
|
|
|
|
},
|
|
|
|
|
debugFetch: async (url) => ({
|
|
|
|
|
ok: true,
|
|
|
|
|
json: async () => mockExplorerAnswer(url),
|
|
|
|
|
}),
|
|
|
|
|
setRuntimeDebug: () => {},
|
|
|
|
|
isDebug: () => false,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
globalThis.chrome = {
|
|
|
|
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const { state } = require("../src/shared/state");
|
|
|
|
|
const { fetchRecentTransactions } = require("../src/shared/transactions");
|
|
|
|
|
const transactionDetail = require("../src/popup/views/transactionDetail");
|
|
|
|
|
|
|
|
|
|
const EXPLORER = "https://explorer.example/api/v2";
|
|
|
|
|
const HOLDER = "0x0000000000000000000000000000000000000a11";
|
|
|
|
|
const RECIPIENT = "0x711e691962E5c054b78071C789A0C8CE657377aD";
|
|
|
|
|
// A token the wallet tracks and the bundled token list does not name.
|
|
|
|
|
const TOKEN = "0x74de5d4FCbf63E00296fd95d33236B9794016631";
|
|
|
|
|
const TX_HASH =
|
|
|
|
|
"0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84";
|
|
|
|
|
|
|
|
|
|
// HOLDER sends 1234.5 of the 6-decimal token to RECIPIENT.
|
|
|
|
|
const TRANSFER_DATA = new Interface([
|
|
|
|
|
"function transfer(address to, uint256 amount)",
|
|
|
|
|
]).encodeFunctionData("transfer", [RECIPIENT, 1234500000n]);
|
|
|
|
|
|
|
|
|
|
// The explorer's answers: the transfer in HOLDER's token transfers, and the
|
|
|
|
|
// transaction itself, sent to the token contract.
|
|
|
|
|
function mockExplorerAnswer(url) {
|
|
|
|
|
if (
|
|
|
|
|
url ===
|
|
|
|
|
EXPLORER + "/addresses/" + HOLDER + "/token-transfers?type=ERC-20"
|
|
|
|
|
) {
|
|
|
|
|
return {
|
|
|
|
|
items: [
|
|
|
|
|
{
|
|
|
|
|
transaction_hash: TX_HASH,
|
|
|
|
|
block_number: 100,
|
|
|
|
|
timestamp: "2026-10-01T00:00:00Z",
|
|
|
|
|
from: { hash: HOLDER },
|
|
|
|
|
to: { hash: RECIPIENT },
|
|
|
|
|
total: { value: "1234500000", decimals: "6" },
|
|
|
|
|
token: {
|
|
|
|
|
address_hash: TOKEN,
|
|
|
|
|
symbol: "TDOL",
|
|
|
|
|
holders_count: "5000",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
if (url === EXPLORER + "/transactions/" + TX_HASH) {
|
|
|
|
|
return { to: { hash: TOKEN }, raw_input: TRANSFER_DATA };
|
|
|
|
|
}
|
|
|
|
|
return { items: [] };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function makeElement(id) {
|
|
|
|
|
const classes = new Set();
|
|
|
|
|
const el = {
|
|
|
|
|
id,
|
|
|
|
|
textContent: "",
|
|
|
|
|
value: "",
|
|
|
|
|
innerHTML: "",
|
|
|
|
|
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: () => false,
|
|
|
|
|
},
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// One line of the decoded details: its label and what is drawn under it.
|
|
|
|
|
function decodedLine(label) {
|
|
|
|
|
return node("tx-detail-calldata-details")
|
|
|
|
|
.innerHTML.split('<div class="mb-2">')
|
|
|
|
|
.find((line) =>
|
|
|
|
|
line.startsWith(`<div class="text-muted">${label}</div>`),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
globalThis.document = makeDocument();
|
|
|
|
|
globalThis.window = { location: { search: "" } };
|
|
|
|
|
state.blockscoutUrl = EXPLORER;
|
|
|
|
|
state.wallets = [
|
|
|
|
|
{
|
|
|
|
|
name: "Main",
|
|
|
|
|
type: "key",
|
|
|
|
|
addresses: [{ address: HOLDER, balance: "0.0000" }],
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
state.trackedTokens = [{ address: TOKEN, symbol: "TDOL", decimals: 6 }];
|
|
|
|
|
state.viewData = {};
|
|
|
|
|
state.viewStack = [];
|
|
|
|
|
state.currentView = null;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("a token transfer's decoded details name the token contract and the amount at its scale", async () => {
|
|
|
|
|
const [row] = await fetchRecentTransactions(HOLDER, EXPLORER, 1);
|
|
|
|
|
expect(row.to).toBe(RECIPIENT);
|
|
|
|
|
expect(row.contractAddress).toBe(TOKEN.toLowerCase());
|
|
|
|
|
|
|
|
|
|
transactionDetail.show(row);
|
|
|
|
|
// The decoded details are drawn once the explorer has answered.
|
|
|
|
|
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
|
|
|
|
|
|
|
|
|
|
expect(node("tx-detail-calldata-action").textContent).toBe(
|
|
|
|
|
"Token Transfer",
|
|
|
|
|
);
|
|
|
|
|
const token = decodedLine("Token");
|
|
|
|
|
expect(token).toContain('<div class="font-bold">TDOL</div>');
|
|
|
|
|
expect(token).toContain(TOKEN);
|
|
|
|
|
expect(token).not.toContain(RECIPIENT);
|
|
|
|
|
expect(decodedLine("Recipient")).toContain(RECIPIENT);
|
|
|
|
|
expect(decodedLine("Amount")).toContain(
|
|
|
|
|
'<div class="font-bold">1234.5000 TDOL</div>',
|
|
|
|
|
);
|
|
|
|
|
});
|