The flash line reserves one line, so a message that wrapped pushed the screen below it down. #flash-msg no longer wraps: text too long for it is cut with an ellipsis, and showFlash() puts the whole message in its title. Every message is also reworded to at most 50 characters so none is cut, and the add-token screens flash a fixed line for any error other than the two lookup messages, logging the detail. A new end-to-end test writes a message several lines long into the line and fails if the line or the screen below it moves. 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 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="${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 = tokenLabel(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 };
|