1 Commits
Author SHA1 Message Date
sneak f800202356 fix: the address screen shows a token just added at its fetched balance, not 0.0000 (closes #516)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
A balance refresh that returns now draws the balances on AddressDetail or
AddressToken again when that screen is up, not only the wallet list; their
transaction lists are left alone. Every add path on AddToken and
SettingsAddToken tracks a token through one step, trackToken(), which also
lists it in the new stored field tokensNotFetched: its line reads `quantity
unknown` instead of 0.0000 until a refresh that started with it tracked has
fetched every address's token balances, even if the popup closes first. To
tell an address whose token balances the explorer did not return, the refresh
takes them off its copy of the wallets before fetching.

Model: opus-5-5
2026-10-21 04:19:44 +00:00
7 changed files with 64 additions and 29 deletions
+7 -6
View File
@@ -1573,8 +1573,8 @@ The popup refreshes balances every ten seconds while it is open
(`doRefreshAndRender()` in `src/popup/index.js`). Each refresh that returns
draws the wallet list again and, when AddressDetail or AddressToken is up, that
screen's USD total and balance lines; its transaction list is left alone. No
other screen is drawn again by a refresh, since the rest show no balances or
hold what the user typed.
other screen is drawn again by a refresh, DeleteAddress and its balance lines
included.
A network switch leaves every screen on the page showing the previous network.
The user makes it in Settings, or approves a site's request and the background
@@ -2090,10 +2090,11 @@ view would leave a wallet one click from deletion.
**AddressDetail**. The token's balance is not known until a balance
refresh that started with it tracked has fetched every address's token
balances; until then its balance line reads `quantity unknown`, never
`0.0000`, unless the explorer had already listed a holding of it. A
refresh that returns draws AddressDetail's balances again (see
Navigation), so the fetched balance shows without the screen being
reopened
`0.0000`, unless the explorer had already listed a holding of it. That
holds after the popup is closed and opened again, as the tokens still
waiting for a fetch are stored with the profile. A refresh that returns
draws AddressDetail's balances again (see Navigation), so the fetched
balance shows without the screen being reopened
- "Add" with a token already tracked, a scam-listed address, or a failed
contract lookup → flash message, no screen change
- "Add", then a network switch in Settings before the contract lookup
+4 -4
View File
@@ -21,7 +21,6 @@ const {
showSaveFailureBanner,
pushCurrentView,
goBack,
tokensNotFetched,
} = require("./views/helpers");
const { applyTheme } = require("./theme");
const {
@@ -86,8 +85,7 @@ let refreshTimer = null;
//
// Once the save is done, the wallet list is drawn again, and so are the
// balances on AddressDetail or AddressToken when that is the screen up. No
// other screen is drawn again, since the rest show no balances or hold what the
// user typed.
// other screen is drawn again, DeleteAddress and its balance lines included.
async function doRefreshAndRender() {
if (refreshInFlight) return;
refreshInFlight = true;
@@ -140,7 +138,9 @@ async function doRefreshAndRender() {
// Each token this refresh asked about is now fetched for every
// address.
if (allTokenBalancesFetched) {
for (const token of tokens) tokensNotFetched.delete(token);
state.tokensNotFetched = state.tokensNotFetched.filter(
(token) => !tokens.includes(token),
);
}
state.lastBalanceRefresh = Date.now();
return true;
+1 -2
View File
@@ -13,7 +13,6 @@ const {
nativeCurrency,
balanceLine,
unknownableAmount,
tokensNotFetched,
renderAddressHtml,
attachCopyHandlers,
goBack,
@@ -165,7 +164,7 @@ function renderBalance() {
// token just added: no quantity to show, and none to price.
// balanceLine() states that rather than printing 0.0000.
if (tb) amount = unknownableAmount(tb.balance);
else amount = tokensNotFetched.has(id) ? null : 0;
else amount = state.tokensNotFetched.includes(id) ? null : 0;
price = getPrice(currentSymbol);
}
+8 -13
View File
@@ -380,20 +380,16 @@ function balanceLine(symbol, amount, price, tokenId) {
);
}
// Contract addresses, lowercased, of the tokens the user added that no balance
// refresh has fetched yet. An address with no balance row for one of them has
// no known balance of it, so its line says the quantity is unknown rather than
// 0.0000. trackToken() adds a token here, and doRefreshAndRender() in
// src/popup/index.js takes it off once a refresh that started with it tracked
// has fetched every address's token balances
// (https://git.eeqj.de/sneak/AutistMask/issues/516).
const tokensNotFetched = new Set();
// Tracks `token` on the active network, as every add path on AddToken and
// SettingsAddToken does. No refresh has fetched its balance yet.
// SettingsAddToken does, and lists it in `state.tokensNotFetched`: no refresh
// has fetched its balance yet. An address with no balance row for it has no
// known balance of it, so its line says the quantity is unknown rather than
// 0.0000. doRefreshAndRender() in src/popup/index.js takes it off once a
// refresh that started with it tracked has fetched every address's token
// balances (https://git.eeqj.de/sneak/AutistMask/issues/516).
function trackToken(token) {
state.trackedTokens.push(token);
tokensNotFetched.add(token.address.toLowerCase());
state.tokensNotFetched.push(token.address.toLowerCase());
}
function balanceLinesForAddress(addr, trackedTokens, showZero) {
@@ -429,7 +425,7 @@ function balanceLinesForAddress(addr, trackedTokens, showZero) {
if (seen.has(id)) continue;
html += balanceLine(
t.symbol,
tokensNotFetched.has(id) ? null : 0,
state.tokensNotFetched.includes(id) ? null : 0,
getPrice(t.symbol),
id,
);
@@ -803,7 +799,6 @@ module.exports = {
flashCopyFeedback,
balanceLine,
balanceLinesForAddress,
tokensNotFetched,
trackToken,
addressHoldsFunds,
keyMaterialName,
+9
View File
@@ -28,6 +28,13 @@ const DEFAULT_STATE = {
// others are restored from when the active network changes. See
// applyChainSwitchFields().
networkTrackedTokens: {},
// Contract addresses, lowercased, of the tokens the user added that no
// balance refresh has fetched yet. trackToken() in
// src/popup/views/helpers.js adds one, and doRefreshAndRender() in
// src/popup/index.js takes it off. Stored, so a token added just before
// the popup closed still reads `quantity unknown` when it opens again
// (https://git.eeqj.de/sneak/AutistMask/issues/516).
tokensNotFetched: [],
networkId: "mainnet",
rpcUrl: DEFAULT_RPC_URL,
blockscoutUrl: DEFAULT_BLOCKSCOUT_URL,
@@ -412,6 +419,8 @@ function normalizePersisted(saved) {
// so a stored string walks through the `|| []` and a stored number walks
// through an Array.isArray().
out.fraudContracts = textList(saved.fraudContracts);
// Contract addresses too, read with `includes()` and `filter()`.
out.tokensNotFetched = textList(saved.tokensNotFetched);
out.tokenHolderCache = structuredClone(saved.tokenHolderCache || {});
out.theme = saved.theme || "system";
out.debugMode = saved.debugMode !== undefined ? saved.debugMode : false;
+28 -4
View File
@@ -1,9 +1,10 @@
// After "Add" on AddToken, AddressDetail lists the new token, and its line reads
// `quantity unknown`, not 0.0000, until a balance refresh that asked the
// explorer about the token has returned for every address; so does a token
// added on SettingsAddToken, wherever it is listed. A refresh that
// returns draws the balances on AddressDetail or AddressToken again, so the
// fetched balance shows without the screen being reopened
// explorer about the token has returned for every address, the popup closing
// and opening again meanwhile included; so does a token added on
// SettingsAddToken, wherever it is listed. A refresh that returns draws the
// balances on AddressDetail or AddressToken again, so the fetched balance shows
// without the screen being reopened
// (https://git.eeqj.de/sneak/AutistMask/issues/516). 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.
@@ -163,6 +164,29 @@ test("a refresh already running when the token is added does not count as fetchi
expect(newTokenRow(page)).toContain("0.0000");
});
test("closed and opened again before any refresh has fetched it, the new token's line still reads quantity unknown", async () => {
const closed = await bootOnAddress();
holdNextRefresh(closed);
await addNewToken(closed);
cleanupPopup();
// The refresh this boot starts gets no token balances back: the boot's
// stub of refreshBalances() writes none.
const page = await bootPopup(undefined, { storage: closed.storage });
expect(page.visibleViews()).toEqual(["address"]);
const row = newTokenRow(page);
expect(row).toContain("quantity unknown");
expect(row).not.toContain("0.0000");
const finish = holdNextRefresh(page);
const ticking = page.tick();
await page.settle();
await finish([MILLION]);
await ticking;
expect(newTokenRow(page)).toContain("1000000.0000");
});
test("a refresh in which the explorer did not answer leaves the new token's line quantity unknown", async () => {
const page = await bootOnAddress();
const finish = holdNextRefresh(page);
+7
View File
@@ -248,6 +248,13 @@ const CONTRACT = [
floorOnly: [[42], [null], [{}], [[TOKEN_ADDRESS]]],
holds: (v) => everyEntry(v, isText),
},
{
field: "tokensNotFetched",
kind: KIND.ENTRIES,
hostile: [42, "notalist", { a: 1 }],
floorOnly: [[42], [null], [{}], [[TOKEN_ADDRESS]]],
holds: (v) => everyEntry(v, isText),
},
{
field: "viewStack",
kind: KIND.ENTRIES,