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 is contained in:
@@ -1025,6 +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
|
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
|
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.
|
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. The warning
|
||||||
|
then reads "This address may hold a balance."
|
||||||
|
|
||||||
"Remove Address" removes the address and its site permissions. It returns to
|
"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
|
Home with the flash message "Address removed." The wallet's `nextIndex` is not
|
||||||
@@ -1212,13 +1214,25 @@ there in place of a zero. It does so after each of these:
|
|||||||
- an address or a wallet is added;
|
- an address or a wallet is added;
|
||||||
- the network is switched.
|
- the network is switched.
|
||||||
|
|
||||||
This lasts until a refresh has read the token's balance at every address.
|
This lasts until a refresh in the popup has read the token's balance at every
|
||||||
|
address.
|
||||||
|
|
||||||
|
An address's ETH balance is `null` until a refresh has read that address. This
|
||||||
|
holds for a new address, and for every address after a network switch. While it
|
||||||
|
is `null`:
|
||||||
|
|
||||||
|
- the balance lists show "quantity unknown";
|
||||||
|
- Home's ETH balance reads `unknown (ETH)`;
|
||||||
|
- Send reads `Current balance: unknown (ETH)`, and "Max" fills in nothing;
|
||||||
|
- "Send" is not refused for a zero balance;
|
||||||
|
- the address has no total;
|
||||||
|
- DeleteAddress shows its warning.
|
||||||
|
|
||||||
Only a whole number from 0 to 80 counts as a token's decimals. A token's
|
Only a whole number from 0 to 80 counts as a token's decimals. A token's
|
||||||
`decimals` is stored only as a source reported it, never guessed. A `null`
|
`decimals` is stored only as a source reported it, never guessed. A token's
|
||||||
balance means the decimals are unknown, never that the balance is zero. While
|
`null` balance means the decimals are unknown, never that the balance is zero.
|
||||||
"Use Blockscout" is off, the bundled list and the tracked tokens may lack a
|
While "Use Blockscout" is off, the bundled list and the tracked tokens may lack
|
||||||
token's symbol or decimals. The approval and status screens then add a line
|
a token's symbol or decimals. The approval and status screens then add a line
|
||||||
under that token saying that Blockscout's report is not used.
|
under that token saying that Blockscout's report is not used.
|
||||||
|
|
||||||
The wallet's own ERC-20 send is encoded with the decimals the screen showed. At
|
The wallet's own ERC-20 send is encoded with the decimals the screen showed. At
|
||||||
@@ -1254,6 +1268,7 @@ unpriced holding was left out. `formatAddressTotal()` renders that result the
|
|||||||
same way on every screen:
|
same way on every screen:
|
||||||
|
|
||||||
- on Sepolia, or before the first price fetch: no total;
|
- on Sepolia, or before the first price fetch: no total;
|
||||||
|
- while the address's ETH balance is unknown: no total;
|
||||||
- everything priced: `Total: $5,500.00`;
|
- everything priced: `Total: $5,500.00`;
|
||||||
- part priced: `Total: $3,000.00 plus unpriced tokens`;
|
- part priced: `Total: $3,000.00 plus unpriced tokens`;
|
||||||
- nothing priced but something held: `Total: unpriced tokens only`.
|
- nothing priced but something held: `Total: unpriced tokens only`.
|
||||||
|
|||||||
+10
-7
@@ -99,12 +99,12 @@ let refreshTimer = null;
|
|||||||
//
|
//
|
||||||
// The save also deletes `balanceNotFetched` from each tracked token whose
|
// The save also deletes `balanceNotFetched` from each tracked token whose
|
||||||
// balance this refresh read at every address. A refresh that did not read every
|
// 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
|
// address this page now holds, and every address in the stored record, deletes
|
||||||
// sets that field (trackToken() in src/popup/views/helpers.js). So does adding
|
// no `balanceNotFetched`. Adding a token sets that field (trackToken() in
|
||||||
// an address, and so does a network switch. The save takes the field off the
|
// src/popup/views/helpers.js). So does adding an address, and so does a network
|
||||||
// matching tokens of the stored record (clearBalanceNotFetched() in
|
// switch. The save takes the field off the matching tokens of the stored record
|
||||||
// src/shared/state.js). The rest of the stored tracked token list stays as it
|
// (clearBalanceNotFetched() in src/shared/state.js). The rest of the stored
|
||||||
// is.
|
// tracked token list stays as it is.
|
||||||
//
|
//
|
||||||
// Once the save is done, the wallet list is drawn again. So are the balances
|
// 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
|
// on AddressDetail or AddressToken when that screen is up. No other screen is
|
||||||
@@ -151,7 +151,10 @@ async function doRefreshAndRender() {
|
|||||||
if (!stored.useBlockscout) {
|
if (!stored.useBlockscout) {
|
||||||
dropUntrackedTokens(state.wallets, stored.trackedTokens);
|
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)),
|
wallet.addresses.every((addr) => refreshed.has(addr.address)),
|
||||||
);
|
);
|
||||||
if (readEveryAddress) clearBalanceNotFetched(stored, readTokens);
|
if (readEveryAddress) clearBalanceNotFetched(stored, readTokens);
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ async function importMnemonic(ctx) {
|
|||||||
{
|
{
|
||||||
address: firstAddress,
|
address: firstAddress,
|
||||||
index: 0,
|
index: 0,
|
||||||
balance: "0.0000",
|
balance: null,
|
||||||
tokenBalances: [],
|
tokenBalances: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -218,7 +218,7 @@ async function importMnemonic(ctx) {
|
|||||||
wallet.addresses = scan.addresses.map((a) => ({
|
wallet.addresses = scan.addresses.map((a) => ({
|
||||||
address: a.address,
|
address: a.address,
|
||||||
index: a.index,
|
index: a.index,
|
||||||
balance: "0.0000",
|
balance: null,
|
||||||
tokenBalances: [],
|
tokenBalances: [],
|
||||||
}));
|
}));
|
||||||
wallet.nextIndex = scan.nextIndex;
|
wallet.nextIndex = scan.nextIndex;
|
||||||
@@ -267,7 +267,7 @@ async function importPrivateKey(ctx) {
|
|||||||
type: "key",
|
type: "key",
|
||||||
name: "Wallet " + walletNum,
|
name: "Wallet " + walletNum,
|
||||||
encryptedSecret: encrypted,
|
encryptedSecret: encrypted,
|
||||||
addresses: [{ address: addr, balance: "0.0000", tokenBalances: [] }],
|
addresses: [{ address: addr, balance: null, tokenBalances: [] }],
|
||||||
});
|
});
|
||||||
markTrackedTokensNotFetched();
|
markTrackedTokensNotFetched();
|
||||||
state.hasWallet = true;
|
state.hasWallet = true;
|
||||||
@@ -328,7 +328,7 @@ async function importXprvKey(ctx) {
|
|||||||
{
|
{
|
||||||
address: firstAddress,
|
address: firstAddress,
|
||||||
index: 0,
|
index: 0,
|
||||||
balance: "0.0000",
|
balance: null,
|
||||||
tokenBalances: [],
|
tokenBalances: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -356,7 +356,7 @@ async function importXprvKey(ctx) {
|
|||||||
wallet.addresses = scan.addresses.map((a) => ({
|
wallet.addresses = scan.addresses.map((a) => ({
|
||||||
address: a.address,
|
address: a.address,
|
||||||
index: a.index,
|
index: a.index,
|
||||||
balance: "0.0000",
|
balance: null,
|
||||||
tokenBalances: [],
|
tokenBalances: [],
|
||||||
}));
|
}));
|
||||||
wallet.nextIndex = scan.nextIndex;
|
wallet.nextIndex = scan.nextIndex;
|
||||||
|
|||||||
@@ -273,7 +273,8 @@ function init(_ctx) {
|
|||||||
state.wallets[state.selectedWallet].addresses[
|
state.wallets[state.selectedWallet].addresses[
|
||||||
state.selectedAddress
|
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.");
|
showFlash("Cannot send \u2014 zero balance.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,8 @@ function renderBalance() {
|
|||||||
const tokenId = state.selectedToken;
|
const tokenId = state.selectedToken;
|
||||||
let amount, price;
|
let amount, price;
|
||||||
if (tokenId === "ETH") {
|
if (tokenId === "ETH") {
|
||||||
amount = parseFloat(addr.balance || "0");
|
// null when no refresh has read the balance.
|
||||||
|
amount = unknownableAmount(addr.balance);
|
||||||
price = getPrice("ETH");
|
price = getPrice("ETH");
|
||||||
} else {
|
} else {
|
||||||
const id = tokenId.toLowerCase();
|
const id = tokenId.toLowerCase();
|
||||||
@@ -372,7 +373,8 @@ function init(_ctx) {
|
|||||||
state.wallets[state.selectedWallet].addresses[
|
state.wallets[state.selectedWallet].addresses[
|
||||||
state.selectedAddress
|
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") {
|
if (state.selectedToken === "ETH") {
|
||||||
showFlash("Cannot send \u2014 zero balance.");
|
showFlash("Cannot send \u2014 zero balance.");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -87,15 +87,22 @@ function recoveryPathText(wallet) {
|
|||||||
// when every holding here is one with no price, since "$0.00" directly under
|
// 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
|
// "This address holds a balance." is a contradiction. A token holding below
|
||||||
// 0.000001 does not count, as the lines below leave it out.
|
// 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) {
|
function balanceWarningHtml(addr) {
|
||||||
if (!addressHoldsFunds(addr)) return " ";
|
if (!addressHoldsFunds(addr)) return " ";
|
||||||
const line = formatAddressTotal(getAddressValue(addr));
|
const line = formatAddressTotal(getAddressValue(addr));
|
||||||
const total = line
|
const total = line
|
||||||
? `<div class="text-xs text-muted mt-1">${escapeHtml(line)}</div>`
|
? `<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 (
|
return (
|
||||||
`<p class="mb-1">This address holds a balance. Removing it does not ` +
|
`<p class="mb-1">${sentence} Removing it does not move or spend ` +
|
||||||
`move or spend anything; the balance stays at the address.</p>` +
|
`anything; the balance stays at the address.</p>` +
|
||||||
balanceLinesForAddress(addr, state.trackedTokens, false) +
|
balanceLinesForAddress(addr, state.trackedTokens, false) +
|
||||||
total
|
total
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -283,9 +283,10 @@ function showFlash(msg, duration = 2000) {
|
|||||||
flashTimer = setTimeout(clearFlash, duration);
|
flashTimer = setTimeout(clearFlash, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A stored token balance as a number, or null when there is no number in it.
|
// 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 this
|
// balances.js writes null for a holding whose scale nothing knows, and an
|
||||||
// keeps that null from becoming a zero one dereference later.
|
// 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) {
|
function unknownableAmount(balance) {
|
||||||
if (balance == null) return null;
|
if (balance == null) return null;
|
||||||
const n = parseFloat(balance);
|
const n = parseFloat(balance);
|
||||||
@@ -394,10 +395,10 @@ function formatFee(wei, symbol, chainId) {
|
|||||||
// holding renders through here.
|
// holding renders through here.
|
||||||
//
|
//
|
||||||
// `amount` is null for a holding whose scale nothing knows
|
// `amount` is null for a holding whose scale nothing knows
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/349). There is no quantity to
|
// (https://git.eeqj.de/sneak/AutistMask/issues/349), and for a balance no
|
||||||
// print for it and no fiat value to derive from one, and printing 0.0000 for
|
// refresh has read. There is no quantity to print for it and no fiat value to
|
||||||
// a real holding is the failure this whole rule exists to prevent, so the row
|
// derive from one, and printing 0.0000 for a real holding is the failure this
|
||||||
// says so instead.
|
// whole rule exists to prevent, so the row says so instead.
|
||||||
function balanceLine(symbol, amount, price, tokenId) {
|
function balanceLine(symbol, amount, price, tokenId) {
|
||||||
const qty = amount === null ? "quantity unknown" : amount.toFixed(4);
|
const qty = amount === null ? "quantity unknown" : amount.toFixed(4);
|
||||||
const usd =
|
const usd =
|
||||||
@@ -440,7 +441,7 @@ function markTrackedTokensNotFetched() {
|
|||||||
function balanceLinesForAddress(addr, trackedTokens, showZero) {
|
function balanceLinesForAddress(addr, trackedTokens, showZero) {
|
||||||
let html = balanceLine(
|
let html = balanceLine(
|
||||||
nativeCurrency(),
|
nativeCurrency(),
|
||||||
parseFloat(addr.balance || "0"),
|
unknownableAmount(addr.balance),
|
||||||
getPrice("ETH"),
|
getPrice("ETH"),
|
||||||
"ETH",
|
"ETH",
|
||||||
);
|
);
|
||||||
@@ -493,7 +494,10 @@ function balanceLinesForAddress(addr, trackedTokens, showZero) {
|
|||||||
// about holdings must ask this, not the rendered figure.
|
// about holdings must ask this, not the rendered figure.
|
||||||
function addressHoldsFunds(addr) {
|
function addressHoldsFunds(addr) {
|
||||||
if (!addr) return false;
|
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 || []) {
|
for (const t of addr.tokenBalances || []) {
|
||||||
if (isBelowOneMillionth(t.balance)) continue;
|
if (isBelowOneMillionth(t.balance)) continue;
|
||||||
// A null balance is a holding whose amount could not be stated —
|
// A null balance is a holding whose amount could not be stated —
|
||||||
|
|||||||
+14
-6
@@ -3,6 +3,7 @@ const {
|
|||||||
showView,
|
showView,
|
||||||
showFlash,
|
showFlash,
|
||||||
balanceLinesForAddress,
|
balanceLinesForAddress,
|
||||||
|
unknownableAmount,
|
||||||
markTrackedTokensNotFetched,
|
markTrackedTokensNotFetched,
|
||||||
isoDate,
|
isoDate,
|
||||||
timeAgo,
|
timeAgo,
|
||||||
@@ -76,10 +77,16 @@ function renderTotalValue() {
|
|||||||
if (subEl) subEl.innerHTML = " ";
|
if (subEl) subEl.innerHTML = " ";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ethBal = parseFloat(addr.balance || "0");
|
const ethBal = unknownableAmount(addr.balance);
|
||||||
const ethStr = ethBal.toFixed(4) + " " + nativeCurrency();
|
if (ethBal === null) {
|
||||||
const ethUsd = ethPrice ? " (" + formatUsd(ethBal * ethPrice) + ")" : "";
|
el.textContent = "unknown (" + nativeCurrency() + ")";
|
||||||
el.textContent = ethStr + ethUsd;
|
} else {
|
||||||
|
const ethStr = ethBal.toFixed(4) + " " + nativeCurrency();
|
||||||
|
const ethUsd = ethPrice
|
||||||
|
? " (" + formatUsd(ethBal * ethPrice) + ")"
|
||||||
|
: "";
|
||||||
|
el.textContent = ethStr + ethUsd;
|
||||||
|
}
|
||||||
|
|
||||||
if (subEl) {
|
if (subEl) {
|
||||||
subEl.innerHTML =
|
subEl.innerHTML =
|
||||||
@@ -411,7 +418,7 @@ function render(ctx) {
|
|||||||
wallet.addresses.push({
|
wallet.addresses.push({
|
||||||
address: newAddr,
|
address: newAddr,
|
||||||
index: wallet.nextIndex,
|
index: wallet.nextIndex,
|
||||||
balance: "0.0000",
|
balance: null,
|
||||||
tokenBalances: [],
|
tokenBalances: [],
|
||||||
});
|
});
|
||||||
wallet.nextIndex++;
|
wallet.nextIndex++;
|
||||||
@@ -498,7 +505,8 @@ function init(ctx) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const addr = currentAddress();
|
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.");
|
showFlash("Cannot send \u2014 zero balance.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-6
@@ -230,11 +230,14 @@ function updateSendBalance() {
|
|||||||
attachCopyHandlers($("send-from"));
|
attachCopyHandlers($("send-from"));
|
||||||
const token = state.selectedToken || $("send-token").value;
|
const token = state.selectedToken || $("send-token").value;
|
||||||
if (token === "ETH") {
|
if (token === "ETH") {
|
||||||
|
// A null balance is one no refresh has read.
|
||||||
$("send-balance").textContent =
|
$("send-balance").textContent =
|
||||||
"Current balance: " +
|
addr.balance == null
|
||||||
truncateAmountNeverZero(addr.balance || "0") +
|
? "Current balance: unknown (" + nativeCurrency() + ")"
|
||||||
" " +
|
: "Current balance: " +
|
||||||
nativeCurrency();
|
truncateAmountNeverZero(addr.balance) +
|
||||||
|
" " +
|
||||||
|
nativeCurrency();
|
||||||
} else {
|
} else {
|
||||||
const symbol = resolveSymbol(
|
const symbol = resolveSymbol(
|
||||||
token,
|
token,
|
||||||
@@ -289,6 +292,12 @@ async function fillMaxAmount() {
|
|||||||
return;
|
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
|
// The fee is estimated for this recipient, as the confirmation screen
|
||||||
// estimates it: sending to a contract can cost more gas.
|
// estimates it: sending to a contract can cost more gas.
|
||||||
const to = $("send-to").value.trim();
|
const to = $("send-to").value.trim();
|
||||||
@@ -306,7 +315,7 @@ async function fillMaxAmount() {
|
|||||||
provider.estimateGas({
|
provider.estimateGas({
|
||||||
from: addr.address,
|
from: addr.address,
|
||||||
to,
|
to,
|
||||||
value: parseEther(addr.balance || "0"),
|
value: parseEther(addr.balance),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
feeWei = feeReserveWei(gasLimit, feeData);
|
feeWei = feeReserveWei(gasLimit, feeData);
|
||||||
@@ -321,8 +330,10 @@ async function fillMaxAmount() {
|
|||||||
}
|
}
|
||||||
// While the estimate was in flight the user left the screen (and perhaps
|
// 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
|
// 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 (
|
if (
|
||||||
|
addr.balance == null ||
|
||||||
state.currentView !== "send" ||
|
state.currentView !== "send" ||
|
||||||
sendScreenOpenings !== opening ||
|
sendScreenOpenings !== opening ||
|
||||||
currentAddress()?.address !== addr.address ||
|
currentAddress()?.address !== addr.address ||
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function clearPopupCaches() {
|
|||||||
// follow runs first, and this switch is made after it.
|
// follow runs first, and this switch is made after it.
|
||||||
//
|
//
|
||||||
// The save writes only the fields that differ from what this page last saved.
|
// The save writes only the fields that differ from what this page last saved.
|
||||||
// When this page last saved a balance of "0", clearing that balance is no
|
// When this page last saved a balance of null, clearing that balance is no
|
||||||
// change. So the switch also resets these fields in the stored record:
|
// change. So the switch also resets these fields in the stored record:
|
||||||
//
|
//
|
||||||
// - every address's balance and token balances;
|
// - every address's balance and token balances;
|
||||||
@@ -100,7 +100,7 @@ async function onChainSwitch(newNetworkId) {
|
|||||||
stored.fraudContracts = [];
|
stored.fraudContracts = [];
|
||||||
for (const wallet of stored.wallets) {
|
for (const wallet of stored.wallets) {
|
||||||
for (const addr of wallet.addresses) {
|
for (const addr of wallet.addresses) {
|
||||||
addr.balance = "0";
|
addr.balance = null;
|
||||||
addr.tokenBalances = [];
|
addr.tokenBalances = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,10 +68,11 @@ function applyChainSwitchFields(s, newNetworkId) {
|
|||||||
|
|
||||||
// Clear per-address balances and token balances so stale data
|
// Clear per-address balances and token balances so stale data
|
||||||
// from the previous chain is never displayed while the first
|
// from the previous chain is never displayed while the first
|
||||||
// refresh on the new chain is in flight.
|
// refresh on the new chain is in flight. A balance of null is one no
|
||||||
|
// refresh has read: it reads as unknown until one has, not as 0.0000.
|
||||||
for (const wallet of s.wallets || []) {
|
for (const wallet of s.wallets || []) {
|
||||||
for (const addr of wallet.addresses || []) {
|
for (const addr of wallet.addresses || []) {
|
||||||
addr.balance = "0";
|
addr.balance = null;
|
||||||
addr.tokenBalances = [];
|
addr.tokenBalances = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +109,7 @@ function copyChainSwitchFields(s, switched) {
|
|||||||
for (const wallet of s.wallets) {
|
for (const wallet of s.wallets) {
|
||||||
for (const addr of wallet.addresses) {
|
for (const addr of wallet.addresses) {
|
||||||
const got = stored.get(addr.address);
|
const got = stored.get(addr.address);
|
||||||
addr.balance = got ? got.balance : "0";
|
addr.balance = got ? got.balance : null;
|
||||||
addr.tokenBalances = got ? got.tokenBalances : [];
|
addr.tokenBalances = got ? got.tokenBalances : [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,7 +146,8 @@ function formatUsd(amount) {
|
|||||||
// way an absent holders_count is not a count of zero.
|
// 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
|
// usd: the value of the holdings a price is known for, or null when
|
||||||
// nothing is knowable at all — testnet, or before the first fetch.
|
// 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
|
// partial: the address also holds a token with no price, so usd is a floor
|
||||||
// and not the total.
|
// and not the total.
|
||||||
//
|
//
|
||||||
@@ -155,7 +156,8 @@ function getAddressValue(addr) {
|
|||||||
const { currentNetwork } = require("./state");
|
const { currentNetwork } = require("./state");
|
||||||
if (currentNetwork().isTestnet) return { usd: null, partial: false };
|
if (currentNetwork().isTestnet) return { usd: null, partial: false };
|
||||||
if (!prices.ETH) return { usd: null, partial: false };
|
if (!prices.ETH) return { usd: null, partial: false };
|
||||||
let usd = parseFloat(addr.balance || "0") * prices.ETH;
|
if (addr.balance == null) return { usd: null, partial: false };
|
||||||
|
let usd = parseFloat(addr.balance) * prices.ETH;
|
||||||
let partial = false;
|
let partial = false;
|
||||||
for (const token of addr.tokenBalances || []) {
|
for (const token of addr.tokenBalances || []) {
|
||||||
// A holding below 0.000001 is left out, as the balance lists leave it
|
// A holding below 0.000001 is left out, as the balance lists leave it
|
||||||
|
|||||||
@@ -15,6 +15,10 @@
|
|||||||
// added, and after a network switch, so it reads the same until a refresh has
|
// 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).
|
// 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,
|
// 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.
|
// 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
|
// 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
|
// with every address's ETH balance at 2 and `tokenBalances` as the first
|
||||||
// entries, having read the balance of every token tracked when it started.
|
// address's token entries, having read the balance of every token tracked when
|
||||||
// Given null, it finishes having read no token's balance, as when every
|
// it started. Given null, it finishes having read no token's balance, as when
|
||||||
// balanceOf fails, and leaves the token entries as they were.
|
// every balanceOf fails, and leaves the token entries as they were.
|
||||||
function holdNextRefresh(page) {
|
function holdNextRefresh(page) {
|
||||||
const { refreshBalances } = require("../src/shared/balances");
|
const { refreshBalances } = require("../src/shared/balances");
|
||||||
let release;
|
let release;
|
||||||
@@ -138,7 +142,9 @@ function holdNextRefresh(page) {
|
|||||||
const tokenBalances = await new Promise((resolve) => {
|
const tokenBalances = await new Promise((resolve) => {
|
||||||
release = 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 [];
|
if (tokenBalances === null) return [];
|
||||||
wallets[0].addresses[0].tokenBalances = tokenBalances;
|
wallets[0].addresses[0].tokenBalances = tokenBalances;
|
||||||
return read;
|
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 "+" 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
|
// 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.
|
// `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).toContain("quantity unknown");
|
||||||
expect(row).not.toContain("0.0000");
|
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.");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -125,6 +125,14 @@ describe("the value of an address, and whether it is the whole value", () => {
|
|||||||
expect(getAddressValue(addr)).toEqual({ usd: 0, partial: false });
|
expect(getAddressValue(addr)).toEqual({ usd: 0, partial: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// No total can include an ETH balance no refresh has read, and "unpriced
|
||||||
|
// tokens only" would be untrue of an address with nothing known at all.
|
||||||
|
test("an ETH balance no refresh has read means no value", () => {
|
||||||
|
const addr = { address: "0x1", balance: null, tokenBalances: [] };
|
||||||
|
expect(getAddressValue(addr)).toEqual({ usd: null, partial: false });
|
||||||
|
expect(formatAddressTotal(getAddressValue(addr))).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
// Before the first price fetch, and on testnet, nothing is knowable: that
|
// Before the first price fetch, and on testnet, nothing is knowable: that
|
||||||
// is a third state, and it stays distinct from both of the others.
|
// is a third state, and it stays distinct from both of the others.
|
||||||
test("no prices at all means no value, not an incomplete one", () => {
|
test("no prices at all means no value, not an incomplete one", () => {
|
||||||
|
|||||||
@@ -445,7 +445,7 @@ describe("a balance refresh under a network switch", () => {
|
|||||||
|
|
||||||
const after = bg.persisted();
|
const after = bg.persisted();
|
||||||
expect(after.networkId).toBe("mainnet");
|
expect(after.networkId).toBe("mainnet");
|
||||||
expect(after.wallets[0].addresses[0].balance).toBe("0");
|
expect(after.wallets[0].addresses[0].balance).toBeNull();
|
||||||
expect(after.lastBalanceRefresh).toBe(0);
|
expect(after.lastBalanceRefresh).toBe(0);
|
||||||
|
|
||||||
await bg.fireBalanceAlarm();
|
await bg.fireBalanceAlarm();
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ async function chooseNetwork(page, networkId) {
|
|||||||
function expectNothingStored(page) {
|
function expectNothingStored(page) {
|
||||||
const stored = page.storage.read("autistmask");
|
const stored = page.storage.read("autistmask");
|
||||||
expect(stored.networkId).toBe("sepolia");
|
expect(stored.networkId).toBe("sepolia");
|
||||||
expect(stored.wallets[0].addresses[0].balance).toBe("0");
|
expect(stored.wallets[0].addresses[0].balance).toBeNull();
|
||||||
expect(stored.wallets[0].addresses[0].tokenBalances).toEqual([]);
|
expect(stored.wallets[0].addresses[0].tokenBalances).toEqual([]);
|
||||||
expect(stored.lastBalanceRefresh).toBe(0);
|
expect(stored.lastBalanceRefresh).toBe(0);
|
||||||
}
|
}
|
||||||
@@ -86,13 +86,13 @@ test("a refresh that returns after a switch in Settings writes nothing, and the
|
|||||||
await chooseNetwork(page, "sepolia");
|
await chooseNetwork(page, "sepolia");
|
||||||
await refresh.finish();
|
await refresh.finish();
|
||||||
|
|
||||||
expect(state.wallets[0].addresses[0].balance).toBe("0");
|
expect(state.wallets[0].addresses[0].balance).toBeNull();
|
||||||
expect(state.wallets[0].addresses[0].tokenBalances).toEqual([]);
|
expect(state.wallets[0].addresses[0].tokenBalances).toEqual([]);
|
||||||
expectNothingStored(page);
|
expectNothingStored(page);
|
||||||
|
|
||||||
await page.click("btn-settings-back");
|
await page.click("btn-settings-back");
|
||||||
expect(page.visibleViews()).toEqual(["main"]);
|
expect(page.visibleViews()).toEqual(["main"]);
|
||||||
expect(page.text("total-value")).toBe("0.0000 SepoliaETH");
|
expect(page.text("total-value")).toBe("unknown (SepoliaETH)");
|
||||||
|
|
||||||
await page.tick();
|
await page.tick();
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ test("a refresh whose save is reading the stored record when the user switches i
|
|||||||
await page.settle();
|
await page.settle();
|
||||||
|
|
||||||
expect(state.networkId).toBe("sepolia");
|
expect(state.networkId).toBe("sepolia");
|
||||||
expect(state.wallets[0].addresses[0].balance).toBe("0");
|
expect(state.wallets[0].addresses[0].balance).toBeNull();
|
||||||
expect(state.wallets[0].addresses[0].tokenBalances).toEqual([]);
|
expect(state.wallets[0].addresses[0].tokenBalances).toEqual([]);
|
||||||
expectNothingStored(page);
|
expectNothingStored(page);
|
||||||
});
|
});
|
||||||
@@ -166,11 +166,11 @@ test("a refresh that returns after the background switched the network writes no
|
|||||||
});
|
});
|
||||||
|
|
||||||
// The background's refresh saves Ethereum Mainnet's balances while the switch
|
// The background's refresh saves Ethereum Mainnet's balances while the switch
|
||||||
// in Settings waits for the lock. This page last saved a balance of "0" and no
|
// in Settings waits for the lock. This page last saved a balance of null and no
|
||||||
// token balances, the values that the switch sets.
|
// token balances, the values that the switch sets.
|
||||||
test("a background refresh saved while a switch in Settings waits for the lock leaves none of its balances stored", async () => {
|
test("a background refresh saved while a switch in Settings waits for the lock leaves none of its balances stored", async () => {
|
||||||
const profile = unversionedValidProfile();
|
const profile = unversionedValidProfile();
|
||||||
profile.wallets[0].addresses[0].balance = "0";
|
profile.wallets[0].addresses[0].balance = null;
|
||||||
const page = await bootPopup(profile);
|
const page = await bootPopup(profile);
|
||||||
expect(page.pageErrors).toEqual([]);
|
expect(page.pageErrors).toEqual([]);
|
||||||
const background = require("../src/background/state");
|
const background = require("../src/background/state");
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ describe("only the user switches the network", () => {
|
|||||||
const after = bg.walletState();
|
const after = bg.walletState();
|
||||||
expect(after.networkId).toBe("sepolia");
|
expect(after.networkId).toBe("sepolia");
|
||||||
expect(after.rpcUrl).toBe(SEPOLIA.defaultRpcUrl);
|
expect(after.rpcUrl).toBe(SEPOLIA.defaultRpcUrl);
|
||||||
expect(after.wallets[0].addresses[0].balance).toBe("0");
|
expect(after.wallets[0].addresses[0].balance).toBeNull();
|
||||||
expect(bg.chainChangedEvents()).toEqual([
|
expect(bg.chainChangedEvents()).toEqual([
|
||||||
{
|
{
|
||||||
type: "AUTISTMASK_EVENT",
|
type: "AUTISTMASK_EVENT",
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function walletFixture() {
|
|||||||
{
|
{
|
||||||
name: "Wallet 1",
|
name: "Wallet 1",
|
||||||
type: "hd",
|
type: "hd",
|
||||||
addresses: [{ address: ADDRESS, balance: "0", tokenBalances: [] }],
|
addresses: [{ address: ADDRESS, balance: null, tokenBalances: [] }],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const USDC = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
|
|||||||
const EMPTY = { address: "0x1", balance: "0.0000", tokenBalances: [] };
|
const EMPTY = { address: "0x1", balance: "0.0000", tokenBalances: [] };
|
||||||
const ETH_ONLY = { address: "0x1", balance: "1.5", tokenBalances: [] };
|
const ETH_ONLY = { address: "0x1", balance: "1.5", tokenBalances: [] };
|
||||||
const DUST = { address: "0x1", balance: "0.00001", tokenBalances: [] };
|
const DUST = { address: "0x1", balance: "0.00001", tokenBalances: [] };
|
||||||
|
const NOT_READ = { address: "0x1", balance: null, tokenBalances: [] };
|
||||||
const TOKEN_ONLY = {
|
const TOKEN_ONLY = {
|
||||||
address: "0x1",
|
address: "0x1",
|
||||||
balance: "0.0000",
|
balance: "0.0000",
|
||||||
@@ -106,14 +107,23 @@ describe("whether an address holds anything", () => {
|
|||||||
expect(addressHoldsFunds(DUST)).toBe(true);
|
expect(addressHoldsFunds(DUST)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// No refresh has read it, so the address may hold funds.
|
||||||
|
test("an ETH balance no refresh has read counts", () => {
|
||||||
|
expect(addressHoldsFunds(NOT_READ)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
test("an address holding nothing does not", () => {
|
test("an address holding nothing does not", () => {
|
||||||
expect(addressHoldsFunds(EMPTY)).toBe(false);
|
expect(addressHoldsFunds(EMPTY)).toBe(false);
|
||||||
expect(addressHoldsFunds(ZERO_TOKEN)).toBe(false);
|
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(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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -140,6 +150,20 @@ describe("the balance warning on the removal confirmation", () => {
|
|||||||
expect(html).toContain("<span>0.0000</span>");
|
expect(html).toContain("<span>0.0000</span>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 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
|
// 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
|
// assert has been rounded: "This address holds 0.0000 ETH." is what the
|
||||||
// rounded form produces for an address that holds real money.
|
// rounded form produces for an address that holds real money.
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ test.each([
|
|||||||
await backgroundSwitch(page, "sepolia");
|
await backgroundSwitch(page, "sepolia");
|
||||||
|
|
||||||
expect(page.visibleViews()).toEqual(["main"]);
|
expect(page.visibleViews()).toEqual(["main"]);
|
||||||
expect(page.text("total-value")).toBe("0.0000 SepoliaETH");
|
expect(page.text("total-value")).toBe("unknown (SepoliaETH)");
|
||||||
});
|
});
|
||||||
|
|
||||||
// A request on Ethereum Mainnet fails, so Home's list holds the failure message.
|
// A request on Ethereum Mainnet fails, so Home's list holds the failure message.
|
||||||
|
|||||||
@@ -432,6 +432,27 @@ describe("Max on an ETH send", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test("says so when no refresh has read the ETH balance", async () => {
|
||||||
|
await refreshWith(BALANCE_WEI);
|
||||||
|
state.wallets[0].addresses[0].balance = null;
|
||||||
|
openSend();
|
||||||
|
await pressMax();
|
||||||
|
expect(el("send-amount").value).toBe("");
|
||||||
|
expect(text("flash-msg")).toBe("This token's balance is unknown.");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fills nothing in and says nothing when a network switch cleared the balance while the fee was estimated", async () => {
|
||||||
|
await refreshWith(BALANCE_WEI);
|
||||||
|
openSend();
|
||||||
|
const release = holdFeeEstimate();
|
||||||
|
const pressed = pressMax();
|
||||||
|
state.wallets[0].addresses[0].balance = null;
|
||||||
|
release();
|
||||||
|
await pressed;
|
||||||
|
expect(el("send-amount").value).toBe("");
|
||||||
|
expect(text("flash-msg")).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
test("fills nothing in and says nothing once Send was left while the fee was estimated", async () => {
|
test("fills nothing in and says nothing once Send was left while the fee was estimated", async () => {
|
||||||
// 0.0001 ETH, which does not cover the fee: a result that landed
|
// 0.0001 ETH, which does not cover the fee: a result that landed
|
||||||
// would say so on whichever screen is shown.
|
// would say so on whichever screen is shown.
|
||||||
|
|||||||
Reference in New Issue
Block a user