diff --git a/src/popup/index.html b/src/popup/index.html index 6cb7923..653093b 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -713,6 +713,21 @@ +
+

Tracked Tokens

+

+ ERC-20 tokens whose balances are tracked across all + addresses. +

+
+ +
+

Display

+ + + `; + }); + container.innerHTML = html; + container.querySelectorAll(".btn-remove-token").forEach((btn) => { + btn.addEventListener("click", async () => { + const idx = parseInt(btn.dataset.idx, 10); + state.trackedTokens.splice(idx, 1); + await saveState(); + renderTrackedTokens(); + }); + }); +} + function show() { $("settings-rpc").value = state.rpcUrl; $("settings-blockscout").value = state.blockscoutUrl; + renderTrackedTokens(); renderSiteLists(); showView("settings"); } @@ -155,6 +183,11 @@ function init(ctx) { $("btn-main-add-wallet").addEventListener("click", ctx.showAddWalletView); + $("btn-settings-add-token").addEventListener( + "click", + ctx.showSettingsAddTokenView, + ); + $("btn-settings-back").addEventListener("click", () => { ctx.renderWalletList(); showView("main"); diff --git a/src/popup/views/settingsAddToken.js b/src/popup/views/settingsAddToken.js new file mode 100644 index 0000000..8829dd0 --- /dev/null +++ b/src/popup/views/settingsAddToken.js @@ -0,0 +1,159 @@ +const { $, showView, showFlash } = 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 tokenLabel(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 ( + `` + ); + }) + .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 = ''; + for (const t of tokens) { + const tracked = isTracked(t.address); + const label = tokenLabel(t) + (tracked ? " (tracked)" : ""); + html += + ``; + } + sel.innerHTML = html; +} + +function show() { + $("settings-addtoken-address").value = ""; + $("settings-addtoken-info").classList.add("hidden"); + renderTop10(); + renderDropdown(); + showView("settings-addtoken"); +} + +function init(_ctx) { + ctx = _ctx; + + $("btn-settings-addtoken-back").addEventListener("click", () => { + ctx.showSettingsView(); + }); + + $("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( + "Please 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.classList.remove("hidden"); + log.debugf("Looking up token contract", addr); + try { + const info = await lookupTokenInfo(addr, state.rpcUrl); + 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.classList.add("hidden"); + renderTop10(); + renderDropdown(); + ctx.doRefreshAndRender(); + } catch (e) { + const detail = e.shortMessage || e.message || String(e); + log.errorf("Token lookup failed for", addr, detail); + showFlash(detail); + infoEl.classList.add("hidden"); + } + }); +} + +module.exports = { init, show };