Add address-token detail view for per-token transaction filtering
All checks were successful
check / check (push) Successful in 17s
All checks were successful
check / check (push) Successful in 17s
Clicking a token balance on the address detail view navigates to a focused view showing only that token's transactions. Send pre-selects and locks the token dropdown, Receive shows an ERC-20 warning for non-ETH tokens, and all back buttons return to the correct parent view.
This commit is contained in:
@@ -314,6 +314,75 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ ADDRESS-TOKEN DETAIL VIEW ============ -->
|
||||
<div id="view-address-token" class="view hidden">
|
||||
<button
|
||||
id="btn-address-token-back"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mb-2"
|
||||
>
|
||||
< Back
|
||||
</button>
|
||||
<div
|
||||
id="address-token-jazzicon"
|
||||
class="flex justify-center mt-1 mb-3"
|
||||
></div>
|
||||
<div
|
||||
class="flex justify-between items-center border-b border-border pb-1 mb-2"
|
||||
>
|
||||
<h2 class="font-bold" id="address-token-title">Token</h2>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-xs mb-1 cursor-pointer break-all"
|
||||
title="Click to copy"
|
||||
id="address-token-line"
|
||||
>
|
||||
<span id="address-token-dot"></span>
|
||||
<span id="address-token-full"></span>
|
||||
<span id="address-token-etherscan-link"></span>
|
||||
</div>
|
||||
<div
|
||||
id="address-token-usd-total"
|
||||
class="text-xs text-muted mb-3 text-right"
|
||||
></div>
|
||||
<!-- single token balance -->
|
||||
<div class="border-b border-border-light pb-2 mb-2">
|
||||
<div id="address-token-balance"></div>
|
||||
</div>
|
||||
|
||||
<!-- actions -->
|
||||
<div class="flex gap-2 mb-3">
|
||||
<button
|
||||
id="btn-address-token-send"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer flex-1"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
<button
|
||||
id="btn-address-token-receive"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer flex-1"
|
||||
>
|
||||
Receive
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ERC-20 warning -->
|
||||
<div
|
||||
id="address-token-erc20-warning"
|
||||
class="text-xs border border-border border-dashed p-2 mb-3 hidden"
|
||||
></div>
|
||||
|
||||
<!-- token-filtered transactions -->
|
||||
<div class="mt-3">
|
||||
<div class="border-b border-border pb-1 mb-1">
|
||||
<h2 class="font-bold">Transactions</h2>
|
||||
</div>
|
||||
<div id="address-token-tx-list" class="overflow-hidden">
|
||||
<div class="text-muted text-xs py-1">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ SEND ============ -->
|
||||
<div id="view-send" class="view hidden">
|
||||
<button
|
||||
|
||||
@@ -12,6 +12,7 @@ const welcome = require("./views/welcome");
|
||||
const addWallet = require("./views/addWallet");
|
||||
const importKey = require("./views/importKey");
|
||||
const addressDetail = require("./views/addressDetail");
|
||||
const addressToken = require("./views/addressToken");
|
||||
const send = require("./views/send");
|
||||
const confirmTx = require("./views/confirmTx");
|
||||
const receive = require("./views/receive");
|
||||
@@ -47,6 +48,7 @@ const ctx = {
|
||||
showAddWalletView: () => addWallet.show(),
|
||||
showImportKeyView: () => importKey.show(),
|
||||
showAddressDetail: () => addressDetail.show(),
|
||||
showAddressToken: () => addressToken.show(),
|
||||
showAddTokenView: () => addToken.show(),
|
||||
showConfirmTx: (txInfo) => confirmTx.show(txInfo),
|
||||
};
|
||||
@@ -106,6 +108,7 @@ async function init() {
|
||||
importKey.init(ctx);
|
||||
home.init(ctx);
|
||||
addressDetail.init(ctx);
|
||||
addressToken.init(ctx);
|
||||
send.init(ctx);
|
||||
confirmTx.init(ctx);
|
||||
receive.init(ctx);
|
||||
|
||||
@@ -20,6 +20,8 @@ const { log } = require("../../shared/log");
|
||||
const QRCode = require("qrcode");
|
||||
const makeBlockie = require("ethereum-blockies-base64");
|
||||
|
||||
let ctx;
|
||||
|
||||
const EXT_ICON =
|
||||
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
|
||||
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
|
||||
@@ -36,6 +38,7 @@ function etherscanTxLink(hash) {
|
||||
}
|
||||
|
||||
function show() {
|
||||
state.selectedToken = null;
|
||||
const wallet = state.wallets[state.selectedWallet];
|
||||
const addr = wallet.addresses[state.selectedAddress];
|
||||
const wi = state.selectedWallet;
|
||||
@@ -71,6 +74,14 @@ function show() {
|
||||
state.trackedTokens,
|
||||
state.showZeroBalanceTokens,
|
||||
);
|
||||
$("address-balances")
|
||||
.querySelectorAll(".balance-row")
|
||||
.forEach((row) => {
|
||||
row.addEventListener("click", () => {
|
||||
state.selectedToken = row.dataset.token;
|
||||
ctx.showAddressToken();
|
||||
});
|
||||
});
|
||||
renderSendTokenSelect(addr);
|
||||
$("tx-list").innerHTML =
|
||||
'<div class="text-muted text-xs py-1">Loading...</div>';
|
||||
@@ -272,7 +283,8 @@ function showTxDetail(tx) {
|
||||
});
|
||||
}
|
||||
|
||||
function init(ctx) {
|
||||
function init(_ctx) {
|
||||
ctx = _ctx;
|
||||
$("address-full").addEventListener("click", () => {
|
||||
const addr = $("address-full").dataset.full;
|
||||
if (addr) {
|
||||
@@ -297,6 +309,7 @@ function init(ctx) {
|
||||
}
|
||||
$("send-to").value = "";
|
||||
$("send-amount").value = "";
|
||||
$("send-token").disabled = false;
|
||||
updateSendBalance();
|
||||
showView("send");
|
||||
});
|
||||
@@ -318,7 +331,11 @@ function init(ctx) {
|
||||
$("btn-add-token").addEventListener("click", ctx.showAddTokenView);
|
||||
|
||||
$("btn-tx-back").addEventListener("click", () => {
|
||||
show();
|
||||
if (state.selectedToken) {
|
||||
ctx.showAddressToken();
|
||||
} else {
|
||||
show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
370
src/popup/views/addressToken.js
Normal file
370
src/popup/views/addressToken.js
Normal file
@@ -0,0 +1,370 @@
|
||||
// Address-token detail view: shows a single token's balance and
|
||||
// filtered transactions for the selected address.
|
||||
|
||||
const {
|
||||
$,
|
||||
showView,
|
||||
showFlash,
|
||||
addressDotHtml,
|
||||
escapeHtml,
|
||||
truncateMiddle,
|
||||
balanceLine,
|
||||
} = require("./helpers");
|
||||
const { state, currentAddress, saveState } = require("../../shared/state");
|
||||
const {
|
||||
formatUsd,
|
||||
getPrice,
|
||||
getAddressValueUsd,
|
||||
} = require("../../shared/prices");
|
||||
const {
|
||||
fetchRecentTransactions,
|
||||
filterTransactions,
|
||||
} = require("../../shared/transactions");
|
||||
const { resolveEnsNames } = require("../../shared/ens");
|
||||
const { updateSendBalance, renderSendTokenSelect } = require("./send");
|
||||
const { log } = require("../../shared/log");
|
||||
const QRCode = require("qrcode");
|
||||
const makeBlockie = require("ethereum-blockies-base64");
|
||||
|
||||
const EXT_ICON =
|
||||
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
|
||||
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
|
||||
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
|
||||
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
|
||||
`</svg></span>`;
|
||||
|
||||
function etherscanAddressLink(address) {
|
||||
return `https://etherscan.io/address/${address}`;
|
||||
}
|
||||
|
||||
function etherscanTxLink(hash) {
|
||||
return `https://etherscan.io/tx/${hash}`;
|
||||
}
|
||||
|
||||
function isoDate(timestamp) {
|
||||
const d = new Date(timestamp * 1000);
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
return (
|
||||
d.getFullYear() +
|
||||
"-" +
|
||||
pad(d.getMonth() + 1) +
|
||||
"-" +
|
||||
pad(d.getDate()) +
|
||||
" " +
|
||||
pad(d.getHours()) +
|
||||
":" +
|
||||
pad(d.getMinutes()) +
|
||||
":" +
|
||||
pad(d.getSeconds())
|
||||
);
|
||||
}
|
||||
|
||||
function timeAgo(timestamp) {
|
||||
const seconds = Math.floor(Date.now() / 1000 - timestamp);
|
||||
if (seconds < 60) return seconds + " seconds ago";
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60)
|
||||
return minutes + " minute" + (minutes !== 1 ? "s" : "") + " ago";
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return hours + " hour" + (hours !== 1 ? "s" : "") + " ago";
|
||||
const days = Math.floor(hours / 24);
|
||||
if (days < 30) return days + " day" + (days !== 1 ? "s" : "") + " ago";
|
||||
const months = Math.floor(days / 30);
|
||||
if (months < 12)
|
||||
return months + " month" + (months !== 1 ? "s" : "") + " ago";
|
||||
const years = Math.floor(days / 365);
|
||||
return years + " year" + (years !== 1 ? "s" : "") + " ago";
|
||||
}
|
||||
|
||||
let loadedTxs = [];
|
||||
let ensNameMap = new Map();
|
||||
|
||||
function show() {
|
||||
const wallet = state.wallets[state.selectedWallet];
|
||||
const addr = wallet.addresses[state.selectedAddress];
|
||||
const ai = state.selectedAddress;
|
||||
const tokenId = state.selectedToken;
|
||||
|
||||
// Determine token symbol and balance
|
||||
let symbol, amount, price;
|
||||
if (tokenId === "ETH") {
|
||||
symbol = "ETH";
|
||||
amount = parseFloat(addr.balance || "0");
|
||||
price = getPrice("ETH");
|
||||
} else {
|
||||
const tb = (addr.tokenBalances || []).find(
|
||||
(t) => t.address.toLowerCase() === tokenId.toLowerCase(),
|
||||
);
|
||||
symbol = tb ? tb.symbol : "?";
|
||||
amount = tb ? parseFloat(tb.balance || "0") : 0;
|
||||
price = getPrice(symbol);
|
||||
}
|
||||
|
||||
$("address-token-title").textContent =
|
||||
wallet.name + " \u2014 Address " + (ai + 1) + " \u2014 " + symbol;
|
||||
|
||||
// Blockie
|
||||
const blockieEl = $("address-token-jazzicon");
|
||||
blockieEl.innerHTML = "";
|
||||
const img = document.createElement("img");
|
||||
img.src = makeBlockie(addr.address);
|
||||
img.width = 48;
|
||||
img.height = 48;
|
||||
img.style.imageRendering = "pixelated";
|
||||
img.style.borderRadius = "50%";
|
||||
blockieEl.appendChild(img);
|
||||
|
||||
// Address line
|
||||
$("address-token-dot").innerHTML = addressDotHtml(addr.address);
|
||||
$("address-token-full").dataset.full = addr.address;
|
||||
$("address-token-full").textContent = addr.address;
|
||||
const addrLink = etherscanAddressLink(addr.address);
|
||||
$("address-token-etherscan-link").innerHTML =
|
||||
`<a href="${addrLink}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
|
||||
|
||||
// USD total for this token only
|
||||
const usdVal = price ? amount * price : 0;
|
||||
$("address-token-usd-total").textContent = formatUsd(usdVal);
|
||||
|
||||
// Single token balance line (no tokenId — not clickable here)
|
||||
$("address-token-balance").innerHTML = balanceLine(symbol, amount, price);
|
||||
|
||||
// ERC-20 warning
|
||||
const warningEl = $("address-token-erc20-warning");
|
||||
if (tokenId !== "ETH") {
|
||||
warningEl.textContent =
|
||||
"This is an ERC-20 token. Only send " +
|
||||
symbol +
|
||||
" on the Ethereum network to this address. Sending tokens on other networks will result in permanent loss.";
|
||||
warningEl.classList.remove("hidden");
|
||||
} else {
|
||||
warningEl.classList.add("hidden");
|
||||
}
|
||||
|
||||
// Transactions
|
||||
$("address-token-tx-list").innerHTML =
|
||||
'<div class="text-muted text-xs py-1">Loading...</div>';
|
||||
showView("address-token");
|
||||
loadTransactions(addr.address, tokenId);
|
||||
}
|
||||
|
||||
async function loadTransactions(address, tokenId) {
|
||||
try {
|
||||
const rawTxs = await fetchRecentTransactions(
|
||||
address,
|
||||
state.blockscoutUrl,
|
||||
);
|
||||
const result = filterTransactions(rawTxs, {
|
||||
hideLowHolderTokens: state.hideLowHolderTokens,
|
||||
hideFraudContracts: state.hideFraudContracts,
|
||||
hideDustTransactions: state.hideDustTransactions,
|
||||
dustThresholdGwei: state.dustThresholdGwei,
|
||||
fraudContracts: state.fraudContracts,
|
||||
});
|
||||
let txs = result.transactions;
|
||||
|
||||
// Persist any newly discovered fraud contracts
|
||||
if (result.newFraudContracts.length > 0) {
|
||||
for (const addr of result.newFraudContracts) {
|
||||
if (!state.fraudContracts.includes(addr)) {
|
||||
state.fraudContracts.push(addr);
|
||||
}
|
||||
}
|
||||
await saveState();
|
||||
}
|
||||
|
||||
// Filter to this token only
|
||||
if (tokenId === "ETH") {
|
||||
txs = txs.filter((tx) => tx.contractAddress === null);
|
||||
} else {
|
||||
txs = txs.filter(
|
||||
(tx) =>
|
||||
tx.contractAddress &&
|
||||
tx.contractAddress.toLowerCase() === tokenId.toLowerCase(),
|
||||
);
|
||||
}
|
||||
|
||||
loadedTxs = txs;
|
||||
|
||||
// Collect unique counterparty addresses for ENS resolution
|
||||
const counterparties = [
|
||||
...new Set(
|
||||
txs.map((tx) => (tx.direction === "sent" ? tx.to : tx.from)),
|
||||
),
|
||||
];
|
||||
if (counterparties.length > 0) {
|
||||
try {
|
||||
ensNameMap = await resolveEnsNames(
|
||||
counterparties,
|
||||
state.rpcUrl,
|
||||
);
|
||||
} catch {
|
||||
ensNameMap = new Map();
|
||||
}
|
||||
}
|
||||
|
||||
renderTransactions(txs);
|
||||
} catch (e) {
|
||||
log.errorf("loadTransactions failed:", e.message);
|
||||
$("address-token-tx-list").innerHTML =
|
||||
'<div class="text-muted text-xs py-1">Failed to load transactions.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderTransactions(txs) {
|
||||
const list = $("address-token-tx-list");
|
||||
if (txs.length === 0) {
|
||||
list.innerHTML =
|
||||
'<div class="text-muted text-xs py-1">No transactions found.</div>';
|
||||
return;
|
||||
}
|
||||
let html = "";
|
||||
let i = 0;
|
||||
for (const tx of txs) {
|
||||
const counterparty = tx.direction === "sent" ? tx.to : tx.from;
|
||||
const ensName = ensNameMap.get(counterparty) || null;
|
||||
const dirLabel = tx.direction === "sent" ? "Sent" : "Received";
|
||||
const amountStr = escapeHtml(tx.value + " " + tx.symbol);
|
||||
const maxAddr = Math.max(10, 36 - Math.max(0, amountStr.length - 10));
|
||||
const displayAddr = ensName || truncateMiddle(counterparty, maxAddr);
|
||||
const addrStr = escapeHtml(displayAddr);
|
||||
const dot = addressDotHtml(counterparty);
|
||||
const err = tx.isError ? " (failed)" : "";
|
||||
const opacity = tx.isError ? " opacity:0.5;" : "";
|
||||
const ago = escapeHtml(timeAgo(tx.timestamp));
|
||||
const iso = escapeHtml(isoDate(tx.timestamp));
|
||||
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
|
||||
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
|
||||
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${addrStr}</span><span>${amountStr}</span></div>`;
|
||||
html += `</div>`;
|
||||
i++;
|
||||
}
|
||||
list.innerHTML = html;
|
||||
list.querySelectorAll(".tx-row").forEach((row) => {
|
||||
row.addEventListener("click", () => {
|
||||
const idx = parseInt(row.dataset.tx, 10);
|
||||
showTxDetail(loadedTxs[idx]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function copyableHtml(text, extraClass) {
|
||||
const cls =
|
||||
"underline decoration-dashed cursor-pointer" +
|
||||
(extraClass ? " " + extraClass : "");
|
||||
return `<span class="${cls}" data-copy="${escapeHtml(text)}">${escapeHtml(text)}</span>`;
|
||||
}
|
||||
|
||||
function blockieHtml(address) {
|
||||
const src = makeBlockie(address);
|
||||
return `<img src="${src}" width="48" height="48" style="image-rendering:pixelated;border-radius:50%;display:inline-block">`;
|
||||
}
|
||||
|
||||
function txDetailAddressHtml(address) {
|
||||
const ensName = ensNameMap.get(address) || null;
|
||||
const blockie = blockieHtml(address);
|
||||
const dot = addressDotHtml(address);
|
||||
const link = etherscanAddressLink(address);
|
||||
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
|
||||
let html = `<div class="mb-1">${blockie}</div>`;
|
||||
if (ensName) {
|
||||
html +=
|
||||
`<div class="flex items-center">${dot}` +
|
||||
copyableHtml(ensName, "") +
|
||||
extLink +
|
||||
`</div>` +
|
||||
`<div class="break-all">` +
|
||||
copyableHtml(address, "break-all") +
|
||||
`</div>`;
|
||||
} else {
|
||||
html +=
|
||||
`<div class="flex items-center">${dot}` +
|
||||
copyableHtml(address, "break-all") +
|
||||
extLink +
|
||||
`</div>`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function txDetailHashHtml(hash) {
|
||||
const link = etherscanTxLink(hash);
|
||||
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
|
||||
return copyableHtml(hash, "break-all") + extLink;
|
||||
}
|
||||
|
||||
function showTxDetail(tx) {
|
||||
$("tx-detail-hash").innerHTML = txDetailHashHtml(tx.hash);
|
||||
$("tx-detail-from").innerHTML = txDetailAddressHtml(tx.from);
|
||||
$("tx-detail-to").innerHTML = txDetailAddressHtml(tx.to);
|
||||
$("tx-detail-value").textContent = tx.value + " " + tx.symbol;
|
||||
$("tx-detail-time").textContent =
|
||||
isoDate(tx.timestamp) + " (" + timeAgo(tx.timestamp) + ")";
|
||||
$("tx-detail-status").textContent = tx.isError ? "Failed" : "Success";
|
||||
showView("transaction");
|
||||
|
||||
// Attach copy handlers
|
||||
document
|
||||
.getElementById("view-transaction")
|
||||
.querySelectorAll("[data-copy]")
|
||||
.forEach((el) => {
|
||||
el.onclick = () => {
|
||||
navigator.clipboard.writeText(el.dataset.copy);
|
||||
showFlash("Copied!");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function init(ctx) {
|
||||
$("address-token-full").addEventListener("click", () => {
|
||||
const addr = $("address-token-full").dataset.full;
|
||||
if (addr) {
|
||||
navigator.clipboard.writeText(addr);
|
||||
showFlash("Copied!");
|
||||
}
|
||||
});
|
||||
|
||||
$("btn-address-token-back").addEventListener("click", () => {
|
||||
ctx.showAddressDetail();
|
||||
});
|
||||
|
||||
$("btn-address-token-send").addEventListener("click", () => {
|
||||
const addr =
|
||||
state.wallets[state.selectedWallet].addresses[
|
||||
state.selectedAddress
|
||||
];
|
||||
if (!addr.balance || parseFloat(addr.balance) === 0) {
|
||||
if (state.selectedToken === "ETH") {
|
||||
showFlash("Cannot send \u2014 zero balance.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
renderSendTokenSelect(addr);
|
||||
$("send-to").value = "";
|
||||
$("send-amount").value = "";
|
||||
const tokenId = state.selectedToken;
|
||||
if (tokenId === "ETH") {
|
||||
$("send-token").value = "ETH";
|
||||
} else {
|
||||
$("send-token").value = tokenId;
|
||||
}
|
||||
$("send-token").disabled = true;
|
||||
updateSendBalance();
|
||||
showView("send");
|
||||
});
|
||||
|
||||
$("btn-address-token-receive").addEventListener("click", () => {
|
||||
const addr = currentAddress();
|
||||
const address = addr ? addr.address : "";
|
||||
$("receive-address").textContent = address;
|
||||
if (address) {
|
||||
QRCode.toCanvas($("receive-qr"), address, {
|
||||
width: 200,
|
||||
margin: 2,
|
||||
color: { dark: "#000000", light: "#ffffff" },
|
||||
});
|
||||
}
|
||||
showView("receive");
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { init, show };
|
||||
@@ -13,6 +13,7 @@ const VIEWS = [
|
||||
"import-key",
|
||||
"main",
|
||||
"address",
|
||||
"address-token",
|
||||
"send",
|
||||
"confirm-tx",
|
||||
"receive",
|
||||
@@ -72,11 +73,15 @@ function showFlash(msg, duration = 2000) {
|
||||
}, duration);
|
||||
}
|
||||
|
||||
function balanceLine(symbol, amount, price) {
|
||||
function balanceLine(symbol, amount, price, tokenId) {
|
||||
const qty = amount.toFixed(4);
|
||||
const usd = price ? formatUsd(amount * price) : "";
|
||||
const tokenAttr = tokenId ? ` data-token="${tokenId}"` : "";
|
||||
const clickClass = tokenId
|
||||
? " cursor-pointer hover:bg-hover balance-row"
|
||||
: "";
|
||||
return (
|
||||
`<div class="flex text-xs">` +
|
||||
`<div class="flex text-xs${clickClass}"${tokenAttr}>` +
|
||||
`<span class="flex justify-between" style="width:42ch;max-width:100%">` +
|
||||
`<span>${symbol}</span>` +
|
||||
`<span>${qty}</span>` +
|
||||
@@ -91,18 +96,29 @@ function balanceLinesForAddress(addr, trackedTokens, showZero) {
|
||||
"ETH",
|
||||
parseFloat(addr.balance || "0"),
|
||||
getPrice("ETH"),
|
||||
"ETH",
|
||||
);
|
||||
const seen = new Set();
|
||||
for (const t of addr.tokenBalances || []) {
|
||||
const bal = parseFloat(t.balance || "0");
|
||||
if (bal === 0 && !showZero) continue;
|
||||
html += balanceLine(t.symbol, bal, getPrice(t.symbol));
|
||||
html += balanceLine(
|
||||
t.symbol,
|
||||
bal,
|
||||
getPrice(t.symbol),
|
||||
t.address.toLowerCase(),
|
||||
);
|
||||
seen.add(t.address.toLowerCase());
|
||||
}
|
||||
if (showZero && trackedTokens) {
|
||||
for (const t of trackedTokens) {
|
||||
if (seen.has(t.address.toLowerCase())) continue;
|
||||
html += balanceLine(t.symbol, 0, getPrice(t.symbol));
|
||||
html += balanceLine(
|
||||
t.symbol,
|
||||
0,
|
||||
getPrice(t.symbol),
|
||||
t.address.toLowerCase(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return html;
|
||||
@@ -193,6 +209,7 @@ module.exports = {
|
||||
hideError,
|
||||
showView,
|
||||
showFlash,
|
||||
balanceLine,
|
||||
balanceLinesForAddress,
|
||||
addressColor,
|
||||
addressDotHtml,
|
||||
|
||||
@@ -403,6 +403,7 @@ function init(ctx) {
|
||||
}
|
||||
$("send-to").value = "";
|
||||
$("send-amount").value = "";
|
||||
$("send-token").disabled = false;
|
||||
renderSendTokenSelect(addr);
|
||||
updateSendBalance();
|
||||
showView("send");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const { $, showFlash } = require("./helpers");
|
||||
const { state } = require("../../shared/state");
|
||||
|
||||
function init(ctx) {
|
||||
$("btn-receive-copy").addEventListener("click", () => {
|
||||
@@ -9,7 +10,13 @@ function init(ctx) {
|
||||
}
|
||||
});
|
||||
|
||||
$("btn-receive-back").addEventListener("click", ctx.showAddressDetail);
|
||||
$("btn-receive-back").addEventListener("click", () => {
|
||||
if (state.selectedToken) {
|
||||
ctx.showAddressToken();
|
||||
} else {
|
||||
ctx.showAddressDetail();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { init };
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const { $, showFlash, formatAddressHtml } = require("./helpers");
|
||||
const { state, currentAddress } = require("../../shared/state");
|
||||
let ctx;
|
||||
const { getProvider } = require("../../shared/balances");
|
||||
const { KNOWN_SYMBOLS } = require("../../shared/tokens");
|
||||
|
||||
@@ -53,7 +54,8 @@ function updateSendBalance() {
|
||||
}
|
||||
}
|
||||
|
||||
function init(ctx) {
|
||||
function init(_ctx) {
|
||||
ctx = _ctx;
|
||||
$("send-token").addEventListener("change", updateSendBalance);
|
||||
|
||||
$("btn-send-review").addEventListener("click", async () => {
|
||||
@@ -100,7 +102,14 @@ function init(ctx) {
|
||||
});
|
||||
});
|
||||
|
||||
$("btn-send-back").addEventListener("click", ctx.showAddressDetail);
|
||||
$("btn-send-back").addEventListener("click", () => {
|
||||
$("send-token").disabled = false;
|
||||
if (state.selectedToken) {
|
||||
ctx.showAddressToken();
|
||||
} else {
|
||||
ctx.showAddressDetail();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { init, updateSendBalance, renderSendTokenSelect };
|
||||
|
||||
@@ -31,6 +31,7 @@ const state = {
|
||||
...DEFAULT_STATE,
|
||||
selectedWallet: null,
|
||||
selectedAddress: null,
|
||||
selectedToken: null,
|
||||
};
|
||||
|
||||
async function saveState() {
|
||||
|
||||
Reference in New Issue
Block a user