Compare commits

..

11 Commits

Author SHA1 Message Date
clawbot
d6a6e24c4e fix: make debug mode toggle work at runtime
All checks were successful
check / check (push) Successful in 13s
The debug/insecure warning banner was only controlled by the compile-time
DEBUG constant. The settings easter egg checkbox toggled state.debugMode
and the runtime log flag, but neither index.js nor helpers.js checked
these runtime values — the banner was created/updated based solely on
the compile-time constant.

Changes:
- Extract banner logic into updateDebugBanner() in helpers.js that
  checks isDebug() (combines compile-time DEBUG and runtime debugMode)
- Banner is dynamically created/removed: appears when debug is enabled,
  removed when disabled (no stale banners)
- index.js init() syncs runtime debug flag from persisted state before
  first render, then delegates to updateDebugBanner()
- settings.js calls updateDebugBanner() after the checkbox change so
  the banner immediately reflects the new state
- Debug mode persists across popup close/reopen via state.debugMode

Fixes the bug where the settings debug toggle had no visible effect.
2026-03-01 15:21:51 -08:00
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
a138a36710 fix: suppress USD display on testnet networks (#142)
All checks were successful
check / check (push) Successful in 24s
## Summary

Fixes USD prices still showing on the main view when connected to a testnet (e.g. Sepolia). The root cause was stale mainnet prices lingering in the in-memory price cache after switching networks.

### Root Cause

PR #137 correctly made `refreshPrices()` skip fetching on testnets, but the cached prices from a prior mainnet session remained in the `prices` object. All display functions (`getPrice()`, `getAddressValueUsd()`, etc.) used whatever was cached without checking which network was active.

### Changes

- **`src/shared/prices.js`**
  - `refreshPrices()` now clears the price cache when on a testnet instead of silently returning
  - New `clearPrices()` function empties the cache and resets the fetch timestamp
  - `getPrice()` returns null on testnets (defense-in-depth)
  - `getAddressValueUsd()`, `getWalletValueUsd()`, `getTotalValueUsd()` return null on testnets

- **`src/popup/views/settings.js`**
  - Network switcher immediately clears prices when switching to a testnet, so the UI updates without waiting for the next refresh cycle

closes #139

Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #142
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-01 20:31:45 +01:00
6b40fa8836 feat: show estimated USD value for ETH in approve-tx view (#141)
All checks were successful
check / check (push) Successful in 25s
The approve-tx view (dapp-initiated transaction approval) now shows the estimated USD value next to the ETH amount being transferred, using the existing `getPrice`/`formatUsd` from `shared/prices.js`.

This matches the behavior already present in the manual send confirmation view (`confirmTx.js`).

When ETH price is available, the value line shows e.g. `0.5000 ETH ($1,650.00)`. When price is unavailable, it falls back gracefully to just the ETH amount.

closes #138

Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: #141
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-01 20:16:00 +01:00
bc2aedaab6 fix: etherscan link on address-token page goes to token-specific URL (#136)
All checks were successful
check / check (push) Successful in 9s
When viewing the address-token page for our own address with an ERC-20 token, the etherscan link now navigates to the token-specific page (`etherscan.io/token/<contract>?a=<address>`) instead of the plain address page.

Closes #135

Co-authored-by: user <user@Mac.lan guest wan>
Co-authored-by: Jeffrey Paul <sneak@noreply.example.org>
Reviewed-on: #136
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
2026-03-01 20:13:55 +01:00
27 changed files with 714 additions and 476 deletions

View File

@@ -1,4 +1,3 @@
.git
node_modules
.DS_Store
dist

View File

@@ -1,7 +1,7 @@
# node:22-slim (22.x LTS), 2026-02-24
FROM node@sha256:5373f1906319b3a1f291da5d102f4ce5c77ccbe29eb637f072b6c7b70443fc36
RUN apt-get update && apt-get install -y --no-install-recommends make && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y --no-install-recommends make git && rm -rf /var/lib/apt/lists/*
RUN corepack enable && corepack prepare yarn@1.22.22 --activate
WORKDIR /app

View File

@@ -11,9 +11,51 @@ function ensureDir(dir) {
fs.mkdirSync(dir, { recursive: true });
}
function getBuildInfo() {
const pkg = JSON.parse(
fs.readFileSync(path.join(__dirname, "package.json"), "utf8"),
);
let commitHash = "unknown";
try {
commitHash = execSync("git rev-parse --short HEAD", {
encoding: "utf8",
}).trim();
} catch (_) {
// not a git repo or git not available
}
let commitHashFull = "unknown";
try {
commitHashFull = execSync("git rev-parse HEAD", {
encoding: "utf8",
}).trim();
} catch (_) {
// not a git repo or git not available
}
return {
version: pkg.version,
license: pkg.license,
author: pkg.author,
commitHash,
commitHashFull,
buildDate: new Date().toISOString().slice(0, 10),
};
}
async function build() {
console.log("Building AutistMask extension...");
const buildInfo = getBuildInfo();
console.log("Build info:", buildInfo);
const define = {
__BUILD_VERSION__: JSON.stringify(buildInfo.version),
__BUILD_LICENSE__: JSON.stringify(buildInfo.license),
__BUILD_AUTHOR__: JSON.stringify(buildInfo.author),
__BUILD_COMMIT__: JSON.stringify(buildInfo.commitHash),
__BUILD_COMMIT_FULL__: JSON.stringify(buildInfo.commitHashFull),
__BUILD_DATE__: JSON.stringify(buildInfo.buildDate),
};
// compile tailwind CSS
console.log("Compiling Tailwind CSS...");
const tailwindInput = path.join(SRC, "popup", "styles", "main.css");
@@ -38,6 +80,7 @@ async function build() {
platform: "browser",
target: ["chrome110", "firefox110"],
minify: true,
define,
});
// bundle background script
@@ -49,6 +92,7 @@ async function build() {
platform: "browser",
target: ["chrome110", "firefox110"],
minify: true,
define,
});
// bundle content script
@@ -60,6 +104,7 @@ async function build() {
platform: "browser",
target: ["chrome110", "firefox110"],
minify: true,
define,
});
// bundle inpage script (injected into page context, separate file)
@@ -71,6 +116,7 @@ async function build() {
platform: "browser",
target: ["chrome110", "firefox110"],
minify: true,
define,
});
// copy popup HTML

View File

@@ -4,6 +4,7 @@
const { DEFAULT_RPC_URL } = require("../shared/constants");
const { SUPPORTED_CHAIN_IDS, networkByChainId } = require("../shared/networks");
const { onChainSwitch } = require("../shared/chainSwitch");
const { getBytes } = require("ethers");
const {
state,
@@ -345,12 +346,8 @@ async function handleRpc(method, params, origin) {
return { result: null };
}
if (SUPPORTED_CHAIN_IDS.has(chainId)) {
// Switch to the requested network
const target = networkByChainId(chainId);
state.networkId = target.id;
state.rpcUrl = target.defaultRpcUrl;
state.blockscoutUrl = target.defaultBlockscoutUrl;
await saveState();
await onChainSwitch(target.id);
broadcastChainChanged(target.chainId);
return { result: null };
}

View File

@@ -1002,6 +1002,64 @@
</p>
<div id="settings-denied-sites"></div>
</div>
<div class="bg-well p-3 mx-1 mb-3">
<h3 class="font-bold mb-1">About</h3>
<p class="text-xs mb-2">
<a
href="https://git.eeqj.de/sneak/AutistMask"
class="underline decoration-dashed"
target="_blank"
rel="noopener noreferrer"
>AutistMask</a
>
— Minimal Ethereum wallet browser extension.
</p>
<div class="text-xs">
<div class="mb-1">
<span class="text-muted">License:</span>
<span id="about-license"></span>
</div>
<div class="mb-1">
<span class="text-muted">Author:</span>
<span id="about-author"></span>
</div>
<div class="mb-1">
<span class="text-muted">Version:</span>
<span
id="about-version"
class="cursor-pointer select-none"
></span>
</div>
<div class="mb-1">
<span class="text-muted">Release date:</span>
<span id="about-release-date"></span>
</div>
<div>
<span class="text-muted">Commit:</span>
<a
id="about-commit-link"
class="underline decoration-dashed"
target="_blank"
rel="noopener noreferrer"
></a>
</div>
</div>
</div>
<div
id="settings-debug-well"
class="bg-well p-3 mx-1 mb-3"
style="display: none"
>
<h3 class="font-bold mb-1">Debug</h3>
<label
class="text-xs flex items-center gap-1 cursor-pointer"
>
<input type="checkbox" id="settings-debug-mode" />
Enable debug mode
</label>
</div>
</div>
<!-- ============ DELETE WALLET CONFIRM ============ -->

View File

@@ -1,11 +1,24 @@
// AutistMask popup entry point.
// 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 { isDebug, setRuntimeDebug } = require("../shared/log");
const { refreshPrices } = require("../shared/prices");
const { refreshBalances } = require("../shared/balances");
const { $, showView } = require("./views/helpers");
const {
$,
showView,
updateDebugBanner,
setRenderMain,
pushCurrentView,
goBack,
clearViewStack,
} = require("./views/helpers");
const { applyTheme } = require("./theme");
const home = require("./views/home");
@@ -53,15 +66,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 +207,15 @@ function fallbackView() {
}
async function init() {
if (DEBUG) {
const banner = document.createElement("div");
banner.id = "debug-banner";
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);
// Sync runtime debug flag from persisted state before first render
setRuntimeDebug(state.debugMode);
// Create the debug/testnet banner if needed (uses runtime debug state)
updateDebugBanner();
// Auto-default active address
if (
state.activeAddress === null &&
@@ -208,13 +245,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

@@ -10,7 +10,7 @@
--color-border: #000000;
--color-border-light: #cccccc;
--color-hover: #eeeeee;
--color-well: #f5f5f5;
--color-well: #e8e8e8;
--color-danger-well: #fef2f2;
--color-section: #dddddd;
}

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 {
ensEl.classList.add("hidden");
}
// 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,17 +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 isoDate(timestamp) {
const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0");
@@ -153,12 +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 = 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;
@@ -198,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)
@@ -218,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 = "";
@@ -339,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) {
@@ -358,7 +333,7 @@ function init(_ctx) {
});
$("btn-address-token-back").addEventListener("click", () => {
ctx.showAddressDetail();
goBack();
});
$("btn-address-token-send").addEventListener("click", () => {
@@ -385,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,14 +1,16 @@
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");
const { getPrice, formatUsd } = require("../../shared/prices");
const { ERC20_ABI } = require("../../shared/constants");
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
const txStatus = require("./txStatus");
@@ -16,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) {
@@ -52,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) {
@@ -234,17 +215,19 @@ 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)");
}
const ethValueFormatted = formatTxValue(
formatEther(details.txParams.value || "0"),
);
const ethPrice = getPrice("ETH");
const ethUsd = ethPrice ? parseFloat(ethValueFormatted) * ethPrice : null;
const usdStr = formatUsd(ethUsd);
$("approve-tx-value").textContent =
formatTxValue(formatEther(details.txParams.value || "0")) + " ETH";
ethValueFormatted + " ETH" + (usdStr ? " (" + usdStr + ")" : "");
// Decode calldata (reuse decoded from above)
const decodedEl = $("approve-tx-decoded");
@@ -259,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>`;
}
@@ -288,6 +268,7 @@ function showTxApproval(details) {
hideError("approve-tx-error");
showView("approve-tx");
attachCopyHandlers("view-approve-tx");
}
function decodeHexMessage(hex) {
@@ -385,6 +366,7 @@ function showSignApproval(details) {
$("btn-approve-sign").classList.remove("text-muted");
showView("approve-sign");
attachCopyHandlers("view-approve-sign");
}
function show(id) {
@@ -412,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

@@ -1,12 +1,13 @@
// Shared DOM helpers used by all views.
const { DEBUG } = require("../../shared/constants");
const { isDebug } = require("../../shared/log");
const {
formatUsd,
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,14 +60,77 @@ function showView(name) {
clearFlash();
state.currentView = name;
saveState();
if (DEBUG) {
const banner = document.getElementById("debug-banner");
if (banner) {
banner.textContent = "DEBUG / INSECURE (" + name + ")";
updateDebugBanner(name);
}
// Create or update the debug/insecure warning banner.
// Called on every view switch and after the settings debug toggle changes.
// The banner is shown when the compile-time DEBUG constant is true OR when
// the user has enabled runtime debug mode via the settings easter egg, OR
// when the active network is a testnet.
function updateDebugBanner(viewName) {
const debug = isDebug();
const net = currentNetwork();
const show = debug || net.isTestnet;
let banner = document.getElementById("debug-banner");
if (show) {
if (!banner) {
banner = document.createElement("div");
banner.id = "debug-banner";
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);
}
const suffix = viewName ? " (" + viewName + ")" : "";
if (debug && net.isTestnet) {
banner.textContent = "DEBUG / INSECURE [TESTNET]" + suffix;
} else if (net.isTestnet) {
banner.textContent = "[TESTNET]" + suffix;
} else {
banner.textContent = "DEBUG / INSECURE" + suffix;
}
} else if (banner) {
banner.remove();
}
}
// 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;
function clearFlash() {
@@ -208,21 +272,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 +333,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 +436,11 @@ module.exports = {
showError,
hideError,
showView,
updateDebugBanner,
setRenderMain,
pushCurrentView,
goBack,
clearViewStack,
showFlash,
flashCopyFeedback,
balanceLine,
@@ -308,6 +450,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

@@ -1,14 +1,34 @@
const { $, showView, showFlash, escapeHtml } = require("./helpers");
const {
$,
showView,
updateDebugBanner,
showFlash,
escapeHtml,
flashCopyFeedback,
goBack,
pushCurrentView,
} = require("./helpers");
const { applyTheme } = require("../theme");
const { state, saveState, currentNetwork } = require("../../shared/state");
const { NETWORKS, SUPPORTED_CHAIN_IDS } = require("../../shared/networks");
const { clearPrices } = require("../../shared/prices");
const { log, debugFetch } = require("../../shared/log");
const { onChainSwitch } = require("../../shared/chainSwitch");
const { log, debugFetch, setRuntimeDebug } = require("../../shared/log");
const deleteWallet = require("./deleteWallet");
const {
BUILD_VERSION,
BUILD_LICENSE,
BUILD_AUTHOR,
BUILD_COMMIT,
BUILD_DATE,
GITEA_COMMIT_URL,
} = require("../../shared/buildInfo");
const runtime =
typeof browser !== "undefined" ? browser.runtime : chrome.runtime;
let versionClickCount = 0;
let versionClickTimer = null;
function renderSiteList(containerId, siteMap, stateKey) {
const container = $(containerId);
const hostnames = [...new Set(Object.values(siteMap).flat())];
@@ -86,6 +106,7 @@ function renderWalletListSettings() {
container.querySelectorAll(".btn-delete-wallet").forEach((btn) => {
btn.addEventListener("click", () => {
const idx = parseInt(btn.dataset.idx, 10);
pushCurrentView();
deleteWallet.show(idx);
});
});
@@ -134,6 +155,28 @@ function show() {
renderSiteLists();
renderWalletListSettings();
// Populate About well
$("about-license").textContent = BUILD_LICENSE;
// Show only the name part of the author field (strip email)
const authorName = BUILD_AUTHOR.replace(/\s*<[^>]+>/, "");
$("about-author").textContent = authorName;
$("about-version").textContent = BUILD_VERSION;
$("about-release-date").textContent = BUILD_DATE;
$("about-commit-link").textContent = BUILD_COMMIT;
$("about-commit-link").href = GITEA_COMMIT_URL;
// Reset version click counter each time settings opens
versionClickCount = 0;
// Show debug well if debug mode is already enabled
const debugWell = $("settings-debug-well");
if (state.debugMode) {
debugWell.style.display = "";
} else {
debugWell.style.display = "none";
}
$("settings-debug-mode").checked = state.debugMode;
showView("settings");
}
@@ -221,15 +264,9 @@ function init(ctx) {
if (networkSelect) {
networkSelect.addEventListener("change", async () => {
const newId = networkSelect.value;
const net = NETWORKS[newId];
if (!net) return;
state.networkId = newId;
state.rpcUrl = net.defaultRpcUrl;
state.blockscoutUrl = net.defaultBlockscoutUrl;
const net = await onChainSwitch(newId);
$("settings-rpc").value = state.rpcUrl;
$("settings-blockscout").value = state.blockscoutUrl;
if (net.isTestnet) clearPrices();
await saveState();
showFlash("Switched to " + net.name + ".");
});
}
@@ -287,9 +324,68 @@ function init(ctx) {
ctx.showSettingsAddTokenView,
);
// Bright saturated colors for easter egg flashes (clicks 610)
const easterEggColors = [
"#ff0055", // hot pink
"#00cc44", // vivid green
"#3366ff", // electric blue
"#ff9900", // bright orange
"#aa00ff", // vivid purple
];
// Easter egg: click version 10 times to reveal the debug well.
// Each click does a copy-flash animation. After 5 clicks, each
// additional click flashes a different bright saturated color.
$("about-version").addEventListener("click", () => {
versionClickCount++;
clearTimeout(versionClickTimer);
// Reset counter if user stops clicking for 3 seconds
versionClickTimer = setTimeout(() => {
versionClickCount = 0;
}, 3000);
const el = $("about-version");
if (versionClickCount > 5) {
// Colored flash for clicks 610
const colorIdx = versionClickCount - 6;
const color = easterEggColors[colorIdx % easterEggColors.length];
el.classList.remove("copy-flash-fade");
el.style.backgroundColor = color;
el.style.color = "#ffffff";
setTimeout(() => {
el.style.backgroundColor = "";
el.style.color = "";
el.classList.add("copy-flash-fade");
setTimeout(() => {
el.classList.remove("copy-flash-fade");
}, 275);
}, 75);
} else {
// Standard copy-flash for clicks 15
flashCopyFeedback(el);
}
if (versionClickCount >= 10) {
versionClickCount = 0;
clearTimeout(versionClickTimer);
$("settings-debug-well").style.display = "";
}
});
// Debug mode toggle — update runtime flag, persist, and re-render banner
$("settings-debug-mode").addEventListener("change", async () => {
state.debugMode = $("settings-debug-mode").checked;
setRuntimeDebug(state.debugMode);
await saveState();
updateDebugBanner(state.currentView);
});
// Sync runtime debug flag on init
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();
}
}

35
src/shared/buildInfo.js Normal file
View File

@@ -0,0 +1,35 @@
// Build-time constants injected by esbuild define in build.js.
// These globals are replaced at bundle time with string literals.
/* global __BUILD_VERSION__, __BUILD_LICENSE__, __BUILD_AUTHOR__,
__BUILD_COMMIT__, __BUILD_COMMIT_FULL__, __BUILD_DATE__ */
const BUILD_VERSION =
typeof __BUILD_VERSION__ !== "undefined" ? __BUILD_VERSION__ : "dev";
const BUILD_LICENSE =
typeof __BUILD_LICENSE__ !== "undefined" ? __BUILD_LICENSE__ : "GPL-3.0";
const BUILD_AUTHOR =
typeof __BUILD_AUTHOR__ !== "undefined"
? __BUILD_AUTHOR__
: "sneak <sneak@sneak.berlin>";
const BUILD_COMMIT =
typeof __BUILD_COMMIT__ !== "undefined" ? __BUILD_COMMIT__ : "unknown";
const BUILD_COMMIT_FULL =
typeof __BUILD_COMMIT_FULL__ !== "undefined"
? __BUILD_COMMIT_FULL__
: "unknown";
const BUILD_DATE =
typeof __BUILD_DATE__ !== "undefined" ? __BUILD_DATE__ : "unknown";
const GITEA_COMMIT_URL =
"https://git.eeqj.de/sneak/AutistMask/commit/" + BUILD_COMMIT_FULL;
module.exports = {
BUILD_VERSION,
BUILD_LICENSE,
BUILD_AUTHOR,
BUILD_COMMIT,
BUILD_COMMIT_FULL,
BUILD_DATE,
GITEA_COMMIT_URL,
};

57
src/shared/chainSwitch.js Normal file
View File

@@ -0,0 +1,57 @@
// Consolidated chain-switch handler.
//
// Every state change required when the active network changes is
// performed here so that callers (settings UI, background
// wallet_switchEthereumChain, future chain additions) all go
// through a single code path.
//
// Adding a new chain (e.g. ETC) requires only a new entry in
// networks.js — no per-caller wiring is needed.
const { networkById } = require("./networks");
const { clearPrices } = require("./prices");
// Switch the active chain and reset all chain-specific cached state.
// Returns the network configuration object for the new chain.
async function onChainSwitch(newNetworkId) {
const { state, saveState } = require("./state");
const net = networkById(newNetworkId);
// --- core identity ---
state.networkId = net.id;
state.rpcUrl = net.defaultRpcUrl;
state.blockscoutUrl = net.defaultBlockscoutUrl;
// --- price cache ---
// Prices are chain-specific (testnet tokens are worthless,
// ETC has different pricing, etc.).
clearPrices();
// --- balance / refresh state ---
// Reset last-refresh timestamp so the next polling cycle
// triggers an immediate balance refresh on the new chain.
state.lastBalanceRefresh = 0;
// Clear per-address balances and token balances so stale data
// from the previous chain is never displayed while the first
// refresh on the new chain is in flight.
for (const wallet of state.wallets) {
for (const addr of wallet.addresses) {
addr.balance = "0";
addr.tokenBalances = [];
}
}
// --- chain-specific caches ---
// Token holder counts and fraud contract lists are
// chain-specific and must not carry over.
state.tokenHolderCache = {};
state.fraudContracts = [];
await saveState();
return net;
}
module.exports = { onChainSwitch };

View File

@@ -1,12 +1,27 @@
// Leveled logger. Outputs to console with [AutistMask] prefix.
// Level is DEBUG when the DEBUG constant is true, INFO otherwise.
// Level is DEBUG when the compile-time DEBUG constant is true or the runtime
// debugMode state flag is enabled. The runtime flag is checked lazily so it
// responds immediately when toggled in settings.
const { DEBUG } = require("./constants");
const LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
const threshold = DEBUG ? LEVELS.debug : LEVELS.info;
// Runtime debug mode flag — set by settings.js when the user toggles debug
// mode via the easter egg. Kept here as a simple mutable reference so it can
// be updated without circular dependency issues with state.js.
let _runtimeDebug = false;
function setRuntimeDebug(enabled) {
_runtimeDebug = enabled;
}
function isDebug() {
return DEBUG || _runtimeDebug;
}
function emit(level, method, args) {
const threshold = isDebug() ? LEVELS.debug : LEVELS.info;
if (LEVELS[level] >= threshold) {
console[method]("[AutistMask]", ...args);
}
@@ -37,4 +52,4 @@ async function debugFetch(url, opts) {
return resp;
}
module.exports = { log, debugFetch };
module.exports = { log, debugFetch, setRuntimeDebug, isDebug };

View File

@@ -26,6 +26,8 @@ async function refreshPrices() {
}
}
// Clear all cached prices and reset the fetch timestamp so the
// next refreshPrices() call will fetch fresh data.
function clearPrices() {
for (const key of Object.keys(prices)) {
delete prices[key];
@@ -33,6 +35,7 @@ function clearPrices() {
lastFetchedAt = 0;
}
// Return the USD price for a symbol, or null on testnet / unknown.
function getPrice(symbol) {
const { currentNetwork } = require("./state");
if (currentNetwork().isTestnet) return null;

View File

@@ -29,6 +29,7 @@ const DEFAULT_STATE = {
fraudContracts: [],
tokenHolderCache: {},
theme: "system",
debugMode: false,
};
const state = {
@@ -38,6 +39,7 @@ const state = {
selectedAddress: null,
selectedToken: null,
viewData: {},
viewStack: [],
};
// Return the network configuration for the currently selected network.
@@ -67,11 +69,13 @@ async function saveState() {
fraudContracts: state.fraudContracts,
tokenHolderCache: state.tokenHolderCache,
theme: state.theme,
debugMode: state.debugMode,
currentView: state.currentView,
selectedWallet: state.selectedWallet,
selectedAddress: state.selectedAddress,
selectedToken: state.selectedToken,
viewData: state.viewData,
viewStack: state.viewStack,
};
await storageApi.set({ autistmask: persisted });
}
@@ -126,6 +130,8 @@ async function loadState() {
state.fraudContracts = saved.fraudContracts || [];
state.tokenHolderCache = saved.tokenHolderCache || {};
state.theme = saved.theme || "system";
state.debugMode =
saved.debugMode !== undefined ? saved.debugMode : false;
state.currentView = saved.currentView || null;
state.selectedWallet =
saved.selectedWallet !== undefined ? saved.selectedWallet : null;
@@ -133,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 : [];
}
}