Files
AutistMask/tests/unknownScaleDisplay.test.js
T
clawbot b4e166cdc7
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
fix: read USD prices from Chainlink price feeds through the RPC endpoint (closes #525)
CoinDesk now refuses requests without an API key, so mainnet showed no USD prices. Mainnet prices now come from Chainlink's USD price feeds, read with eth_call through the user's RPC endpoint; no other service is asked. The bundled list pairs each of 53 tokens with its feed by contract address, and ETH and WETH use ETH/USD. The popup reads the feeds of ETH and the tokens the user holds or tracks in one batched request, at most every 5 minutes. A reading older than its heartbeat plus 30 minutes, or not positive, is no price. A token with no feed shows no USD value. CoinDesk is gone.

RULES.md still names the CoinDesk price API.

Model: opus-5-5
2026-10-27 04:23:17 +01:00

186 lines
6.9 KiB
JavaScript

// What the screens that READ a stored token balance do with a holding whose
// scale nothing knows.
//
// https://git.eeqj.de/sneak/AutistMask/issues/349 stopped `fetchTokenBalances()`
// fabricating a scale of 18, so a row it cannot state a quantity for is now
// stored with `balance: null`. Every reader of that field therefore has two
// distinct inputs where it used to have one, and the property that has to hold
// at each of them is the same one this codebase keeps losing:
//
// null (unknown) and 0 (genuinely zero) must produce DIFFERENT output.
//
// Losing it is what https://git.eeqj.de/sneak/AutistMask/issues/246,
// https://git.eeqj.de/sneak/AutistMask/issues/306,
// https://git.eeqj.de/sneak/AutistMask/issues/322,
// https://git.eeqj.de/sneak/AutistMask/issues/359 and
// https://git.eeqj.de/sneak/AutistMask/issues/364 each were. So every case
// below asserts the pair, not just that the null branch does something
// reasonable: an assertion on the null alone still passes on a build that
// renders both as zero, which is precisely the build being guarded against.
//
// The writer half — that the fetcher stores null rather than 18 — is in
// tests/fabricatedDecimals.test.js, and the Send and confirmation screens are
// in tests/unknownScaleSend.test.js.
"use strict";
// helpers.js reaches for both at module scope through the modules it pulls in.
globalThis.chrome = {
storage: {
local: {
get: () => Promise.resolve({}),
set: () => Promise.resolve(),
},
},
runtime: { sendMessage: () => {} },
};
globalThis.document = {
getElementById: () => null,
createElement: () => ({ style: {}, classList: { toggle() {} } }),
body: { prepend: () => {} },
addEventListener: () => {},
};
const {
balanceLine,
balanceLinesForAddress,
addressHoldsFunds,
} = require("../src/popup/views/helpers");
const {
prices,
clearPrices,
getAddressValue,
} = require("../src/shared/prices");
const { state } = require("../src/shared/state");
const NOVEL = "0x1111111111111111111111111111111111111111";
// One stored tokenBalances row. `balance: null` is what balances.js writes for
// a holding whose scale nothing knows; "0.0" is a quantity that was actually
// established and is zero.
function holding(balance) {
return {
address: NOVEL,
symbol: "NOVEL",
decimals: balance === null ? null : 18,
balance,
holders: 50000,
};
}
function address(balance) {
return {
address: "0x" + "a".repeat(40),
balance: "0",
tokenBalances: [holding(balance)],
};
}
// The quantity cell of a rendered row, which is the second of the two spans
// inside the fixed-width span.
function quantities(html) {
return [...html.matchAll(/<span>([^<]*)<\/span>/g)].map((m) => m[1]);
}
beforeEach(() => {
clearPrices();
state.wallets = [];
state.trackedTokens = [];
state.activeAddress = null;
});
afterEach(() => {
clearPrices();
});
describe("balanceLine", () => {
test("an unknown quantity and a zero one render differently", () => {
const unknown = balanceLine("NOVEL", null, null, NOVEL);
const zero = balanceLine("NOVEL", 0, null, NOVEL);
expect(unknown).not.toBe(zero);
expect(quantities(unknown)).toEqual(["NOVEL", "quantity unknown"]);
expect(quantities(zero)).toEqual(["NOVEL", "0.0000"]);
});
test("an unknown quantity produces no fiat figure, a zero one does", () => {
prices[NOVEL] = 3;
const unknown = balanceLine("NOVEL", null, 3, NOVEL);
const zero = balanceLine("NOVEL", 0, 3, NOVEL);
// A price times an unknown quantity is not $0.00: that is the same
// claim of "nothing here" the quantity cell just refused to make.
expect(unknown).toContain(
'<span class="text-right text-muted flex-1">&nbsp;</span>',
);
expect(zero).toContain(
'<span class="text-right text-muted flex-1">$0.00</span>',
);
});
});
describe("balanceLinesForAddress", () => {
// The show-zero setting is a statement about zeroes. An unknown quantity
// is not one, so hiding the row would assert the zero nobody established
// and the holding would vanish from the list entirely.
test("hiding zero balances hides the zero row and keeps the unknown one", () => {
const unknown = balanceLinesForAddress(address(null), [], false);
const zero = balanceLinesForAddress(address("0.0"), [], false);
expect(unknown).not.toBe(zero);
expect(unknown).toContain("quantity unknown");
expect(unknown).toContain("NOVEL");
expect(zero).not.toContain("NOVEL");
});
test("showing zero balances still tells the two apart", () => {
const unknown = balanceLinesForAddress(address(null), [], true);
const zero = balanceLinesForAddress(address("0.0"), [], true);
expect(unknown).not.toBe(zero);
expect(quantities(unknown)).toEqual([
"ETH",
"0.0000",
"NOVEL",
"quantity unknown",
]);
expect(quantities(zero)).toEqual(["ETH", "0.0000", "NOVEL", "0.0000"]);
});
});
describe("addressHoldsFunds", () => {
// Read by deleteAddress.js to decide whether removing the address is
// warned about. balances.js drops a row of zero base units before any
// scale is consulted, so a row that survived with no quantity is holding
// something, and the warning must err towards warning.
test("an unknown balance holds funds, a zero balance does not", () => {
expect(addressHoldsFunds(address(null))).toBe(true);
expect(addressHoldsFunds(address("0.0"))).toBe(false);
});
});
describe("getAddressValue", () => {
// `usd` is the value of what could be priced and `partial` says it is a
// floor rather than the total. An unpriceable holding is exactly what
// `partial` exists for; a holding of zero can neither add to the total nor
// make it incomplete.
test("an unknown balance makes the total partial, a zero balance does not", () => {
prices.ETH = 2000;
prices[NOVEL] = 3;
const unknown = getAddressValue(address(null));
const zero = getAddressValue(address("0.0"));
expect(unknown).not.toEqual(zero);
expect(unknown).toEqual({ usd: 0, partial: true });
expect(zero).toEqual({ usd: 0, partial: false });
});
test("an unknown balance is not priced as zero of the token", () => {
prices.ETH = 2000;
prices[NOVEL] = 3;
// The same row with a real quantity of 10 is worth $30. Neither that
// figure nor a confident $0.00 may be stated for the unknown one.
expect(getAddressValue(address("10.0"))).toEqual({
usd: 30,
partial: false,
});
expect(getAddressValue(address(null)).usd).toBe(0);
expect(getAddressValue(address(null)).partial).toBe(true);
});
});