fix: a tracked token reads quantity unknown at an address no refresh has read (closes #720)
A new address, a new wallet and a network switch each leave tracked tokens with no balance entry, so their lines read 0.0000 before any refresh had read them. Each of these now sets `balanceNotFetched` on the tracked tokens, and a refresh already clears it. Their lines read `quantity unknown` until then. Addresses that the wallet scan finds get the same. A refresh that did not read every address the page now holds leaves the field set. Model: opus-5-5
This commit was merged in pull request #735.
This commit is contained in:
@@ -11,14 +11,44 @@
|
||||
// switch keeps each network's records apart. A refresh takes the field off the
|
||||
// stored record's tokens and keeps another page's changes to the stored list.
|
||||
//
|
||||
// Every tracked token carries that field again once an address or a wallet is
|
||||
// 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).
|
||||
//
|
||||
// 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.
|
||||
|
||||
const { HDNodeWallet, Mnemonic } = require("ethers");
|
||||
|
||||
const {
|
||||
bootPopup,
|
||||
cleanupPopup,
|
||||
unversionedValidProfile,
|
||||
} = require("./support/popupBoot");
|
||||
const { hdWalletFromMnemonic } = require("../src/shared/wallet");
|
||||
|
||||
const PHRASE =
|
||||
"abandon abandon abandon abandon abandon abandon " +
|
||||
"abandon abandon abandon abandon abandon about";
|
||||
const PASSWORD = "correct horse battery staple";
|
||||
|
||||
// What AddWallet's tabs take. The first and last scan for more addresses.
|
||||
const FROM_PHRASE = {
|
||||
tab: "tab-mnemonic",
|
||||
field: "wallet-mnemonic",
|
||||
secret: PHRASE,
|
||||
};
|
||||
const FROM_PRIVATE_KEY = {
|
||||
tab: "tab-privkey",
|
||||
field: "import-private-key",
|
||||
secret: "0x" + "11".repeat(32),
|
||||
};
|
||||
const FROM_XPRV = {
|
||||
tab: "tab-xprv",
|
||||
field: "import-xprv-key",
|
||||
secret: HDNodeWallet.fromSeed(Mnemonic.fromPhrase(PHRASE).computeSeed())
|
||||
.extendedKey,
|
||||
};
|
||||
|
||||
// Tracked by nobody until a test adds it.
|
||||
const NEW_TOKEN = "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
||||
@@ -66,6 +96,34 @@ function bootOnAddress(extra) {
|
||||
);
|
||||
}
|
||||
|
||||
// The popup over one wallet that "+" can derive an address from. Its one
|
||||
// address holds a million NEW_TOKEN, and NEW_TOKEN is tracked.
|
||||
function bootOnDerivingWallet() {
|
||||
const { xpub, firstAddress } = hdWalletFromMnemonic(PHRASE);
|
||||
return bootPopup(
|
||||
unversionedValidProfile({
|
||||
wallets: [
|
||||
{
|
||||
type: "hd",
|
||||
name: "Wallet 1",
|
||||
xpub,
|
||||
encryptedSecret: "encrypted-secret-1",
|
||||
nextIndex: 1,
|
||||
addresses: [
|
||||
{
|
||||
address: firstAddress,
|
||||
balance: "1.5",
|
||||
tokenBalances: [MILLION],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
activeAddress: firstAddress,
|
||||
trackedTokens: [NEW_TRACKED],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -175,6 +233,47 @@ function newTokenRow(page, id = "address-balances") {
|
||||
);
|
||||
}
|
||||
|
||||
// Every address's row for NEW_TOKEN on Home, as markup, in the order Home lists
|
||||
// the addresses.
|
||||
function newTokenRowsOnHome(page) {
|
||||
return page
|
||||
.node("wallet-list")
|
||||
.innerHTML.split("<div ")
|
||||
.filter((row) =>
|
||||
row.includes('data-token="' + NEW_TOKEN.toLowerCase() + '"'),
|
||||
);
|
||||
}
|
||||
|
||||
// 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.
|
||||
function plusButton(page) {
|
||||
const button = { press: null };
|
||||
page.node("wallet-list").querySelectorAll = (selector) =>
|
||||
selector !== ".btn-add-address"
|
||||
? []
|
||||
: [
|
||||
{
|
||||
dataset: { wallet: "0" },
|
||||
addEventListener: (name, fn) => {
|
||||
button.press = () =>
|
||||
fn({ stopPropagation: () => {} });
|
||||
},
|
||||
},
|
||||
];
|
||||
return button;
|
||||
}
|
||||
|
||||
// "Add wallet" on Home, filled in from `source`, and "Import".
|
||||
async function addWallet(page, { tab, field, secret }) {
|
||||
await page.click("btn-add-wallet-bottom");
|
||||
await page.click(tab);
|
||||
page.node(field).value = secret;
|
||||
page.node("add-wallet-password").value = PASSWORD;
|
||||
page.node("add-wallet-password-confirm").value = PASSWORD;
|
||||
await page.click("btn-add-wallet-confirm");
|
||||
}
|
||||
|
||||
// AddressDetail for the one address opened from Home, as its `[info]` button
|
||||
// does.
|
||||
async function openAddress(page) {
|
||||
@@ -456,3 +555,208 @@ test("a refresh does not bring back a token that another page removed after this
|
||||
NEW_TRACKED,
|
||||
]);
|
||||
});
|
||||
|
||||
test("an address just added reads quantity unknown for a tracked token until a refresh has read it", async () => {
|
||||
const page = await bootOnDerivingWallet();
|
||||
const plus = plusButton(page);
|
||||
await refreshOnce(page, [MILLION]);
|
||||
const finish = holdNextRefresh(page);
|
||||
|
||||
await plus.press();
|
||||
await page.settle();
|
||||
|
||||
const [held, added] = newTokenRowsOnHome(page);
|
||||
expect(held).toContain("1000000.0000");
|
||||
expect(added).toContain("quantity unknown");
|
||||
expect(added).not.toContain("0.0000");
|
||||
expect(
|
||||
page.storage.read("autistmask").trackedTokens[0].balanceNotFetched,
|
||||
).toBe(true);
|
||||
|
||||
// It reads the new address's balance, which is zero.
|
||||
await finish([MILLION]);
|
||||
|
||||
expect(newTokenRowsOnHome(page)[1]).toContain("0.0000");
|
||||
});
|
||||
|
||||
test('an address "+" adds during a refresh reads quantity unknown when that refresh returns', async () => {
|
||||
const page = await bootOnDerivingWallet();
|
||||
const plus = plusButton(page);
|
||||
await refreshOnce(page, [MILLION]);
|
||||
const finishRunning = holdNextRefresh(page);
|
||||
const ticking = page.tick();
|
||||
await page.settle();
|
||||
|
||||
await plus.press();
|
||||
await page.settle();
|
||||
const finishNext = holdNextRefresh(page);
|
||||
await finishRunning([MILLION]);
|
||||
await ticking;
|
||||
|
||||
const added = newTokenRowsOnHome(page)[1];
|
||||
expect(added).toContain("quantity unknown");
|
||||
expect(added).not.toContain("0.0000");
|
||||
|
||||
// It reads the new address's balance, which is zero.
|
||||
await finishNext([MILLION]);
|
||||
|
||||
expect(newTokenRowsOnHome(page)[1]).toContain("0.0000");
|
||||
});
|
||||
|
||||
test.each([FROM_PHRASE, FROM_PRIVATE_KEY, FROM_XPRV])(
|
||||
"a wallet just added from $tab reads quantity unknown for a tracked token until a refresh has read it",
|
||||
async (source) => {
|
||||
const page = await bootPopup(
|
||||
unversionedValidProfile({ trackedTokens: [NEW_TRACKED] }),
|
||||
);
|
||||
const finish = holdNextRefresh(page);
|
||||
|
||||
await addWallet(page, source);
|
||||
|
||||
const added = newTokenRowsOnHome(page)[1];
|
||||
expect(added).toContain("quantity unknown");
|
||||
expect(added).not.toContain("0.0000");
|
||||
|
||||
// It reads the new address's balance, which is zero.
|
||||
await finish([]);
|
||||
|
||||
expect(newTokenRowsOnHome(page)[1]).toContain("0.0000");
|
||||
},
|
||||
);
|
||||
|
||||
test("a wallet from a private key added during a refresh reads quantity unknown when that refresh returns", async () => {
|
||||
const page = await bootPopup(
|
||||
unversionedValidProfile({ trackedTokens: [NEW_TRACKED] }),
|
||||
);
|
||||
const finishRunning = holdNextRefresh(page);
|
||||
const ticking = page.tick();
|
||||
await page.settle();
|
||||
|
||||
await addWallet(page, FROM_PRIVATE_KEY);
|
||||
const finishNext = holdNextRefresh(page);
|
||||
await finishRunning([]);
|
||||
await ticking;
|
||||
|
||||
const added = newTokenRowsOnHome(page)[1];
|
||||
expect(added).toContain("quantity unknown");
|
||||
expect(added).not.toContain("0.0000");
|
||||
|
||||
// It reads the new address's balance, which is zero.
|
||||
await finishNext([]);
|
||||
|
||||
expect(newTokenRowsOnHome(page)[1]).toContain("0.0000");
|
||||
});
|
||||
|
||||
test.each([FROM_PHRASE, FROM_XPRV])(
|
||||
"addresses the scan finds for a wallet from $tab read quantity unknown, even after a refresh returned during the scan",
|
||||
async (source) => {
|
||||
const page = await bootPopup(
|
||||
unversionedValidProfile({ trackedTokens: [NEW_TRACKED] }),
|
||||
);
|
||||
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] = newTokenRowsOnHome(page);
|
||||
for (const found of [first, second]) {
|
||||
expect(found).toContain("quantity unknown");
|
||||
expect(found).not.toContain("0.0000");
|
||||
}
|
||||
|
||||
await finish([]);
|
||||
|
||||
expect(newTokenRowsOnHome(page)[2]).toContain("0.0000");
|
||||
},
|
||||
);
|
||||
|
||||
test.each([FROM_PHRASE, FROM_XPRV])(
|
||||
"addresses the scan finds for a wallet from $tab read quantity unknown when a refresh running as the scan ends returns",
|
||||
async (source) => {
|
||||
const page = await bootPopup(
|
||||
unversionedValidProfile({ trackedTokens: [NEW_TRACKED] }),
|
||||
);
|
||||
const { scanForAddresses } = require("../src/shared/balances");
|
||||
let finishRunning;
|
||||
let ticking;
|
||||
scanForAddresses.mockImplementationOnce(async () => {
|
||||
finishRunning = holdNextRefresh(page);
|
||||
ticking = page.tick();
|
||||
await page.settle();
|
||||
return {
|
||||
addresses: [
|
||||
{ address: "0x" + "dd".repeat(20), index: 0 },
|
||||
{ address: "0x" + "ee".repeat(20), index: 1 },
|
||||
],
|
||||
nextIndex: 2,
|
||||
};
|
||||
});
|
||||
|
||||
await addWallet(page, source);
|
||||
const finishNext = holdNextRefresh(page);
|
||||
await finishRunning([]);
|
||||
await ticking;
|
||||
|
||||
const [, first, second] = newTokenRowsOnHome(page);
|
||||
for (const found of [first, second]) {
|
||||
expect(found).toContain("quantity unknown");
|
||||
expect(found).not.toContain("0.0000");
|
||||
}
|
||||
|
||||
// It reads the new addresses' balances, which are zero.
|
||||
await finishNext([]);
|
||||
|
||||
expect(newTokenRowsOnHome(page)[2]).toContain("0.0000");
|
||||
},
|
||||
);
|
||||
|
||||
test("after a network switch in Settings, a tracked token reads quantity unknown until a refresh has read it", async () => {
|
||||
const page = await bootOnAddress({
|
||||
trackedTokens: [OTHER_TOKEN],
|
||||
networkTrackedTokens: { sepolia: [NEW_TRACKED] },
|
||||
});
|
||||
|
||||
await page.click("btn-settings");
|
||||
await chooseNetwork(page, "sepolia");
|
||||
await page.click("btn-settings-back");
|
||||
|
||||
expect(page.visibleViews()).toEqual(["address"]);
|
||||
const row = newTokenRow(page);
|
||||
expect(row).toContain("quantity unknown");
|
||||
expect(row).not.toContain("0.0000");
|
||||
|
||||
// It reads the balance, which is zero.
|
||||
await refreshOnce(page, []);
|
||||
|
||||
expect(newTokenRow(page)).toContain("0.0000");
|
||||
});
|
||||
|
||||
test("after a switch saved by the background, a tracked token reads quantity unknown until a refresh has read it", async () => {
|
||||
const page = await bootOnAddress({
|
||||
networkTrackedTokens: { sepolia: [NEW_TRACKED] },
|
||||
});
|
||||
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 token's balance.
|
||||
await page.tick();
|
||||
|
||||
const row = newTokenRow(page);
|
||||
expect(row).toContain("quantity unknown");
|
||||
expect(row).not.toContain("0.0000");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user