239 lines
8.4 KiB
JavaScript
239 lines
8.4 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,
|
|
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\]">([^<]*)</);
|
|
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,
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|