bundledToken(address, chainId) in tokenList.js answers only for Ethereum Mainnet's chain id. Every reader of TOKEN_BY_ADDRESS now goes through it, with the chain id of the transaction, balance or history entry it draws. The token sources the screens build carry that chain id. The Uniswap decoder sets the transaction's chain id on them. fetchTokenBalances() takes the chain id of the network it asks about. isSpoofedSymbol() takes the chain id of the token's network. It checks the bundled list's symbols only on Ethereum Mainnet. The native asset's labels stay reserved on every network. Twenty existing tests and four shared test helpers read the bundled list without naming a network. Each now names Ethereum Mainnet. 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, 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 };
|