1 Commits
Author SHA1 Message Date
sneak 617bd91688 fix: the ETH line reads unknown at an address no refresh has read (closes #736)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
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 warns that
it may hold a balance.

A refresh also deletes `balanceNotFetched` only when it read every address in
the stored record.

Model: opus-5-5
2026-11-03 09:46:50 +00:00
5 changed files with 32 additions and 13 deletions
+2 -1
View File
@@ -1025,7 +1025,8 @@ removing the address only stops the wallet tracking it. It explains the way
back: delete the whole wallet in Settings and import its backup again. The scan
after an import finds only addresses with on-chain activity. When the address
holds anything, the screen lists the holdings and the USD total under a warning.
An ETH balance that no refresh has read counts as holding something.
An ETH balance that no refresh has read counts as holding something. The warning
then reads "This address may hold a balance."
"Remove Address" removes the address and its site permissions. It returns to
Home with the flash message "Address removed." The wallet's `nextIndex` is not
+9 -2
View File
@@ -87,15 +87,22 @@ function recoveryPathText(wallet) {
// when every holding here is one with no price, since "$0.00" directly under
// "This address holds a balance." is a contradiction. A token holding below
// 0.000001 does not count, as the lines below leave it out.
//
// While no refresh has read the ETH balance the address may be empty, so the
// sentence says "may hold".
function balanceWarningHtml(addr) {
if (!addressHoldsFunds(addr)) return " ";
const line = formatAddressTotal(getAddressValue(addr));
const total = line
? `<div class="text-xs text-muted mt-1">${escapeHtml(line)}</div>`
: "";
const sentence =
addr.balance == null
? "This address may hold a balance."
: "This address holds a balance.";
return (
`<p class="mb-1">This address holds a balance. Removing it does not ` +
`move or spend anything; the balance stays at the address.</p>` +
`<p class="mb-1">${sentence} Removing it does not move or spend ` +
`anything; the balance stays at the address.</p>` +
balanceLinesForAddress(addr, state.trackedTokens, false) +
total
);
+2 -2
View File
@@ -494,9 +494,9 @@ function balanceLinesForAddress(addr, trackedTokens, showZero) {
// about holdings must ask this, not the rendered figure.
function addressHoldsFunds(addr) {
if (!addr) return false;
// A null ETH balance is one no refresh has read, so the address may hold
// A null or absent ETH balance reads as unknown, so the address may hold
// funds. Warning about funds must err towards warning.
if (addr.balance === null) return true;
if (addr.balance == null) return true;
if (parseFloat(addr.balance) > 0) return true;
for (const t of addr.tokenBalances || []) {
if (isBelowOneMillionth(t.balance)) continue;
+2 -2
View File
@@ -146,8 +146,8 @@ function formatUsd(amount) {
// way an absent holders_count is not a count of zero.
//
// usd: the value of the holdings a price is known for, or null when
// nothing is knowable at all — testnet, before the first fetch, or
// an ETH balance no refresh has read.
// no total can be stated — testnet, before the first fetch, or an
// ETH balance no refresh has read.
// partial: the address also holds a token with no price, so usd is a floor
// and not the total.
//
+17 -6
View File
@@ -117,9 +117,13 @@ describe("whether an address holds anything", () => {
expect(addressHoldsFunds(ZERO_TOKEN)).toBe(false);
});
test("a missing address or missing fields do not", () => {
test("a missing address does not", () => {
expect(addressHoldsFunds(undefined)).toBe(false);
expect(addressHoldsFunds({ address: "0x1" })).toBe(false);
});
// It reads as unknown, as a null balance does.
test("an address with no ETH balance field counts", () => {
expect(addressHoldsFunds({ address: "0x1" })).toBe(true);
});
});
@@ -146,16 +150,23 @@ describe("the balance warning on the removal confirmation", () => {
expect(html).toContain("<span>0.0000</span>");
});
// The sentence must not assert an amount, because any amount it could
// assert has been rounded: "This address holds 0.0000 ETH." is what the
// rounded form produces for an address that holds real money.
test("an ETH balance no refresh has read is warned about, and reads as unknown", () => {
const html = balanceWarningHtml(NOT_READ);
expect(html).toContain("This address holds a balance");
expect(html).toContain("This address may hold a balance.");
expect(html).not.toContain("This address holds a balance.");
expect(html).toContain("quantity unknown");
expect(html).not.toContain("0.0000");
});
test("an address with no ETH balance field reads the same", () => {
expect(balanceWarningHtml({ address: "0x1" })).toContain(
"This address may hold a balance.",
);
});
// The sentence must not assert an amount, because any amount it could
// assert has been rounded: "This address holds 0.0000 ETH." is what the
// rounded form produces for an address that holds real money.
test("the warning sentence asserts no rounded amount", () => {
for (const addr of [DUST, ETH_ONLY, TOKEN_ONLY]) {
expect(balanceWarningHtml(addr)).not.toMatch(