fix: Home's list drops the previous network's rows after a network switch (closes #718)
A network switch left the previous network's rows or failure message in Home's list until the new network's answer landed. Home now replaces them with "Loading..." when it first draws on a new network. A redraw on the same network keeps its rows. Home, AddressDetail and AddressToken compared the page's network before reading storage. A switch made in Settings could change the page during that read, and the answer was still drawn. They now compare after the read, for an answer and for a failure. Model: opus-5-5
This commit was merged in pull request #738.
This commit is contained in:
@@ -558,7 +558,8 @@ three lines:
|
|||||||
A contract creation has no counterparty. Its last line reads "This transaction
|
A contract creation has no counterparty. Its last line reads "This transaction
|
||||||
creates a new contract. It has no recipient." While "Use Blockscout" is off, the
|
creates a new contract. It has no recipient." While "Use Blockscout" is off, the
|
||||||
list shows only the line "No transaction history is shown, because "Use
|
list shows only the line "No transaction history is shown, because "Use
|
||||||
Blockscout" is off in Settings."
|
Blockscout" is off in Settings." After a network switch, the list shows
|
||||||
|
"Loading..." in place of the previous network's rows or failure message.
|
||||||
|
|
||||||
Transitions:
|
Transitions:
|
||||||
|
|
||||||
|
|||||||
@@ -146,11 +146,12 @@ async function loadTransactions(address) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the network changed during the lookups above, or the screen now
|
// If the network changed during the lookups above, or the screen now
|
||||||
// shows another address, nothing is saved or drawn. See
|
// shows another address, nothing is saved or drawn. The page's network
|
||||||
// src/shared/storedSettings.js.
|
// is compared after the read of storage, because a switch made on this
|
||||||
|
// page can land during that read. See src/shared/storedSettings.js.
|
||||||
if (
|
if (
|
||||||
state.networkId !== networkId ||
|
|
||||||
!(await storedNetworkIs(networkId)) ||
|
!(await storedNetworkIs(networkId)) ||
|
||||||
|
state.networkId !== networkId ||
|
||||||
$("address-line").dataset.full !== address
|
$("address-line").dataset.full !== address
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -187,8 +188,8 @@ async function loadTransactions(address) {
|
|||||||
// If the network changed, or the screen now shows another address,
|
// If the network changed, or the screen now shows another address,
|
||||||
// the failure is not shown.
|
// the failure is not shown.
|
||||||
if (
|
if (
|
||||||
state.networkId !== networkId ||
|
|
||||||
!(await storedNetworkIs(networkId)) ||
|
!(await storedNetworkIs(networkId)) ||
|
||||||
|
state.networkId !== networkId ||
|
||||||
$("address-line").dataset.full !== address
|
$("address-line").dataset.full !== address
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -241,11 +241,13 @@ async function loadTransactions(address, tokenId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the network changed during the lookups above, or the screen now
|
// If the network changed during the lookups above, or the screen now
|
||||||
// shows another address or token, nothing is saved or drawn. See
|
// shows another address or token, nothing is saved or drawn. The page's
|
||||||
|
// network is compared after the read of storage, because a switch made
|
||||||
|
// on this page can land during that read. See
|
||||||
// src/shared/storedSettings.js.
|
// src/shared/storedSettings.js.
|
||||||
if (
|
if (
|
||||||
state.networkId !== networkId ||
|
|
||||||
!(await storedNetworkIs(networkId)) ||
|
!(await storedNetworkIs(networkId)) ||
|
||||||
|
state.networkId !== networkId ||
|
||||||
$("address-token-line").dataset.full !== address ||
|
$("address-token-line").dataset.full !== address ||
|
||||||
state.selectedToken !== tokenId
|
state.selectedToken !== tokenId
|
||||||
) {
|
) {
|
||||||
@@ -283,8 +285,8 @@ async function loadTransactions(address, tokenId) {
|
|||||||
// If the network changed, or the screen now shows another address or
|
// If the network changed, or the screen now shows another address or
|
||||||
// token, the failure is not shown.
|
// token, the failure is not shown.
|
||||||
if (
|
if (
|
||||||
state.networkId !== networkId ||
|
|
||||||
!(await storedNetworkIs(networkId)) ||
|
!(await storedNetworkIs(networkId)) ||
|
||||||
|
state.networkId !== networkId ||
|
||||||
$("address-token-line").dataset.full !== address ||
|
$("address-token-line").dataset.full !== address ||
|
||||||
state.selectedToken !== tokenId
|
state.selectedToken !== tokenId
|
||||||
) {
|
) {
|
||||||
|
|||||||
+25
-5
@@ -100,6 +100,9 @@ function renderActiveAddress() {
|
|||||||
|
|
||||||
let homeTxs = [];
|
let homeTxs = [];
|
||||||
|
|
||||||
|
// The network that `home-tx-list` holds rows or a message for.
|
||||||
|
let listNetworkId = null;
|
||||||
|
|
||||||
function renderHomeTxList(ctx) {
|
function renderHomeTxList(ctx) {
|
||||||
const list = $("home-tx-list");
|
const list = $("home-tx-list");
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
@@ -186,6 +189,19 @@ async function loadHomeTxs(ctx) {
|
|||||||
if (allAddresses.length === 0) return;
|
if (allAddresses.length === 0) return;
|
||||||
|
|
||||||
const networkId = state.networkId;
|
const networkId = state.networkId;
|
||||||
|
|
||||||
|
// After a network switch the list still holds the previous network's rows
|
||||||
|
// or failure message. They go here, because every way onto Home draws Home
|
||||||
|
// first. A redraw on the same network keeps its rows.
|
||||||
|
if (listNetworkId !== networkId) {
|
||||||
|
listNetworkId = networkId;
|
||||||
|
const list = $("home-tx-list");
|
||||||
|
if (list) {
|
||||||
|
list.innerHTML =
|
||||||
|
'<div class="text-muted text-xs py-1">Loading...</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const filters = {
|
const filters = {
|
||||||
hideSpoofedSymbols: state.hideSpoofedSymbols,
|
hideSpoofedSymbols: state.hideSpoofedSymbols,
|
||||||
hideLowHolderTokens: state.hideLowHolderTokens,
|
hideLowHolderTokens: state.hideLowHolderTokens,
|
||||||
@@ -206,10 +222,12 @@ async function loadHomeTxs(ctx) {
|
|||||||
const results = await Promise.all(fetches);
|
const results = await Promise.all(fetches);
|
||||||
|
|
||||||
// If the network changed during the lookups, nothing is saved or
|
// If the network changed during the lookups, nothing is saved or
|
||||||
// drawn. See src/shared/storedSettings.js.
|
// drawn. The page's network is compared after the read of storage,
|
||||||
|
// because a switch made on this page can land during that read. See
|
||||||
|
// src/shared/storedSettings.js.
|
||||||
if (
|
if (
|
||||||
state.networkId !== networkId ||
|
!(await storedNetworkIs(networkId)) ||
|
||||||
!(await storedNetworkIs(networkId))
|
state.networkId !== networkId
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -250,6 +268,7 @@ async function loadHomeTxs(ctx) {
|
|||||||
merged = filtered.transactions;
|
merged = filtered.transactions;
|
||||||
merged.sort((a, b) => b.blockNumber - a.blockNumber);
|
merged.sort((a, b) => b.blockNumber - a.blockNumber);
|
||||||
homeTxs = merged.slice(0, 25);
|
homeTxs = merged.slice(0, 25);
|
||||||
|
listNetworkId = networkId;
|
||||||
renderHomeTxList(ctx);
|
renderHomeTxList(ctx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Cancelled by the popup closing, not failed: see pageClosed in
|
// Cancelled by the popup closing, not failed: see pageClosed in
|
||||||
@@ -258,11 +277,12 @@ async function loadHomeTxs(ctx) {
|
|||||||
log.errorf("loadHomeTxs failed:", e.message);
|
log.errorf("loadHomeTxs failed:", e.message);
|
||||||
// If the network changed, the failure is not shown.
|
// If the network changed, the failure is not shown.
|
||||||
if (
|
if (
|
||||||
state.networkId !== networkId ||
|
!(await storedNetworkIs(networkId)) ||
|
||||||
!(await storedNetworkIs(networkId))
|
state.networkId !== networkId
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
listNetworkId = networkId;
|
||||||
const list = $("home-tx-list");
|
const list = $("home-tx-list");
|
||||||
if (list) {
|
if (list) {
|
||||||
list.innerHTML =
|
list.innerHTML =
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// Home, AddressDetail and AddressToken drop a transaction history answer that
|
// Home, AddressDetail and AddressToken drop a transaction history answer that
|
||||||
// lands after a network switch. A dropped answer draws no rows and saves no
|
// lands after a network switch. A dropped answer draws no rows and saves no
|
||||||
// fraud contracts (https://git.eeqj.de/sneak/AutistMask/issues/657).
|
// fraud contracts (https://git.eeqj.de/sneak/AutistMask/issues/657). Home's list
|
||||||
|
// shows nothing of the previous network once it is drawn after a switch
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/718).
|
||||||
//
|
//
|
||||||
// Most tests hold the history request open, switch the network or what the
|
// Most tests hold the history request open, switch the network or what the
|
||||||
// screen shows, and then let the answer land. The views run against a minimal
|
// screen shows, and then let the answer land. The views run against a minimal
|
||||||
@@ -154,6 +156,21 @@ function holdLock() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every read of storage waits until the returned function is called. Each read
|
||||||
|
// then answers with what storage holds at that moment, the oldest read first.
|
||||||
|
function holdReads() {
|
||||||
|
const read = storage.local.get;
|
||||||
|
const waiting = [];
|
||||||
|
storage.local.get = (key) =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
waiting.push(() => resolve(read(key)));
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
storage.local.get = read;
|
||||||
|
for (const answer of waiting) answer();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function makeElement(id) {
|
function makeElement(id) {
|
||||||
const el = {
|
const el = {
|
||||||
id,
|
id,
|
||||||
@@ -409,6 +426,123 @@ describe.each([HOME, ADDRESS_DETAIL, ADDRESS_TOKEN])("$name", (screen) => {
|
|||||||
expect(drawn).toContain("tx-row");
|
expect(drawn).toContain("tx-row");
|
||||||
expect(node(screen.listId).innerHTML).toBe(drawn);
|
expect(node(screen.listId).innerHTML).toBe(drawn);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The user picks Sepolia in Settings, and the switch reads storage. The
|
||||||
|
// answer lands during that read, so the screen's own read of storage still
|
||||||
|
// finds Ethereum Mainnet. The switch changes the page before the screen's
|
||||||
|
// read ends. The answer is the transfer of ETH alone, so nothing is saved,
|
||||||
|
// and only a check after the screen's read can drop the answer.
|
||||||
|
test("an answer that lands during the first read of a switch made in Settings adds nothing", async () => {
|
||||||
|
const land = holdHistory(history().slice(0, 1));
|
||||||
|
screen.open();
|
||||||
|
await settle();
|
||||||
|
const before = node(screen.listId).innerHTML;
|
||||||
|
const answerReads = holdReads();
|
||||||
|
const switching = onChainSwitch("sepolia");
|
||||||
|
await settle();
|
||||||
|
land();
|
||||||
|
await settle();
|
||||||
|
answerReads();
|
||||||
|
await switching;
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
expectNothingAdded(screen, before);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a request that fails during the first read of a switch made in Settings shows no message", async () => {
|
||||||
|
const fail = holdFailure();
|
||||||
|
screen.open();
|
||||||
|
await settle();
|
||||||
|
const before = node(screen.listId).innerHTML;
|
||||||
|
const answerReads = holdReads();
|
||||||
|
const switching = onChainSwitch("sepolia");
|
||||||
|
await settle();
|
||||||
|
fail();
|
||||||
|
await settle();
|
||||||
|
answerReads();
|
||||||
|
await switching;
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
expect(node(screen.listId).innerHTML).toBe(before);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Home's list", () => {
|
||||||
|
test.each([
|
||||||
|
{ what: "rows", hold: holdHistory, shown: "tx-row" },
|
||||||
|
{
|
||||||
|
what: "failure message",
|
||||||
|
hold: holdFailure,
|
||||||
|
shown: "Failed to load transactions.",
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"shows the previous network's $what neither before nor after the new network's answer lands",
|
||||||
|
async ({ hold, shown }) => {
|
||||||
|
const answer = hold();
|
||||||
|
HOME.open();
|
||||||
|
answer();
|
||||||
|
await settle();
|
||||||
|
expect(node(HOME.listId).innerHTML).toContain(shown);
|
||||||
|
|
||||||
|
await onChainSwitch("sepolia");
|
||||||
|
const land = holdHistory([]);
|
||||||
|
HOME.open();
|
||||||
|
await settle();
|
||||||
|
expect(node(HOME.listId).innerHTML).toContain("Loading...");
|
||||||
|
|
||||||
|
land();
|
||||||
|
await settle();
|
||||||
|
expect(node(HOME.listId).innerHTML).toContain(
|
||||||
|
"No transactions found.",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// The user leaves Ethereum Mainnet with a request waiting, comes back, and
|
||||||
|
// that request lands. The list then holds Ethereum Mainnet's answer, so the
|
||||||
|
// next draw on Sepolia must replace it.
|
||||||
|
test.each([
|
||||||
|
{ what: "rows", hold: holdHistory },
|
||||||
|
{ what: "failure message", hold: holdFailure },
|
||||||
|
])(
|
||||||
|
"shows no $what of an older request that lands after a switch back",
|
||||||
|
async ({ hold }) => {
|
||||||
|
const answer = hold();
|
||||||
|
HOME.open();
|
||||||
|
|
||||||
|
await onChainSwitch("sepolia");
|
||||||
|
holdHistory();
|
||||||
|
HOME.open();
|
||||||
|
|
||||||
|
await onChainSwitch("mainnet");
|
||||||
|
answer();
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
await onChainSwitch("sepolia");
|
||||||
|
holdHistory();
|
||||||
|
HOME.open();
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
const list = node(HOME.listId).innerHTML;
|
||||||
|
expect(list).toContain("Loading...");
|
||||||
|
expect(list).not.toContain("tx-row");
|
||||||
|
expect(list).not.toContain("Failed to load transactions.");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Each balance refresh draws Home again, so the rows must not blink.
|
||||||
|
test("keeps its rows while a redraw on the same network waits for the answer", async () => {
|
||||||
|
fetchRecentTransactions.mockImplementationOnce(async () => history());
|
||||||
|
HOME.open();
|
||||||
|
await settle();
|
||||||
|
const drawn = node(HOME.listId).innerHTML;
|
||||||
|
holdHistory();
|
||||||
|
HOME.open();
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
expect(drawn).toContain("tx-row");
|
||||||
|
expect(node(HOME.listId).innerHTML).toBe(drawn);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.each([ADDRESS_DETAIL, ADDRESS_TOKEN])("$name", (screen) => {
|
describe.each([ADDRESS_DETAIL, ADDRESS_TOKEN])("$name", (screen) => {
|
||||||
|
|||||||
@@ -137,6 +137,60 @@ test.each([
|
|||||||
expect(page.text("total-value")).toBe("0.0000 SepoliaETH");
|
expect(page.text("total-value")).toBe("0.0000 SepoliaETH");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// A request on Ethereum Mainnet fails, so Home's list holds the failure message.
|
||||||
|
// The list shows it neither before nor after Sepolia's answer lands.
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
how: "saved by the background",
|
||||||
|
switchToSepolia: (page) => backgroundSwitch(page, "sepolia"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
how: "made in Settings",
|
||||||
|
switchToSepolia: async (page) => {
|
||||||
|
await page.click("btn-settings");
|
||||||
|
const select = page.node("settings-network");
|
||||||
|
select.value = "sepolia";
|
||||||
|
for (const fn of select.listeners.change) await fn();
|
||||||
|
await page.settle();
|
||||||
|
await page.click("btn-settings-back");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"Home's list drops the previous network's message after a switch $how",
|
||||||
|
async ({ switchToSepolia }) => {
|
||||||
|
const page = await bootPopup(
|
||||||
|
unversionedValidProfile({ useBlockscout: true }),
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
fetchRecentTransactions,
|
||||||
|
} = require("../src/shared/transactions");
|
||||||
|
fetchRecentTransactions.mockRejectedValueOnce(
|
||||||
|
new Error("Blockscout is down"),
|
||||||
|
);
|
||||||
|
await page.tick();
|
||||||
|
expect(page.node("home-tx-list").innerHTML).toContain(
|
||||||
|
"Failed to load transactions.",
|
||||||
|
);
|
||||||
|
|
||||||
|
let land;
|
||||||
|
fetchRecentTransactions.mockImplementationOnce(
|
||||||
|
() =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
land = () => resolve([]);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await switchToSepolia(page);
|
||||||
|
expect(page.visibleViews()).toEqual(["main"]);
|
||||||
|
expect(page.node("home-tx-list").innerHTML).toContain("Loading...");
|
||||||
|
|
||||||
|
land();
|
||||||
|
await page.settle();
|
||||||
|
expect(page.node("home-tx-list").innerHTML).toContain(
|
||||||
|
"No transactions found.",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// The report of the switch's own save names the network the page already has.
|
// The report of the switch's own save names the network the page already has.
|
||||||
test("a switch in Settings is not taken a second time when its save is reported", async () => {
|
test("a switch in Settings is not taken a second time when its save is reported", async () => {
|
||||||
const page = await bootOnAddress();
|
const page = await bootOnAddress();
|
||||||
|
|||||||
Reference in New Issue
Block a user