Compare commits

..

7 Commits

Author SHA1 Message Date
user
be38ce081e refactor: derive git info inside Docker instead of --build-arg
All checks were successful
check / check (push) Successful in 45s
- Remove .git from .dockerignore so it's available in Docker build context
- Install git in Docker image (apt-get)
- Remove ARG/ENV GIT_COMMIT_SHORT/FULL from Dockerfile
- Remove --build-arg from Makefile docker target
- Simplify build.js to use git CLI directly (no env var indirection)

build.js already had fallback logic to shell out to git; now that .git
is present in the build context, it works directly without needing
values passed in from outside Docker.
2026-03-01 15:16:16 -08:00
clawbot
004cb41868 fix: add app name/repo link, rename build date to release date per issue #144
- Add AutistMask name with link to repo at top of About well
- Rename 'Build date' label to 'Release date' to match issue requirements
- Update element ID from about-build-date to about-release-date
2026-03-01 15:16:16 -08:00
clawbot
1a749a978e feat: add version click flash animation with colored easter egg, darken light mode wells
- Version number clicks now trigger copy-flash animation
- After 5 clicks, each additional click flashes a different bright
  saturated color (hot pink, vivid green, electric blue, orange, purple)
- 10th click reveals debug well as before
- Wells in light mode darkened from #f5f5f5 to #e8e8e8 for better
  contrast with white background

Addresses additional requirements from issue #144 comments.
2026-03-01 15:16:16 -08:00
clawbot
a2464fcf04 feat: add About well to settings with build info and debug easter egg
Add a new well at the bottom of the settings view that displays:
- License (GPL-3.0)
- Author (sneak)
- Version (from package.json)
- Build date (injected at build time)
- Git commit short hash (linked to Gitea commit URL)

Build-time injection: build.js now reads the git commit hash and version
from package.json, injecting them via esbuild define constants. The
Dockerfile and Makefile pass commit hashes as build args so the info is
available even when .git is excluded from the Docker context.

Easter egg: clicking the version number 10 times reveals a hidden debug
well below the About well, containing a toggle for debug mode. The debug
mode flag is persisted in state and enables verbose console logging via
the runtime debug flag in the logger.

closes #144
2026-03-01 15:15:44 -08:00
a22f33d511 fix: implement proper view navigation stack (#146)
All checks were successful
check / check (push) Successful in 25s
## Summary

Fixes the view stack pop bug where pressing Back in Settings (or any view) always returned to Main instead of the previous view.

Closes [issue #134](#134)

## Problem

The popup UI had no navigation stack. Every back button was hardcoded to a specific destination (usually Main). The reported path:

> Main → Address → Transaction → Settings (gear icon) → Back

...would go to Main instead of returning to the Transaction view.

## Solution

Implemented a proper view navigation stack (like iOS) as already described in the README:

- **`viewStack`** array added to persisted state — survives popup close/reopen
- **`pushCurrentView()`** — pushes the current view name onto the stack before any forward navigation
- **`goBack()`** — pops the stack and shows the previous view; falls back to Main if the stack is empty; re-renders the wallet list when returning to Main
- **`clearViewStack()`** — resets the stack for root transitions (e.g., after adding/deleting a wallet)

### What Changed

1. **helpers.js** — Added navigation stack functions (`pushCurrentView`, `goBack`, `clearViewStack`, `setRenderMain`)
2. **state.js** — Added `viewStack` to persisted state
3. **index.js** — All `ctx.show*()` wrappers now push before navigating forward; gear button uses stack for toggle behavior
4. **All view back buttons** — Replaced hardcoded destinations with `goBack()` (settings, addressDetail, addressToken, transactionDetail, send, receive, addToken, confirmTx, addWallet, settingsAddToken, deleteWallet, export-privkey)
5. **Direct `showView()` forward navigations** — Added `pushCurrentView()` calls before `showView("send")` in addressDetail, addressToken, and home; before `showView("export-privkey")` in addressDetail; before `deleteWallet.show()` in settings
6. **Reset-to-root transitions** — `clearViewStack()` called after adding a wallet (all 3 import types), after deleting the last wallet, and after transaction completion (Done button)

### Navigation Paths Verified

- **Main → Settings → Back** → returns to Main ✓
- **Main → Address → Settings → Back** → returns to Address ✓
- **Main → Address → Transaction → Settings → Back** → returns to Transaction ✓ (the reported bug)
- **Main → Address → Token → Send → ConfirmTx → Back → Back → Back → Back** → unwinds correctly through each view back to Main ✓
- **Main → Address → Token → Transaction → Settings → Back** → returns to Transaction ✓
- **Settings → Add Wallet → (add) → Main** → stack cleared, fresh root ✓
- **Settings → Delete Wallet → Back** → returns to Settings ✓
- **Settings → Delete Wallet → (confirm)** → stack reset to [main], settings shown ✓
- **Address → Send → ConfirmTx → (broadcast) → SuccessTx → Done** → stack reset, returns to address context ✓
- **Popup close/reopen** → viewStack persisted, back navigation still works ✓

Co-authored-by: user <user@Mac.lan guest wan>
Reviewed-on: #146
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-02 00:15:01 +01:00
39db06c83d feat: show debug banner on testnet or debug mode, add TESTNET tag (#143)
All checks were successful
check / check (push) Successful in 12s
Display the red debug banner when on a testnet OR when DEBUG is enabled.

When on a testnet, a "TESTNET" label is shown on the far right side of the banner. The banner label shows the network name when not in debug mode, and "DEBUG / INSECURE" when debug is on.

closes #140

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: #143
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-01 21:55:36 +01:00
df031fd07d fix: unify address display with shared renderAddressHtml utility (#129)
All checks were successful
check / check (push) Successful in 6s
## Summary

All address rendering now uses a single `renderAddressHtml()` function in helpers.js that produces consistent output everywhere:
- Color dot (deterministic from address)
- Full address with dashed-underline click-to-copy affordance
- Etherscan external link icon

## Changes

Refactored all 9 view files that display addresses to use the shared utility:
- **approval.js** (approve-tx, approve-sign, approve-site): addresses now have click-to-copy with dashed underline affordance
- **confirmTx.js**: from/to addresses and token contract address use shared renderer
- **txStatus.js**: wait/success/error transaction addresses
- **transactionDetail.js**: from/to and decoded calldata addresses
- **home.js**: active address display
- **send.js**: from-address display
- **receive.js**: receive address display
- **addressDetail.js**: address line and export-privkey address
- **addressToken.js**: address line and contract info

## Consolidation

- `EXT_ICON` SVG constant: removed 6 duplicates, now in helpers.js
- `copyableHtml()`: removed duplicate, now in helpers.js
- `etherscanLinkHtml()`: removed duplicates, now in helpers.js
- `attachCopyHandlers()`: removed duplicate, now in helpers.js
- Net: **-193 lines** (174 added, 367 removed)

closes #97

Co-authored-by: user <user@Mac.lan guest wan>
Reviewed-on: #129
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-01 21:54:38 +01:00
17 changed files with 359 additions and 454 deletions

View File

@@ -2,10 +2,22 @@
// Loads state, initializes views, triggers first render.
const { DEBUG } = require("../shared/constants");
const { state, saveState, loadState } = require("../shared/state");
const {
state,
saveState,
loadState,
currentNetwork,
} = require("../shared/state");
const { refreshPrices } = require("../shared/prices");
const { refreshBalances } = require("../shared/balances");
const { $, showView } = require("./views/helpers");
const {
$,
showView,
setRenderMain,
pushCurrentView,
goBack,
clearViewStack,
} = require("./views/helpers");
const { applyTheme } = require("./theme");
const home = require("./views/home");
@@ -53,15 +65,42 @@ async function doRefreshAndRender() {
const ctx = {
renderWalletList,
doRefreshAndRender,
showAddWalletView: () => addWallet.show(),
showAddressDetail: () => addressDetail.show(),
showAddressToken: () => addressToken.show(),
showAddTokenView: () => addToken.show(),
showConfirmTx: (txInfo) => confirmTx.show(txInfo),
showReceive: () => receive.show(),
showTransactionDetail: (tx) => transactionDetail.show(tx),
showSettingsView: () => settings.show(),
showSettingsAddTokenView: () => settingsAddToken.show(),
showAddWalletView: () => {
pushCurrentView();
addWallet.show();
},
showAddressDetail: () => {
pushCurrentView();
addressDetail.show();
},
showAddressToken: () => {
pushCurrentView();
addressToken.show();
},
showAddTokenView: () => {
pushCurrentView();
addToken.show();
},
showConfirmTx: (txInfo) => {
pushCurrentView();
confirmTx.show(txInfo);
},
showReceive: () => {
pushCurrentView();
receive.show();
},
showTransactionDetail: (tx) => {
pushCurrentView();
transactionDetail.show(tx);
},
showSettingsView: () => {
pushCurrentView();
settings.show();
},
showSettingsAddTokenView: () => {
pushCurrentView();
settingsAddToken.show();
},
};
// Views that can be fully re-rendered from persisted state.
@@ -167,18 +206,25 @@ function fallbackView() {
}
async function init() {
if (DEBUG) {
await loadState();
applyTheme(state.theme);
const net = currentNetwork();
if (DEBUG || net.isTestnet) {
const banner = document.createElement("div");
banner.id = "debug-banner";
if (DEBUG && net.isTestnet) {
banner.textContent = "DEBUG / INSECURE [TESTNET]";
} else if (net.isTestnet) {
banner.textContent = "[TESTNET]";
} else {
banner.textContent = "DEBUG / INSECURE";
}
banner.style.cssText =
"background:#c00;color:#fff;text-align:center;font-size:10px;padding:1px 0;font-family:monospace;position:sticky;top:0;z-index:9999;";
document.body.prepend(banner);
}
await loadState();
applyTheme(state.theme);
// Auto-default active address
if (
state.activeAddress === null &&
@@ -208,13 +254,15 @@ async function init() {
.getElementById("view-settings")
.classList.contains("hidden")
) {
renderWalletList();
showView("main");
goBack();
return;
}
pushCurrentView();
settings.show();
});
setRenderMain(renderWalletList);
welcome.init(ctx);
addWallet.init(ctx);
home.init(ctx);

View File

@@ -1,4 +1,4 @@
const { $, showView, showFlash } = require("./helpers");
const { $, showFlash, goBack } = require("./helpers");
const { getTopTokens } = require("../../shared/tokenList");
const { state, saveState } = require("../../shared/state");
const { lookupTokenInfo } = require("../../shared/balances");
@@ -59,7 +59,12 @@ function init(ctx) {
});
await saveState();
ctx.doRefreshAndRender();
ctx.showAddressDetail();
// Pop the stack (back to address detail) and re-render it
// so the newly added token is visible immediately.
if (state.viewStack.length > 0) {
state.viewStack.pop();
}
require("./addressDetail").show();
} catch (e) {
const detail = e.shortMessage || e.message || String(e);
log.errorf("Token lookup failed for", contractAddr, detail);
@@ -69,7 +74,9 @@ function init(ctx) {
}
});
$("btn-add-token-back").addEventListener("click", ctx.showAddressDetail);
$("btn-add-token-back").addEventListener("click", () => {
goBack();
});
}
module.exports = { init, show };

View File

@@ -1,4 +1,4 @@
const { $, showView, showFlash } = require("./helpers");
const { $, showView, showFlash, goBack, clearViewStack } = require("./helpers");
const {
generateMnemonic,
hdWalletFromMnemonic,
@@ -143,6 +143,7 @@ async function importMnemonic(ctx) {
state.wallets.push(wallet);
state.hasWallet = true;
await saveState();
clearViewStack();
ctx.renderWalletList();
showView("main");
@@ -198,6 +199,7 @@ async function importPrivateKey(ctx) {
});
state.hasWallet = true;
await saveState();
clearViewStack();
ctx.renderWalletList();
showView("main");
@@ -249,6 +251,7 @@ async function importXprvKey(ctx) {
state.wallets.push(wallet);
state.hasWallet = true;
await saveState();
clearViewStack();
ctx.renderWalletList();
showView("main");
@@ -297,12 +300,7 @@ function init(ctx) {
// Back button
$("btn-add-wallet-back").addEventListener("click", () => {
if (!state.hasWallet) {
showView("welcome");
} else {
ctx.renderWalletList();
showView("main");
}
goBack();
});
}

View File

@@ -8,13 +8,12 @@ const {
addressTitle,
escapeHtml,
truncateMiddle,
renderAddressHtml,
attachCopyHandlers,
goBack,
pushCurrentView,
} = require("./helpers");
const {
state,
currentAddress,
saveState,
currentNetwork,
} = require("../../shared/state");
const { state, currentAddress, saveState } = require("../../shared/state");
const { formatUsd, getAddressValueUsd } = require("../../shared/prices");
const {
fetchRecentTransactions,
@@ -33,17 +32,6 @@ const { getSignerForAddress } = require("../../shared/wallet");
let ctx;
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
function etherscanAddressLink(address) {
return `${currentNetwork().explorerUrl}/address/${address}`;
}
function show() {
state.selectedToken = null;
const wallet = state.wallets[state.selectedWallet];
@@ -61,22 +49,18 @@ function show() {
img.style.imageRendering = "pixelated";
img.style.borderRadius = "50%";
blockieEl.appendChild(img);
$("address-dot").innerHTML = addressDotHtml(addr.address);
$("address-full").dataset.full = addr.address;
$("address-full").textContent = addr.address;
const addrLink = etherscanAddressLink(addr.address);
$("address-etherscan-link").innerHTML =
`<a href="${addrLink}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
const addrTitle = addressTitle(addr.address, state.wallets);
$("address-line").innerHTML = renderAddressHtml(addr.address, {
title: addrTitle,
ensName: addr.ensName,
});
$("address-line").dataset.full = addr.address;
attachCopyHandlers($("address-line"));
const usdTotal = formatUsd(getAddressValueUsd(addr));
$("address-usd-total").innerHTML = usdTotal || "&nbsp;";
const ensEl = $("address-ens");
if (addr.ensName) {
ensEl.innerHTML =
addressDotHtml(addr.address) + escapeHtml(addr.ensName);
ensEl.classList.remove("hidden");
} else {
// ENS is now shown inside renderAddressHtml, hide the separate element
ensEl.classList.add("hidden");
}
$("address-balances").innerHTML = balanceLinesForAddress(
addr,
state.trackedTokens,
@@ -263,18 +247,9 @@ function renderTransactions(txs) {
function init(_ctx) {
ctx = _ctx;
$("address-full").addEventListener("click", () => {
const addr = $("address-full").dataset.full;
if (addr) {
navigator.clipboard.writeText(addr);
showFlash("Copied!");
flashCopyFeedback($("address-full"));
}
});
$("btn-address-back").addEventListener("click", () => {
ctx.renderWalletList();
showView("main");
goBack();
});
$("btn-send").addEventListener("click", () => {
@@ -292,6 +267,7 @@ function init(_ctx) {
$("send-token-static").classList.add("hidden");
updateSendBalance();
resetSendValidation();
pushCurrentView();
showView("send");
});
@@ -321,6 +297,7 @@ function init(_ctx) {
$("btn-export-privkey").addEventListener("click", () => {
moreDropdown.classList.add("hidden");
moreBtn.classList.remove("bg-fg", "text-bg");
pushCurrentView();
const wallet = state.wallets[state.selectedWallet];
const addr = wallet.addresses[state.selectedAddress];
const blockieEl = $("export-privkey-jazzicon");
@@ -334,9 +311,9 @@ function init(_ctx) {
blockieEl.appendChild(bImg);
$("export-privkey-title").textContent =
wallet.name + " \u2014 Address " + (state.selectedAddress + 1);
$("export-privkey-dot").innerHTML = addressDotHtml(addr.address);
$("export-privkey-address").textContent = addr.address;
$("export-privkey-address").dataset.full = addr.address;
const exportAddrContainer = $("export-privkey-dot").parentElement;
exportAddrContainer.innerHTML = renderAddressHtml(addr.address);
attachCopyHandlers(exportAddrContainer);
$("export-privkey-password").value = "";
$("export-privkey-flash").textContent = "";
$("export-privkey-flash").style.visibility = "hidden";
@@ -390,19 +367,10 @@ function init(_ctx) {
}
});
$("export-privkey-address").addEventListener("click", () => {
const full = $("export-privkey-address").dataset.full;
if (full) {
navigator.clipboard.writeText(full);
showFlash("Copied!");
flashCopyFeedback($("export-privkey-address"));
}
});
$("btn-export-privkey-back").addEventListener("click", () => {
$("export-privkey-value").textContent = "";
$("export-privkey-password").value = "";
show();
goBack();
});
}

View File

@@ -11,13 +11,12 @@ const {
escapeHtml,
truncateMiddle,
balanceLine,
renderAddressHtml,
attachCopyHandlers,
goBack,
pushCurrentView,
} = require("./helpers");
const {
state,
currentAddress,
saveState,
currentNetwork,
} = require("../../shared/state");
const { state, currentAddress, saveState } = require("../../shared/state");
const { TOKEN_BY_ADDRESS, resolveSymbol } = require("../../shared/tokenList");
const {
formatUsd,
@@ -39,21 +38,6 @@ const makeBlockie = require("ethereum-blockies-base64");
let ctx;
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
function etherscanAddressLink(address) {
return `${currentNetwork().explorerUrl}/address/${address}`;
}
function etherscanTokenLink(tokenContract, holderAddress) {
return `${currentNetwork().explorerUrl}/token/${tokenContract}?a=${holderAddress}`;
}
function isoDate(timestamp) {
const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0");
@@ -157,15 +141,13 @@ function show() {
blockieEl.appendChild(img);
// Address line
$("address-token-dot").innerHTML = addressDotHtml(addr.address);
$("address-token-full").dataset.full = addr.address;
$("address-token-full").textContent = addr.address;
const addrLink =
tokenId !== "ETH"
? etherscanTokenLink(tokenId, addr.address)
: etherscanAddressLink(addr.address);
$("address-token-etherscan-link").innerHTML =
`<a href="${addrLink}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
const addrTitle = addressTitle(addr.address, state.wallets);
$("address-token-line").innerHTML = renderAddressHtml(addr.address, {
title: addrTitle,
ensName: addr.ensName,
});
$("address-token-line").dataset.full = addr.address;
attachCopyHandlers($("address-token-line"));
// USD total for this token only
const usdVal = price ? amount * price : null;
@@ -205,15 +187,9 @@ function show() {
? knownToken.decimals
: null;
const tokenHolders = tb && tb.holders != null ? tb.holders : null;
const dot = addressDotHtml(tokenId);
const tokenLink = `${currentNetwork().explorerUrl}/token/${escapeHtml(tokenId)}`;
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>`;
infoHtml += `<div class="mb-2">${renderAddressHtml(tokenId)}</div>`;
if (tokenName)
infoHtml += `<div class="mb-1"><span class="text-muted">Name:</span> ${tokenName}</div>`;
if (tokenSymbol)
@@ -225,6 +201,7 @@ function show() {
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;
attachCopyHandlers(contractInfo);
contractInfo.classList.remove("hidden");
} else {
contractInfo.innerHTML = "";
@@ -346,15 +323,6 @@ function renderTransactions(txs) {
function init(_ctx) {
ctx = _ctx;
$("address-token-full").addEventListener("click", () => {
const addr = $("address-token-full").dataset.full;
if (addr) {
navigator.clipboard.writeText(addr);
showFlash("Copied!");
flashCopyFeedback($("address-token-full"));
}
});
$("address-token-contract-info").addEventListener("click", (e) => {
const copyEl = e.target.closest("[data-copy]");
if (copyEl) {
@@ -365,7 +333,7 @@ function init(_ctx) {
});
$("btn-address-token-back").addEventListener("click", () => {
ctx.showAddressDetail();
goBack();
});
$("btn-address-token-send").addEventListener("click", () => {
@@ -392,28 +360,14 @@ function init(_ctx) {
$("send-token").classList.add("hidden");
let staticHtml = `<div class="font-bold">${escapeHtml(currentSymbol)}</div>`;
if (tokenId !== "ETH") {
const dot = addressDotHtml(tokenId);
const link = `${currentNetwork().explorerUrl}/token/${tokenId}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
staticHtml +=
`<div class="flex items-center text-xs">${dot}` +
`<span class="break-all underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(tokenId)}">${escapeHtml(tokenId)}</span>` +
extLink +
`</div>`;
staticHtml += `<div class="text-xs">${renderAddressHtml(tokenId)}</div>`;
}
$("send-token-static").innerHTML = staticHtml;
$("send-token-static").classList.remove("hidden");
// Attach copy handler for the contract address
const copyEl = $("send-token-static").querySelector("[data-copy]");
if (copyEl) {
copyEl.addEventListener("click", () => {
navigator.clipboard.writeText(copyEl.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(copyEl);
});
}
attachCopyHandlers($("send-token-static"));
updateSendBalance();
resetSendValidation();
pushCurrentView();
showView("send");
});

View File

@@ -1,11 +1,12 @@
const {
$,
addressDotHtml,
addressTitle,
escapeHtml,
showView,
showError,
hideError,
renderAddressHtml,
attachCopyHandlers,
} = require("./helpers");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { formatEther, formatUnits, Interface, toUtf8String } = require("ethers");
@@ -17,28 +18,11 @@ const uniswap = require("../../shared/uniswap");
const runtime =
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
const erc20Iface = new Interface(ERC20_ABI);
function approvalAddressHtml(address) {
const dot = addressDotHtml(address);
const link = `${currentNetwork().explorerUrl}/address/${address}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
const title = addressTitle(address, state.wallets);
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;
return renderAddressHtml(address, { title });
}
function formatTxValue(val) {
@@ -53,10 +37,6 @@ function tokenLabel(address) {
return t ? t.symbol : null;
}
function etherscanTokenLink(address) {
return `${currentNetwork().explorerUrl}/token/${address}`;
}
// Try to decode calldata using known ABIs.
// Returns { name, description, details } or null.
function decodeCalldata(data, toAddress) {
@@ -235,10 +215,6 @@ function showTxApproval(details) {
toHtml += `<div class="font-bold mb-1">${escapeHtml(symbol)}</div>`;
}
toHtml += approvalAddressHtml(toAddr);
if (symbol) {
const link = etherscanTokenLink(toAddr);
toHtml = toHtml.replace("</div>", "") + ""; // approvalAddressHtml already has etherscan link
}
$("approve-tx-to").innerHTML = toHtml;
} else {
$("approve-tx-to").innerHTML = escapeHtml("(contract creation)");
@@ -266,12 +242,9 @@ function showTxApproval(details) {
detailsHtml += `<div class="text-muted">${escapeHtml(d.label)}</div>`;
if (d.address) {
if (d.isToken) {
const tLink = etherscanTokenLink(d.address);
detailsHtml += `<div class="font-bold">${escapeHtml(tokenLabel(d.address) || "Unknown token")}</div>`;
detailsHtml += approvalAddressHtml(d.address);
} else {
detailsHtml += approvalAddressHtml(d.address);
}
detailsHtml += approvalAddressHtml(d.address);
} else {
detailsHtml += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
}
@@ -295,6 +268,7 @@ function showTxApproval(details) {
hideError("approve-tx-error");
showView("approve-tx");
attachCopyHandlers("view-approve-tx");
}
function decodeHexMessage(hex) {
@@ -392,6 +366,7 @@ function showSignApproval(details) {
$("btn-approve-sign").classList.remove("text-muted");
showView("approve-sign");
attachCopyHandlers("view-approve-sign");
}
function show(id) {
@@ -419,6 +394,7 @@ function show(id) {
$("approve-address").innerHTML = approvalAddressHtml(
state.activeAddress,
);
attachCopyHandlers("view-approve-site");
$("approve-remember").checked = state.rememberSiteChoice;
});
}

View File

@@ -17,8 +17,10 @@ const {
showFlash,
flashCopyFeedback,
addressTitle,
addressDotHtml,
escapeHtml,
renderAddressHtml,
attachCopyHandlers,
goBack,
} = require("./helpers");
const { state, currentNetwork } = require("../../shared/state");
const { getSignerForAddress } = require("../../shared/wallet");
@@ -34,13 +36,6 @@ const { log } = require("../../shared/log");
const makeBlockie = require("ethereum-blockies-base64");
const txStatus = require("./txStatus");
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
let pendingTx = null;
function restore() {
@@ -50,14 +45,6 @@ function restore() {
}
}
function etherscanTokenLink(address) {
return `${currentNetwork().explorerUrl}/token/${address}`;
}
function etherscanAddressLink(address) {
return `${currentNetwork().explorerUrl}/address/${address}`;
}
function blockieHtml(address) {
const src = makeBlockie(address);
return `<img src="${src}" width="48" height="48" style="image-rendering:pixelated;border-radius:50%;display:inline-block">`;
@@ -65,22 +52,10 @@ function blockieHtml(address) {
function confirmAddressHtml(address, ensName, title) {
const blockie = blockieHtml(address);
const dot = addressDotHtml(address);
const link = etherscanAddressLink(address);
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
let html = `<div class="mb-1">${blockie}</div>`;
if (title) {
html += `<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>`;
}
if (ensName) {
html += `<div class="flex items-center font-bold">${title ? "" : dot}${escapeHtml(ensName)}</div>`;
}
html +=
`<div class="flex items-center">${title || ensName ? "" : dot}` +
`<span class="break-all">${escapeHtml(address)}</span>` +
extLink +
`</div>`;
return html;
return (
`<div class="mb-1">${blockie}</div>` +
renderAddressHtml(address, { title, ensName })
);
}
function valueWithUsd(text, usdAmount) {
@@ -107,23 +82,12 @@ function show(txInfo) {
// Token contract section (ERC-20 only)
const tokenSection = $("confirm-token-section");
if (isErc20) {
const dot = addressDotHtml(txInfo.token);
const link = etherscanTokenLink(txInfo.token);
$("confirm-token-contract").innerHTML =
`<div class="flex items-center">${dot}` +
`<span class="break-all underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(txInfo.token)}">${escapeHtml(txInfo.token)}</span>` +
`<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>` +
`</div>`;
$("confirm-token-contract").innerHTML = renderAddressHtml(
txInfo.token,
{},
);
tokenSection.classList.remove("hidden");
// Attach click-to-copy on the contract address
const copyEl = tokenSection.querySelector("[data-copy]");
if (copyEl) {
copyEl.onclick = () => {
navigator.clipboard.writeText(copyEl.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(copyEl);
};
}
attachCopyHandlers(tokenSection);
} else {
tokenSection.classList.add("hidden");
}
@@ -243,6 +207,7 @@ function show(txInfo) {
$("confirm-fee-amount").textContent = "Estimating...";
state.viewData = { pendingTx: txInfo };
showView("confirm-tx");
attachCopyHandlers("view-confirm-tx");
// Reset async warnings to hidden (space always reserved, no layout shift)
$("confirm-recipient-warning").style.visibility = "hidden";
@@ -391,7 +356,7 @@ function init(ctx) {
});
$("btn-confirm-back").addEventListener("click", () => {
showView("send");
goBack();
});
}

View File

@@ -1,4 +1,4 @@
const { $, showView, showFlash } = require("./helpers");
const { $, showView, showFlash, goBack, clearViewStack } = require("./helpers");
const { state, saveState } = require("../../shared/state");
const { decryptWithPassword } = require("../../shared/vault");
@@ -21,7 +21,7 @@ function init(_ctx) {
$("btn-delete-wallet-back").addEventListener("click", () => {
deleteWalletIndex = null;
ctx.showSettingsView();
goBack();
});
$("btn-delete-wallet-confirm").addEventListener("click", async () => {
@@ -77,6 +77,7 @@ function init(_ctx) {
state.selectedWallet = null;
state.selectedAddress = null;
state.activeAddress = null;
clearViewStack();
await saveState();
showView("welcome");
} else {
@@ -86,8 +87,14 @@ function init(_ctx) {
state.activeAddress =
state.wallets[0].addresses[0]?.address || null;
await saveState();
// Reset stack to [main] so Settings back goes home.
// Use require() lazily to avoid circular dependency
// (settings.js requires deleteWallet.js).
clearViewStack();
state.viewStack.push("main");
ctx.renderWalletList();
ctx.showSettingsView();
const settings = require("./settings");
settings.show();
showFlash("Wallet deleted.");
}
});

View File

@@ -6,7 +6,7 @@ const {
getPrice,
getAddressValueUsd,
} = require("../../shared/prices");
const { state, saveState } = require("../../shared/state");
const { state, saveState, currentNetwork } = require("../../shared/state");
// When views are added, removed, or transitions between them change,
// update the view-navigation documentation in README.md to match.
@@ -59,13 +59,58 @@ function showView(name) {
clearFlash();
state.currentView = name;
saveState();
if (DEBUG) {
const net = currentNetwork();
if (DEBUG || net.isTestnet) {
const banner = document.getElementById("debug-banner");
if (banner) {
if (DEBUG && net.isTestnet) {
banner.textContent =
"DEBUG / INSECURE [TESTNET] (" + name + ")";
} else if (net.isTestnet) {
banner.textContent = "[TESTNET]";
} else {
banner.textContent = "DEBUG / INSECURE (" + name + ")";
}
}
}
}
// Callback to re-render the main/home view when navigating back to it.
// Set once by index.js via setRenderMain().
let _renderMain = null;
function setRenderMain(fn) {
_renderMain = fn;
}
// Push the current view onto the navigation stack so goBack() can
// return to it. Call this before any forward navigation.
function pushCurrentView() {
if (state.currentView) {
state.viewStack.push(state.currentView);
}
}
// Pop the navigation stack and show the previous view. If the stack
// is empty, fall back to the main (home) view.
function goBack() {
let target;
if (state.viewStack.length > 0) {
target = state.viewStack.pop();
} else {
target = "main";
}
if (target === "main" && _renderMain) {
_renderMain();
}
showView(target);
}
// Clear the entire navigation stack (used when resetting to root,
// e.g. after adding or deleting a wallet).
function clearViewStack() {
state.viewStack = [];
}
let flashTimer = null;
@@ -208,21 +253,9 @@ function addressTitle(address, wallets) {
// Render an address with color dot, optional ENS name, optional title,
// and optional truncation. Title and ENS are shown as bold labels above
// the full address.
// Delegates to renderAddressHtml for consistent output.
function formatAddressHtml(address, ensName, maxLen, title) {
const dot = addressDotHtml(address);
const displayAddr = maxLen ? truncateMiddle(address, maxLen) : address;
if (title || ensName) {
let html = "";
if (title) {
html += `<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>`;
}
if (ensName) {
html += `<div class="flex items-center font-bold">${title ? "" : dot}${escapeHtml(ensName)}</div>`;
}
html += `<div class="break-all">${escapeHtml(displayAddr)}</div>`;
return html;
}
return `<div class="flex items-center">${dot}<span class="break-all">${escapeHtml(displayAddr)}</span></div>`;
return renderAddressHtml(address, { title, ensName, maxLen });
}
function isoDate(timestamp) {
@@ -281,6 +314,91 @@ function timeAgo(timestamp) {
return years + " year" + (years !== 1 ? "s" : "") + " ago";
}
// Shared external-link icon SVG used across all views.
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
function etherscanAddressUrl(address) {
return `${currentNetwork().explorerUrl}/address/${address}`;
}
function etherscanLinkHtml(url) {
return (
`<a href="${url}" target="_blank" rel="noopener" ` +
`class="inline-flex items-center">${EXT_ICON}</a>`
);
}
// Render a copyable text span with dashed underline affordance.
// The caller must attach click handlers via attachCopyHandlers() or
// manually wire up [data-copy] elements after inserting the HTML.
function copyableHtml(text, extraClass) {
const cls =
"underline decoration-dashed cursor-pointer" +
(extraClass ? " " + extraClass : "");
return `<span class="${cls}" data-copy="${escapeHtml(text)}">${escapeHtml(text)}</span>`;
}
// Attach click-to-copy handlers to all [data-copy] elements within
// a container. Safe to call multiple times on the same container.
function attachCopyHandlers(container) {
const root =
typeof container === "string"
? document.getElementById(container)
: container;
if (!root) return;
root.querySelectorAll("[data-copy]").forEach((el) => {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(el);
};
});
}
// Unified address rendering.
//
// Produces consistent HTML for any Ethereum address:
// • Color dot
// • Optional title (e.g. "Wallet 1 — Address 2") shown bold above address
// • Optional ENS name shown bold above address
// • Full address (or truncated via maxLen) with dashed-underline click-to-copy
// • Etherscan external link icon
//
// Options object:
// title — wallet title string (from addressTitle)
// ensName — ENS name string
// maxLen — if set, truncate address display (min 32 chars enforced)
// noLink — if true, omit etherscan link
//
// After inserting the returned HTML into the DOM, call
// attachCopyHandlers() on the parent to wire up click-to-copy.
function renderAddressHtml(address, opts) {
const { title, ensName, maxLen, noLink } = opts || {};
const dot = addressDotHtml(address);
const displayAddr = maxLen ? truncateMiddle(address, maxLen) : address;
const link = etherscanAddressUrl(address);
const extLink = noLink ? "" : etherscanLinkHtml(link);
let html = "";
if (title) {
html += `<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>`;
}
if (ensName) {
html += `<div class="flex items-center font-bold">${title ? "" : dot}${escapeHtml(ensName)}</div>`;
}
if (title || ensName) {
html += `<div class="flex items-center">${copyableHtml(displayAddr, "break-all")}${extLink}</div>`;
} else {
html += `<div class="flex items-center">${dot}${copyableHtml(displayAddr, "break-all")}${extLink}</div>`;
}
return html;
}
function flashCopyFeedback(el) {
if (!el) return;
el.classList.remove("copy-flash-fade");
@@ -299,6 +417,10 @@ module.exports = {
showError,
hideError,
showView,
setRenderMain,
pushCurrentView,
goBack,
clearViewStack,
showFlash,
flashCopyFeedback,
balanceLine,
@@ -308,6 +430,12 @@ module.exports = {
escapeHtml,
addressTitle,
formatAddressHtml,
renderAddressHtml,
copyableHtml,
attachCopyHandlers,
etherscanAddressUrl,
etherscanLinkHtml,
EXT_ICON,
truncateMiddle,
isoDate,
timeAgo,

View File

@@ -10,13 +10,11 @@ const {
addressTitle,
escapeHtml,
truncateMiddle,
renderAddressHtml,
attachCopyHandlers,
pushCurrentView,
} = require("./helpers");
const {
state,
saveState,
currentAddress,
currentNetwork,
} = require("../../shared/state");
const { state, saveState, currentAddress } = require("../../shared/state");
const {
updateSendBalance,
renderSendTokenSelect,
@@ -74,28 +72,12 @@ function renderTotalValue() {
}
}
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
function renderActiveAddress() {
const el = $("active-address-display");
if (!el) return;
if (state.activeAddress) {
const addr = state.activeAddress;
const dot = addressDotHtml(addr);
const link = `${currentNetwork().explorerUrl}/address/${addr}`;
el.innerHTML =
`<span class="underline decoration-dashed cursor-pointer" id="active-addr-copy">${dot}${escapeHtml(addr)}</span>` +
`<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
$("active-addr-copy").addEventListener("click", (e) => {
navigator.clipboard.writeText(addr);
showFlash("Copied!");
flashCopyFeedback(e.currentTarget);
});
el.innerHTML = renderAddressHtml(state.activeAddress);
attachCopyHandlers(el);
} else {
el.textContent = "";
}
@@ -400,6 +382,7 @@ function init(ctx) {
renderSendTokenSelect(addr);
updateSendBalance();
resetSendValidation();
pushCurrentView();
showView("send");
});

View File

@@ -5,17 +5,12 @@ const {
flashCopyFeedback,
formatAddressHtml,
addressTitle,
attachCopyHandlers,
goBack,
} = require("./helpers");
const { state, currentAddress, currentNetwork } = require("../../shared/state");
const QRCode = require("qrcode");
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
function show() {
const addr = currentAddress();
const address = addr ? addr.address : "";
@@ -25,11 +20,8 @@ function show() {
? formatAddressHtml(address, ensName, null, title)
: "";
$("receive-address-block").dataset.full = address;
const net = currentNetwork();
const link = address ? `${net.explorerUrl}/address/${address}` : "";
$("receive-etherscan-link").innerHTML = link
? `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`
: "";
// Etherscan link is now included in formatAddressHtml via renderAddressHtml
$("receive-etherscan-link").innerHTML = "";
if (address) {
QRCode.toCanvas($("receive-qr"), address, {
width: 200,
@@ -62,18 +54,10 @@ function show() {
warningEl.style.visibility = "hidden";
}
showView("receive");
attachCopyHandlers("view-receive");
}
function init(ctx) {
$("receive-address-block").addEventListener("click", (e) => {
const addr = $("receive-address-block").dataset.full;
if (addr) {
navigator.clipboard.writeText(addr);
showFlash("Copied!");
flashCopyFeedback(e.currentTarget);
}
});
$("btn-receive-copy").addEventListener("click", () => {
const addr = $("receive-address-block").dataset.full;
if (addr) {
@@ -84,11 +68,7 @@ function init(ctx) {
});
$("btn-receive-back").addEventListener("click", () => {
if (state.selectedToken) {
ctx.showAddressToken();
} else {
ctx.showAddressDetail();
}
goBack();
});
}

View File

@@ -3,11 +3,13 @@
const {
$,
showFlash,
addressDotHtml,
addressTitle,
escapeHtml,
renderAddressHtml,
attachCopyHandlers,
goBack,
} = require("./helpers");
const { state, currentAddress, currentNetwork } = require("../../shared/state");
const { state, currentAddress } = require("../../shared/state");
let ctx;
const { getProvider } = require("../../shared/balances");
const { KNOWN_SYMBOLS, resolveSymbol } = require("../../shared/tokenList");
@@ -113,13 +115,6 @@ function updateToValidation() {
}
}
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
function isSpoofedToken(t) {
const upper = (t.symbol || "").toUpperCase();
if (!KNOWN_SYMBOLS.has(upper)) return false;
@@ -148,24 +143,12 @@ function renderSendTokenSelect(addr) {
function updateSendBalance() {
const addr = currentAddress();
if (!addr) return;
const dot = addressDotHtml(addr.address);
const link = `${currentNetwork().explorerUrl}/address/${addr.address}`;
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 = "";
if (title) {
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="break-all">${escapeHtml(addr.address)}${extLink}</div>`;
} else {
fromHtml += `<div class="flex items-center">${dot}<span class="break-all">${escapeHtml(addr.address)}</span>${extLink}</div>`;
}
$("send-from").innerHTML = fromHtml;
$("send-from").innerHTML = renderAddressHtml(addr.address, {
title,
ensName: addr.ensName,
});
attachCopyHandlers($("send-from"));
const token = state.selectedToken || $("send-token").value;
if (token === "ETH") {
$("send-balance").textContent =
@@ -268,11 +251,7 @@ function init(_ctx) {
$("btn-send-back").addEventListener("click", () => {
$("send-token").classList.remove("hidden");
$("send-token-static").classList.add("hidden");
if (state.selectedToken) {
ctx.showAddressToken();
} else {
ctx.showAddressDetail();
}
goBack();
});
}

View File

@@ -4,6 +4,8 @@ const {
showFlash,
escapeHtml,
flashCopyFeedback,
goBack,
pushCurrentView,
} = require("./helpers");
const { applyTheme } = require("../theme");
const { state, saveState, currentNetwork } = require("../../shared/state");
@@ -103,6 +105,7 @@ function renderWalletListSettings() {
container.querySelectorAll(".btn-delete-wallet").forEach((btn) => {
btn.addEventListener("click", () => {
const idx = parseInt(btn.dataset.idx, 10);
pushCurrentView();
deleteWallet.show(idx);
});
});
@@ -380,8 +383,7 @@ function init(ctx) {
setRuntimeDebug(state.debugMode);
$("btn-settings-back").addEventListener("click", () => {
ctx.renderWalletList();
showView("main");
goBack();
});
}

View File

@@ -1,4 +1,4 @@
const { $, showView, showFlash } = require("./helpers");
const { $, showView, showFlash, goBack } = require("./helpers");
const { getTopTokens } = require("../../shared/tokenList");
const { state, saveState } = require("../../shared/state");
const { lookupTokenInfo } = require("../../shared/balances");
@@ -84,7 +84,7 @@ function init(_ctx) {
ctx = _ctx;
$("btn-settings-addtoken-back").addEventListener("click", () => {
ctx.showSettingsView();
goBack();
});
$("btn-settings-addtoken-select").addEventListener("click", async () => {

View File

@@ -6,11 +6,15 @@ const {
showView,
showFlash,
flashCopyFeedback,
addressDotHtml,
addressTitle,
escapeHtml,
isoDate,
timeAgo,
renderAddressHtml,
attachCopyHandlers,
copyableHtml,
etherscanLinkHtml,
goBack,
} = require("./helpers");
const { state, currentNetwork } = require("../../shared/state");
const { formatEther, formatUnits } = require("ethers");
@@ -18,13 +22,6 @@ const makeBlockie = require("ethereum-blockies-base64");
const { log, debugFetch } = require("../../shared/log");
const { decodeCalldata } = require("./approval");
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
let ctx;
/**
@@ -46,52 +43,17 @@ function getTransactionType(tx) {
return "Native ETH Transfer";
}
function copyableHtml(text, extraClass) {
const cls =
"underline decoration-dashed cursor-pointer" +
(extraClass ? " " + extraClass : "");
return `<span class="${cls}" data-copy="${escapeHtml(text)}">${escapeHtml(text)}</span>`;
}
function blockieHtml(address) {
const src = makeBlockie(address);
return `<img src="${src}" width="48" height="48" style="image-rendering:pixelated;border-radius:50%;display:inline-block">`;
}
function etherscanLinkHtml(url) {
return (
`<a href="${url}" target="_blank" rel="noopener" ` +
`class="inline-flex items-center"` +
`>${EXT_ICON}</a>`
);
}
function txAddressHtml(address, ensName, title) {
const blockie = blockieHtml(address);
const dot = addressDotHtml(address);
const link = `${currentNetwork().explorerUrl}/address/${address}`;
const extLink = etherscanLinkHtml(link);
let html = `<div class="mb-1">${blockie}</div>`;
if (title) {
html += `<div class="font-bold">${escapeHtml(title)}</div>`;
}
if (ensName) {
html +=
`<div class="flex items-center">${dot}` +
copyableHtml(ensName, "") +
`</div>` +
`<div class="flex items-center">${dot}` +
copyableHtml(address, "break-all") +
extLink +
`</div>`;
} else {
html +=
`<div class="flex items-center">${dot}` +
copyableHtml(address, "break-all") +
extLink +
`</div>`;
}
return html;
return (
`<div class="mb-1">${blockie}</div>` +
renderAddressHtml(address, { title, ensName })
);
}
function txHashHtml(hash) {
@@ -210,17 +172,7 @@ function render() {
copyableHtml(isoStr) + " (" + escapeHtml(timeAgo(tx.timestamp)) + ")";
$("tx-detail-status").textContent = tx.isError ? "Failed" : "Success";
showView("transaction");
document
.getElementById("view-transaction")
.querySelectorAll("[data-copy]")
.forEach((el) => {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(el);
};
});
attachCopyHandlers("view-transaction");
}
function showDetailField(sectionId, contentId, value) {
@@ -355,19 +307,14 @@ async function loadFullTxDetails(txHash, toAddress, isContractCall) {
detailsHtml += `<div class="mb-2">`;
detailsHtml += `<div class="text-muted">${escapeHtml(d.label)}</div>`;
if (d.address && d.isToken) {
// Token entry: show symbol on its own line, then dot + address + Etherscan link
const dot = addressDotHtml(d.address);
// Token entry: show symbol on its own line, then address via shared renderer
const tokenSymbol = d.value.match(/^(\S+)\s*\(/)?.[1];
if (tokenSymbol) {
detailsHtml += `<div class="font-bold">${escapeHtml(tokenSymbol)}</div>`;
}
const etherscanUrl = `${currentNetwork().explorerUrl}/token/${d.address}`;
detailsHtml += `<div class="flex items-center">${dot}${copyableHtml(d.address, "break-all")}${etherscanLinkHtml(etherscanUrl)}</div>`;
detailsHtml += renderAddressHtml(d.address);
} else if (d.address) {
// Protocol/contract entry: show name + Etherscan link
const dot = addressDotHtml(d.address);
const etherscanUrl = `${currentNetwork().explorerUrl}/address/${d.address}`;
detailsHtml += `<div class="flex items-center">${dot}${copyableHtml(d.value, "break-all")}${etherscanLinkHtml(etherscanUrl)}</div>`;
detailsHtml += renderAddressHtml(d.address);
} else {
detailsHtml += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
}
@@ -394,13 +341,7 @@ async function loadFullTxDetails(txHash, toAddress, isContractCall) {
// Bind copy handlers for new elements (including raw data now outside section)
const copyTargets = [section, rawSection].filter(Boolean);
for (const container of copyTargets) {
container.querySelectorAll("[data-copy]").forEach((el) => {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(el);
};
});
attachCopyHandlers(container);
}
} catch (e) {
log.errorf("loadCalldata failed:", e.message);
@@ -410,11 +351,7 @@ async function loadFullTxDetails(txHash, toAddress, isContractCall) {
function init(_ctx) {
ctx = _ctx;
$("btn-tx-back").addEventListener("click", () => {
if (state.selectedToken) {
ctx.showAddressToken();
} else {
ctx.showAddressDetail();
}
goBack();
});
}

View File

@@ -3,24 +3,19 @@
const {
$,
showView,
showFlash,
flashCopyFeedback,
addressDotHtml,
addressTitle,
escapeHtml,
renderAddressHtml,
attachCopyHandlers,
copyableHtml,
etherscanLinkHtml,
clearViewStack,
} = require("./helpers");
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { getProvider } = require("../../shared/balances");
const { log } = require("../../shared/log");
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
`</svg></span>`;
let ctx;
let elapsedTimer = null;
let pollTimer = null;
@@ -37,50 +32,19 @@ function clearTimers() {
}
function toAddressHtml(address) {
const dot = addressDotHtml(address);
const link = `${currentNetwork().explorerUrl}/address/${address}`;
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 underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(address)}">${escapeHtml(address)}</div>` +
extLink
);
}
return `<div class="flex items-center">${dot}<span class="break-all underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(address)}">${escapeHtml(address)}</span>${extLink}</div>`;
return renderAddressHtml(address, { title });
}
function txHashHtml(hash) {
const link = `${currentNetwork().explorerUrl}/tx/${hash}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
return (
`<span class="underline decoration-dashed cursor-pointer break-all" data-copy="${escapeHtml(hash)}">${escapeHtml(hash)}</span>` +
extLink
);
return copyableHtml(hash, "break-all") + etherscanLinkHtml(link);
}
function blockNumberHtml(blockNumber) {
const num = String(blockNumber);
const link = `${currentNetwork().explorerUrl}/block/${num}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
return (
`<span class="underline decoration-dashed cursor-pointer" data-copy="${escapeHtml(num)}">${escapeHtml(num)}</span>` +
extLink
);
}
function attachCopyHandlers(viewId) {
document
.getElementById(viewId)
.querySelectorAll("[data-copy]")
.forEach((el) => {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(el);
};
});
return copyableHtml(num) + etherscanLinkHtml(link);
}
function showWait(txInfo, txHash) {
@@ -258,10 +222,16 @@ function navigateBack() {
window.close();
return;
}
// After a completed transaction, reset the navigation stack
// and go directly to the address view (token or detail).
// Use require() lazily to call show() without the ctx push wrapper.
clearViewStack();
state.viewStack.push("main");
if (state.selectedToken) {
ctx.showAddressToken();
state.viewStack.push("address");
require("./addressToken").show();
} else {
ctx.showAddressDetail();
require("./addressDetail").show();
}
}

View File

@@ -39,6 +39,7 @@ const state = {
selectedAddress: null,
selectedToken: null,
viewData: {},
viewStack: [],
};
// Return the network configuration for the currently selected network.
@@ -74,6 +75,7 @@ async function saveState() {
selectedAddress: state.selectedAddress,
selectedToken: state.selectedToken,
viewData: state.viewData,
viewStack: state.viewStack,
};
await storageApi.set({ autistmask: persisted });
}
@@ -137,6 +139,7 @@ async function loadState() {
saved.selectedAddress !== undefined ? saved.selectedAddress : null;
state.selectedToken = saved.selectedToken || null;
state.viewData = saved.viewData || {};
state.viewStack = Array.isArray(saved.viewStack) ? saved.viewStack : [];
}
}