Merge pull request 'add tracked token list management to settings' (#5) from feature/settings-token-management into main
All checks were successful
check / check (push) Successful in 9s
All checks were successful
check / check (push) Successful in 9s
Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
@@ -713,6 +713,21 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-well p-3 mx-1 mb-3">
|
||||||
|
<h3 class="font-bold mb-1">Tracked Tokens</h3>
|
||||||
|
<p class="text-xs text-muted mb-2">
|
||||||
|
ERC-20 tokens whose balances are tracked across all
|
||||||
|
addresses.
|
||||||
|
</p>
|
||||||
|
<div id="settings-tracked-tokens"></div>
|
||||||
|
<button
|
||||||
|
id="btn-settings-add-token"
|
||||||
|
class="border border-border px-2 py-1 mt-2 hover:bg-fg hover:text-bg cursor-pointer"
|
||||||
|
>
|
||||||
|
+ Add token
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="bg-well p-3 mx-1 mb-3">
|
<div class="bg-well p-3 mx-1 mb-3">
|
||||||
<h3 class="font-bold mb-1">Display</h3>
|
<h3 class="font-bold mb-1">Display</h3>
|
||||||
<label
|
<label
|
||||||
@@ -824,6 +839,73 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ SETTINGS: ADD TOKEN ============ -->
|
||||||
|
<div id="view-settings-addtoken" class="view hidden">
|
||||||
|
<button
|
||||||
|
id="btn-settings-addtoken-back"
|
||||||
|
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mb-2"
|
||||||
|
>
|
||||||
|
< Back
|
||||||
|
</button>
|
||||||
|
<h2 class="font-bold mb-2">Add Token</h2>
|
||||||
|
<p class="text-xs text-muted mb-3">
|
||||||
|
Pick a common token or enter a contract address manually.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- top 10 quick-pick buttons -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="block mb-1 text-xs text-muted"
|
||||||
|
>Top tokens:</label
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id="settings-addtoken-top10"
|
||||||
|
class="flex flex-wrap gap-1"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- top 100 dropdown -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="block mb-1 text-xs text-muted"
|
||||||
|
>Or pick from top 100:</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
id="settings-addtoken-select"
|
||||||
|
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
||||||
|
>
|
||||||
|
<option value="">-- select --</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
id="btn-settings-addtoken-select"
|
||||||
|
class="border border-border px-2 py-1 mt-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||||
|
>
|
||||||
|
Add selected
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- manual contract address -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="block mb-1 text-xs text-muted"
|
||||||
|
>Or enter contract address:</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="settings-addtoken-address"
|
||||||
|
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
||||||
|
placeholder="0x..."
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
id="settings-addtoken-info"
|
||||||
|
class="text-xs text-muted mt-1 hidden"
|
||||||
|
></div>
|
||||||
|
<button
|
||||||
|
id="btn-settings-addtoken-manual"
|
||||||
|
class="border border-border px-2 py-1 mt-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ============ TRANSACTION DETAIL ============ -->
|
<!-- ============ TRANSACTION DETAIL ============ -->
|
||||||
<div id="view-transaction" class="view hidden">
|
<div id="view-transaction" class="view hidden">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const transactionDetail = require("./views/transactionDetail");
|
|||||||
const receive = require("./views/receive");
|
const receive = require("./views/receive");
|
||||||
const addToken = require("./views/addToken");
|
const addToken = require("./views/addToken");
|
||||||
const settings = require("./views/settings");
|
const settings = require("./views/settings");
|
||||||
|
const settingsAddToken = require("./views/settingsAddToken");
|
||||||
const approval = require("./views/approval");
|
const approval = require("./views/approval");
|
||||||
|
|
||||||
function renderWalletList() {
|
function renderWalletList() {
|
||||||
@@ -60,6 +61,8 @@ const ctx = {
|
|||||||
showConfirmTx: (txInfo) => confirmTx.show(txInfo),
|
showConfirmTx: (txInfo) => confirmTx.show(txInfo),
|
||||||
showReceive: () => receive.show(),
|
showReceive: () => receive.show(),
|
||||||
showTransactionDetail: (tx) => transactionDetail.show(tx),
|
showTransactionDetail: (tx) => transactionDetail.show(tx),
|
||||||
|
showSettingsView: () => settings.show(),
|
||||||
|
showSettingsAddTokenView: () => settingsAddToken.show(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Views that can be fully re-rendered from persisted state.
|
// Views that can be fully re-rendered from persisted state.
|
||||||
@@ -70,6 +73,7 @@ const RESTORABLE_VIEWS = new Set([
|
|||||||
"address-token",
|
"address-token",
|
||||||
"receive",
|
"receive",
|
||||||
"settings",
|
"settings",
|
||||||
|
"settings-addtoken",
|
||||||
"transaction",
|
"transaction",
|
||||||
"success-tx",
|
"success-tx",
|
||||||
"error-tx",
|
"error-tx",
|
||||||
@@ -120,6 +124,9 @@ function restoreView() {
|
|||||||
case "settings":
|
case "settings":
|
||||||
settings.show();
|
settings.show();
|
||||||
break;
|
break;
|
||||||
|
case "settings-addtoken":
|
||||||
|
settingsAddToken.show();
|
||||||
|
break;
|
||||||
case "transaction":
|
case "transaction":
|
||||||
if (state.viewData && state.viewData.tx) {
|
if (state.viewData && state.viewData.tx) {
|
||||||
transactionDetail.render();
|
transactionDetail.render();
|
||||||
@@ -212,6 +219,7 @@ async function init() {
|
|||||||
receive.init(ctx);
|
receive.init(ctx);
|
||||||
addToken.init(ctx);
|
addToken.init(ctx);
|
||||||
settings.init(ctx);
|
settings.init(ctx);
|
||||||
|
settingsAddToken.init(ctx);
|
||||||
|
|
||||||
if (!state.hasWallet) {
|
if (!state.hasWallet) {
|
||||||
showView("welcome");
|
showView("welcome");
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ const {
|
|||||||
} = require("../../shared/prices");
|
} = require("../../shared/prices");
|
||||||
const { state, saveState } = require("../../shared/state");
|
const { state, saveState } = require("../../shared/state");
|
||||||
|
|
||||||
|
// When views are added, removed, or transitions between them change,
|
||||||
|
// update the view-navigation documentation in README.md to match.
|
||||||
const VIEWS = [
|
const VIEWS = [
|
||||||
"welcome",
|
"welcome",
|
||||||
"add-wallet",
|
"add-wallet",
|
||||||
@@ -23,6 +25,7 @@ const VIEWS = [
|
|||||||
"receive",
|
"receive",
|
||||||
"add-token",
|
"add-token",
|
||||||
"settings",
|
"settings",
|
||||||
|
"settings-addtoken",
|
||||||
"transaction",
|
"transaction",
|
||||||
"approve-site",
|
"approve-site",
|
||||||
"approve-tx",
|
"approve-tx",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const { $, showView, showFlash } = require("./helpers");
|
const { $, showView, showFlash, escapeHtml } = require("./helpers");
|
||||||
const { state, saveState } = require("../../shared/state");
|
const { state, saveState } = require("../../shared/state");
|
||||||
const { ETHEREUM_MAINNET_CHAIN_ID } = require("../../shared/constants");
|
const { ETHEREUM_MAINNET_CHAIN_ID } = require("../../shared/constants");
|
||||||
const { log, debugFetch } = require("../../shared/log");
|
const { log, debugFetch } = require("../../shared/log");
|
||||||
@@ -38,9 +38,37 @@ function renderSiteList(containerId, siteMap, stateKey) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderTrackedTokens() {
|
||||||
|
const container = $("settings-tracked-tokens");
|
||||||
|
if (state.trackedTokens.length === 0) {
|
||||||
|
container.innerHTML = '<p class="text-xs text-muted">None</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let html = "";
|
||||||
|
state.trackedTokens.forEach((token, idx) => {
|
||||||
|
const label = token.name
|
||||||
|
? escapeHtml(token.name) + " (" + escapeHtml(token.symbol) + ")"
|
||||||
|
: escapeHtml(token.symbol);
|
||||||
|
html += `<div class="flex justify-between items-center text-xs py-1 border-b border-border-light">`;
|
||||||
|
html += `<span>${label}</span>`;
|
||||||
|
html += `<button class="btn-remove-token border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer" data-idx="${idx}">[x]</button>`;
|
||||||
|
html += `</div>`;
|
||||||
|
});
|
||||||
|
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() {
|
function show() {
|
||||||
$("settings-rpc").value = state.rpcUrl;
|
$("settings-rpc").value = state.rpcUrl;
|
||||||
$("settings-blockscout").value = state.blockscoutUrl;
|
$("settings-blockscout").value = state.blockscoutUrl;
|
||||||
|
renderTrackedTokens();
|
||||||
renderSiteLists();
|
renderSiteLists();
|
||||||
showView("settings");
|
showView("settings");
|
||||||
}
|
}
|
||||||
@@ -155,6 +183,11 @@ function init(ctx) {
|
|||||||
|
|
||||||
$("btn-main-add-wallet").addEventListener("click", ctx.showAddWalletView);
|
$("btn-main-add-wallet").addEventListener("click", ctx.showAddWalletView);
|
||||||
|
|
||||||
|
$("btn-settings-add-token").addEventListener(
|
||||||
|
"click",
|
||||||
|
ctx.showSettingsAddTokenView,
|
||||||
|
);
|
||||||
|
|
||||||
$("btn-settings-back").addEventListener("click", () => {
|
$("btn-settings-back").addEventListener("click", () => {
|
||||||
ctx.renderWalletList();
|
ctx.renderWalletList();
|
||||||
showView("main");
|
showView("main");
|
||||||
|
|||||||
159
src/popup/views/settingsAddToken.js
Normal file
159
src/popup/views/settingsAddToken.js
Normal file
@@ -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 (
|
||||||
|
`<button class="settings-addtoken-quick ${cls}"` +
|
||||||
|
` data-address="${t.address}"` +
|
||||||
|
` data-symbol="${t.symbol}"` +
|
||||||
|
` data-decimals="${t.decimals}"` +
|
||||||
|
` data-name="${(t.name || "").replace(/"/g, """)}"` +
|
||||||
|
`${tracked ? " disabled" : ""}>${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 = tokenLabel(t) + (tracked ? " (tracked)" : "");
|
||||||
|
html +=
|
||||||
|
`<option value="${t.address}"` +
|
||||||
|
` data-symbol="${t.symbol}"` +
|
||||||
|
` data-decimals="${t.decimals}"` +
|
||||||
|
` data-name="${(t.name || "").replace(/"/g, """)}"` +
|
||||||
|
`${tracked ? " disabled" : ""}>${label}</option>`;
|
||||||
|
}
|
||||||
|
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 };
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user