fix: a second click on Add while the lookup waits tracks the token once (closes #727)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

The "already tracked" check ran only before the contract lookup, so a second click while the first lookup waited tracked the token twice. AddToken and SettingsAddToken now check again when the lookup answers. An answer for a token tracked meanwhile shows the "already tracked" flash message and tracks nothing.

Model: opus-5-5
This commit was merged in pull request #728.
This commit is contained in:
2026-11-03 06:24:14 +01:00
parent 1a4fb0803d
commit 267b69382e
3 changed files with 109 additions and 9 deletions
+18 -3
View File
@@ -17,6 +17,11 @@ const { log } = require("../../shared/log");
// AddToken was last opened changes neither the screen nor the status line.
let addTokenScreenOpenings = 0;
function findTracked(address) {
const lower = address.toLowerCase();
return state.trackedTokens.find((t) => t.address.toLowerCase() === lower);
}
function show() {
addTokenScreenOpenings++;
$("add-token-address").value = "";
@@ -50,9 +55,7 @@ function init(ctx) {
showFlash("Enter a valid contract address starting with 0x.");
return;
}
const already = state.trackedTokens.find(
(t) => t.address.toLowerCase() === contractAddr.toLowerCase(),
);
const already = findTracked(contractAddr);
if (already) {
showFlash(already.symbol + " is already being tracked.");
return;
@@ -85,6 +88,18 @@ function init(ctx) {
);
return;
}
// Another click on Add may have tracked this token while this
// lookup waited.
const trackedMeanwhile = findTracked(contractAddr);
if (trackedMeanwhile) {
showFlash(
trackedMeanwhile.symbol + " is already being tracked.",
);
if (addTokenScreenOpenings !== opening) return;
infoEl.textContent = "";
infoEl.style.visibility = "hidden";
return;
}
// The balance list never shows a token that the spoof rule
// refuses, so such a token is not tracked.
if (isSpoofedSymbol(info.symbol, contractAddr, chainId)) {
+8
View File
@@ -163,6 +163,14 @@ function init(_ctx) {
);
return;
}
// Another click on Add, or a "Top tokens:" button, may have
// tracked this token while this lookup waited.
if (isTracked(addr)) {
showFlash("Already tracked.");
infoEl.textContent = "";
infoEl.style.visibility = "hidden";
return;
}
// The balance list never shows a token that the spoof rule
// refuses, so such a token is not tracked. The rule never refuses
// a bundled token, so the "Top tokens:" buttons and the top 100
+83 -6
View File
@@ -1,8 +1,10 @@
// A contract lookup on AddToken can answer after the user has left AddToken.
// The token is still added, and the screen the user is on does not change
// (https://git.eeqj.de/sneak/AutistMask/issues/603). An answer while AddToken
// still shows goes to AddressDetail. Driven through the real popup over a
// stored profile.
// still shows goes to AddressDetail. On either add-token screen, an answer for a
// token tracked while the lookup waited tracks nothing
// (https://git.eeqj.de/sneak/AutistMask/issues/727). Driven through the real
// popup over a stored profile.
const {
bootPopup,
@@ -43,9 +45,9 @@ async function openAddToken() {
return boot;
}
// Presses Add with a contract lookup that answers only when the function
// returned here is called. That function waits for Add to finish.
function addWithPendingLookup(boot) {
// Presses `button` with a contract lookup that answers only when the function
// returned here is called. That function waits for the add to finish.
function addWithPendingLookup(boot, button = "btn-add-token-confirm") {
const { lookupTokenInfo } = require("../src/shared/balances");
let answer;
lookupTokenInfo.mockImplementationOnce(
@@ -54,7 +56,7 @@ function addWithPendingLookup(boot) {
answer = resolve;
}),
);
const adding = boot.click("btn-add-token-confirm");
const adding = boot.click(button);
return async () => {
answer({
symbol: TOKEN.symbol,
@@ -136,3 +138,78 @@ test("an answer while AddToken still shows goes to AddressDetail with the token
expect(boot.node("address-balances").innerHTML).toContain("TST");
expect(boot.storage.read("autistmask").trackedTokens).toEqual([TOKEN]);
});
test("an answer for a token tracked after AddToken was opened again leaves the status line as it is", async () => {
const boot = await openAddToken();
const answerFirstLookup = addWithPendingLookup(boot);
await boot.click("btn-add-token-back");
await boot.click("btn-add-token");
boot.node("add-token-address").value = OTHER_TOKEN_ADDRESS;
// The second lookup is never answered.
addWithPendingLookup(boot);
require("../src/popup/views/helpers").trackToken(TOKEN);
await answerFirstLookup();
expect(boot.text("flash-msg")).toBe("TST is already being tracked.");
expect(boot.text("add-token-info")).toBe("Looking up token...");
});
// Each screen is opened from AddressDetail by the buttons named, in order.
describe.each([
[
"AddToken",
["btn-add-token"],
"add-token-address",
"btn-add-token-confirm",
"add-token-info",
"TST is already being tracked.",
],
[
"SettingsAddToken",
["btn-settings", "btn-settings-add-token"],
"settings-addtoken-address",
"btn-settings-addtoken-manual",
"settings-addtoken-info",
"Already tracked.",
],
])("%s", (_, opening, field, button, statusLine, alreadyTrackedFlash) => {
// The screen open with TOKEN's contract address typed in.
async function openScreen() {
const boot = await bootPopup(
unversionedValidProfile({
currentView: "address",
selectedWallet: 0,
selectedAddress: 0,
}),
);
expect(boot.pageErrors).toEqual([]);
for (const id of opening) await boot.click(id);
boot.node(field).value = TOKEN.address;
return boot;
}
test("a second click on Add while the lookup waits tracks the token once", async () => {
const boot = await openScreen();
const answerFirstClick = addWithPendingLookup(boot, button);
const answerSecondClick = addWithPendingLookup(boot, button);
await answerFirstClick();
await answerSecondClick();
expect(boot.text("flash-msg")).toBe(alreadyTrackedFlash);
expect(boot.storage.read("autistmask").trackedTokens).toEqual([TOKEN]);
});
test("an answer for a token tracked while the lookup waited clears the status line", async () => {
const boot = await openScreen();
const answerLookup = addWithPendingLookup(boot, button);
expect(boot.text(statusLine)).toBe("Looking up token...");
require("../src/popup/views/helpers").trackToken(TOKEN);
await answerLookup();
expect(boot.text("flash-msg")).toBe(alreadyTrackedFlash);
expect(boot.text(statusLine)).toBe("");
expect(boot.node(statusLine).style.visibility).toBe("hidden");
const { state } = require("../src/shared/state");
expect(state.trackedTokens).toEqual([TOKEN]);
});
});