AddressDetail and AddressToken defined their own isoDate() and timeAgo(), hiding the shared pair in helpers.js, so a fix there would not have reached them. The copies were identical and are deleted; blockieHtml() and tokenLabel(), each defined twice, move to helpers.js. A new test shows the history rows and the transaction detail view write the time with the shared pair. Deleted as never called: explorerLink(), ETHEREUM_SEPOLIA_CHAIN_ID, getWalletValue() and getTotalValue() with their tests. Home's "Total:" is the active address's total, as README.md already says. addressColor() and etherscanAddressUrl() are no longer exported. Model: opus-5-5
172 lines
6.1 KiB
JavaScript
172 lines
6.1 KiB
JavaScript
const { $, showView, showFlash, escapeHtml, goBack } = require("./helpers");
|
|
const { getTopTokens } = require("../../shared/tokenList");
|
|
const { state, saveState } = require("../../shared/state");
|
|
const { lookupTokenInfo } = require("../../shared/balances");
|
|
const { isScamAddress } = require("../../shared/scamlist");
|
|
const { log } = require("../../shared/log");
|
|
|
|
let ctx;
|
|
|
|
function isTracked(address) {
|
|
const lower = address.toLowerCase();
|
|
return state.trackedTokens.some((t) => t.address.toLowerCase() === lower);
|
|
}
|
|
|
|
function nameAndSymbol(t) {
|
|
return t.name ? t.name + " (" + t.symbol + ")" : t.symbol;
|
|
}
|
|
|
|
function renderTop10() {
|
|
const el = $("settings-addtoken-top10");
|
|
el.innerHTML = getTopTokens(10)
|
|
.map((t) => {
|
|
const tracked = isTracked(t.address);
|
|
const cls = tracked
|
|
? "border border-border px-1 text-xs opacity-40 cursor-default"
|
|
: "border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer text-xs";
|
|
return (
|
|
`<button class="settings-addtoken-quick ${cls}"` +
|
|
` data-address="${escapeHtml(t.address)}"` +
|
|
` data-symbol="${escapeHtml(t.symbol)}"` +
|
|
` data-decimals="${escapeHtml(t.decimals)}"` +
|
|
` data-name="${escapeHtml(t.name || "")}"` +
|
|
`${tracked ? " disabled" : ""}>${escapeHtml(t.symbol)}</button>`
|
|
);
|
|
})
|
|
.join("");
|
|
el.querySelectorAll(".settings-addtoken-quick:not([disabled])").forEach(
|
|
(btn) => {
|
|
btn.addEventListener("click", async () => {
|
|
const token = {
|
|
address: btn.dataset.address,
|
|
symbol: btn.dataset.symbol,
|
|
decimals: parseInt(btn.dataset.decimals, 10),
|
|
name: btn.dataset.name || btn.dataset.symbol,
|
|
};
|
|
state.trackedTokens.push(token);
|
|
await saveState();
|
|
showFlash("Added " + token.symbol);
|
|
renderTop10();
|
|
renderDropdown();
|
|
ctx.doRefreshAndRender();
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
function renderDropdown() {
|
|
const sel = $("settings-addtoken-select");
|
|
const tokens = getTopTokens(100);
|
|
let html = '<option value="">-- select --</option>';
|
|
for (const t of tokens) {
|
|
const tracked = isTracked(t.address);
|
|
const label = nameAndSymbol(t) + (tracked ? " (tracked)" : "");
|
|
html +=
|
|
`<option value="${escapeHtml(t.address)}"` +
|
|
` data-symbol="${escapeHtml(t.symbol)}"` +
|
|
` data-decimals="${escapeHtml(t.decimals)}"` +
|
|
` data-name="${escapeHtml(t.name || "")}"` +
|
|
`${tracked ? " disabled" : ""}>${escapeHtml(label)}</option>`;
|
|
}
|
|
sel.innerHTML = html;
|
|
}
|
|
|
|
function show() {
|
|
$("settings-addtoken-address").value = "";
|
|
$("settings-addtoken-info").textContent = "";
|
|
$("settings-addtoken-info").style.visibility = "hidden";
|
|
renderTop10();
|
|
renderDropdown();
|
|
showView("settings-addtoken");
|
|
}
|
|
|
|
function init(_ctx) {
|
|
ctx = _ctx;
|
|
|
|
$("btn-settings-addtoken-back").addEventListener("click", () => {
|
|
goBack();
|
|
});
|
|
|
|
$("btn-settings-addtoken-select").addEventListener("click", async () => {
|
|
const sel = $("settings-addtoken-select");
|
|
const opt = sel.options[sel.selectedIndex];
|
|
if (!opt || !opt.value) {
|
|
showFlash("Please select a token.");
|
|
return;
|
|
}
|
|
if (isTracked(opt.value)) {
|
|
showFlash("Already tracked.");
|
|
return;
|
|
}
|
|
const token = {
|
|
address: opt.value,
|
|
symbol: opt.dataset.symbol,
|
|
decimals: parseInt(opt.dataset.decimals, 10),
|
|
name: opt.dataset.name || opt.dataset.symbol,
|
|
};
|
|
state.trackedTokens.push(token);
|
|
await saveState();
|
|
showFlash("Added " + token.symbol);
|
|
renderTop10();
|
|
renderDropdown();
|
|
ctx.doRefreshAndRender();
|
|
});
|
|
|
|
$("btn-settings-addtoken-manual").addEventListener("click", async () => {
|
|
const addr = $("settings-addtoken-address").value.trim();
|
|
if (!addr || !addr.startsWith("0x")) {
|
|
showFlash("Enter a valid contract address starting with 0x.");
|
|
return;
|
|
}
|
|
if (isTracked(addr)) {
|
|
showFlash("Already tracked.");
|
|
return;
|
|
}
|
|
if (isScamAddress(addr)) {
|
|
showFlash("This address is on a known scam/fraud list.");
|
|
return;
|
|
}
|
|
const infoEl = $("settings-addtoken-info");
|
|
infoEl.textContent = "Looking up token...";
|
|
infoEl.style.visibility = "visible";
|
|
log.debugf("Looking up token contract", addr);
|
|
try {
|
|
const info = await lookupTokenInfo(
|
|
addr,
|
|
state.rpcUrl,
|
|
state.networkId,
|
|
);
|
|
log.infof("Adding token", info.symbol, addr);
|
|
state.trackedTokens.push({
|
|
address: addr,
|
|
symbol: info.symbol,
|
|
decimals: info.decimals,
|
|
name: info.name,
|
|
});
|
|
await saveState();
|
|
showFlash("Added " + info.symbol);
|
|
$("settings-addtoken-address").value = "";
|
|
infoEl.textContent = "";
|
|
infoEl.style.visibility = "hidden";
|
|
renderTop10();
|
|
renderDropdown();
|
|
ctx.doRefreshAndRender();
|
|
} catch (e) {
|
|
const detail = e.shortMessage || e.message || String(e);
|
|
log.errorf("Adding token failed for", addr, 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.",
|
|
);
|
|
infoEl.textContent = "";
|
|
infoEl.style.visibility = "hidden";
|
|
}
|
|
});
|
|
}
|
|
|
|
module.exports = { init, show };
|