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>
|
||||||
</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 ============ -->
|
<!-- ============ SEND ============ -->
|
||||||
<div id="view-send" class="view hidden">
|
<div id="view-send" class="view hidden">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const welcome = require("./views/welcome");
|
|||||||
const addWallet = require("./views/addWallet");
|
const addWallet = require("./views/addWallet");
|
||||||
const importKey = require("./views/importKey");
|
const importKey = require("./views/importKey");
|
||||||
const addressDetail = require("./views/addressDetail");
|
const addressDetail = require("./views/addressDetail");
|
||||||
|
const addressToken = require("./views/addressToken");
|
||||||
const send = require("./views/send");
|
const send = require("./views/send");
|
||||||
const confirmTx = require("./views/confirmTx");
|
const confirmTx = require("./views/confirmTx");
|
||||||
const receive = require("./views/receive");
|
const receive = require("./views/receive");
|
||||||
@@ -47,6 +48,7 @@ const ctx = {
|
|||||||
showAddWalletView: () => addWallet.show(),
|
showAddWalletView: () => addWallet.show(),
|
||||||
showImportKeyView: () => importKey.show(),
|
showImportKeyView: () => importKey.show(),
|
||||||
showAddressDetail: () => addressDetail.show(),
|
showAddressDetail: () => addressDetail.show(),
|
||||||
|
showAddressToken: () => addressToken.show(),
|
||||||
showAddTokenView: () => addToken.show(),
|
showAddTokenView: () => addToken.show(),
|
||||||
showConfirmTx: (txInfo) => confirmTx.show(txInfo),
|
showConfirmTx: (txInfo) => confirmTx.show(txInfo),
|
||||||
};
|
};
|
||||||
@@ -106,6 +108,7 @@ async function init() {
|
|||||||
importKey.init(ctx);
|
importKey.init(ctx);
|
||||||
home.init(ctx);
|
home.init(ctx);
|
||||||
addressDetail.init(ctx);
|
addressDetail.init(ctx);
|
||||||
|
addressToken.init(ctx);
|
||||||
send.init(ctx);
|
send.init(ctx);
|
||||||
confirmTx.init(ctx);
|
confirmTx.init(ctx);
|
||||||
receive.init(ctx);
|
receive.init(ctx);
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ const { log } = require("../../shared/log");
|
|||||||
const QRCode = require("qrcode");
|
const QRCode = require("qrcode");
|
||||||
const makeBlockie = require("ethereum-blockies-base64");
|
const makeBlockie = require("ethereum-blockies-base64");
|
||||||
|
|
||||||
|
let ctx;
|
||||||
|
|
||||||
const EXT_ICON =
|
const EXT_ICON =
|
||||||
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
|
`<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">` +
|
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
|
||||||
@@ -36,6 +38,7 @@ function etherscanTxLink(hash) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
|
state.selectedToken = null;
|
||||||
const wallet = state.wallets[state.selectedWallet];
|
const wallet = state.wallets[state.selectedWallet];
|
||||||
const addr = wallet.addresses[state.selectedAddress];
|
const addr = wallet.addresses[state.selectedAddress];
|
||||||
const wi = state.selectedWallet;
|
const wi = state.selectedWallet;
|
||||||
@@ -71,6 +74,14 @@ function show() {
|
|||||||
state.trackedTokens,
|
state.trackedTokens,
|
||||||
state.showZeroBalanceTokens,
|
state.showZeroBalanceTokens,
|
||||||
);
|
);
|
||||||
|
$("address-balances")
|
||||||
|
.querySelectorAll(".balance-row")
|
||||||
|
.forEach((row) => {
|
||||||
|
row.addEventListener("click", () => {
|
||||||
|
state.selectedToken = row.dataset.token;
|
||||||
|
ctx.showAddressToken();
|
||||||
|
});
|
||||||
|
});
|
||||||
renderSendTokenSelect(addr);
|
renderSendTokenSelect(addr);
|
||||||
$("tx-list").innerHTML =
|
$("tx-list").innerHTML =
|
||||||
'<div class="text-muted text-xs py-1">Loading...</div>';
|
'<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", () => {
|
$("address-full").addEventListener("click", () => {
|
||||||
const addr = $("address-full").dataset.full;
|
const addr = $("address-full").dataset.full;
|
||||||
if (addr) {
|
if (addr) {
|
||||||
@@ -297,6 +309,7 @@ function init(ctx) {
|
|||||||
}
|
}
|
||||||
$("send-to").value = "";
|
$("send-to").value = "";
|
||||||
$("send-amount").value = "";
|
$("send-amount").value = "";
|
||||||
|
$("send-token").disabled = false;
|
||||||
updateSendBalance();
|
updateSendBalance();
|
||||||
showView("send");
|
showView("send");
|
||||||
});
|
});
|
||||||
@@ -318,7 +331,11 @@ function init(ctx) {
|
|||||||
$("btn-add-token").addEventListener("click", ctx.showAddTokenView);
|
$("btn-add-token").addEventListener("click", ctx.showAddTokenView);
|
||||||
|
|
||||||
$("btn-tx-back").addEventListener("click", () => {
|
$("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",
|
"import-key",
|
||||||
"main",
|
"main",
|
||||||
"address",
|
"address",
|
||||||
|
"address-token",
|
||||||
"send",
|
"send",
|
||||||
"confirm-tx",
|
"confirm-tx",
|
||||||
"receive",
|
"receive",
|
||||||
@@ -72,11 +73,15 @@ function showFlash(msg, duration = 2000) {
|
|||||||
}, duration);
|
}, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
function balanceLine(symbol, amount, price) {
|
function balanceLine(symbol, amount, price, tokenId) {
|
||||||
const qty = amount.toFixed(4);
|
const qty = amount.toFixed(4);
|
||||||
const usd = price ? formatUsd(amount * price) : "";
|
const usd = price ? formatUsd(amount * price) : "";
|
||||||
|
const tokenAttr = tokenId ? ` data-token="${tokenId}"` : "";
|
||||||
|
const clickClass = tokenId
|
||||||
|
? " cursor-pointer hover:bg-hover balance-row"
|
||||||
|
: "";
|
||||||
return (
|
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 class="flex justify-between" style="width:42ch;max-width:100%">` +
|
||||||
`<span>${symbol}</span>` +
|
`<span>${symbol}</span>` +
|
||||||
`<span>${qty}</span>` +
|
`<span>${qty}</span>` +
|
||||||
@@ -91,18 +96,29 @@ function balanceLinesForAddress(addr, trackedTokens, showZero) {
|
|||||||
"ETH",
|
"ETH",
|
||||||
parseFloat(addr.balance || "0"),
|
parseFloat(addr.balance || "0"),
|
||||||
getPrice("ETH"),
|
getPrice("ETH"),
|
||||||
|
"ETH",
|
||||||
);
|
);
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
for (const t of addr.tokenBalances || []) {
|
for (const t of addr.tokenBalances || []) {
|
||||||
const bal = parseFloat(t.balance || "0");
|
const bal = parseFloat(t.balance || "0");
|
||||||
if (bal === 0 && !showZero) continue;
|
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());
|
seen.add(t.address.toLowerCase());
|
||||||
}
|
}
|
||||||
if (showZero && trackedTokens) {
|
if (showZero && trackedTokens) {
|
||||||
for (const t of trackedTokens) {
|
for (const t of trackedTokens) {
|
||||||
if (seen.has(t.address.toLowerCase())) continue;
|
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;
|
return html;
|
||||||
@@ -193,6 +209,7 @@ module.exports = {
|
|||||||
hideError,
|
hideError,
|
||||||
showView,
|
showView,
|
||||||
showFlash,
|
showFlash,
|
||||||
|
balanceLine,
|
||||||
balanceLinesForAddress,
|
balanceLinesForAddress,
|
||||||
addressColor,
|
addressColor,
|
||||||
addressDotHtml,
|
addressDotHtml,
|
||||||
|
|||||||
@@ -403,6 +403,7 @@ function init(ctx) {
|
|||||||
}
|
}
|
||||||
$("send-to").value = "";
|
$("send-to").value = "";
|
||||||
$("send-amount").value = "";
|
$("send-amount").value = "";
|
||||||
|
$("send-token").disabled = false;
|
||||||
renderSendTokenSelect(addr);
|
renderSendTokenSelect(addr);
|
||||||
updateSendBalance();
|
updateSendBalance();
|
||||||
showView("send");
|
showView("send");
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const { $, showFlash } = require("./helpers");
|
const { $, showFlash } = require("./helpers");
|
||||||
|
const { state } = require("../../shared/state");
|
||||||
|
|
||||||
function init(ctx) {
|
function init(ctx) {
|
||||||
$("btn-receive-copy").addEventListener("click", () => {
|
$("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 };
|
module.exports = { init };
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
const { $, showFlash, formatAddressHtml } = require("./helpers");
|
const { $, showFlash, formatAddressHtml } = require("./helpers");
|
||||||
const { state, currentAddress } = require("../../shared/state");
|
const { state, currentAddress } = require("../../shared/state");
|
||||||
|
let ctx;
|
||||||
const { getProvider } = require("../../shared/balances");
|
const { getProvider } = require("../../shared/balances");
|
||||||
const { KNOWN_SYMBOLS } = require("../../shared/tokens");
|
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);
|
$("send-token").addEventListener("change", updateSendBalance);
|
||||||
|
|
||||||
$("btn-send-review").addEventListener("click", async () => {
|
$("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 };
|
module.exports = { init, updateSendBalance, renderSendTokenSelect };
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const state = {
|
|||||||
...DEFAULT_STATE,
|
...DEFAULT_STATE,
|
||||||
selectedWallet: null,
|
selectedWallet: null,
|
||||||
selectedAddress: null,
|
selectedAddress: null,
|
||||||
|
selectedToken: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function saveState() {
|
async function saveState() {
|
||||||
|
|||||||
Reference in New Issue
Block a user