fix: the ETH line reads unknown at an address no refresh has read (closes #736)
An address's ETH balance is now null until a refresh has read it. That covers a new address, a new wallet, addresses a wallet scan finds, and every address after a network switch. Its line reads `quantity unknown`, Home's balance `unknown (ETH)`, and Send `Current balance: unknown (ETH)`. Max says the balance is unknown. Send is no longer refused as zero. The address has no total, and DeleteAddress keeps its warning. A refresh also deletes `balanceNotFetched` only when it read every address in the stored record. Model: opus-5-5
This commit is contained in:
@@ -15,6 +15,10 @@
|
||||
// added, and after a network switch, so it reads the same until a refresh has
|
||||
// read it (https://git.eeqj.de/sneak/AutistMask/issues/720).
|
||||
//
|
||||
// An address's ETH balance is null until a refresh has read that address, so
|
||||
// its ETH line reads the same, and Send states it as unknown
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/736).
|
||||
//
|
||||
// Driven through the real popup over a stored profile on Ethereum Mainnet,
|
||||
// with "Show tracked tokens with zero balance" on, as it is by default.
|
||||
|
||||
@@ -125,10 +129,10 @@ function bootOnDerivingWallet() {
|
||||
}
|
||||
|
||||
// Holds the next balance refresh open. The returned function lets it finish
|
||||
// with the address's ETH balance at 2 and `tokenBalances` as its token
|
||||
// entries, having read the balance of every token tracked when it started.
|
||||
// Given null, it finishes having read no token's balance, as when every
|
||||
// balanceOf fails, and leaves the token entries as they were.
|
||||
// with every address's ETH balance at 2 and `tokenBalances` as the first
|
||||
// address's token entries, having read the balance of every token tracked when
|
||||
// it started. Given null, it finishes having read no token's balance, as when
|
||||
// every balanceOf fails, and leaves the token entries as they were.
|
||||
function holdNextRefresh(page) {
|
||||
const { refreshBalances } = require("../src/shared/balances");
|
||||
let release;
|
||||
@@ -138,7 +142,9 @@ function holdNextRefresh(page) {
|
||||
const tokenBalances = await new Promise((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
wallets[0].addresses[0].balance = "2.0";
|
||||
for (const wallet of wallets) {
|
||||
for (const addr of wallet.addresses) addr.balance = "2.0";
|
||||
}
|
||||
if (tokenBalances === null) return [];
|
||||
wallets[0].addresses[0].tokenBalances = tokenBalances;
|
||||
return read;
|
||||
@@ -244,6 +250,22 @@ function newTokenRowsOnHome(page) {
|
||||
);
|
||||
}
|
||||
|
||||
// Every ETH row in the balance list in element `id`, as markup, in the order
|
||||
// the list holds them.
|
||||
function ethRows(page, id) {
|
||||
return page
|
||||
.node(id)
|
||||
.innerHTML.split("<div ")
|
||||
.filter((row) => row.includes('data-token="ETH"'));
|
||||
}
|
||||
|
||||
// A stored profile whose one address no refresh has read.
|
||||
function profileNotRead(extra) {
|
||||
const profile = unversionedValidProfile(extra);
|
||||
profile.wallets[0].addresses[0].balance = null;
|
||||
return profile;
|
||||
}
|
||||
|
||||
// The "+" button Home draws for the first wallet. As in firstRemoveButton(),
|
||||
// the list hands Home a stand-in for that button each time it is drawn, and
|
||||
// `press()` presses the last one drawn. Call it before Home next draws its list.
|
||||
@@ -760,3 +782,277 @@ test("after a switch saved by the background, a tracked token reads quantity unk
|
||||
expect(row).toContain("quantity unknown");
|
||||
expect(row).not.toContain("0.0000");
|
||||
});
|
||||
|
||||
test("a refresh deletes no balanceNotFetched while the stored record holds an address it did not read", async () => {
|
||||
const page = await bootOnAddress({
|
||||
trackedTokens: [{ ...NEW_TRACKED, balanceNotFetched: true }],
|
||||
});
|
||||
// Another popup page adds an address. This page is not told of the write.
|
||||
const record = page.storage.read("autistmask");
|
||||
record.wallets[0].addresses.push({
|
||||
address: "0x" + "dd".repeat(20),
|
||||
balance: null,
|
||||
tokenBalances: [],
|
||||
});
|
||||
page.storage.write("autistmask", record);
|
||||
|
||||
await refreshOnce(page, []);
|
||||
|
||||
expect(
|
||||
page.storage.read("autistmask").trackedTokens[0].balanceNotFetched,
|
||||
).toBe(true);
|
||||
expect(newTokenRow(page)).toContain("quantity unknown");
|
||||
});
|
||||
|
||||
test("an address just added reads quantity unknown for ETH until a refresh has read it, then the fetched balance", async () => {
|
||||
const page = await bootOnDerivingWallet();
|
||||
const plus = plusButton(page);
|
||||
await refreshOnce(page, []);
|
||||
const finish = holdNextRefresh(page);
|
||||
|
||||
await plus.press();
|
||||
await page.settle();
|
||||
|
||||
const [held, added] = ethRows(page, "wallet-list");
|
||||
expect(held).toContain("2.0000");
|
||||
expect(added).toContain("quantity unknown");
|
||||
expect(added).not.toContain("0.0000");
|
||||
expect(
|
||||
page.storage.read("autistmask").wallets[0].addresses[1].balance,
|
||||
).toBeNull();
|
||||
|
||||
await finish([]);
|
||||
|
||||
expect(ethRows(page, "wallet-list")[1]).toContain("2.0000");
|
||||
});
|
||||
|
||||
test('an address "+" adds during a refresh reads quantity unknown for ETH when that refresh returns', async () => {
|
||||
const page = await bootOnDerivingWallet();
|
||||
const plus = plusButton(page);
|
||||
await refreshOnce(page, []);
|
||||
const finishRunning = holdNextRefresh(page);
|
||||
const ticking = page.tick();
|
||||
await page.settle();
|
||||
|
||||
await plus.press();
|
||||
await page.settle();
|
||||
const finishNext = holdNextRefresh(page);
|
||||
await finishRunning([]);
|
||||
await ticking;
|
||||
|
||||
const added = ethRows(page, "wallet-list")[1];
|
||||
expect(added).toContain("quantity unknown");
|
||||
expect(added).not.toContain("0.0000");
|
||||
|
||||
await finishNext([]);
|
||||
|
||||
expect(ethRows(page, "wallet-list")[1]).toContain("2.0000");
|
||||
});
|
||||
|
||||
test.each([FROM_PHRASE, FROM_PRIVATE_KEY, FROM_XPRV])(
|
||||
"a wallet just added from $tab reads quantity unknown for ETH until a refresh has read it",
|
||||
async (source) => {
|
||||
const page = await bootPopup(unversionedValidProfile());
|
||||
const finish = holdNextRefresh(page);
|
||||
|
||||
await addWallet(page, source);
|
||||
|
||||
const added = ethRows(page, "wallet-list")[1];
|
||||
expect(added).toContain("quantity unknown");
|
||||
expect(added).not.toContain("0.0000");
|
||||
|
||||
await finish([]);
|
||||
|
||||
expect(ethRows(page, "wallet-list")[1]).toContain("2.0000");
|
||||
},
|
||||
);
|
||||
|
||||
test.each([FROM_PHRASE, FROM_XPRV])(
|
||||
"addresses the scan finds for a wallet from $tab read quantity unknown for ETH until a refresh has read them",
|
||||
async (source) => {
|
||||
const page = await bootPopup(unversionedValidProfile());
|
||||
const { scanForAddresses } = require("../src/shared/balances");
|
||||
let finish;
|
||||
scanForAddresses.mockImplementationOnce(async () => {
|
||||
await refreshOnce(page, []);
|
||||
finish = holdNextRefresh(page);
|
||||
return {
|
||||
addresses: [
|
||||
{ address: "0x" + "dd".repeat(20), index: 0 },
|
||||
{ address: "0x" + "ee".repeat(20), index: 1 },
|
||||
],
|
||||
nextIndex: 2,
|
||||
};
|
||||
});
|
||||
|
||||
await addWallet(page, source);
|
||||
|
||||
const [, first, second] = ethRows(page, "wallet-list");
|
||||
for (const found of [first, second]) {
|
||||
expect(found).toContain("quantity unknown");
|
||||
expect(found).not.toContain("0.0000");
|
||||
}
|
||||
|
||||
await finish([]);
|
||||
|
||||
expect(ethRows(page, "wallet-list")[2]).toContain("2.0000");
|
||||
},
|
||||
);
|
||||
|
||||
test("after a network switch in Settings, the ETH line reads quantity unknown until a refresh has read it, then the fetched balance", async () => {
|
||||
const page = await bootOnAddress();
|
||||
|
||||
await page.click("btn-settings");
|
||||
await chooseNetwork(page, "sepolia");
|
||||
await page.click("btn-settings-back");
|
||||
|
||||
expect(page.visibleViews()).toEqual(["address"]);
|
||||
const [row] = ethRows(page, "address-balances");
|
||||
expect(row).toContain("quantity unknown");
|
||||
expect(row).not.toContain("0.0000");
|
||||
expect(
|
||||
page.storage.read("autistmask").wallets[0].addresses[0].balance,
|
||||
).toBeNull();
|
||||
|
||||
await refreshOnce(page, []);
|
||||
|
||||
expect(ethRows(page, "address-balances")[0]).toContain("2.0000");
|
||||
});
|
||||
|
||||
test("after a switch saved by the background, the ETH line reads quantity unknown until a refresh has read it, then the fetched balance", async () => {
|
||||
const page = await bootOnAddress();
|
||||
const {
|
||||
applyChainSwitchFields,
|
||||
} = require("../src/shared/chainSwitchFields");
|
||||
const record = page.storage.read("autistmask");
|
||||
applyChainSwitchFields(record, "sepolia");
|
||||
page.storage.write("autistmask", record);
|
||||
|
||||
// The popup takes the switch, and this refresh reads no ETH balance.
|
||||
await page.tick();
|
||||
|
||||
const [row] = ethRows(page, "address-balances");
|
||||
expect(row).toContain("quantity unknown");
|
||||
expect(row).not.toContain("0.0000");
|
||||
|
||||
await refreshOnce(page, []);
|
||||
|
||||
expect(ethRows(page, "address-balances")[0]).toContain("2.0000");
|
||||
});
|
||||
|
||||
test("a switch taken from the stored record leaves an address the record does not have with no ETH balance", () => {
|
||||
const {
|
||||
copyChainSwitchFields,
|
||||
} = require("../src/shared/chainSwitchFields");
|
||||
const taker = {
|
||||
wallets: [
|
||||
{
|
||||
addresses: [
|
||||
{ address: "0x1", balance: "1.5", tokenBalances: [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
copyChainSwitchFields(taker, { wallets: [] });
|
||||
|
||||
expect(taker.wallets[0].addresses[0].balance).toBeNull();
|
||||
});
|
||||
|
||||
test("closed and opened again before any refresh has read it, the ETH line still reads quantity unknown", async () => {
|
||||
const closed = await bootOnAddress();
|
||||
await closed.click("btn-settings");
|
||||
await chooseNetwork(closed, "sepolia");
|
||||
await closed.click("btn-settings-back");
|
||||
cleanupPopup();
|
||||
|
||||
const page = await bootPopup(undefined, { storage: closed.storage });
|
||||
|
||||
expect(page.visibleViews()).toEqual(["address"]);
|
||||
const [row] = ethRows(page, "address-balances");
|
||||
expect(row).toContain("quantity unknown");
|
||||
expect(row).not.toContain("0.0000");
|
||||
});
|
||||
|
||||
test("Home's ETH balance reads unknown until a refresh has read it, then the fetched balance", async () => {
|
||||
const page = await bootPopup(profileNotRead());
|
||||
|
||||
expect(page.visibleViews()).toEqual(["main"]);
|
||||
expect(page.text("total-value")).toBe("unknown (ETH)");
|
||||
|
||||
await refreshOnce(page, []);
|
||||
|
||||
expect(page.text("total-value")).toBe("2.0000 ETH");
|
||||
});
|
||||
|
||||
test("AddressToken for ETH reads quantity unknown until a refresh has read it, then the fetched balance", async () => {
|
||||
const page = await bootPopup(
|
||||
profileNotRead({
|
||||
currentView: "address-token",
|
||||
viewStack: ["main", "address"],
|
||||
selectedWallet: 0,
|
||||
selectedAddress: 0,
|
||||
selectedToken: "ETH",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(page.visibleViews()).toEqual(["address-token"]);
|
||||
const before = page.node("address-token-balance").innerHTML;
|
||||
expect(before).toContain("quantity unknown");
|
||||
expect(before).not.toContain("0.0000");
|
||||
|
||||
await refreshOnce(page, []);
|
||||
|
||||
expect(page.node("address-token-balance").innerHTML).toContain("2.0000");
|
||||
});
|
||||
|
||||
test.each([
|
||||
["main", {}, "btn-main-send"],
|
||||
[
|
||||
"address",
|
||||
{ currentView: "address", selectedWallet: 0, selectedAddress: 0 },
|
||||
"btn-send",
|
||||
],
|
||||
[
|
||||
"address-token",
|
||||
{
|
||||
currentView: "address-token",
|
||||
viewStack: ["main", "address"],
|
||||
selectedWallet: 0,
|
||||
selectedAddress: 0,
|
||||
selectedToken: "ETH",
|
||||
},
|
||||
"btn-address-token-send",
|
||||
],
|
||||
])(
|
||||
"Send from %s reads an unread ETH balance as unknown, Max says so, and only a balance read as zero is refused",
|
||||
async (view, extra, sendButton) => {
|
||||
const page = await bootPopup(profileNotRead(extra));
|
||||
expect(page.visibleViews()).toEqual([view]);
|
||||
// The dropdown's first option, which a browser selects by itself.
|
||||
page.node("send-token").value = "ETH";
|
||||
|
||||
await page.click(sendButton);
|
||||
|
||||
expect(page.visibleViews()).toEqual(["send"]);
|
||||
expect(page.text("send-balance")).toBe(
|
||||
"Current balance: unknown (ETH)",
|
||||
);
|
||||
|
||||
await page.click("btn-send-max");
|
||||
|
||||
expect(page.text("flash-msg")).toBe("This token's balance is unknown.");
|
||||
|
||||
await page.click("btn-send-back");
|
||||
const { refreshBalances } = require("../src/shared/balances");
|
||||
refreshBalances.mockImplementationOnce(async (wallets) => {
|
||||
wallets[0].addresses[0].balance = "0.0";
|
||||
return [];
|
||||
});
|
||||
await page.tick();
|
||||
await page.click(sendButton);
|
||||
|
||||
expect(page.visibleViews()).toEqual([view]);
|
||||
expect(page.text("flash-msg")).toBe("Cannot send — zero balance.");
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user