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
164 lines
6.6 KiB
JavaScript
164 lines
6.6 KiB
JavaScript
const {
|
|
$,
|
|
showView,
|
|
showFlash,
|
|
escapeHtml,
|
|
goBack,
|
|
trackToken,
|
|
} = require("./helpers");
|
|
const { getTopTokens } = require("../../shared/tokenList");
|
|
const { state, saveState, currentNetwork } = require("../../shared/state");
|
|
const { lookupTokenInfo } = require("../../shared/balances");
|
|
const { isScamAddress } = require("../../shared/scamlist");
|
|
const { isSpoofedSymbol } = require("../../shared/symbolSpoof");
|
|
const { log } = require("../../shared/log");
|
|
|
|
// Counts the times AddToken has opened. A contract lookup started before
|
|
// 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 = "";
|
|
$("add-token-info").textContent = "";
|
|
$("add-token-info").style.visibility = "hidden";
|
|
// The bundled list holds Ethereum Mainnet contracts, which name no token
|
|
// on any other network.
|
|
$("add-token-common").classList.toggle(
|
|
"hidden",
|
|
state.networkId !== "mainnet",
|
|
);
|
|
const list = $("common-token-list");
|
|
list.innerHTML = getTopTokens(25)
|
|
.map(
|
|
(t) =>
|
|
`<button class="common-token border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer text-xs" data-address="${escapeHtml(t.address)}" data-symbol="${escapeHtml(t.symbol)}" data-decimals="${escapeHtml(t.decimals)}">${escapeHtml(t.symbol)}</button>`,
|
|
)
|
|
.join("");
|
|
list.querySelectorAll(".common-token").forEach((btn) => {
|
|
btn.addEventListener("click", () => {
|
|
$("add-token-address").value = btn.dataset.address;
|
|
});
|
|
});
|
|
showView("add-token");
|
|
}
|
|
|
|
function init(ctx) {
|
|
$("btn-add-token-confirm").addEventListener("click", async () => {
|
|
const contractAddr = $("add-token-address").value.trim();
|
|
if (!contractAddr || !contractAddr.startsWith("0x")) {
|
|
showFlash("Enter a valid contract address starting with 0x.");
|
|
return;
|
|
}
|
|
const already = findTracked(contractAddr);
|
|
if (already) {
|
|
showFlash(already.symbol + " is already being tracked.");
|
|
return;
|
|
}
|
|
if (isScamAddress(contractAddr)) {
|
|
showFlash("This address is on a known scam/fraud list.");
|
|
return;
|
|
}
|
|
const infoEl = $("add-token-info");
|
|
infoEl.textContent = "Looking up token...";
|
|
infoEl.style.visibility = "visible";
|
|
log.debugf("Looking up token contract", contractAddr);
|
|
// The network the lookup asks, which Settings may switch away from
|
|
// before it answers, as for a transaction in confirmTx.js.
|
|
const chainId = currentNetwork().chainId;
|
|
const opening = addTokenScreenOpenings;
|
|
try {
|
|
const info = await lookupTokenInfo(
|
|
contractAddr,
|
|
state.rpcUrl,
|
|
state.networkId,
|
|
ctx.pageClosed,
|
|
);
|
|
// A contract address names a token on one network only, so an
|
|
// answer after a switch is not added to the new network's list.
|
|
if (currentNetwork().chainId !== chainId) {
|
|
showFlash(
|
|
"The active network changed before the lookup finished," +
|
|
" so the token was not added.",
|
|
);
|
|
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)) {
|
|
showFlash("Not added: this token fakes a known symbol.");
|
|
if (addTokenScreenOpenings !== opening) return;
|
|
infoEl.textContent = "";
|
|
infoEl.style.visibility = "hidden";
|
|
return;
|
|
}
|
|
log.infof("Adding token", info.symbol, contractAddr);
|
|
trackToken({
|
|
address: contractAddr,
|
|
symbol: info.symbol,
|
|
decimals: info.decimals,
|
|
name: info.name,
|
|
});
|
|
await saveState();
|
|
ctx.doRefreshAndRender();
|
|
// If the lookup answers after AddToken was opened again, the
|
|
// token is still added, but the screen and the status line stay
|
|
// as they are.
|
|
if (addTokenScreenOpenings !== opening) return;
|
|
// The lookup is over, so a later Back onto AddToken shows no
|
|
// status.
|
|
infoEl.textContent = "";
|
|
infoEl.style.visibility = "hidden";
|
|
// If the lookup answers while another screen shows, the token is
|
|
// still added but the screen does not change.
|
|
if (state.currentView !== "add-token") return;
|
|
// Pop the stack (back to address detail) and re-render it
|
|
// so the newly added token is visible immediately.
|
|
if (state.viewStack.length > 0) {
|
|
state.viewStack.pop();
|
|
}
|
|
require("./addressDetail").show();
|
|
} catch (e) {
|
|
// Cancelled by the popup closing, not failed: see pageClosed in
|
|
// src/popup/index.js.
|
|
if (ctx.pageClosed.aborted) return;
|
|
const detail = e.shortMessage || e.message || String(e);
|
|
log.errorf("Adding token failed for", contractAddr, detail);
|
|
// lookupTokenInfo() rejects a contract with a one-line message
|
|
// starting "Not a valid ERC-20 token". Any other error, such as a
|
|
// failed save, can be far longer, so it is only logged.
|
|
showFlash(
|
|
detail.startsWith("Not a valid ERC-20 token")
|
|
? detail
|
|
: "Could not add the token.",
|
|
);
|
|
if (addTokenScreenOpenings !== opening) return;
|
|
infoEl.textContent = "";
|
|
infoEl.style.visibility = "hidden";
|
|
}
|
|
});
|
|
|
|
$("btn-add-token-back").addEventListener("click", () => {
|
|
goBack();
|
|
});
|
|
}
|
|
|
|
module.exports = { init, show };
|