Compare commits
3 Commits
fix/55-swa
...
53506bad7b
| Author | SHA1 | Date | |
|---|---|---|---|
| 53506bad7b | |||
| d5849c831b | |||
|
|
6e4dcb2e4f |
@@ -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];
|
||||||
|
|||||||
@@ -13,26 +13,6 @@ if (typeof browser !== "undefined") {
|
|||||||
(document.head || document.documentElement).appendChild(script);
|
(document.head || document.documentElement).appendChild(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the persisted EIP-6963 provider UUID to the inpage script.
|
|
||||||
// Generated once at install time and stored in chrome.storage.local.
|
|
||||||
(function sendProviderUuid() {
|
|
||||||
const storage =
|
|
||||||
typeof browser !== "undefined"
|
|
||||||
? browser.storage.local
|
|
||||||
: chrome.storage.local;
|
|
||||||
storage.get("eip6963Uuid", (items) => {
|
|
||||||
let uuid = items?.eip6963Uuid;
|
|
||||||
if (!uuid) {
|
|
||||||
uuid = crypto.randomUUID();
|
|
||||||
storage.set({ eip6963Uuid: uuid });
|
|
||||||
}
|
|
||||||
window.postMessage(
|
|
||||||
{ type: "AUTISTMASK_PROVIDER_UUID", uuid },
|
|
||||||
location.origin,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Relay requests from the page to the background script
|
// Relay requests from the page to the background script
|
||||||
window.addEventListener("message", (event) => {
|
window.addEventListener("message", (event) => {
|
||||||
if (event.source !== window) return;
|
if (event.source !== window) return;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
// Some dApps (wagmi) check this to confirm MetaMask-like behavior
|
// Some dApps (wagmi) check this to confirm MetaMask-like behavior
|
||||||
_metamask: {
|
_metamask: {
|
||||||
isUnlocked() {
|
isUnlocked() {
|
||||||
return Promise.resolve(provider.selectedAddress !== null);
|
return Promise.resolve(true);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -155,38 +155,21 @@
|
|||||||
"</svg>",
|
"</svg>",
|
||||||
);
|
);
|
||||||
|
|
||||||
let providerUuid = crypto.randomUUID(); // fallback until real UUID arrives
|
const providerInfo = {
|
||||||
|
uuid: "f3c5b2a1-8d4e-4f6a-9c7b-1e2d3a4b5c6d",
|
||||||
function buildProviderInfo() {
|
name: "AutistMask",
|
||||||
return {
|
icon: ICON_SVG,
|
||||||
uuid: providerUuid,
|
rdns: "berlin.sneak.autistmask",
|
||||||
name: "AutistMask",
|
};
|
||||||
icon: ICON_SVG,
|
|
||||||
rdns: "berlin.sneak.autistmask",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function announceProvider() {
|
function announceProvider() {
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent("eip6963:announceProvider", {
|
new CustomEvent("eip6963:announceProvider", {
|
||||||
detail: Object.freeze({
|
detail: Object.freeze({ info: providerInfo, provider }),
|
||||||
info: buildProviderInfo(),
|
|
||||||
provider,
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for the persisted UUID from the content script
|
|
||||||
function onProviderUuid(event) {
|
|
||||||
if (event.source !== window) return;
|
|
||||||
if (event.data?.type !== "AUTISTMASK_PROVIDER_UUID") return;
|
|
||||||
window.removeEventListener("message", onProviderUuid);
|
|
||||||
providerUuid = event.data.uuid;
|
|
||||||
announceProvider();
|
|
||||||
}
|
|
||||||
window.addEventListener("message", onProviderUuid);
|
|
||||||
|
|
||||||
window.addEventListener("eip6963:requestProvider", announceProvider);
|
window.addEventListener("eip6963:requestProvider", announceProvider);
|
||||||
announceProvider();
|
announceProvider();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -358,6 +358,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- token contract details (ERC-20 only) -->
|
||||||
|
<div
|
||||||
|
id="address-token-contract-info"
|
||||||
|
class="border-b border-border-light pb-2 mb-2 text-xs hidden"
|
||||||
|
></div>
|
||||||
|
|
||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
<div class="flex gap-2 mb-3">
|
<div class="flex gap-2 mb-3">
|
||||||
<button
|
<button
|
||||||
@@ -374,12 +380,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">
|
||||||
@@ -841,41 +841,39 @@
|
|||||||
</p>
|
</p>
|
||||||
<div id="settings-denied-sites"></div>
|
<div id="settings-denied-sites"></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"
|
|
||||||
>
|
|
||||||
< 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
|
<div
|
||||||
id="delete-wallet-flash"
|
id="delete-wallet-confirm"
|
||||||
class="text-xs text-red-500 mb-2 hidden"
|
class="hidden border border-border border-dashed p-3 mt-2 mb-3"
|
||||||
></div>
|
>
|
||||||
<div class="mb-2">
|
<h3 class="font-bold mb-1">Confirm Deletion</h3>
|
||||||
<label class="block mb-1">Password</label>
|
<p class="text-xs text-muted mb-2">
|
||||||
|
Delete <strong id="delete-wallet-name"></strong>? This
|
||||||
|
is permanent. Any funds will be unrecoverable without
|
||||||
|
your recovery phrase.
|
||||||
|
</p>
|
||||||
|
<p class="text-xs mb-2">Enter your password to confirm:</p>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="delete-wallet-password"
|
id="delete-wallet-password"
|
||||||
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
class="border border-border p-1 w-full text-sm bg-bg text-fg mb-2"
|
||||||
placeholder="Enter your password to confirm"
|
placeholder="Password"
|
||||||
/>
|
/>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
id="btn-delete-wallet-confirm"
|
||||||
|
class="border border-border text-fg px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="btn-delete-wallet-cancel"
|
||||||
|
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- ============ SETTINGS: ADD TOKEN ============ -->
|
<!-- ============ SETTINGS: ADD TOKEN ============ -->
|
||||||
@@ -953,13 +951,7 @@
|
|||||||
>
|
>
|
||||||
< Back
|
< 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>
|
||||||
@@ -984,29 +976,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>
|
||||||
|
|||||||
@@ -189,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,21 +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 === "contract"
|
|
||||||
? tx.from
|
|
||||||
: 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)" : "";
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -144,24 +142,30 @@ function show() {
|
|||||||
const tokenHolders = tb && tb.holders != null ? tb.holders : null;
|
const tokenHolders = tb && tb.holders != null ? tb.holders : null;
|
||||||
const dot = addressDotHtml(tokenId);
|
const dot = addressDotHtml(tokenId);
|
||||||
const tokenLink = `https://etherscan.io/token/${escapeHtml(tokenId)}`;
|
const tokenLink = `https://etherscan.io/token/${escapeHtml(tokenId)}`;
|
||||||
const knownToken = TOKEN_BY_ADDRESS.get(tokenId.toLowerCase());
|
let infoHtml =
|
||||||
const projectUrl = knownToken && knownToken.url ? knownToken.url : null;
|
`<div class="font-bold mb-1">Token Contract</div>` +
|
||||||
let infoHtml = `<div class="font-bold mb-2">Contract Address</div>`;
|
`<div class="flex items-center mb-1">${dot}` +
|
||||||
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>` +
|
`<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>` +
|
`<a href="${tokenLink}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>` +
|
||||||
`</div>`;
|
`</div>`;
|
||||||
|
const details = [];
|
||||||
if (tokenName)
|
if (tokenName)
|
||||||
infoHtml += `<div class="mb-1"><span class="text-muted">Name:</span> ${tokenName}</div>`;
|
details.push(`<span class="text-muted">Name:</span> ${tokenName}`);
|
||||||
if (tokenSymbol)
|
if (tokenSymbol)
|
||||||
infoHtml += `<div class="mb-1"><span class="text-muted">Symbol:</span> ${tokenSymbol}</div>`;
|
details.push(
|
||||||
|
`<span class="text-muted">Symbol:</span> ${tokenSymbol}`,
|
||||||
|
);
|
||||||
if (tokenDecimals != null)
|
if (tokenDecimals != null)
|
||||||
infoHtml += `<div class="mb-1"><span class="text-muted">Decimals:</span> ${tokenDecimals}</div>`;
|
details.push(
|
||||||
|
`<span class="text-muted">Decimals:</span> ${tokenDecimals}`,
|
||||||
|
);
|
||||||
if (tokenHolders != null)
|
if (tokenHolders != null)
|
||||||
infoHtml += `<div class="mb-1"><span class="text-muted">Holders:</span> ${Number(tokenHolders).toLocaleString()}</div>`;
|
details.push(
|
||||||
if (projectUrl)
|
`<span class="text-muted">Holders:</span> ${Number(tokenHolders).toLocaleString()}`,
|
||||||
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>`;
|
);
|
||||||
|
if (details.length > 0) {
|
||||||
|
infoHtml += `<div class="flex flex-wrap gap-x-3 gap-y-1">${details.join("")}</div>`;
|
||||||
|
}
|
||||||
contractInfo.innerHTML = infoHtml;
|
contractInfo.innerHTML = infoHtml;
|
||||||
contractInfo.classList.remove("hidden");
|
contractInfo.classList.remove("hidden");
|
||||||
} else {
|
} else {
|
||||||
@@ -214,10 +218,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 +255,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)" : "";
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
@@ -25,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",
|
||||||
|
|||||||
@@ -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)" : "";
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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 { decryptWithPassword } = require("../../shared/vault");
|
||||||
|
|
||||||
const runtime =
|
const runtime =
|
||||||
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
|
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
|
||||||
@@ -66,6 +66,8 @@ function renderTrackedTokens() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let deleteWalletIndex = null;
|
||||||
|
|
||||||
function renderWalletListSettings() {
|
function renderWalletListSettings() {
|
||||||
const container = $("settings-wallet-list");
|
const container = $("settings-wallet-list");
|
||||||
if (state.wallets.length === 0) {
|
if (state.wallets.length === 0) {
|
||||||
@@ -76,47 +78,20 @@ function renderWalletListSettings() {
|
|||||||
state.wallets.forEach((wallet, idx) => {
|
state.wallets.forEach((wallet, idx) => {
|
||||||
const name = escapeHtml(wallet.name || "Wallet " + (idx + 1));
|
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 += `<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 += `<span>${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 += `<span class="btn-delete-wallet border-b border-dashed border-fg text-fg cursor-pointer" data-idx="${idx}">[delete]</span>`;
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
});
|
});
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
container.querySelectorAll(".btn-delete-wallet").forEach((btn) => {
|
container.querySelectorAll(".btn-delete-wallet").forEach((btn) => {
|
||||||
btn.addEventListener("click", () => {
|
btn.addEventListener("click", () => {
|
||||||
const idx = parseInt(btn.dataset.idx, 10);
|
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 wallet = state.wallets[idx];
|
||||||
const input = document.createElement("input");
|
deleteWalletIndex = idx;
|
||||||
input.type = "text";
|
$("delete-wallet-name").textContent =
|
||||||
input.className =
|
wallet.name || "Wallet " + (idx + 1);
|
||||||
"border border-border p-0 text-xs bg-bg text-fg w-full";
|
$("delete-wallet-password").value = "";
|
||||||
input.value = wallet.name || "Wallet " + (idx + 1);
|
$("delete-wallet-confirm").classList.remove("hidden");
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -127,6 +102,8 @@ function show() {
|
|||||||
renderTrackedTokens();
|
renderTrackedTokens();
|
||||||
renderSiteLists();
|
renderSiteLists();
|
||||||
renderWalletListSettings();
|
renderWalletListSettings();
|
||||||
|
deleteWalletIndex = null;
|
||||||
|
$("delete-wallet-confirm").classList.add("hidden");
|
||||||
|
|
||||||
showView("settings");
|
showView("settings");
|
||||||
}
|
}
|
||||||
@@ -141,8 +118,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) {
|
||||||
@@ -252,6 +227,72 @@ function init(ctx) {
|
|||||||
ctx.renderWalletList();
|
ctx.renderWalletList();
|
||||||
showView("main");
|
showView("main");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("btn-delete-wallet-cancel").addEventListener("click", () => {
|
||||||
|
$("delete-wallet-confirm").classList.add("hidden");
|
||||||
|
$("delete-wallet-password").value = "";
|
||||||
|
deleteWalletIndex = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
$("btn-delete-wallet-confirm").addEventListener("click", async () => {
|
||||||
|
const pw = $("delete-wallet-password").value;
|
||||||
|
if (!pw) {
|
||||||
|
showFlash("Password required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteWalletIndex === null) {
|
||||||
|
showFlash("No wallet selected for deletion.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const walletIdx = deleteWalletIndex;
|
||||||
|
const wallet = state.wallets[walletIdx];
|
||||||
|
|
||||||
|
// Verify password against the wallet's encrypted data
|
||||||
|
try {
|
||||||
|
await decryptWithPassword(wallet.encrypted, pw);
|
||||||
|
} catch (_e) {
|
||||||
|
showFlash("Wrong password.");
|
||||||
|
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();
|
||||||
|
$("delete-wallet-confirm").classList.add("hidden");
|
||||||
|
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();
|
||||||
|
$("delete-wallet-confirm").classList.add("hidden");
|
||||||
|
showFlash("Wallet deleted.");
|
||||||
|
renderWalletListSettings();
|
||||||
|
ctx.renderWalletList();
|
||||||
|
showView("main");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { init, show, renderSiteLists };
|
module.exports = { init, show, renderSiteLists };
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -204,10 +192,6 @@ async function lookupTokenInfo(contractAddress, rpcUrl) {
|
|||||||
name = symbol;
|
name = symbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Truncate to prevent storage of excessively long values from RPC
|
|
||||||
name = String(name).slice(0, 64);
|
|
||||||
symbol = String(symbol).slice(0, 12);
|
|
||||||
|
|
||||||
log.infof("Token resolved:", symbol, "decimals", Number(decimals));
|
log.infof("Token resolved:", symbol, "decimals", Number(decimals));
|
||||||
return { name, symbol, decimals: Number(decimals) };
|
return { name, symbol, decimals: Number(decimals) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user