Files
AutistMask/tests/addressValue.test.js
T
clawbot eec3e23099
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
chore: escape every value the views write as markup, and cut symbols on code points (closes #329)
The token screen's decimals and holder count, the ETH price, every address
total and each balance row's USD value went into innerHTML unescaped, against
the rule at the top of src/popup/views/helpers.js. They are escaped now. None
could carry markup, but formatUsd() writes a value under a cent as "< $0.01".

displaySymbol() counts a symbol in code points, not UTF-16 units, so the cut
never leaves half of an emoji, which rendered as U+FFFD.

explorerLink() was already removed on next.

Model: opus-5-5
2026-10-05 10:43:06 +02:00

234 lines
8.3 KiB
JavaScript

// The USD total of an address that holds something this build cannot price
// (issue #261).
//
// Prices exist for the top 25 tokens only, so an address can hold real assets
// with no price attached. Summing what is priced and printing the result as
// the total says "$0.00" for an address holding nothing but unpriced tokens —
// worth-nothing and worth-an-unknown-amount collapsed into one number, in the
// direction that matters. The two are separate facts here, the same way an
// absent holders_count is not a count of zero.
//
// The value and its rendering are asserted directly, and then through the two
// call sites that return their markup as a string: the wallet list on Home and
// the balance warning on the address-removal confirmation. AddressDetail and
// the Home summary line render into the DOM and are covered by tests/e2e.
// helpers.js pulls in state.js, which reads chrome.storage.local at load.
globalThis.chrome = {
storage: { local: { get: async () => ({}), set: async () => {} } },
};
const {
prices,
clearPrices,
getAddressValue,
formatAddressTotal,
} = require("../src/shared/prices");
const { state } = require("../src/shared/state");
const { walletListHtml } = require("../src/popup/views/home");
const { balanceWarningHtml } = require("../src/popup/views/deleteAddress");
const USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
const NOVEL = "0x1111111111111111111111111111111111111111";
// No ETH, and a token no price is known for. The case the user is told is
// worth $0.00 today.
const UNPRICED_ONLY = {
address: "0x" + "a".repeat(40),
balance: "0",
tokenBalances: [{ address: NOVEL, symbol: "NOVEL", balance: "5000.0" }],
};
// Nothing at all: the address really is worth zero.
const EMPTY = {
address: "0x" + "b".repeat(40),
balance: "0",
tokenBalances: [],
};
// Every holding priced.
const FULLY_PRICED = {
address: "0x" + "c".repeat(40),
balance: "1.5",
tokenBalances: [{ address: USDC, symbol: "USDC", balance: "2500.0" }],
};
// Part priced, part not: 1.5 ETH plus a token with no price.
const PARTLY_PRICED = {
address: "0x" + "d".repeat(40),
balance: "1.5",
tokenBalances: [{ address: NOVEL, symbol: "NOVEL", balance: "5000.0" }],
};
beforeEach(() => {
clearPrices();
prices.ETH = 2000;
prices.USDC = 1;
state.wallets = [];
state.trackedTokens = [];
state.showZeroBalanceTokens = false;
state.activeAddress = null;
});
afterEach(() => {
clearPrices();
});
// The total line only, in each of the two markup-returning call sites. The
// ETH balance line above it legitimately reads $0.00 for an address with no
// ETH, so the assertions have to name the line under test.
function walletListTotal(addr) {
state.wallets = [{ name: "Wallet 1", type: "hd", addresses: [addr] }];
const match = walletListHtml().match(/min-h-\[1rem\]">([^<]*)</);
return match && match[1];
}
function removalWarningTotal(addr) {
const match = balanceWarningHtml(addr).match(/mt-1">([^<]*)</);
return match && match[1];
}
describe("the value of an address, and whether it is the whole value", () => {
test("an address holding only unpriced tokens has an incomplete value", () => {
expect(getAddressValue(UNPRICED_ONLY)).toEqual({
usd: 0,
partial: true,
});
});
test("an address holding nothing is complete, and zero", () => {
expect(getAddressValue(EMPTY)).toEqual({ usd: 0, partial: false });
});
test("a fully priced address is complete, and unchanged", () => {
expect(getAddressValue(FULLY_PRICED)).toEqual({
usd: 5500,
partial: false,
});
});
test("a partly priced address keeps the part it can price", () => {
expect(getAddressValue(PARTLY_PRICED)).toEqual({
usd: 3000,
partial: true,
});
});
// A token balance of zero is not a holding, so it cannot make the total
// incomplete: an address with a spent-out unpriced token is worth zero.
test("a zero balance in an unpriced token leaves the value complete", () => {
const addr = {
address: "0x1",
balance: "0",
tokenBalances: [{ address: NOVEL, symbol: "NOVEL", balance: "0" }],
};
expect(getAddressValue(addr)).toEqual({ usd: 0, partial: false });
});
// Before the first price fetch, and on testnet, nothing is knowable: that
// is a third state, and it stays distinct from both of the others.
test("no prices at all means no value, not an incomplete one", () => {
clearPrices();
expect(getAddressValue(FULLY_PRICED)).toEqual({
usd: null,
partial: false,
});
});
});
describe("how that value is written on screen", () => {
test("a complete total is the figure", () => {
expect(formatAddressTotal(getAddressValue(FULLY_PRICED))).toBe(
"Total: $5,500.00",
);
});
test("an address worth zero says so", () => {
expect(formatAddressTotal(getAddressValue(EMPTY))).toBe("Total: $0.00");
});
// The figure is still worth having — it is the ETH the user does hold —
// but on its own it understates the address, so it is named as partial.
test("a partly priced total is given, and marked as partial", () => {
expect(formatAddressTotal(getAddressValue(PARTLY_PRICED))).toBe(
"Total: $3,000.00 plus unpriced tokens",
);
});
// Nothing priced is held, so there is no figure to give: printing the
// $0.00 sum of an empty set is the bug.
test("a total with nothing priced in it gives no figure", () => {
const line = formatAddressTotal(getAddressValue(UNPRICED_ONLY));
expect(line).toBe("Total: unpriced tokens only");
expect(line).not.toContain("$");
});
test("an unknown value is written as nothing at all", () => {
clearPrices();
expect(formatAddressTotal(getAddressValue(FULLY_PRICED))).toBe("");
});
});
describe("the wallet list on Home", () => {
test("an address holding only unpriced tokens is not totalled at $0.00", () => {
expect(walletListTotal(UNPRICED_ONLY)).toBe(
"Total: unpriced tokens only",
);
});
test("an address holding nothing is still totalled at $0.00", () => {
expect(walletListTotal(EMPTY)).toBe("Total: $0.00");
});
test("a fully priced address shows its total", () => {
expect(walletListTotal(FULLY_PRICED)).toBe("Total: $5,500.00");
});
test("a partly priced address shows the priced part, marked partial", () => {
expect(walletListTotal(PARTLY_PRICED)).toBe(
"Total: $3,000.00 plus unpriced tokens",
);
});
test("an address whose value is unknown keeps its blank line", () => {
clearPrices();
expect(walletListTotal(FULLY_PRICED)).toBe("&nbsp;");
});
// A total under a cent is written "< $0.01", and the "<" is escaped
// here as the removal warning escapes it.
test("a total under a cent is escaped, as on the removal warning", () => {
const tiny = { ...EMPTY, balance: "0.000001" };
expect(walletListTotal(tiny)).toBe("Total: &lt; $0.01");
expect(removalWarningTotal(tiny)).toBe("Total: &lt; $0.01");
});
});
describe("the balance warning on the address-removal confirmation", () => {
// "This address holds a balance." followed by "Total: $0.00" is a flat
// contradiction, on the one screen whose job is to warn.
test("an address holding only unpriced tokens is not totalled at $0.00", () => {
expect(balanceWarningHtml(UNPRICED_ONLY)).toContain(
"This address holds a balance.",
);
expect(removalWarningTotal(UNPRICED_ONLY)).toBe(
"Total: unpriced tokens only",
);
});
test("a fully priced address still shows its total", () => {
expect(removalWarningTotal(FULLY_PRICED)).toBe("Total: $5,500.00");
});
test("a partly priced address shows the priced part, marked partial", () => {
expect(removalWarningTotal(PARTLY_PRICED)).toBe(
"Total: $3,000.00 plus unpriced tokens",
);
});
test("no total line is written when the value is unknown", () => {
clearPrices();
expect(removalWarningTotal(FULLY_PRICED)).toBe(null);
});
});