// 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, getWalletValue, getTotalValue, 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\]">([^<]*)([^<]*) { 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, }); }); test("one unpriced holding makes a wallet and the grand total partial", () => { const wallet = { addresses: [FULLY_PRICED, UNPRICED_ONLY] }; expect(getWalletValue(wallet)).toEqual({ usd: 5500, partial: true }); expect(getTotalValue([wallet])).toEqual({ usd: 5500, partial: true }); }); test("a wallet of fully priced addresses stays complete", () => { const wallet = { addresses: [FULLY_PRICED, EMPTY] }; expect(getWalletValue(wallet)).toEqual({ usd: 5500, 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(" "); }); }); 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); }); });