Transaction hash
diff --git a/src/popup/index.js b/src/popup/index.js
index 9413805..5a12180 100644
--- a/src/popup/index.js
+++ b/src/popup/index.js
@@ -189,7 +189,7 @@ async function init() {
const params = new URLSearchParams(window.location.search);
const approvalId = params.get("approval");
if (approvalId) {
- approval.show(parseInt(approvalId, 10));
+ approval.show(approvalId);
showView("approve-site");
return;
}
diff --git a/src/popup/views/addressDetail.js b/src/popup/views/addressDetail.js
index a91cf20..3f4f4f1 100644
--- a/src/popup/views/addressDetail.js
+++ b/src/popup/views/addressDetail.js
@@ -185,7 +185,10 @@ function renderTransactions(txs) {
let html = "";
let i = 0;
for (const tx of txs) {
- const counterparty = tx.direction === "sent" ? tx.to : tx.from;
+ const counterparty =
+ tx.direction === "sent" || tx.direction === "contract"
+ ? tx.to
+ : tx.from;
const ensName = ensNameMap.get(counterparty) || null;
const dirLabel = tx.directionLabel;
const amountStr = tx.value
diff --git a/src/popup/views/addressToken.js b/src/popup/views/addressToken.js
index 8ecbd60..1284029 100644
--- a/src/popup/views/addressToken.js
+++ b/src/popup/views/addressToken.js
@@ -11,6 +11,7 @@ const {
balanceLine,
} = require("./helpers");
const { state, currentAddress, saveState } = require("../../shared/state");
+const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
const {
formatUsd,
getPrice,
@@ -130,6 +131,43 @@ function show() {
// Single token balance line (no tokenId — not clickable here)
$("address-token-balance").innerHTML = balanceLine(symbol, amount, price);
+ // Token contract details (ERC-20 only)
+ const contractInfo = $("address-token-contract-info");
+ if (tokenId !== "ETH") {
+ const tb = (addr.tokenBalances || []).find(
+ (t) => t.address.toLowerCase() === tokenId.toLowerCase(),
+ );
+ const tokenName = tb && tb.name ? escapeHtml(tb.name) : null;
+ const tokenSymbol = tb && tb.symbol ? escapeHtml(tb.symbol) : null;
+ const tokenDecimals = tb && tb.decimals != null ? tb.decimals : null;
+ const tokenHolders = tb && tb.holders != null ? tb.holders : null;
+ const dot = addressDotHtml(tokenId);
+ const tokenLink = `https://etherscan.io/token/${escapeHtml(tokenId)}`;
+ const knownToken = TOKEN_BY_ADDRESS.get(tokenId.toLowerCase());
+ const projectUrl = knownToken && knownToken.url ? knownToken.url : null;
+ let infoHtml = `
Contract Address
`;
+ infoHtml +=
+ `
${dot}` +
+ `
${escapeHtml(tokenId)}` +
+ `
${EXT_ICON}` +
+ `
`;
+ if (tokenName)
+ infoHtml += `
Name: ${tokenName}
`;
+ if (tokenSymbol)
+ infoHtml += `
Symbol: ${tokenSymbol}
`;
+ if (tokenDecimals != null)
+ infoHtml += `
Decimals: ${tokenDecimals}
`;
+ if (tokenHolders != null)
+ infoHtml += `
Holders: ${Number(tokenHolders).toLocaleString()}
`;
+ if (projectUrl)
+ infoHtml += `
`;
+ contractInfo.innerHTML = infoHtml;
+ contractInfo.classList.remove("hidden");
+ } else {
+ contractInfo.innerHTML = "";
+ contractInfo.classList.add("hidden");
+ }
+
// Transactions
$("address-token-tx-list").innerHTML =
'
Loading...
';
@@ -252,6 +290,14 @@ function init(_ctx) {
}
});
+ $("address-token-contract-info").addEventListener("click", (e) => {
+ const copyEl = e.target.closest("[data-copy]");
+ if (copyEl) {
+ navigator.clipboard.writeText(copyEl.dataset.copy);
+ showFlash("Copied!");
+ }
+ });
+
$("btn-address-token-back").addEventListener("click", () => {
ctx.showAddressDetail();
});
diff --git a/src/popup/views/approval.js b/src/popup/views/approval.js
index 359b506..58319ed 100644
--- a/src/popup/views/approval.js
+++ b/src/popup/views/approval.js
@@ -453,4 +453,4 @@ function init(ctx) {
});
}
-module.exports = { init, show };
+module.exports = { init, show, decodeCalldata };
diff --git a/src/popup/views/deleteWallet.js b/src/popup/views/deleteWallet.js
new file mode 100644
index 0000000..49e47bf
--- /dev/null
+++ b/src/popup/views/deleteWallet.js
@@ -0,0 +1,90 @@
+const { $, showView, showFlash } = require("./helpers");
+const { state, saveState } = require("../../shared/state");
+const { decryptWithPassword } = require("../../shared/vault");
+
+let deleteWalletIndex = null;
+let ctx = null;
+
+function show(walletIdx) {
+ deleteWalletIndex = walletIdx;
+ const wallet = state.wallets[walletIdx];
+ $("delete-wallet-name").textContent =
+ wallet.name || "Wallet " + (walletIdx + 1);
+ $("delete-wallet-password").value = "";
+ $("delete-wallet-flash").textContent = "";
+ $("delete-wallet-flash").classList.add("hidden");
+ showView("delete-wallet-confirm");
+}
+
+function init(_ctx) {
+ ctx = _ctx;
+
+ $("btn-delete-wallet-back").addEventListener("click", () => {
+ deleteWalletIndex = null;
+ ctx.showSettingsView();
+ });
+
+ $("btn-delete-wallet-confirm").addEventListener("click", async () => {
+ const pw = $("delete-wallet-password").value;
+ if (!pw) {
+ $("delete-wallet-flash").textContent =
+ "Please enter your password.";
+ $("delete-wallet-flash").classList.remove("hidden");
+ return;
+ }
+
+ if (deleteWalletIndex === null) {
+ $("delete-wallet-flash").textContent =
+ "No wallet selected for deletion.";
+ $("delete-wallet-flash").classList.remove("hidden");
+ return;
+ }
+
+ const walletIdx = deleteWalletIndex;
+ const wallet = state.wallets[walletIdx];
+
+ // Verify password against the wallet's encrypted data
+ try {
+ await decryptWithPassword(wallet.encryptedSecret, pw);
+ } catch (_e) {
+ $("delete-wallet-flash").textContent = "Wrong password.";
+ $("delete-wallet-flash").classList.remove("hidden");
+ return;
+ }
+
+ // Collect addresses to clean up from allowedSites/deniedSites
+ const addresses = (wallet.addresses || []).map((a) => a.address);
+
+ // Remove wallet
+ state.wallets.splice(walletIdx, 1);
+
+ // Clean up site permissions for deleted addresses
+ for (const addr of addresses) {
+ delete state.allowedSites[addr];
+ delete state.deniedSites[addr];
+ }
+
+ deleteWalletIndex = null;
+
+ if (state.wallets.length === 0) {
+ // No wallets left — reset selection and show welcome
+ state.selectedWallet = null;
+ state.selectedAddress = null;
+ state.activeAddress = null;
+ await saveState();
+ showView("welcome");
+ } else {
+ // Switch to first wallet if deleted wallet was active
+ state.selectedWallet = 0;
+ state.selectedAddress = 0;
+ state.activeAddress =
+ state.wallets[0].addresses[0]?.address || null;
+ await saveState();
+ ctx.renderWalletList();
+ ctx.showSettingsView();
+ showFlash("Wallet deleted.");
+ }
+ });
+}
+
+module.exports = { init, show };
diff --git a/src/popup/views/helpers.js b/src/popup/views/helpers.js
index 5d9daa8..e5b71d0 100644
--- a/src/popup/views/helpers.js
+++ b/src/popup/views/helpers.js
@@ -25,6 +25,7 @@ const VIEWS = [
"receive",
"add-token",
"settings",
+ "delete-wallet-confirm",
"settings-addtoken",
"transaction",
"approve-site",
diff --git a/src/popup/views/home.js b/src/popup/views/home.js
index 62b352b..78fbff2 100644
--- a/src/popup/views/home.js
+++ b/src/popup/views/home.js
@@ -102,7 +102,10 @@ function renderHomeTxList(ctx) {
let html = "";
let i = 0;
for (const tx of homeTxs) {
- const counterparty = tx.direction === "sent" ? tx.to : tx.from;
+ const counterparty =
+ tx.direction === "sent" || tx.direction === "contract"
+ ? tx.to
+ : tx.from;
const dirLabel = tx.directionLabel;
const amountStr = tx.value
? escapeHtml(tx.value + " " + tx.symbol)
diff --git a/src/popup/views/settings.js b/src/popup/views/settings.js
index 8562b96..ea67337 100644
--- a/src/popup/views/settings.js
+++ b/src/popup/views/settings.js
@@ -2,6 +2,7 @@ const { $, showView, showFlash, escapeHtml } = require("./helpers");
const { state, saveState } = require("../../shared/state");
const { ETHEREUM_MAINNET_CHAIN_ID } = require("../../shared/constants");
const { log, debugFetch } = require("../../shared/log");
+const deleteWallet = require("./deleteWallet");
const runtime =
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
@@ -65,11 +66,68 @@ function renderTrackedTokens() {
});
}
+function renderWalletListSettings() {
+ const container = $("settings-wallet-list");
+ if (state.wallets.length === 0) {
+ container.innerHTML = '
No wallets.
';
+ return;
+ }
+ let html = "";
+ state.wallets.forEach((wallet, idx) => {
+ const name = escapeHtml(wallet.name || "Wallet " + (idx + 1));
+ html += `
`;
+ html += `${name}`;
+ html += ``;
+ html += `
`;
+ });
+ container.innerHTML = html;
+ container.querySelectorAll(".btn-delete-wallet").forEach((btn) => {
+ btn.addEventListener("click", () => {
+ const idx = parseInt(btn.dataset.idx, 10);
+ deleteWallet.show(idx);
+ });
+ });
+
+ // Inline rename on click
+ container.querySelectorAll(".settings-wallet-name").forEach((span) => {
+ span.addEventListener("click", () => {
+ const idx = parseInt(span.dataset.idx, 10);
+ const wallet = state.wallets[idx];
+ const input = document.createElement("input");
+ input.type = "text";
+ input.className =
+ "border border-border p-0 text-xs bg-bg text-fg w-full";
+ input.value = wallet.name || "Wallet " + (idx + 1);
+ span.replaceWith(input);
+ input.focus();
+ input.select();
+ const finish = async () => {
+ const val = input.value.trim();
+ if (val && val !== wallet.name) {
+ wallet.name = val;
+ await saveState();
+ }
+ renderWalletListSettings();
+ };
+ input.addEventListener("blur", finish);
+ input.addEventListener("keydown", (e) => {
+ if (e.key === "Enter") input.blur();
+ if (e.key === "Escape") {
+ input.value = wallet.name || "Wallet " + (idx + 1);
+ input.blur();
+ }
+ });
+ });
+ });
+}
+
function show() {
$("settings-rpc").value = state.rpcUrl;
$("settings-blockscout").value = state.blockscoutUrl;
renderTrackedTokens();
renderSiteLists();
+ renderWalletListSettings();
+
showView("settings");
}
@@ -83,6 +141,8 @@ function renderSiteLists() {
}
function init(ctx) {
+ deleteWallet.init(ctx);
+
$("btn-save-rpc").addEventListener("click", async () => {
const url = $("settings-rpc").value.trim();
if (!url) {
diff --git a/src/popup/views/transactionDetail.js b/src/popup/views/transactionDetail.js
index 8ecbfe3..1091005 100644
--- a/src/popup/views/transactionDetail.js
+++ b/src/popup/views/transactionDetail.js
@@ -13,6 +13,8 @@ const {
} = require("./helpers");
const { state } = require("../../shared/state");
const makeBlockie = require("ethereum-blockies-base64");
+const { log, debugFetch } = require("../../shared/log");
+const { decodeCalldata } = require("./approval");
const EXT_ICON =
`
` +
@@ -42,11 +44,11 @@ function txAddressHtml(address, ensName, title) {
const extLink = `${EXT_ICON}`;
let html = `${blockie}
`;
if (title) {
- html += `${dot}${escapeHtml(title)}
`;
+ html += `${escapeHtml(title)}
`;
}
if (ensName) {
html +=
- `${title ? "" : dot}` +
+ `
${dot}` +
copyableHtml(ensName, "") +
extLink +
`
` +
@@ -55,7 +57,7 @@ function txAddressHtml(address, ensName, title) {
`
`;
} else {
html +=
- `${title ? "" : dot}` +
+ `
${dot}` +
copyableHtml(address, "break-all") +
extLink +
`
`;
@@ -85,9 +87,15 @@ function show(tx) {
fromEns: tx.fromEns || null,
toEns: tx.toEns || null,
directionLabel: tx.directionLabel || null,
+ direction: tx.direction || null,
+ isContractCall: tx.isContractCall || false,
+ method: tx.method || null,
},
};
render();
+ if (tx.isContractCall || tx.direction === "contract") {
+ loadCalldata(tx.hash, tx.to);
+ }
}
function render() {
@@ -121,6 +129,25 @@ function render() {
nativeEl.parentElement.classList.add("hidden");
}
+ // Show type label for contract interactions (Swap, Execute, etc.)
+ const typeSection = $("tx-detail-type-section");
+ const typeEl = $("tx-detail-type");
+ const headingEl = $("tx-detail-heading");
+ if (tx.direction === "contract" && tx.directionLabel) {
+ if (typeSection) {
+ typeEl.textContent = tx.directionLabel;
+ typeSection.classList.remove("hidden");
+ }
+ if (headingEl) headingEl.textContent = tx.directionLabel;
+ } else {
+ if (typeSection) typeSection.classList.add("hidden");
+ if (headingEl) headingEl.textContent = "Transaction";
+ }
+
+ // Hide calldata section by default; loadCalldata will show it if needed
+ const calldataSection = $("tx-detail-calldata-section");
+ if (calldataSection) calldataSection.classList.add("hidden");
+
$("tx-detail-time").textContent =
isoDate(tx.timestamp) + " (" + timeAgo(tx.timestamp) + ")";
$("tx-detail-status").textContent = tx.isError ? "Failed" : "Success";
@@ -137,6 +164,73 @@ function render() {
});
}
+async function loadCalldata(txHash, toAddress) {
+ const section = $("tx-detail-calldata-section");
+ const actionEl = $("tx-detail-calldata-action");
+ const detailsEl = $("tx-detail-calldata-details");
+ const wellEl = $("tx-detail-calldata-well");
+ const rawSection = $("tx-detail-rawdata-section");
+ const rawEl = $("tx-detail-rawdata");
+ if (!section || !actionEl || !detailsEl) return;
+
+ try {
+ const resp = await debugFetch(
+ state.blockscoutUrl + "/transactions/" + txHash,
+ );
+ if (!resp.ok) return;
+ const txData = await resp.json();
+ const inputData = txData.raw_input || txData.input || null;
+ if (!inputData || inputData === "0x") return;
+
+ const decoded = decodeCalldata(inputData, toAddress || "");
+ if (decoded) {
+ // Render decoded calldata matching approval view style
+ actionEl.textContent = decoded.name;
+ let detailsHtml = "";
+ if (decoded.description) {
+ detailsHtml += `
${escapeHtml(decoded.description)}
`;
+ }
+ for (const d of decoded.details || []) {
+ detailsHtml += `
`;
+ detailsHtml += `
${escapeHtml(d.label)}
`;
+ if (d.address) {
+ const dot = addressDotHtml(d.address);
+ detailsHtml += `
${dot}${copyableHtml(d.value, "break-all")}
`;
+ } else {
+ detailsHtml += `
${escapeHtml(d.value)}
`;
+ }
+ detailsHtml += `
`;
+ }
+ detailsEl.innerHTML = detailsHtml;
+ if (wellEl) wellEl.classList.remove("hidden");
+ } else {
+ // Unknown contract call — show method name in well
+ const method = txData.method || "Unknown contract call";
+ actionEl.textContent = method;
+ detailsEl.innerHTML = "";
+ if (wellEl) wellEl.classList.remove("hidden");
+ }
+
+ // Always show raw data
+ if (rawSection && rawEl) {
+ rawEl.innerHTML = copyableHtml(inputData, "break-all");
+ rawSection.classList.remove("hidden");
+ }
+
+ section.classList.remove("hidden");
+
+ // Bind copy handlers for new elements
+ section.querySelectorAll("[data-copy]").forEach((el) => {
+ el.onclick = () => {
+ navigator.clipboard.writeText(el.dataset.copy);
+ showFlash("Copied!");
+ };
+ });
+ } catch (e) {
+ log.errorf("loadCalldata failed:", e.message);
+ }
+}
+
function init(_ctx) {
ctx = _ctx;
$("btn-tx-back").addEventListener("click", () => {
diff --git a/src/shared/transactions.js b/src/shared/transactions.js
index f926784..e12d310 100644
--- a/src/shared/transactions.js
+++ b/src/shared/transactions.js
@@ -37,7 +37,21 @@ function parseTx(tx, addrLower) {
if (token) {
symbol = token.symbol;
}
- const label = method.charAt(0).toUpperCase() + method.slice(1);
+ // Map known DEX methods to "Swap" for cleaner display
+ const SWAP_METHODS = new Set([
+ "execute",
+ "swap",
+ "swapExactTokensForTokens",
+ "swapTokensForExactTokens",
+ "swapExactETHForTokens",
+ "swapTokensForExactETH",
+ "swapExactTokensForETH",
+ "swapETHForExactTokens",
+ "multicall",
+ ]);
+ const label = SWAP_METHODS.has(method)
+ ? "Swap"
+ : method.charAt(0).toUpperCase() + method.slice(1);
direction = "contract";
directionLabel = label;
value = "";
@@ -139,9 +153,18 @@ async function fetchRecentTransactions(address, blockscoutUrl, count = 25) {
// When a token transfer shares a hash with a normal tx, the normal tx
// is the contract call (0 ETH) and the token transfer has the real
- // amount and symbol. Replace the normal tx with the token transfer.
+ // amount and symbol. Replace the normal tx with the token transfer,
+ // but preserve contract call metadata (direction, label, method) so
+ // swaps and other contract interactions display correctly.
for (const tt of ttJson.items || []) {
const parsed = parseTokenTransfer(tt, addrLower);
+ const existing = txsByHash.get(parsed.hash);
+ if (existing && existing.direction === "contract") {
+ parsed.direction = "contract";
+ parsed.directionLabel = existing.directionLabel;
+ parsed.isContractCall = true;
+ parsed.method = existing.method;
+ }
txsByHash.set(parsed.hash, parsed);
}