Compare commits

..

4 Commits

Author SHA1 Message Date
cabf8311e5 Merge branch 'main' into fix/low-severity-security
All checks were successful
check / check (push) Successful in 22s
2026-02-27 20:58:09 +01:00
fbcb679bcf fix(L5): truncate token name/symbol from RPC responses
All checks were successful
check / check (push) Successful in 22s
Limits token name to 64 chars and symbol to 12 chars to prevent
storage of excessively long values from malicious contracts.
2026-02-27 11:42:18 -08:00
aa15f771d5 fix(L4): generate EIP-6963 provider UUID at install time
UUID is generated once via crypto.randomUUID(), persisted in
chrome.storage.local, and sent from the content script to the
inpage script via postMessage.
2026-02-27 11:42:15 -08:00
571f2d6906 fix(L3): isUnlocked() returns false when no accounts exposed
_metamask.isUnlocked() now checks provider.selectedAddress instead of
always returning true.
2026-02-27 11:42:11 -08:00
21 changed files with 75 additions and 715 deletions

View File

@@ -213,22 +213,6 @@ create an address with the same visible characters and trick the user into
sending funds to it. Showing the complete identifier defeats this class of sending funds to it. Showing the complete identifier defeats this class of
attack. attack.
#### Clipboard Policy
AutistMask never clears or overwrites the user's clipboard. When sensitive data
such as a private key is copied, it is the user's responsibility to manage their
clipboard afterwards. We deliberately avoid auto-clearing the clipboard for two
reasons:
1. **User expectations**: silently modifying the clipboard violates the
principle of least surprise. The user initiated the copy and knows the
content is sensitive.
2. **Data safety**: the user may have copied something else important in the
intervening time. A timed clipboard clear would destroy that unrelated data.
The warning shown before revealing a private key makes it clear that the key is
sensitive and that clipboard management is the user's responsibility.
#### Data Model #### Data Model
The core hierarchy is **Wallets → Addresses**: The core hierarchy is **Wallets → Addresses**:
@@ -332,34 +316,15 @@ transitions.
- Balance list: ETH + tracked ERC-20 tokens (4 decimal places, USD inline). - Balance list: ETH + tracked ERC-20 tokens (4 decimal places, USD inline).
Each balance row is clickable → **AddressToken** Each balance row is clickable → **AddressToken**
- Send / Receive / + Token buttons - Send / Receive / + Token buttons
- "Show private key" button
- Transaction list (with ENS resolution for counterparties) - Transaction list (with ENS resolution for counterparties)
- **Transitions**: - **Transitions**:
- Tap balance row → **AddressToken** (for that token) - Tap balance row → **AddressToken** (for that token)
- "Send" → **Send** - "Send" → **Send**
- "Receive" → **Receive** - "Receive" → **Receive**
- "+ Token" → **AddToken** - "+ Token" → **AddToken**
- "Show private key" → **ShowPrivateKey**
- Tap transaction row → **TransactionDetail** - Tap transaction row → **TransactionDetail**
- "Back" → **Home** - "Back" → **Home**
#### ShowPrivateKey
- **When**: User clicked "Show private key" on AddressDetail.
- **Elements**:
- "Back" button
- Title: "Display Private Key"
- Warning box (lock + money icons) explaining the key controls funds and
that the user is responsible for clipboard management
- Password input
- "Display Private Key" button (with lock + money icons)
- After reveal: private key in a read-only well (monospace, select-all),
Copy button, Done button
- **Transitions**:
- "Display Private Key" (correct password) → reveals key in-place
- "Copy" → copies key to clipboard
- "Done" / "Back" → **AddressDetail** (key cleared from DOM)
#### AddressToken #### AddressToken
- **When**: User clicked a specific token balance on AddressDetail. - **When**: User clicked a specific token balance on AddressDetail.

View File

@@ -93,13 +93,11 @@ function resetPopupUrl() {
} }
} }
// Open approval in a separate popup window. // Fallback: open approval in a separate window (used when openPopup is unavailable)
// This is the primary mechanism for tx/sign approvals (triggered programmatically,
// not from a user gesture) and the fallback for site-connection approvals.
function openApprovalWindow(id) { function openApprovalWindow(id) {
const popupUrl = runtime.getURL("src/popup/index.html?approval=" + id); const popupUrl = runtime.getURL("src/popup/index.html?approval=" + id);
const popupWidth = 360; const popupWidth = 400;
const popupHeight = 600; const popupHeight = 500;
windowsApi.getLastFocused((currentWin) => { windowsApi.getLastFocused((currentWin) => {
const opts = { const opts = {
@@ -150,9 +148,7 @@ function requestApproval(origin, hostname) {
} }
// Open a tx-approval popup and return a promise that resolves with txHash or error. // Open a tx-approval popup and return a promise that resolves with txHash or error.
// Uses windows.create() directly because tx approvals are triggered programmatically // Uses the toolbar popup only — no fallback window.
// (from a dApp RPC call), not from a user gesture, so action.openPopup() is
// unreliable in this context.
function requestTxApproval(origin, hostname, txParams) { function requestTxApproval(origin, hostname, txParams) {
return new Promise((resolve) => { return new Promise((resolve) => {
const id = crypto.randomUUID(); const id = crypto.randomUUID();
@@ -164,14 +160,27 @@ function requestTxApproval(origin, hostname, txParams) {
type: "tx", type: "tx",
}; };
openApprovalWindow(id); if (actionApi && typeof actionApi.setPopup === "function") {
actionApi.setPopup({
popup: "src/popup/index.html?approval=" + id,
});
}
if (actionApi && typeof actionApi.openPopup === "function") {
try {
const result = actionApi.openPopup();
if (result && typeof result.catch === "function") {
result.catch(() => {});
}
} catch {
// openPopup unsupported — user clicks toolbar icon
}
}
}); });
} }
// Open a sign-approval popup and return a promise that resolves with { signature } or { error }. // Open a sign-approval popup and return a promise that resolves with { signature } or { error }.
// Uses windows.create() directly because sign approvals are triggered programmatically // Uses the toolbar popup only — no fallback window. If openPopup() fails the
// (from a dApp RPC call), not from a user gesture, so action.openPopup() is // popup URL is still set, so the user can click the toolbar icon to respond.
// unreliable in this context.
function requestSignApproval(origin, hostname, signParams) { function requestSignApproval(origin, hostname, signParams) {
return new Promise((resolve) => { return new Promise((resolve) => {
const id = crypto.randomUUID(); const id = crypto.randomUUID();
@@ -183,14 +192,27 @@ function requestSignApproval(origin, hostname, signParams) {
type: "sign", type: "sign",
}; };
openApprovalWindow(id); if (actionApi && typeof actionApi.setPopup === "function") {
actionApi.setPopup({
popup: "src/popup/index.html?approval=" + id,
});
}
if (actionApi && typeof actionApi.openPopup === "function") {
try {
const result = actionApi.openPopup();
if (result && typeof result.catch === "function") {
result.catch(() => {});
}
} catch {
// openPopup unsupported — user clicks toolbar icon
}
}
}); });
} }
// Detect when an approval popup (browser-action) closes without a response. // Detect when an approval popup (browser-action) closes without a response.
// TX and sign approvals now use windows.create() and are handled by the // TX and sign approvals are NOT auto-rejected on disconnect because toolbar
// windowsApi.onRemoved listener below, but we still handle site-connection // popups naturally close on focus loss and the user can reopen them.
// approval disconnects here.
runtime.onConnect.addListener((port) => { runtime.onConnect.addListener((port) => {
if (port.name.startsWith("approval:")) { if (port.name.startsWith("approval:")) {
const id = port.name.split(":")[1]; const id = port.name.split(":")[1];

View File

@@ -26,10 +26,7 @@ if (typeof browser !== "undefined") {
uuid = crypto.randomUUID(); uuid = crypto.randomUUID();
storage.set({ eip6963Uuid: uuid }); storage.set({ eip6963Uuid: uuid });
} }
window.postMessage( window.postMessage({ type: "AUTISTMASK_PROVIDER_UUID", uuid }, "*");
{ type: "AUTISTMASK_PROVIDER_UUID", uuid },
location.origin,
);
}); });
})(); })();

View File

@@ -9,7 +9,7 @@
const pending = {}; const pending = {};
// Listen for responses from the content script // Listen for responses from the content script
window.addEventListener("message", function onUuid(event) { window.addEventListener("message", (event) => {
if (event.source !== window) return; if (event.source !== window) return;
if (event.data?.type !== "AUTISTMASK_RESPONSE") return; if (event.data?.type !== "AUTISTMASK_RESPONSE") return;
const { id, result, error } = event.data; const { id, result, error } = event.data;
@@ -24,7 +24,7 @@
}); });
// Listen for events pushed from the extension // Listen for events pushed from the extension
window.addEventListener("message", function onUuid(event) { window.addEventListener("message", (event) => {
if (event.source !== window) return; if (event.source !== window) return;
if (event.data?.type !== "AUTISTMASK_EVENT") return; if (event.data?.type !== "AUTISTMASK_EVENT") return;
const { eventName, data } = event.data; const { eventName, data } = event.data;
@@ -178,14 +178,12 @@
} }
// Listen for the persisted UUID from the content script // Listen for the persisted UUID from the content script
function onProviderUuid(event) { window.addEventListener("message", (event) => {
if (event.source !== window) return; if (event.source !== window) return;
if (event.data?.type !== "AUTISTMASK_PROVIDER_UUID") return; if (event.data?.type !== "AUTISTMASK_PROVIDER_UUID") return;
window.removeEventListener("message", onProviderUuid);
providerUuid = event.data.uuid; providerUuid = event.data.uuid;
announceProvider(); announceProvider();
} });
window.addEventListener("message", onProviderUuid);
window.addEventListener("eip6963:requestProvider", announceProvider); window.addEventListener("eip6963:requestProvider", announceProvider);
announceProvider(); announceProvider();

View File

@@ -307,15 +307,6 @@
</button> </button>
</div> </div>
<div class="mb-3">
<button
id="btn-show-private-key"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer text-xs"
>
&#128274; Show private key
</button>
</div>
<!-- transactions --> <!-- transactions -->
<div class="mt-3"> <div class="mt-3">
<div class="border-b border-border pb-1 mb-1"> <div class="border-b border-border pb-1 mb-1">
@@ -327,77 +318,6 @@
</div> </div>
</div> </div>
<!-- ============ SHOW PRIVATE KEY ============ -->
<div id="view-show-private-key" class="view hidden">
<button
id="btn-show-pk-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mb-2"
>
&lt; Back
</button>
<h2 class="font-bold mb-2">Display Private Key</h2>
<!-- password prompt section -->
<div id="show-pk-prompt">
<div
class="border border-border border-dashed p-3 mb-3 text-xs"
>
<p class="mb-1">
&#128274;&#128176; Your private key controls this
address and all its funds. Anyone who has it can
spend your tokens.
</p>
<p>
Do not share it. Do not paste it into websites. If
you copy it, you are responsible for clearing your
clipboard when you are done.
</p>
</div>
<div class="mb-2">
<label class="block mb-1">Password</label>
<input
type="password"
id="show-pk-password"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="Enter your password"
/>
</div>
<div
id="show-pk-error"
class="text-xs mb-2 border border-border border-dashed p-1 hidden"
></div>
<button
id="btn-show-pk-reveal"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
&#128274;&#128176; Display Private Key
</button>
</div>
<!-- revealed key section -->
<div id="show-pk-key-well" class="hidden">
<div
class="bg-well p-3 mx-1 mb-3 break-all font-mono text-xs select-all"
>
<span id="show-pk-key-value"></span>
</div>
<div class="flex gap-2">
<button
id="btn-show-pk-copy"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
Copy
</button>
<button
id="btn-show-pk-done"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
Done
</button>
</div>
</div>
</div>
<!-- ============ ADDRESS-TOKEN DETAIL VIEW ============ --> <!-- ============ ADDRESS-TOKEN DETAIL VIEW ============ -->
<div id="view-address-token" class="view hidden"> <div id="view-address-token" class="view hidden">
<button <button
@@ -454,12 +374,6 @@
</button> </button>
</div> </div>
<!-- token contract details (ERC-20 only) -->
<div
id="address-token-contract-info"
class="bg-hover rounded-md mx-1 p-3 mb-3 text-xs hidden"
></div>
<!-- token-filtered transactions --> <!-- token-filtered transactions -->
<div class="mt-3"> <div class="mt-3">
<div class="border-b border-border pb-1 mb-1"> <div class="border-b border-border pb-1 mb-1">
@@ -788,7 +702,9 @@
<div class="bg-well p-3 mx-1 mb-3"> <div class="bg-well p-3 mx-1 mb-3">
<h3 class="font-bold mb-1">Wallets</h3> <h3 class="font-bold mb-1">Wallets</h3>
<div id="settings-wallet-list" class="mb-2"></div> <p class="text-xs text-muted mb-2">
Add a new wallet from a recovery phrase or private key.
</p>
<button <button
id="btn-main-add-wallet" id="btn-main-add-wallet"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer" class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
@@ -923,41 +839,6 @@
</div> </div>
</div> </div>
<!-- ============ DELETE WALLET CONFIRM ============ -->
<div id="view-delete-wallet-confirm" class="view hidden">
<button
id="btn-delete-wallet-back"
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer mb-2"
>
&lt; Back
</button>
<h2 class="font-bold mb-3">Delete Wallet</h2>
<p class="text-xs mb-3">
Deleting
<strong id="delete-wallet-name"></strong> is permanent. Any
funds will be unrecoverable without your recovery phrase.
</p>
<div
id="delete-wallet-flash"
class="text-xs text-red-500 mb-2 hidden"
></div>
<div class="mb-2">
<label class="block mb-1">Password</label>
<input
type="password"
id="delete-wallet-password"
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
placeholder="Enter your password to confirm"
/>
</div>
<button
id="btn-delete-wallet-confirm"
class="border border-border text-red-500 px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
>
Confirm Delete
</button>
</div>
<!-- ============ SETTINGS: ADD TOKEN ============ --> <!-- ============ SETTINGS: ADD TOKEN ============ -->
<div id="view-settings-addtoken" class="view hidden"> <div id="view-settings-addtoken" class="view hidden">
<button <button
@@ -1033,13 +914,7 @@
> >
&lt; Back &lt; Back
</button> </button>
<h2 id="tx-detail-heading" class="font-bold mb-2"> <h2 class="font-bold mb-2">Transaction</h2>
Transaction
</h2>
<div id="tx-detail-type-section" class="mb-4 hidden">
<div class="text-xs text-muted mb-1">Type</div>
<div id="tx-detail-type" class="text-xs font-bold"></div>
</div>
<div class="mb-4"> <div class="mb-4">
<div class="text-xs text-muted mb-1">Status</div> <div class="text-xs text-muted mb-1">Status</div>
<div id="tx-detail-status" class="text-xs"></div> <div id="tx-detail-status" class="text-xs"></div>
@@ -1064,29 +939,6 @@
<div class="text-xs text-muted mb-1">To</div> <div class="text-xs text-muted mb-1">To</div>
<div id="tx-detail-to" class="text-xs break-all"></div> <div id="tx-detail-to" class="text-xs break-all"></div>
</div> </div>
<div id="tx-detail-calldata-section" class="mb-4 hidden">
<div
id="tx-detail-calldata-well"
class="mb-3 border border-border border-dashed p-2"
>
<div class="text-xs text-muted mb-1">Action</div>
<div
id="tx-detail-calldata-action"
class="text-xs font-bold mb-2"
></div>
<div
id="tx-detail-calldata-details"
class="text-xs"
></div>
</div>
<div id="tx-detail-rawdata-section" class="hidden">
<div class="text-xs text-muted mb-1">Raw data</div>
<div
id="tx-detail-rawdata"
class="text-xs break-all font-mono border border-border border-dashed p-2"
></div>
</div>
</div>
<div class="mb-4"> <div class="mb-4">
<div class="text-xs text-muted mb-1">Transaction hash</div> <div class="text-xs text-muted mb-1">Transaction hash</div>
<div id="tx-detail-hash" class="text-xs break-all"></div> <div id="tx-detail-hash" class="text-xs break-all"></div>

View File

@@ -19,7 +19,6 @@ const txStatus = require("./views/txStatus");
const transactionDetail = require("./views/transactionDetail"); const transactionDetail = require("./views/transactionDetail");
const receive = require("./views/receive"); const receive = require("./views/receive");
const addToken = require("./views/addToken"); const addToken = require("./views/addToken");
const showPrivateKey = require("./views/showPrivateKey");
const settings = require("./views/settings"); const settings = require("./views/settings");
const settingsAddToken = require("./views/settingsAddToken"); const settingsAddToken = require("./views/settingsAddToken");
const approval = require("./views/approval"); const approval = require("./views/approval");
@@ -57,7 +56,6 @@ const ctx = {
showAddWalletView: () => addWallet.show(), showAddWalletView: () => addWallet.show(),
showImportKeyView: () => importKey.show(), showImportKeyView: () => importKey.show(),
showAddressDetail: () => addressDetail.show(), showAddressDetail: () => addressDetail.show(),
showPrivateKey: () => showPrivateKey.show(),
showAddressToken: () => addressToken.show(), showAddressToken: () => addressToken.show(),
showAddTokenView: () => addToken.show(), showAddTokenView: () => addToken.show(),
showConfirmTx: (txInfo) => confirmTx.show(txInfo), showConfirmTx: (txInfo) => confirmTx.show(txInfo),
@@ -191,7 +189,7 @@ async function init() {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const approvalId = params.get("approval"); const approvalId = params.get("approval");
if (approvalId) { if (approvalId) {
approval.show(approvalId); approval.show(parseInt(approvalId, 10));
showView("approve-site"); showView("approve-site");
return; return;
} }
@@ -214,7 +212,6 @@ async function init() {
importKey.init(ctx); importKey.init(ctx);
home.init(ctx); home.init(ctx);
addressDetail.init(ctx); addressDetail.init(ctx);
showPrivateKey.init(ctx);
addressToken.init(ctx); addressToken.init(ctx);
send.init(ctx); send.init(ctx);
confirmTx.init(ctx); confirmTx.init(ctx);

View File

@@ -4,7 +4,6 @@ const {
showFlash, showFlash,
balanceLinesForAddress, balanceLinesForAddress,
addressDotHtml, addressDotHtml,
addressTitle,
escapeHtml, escapeHtml,
truncateMiddle, truncateMiddle,
} = require("./helpers"); } = require("./helpers");
@@ -151,11 +150,11 @@ async function loadTransactions(address) {
loadedTxs = txs; loadedTxs = txs;
// Collect ALL unique addresses (from + to) for ENS resolution so // Collect unique counterparty addresses for ENS resolution.
// that reverse lookups work for every displayed address, not just
// the ones that were originally entered as ENS names.
const counterparties = [ const counterparties = [
...new Set(txs.flatMap((tx) => [tx.from, tx.to].filter(Boolean))), ...new Set(
txs.map((tx) => (tx.direction === "sent" ? tx.to : tx.from)),
),
]; ];
if (counterparties.length > 0) { if (counterparties.length > 0) {
try { try {
@@ -186,19 +185,14 @@ function renderTransactions(txs) {
let html = ""; let html = "";
let i = 0; let i = 0;
for (const tx of txs) { for (const tx of txs) {
const counterparty = const counterparty = tx.direction === "sent" ? tx.to : tx.from;
tx.direction === "sent" || tx.direction === "contract"
? tx.to
: tx.from;
const ensName = ensNameMap.get(counterparty) || null; const ensName = ensNameMap.get(counterparty) || null;
const title = addressTitle(counterparty, state.wallets);
const dirLabel = tx.directionLabel; const dirLabel = tx.directionLabel;
const amountStr = tx.value const amountStr = tx.value
? escapeHtml(tx.value + " " + tx.symbol) ? escapeHtml(tx.value + " " + tx.symbol)
: escapeHtml(tx.symbol); : escapeHtml(tx.symbol);
const maxAddr = Math.max(32, 36 - Math.max(0, amountStr.length - 10)); const maxAddr = Math.max(32, 36 - Math.max(0, amountStr.length - 10));
const displayAddr = const displayAddr = ensName || truncateMiddle(counterparty, maxAddr);
title || ensName || truncateMiddle(counterparty, maxAddr);
const addrStr = escapeHtml(displayAddr); const addrStr = escapeHtml(displayAddr);
const dot = addressDotHtml(counterparty); const dot = addressDotHtml(counterparty);
const err = tx.isError ? " (failed)" : ""; const err = tx.isError ? " (failed)" : "";
@@ -261,10 +255,6 @@ function init(_ctx) {
}); });
$("btn-add-token").addEventListener("click", ctx.showAddTokenView); $("btn-add-token").addEventListener("click", ctx.showAddTokenView);
$("btn-show-private-key").addEventListener("click", () => {
ctx.showPrivateKey();
});
} }
module.exports = { init, show }; module.exports = { init, show };

View File

@@ -6,13 +6,11 @@ const {
showView, showView,
showFlash, showFlash,
addressDotHtml, addressDotHtml,
addressTitle,
escapeHtml, escapeHtml,
truncateMiddle, truncateMiddle,
balanceLine, balanceLine,
} = require("./helpers"); } = require("./helpers");
const { state, currentAddress, saveState } = require("../../shared/state"); const { state, currentAddress, saveState } = require("../../shared/state");
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
const { const {
formatUsd, formatUsd,
getPrice, getPrice,
@@ -132,43 +130,6 @@ function show() {
// Single token balance line (no tokenId — not clickable here) // Single token balance line (no tokenId — not clickable here)
$("address-token-balance").innerHTML = balanceLine(symbol, amount, price); $("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 = `<div class="font-bold mb-2">Contract Address</div>`;
infoHtml +=
`<div class="flex items-center mb-2">${dot}` +
`<span class="break-all underline decoration-dashed cursor-pointer" id="address-token-contract-copy" data-copy="${escapeHtml(tokenId)}">${escapeHtml(tokenId)}</span>` +
`<a href="${tokenLink}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>` +
`</div>`;
if (tokenName)
infoHtml += `<div class="mb-1"><span class="text-muted">Name:</span> ${tokenName}</div>`;
if (tokenSymbol)
infoHtml += `<div class="mb-1"><span class="text-muted">Symbol:</span> ${tokenSymbol}</div>`;
if (tokenDecimals != null)
infoHtml += `<div class="mb-1"><span class="text-muted">Decimals:</span> ${tokenDecimals}</div>`;
if (tokenHolders != null)
infoHtml += `<div class="mb-1"><span class="text-muted">Holders:</span> ${Number(tokenHolders).toLocaleString()}</div>`;
if (projectUrl)
infoHtml += `<div class="mb-1"><span class="text-muted">Website:</span> <a href="${escapeHtml(projectUrl)}" target="_blank" rel="noopener" class="underline decoration-dashed">${escapeHtml(projectUrl)}</a></div>`;
contractInfo.innerHTML = infoHtml;
contractInfo.classList.remove("hidden");
} else {
contractInfo.innerHTML = "";
contractInfo.classList.add("hidden");
}
// Transactions // Transactions
$("address-token-tx-list").innerHTML = $("address-token-tx-list").innerHTML =
'<div class="text-muted text-xs py-1">Loading...</div>'; '<div class="text-muted text-xs py-1">Loading...</div>';
@@ -214,10 +175,11 @@ async function loadTransactions(address, tokenId) {
loadedTxs = txs; loadedTxs = txs;
// Collect ALL unique addresses for ENS resolution so reverse // Collect unique counterparty addresses for ENS resolution
// lookups work for every displayed address.
const counterparties = [ const counterparties = [
...new Set(txs.flatMap((tx) => [tx.from, tx.to].filter(Boolean))), ...new Set(
txs.map((tx) => (tx.direction === "sent" ? tx.to : tx.from)),
),
]; ];
if (counterparties.length > 0) { if (counterparties.length > 0) {
try { try {
@@ -250,14 +212,12 @@ function renderTransactions(txs) {
for (const tx of txs) { for (const tx of txs) {
const counterparty = tx.direction === "sent" ? tx.to : tx.from; const counterparty = tx.direction === "sent" ? tx.to : tx.from;
const ensName = ensNameMap.get(counterparty) || null; const ensName = ensNameMap.get(counterparty) || null;
const title = addressTitle(counterparty, state.wallets);
const dirLabel = tx.directionLabel; const dirLabel = tx.directionLabel;
const amountStr = tx.value const amountStr = tx.value
? escapeHtml(tx.value + " " + tx.symbol) ? escapeHtml(tx.value + " " + tx.symbol)
: escapeHtml(tx.symbol); : escapeHtml(tx.symbol);
const maxAddr = Math.max(32, 36 - Math.max(0, amountStr.length - 10)); const maxAddr = Math.max(32, 36 - Math.max(0, amountStr.length - 10));
const displayAddr = const displayAddr = ensName || truncateMiddle(counterparty, maxAddr);
title || ensName || truncateMiddle(counterparty, maxAddr);
const addrStr = escapeHtml(displayAddr); const addrStr = escapeHtml(displayAddr);
const dot = addressDotHtml(counterparty); const dot = addressDotHtml(counterparty);
const err = tx.isError ? " (failed)" : ""; const err = tx.isError ? " (failed)" : "";
@@ -292,14 +252,6 @@ 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", () => { $("btn-address-token-back").addEventListener("click", () => {
ctx.showAddressDetail(); ctx.showAddressDetail();
}); });

View File

@@ -1,10 +1,4 @@
const { const { $, addressDotHtml, escapeHtml, showView } = require("./helpers");
$,
addressDotHtml,
addressTitle,
escapeHtml,
showView,
} = require("./helpers");
const { state, saveState } = require("../../shared/state"); const { state, saveState } = require("../../shared/state");
const { formatEther, formatUnits, Interface, toUtf8String } = require("ethers"); const { formatEther, formatUnits, Interface, toUtf8String } = require("ethers");
const { ERC20_ABI } = require("../../shared/constants"); const { ERC20_ABI } = require("../../shared/constants");
@@ -28,15 +22,7 @@ function approvalAddressHtml(address) {
const dot = addressDotHtml(address); const dot = addressDotHtml(address);
const link = `https://etherscan.io/address/${address}`; const link = `https://etherscan.io/address/${address}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`; const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
const title = addressTitle(address, state.wallets); return `<div class="flex items-center">${dot}<span class="break-all">${escapeHtml(address)}</span>${extLink}</div>`;
let html = "";
if (title) {
html += `<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>`;
html += `<div class="break-all">${escapeHtml(address)}${extLink}</div>`;
} else {
html += `<div class="flex items-center">${dot}<span class="break-all">${escapeHtml(address)}</span>${extLink}</div>`;
}
return html;
} }
function formatTxValue(val) { function formatTxValue(val) {
@@ -467,4 +453,4 @@ function init(ctx) {
}); });
} }
module.exports = { init, show, decodeCalldata }; module.exports = { init, show };

View File

@@ -1,90 +0,0 @@
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 };

View File

@@ -16,7 +16,6 @@ const VIEWS = [
"import-key", "import-key",
"main", "main",
"address", "address",
"show-private-key",
"address-token", "address-token",
"send", "send",
"confirm-tx", "confirm-tx",
@@ -26,7 +25,6 @@ const VIEWS = [
"receive", "receive",
"add-token", "add-token",
"settings", "settings",
"delete-wallet-confirm",
"settings-addtoken", "settings-addtoken",
"transaction", "transaction",
"approve-site", "approve-site",

View File

@@ -6,7 +6,6 @@ const {
isoDate, isoDate,
timeAgo, timeAgo,
addressDotHtml, addressDotHtml,
addressTitle,
escapeHtml, escapeHtml,
truncateMiddle, truncateMiddle,
} = require("./helpers"); } = require("./helpers");
@@ -103,17 +102,13 @@ function renderHomeTxList(ctx) {
let html = ""; let html = "";
let i = 0; let i = 0;
for (const tx of homeTxs) { for (const tx of homeTxs) {
const counterparty = const counterparty = tx.direction === "sent" ? tx.to : tx.from;
tx.direction === "sent" || tx.direction === "contract"
? tx.to
: tx.from;
const dirLabel = tx.directionLabel; const dirLabel = tx.directionLabel;
const amountStr = tx.value const amountStr = tx.value
? escapeHtml(tx.value + " " + tx.symbol) ? escapeHtml(tx.value + " " + tx.symbol)
: escapeHtml(tx.symbol); : escapeHtml(tx.symbol);
const title = addressTitle(counterparty, state.wallets);
const maxAddr = Math.max(32, 36 - Math.max(0, amountStr.length - 10)); const maxAddr = Math.max(32, 36 - Math.max(0, amountStr.length - 10));
const displayAddr = title || truncateMiddle(counterparty, maxAddr); const displayAddr = truncateMiddle(counterparty, maxAddr);
const addrStr = escapeHtml(displayAddr); const addrStr = escapeHtml(displayAddr);
const dot = addressDotHtml(counterparty); const dot = addressDotHtml(counterparty);
const err = tx.isError ? " (failed)" : ""; const err = tx.isError ? " (failed)" : "";

View File

@@ -1,12 +1,6 @@
// Send view: collect To, Amount, Token. Then go to confirmation. // Send view: collect To, Amount, Token. Then go to confirmation.
const { const { $, showFlash, addressDotHtml, escapeHtml } = require("./helpers");
$,
showFlash,
addressDotHtml,
addressTitle,
escapeHtml,
} = require("./helpers");
const { state, currentAddress } = require("../../shared/state"); const { state, currentAddress } = require("../../shared/state");
let ctx; let ctx;
const { getProvider } = require("../../shared/balances"); const { getProvider } = require("../../shared/balances");
@@ -50,15 +44,8 @@ function updateSendBalance() {
const dot = addressDotHtml(addr.address); const dot = addressDotHtml(addr.address);
const link = `https://etherscan.io/address/${addr.address}`; const link = `https://etherscan.io/address/${addr.address}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`; const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
const title = addressTitle(addr.address, state.wallets);
let fromHtml = ""; let fromHtml = "";
if (title) { if (addr.ensName) {
fromHtml += `<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>`;
if (addr.ensName) {
fromHtml += `<div>${escapeHtml(addr.ensName)}</div>`;
}
fromHtml += `<div class="break-all">${escapeHtml(addr.address)}${extLink}</div>`;
} else if (addr.ensName) {
fromHtml += `<div class="flex items-center font-bold">${dot}${escapeHtml(addr.ensName)}</div>`; fromHtml += `<div class="flex items-center font-bold">${dot}${escapeHtml(addr.ensName)}</div>`;
fromHtml += `<div class="break-all">${escapeHtml(addr.address)}${extLink}</div>`; fromHtml += `<div class="break-all">${escapeHtml(addr.address)}${extLink}</div>`;
} else { } else {

View File

@@ -2,7 +2,6 @@ const { $, showView, showFlash, escapeHtml } = require("./helpers");
const { state, saveState } = require("../../shared/state"); const { state, saveState } = require("../../shared/state");
const { ETHEREUM_MAINNET_CHAIN_ID } = require("../../shared/constants"); const { ETHEREUM_MAINNET_CHAIN_ID } = require("../../shared/constants");
const { log, debugFetch } = require("../../shared/log"); const { log, debugFetch } = require("../../shared/log");
const deleteWallet = require("./deleteWallet");
const runtime = const runtime =
typeof browser !== "undefined" ? browser.runtime : chrome.runtime; typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
@@ -66,68 +65,11 @@ function renderTrackedTokens() {
}); });
} }
function renderWalletListSettings() {
const container = $("settings-wallet-list");
if (state.wallets.length === 0) {
container.innerHTML = '<p class="text-xs text-muted">No wallets.</p>';
return;
}
let html = "";
state.wallets.forEach((wallet, idx) => {
const name = escapeHtml(wallet.name || "Wallet " + (idx + 1));
html += `<div class="flex justify-between items-center text-xs py-1 border-b border-border-light">`;
html += `<span class="settings-wallet-name cursor-pointer underline decoration-dashed" data-idx="${idx}">${name}</span>`;
html += `<button class="btn-delete-wallet border border-border px-1 hover:bg-fg hover:text-bg cursor-pointer" data-idx="${idx}">[x]</button>`;
html += `</div>`;
});
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() { function show() {
$("settings-rpc").value = state.rpcUrl; $("settings-rpc").value = state.rpcUrl;
$("settings-blockscout").value = state.blockscoutUrl; $("settings-blockscout").value = state.blockscoutUrl;
renderTrackedTokens(); renderTrackedTokens();
renderSiteLists(); renderSiteLists();
renderWalletListSettings();
showView("settings"); showView("settings");
} }
@@ -141,8 +83,6 @@ function renderSiteLists() {
} }
function init(ctx) { function init(ctx) {
deleteWallet.init(ctx);
$("btn-save-rpc").addEventListener("click", async () => { $("btn-save-rpc").addEventListener("click", async () => {
const url = $("settings-rpc").value.trim(); const url = $("settings-rpc").value.trim();
if (!url) { if (!url) {

View File

@@ -1,79 +0,0 @@
const { $, showView, showFlash, showError, hideError } = require("./helpers");
const { state } = require("../../shared/state");
const { decryptWithPassword } = require("../../shared/vault");
const { getPrivateKeyForAddress } = require("../../shared/wallet");
let ctx;
let revealed = false;
function show() {
revealed = false;
$("show-pk-password").value = "";
$("show-pk-key-well").classList.add("hidden");
$("show-pk-key-value").textContent = "";
$("show-pk-prompt").classList.remove("hidden");
hideError("show-pk-error");
showView("show-private-key");
}
function init(_ctx) {
ctx = _ctx;
$("btn-show-pk-back").addEventListener("click", () => {
clearKey();
ctx.showAddressDetail();
});
$("btn-show-pk-reveal").addEventListener("click", async () => {
const pw = $("show-pk-password").value;
if (!pw) {
showError("show-pk-error", "Please enter your password.");
return;
}
const wallet = state.wallets[state.selectedWallet];
let decryptedSecret;
try {
decryptedSecret = await decryptWithPassword(
wallet.encryptedSecret,
pw,
);
} catch (_e) {
showError("show-pk-error", "Wrong password.");
return;
}
const privateKey = getPrivateKeyForAddress(
wallet,
state.selectedAddress,
decryptedSecret,
);
revealed = true;
$("show-pk-prompt").classList.add("hidden");
$("show-pk-key-well").classList.remove("hidden");
$("show-pk-key-value").textContent = privateKey;
hideError("show-pk-error");
});
$("btn-show-pk-copy").addEventListener("click", () => {
const key = $("show-pk-key-value").textContent;
if (key) {
navigator.clipboard.writeText(key);
showFlash("Copied!");
}
});
$("btn-show-pk-done").addEventListener("click", () => {
clearKey();
ctx.showAddressDetail();
});
}
function clearKey() {
revealed = false;
$("show-pk-key-value").textContent = "";
$("show-pk-password").value = "";
}
module.exports = { init, show };

View File

@@ -13,8 +13,6 @@ const {
} = require("./helpers"); } = require("./helpers");
const { state } = require("../../shared/state"); const { state } = require("../../shared/state");
const makeBlockie = require("ethereum-blockies-base64"); const makeBlockie = require("ethereum-blockies-base64");
const { log, debugFetch } = require("../../shared/log");
const { decodeCalldata } = require("./approval");
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">` +
@@ -44,11 +42,11 @@ function txAddressHtml(address, ensName, title) {
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`; 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>`; let html = `<div class="mb-1">${blockie}</div>`;
if (title) { if (title) {
html += `<div class="font-bold">${escapeHtml(title)}</div>`; html += `<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>`;
} }
if (ensName) { if (ensName) {
html += html +=
`<div class="flex items-center">${dot}` + `<div class="flex items-center">${title ? "" : dot}` +
copyableHtml(ensName, "") + copyableHtml(ensName, "") +
extLink + extLink +
`</div>` + `</div>` +
@@ -57,7 +55,7 @@ function txAddressHtml(address, ensName, title) {
`</div>`; `</div>`;
} else { } else {
html += html +=
`<div class="flex items-center">${dot}` + `<div class="flex items-center">${title ? "" : dot}` +
copyableHtml(address, "break-all") + copyableHtml(address, "break-all") +
extLink + extLink +
`</div>`; `</div>`;
@@ -87,15 +85,9 @@ function show(tx) {
fromEns: tx.fromEns || null, fromEns: tx.fromEns || null,
toEns: tx.toEns || null, toEns: tx.toEns || null,
directionLabel: tx.directionLabel || null, directionLabel: tx.directionLabel || null,
direction: tx.direction || null,
isContractCall: tx.isContractCall || false,
method: tx.method || null,
}, },
}; };
render(); render();
if (tx.isContractCall || tx.direction === "contract") {
loadCalldata(tx.hash, tx.to);
}
} }
function render() { function render() {
@@ -129,25 +121,6 @@ function render() {
nativeEl.parentElement.classList.add("hidden"); 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 = $("tx-detail-time").textContent =
isoDate(tx.timestamp) + " (" + timeAgo(tx.timestamp) + ")"; isoDate(tx.timestamp) + " (" + timeAgo(tx.timestamp) + ")";
$("tx-detail-status").textContent = tx.isError ? "Failed" : "Success"; $("tx-detail-status").textContent = tx.isError ? "Failed" : "Success";
@@ -164,73 +137,6 @@ 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 += `<div class="mb-2">${escapeHtml(decoded.description)}</div>`;
}
for (const d of decoded.details || []) {
detailsHtml += `<div class="mb-2">`;
detailsHtml += `<div class="text-muted">${escapeHtml(d.label)}</div>`;
if (d.address) {
const dot = addressDotHtml(d.address);
detailsHtml += `<div>${dot}${copyableHtml(d.value, "break-all")}</div>`;
} else {
detailsHtml += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
}
detailsHtml += `</div>`;
}
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) { function init(_ctx) {
ctx = _ctx; ctx = _ctx;
$("btn-tx-back").addEventListener("click", () => { $("btn-tx-back").addEventListener("click", () => {

View File

@@ -5,7 +5,6 @@ const {
showView, showView,
showFlash, showFlash,
addressDotHtml, addressDotHtml,
addressTitle,
escapeHtml, escapeHtml,
} = require("./helpers"); } = require("./helpers");
const { state, saveState } = require("../../shared/state"); const { state, saveState } = require("../../shared/state");
@@ -38,13 +37,6 @@ function toAddressHtml(address) {
const dot = addressDotHtml(address); const dot = addressDotHtml(address);
const link = `https://etherscan.io/address/${address}`; const link = `https://etherscan.io/address/${address}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`; const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
const title = addressTitle(address, state.wallets);
if (title) {
return (
`<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>` +
`<div class="break-all">${escapeHtml(address)}${extLink}</div>`
);
}
return `<div class="flex items-center">${dot}<span class="break-all">${escapeHtml(address)}</span>${extLink}</div>`; return `<div class="flex items-center">${dot}<span class="break-all">${escapeHtml(address)}</span>${extLink}</div>`;
} }

View File

@@ -123,27 +123,15 @@ async function refreshBalances(wallets, rpcUrl, blockscoutUrl, trackedTokens) {
}), }),
); );
// ENS reverse lookup — only overwrite on success so that // ENS reverse lookup
// transient RPC errors don't wipe a previously resolved name.
updates.push( updates.push(
provider provider
.lookupAddress(addr.address) .lookupAddress(addr.address)
.then((name) => { .then((name) => {
addr.ensName = name || null; addr.ensName = name || null;
log.debugf(
"ENS reverse",
addr.address,
"->",
addr.ensName,
);
}) })
.catch((e) => { .catch(() => {
log.errorf( addr.ensName = null;
"ENS reverse failed",
addr.address,
e.message,
);
// Keep existing addr.ensName if we had one
}), }),
); );

View File

@@ -39,7 +39,7 @@ async function resolveEnsName(address, rpcUrl) {
return name; return name;
} catch (e) { } catch (e) {
log.errorf("ENS reverse lookup failed", address, e.message); log.errorf("ENS reverse lookup failed", address, e.message);
// Don't cache failures — let subsequent lookups retry setCache(address, null);
return null; return null;
} }
} }

View File

@@ -37,21 +37,7 @@ function parseTx(tx, addrLower) {
if (token) { if (token) {
symbol = token.symbol; symbol = token.symbol;
} }
// Map known DEX methods to "Swap" for cleaner display const label = method.charAt(0).toUpperCase() + method.slice(1);
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"; direction = "contract";
directionLabel = label; directionLabel = label;
value = ""; value = "";
@@ -153,18 +139,9 @@ async function fetchRecentTransactions(address, blockscoutUrl, count = 25) {
// When a token transfer shares a hash with a normal tx, the normal tx // 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 // 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 || []) { for (const tt of ttJson.items || []) {
const parsed = parseTokenTransfer(tt, addrLower); 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); txsByHash.set(parsed.hash, parsed);
} }

View File

@@ -41,18 +41,6 @@ function getSignerForAddress(walletData, addrIndex, decryptedSecret) {
return new Wallet(decryptedSecret); return new Wallet(decryptedSecret);
} }
function getPrivateKeyForAddress(walletData, addrIndex, decryptedSecret) {
if (walletData.type === "hd") {
const node = HDNodeWallet.fromPhrase(
decryptedSecret,
"",
BIP44_ETH_PATH,
);
return node.deriveChild(addrIndex).privateKey;
}
return decryptedSecret;
}
function isValidMnemonic(mnemonic) { function isValidMnemonic(mnemonic) {
return Mnemonic.isValidMnemonic(mnemonic); return Mnemonic.isValidMnemonic(mnemonic);
} }
@@ -63,6 +51,5 @@ module.exports = {
hdWalletFromMnemonic, hdWalletFromMnemonic,
addressFromPrivateKey, addressFromPrivateKey,
getSignerForAddress, getSignerForAddress,
getPrivateKeyForAddress,
isValidMnemonic, isValidMnemonic,
}; };