A balance refresh that returns now draws the balances on AddressDetail or AddressToken again when that screen is up, not only the wallet list. Their transaction lists are left alone. Every add path on AddToken and SettingsAddToken tracks a token through trackToken(), which gives the tracked record `balanceNotFetched: true`. While the field is set, the token's line reads `quantity unknown` instead of 0.0000, and Send reads its balance as unknown. refreshBalances() now resolves with the tracked tokens whose balance it read at every address. The popup's refresh deletes the field from those tokens in the stored record and leaves the rest of the stored list as it is. Removing the token removes the field, and each network keeps its own records. Model: opus-5-5
149 lines
6.1 KiB
JavaScript
149 lines
6.1 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 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 = state.trackedTokens.find(
|
|
(t) => t.address.toLowerCase() === contractAddr.toLowerCase(),
|
|
);
|
|
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;
|
|
}
|
|
// The balance list never shows a token that the spoof rule
|
|
// refuses, so such a token is not tracked.
|
|
if (isSpoofedSymbol(info.symbol, contractAddr)) {
|
|
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 };
|