All checks were successful
check / check (push) Successful in 22s
Add blockie identicon, wallet/address title, and color dot with full address display to the export-privkey view, matching the pattern used by AddressDetail and other views. Address is click-to-copy.
369 lines
13 KiB
JavaScript
369 lines
13 KiB
JavaScript
const {
|
|
$,
|
|
showView,
|
|
showFlash,
|
|
balanceLinesForAddress,
|
|
addressDotHtml,
|
|
addressTitle,
|
|
escapeHtml,
|
|
truncateMiddle,
|
|
} = require("./helpers");
|
|
const { state, currentAddress, saveState } = require("../../shared/state");
|
|
const { formatUsd, 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 makeBlockie = require("ethereum-blockies-base64");
|
|
const { decryptWithPassword } = require("../../shared/vault");
|
|
const { getSignerForAddress } = require("../../shared/wallet");
|
|
|
|
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">` +
|
|
`<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 show() {
|
|
state.selectedToken = null;
|
|
const wallet = state.wallets[state.selectedWallet];
|
|
const addr = wallet.addresses[state.selectedAddress];
|
|
const wi = state.selectedWallet;
|
|
const ai = state.selectedAddress;
|
|
$("address-title").textContent =
|
|
wallet.name + " \u2014 Address " + (ai + 1);
|
|
const blockieEl = $("address-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-dot").innerHTML = addressDotHtml(addr.address);
|
|
$("address-full").dataset.full = addr.address;
|
|
$("address-full").textContent = addr.address;
|
|
const addrLink = etherscanAddressLink(addr.address);
|
|
$("address-etherscan-link").innerHTML =
|
|
`<a href="${addrLink}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
|
|
const usdTotal = formatUsd(getAddressValueUsd(addr));
|
|
$("address-usd-total").innerHTML = usdTotal || " ";
|
|
const ensEl = $("address-ens");
|
|
if (addr.ensName) {
|
|
ensEl.innerHTML =
|
|
addressDotHtml(addr.address) + escapeHtml(addr.ensName);
|
|
ensEl.classList.remove("hidden");
|
|
} else {
|
|
ensEl.classList.add("hidden");
|
|
}
|
|
$("address-balances").innerHTML = balanceLinesForAddress(
|
|
addr,
|
|
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>';
|
|
showView("address");
|
|
loadTransactions(addr.address);
|
|
}
|
|
|
|
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();
|
|
|
|
async function loadTransactions(address) {
|
|
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,
|
|
});
|
|
const 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();
|
|
}
|
|
|
|
loadedTxs = txs;
|
|
|
|
// Collect ALL unique addresses (from + to) for ENS resolution so
|
|
// that reverse lookups work for every displayed address, not just
|
|
// the ones that were originally entered as ENS names.
|
|
const counterparties = [
|
|
...new Set(txs.flatMap((tx) => [tx.from, tx.to].filter(Boolean))),
|
|
];
|
|
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);
|
|
$("tx-list").innerHTML =
|
|
'<div class="text-muted text-xs py-1">Failed to load transactions.</div>';
|
|
}
|
|
}
|
|
|
|
function renderTransactions(txs) {
|
|
const list = $("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) {
|
|
// For swap transactions, show the user's own labelled wallet
|
|
// address instead of the contract address (see issue #55).
|
|
const counterparty =
|
|
tx.direction === "contract" && tx.directionLabel === "Swap"
|
|
? tx.from
|
|
: tx.direction === "sent" || tx.direction === "contract"
|
|
? tx.to
|
|
: tx.from;
|
|
const ensName = ensNameMap.get(counterparty) || null;
|
|
const title = addressTitle(counterparty, state.wallets);
|
|
const dirLabel = tx.directionLabel;
|
|
const amountStr = tx.value
|
|
? escapeHtml(tx.value + " " + tx.symbol)
|
|
: escapeHtml(tx.symbol);
|
|
const maxAddr = Math.max(32, 36 - Math.max(0, amountStr.length - 10));
|
|
const displayAddr =
|
|
title || 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);
|
|
const tx = loadedTxs[idx];
|
|
const counterparty = tx.direction === "sent" ? tx.to : tx.from;
|
|
tx.fromEns = ensNameMap.get(tx.from) || null;
|
|
tx.toEns = ensNameMap.get(tx.to) || null;
|
|
ctx.showTransactionDetail(tx);
|
|
});
|
|
});
|
|
}
|
|
|
|
function init(_ctx) {
|
|
ctx = _ctx;
|
|
$("address-full").addEventListener("click", () => {
|
|
const addr = $("address-full").dataset.full;
|
|
if (addr) {
|
|
navigator.clipboard.writeText(addr);
|
|
showFlash("Copied!");
|
|
}
|
|
});
|
|
|
|
$("btn-address-back").addEventListener("click", () => {
|
|
ctx.renderWalletList();
|
|
showView("main");
|
|
});
|
|
|
|
$("btn-send").addEventListener("click", () => {
|
|
const addr =
|
|
state.wallets[state.selectedWallet].addresses[
|
|
state.selectedAddress
|
|
];
|
|
if (!addr.balance || parseFloat(addr.balance) === 0) {
|
|
showFlash("Cannot send \u2014 zero balance.");
|
|
return;
|
|
}
|
|
$("send-to").value = "";
|
|
$("send-amount").value = "";
|
|
$("send-token").classList.remove("hidden");
|
|
$("send-token-static").classList.add("hidden");
|
|
updateSendBalance();
|
|
showView("send");
|
|
});
|
|
|
|
$("btn-receive").addEventListener("click", () => {
|
|
ctx.showReceive();
|
|
});
|
|
|
|
$("btn-add-token").addEventListener("click", ctx.showAddTokenView);
|
|
|
|
// More menu dropdown
|
|
const moreBtn = $("btn-more-menu");
|
|
const moreDropdown = $("more-menu-dropdown");
|
|
moreBtn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
const isOpen = !moreDropdown.classList.toggle("hidden");
|
|
moreBtn.classList.toggle("bg-fg", isOpen);
|
|
moreBtn.classList.toggle("text-bg", isOpen);
|
|
});
|
|
document.addEventListener("click", () => {
|
|
moreDropdown.classList.add("hidden");
|
|
moreBtn.classList.remove("bg-fg", "text-bg");
|
|
});
|
|
moreDropdown.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
});
|
|
|
|
$("btn-export-privkey").addEventListener("click", () => {
|
|
moreDropdown.classList.add("hidden");
|
|
moreBtn.classList.remove("bg-fg", "text-bg");
|
|
const wallet = state.wallets[state.selectedWallet];
|
|
const addr = wallet.addresses[state.selectedAddress];
|
|
const blockieEl = $("export-privkey-jazzicon");
|
|
blockieEl.innerHTML = "";
|
|
const bImg = document.createElement("img");
|
|
bImg.src = makeBlockie(addr.address);
|
|
bImg.width = 48;
|
|
bImg.height = 48;
|
|
bImg.style.imageRendering = "pixelated";
|
|
bImg.style.borderRadius = "50%";
|
|
blockieEl.appendChild(bImg);
|
|
$("export-privkey-title").textContent =
|
|
wallet.name + " \u2014 Address " + (state.selectedAddress + 1);
|
|
$("export-privkey-dot").innerHTML = addressDotHtml(addr.address);
|
|
$("export-privkey-address").textContent = addr.address;
|
|
$("export-privkey-address").dataset.full = addr.address;
|
|
$("export-privkey-password").value = "";
|
|
$("export-privkey-flash").classList.add("hidden");
|
|
$("export-privkey-flash").textContent = "";
|
|
$("export-privkey-password-section").classList.remove("hidden");
|
|
$("export-privkey-result").classList.add("hidden");
|
|
$("export-privkey-value").textContent = "";
|
|
showView("export-privkey");
|
|
});
|
|
|
|
$("btn-export-privkey-confirm").addEventListener("click", async () => {
|
|
const password = $("export-privkey-password").value;
|
|
if (!password) {
|
|
$("export-privkey-flash").textContent = "Password is required.";
|
|
$("export-privkey-flash").classList.remove("hidden");
|
|
return;
|
|
}
|
|
const wallet = state.wallets[state.selectedWallet];
|
|
try {
|
|
const secret = await decryptWithPassword(
|
|
wallet.encryptedSecret,
|
|
password,
|
|
);
|
|
const signer = getSignerForAddress(
|
|
wallet,
|
|
state.selectedAddress,
|
|
secret,
|
|
);
|
|
const privateKey = signer.privateKey;
|
|
$("export-privkey-password-section").classList.add("hidden");
|
|
$("export-privkey-value").textContent = privateKey;
|
|
$("export-privkey-result").classList.remove("hidden");
|
|
$("export-privkey-flash").classList.add("hidden");
|
|
} catch {
|
|
$("export-privkey-flash").textContent = "Wrong password.";
|
|
$("export-privkey-flash").classList.remove("hidden");
|
|
}
|
|
});
|
|
|
|
$("export-privkey-value").addEventListener("click", () => {
|
|
const key = $("export-privkey-value").textContent;
|
|
if (key) {
|
|
navigator.clipboard.writeText(key);
|
|
showFlash("Copied!");
|
|
}
|
|
});
|
|
|
|
$("export-privkey-address").addEventListener("click", () => {
|
|
const full = $("export-privkey-address").dataset.full;
|
|
if (full) {
|
|
navigator.clipboard.writeText(full);
|
|
showFlash("Copied!");
|
|
}
|
|
});
|
|
|
|
$("btn-export-privkey-back").addEventListener("click", () => {
|
|
$("export-privkey-value").textContent = "";
|
|
$("export-privkey-password").value = "";
|
|
show();
|
|
});
|
|
}
|
|
|
|
module.exports = { init, show };
|