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 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
This commit was merged in pull request #741.
This commit is contained in:
+10
-7
@@ -99,12 +99,12 @@ let refreshTimer = null;
|
||||
//
|
||||
// The save also deletes `balanceNotFetched` from each tracked token whose
|
||||
// balance this refresh read at every address. A refresh that did not read every
|
||||
// address this page now holds deletes no `balanceNotFetched`. Adding a token
|
||||
// sets that field (trackToken() in src/popup/views/helpers.js). So does adding
|
||||
// an address, and so does a network switch. The save takes the field off the
|
||||
// matching tokens of the stored record (clearBalanceNotFetched() in
|
||||
// src/shared/state.js). The rest of the stored tracked token list stays as it
|
||||
// is.
|
||||
// address this page now holds, and every address in the stored record, deletes
|
||||
// no `balanceNotFetched`. Adding a token sets that field (trackToken() in
|
||||
// src/popup/views/helpers.js). So does adding an address, and so does a network
|
||||
// switch. The save takes the field off the matching tokens of the stored record
|
||||
// (clearBalanceNotFetched() in src/shared/state.js). The rest of the stored
|
||||
// tracked token list stays as it is.
|
||||
//
|
||||
// Once the save is done, the wallet list is drawn again. So are the balances
|
||||
// on AddressDetail or AddressToken when that screen is up. No other screen is
|
||||
@@ -151,7 +151,10 @@ async function doRefreshAndRender() {
|
||||
if (!stored.useBlockscout) {
|
||||
dropUntrackedTokens(state.wallets, stored.trackedTokens);
|
||||
}
|
||||
const readEveryAddress = state.wallets.every((wallet) =>
|
||||
const readEveryAddress = [
|
||||
...state.wallets,
|
||||
...stored.wallets,
|
||||
].every((wallet) =>
|
||||
wallet.addresses.every((addr) => refreshed.has(addr.address)),
|
||||
);
|
||||
if (readEveryAddress) clearBalanceNotFetched(stored, readTokens);
|
||||
|
||||
@@ -190,7 +190,7 @@ async function importMnemonic(ctx) {
|
||||
{
|
||||
address: firstAddress,
|
||||
index: 0,
|
||||
balance: "0.0000",
|
||||
balance: null,
|
||||
tokenBalances: [],
|
||||
},
|
||||
],
|
||||
@@ -218,7 +218,7 @@ async function importMnemonic(ctx) {
|
||||
wallet.addresses = scan.addresses.map((a) => ({
|
||||
address: a.address,
|
||||
index: a.index,
|
||||
balance: "0.0000",
|
||||
balance: null,
|
||||
tokenBalances: [],
|
||||
}));
|
||||
wallet.nextIndex = scan.nextIndex;
|
||||
@@ -267,7 +267,7 @@ async function importPrivateKey(ctx) {
|
||||
type: "key",
|
||||
name: "Wallet " + walletNum,
|
||||
encryptedSecret: encrypted,
|
||||
addresses: [{ address: addr, balance: "0.0000", tokenBalances: [] }],
|
||||
addresses: [{ address: addr, balance: null, tokenBalances: [] }],
|
||||
});
|
||||
markTrackedTokensNotFetched();
|
||||
state.hasWallet = true;
|
||||
@@ -328,7 +328,7 @@ async function importXprvKey(ctx) {
|
||||
{
|
||||
address: firstAddress,
|
||||
index: 0,
|
||||
balance: "0.0000",
|
||||
balance: null,
|
||||
tokenBalances: [],
|
||||
},
|
||||
],
|
||||
@@ -356,7 +356,7 @@ async function importXprvKey(ctx) {
|
||||
wallet.addresses = scan.addresses.map((a) => ({
|
||||
address: a.address,
|
||||
index: a.index,
|
||||
balance: "0.0000",
|
||||
balance: null,
|
||||
tokenBalances: [],
|
||||
}));
|
||||
wallet.nextIndex = scan.nextIndex;
|
||||
|
||||
@@ -273,7 +273,8 @@ function init(_ctx) {
|
||||
state.wallets[state.selectedWallet].addresses[
|
||||
state.selectedAddress
|
||||
];
|
||||
if (!addr.balance || parseFloat(addr.balance) === 0) {
|
||||
// A null balance is one no refresh has read, so it is not refused.
|
||||
if (parseFloat(addr.balance) === 0) {
|
||||
showFlash("Cannot send \u2014 zero balance.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,7 +161,8 @@ function renderBalance() {
|
||||
const tokenId = state.selectedToken;
|
||||
let amount, price;
|
||||
if (tokenId === "ETH") {
|
||||
amount = parseFloat(addr.balance || "0");
|
||||
// null when no refresh has read the balance.
|
||||
amount = unknownableAmount(addr.balance);
|
||||
price = getPrice("ETH");
|
||||
} else {
|
||||
const id = tokenId.toLowerCase();
|
||||
@@ -372,7 +373,8 @@ function init(_ctx) {
|
||||
state.wallets[state.selectedWallet].addresses[
|
||||
state.selectedAddress
|
||||
];
|
||||
if (!addr.balance || parseFloat(addr.balance) === 0) {
|
||||
// A null balance is one no refresh has read, so it is not refused.
|
||||
if (parseFloat(addr.balance) === 0) {
|
||||
if (state.selectedToken === "ETH") {
|
||||
showFlash("Cannot send \u2014 zero balance.");
|
||||
return;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -283,9 +283,10 @@ function showFlash(msg, duration = 2000) {
|
||||
flashTimer = setTimeout(clearFlash, duration);
|
||||
}
|
||||
|
||||
// A stored token balance as a number, or null when there is no number in it.
|
||||
// balances.js writes null for a holding whose scale nothing knows, and this
|
||||
// keeps that null from becoming a zero one dereference later.
|
||||
// A stored balance as a number, or null when there is no number in it.
|
||||
// balances.js writes null for a holding whose scale nothing knows, and an
|
||||
// address's ETH balance is null until a refresh has read it. This keeps that
|
||||
// null from becoming a zero one dereference later.
|
||||
function unknownableAmount(balance) {
|
||||
if (balance == null) return null;
|
||||
const n = parseFloat(balance);
|
||||
@@ -394,10 +395,10 @@ function formatFee(wei, symbol, chainId) {
|
||||
// holding renders through here.
|
||||
//
|
||||
// `amount` is null for a holding whose scale nothing knows
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/349). There is no quantity to
|
||||
// print for it and no fiat value to derive from one, and printing 0.0000 for
|
||||
// a real holding is the failure this whole rule exists to prevent, so the row
|
||||
// says so instead.
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/349), and for a balance no
|
||||
// refresh has read. There is no quantity to print for it and no fiat value to
|
||||
// derive from one, and printing 0.0000 for a real holding is the failure this
|
||||
// whole rule exists to prevent, so the row says so instead.
|
||||
function balanceLine(symbol, amount, price, tokenId) {
|
||||
const qty = amount === null ? "quantity unknown" : amount.toFixed(4);
|
||||
const usd =
|
||||
@@ -440,7 +441,7 @@ function markTrackedTokensNotFetched() {
|
||||
function balanceLinesForAddress(addr, trackedTokens, showZero) {
|
||||
let html = balanceLine(
|
||||
nativeCurrency(),
|
||||
parseFloat(addr.balance || "0"),
|
||||
unknownableAmount(addr.balance),
|
||||
getPrice("ETH"),
|
||||
"ETH",
|
||||
);
|
||||
@@ -493,7 +494,10 @@ function balanceLinesForAddress(addr, trackedTokens, showZero) {
|
||||
// about holdings must ask this, not the rendered figure.
|
||||
function addressHoldsFunds(addr) {
|
||||
if (!addr) return false;
|
||||
if (parseFloat(addr.balance || "0") > 0) return true;
|
||||
// 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 (parseFloat(addr.balance) > 0) return true;
|
||||
for (const t of addr.tokenBalances || []) {
|
||||
if (isBelowOneMillionth(t.balance)) continue;
|
||||
// A null balance is a holding whose amount could not be stated —
|
||||
|
||||
+14
-6
@@ -3,6 +3,7 @@ const {
|
||||
showView,
|
||||
showFlash,
|
||||
balanceLinesForAddress,
|
||||
unknownableAmount,
|
||||
markTrackedTokensNotFetched,
|
||||
isoDate,
|
||||
timeAgo,
|
||||
@@ -76,10 +77,16 @@ function renderTotalValue() {
|
||||
if (subEl) subEl.innerHTML = " ";
|
||||
return;
|
||||
}
|
||||
const ethBal = parseFloat(addr.balance || "0");
|
||||
const ethStr = ethBal.toFixed(4) + " " + nativeCurrency();
|
||||
const ethUsd = ethPrice ? " (" + formatUsd(ethBal * ethPrice) + ")" : "";
|
||||
el.textContent = ethStr + ethUsd;
|
||||
const ethBal = unknownableAmount(addr.balance);
|
||||
if (ethBal === null) {
|
||||
el.textContent = "unknown (" + nativeCurrency() + ")";
|
||||
} else {
|
||||
const ethStr = ethBal.toFixed(4) + " " + nativeCurrency();
|
||||
const ethUsd = ethPrice
|
||||
? " (" + formatUsd(ethBal * ethPrice) + ")"
|
||||
: "";
|
||||
el.textContent = ethStr + ethUsd;
|
||||
}
|
||||
|
||||
if (subEl) {
|
||||
subEl.innerHTML =
|
||||
@@ -411,7 +418,7 @@ function render(ctx) {
|
||||
wallet.addresses.push({
|
||||
address: newAddr,
|
||||
index: wallet.nextIndex,
|
||||
balance: "0.0000",
|
||||
balance: null,
|
||||
tokenBalances: [],
|
||||
});
|
||||
wallet.nextIndex++;
|
||||
@@ -498,7 +505,8 @@ function init(ctx) {
|
||||
return;
|
||||
}
|
||||
const addr = currentAddress();
|
||||
if (!addr.balance || parseFloat(addr.balance) === 0) {
|
||||
// A null balance is one no refresh has read, so it is not refused.
|
||||
if (parseFloat(addr.balance) === 0) {
|
||||
showFlash("Cannot send \u2014 zero balance.");
|
||||
return;
|
||||
}
|
||||
|
||||
+17
-6
@@ -230,11 +230,14 @@ function updateSendBalance() {
|
||||
attachCopyHandlers($("send-from"));
|
||||
const token = state.selectedToken || $("send-token").value;
|
||||
if (token === "ETH") {
|
||||
// A null balance is one no refresh has read.
|
||||
$("send-balance").textContent =
|
||||
"Current balance: " +
|
||||
truncateAmountNeverZero(addr.balance || "0") +
|
||||
" " +
|
||||
nativeCurrency();
|
||||
addr.balance == null
|
||||
? "Current balance: unknown (" + nativeCurrency() + ")"
|
||||
: "Current balance: " +
|
||||
truncateAmountNeverZero(addr.balance) +
|
||||
" " +
|
||||
nativeCurrency();
|
||||
} else {
|
||||
const symbol = resolveSymbol(
|
||||
token,
|
||||
@@ -289,6 +292,12 @@ async function fillMaxAmount() {
|
||||
return;
|
||||
}
|
||||
|
||||
// A null balance is one no refresh has read.
|
||||
if (addr.balance == null) {
|
||||
showFlash("This token's balance is unknown.");
|
||||
return;
|
||||
}
|
||||
|
||||
// The fee is estimated for this recipient, as the confirmation screen
|
||||
// estimates it: sending to a contract can cost more gas.
|
||||
const to = $("send-to").value.trim();
|
||||
@@ -306,7 +315,7 @@ async function fillMaxAmount() {
|
||||
provider.estimateGas({
|
||||
from: addr.address,
|
||||
to,
|
||||
value: parseEther(addr.balance || "0"),
|
||||
value: parseEther(addr.balance),
|
||||
}),
|
||||
]);
|
||||
feeWei = feeReserveWei(gasLimit, feeData);
|
||||
@@ -321,8 +330,10 @@ async function fillMaxAmount() {
|
||||
}
|
||||
// While the estimate was in flight the user left the screen (and perhaps
|
||||
// opened it again), typed an amount, or changed the address, the holding
|
||||
// or the recipient: what they did wins.
|
||||
// or the recipient: what they did wins. A network switch in that time
|
||||
// leaves no balance to fill in from.
|
||||
if (
|
||||
addr.balance == null ||
|
||||
state.currentView !== "send" ||
|
||||
sendScreenOpenings !== opening ||
|
||||
currentAddress()?.address !== addr.address ||
|
||||
|
||||
Reference in New Issue
Block a user