Compare commits

...

18 Commits

Author SHA1 Message Date
user
e13af842df feat: reorganize transaction detail view layout
All checks were successful
check / check (push) Successful in 22s
- Move transaction hash (txid) to first position after title
- Group fields into logical sections with visual dividers:
  Identity (hash, type, status, time), Value (amount, native qty,
  token contract), Parties (from, to), Protocol (calldata/action),
  On-chain (block, nonce, fee, gas price, gas used), Raw data
- Add tx-detail-group CSS class for subtle border separators
- Show on-chain details group wrapper only when data is loaded
- Maintain all existing functionality and copy-to-clipboard behavior

closes #131
2026-03-01 07:23:46 -08:00
6aeab54e8c Merge pull request 'fix: correct swap display — output token, min received, from address, and amount' (#128) from fix/issue-127-swap-amount-display into main
All checks were successful
check / check (push) Successful in 9s
Reviewed-on: #128
2026-03-01 16:19:41 +01:00
f65764d501 Merge branch 'main' into fix/issue-127-swap-amount-display
All checks were successful
check / check (push) Successful in 21s
2026-03-01 16:19:26 +01:00
4e097c1e32 Merge pull request 'feat: add Type field and on-chain details to transaction detail view' (#130) from fix/issue-95-transaction-type-display into main
All checks were successful
check / check (push) Successful in 8s
Reviewed-on: #130
2026-03-01 15:46:33 +01:00
clawbot
3f6f98dcaf docs: update TransactionDetail screen map with new fields
All checks were successful
check / check (push) Successful in 22s
Add Type, Token contract, Block, Nonce, Transaction fee, Gas price,
and Gas used fields to the TransactionDetail section in the README
screen map to match the implemented UI.
2026-03-01 06:17:40 -08:00
user
3e900dc14c feat: add Type field and on-chain details to transaction detail view
All checks were successful
check / check (push) Successful in 9s
- Always display a Type field as the first item under the Transaction
  heading, identifying the transaction as: Native ETH Transfer, ERC-20
  Token Transfer, Swap, Token Approval, Contract Call, or Contract Creation
- Show token contract address with identicon for ERC-20 transfers
- Fetch and display on-chain details from Blockscout: block number,
  nonce, transaction fee, gas price, and gas used
- All new fields are click-copyable with Etherscan links where applicable

closes #95
2026-03-01 06:11:35 -08:00
user
5dfc6e332b fix: correct swap display — output token, min received, from address, and amount
All checks were successful
check / check (push) Successful in 23s
Fix multi-step Uniswap swap decoding and transaction display:

1. uniswap.js: In multi-step swaps (e.g. V3 → V4), the output token and
   min received amount now come from the LAST swap step instead of the
   first. Previously, an intermediate token's amountOutMin (18 decimals)
   was formatted with the final token's decimals (6), producing
   astronomically wrong 'Min. received' values (~2 trillion USDC).

2. transactions.js: Contract call token transfers (swaps) are now
   consolidated into the original transaction entry instead of creating
   separate entries per token transfer. This prevents intermediate hop
   tokens (e.g. USDS in a USDT→USDS→USDC route) from appearing as the
   transaction's Amount. The received token (swap output) is preferred.

3. transactions.js: The original transaction's from/to addresses are
   preserved for contract calls, so the user sees their own address
   instead of a router or Permit2 contract address.

closes #127
2026-03-01 05:52:10 -08:00
a182aa534b Merge pull request 'fix: include timezone offset in all displayed timestamps (closes #116)' (#120) from fix/issue-116-timestamp-timezone into main
All checks were successful
check / check (push) Successful in 9s
Reviewed-on: #120
2026-03-01 13:36:04 +01:00
a388100262 Merge branch 'main' into fix/issue-116-timestamp-timezone
All checks were successful
check / check (push) Successful in 22s
2026-03-01 13:35:26 +01:00
dd3cabf816 Merge pull request 'feat: add theme setting (Light/Dark/System) with dark mode — closes #125' (#126) from feat/issue-125-dark-mode into main
All checks were successful
check / check (push) Successful in 10s
Reviewed-on: #126
2026-03-01 13:35:11 +01:00
user
235e5e7fa7 fix: improve dark mode contrast for hover, well, section, and border colors
All checks were successful
check / check (push) Successful in 22s
2026-03-01 03:49:18 -08:00
user
be06bd8f0c fix: improve dark mode contrast for wells and balance display
All checks were successful
check / check (push) Successful in 10s
- Change dark mode --color-well from #0a0a0a to #111111 for visible
  contrast against #000000 background
- Add explicit text-fg class to balance display element to ensure
  white text in dark mode
2026-03-01 03:38:27 -08:00
user
a72359432b fix: include timezone offset in all displayed timestamps
All checks were successful
check / check (push) Successful in 21s
All isoDate() functions now output proper ISO 8601 format with timezone
offset (e.g. 2026-02-28T15:30:00-08:00) instead of bare datetime strings.
Also uses 'T' separator per ISO 8601.

closes #116
2026-03-01 03:36:49 -08:00
user
2bdb547995 feat: add theme setting (Light/Dark/System) with dark mode
Add theme preference (light/dark/system) stored in extension state.
System mode follows prefers-color-scheme and listens for changes.
Dark mode inverts the monochrome palette (white-on-black).
Theme selector added to Display section in settings.

Closes #125
2026-03-01 03:36:42 -08:00
834228b572 Merge pull request 'fix: reserve space for all error/status messages — closes #123' (#124) from fix/issue-123-layout-shift-audit into main
All checks were successful
check / check (push) Successful in 8s
Reviewed-on: #124
2026-03-01 12:33:08 +01:00
clawbot
813993f17c fix: reserve space for all error/status messages — closes #123
All checks were successful
check / check (push) Successful in 22s
Replace display:none (hidden class) with visibility:hidden/visible for all
error, warning, and status message elements across the extension UI. This
prevents layout shift when messages appear or disappear.

Changes:
- helpers.js: showError/hideError now use visibility instead of hidden class
- index.html: all error/status divs use visibility:hidden + min-height
- confirmTx.js: warnings, errors, fee section use visibility
- approval.js: tx-error, sign-error, danger-warning use visibility
- addressDetail.js: export-privkey-flash uses visibility
- deleteWallet.js: delete-wallet-flash uses visibility
- addWallet.js: phrase-warning uses visibility
- receive.js: erc20-warning uses visibility
- addToken.js: add-token-info uses visibility
- settingsAddToken.js: settings-addtoken-info uses visibility
2026-02-28 16:30:43 -08:00
5f01d9f111 Merge pull request 'feat: speed up copy-flash timing by ~25% — follow-up to #113' (#121) from fix/issue-100-faster-copy-flash into main
All checks were successful
check / check (push) Successful in 11s
Reviewed-on: #121
2026-03-01 01:21:24 +01:00
user
d78af3ec80 feat: speed up copy-flash timing by ~25%
All checks were successful
check / check (push) Successful in 20s
Reduce active phase from 100ms to 75ms, fade transition from 300ms to
225ms, and cleanup delay from 350ms to 275ms for snappier feedback.

Refs #100
2026-02-28 16:17:07 -08:00
20 changed files with 565 additions and 132 deletions

View File

@@ -437,6 +437,10 @@ transitions.
- **When**: User tapped a transaction row from AddressDetail or AddressToken. - **When**: User tapped a transaction row from AddressDetail or AddressToken.
- **Elements**: - **Elements**:
- "Transaction" heading, "Back" button - "Transaction" heading, "Back" button
- Type: transaction classification — one of: Native ETH Transfer, ERC-20
Token Transfer, Swap, Token Approval, Contract Call, Contract Creation
- Token contract: shown for ERC-20 transfers — color dot + full contract
address (tap to copy) + etherscan token link
- Status: "Success" or "Failed" - Status: "Success" or "Failed"
- Time: ISO datetime + relative age in parentheses - Time: ISO datetime + relative age in parentheses
- Amount: value + symbol (bold) - Amount: value + symbol (bold)
@@ -445,6 +449,11 @@ transitions.
- To: blockie + color dot + full address (tap to copy) + etherscan link - To: blockie + color dot + full address (tap to copy) + etherscan link
- ENS name if available - ENS name if available
- Transaction hash: full hash (tap to copy) + etherscan link - Transaction hash: full hash (tap to copy) + etherscan link
- Block: block number (tap to copy) + etherscan block link
- Nonce: transaction nonce (tap to copy)
- Transaction fee: ETH amount (tap to copy)
- Gas price: value in Gwei (tap to copy)
- Gas used: integer (tap to copy)
- **Transitions**: - **Transitions**:
- "Back" → **AddressToken** (if `selectedToken` set) or **AddressDetail** - "Back" → **AddressToken** (if `selectedToken` set) or **AddressDetail**

View File

@@ -107,7 +107,8 @@
</div> </div>
<div <div
id="add-wallet-phrase-warning" id="add-wallet-phrase-warning"
class="text-xs mb-2 border border-border border-dashed p-2 hidden" class="text-xs mb-2 border border-border border-dashed p-2"
style="visibility: hidden"
> >
Write these words down and keep them safe. Anyone with Write these words down and keep them safe. Anyone with
them can take your funds; if you lose them, your wallet them can take your funds; if you lose them, your wallet
@@ -184,7 +185,7 @@
<!-- active address headline --> <!-- active address headline -->
<div <div
id="total-value" id="total-value"
class="text-2xl font-bold min-h-[2rem]" class="text-2xl font-bold min-h-[2rem] text-fg"
></div> ></div>
<div <div
id="total-value-sub" id="total-value-sub"
@@ -375,7 +376,8 @@
</p> </p>
<div <div
id="export-privkey-flash" id="export-privkey-flash"
class="text-xs mb-2 hidden" class="text-xs mb-2 min-h-[1.25rem]"
style="visibility: hidden"
></div> ></div>
<div id="export-privkey-password-section" class="mb-2"> <div id="export-privkey-password-section" class="mb-2">
<label class="block mb-1">Password</label> <label class="block mb-1">Password</label>
@@ -579,13 +581,17 @@
<div class="text-xs text-muted mb-1">Your balance</div> <div class="text-xs text-muted mb-1">Your balance</div>
<div id="confirm-balance" class="text-xs"></div> <div id="confirm-balance" class="text-xs"></div>
</div> </div>
<div id="confirm-fee" class="mb-3 hidden"> <div id="confirm-fee" class="mb-3" style="visibility: hidden">
<div class="text-xs text-muted mb-1"> <div class="text-xs text-muted mb-1">
Estimated network fee Estimated network fee
</div> </div>
<div id="confirm-fee-amount" class="text-xs"></div> <div id="confirm-fee-amount" class="text-xs"></div>
</div> </div>
<div id="confirm-warnings" class="mb-2 hidden"></div> <div
id="confirm-warnings"
class="mb-2"
style="visibility: hidden"
></div>
<div <div
id="confirm-recipient-warning" id="confirm-recipient-warning"
class="mb-2" class="mb-2"
@@ -601,7 +607,8 @@
</div> </div>
<div <div
id="confirm-errors" id="confirm-errors"
class="mb-2 border border-border border-dashed p-2 hidden" class="mb-2 border border-border border-dashed p-2"
style="visibility: hidden; min-height: 1.25rem"
></div> ></div>
<div class="mb-2"> <div class="mb-2">
<label class="block mb-1 text-xs">Password</label> <label class="block mb-1 text-xs">Password</label>
@@ -614,6 +621,7 @@
<div <div
id="confirm-tx-password-error" id="confirm-tx-password-error"
class="text-xs mb-2 min-h-[1.25rem]" class="text-xs mb-2 min-h-[1.25rem]"
style="visibility: hidden"
></div> ></div>
<button <button
id="btn-confirm-send" id="btn-confirm-send"
@@ -728,7 +736,8 @@
</button> </button>
<div <div
id="receive-erc20-warning" id="receive-erc20-warning"
class="text-xs border border-border border-dashed p-2 mt-3 hidden" class="text-xs border border-border border-dashed p-2 mt-3"
style="visibility: hidden"
></div> ></div>
</div> </div>
@@ -756,7 +765,8 @@
</div> </div>
<div <div
id="add-token-info" id="add-token-info"
class="text-xs text-muted mb-2 hidden" class="text-xs text-muted mb-2 min-h-[1.25rem]"
style="visibility: hidden"
></div> ></div>
<div class="mb-2"> <div class="mb-2">
<label class="block mb-1 text-xs text-muted" <label class="block mb-1 text-xs text-muted"
@@ -814,7 +824,7 @@
<div class="bg-well p-3 mx-1 mb-3"> <div class="bg-well p-3 mx-1 mb-3">
<h3 class="font-bold mb-1">Display</h3> <h3 class="font-bold mb-1">Display</h3>
<label <label
class="text-xs flex items-center gap-1 cursor-pointer" class="text-xs flex items-center gap-1 cursor-pointer mb-2"
> >
<input <input
type="checkbox" type="checkbox"
@@ -822,6 +832,17 @@
/> />
Show tracked tokens with zero balance Show tracked tokens with zero balance
</label> </label>
<div class="text-xs flex items-center gap-1">
<label for="settings-theme">Theme:</label>
<select
id="settings-theme"
class="border border-border p-1 bg-bg text-fg text-xs cursor-pointer"
>
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
</div> </div>
<div class="bg-well p-3 mx-1 mb-3"> <div class="bg-well p-3 mx-1 mb-3">
@@ -903,6 +924,12 @@
/> />
<span class="text-xs text-muted">gwei</span> <span class="text-xs text-muted">gwei</span>
</div> </div>
<label
class="text-xs flex items-center gap-1 cursor-pointer mb-1"
>
<input type="checkbox" id="settings-utc-timestamps" />
UTC Timestamps
</label>
</div> </div>
<div class="bg-well p-3 mx-1 mb-3"> <div class="bg-well p-3 mx-1 mb-3">
@@ -938,7 +965,8 @@
</p> </p>
<div <div
id="delete-wallet-flash" id="delete-wallet-flash"
class="text-xs text-red-500 mb-2 hidden" class="text-xs text-red-500 mb-2 min-h-[1.25rem]"
style="visibility: hidden"
></div> ></div>
<div class="mb-2"> <div class="mb-2">
<label class="block mb-1">Password</label> <label class="block mb-1">Password</label>
@@ -1013,7 +1041,8 @@
/> />
<div <div
id="settings-addtoken-info" id="settings-addtoken-info"
class="text-xs text-muted mt-1 hidden" class="text-xs text-muted mt-1 min-h-[1.25rem]"
style="visibility: hidden"
></div> ></div>
<button <button
id="btn-settings-addtoken-manual" id="btn-settings-addtoken-manual"
@@ -1035,60 +1064,134 @@
<h2 id="tx-detail-heading" class="font-bold mb-2"> <h2 id="tx-detail-heading" class="font-bold mb-2">
Transaction Transaction
</h2> </h2>
<div id="tx-detail-type-section" class="mb-4 hidden">
<div class="text-xs text-muted mb-1">Type</div> <!-- ── Identity ── -->
<div id="tx-detail-type" class="text-xs font-bold"></div> <div class="tx-detail-group mb-1">
</div> <div class="mb-3">
<div class="mb-4"> <div class="text-xs text-muted mb-1">
<div class="text-xs text-muted mb-1">Status</div> Transaction hash
<div id="tx-detail-status" class="text-xs"></div> </div>
</div>
<div class="mb-4">
<div class="text-xs text-muted mb-1">Time</div>
<div id="tx-detail-time" class="text-xs"></div>
</div>
<div class="mb-4">
<div class="text-xs text-muted mb-1">Amount</div>
<div id="tx-detail-value" class="text-xs"></div>
</div>
<div class="mb-4 hidden">
<div class="text-xs text-muted mb-1">Native quantity</div>
<div id="tx-detail-native" class="text-xs"></div>
</div>
<div class="mb-4">
<div class="text-xs text-muted mb-1">From</div>
<div id="tx-detail-from" class="text-xs break-all"></div>
</div>
<div class="mb-4">
<div class="text-xs text-muted mb-1">To</div>
<div id="tx-detail-to" class="text-xs break-all"></div>
</div>
<div id="tx-detail-calldata-section" class="mb-4 hidden">
<div
id="tx-detail-calldata-well"
class="mb-3 border border-border border-dashed p-2"
>
<div class="text-xs text-muted mb-1">Action</div>
<div <div
id="tx-detail-calldata-action" id="tx-detail-hash"
class="text-xs font-bold mb-2" class="text-xs break-all"
></div> ></div>
</div>
<div id="tx-detail-type-section" class="mb-3 hidden">
<div class="text-xs text-muted mb-1">Type</div>
<div <div
id="tx-detail-calldata-details" id="tx-detail-type"
class="text-xs" class="text-xs font-bold"
></div>
</div>
<div class="mb-3">
<div class="text-xs text-muted mb-1">Status</div>
<div id="tx-detail-status" class="text-xs"></div>
</div>
<div class="mb-1">
<div class="text-xs text-muted mb-1">Time</div>
<div id="tx-detail-time" class="text-xs"></div>
</div>
</div>
<!-- ── Value ── -->
<div class="tx-detail-group mb-1">
<div class="mb-3">
<div class="text-xs text-muted mb-1">Amount</div>
<div id="tx-detail-value" class="text-xs"></div>
</div>
<div class="mb-3 hidden">
<div class="text-xs text-muted mb-1">
Native quantity
</div>
<div id="tx-detail-native" class="text-xs"></div>
</div>
<div
id="tx-detail-token-contract-section"
class="mb-1 hidden"
>
<div class="text-xs text-muted mb-1">
Token contract
</div>
<div
id="tx-detail-token-contract"
class="text-xs break-all"
></div> ></div>
</div> </div>
</div> </div>
<div class="mb-4">
<div class="text-xs text-muted mb-1">Transaction hash</div> <!-- ── Parties ── -->
<div id="tx-detail-hash" class="text-xs break-all"></div> <div class="tx-detail-group mb-1">
<div class="mb-3">
<div class="text-xs text-muted mb-1">From</div>
<div
id="tx-detail-from"
class="text-xs break-all"
></div>
</div>
<div class="mb-1">
<div class="text-xs text-muted mb-1">To</div>
<div id="tx-detail-to" class="text-xs break-all"></div>
</div>
</div> </div>
<!-- ── Protocol ── -->
<div id="tx-detail-calldata-section" class="mb-1 hidden">
<div class="tx-detail-group mb-1">
<div
id="tx-detail-calldata-well"
class="border border-border border-dashed p-2"
>
<div class="text-xs text-muted mb-1">Action</div>
<div
id="tx-detail-calldata-action"
class="text-xs font-bold mb-2"
></div>
<div
id="tx-detail-calldata-details"
class="text-xs"
></div>
</div>
</div>
</div>
<!-- ── On-chain details ── -->
<div
id="tx-detail-onchain-group"
class="tx-detail-group mb-1 hidden"
>
<div id="tx-detail-block-section" class="mb-3 hidden">
<div class="text-xs text-muted mb-1">Block</div>
<div id="tx-detail-block" class="text-xs"></div>
</div>
<div id="tx-detail-nonce-section" class="mb-3 hidden">
<div class="text-xs text-muted mb-1">Nonce</div>
<div id="tx-detail-nonce" class="text-xs"></div>
</div>
<div id="tx-detail-fee-section" class="mb-3 hidden">
<div class="text-xs text-muted mb-1">
Transaction fee
</div>
<div id="tx-detail-fee" class="text-xs"></div>
</div>
<div id="tx-detail-gasprice-section" class="mb-3 hidden">
<div class="text-xs text-muted mb-1">Gas price</div>
<div id="tx-detail-gasprice" class="text-xs"></div>
</div>
<div id="tx-detail-gasused-section" class="mb-1 hidden">
<div class="text-xs text-muted mb-1">Gas used</div>
<div id="tx-detail-gasused" class="text-xs"></div>
</div>
</div>
<!-- ── Raw data ── -->
<div id="tx-detail-rawdata-section" class="mb-4 hidden"> <div id="tx-detail-rawdata-section" class="mb-4 hidden">
<div class="text-xs text-muted mb-1">Raw data</div> <div class="tx-detail-group">
<div <div class="text-xs text-muted mb-1">Raw data</div>
id="tx-detail-rawdata" <div
class="text-xs break-all font-mono border border-border border-dashed p-2" id="tx-detail-rawdata"
></div> class="text-xs break-all font-mono border border-border border-dashed p-2"
></div>
</div>
</div> </div>
</div> </div>
@@ -1139,7 +1242,8 @@
</div> </div>
<div <div
id="approve-tx-error" id="approve-tx-error"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.25rem] hidden" class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.25rem]"
style="visibility: hidden"
></div> ></div>
<div class="flex justify-between"> <div class="flex justify-between">
<button <button
@@ -1167,8 +1271,10 @@
<div <div
id="approve-sign-danger-warning" id="approve-sign-danger-warning"
class="hidden mb-3 p-2 text-xs font-bold" class="mb-3 p-2 text-xs font-bold"
style=" style="
visibility: hidden;
min-height: 1.25rem;
background: #fee2e2; background: #fee2e2;
color: #991b1b; color: #991b1b;
border: 2px solid #dc2626; border: 2px solid #dc2626;
@@ -1205,7 +1311,8 @@
</div> </div>
<div <div
id="approve-sign-error" id="approve-sign-error"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.25rem] hidden" class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.25rem]"
style="visibility: hidden"
></div> ></div>
<div class="flex justify-between"> <div class="flex justify-between">
<button <button

View File

@@ -6,6 +6,7 @@ const { state, saveState, loadState } = require("../shared/state");
const { refreshPrices } = require("../shared/prices"); const { refreshPrices } = require("../shared/prices");
const { refreshBalances } = require("../shared/balances"); const { refreshBalances } = require("../shared/balances");
const { $, showView } = require("./views/helpers"); const { $, showView } = require("./views/helpers");
const { applyTheme } = require("./theme");
const home = require("./views/home"); const home = require("./views/home");
const welcome = require("./views/welcome"); const welcome = require("./views/welcome");
@@ -176,6 +177,7 @@ async function init() {
} }
await loadState(); await loadState();
applyTheme(state.theme);
// Auto-default active address // Auto-default active address
if ( if (

View File

@@ -15,6 +15,18 @@
--color-section: #dddddd; --color-section: #dddddd;
} }
html.dark {
--color-bg: #000000;
--color-fg: #ffffff;
--color-muted: #aaaaaa;
--color-border: #ffffff;
--color-border-light: #444444;
--color-hover: #222222;
--color-well: #1a1a1a;
--color-danger-well: #2a0a0a;
--color-section: #2a2a2a;
}
body { body {
width: 396px; width: 396px;
overflow-x: hidden; overflow-x: hidden;
@@ -29,6 +41,14 @@ body {
.copy-flash-fade { .copy-flash-fade {
transition: transition:
background-color 300ms ease-out, background-color 225ms ease-out,
color 300ms ease-out; color 225ms ease-out;
}
/* Transaction detail view — visual grouping of related fields */
.tx-detail-group {
border-bottom: 1px solid var(--color-border-light);
padding-bottom: 0.5rem;
margin-bottom: 0.5rem;
padding-top: 0.25rem;
} }

33
src/popup/theme.js Normal file
View File

@@ -0,0 +1,33 @@
// Theme management: applies light/dark class to <html> based on preference.
let mediaQuery = null;
let mediaHandler = null;
function applyTheme(theme) {
// Clean up previous system listener
if (mediaQuery && mediaHandler) {
mediaQuery.removeEventListener("change", mediaHandler);
mediaHandler = null;
}
if (theme === "dark") {
document.documentElement.classList.add("dark");
} else if (theme === "light") {
document.documentElement.classList.remove("dark");
} else {
// system
mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const update = () => {
if (mediaQuery.matches) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
};
mediaHandler = update;
mediaQuery.addEventListener("change", update);
update();
}
}
module.exports = { applyTheme };

View File

@@ -7,7 +7,8 @@ const { log } = require("../../shared/log");
function show() { function show() {
$("add-token-address").value = ""; $("add-token-address").value = "";
$("add-token-info").classList.add("hidden"); $("add-token-info").textContent = "";
$("add-token-info").style.visibility = "hidden";
const list = $("common-token-list"); const list = $("common-token-list");
list.innerHTML = getTopTokens(25) list.innerHTML = getTopTokens(25)
.map( .map(
@@ -45,7 +46,7 @@ function init(ctx) {
} }
const infoEl = $("add-token-info"); const infoEl = $("add-token-info");
infoEl.textContent = "Looking up token..."; infoEl.textContent = "Looking up token...";
infoEl.classList.remove("hidden"); infoEl.style.visibility = "visible";
log.debugf("Looking up token contract", contractAddr); log.debugf("Looking up token contract", contractAddr);
try { try {
const info = await lookupTokenInfo(contractAddr, state.rpcUrl); const info = await lookupTokenInfo(contractAddr, state.rpcUrl);
@@ -63,7 +64,8 @@ function init(ctx) {
const detail = e.shortMessage || e.message || String(e); const detail = e.shortMessage || e.message || String(e);
log.errorf("Token lookup failed for", contractAddr, detail); log.errorf("Token lookup failed for", contractAddr, detail);
showFlash(detail); showFlash(detail);
infoEl.classList.add("hidden"); infoEl.textContent = "";
infoEl.style.visibility = "hidden";
} }
}); });

View File

@@ -71,7 +71,7 @@ function show() {
$("import-xprv-key").value = ""; $("import-xprv-key").value = "";
$("add-wallet-password").value = ""; $("add-wallet-password").value = "";
$("add-wallet-password-confirm").value = ""; $("add-wallet-password-confirm").value = "";
$("add-wallet-phrase-warning").classList.add("hidden"); $("add-wallet-phrase-warning").style.visibility = "hidden";
switchMode("mnemonic"); switchMode("mnemonic");
showView("add-wallet"); showView("add-wallet");
} }
@@ -281,7 +281,7 @@ function init(ctx) {
// Generate mnemonic // Generate mnemonic
$("btn-generate-phrase").addEventListener("click", () => { $("btn-generate-phrase").addEventListener("click", () => {
$("wallet-mnemonic").value = generateMnemonic(); $("wallet-mnemonic").value = generateMnemonic();
$("add-wallet-phrase-warning").classList.remove("hidden"); $("add-wallet-phrase-warning").style.visibility = "visible";
}); });
// Import / confirm // Import / confirm

View File

@@ -95,18 +95,39 @@ function show() {
function isoDate(timestamp) { function isoDate(timestamp) {
const d = new Date(timestamp * 1000); const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0"); const pad = (n) => String(n).padStart(2, "0");
if (state.utcTimestamps) {
return (
d.getUTCFullYear() +
"-" +
pad(d.getUTCMonth() + 1) +
"-" +
pad(d.getUTCDate()) +
"T" +
pad(d.getUTCHours()) +
":" +
pad(d.getUTCMinutes()) +
":" +
pad(d.getUTCSeconds()) +
"Z"
);
}
const offsetMin = -d.getTimezoneOffset();
const sign = offsetMin >= 0 ? "+" : "-";
const absOff = Math.abs(offsetMin);
const tzStr = sign + pad(Math.floor(absOff / 60)) + ":" + pad(absOff % 60);
return ( return (
d.getFullYear() + d.getFullYear() +
"-" + "-" +
pad(d.getMonth() + 1) + pad(d.getMonth() + 1) +
"-" + "-" +
pad(d.getDate()) + pad(d.getDate()) +
" " + "T" +
pad(d.getHours()) + pad(d.getHours()) +
":" + ":" +
pad(d.getMinutes()) + pad(d.getMinutes()) +
":" + ":" +
pad(d.getSeconds()) pad(d.getSeconds()) +
tzStr
); );
} }
@@ -312,8 +333,8 @@ function init(_ctx) {
$("export-privkey-address").textContent = addr.address; $("export-privkey-address").textContent = addr.address;
$("export-privkey-address").dataset.full = addr.address; $("export-privkey-address").dataset.full = addr.address;
$("export-privkey-password").value = ""; $("export-privkey-password").value = "";
$("export-privkey-flash").classList.add("hidden");
$("export-privkey-flash").textContent = ""; $("export-privkey-flash").textContent = "";
$("export-privkey-flash").style.visibility = "hidden";
$("export-privkey-password-section").classList.remove("hidden"); $("export-privkey-password-section").classList.remove("hidden");
$("export-privkey-result").classList.add("hidden"); $("export-privkey-result").classList.add("hidden");
$("export-privkey-value").textContent = ""; $("export-privkey-value").textContent = "";
@@ -324,7 +345,7 @@ function init(_ctx) {
const password = $("export-privkey-password").value; const password = $("export-privkey-password").value;
if (!password) { if (!password) {
$("export-privkey-flash").textContent = "Password is required."; $("export-privkey-flash").textContent = "Password is required.";
$("export-privkey-flash").classList.remove("hidden"); $("export-privkey-flash").style.visibility = "visible";
return; return;
} }
const btn = $("btn-export-privkey-confirm"); const btn = $("btn-export-privkey-confirm");
@@ -345,10 +366,10 @@ function init(_ctx) {
$("export-privkey-password-section").classList.add("hidden"); $("export-privkey-password-section").classList.add("hidden");
$("export-privkey-value").textContent = privateKey; $("export-privkey-value").textContent = privateKey;
$("export-privkey-result").classList.remove("hidden"); $("export-privkey-result").classList.remove("hidden");
$("export-privkey-flash").classList.add("hidden"); $("export-privkey-flash").style.visibility = "hidden";
} catch { } catch {
$("export-privkey-flash").textContent = "Wrong password."; $("export-privkey-flash").textContent = "Wrong password.";
$("export-privkey-flash").classList.remove("hidden"); $("export-privkey-flash").style.visibility = "visible";
} finally { } finally {
btn.disabled = false; btn.disabled = false;
btn.classList.remove("text-muted"); btn.classList.remove("text-muted");

View File

@@ -48,18 +48,39 @@ function etherscanAddressLink(address) {
function isoDate(timestamp) { function isoDate(timestamp) {
const d = new Date(timestamp * 1000); const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0"); const pad = (n) => String(n).padStart(2, "0");
if (state.utcTimestamps) {
return (
d.getUTCFullYear() +
"-" +
pad(d.getUTCMonth() + 1) +
"-" +
pad(d.getUTCDate()) +
"T" +
pad(d.getUTCHours()) +
":" +
pad(d.getUTCMinutes()) +
":" +
pad(d.getUTCSeconds()) +
"Z"
);
}
const offsetMin = -d.getTimezoneOffset();
const sign = offsetMin >= 0 ? "+" : "-";
const absOff = Math.abs(offsetMin);
const tzStr = sign + pad(Math.floor(absOff / 60)) + ":" + pad(absOff % 60);
return ( return (
d.getFullYear() + d.getFullYear() +
"-" + "-" +
pad(d.getMonth() + 1) + pad(d.getMonth() + 1) +
"-" + "-" +
pad(d.getDate()) + pad(d.getDate()) +
" " + "T" +
pad(d.getHours()) + pad(d.getHours()) +
":" + ":" +
pad(d.getMinutes()) + pad(d.getMinutes()) +
":" + ":" +
pad(d.getSeconds()) pad(d.getSeconds()) +
tzStr
); );
} }

View File

@@ -269,7 +269,7 @@ function showTxApproval(details) {
} }
$("approve-tx-password").value = ""; $("approve-tx-password").value = "";
$("approve-tx-error").classList.add("hidden"); hideError("approve-tx-error");
showView("approve-tx"); showView("approve-tx");
} }
@@ -351,10 +351,10 @@ function showSignApproval(details) {
if (warningEl) { if (warningEl) {
if (sp.dangerWarning) { if (sp.dangerWarning) {
warningEl.textContent = sp.dangerWarning; warningEl.textContent = sp.dangerWarning;
warningEl.classList.remove("hidden"); warningEl.style.visibility = "visible";
} else { } else {
warningEl.textContent = ""; warningEl.textContent = "";
warningEl.classList.add("hidden"); warningEl.style.visibility = "hidden";
} }
} }

View File

@@ -186,9 +186,10 @@ function show(txInfo) {
`<div class="border border-border border-dashed p-2 mb-1 text-xs font-bold">WARNING: ${w}</div>`, `<div class="border border-border border-dashed p-2 mb-1 text-xs font-bold">WARNING: ${w}</div>`,
) )
.join(""); .join("");
warningsEl.classList.remove("hidden"); warningsEl.style.visibility = "visible";
} else { } else {
warningsEl.classList.add("hidden"); warningsEl.innerHTML = "";
warningsEl.style.visibility = "hidden";
} }
// Check for errors // Check for errors
@@ -226,11 +227,12 @@ function show(txInfo) {
errorsEl.innerHTML = errors errorsEl.innerHTML = errors
.map((e) => `<div class="text-xs">${e}</div>`) .map((e) => `<div class="text-xs">${e}</div>`)
.join(""); .join("");
errorsEl.classList.remove("hidden"); errorsEl.style.visibility = "visible";
sendBtn.disabled = true; sendBtn.disabled = true;
sendBtn.classList.add("text-muted"); sendBtn.classList.add("text-muted");
} else { } else {
errorsEl.classList.add("hidden"); errorsEl.innerHTML = "";
errorsEl.style.visibility = "hidden";
sendBtn.disabled = false; sendBtn.disabled = false;
sendBtn.classList.remove("text-muted"); sendBtn.classList.remove("text-muted");
} }
@@ -240,7 +242,7 @@ function show(txInfo) {
hideError("confirm-tx-password-error"); hideError("confirm-tx-password-error");
// Gas estimate — show placeholder then fetch async // Gas estimate — show placeholder then fetch async
$("confirm-fee").classList.remove("hidden"); $("confirm-fee").style.visibility = "visible";
$("confirm-fee-amount").textContent = "Estimating..."; $("confirm-fee-amount").textContent = "Estimating...";
state.viewData = { pendingTx: txInfo }; state.viewData = { pendingTx: txInfo };
showView("confirm-tx"); showView("confirm-tx");

View File

@@ -12,7 +12,7 @@ function show(walletIdx) {
wallet.name || "Wallet " + (walletIdx + 1); wallet.name || "Wallet " + (walletIdx + 1);
$("delete-wallet-password").value = ""; $("delete-wallet-password").value = "";
$("delete-wallet-flash").textContent = ""; $("delete-wallet-flash").textContent = "";
$("delete-wallet-flash").classList.add("hidden"); $("delete-wallet-flash").style.visibility = "hidden";
showView("delete-wallet-confirm"); showView("delete-wallet-confirm");
} }
@@ -29,14 +29,14 @@ function init(_ctx) {
if (!pw) { if (!pw) {
$("delete-wallet-flash").textContent = $("delete-wallet-flash").textContent =
"Please enter your password."; "Please enter your password.";
$("delete-wallet-flash").classList.remove("hidden"); $("delete-wallet-flash").style.visibility = "visible";
return; return;
} }
if (deleteWalletIndex === null) { if (deleteWalletIndex === null) {
$("delete-wallet-flash").textContent = $("delete-wallet-flash").textContent =
"No wallet selected for deletion."; "No wallet selected for deletion.";
$("delete-wallet-flash").classList.remove("hidden"); $("delete-wallet-flash").style.visibility = "visible";
return; return;
} }
@@ -52,7 +52,7 @@ function init(_ctx) {
await decryptWithPassword(wallet.encryptedSecret, pw); await decryptWithPassword(wallet.encryptedSecret, pw);
} catch (_e) { } catch (_e) {
$("delete-wallet-flash").textContent = "Wrong password."; $("delete-wallet-flash").textContent = "Wrong password.";
$("delete-wallet-flash").classList.remove("hidden"); $("delete-wallet-flash").style.visibility = "visible";
btn.disabled = false; btn.disabled = false;
btn.classList.remove("text-muted"); btn.classList.remove("text-muted");
return; return;

View File

@@ -40,11 +40,13 @@ function $(id) {
function showError(id, msg) { function showError(id, msg) {
const el = $(id); const el = $(id);
el.textContent = msg; el.textContent = msg;
el.classList.remove("hidden"); el.style.visibility = "visible";
} }
function hideError(id) { function hideError(id) {
$(id).classList.add("hidden"); const el = $(id);
el.textContent = "";
el.style.visibility = "hidden";
} }
function showView(name) { function showView(name) {
@@ -226,18 +228,39 @@ function formatAddressHtml(address, ensName, maxLen, title) {
function isoDate(timestamp) { function isoDate(timestamp) {
const d = new Date(timestamp * 1000); const d = new Date(timestamp * 1000);
const pad = (n) => String(n).padStart(2, "0"); const pad = (n) => String(n).padStart(2, "0");
if (state.utcTimestamps) {
return (
d.getUTCFullYear() +
"-" +
pad(d.getUTCMonth() + 1) +
"-" +
pad(d.getUTCDate()) +
"T" +
pad(d.getUTCHours()) +
":" +
pad(d.getUTCMinutes()) +
":" +
pad(d.getUTCSeconds()) +
"Z"
);
}
const offsetMin = -d.getTimezoneOffset();
const sign = offsetMin >= 0 ? "+" : "-";
const absOff = Math.abs(offsetMin);
const tzStr = sign + pad(Math.floor(absOff / 60)) + ":" + pad(absOff % 60);
return ( return (
d.getFullYear() + d.getFullYear() +
"-" + "-" +
pad(d.getMonth() + 1) + pad(d.getMonth() + 1) +
"-" + "-" +
pad(d.getDate()) + pad(d.getDate()) +
" " + "T" +
pad(d.getHours()) + pad(d.getHours()) +
":" + ":" +
pad(d.getMinutes()) + pad(d.getMinutes()) +
":" + ":" +
pad(d.getSeconds()) pad(d.getSeconds()) +
tzStr
); );
} }
@@ -267,8 +290,8 @@ function flashCopyFeedback(el) {
el.classList.add("copy-flash-fade"); el.classList.add("copy-flash-fade");
setTimeout(() => { setTimeout(() => {
el.classList.remove("copy-flash-fade"); el.classList.remove("copy-flash-fade");
}, 350); }, 275);
}, 100); }, 75);
} }
module.exports = { module.exports = {

View File

@@ -53,9 +53,10 @@ function show() {
"This is an ERC-20 token. Only send " + "This is an ERC-20 token. Only send " +
symbol + symbol +
" on the Ethereum network to this address. Sending tokens on other networks will result in permanent loss."; " on the Ethereum network to this address. Sending tokens on other networks will result in permanent loss.";
warningEl.classList.remove("hidden"); warningEl.style.visibility = "visible";
} else { } else {
warningEl.classList.add("hidden"); warningEl.textContent = "";
warningEl.style.visibility = "hidden";
} }
showView("receive"); showView("receive");
} }

View File

@@ -1,4 +1,5 @@
const { $, showView, showFlash, escapeHtml } = require("./helpers"); const { $, showView, showFlash, escapeHtml } = require("./helpers");
const { applyTheme } = require("../theme");
const { state, saveState } = require("../../shared/state"); const { state, saveState } = require("../../shared/state");
const { ETHEREUM_MAINNET_CHAIN_ID } = require("../../shared/constants"); const { ETHEREUM_MAINNET_CHAIN_ID } = require("../../shared/constants");
const { log, debugFetch } = require("../../shared/log"); const { log, debugFetch } = require("../../shared/log");
@@ -214,6 +215,13 @@ function init(ctx) {
await saveState(); await saveState();
}); });
$("settings-theme").value = state.theme;
$("settings-theme").addEventListener("change", async () => {
state.theme = $("settings-theme").value;
await saveState();
applyTheme(state.theme);
});
$("settings-hide-low-holders").checked = state.hideLowHolderTokens; $("settings-hide-low-holders").checked = state.hideLowHolderTokens;
$("settings-hide-low-holders").addEventListener("change", async () => { $("settings-hide-low-holders").addEventListener("change", async () => {
state.hideLowHolderTokens = $("settings-hide-low-holders").checked; state.hideLowHolderTokens = $("settings-hide-low-holders").checked;
@@ -241,6 +249,12 @@ function init(ctx) {
} }
}); });
$("settings-utc-timestamps").checked = state.utcTimestamps;
$("settings-utc-timestamps").addEventListener("change", async () => {
state.utcTimestamps = $("settings-utc-timestamps").checked;
await saveState();
});
$("btn-main-add-wallet").addEventListener("click", ctx.showAddWalletView); $("btn-main-add-wallet").addEventListener("click", ctx.showAddWalletView);
$("btn-settings-add-token").addEventListener( $("btn-settings-add-token").addEventListener(

View File

@@ -73,7 +73,8 @@ function renderDropdown() {
function show() { function show() {
$("settings-addtoken-address").value = ""; $("settings-addtoken-address").value = "";
$("settings-addtoken-info").classList.add("hidden"); $("settings-addtoken-info").textContent = "";
$("settings-addtoken-info").style.visibility = "hidden";
renderTop10(); renderTop10();
renderDropdown(); renderDropdown();
showView("settings-addtoken"); showView("settings-addtoken");
@@ -129,7 +130,7 @@ function init(_ctx) {
} }
const infoEl = $("settings-addtoken-info"); const infoEl = $("settings-addtoken-info");
infoEl.textContent = "Looking up token..."; infoEl.textContent = "Looking up token...";
infoEl.classList.remove("hidden"); infoEl.style.visibility = "visible";
log.debugf("Looking up token contract", addr); log.debugf("Looking up token contract", addr);
try { try {
const info = await lookupTokenInfo(addr, state.rpcUrl); const info = await lookupTokenInfo(addr, state.rpcUrl);
@@ -143,7 +144,8 @@ function init(_ctx) {
await saveState(); await saveState();
showFlash("Added " + info.symbol); showFlash("Added " + info.symbol);
$("settings-addtoken-address").value = ""; $("settings-addtoken-address").value = "";
infoEl.classList.add("hidden"); infoEl.textContent = "";
infoEl.style.visibility = "hidden";
renderTop10(); renderTop10();
renderDropdown(); renderDropdown();
ctx.doRefreshAndRender(); ctx.doRefreshAndRender();
@@ -151,7 +153,8 @@ function init(_ctx) {
const detail = e.shortMessage || e.message || String(e); const detail = e.shortMessage || e.message || String(e);
log.errorf("Token lookup failed for", addr, detail); log.errorf("Token lookup failed for", addr, detail);
showFlash(detail); showFlash(detail);
infoEl.classList.add("hidden"); infoEl.textContent = "";
infoEl.style.visibility = "hidden";
} }
}); });
} }

View File

@@ -13,6 +13,7 @@ const {
timeAgo, timeAgo,
} = require("./helpers"); } = require("./helpers");
const { state } = require("../../shared/state"); const { state } = require("../../shared/state");
const { formatEther, formatUnits } = require("ethers");
const makeBlockie = require("ethereum-blockies-base64"); const makeBlockie = require("ethereum-blockies-base64");
const { log, debugFetch } = require("../../shared/log"); const { log, debugFetch } = require("../../shared/log");
const { decodeCalldata } = require("./approval"); const { decodeCalldata } = require("./approval");
@@ -26,6 +27,25 @@ const EXT_ICON =
let ctx; let ctx;
/**
* Determine a human-readable transaction type string from tx fields.
*/
function getTransactionType(tx) {
if (!tx.to) return "Contract Creation";
if (tx.direction === "contract") {
if (tx.directionLabel === "Swap") return "Swap";
if (
tx.method === "approve" ||
tx.directionLabel === "Approve" ||
tx.method === "setApprovalForAll"
)
return "Token Approval";
return "Contract Call";
}
if (tx.symbol && tx.symbol !== "ETH") return "ERC-20 Token Transfer";
return "Native ETH Transfer";
}
function copyableHtml(text, extraClass) { function copyableHtml(text, extraClass) {
const cls = const cls =
"underline decoration-dashed cursor-pointer" + "underline decoration-dashed cursor-pointer" +
@@ -99,6 +119,7 @@ function show(tx) {
direction: tx.direction || null, direction: tx.direction || null,
isContractCall: tx.isContractCall || false, isContractCall: tx.isContractCall || false,
method: tx.method || null, method: tx.method || null,
contractAddress: tx.contractAddress || null,
}, },
}; };
render(); render();
@@ -135,30 +156,56 @@ function render() {
nativeEl.parentElement.classList.add("hidden"); nativeEl.parentElement.classList.add("hidden");
} }
// Show type label for contract interactions (Swap, Execute, etc.) // Always show transaction type as the first field
const typeSection = $("tx-detail-type-section"); const typeSection = $("tx-detail-type-section");
const typeEl = $("tx-detail-type"); const typeEl = $("tx-detail-type");
const headingEl = $("tx-detail-heading"); const headingEl = $("tx-detail-heading");
if (tx.direction === "contract" && tx.directionLabel) { if (typeSection && typeEl) {
if (typeSection) { typeEl.textContent = getTransactionType(tx);
typeEl.textContent = tx.directionLabel; typeSection.classList.remove("hidden");
typeSection.classList.remove("hidden");
}
} else {
if (typeSection) typeSection.classList.add("hidden");
} }
if (headingEl) headingEl.textContent = "Transaction"; if (headingEl) headingEl.textContent = "Transaction";
// Hide calldata and raw data sections; re-fetch if this is a contract call // Token contract address (for ERC-20 transfers)
const tokenContractSection = $("tx-detail-token-contract-section");
const tokenContractEl = $("tx-detail-token-contract");
if (tokenContractSection && tokenContractEl) {
if (tx.contractAddress) {
const dot = addressDotHtml(tx.contractAddress);
const link = `https://etherscan.io/token/${tx.contractAddress}`;
tokenContractEl.innerHTML =
`<div class="flex items-center">${dot}` +
copyableHtml(tx.contractAddress, "break-all") +
etherscanLinkHtml(link) +
`</div>`;
tokenContractSection.classList.remove("hidden");
} else {
tokenContractSection.classList.add("hidden");
}
}
// Hide calldata and raw data sections; always fetch full tx details
const calldataSection = $("tx-detail-calldata-section"); const calldataSection = $("tx-detail-calldata-section");
if (calldataSection) calldataSection.classList.add("hidden"); if (calldataSection) calldataSection.classList.add("hidden");
const rawDataSection = $("tx-detail-rawdata-section"); const rawDataSection = $("tx-detail-rawdata-section");
if (rawDataSection) rawDataSection.classList.add("hidden"); if (rawDataSection) rawDataSection.classList.add("hidden");
if (tx.isContractCall || tx.direction === "contract") { // Hide on-chain detail sections (and their group wrapper) until populated
loadCalldata(tx.hash, tx.to); const onchainGroup = $("tx-detail-onchain-group");
if (onchainGroup) onchainGroup.classList.add("hidden");
for (const id of [
"tx-detail-block-section",
"tx-detail-nonce-section",
"tx-detail-fee-section",
"tx-detail-gasprice-section",
"tx-detail-gasused-section",
]) {
const el = $(id);
if (el) el.classList.add("hidden");
} }
loadFullTxDetails(tx.hash, tx.to, tx.isContractCall);
const isoStr = isoDate(tx.timestamp); const isoStr = isoDate(tx.timestamp);
$("tx-detail-time").innerHTML = $("tx-detail-time").innerHTML =
copyableHtml(isoStr) + " (" + escapeHtml(timeAgo(tx.timestamp)) + ")"; copyableHtml(isoStr) + " (" + escapeHtml(timeAgo(tx.timestamp)) + ")";
@@ -177,7 +224,108 @@ function render() {
}); });
} }
async function loadCalldata(txHash, toAddress) { function showDetailField(sectionId, contentId, value) {
const section = $(sectionId);
const el = $(contentId);
if (!section || !el) return;
el.innerHTML = copyableHtml(value, "");
section.classList.remove("hidden");
}
function populateOnChainDetails(txData) {
// Block number
if (txData.block_number != null) {
const blockLink = `https://etherscan.io/block/${txData.block_number}`;
const blockSection = $("tx-detail-block-section");
const blockEl = $("tx-detail-block");
if (blockSection && blockEl) {
blockEl.innerHTML =
copyableHtml(String(txData.block_number), "") +
etherscanLinkHtml(blockLink);
blockSection.classList.remove("hidden");
}
}
// Nonce
if (txData.nonce != null) {
showDetailField(
"tx-detail-nonce-section",
"tx-detail-nonce",
String(txData.nonce),
);
}
// Transaction fee
const feeWei = txData.fee?.value || txData.tx_fee;
if (feeWei) {
const feeEth = formatEther(String(feeWei));
showDetailField(
"tx-detail-fee-section",
"tx-detail-fee",
feeEth + " ETH",
);
}
// Gas price
const gasPrice = txData.gas_price;
if (gasPrice) {
const gwei = formatUnits(String(gasPrice), "gwei");
showDetailField(
"tx-detail-gasprice-section",
"tx-detail-gasprice",
gwei + " Gwei",
);
}
// Gas used
const gasUsed = txData.gas_used;
if (gasUsed) {
showDetailField(
"tx-detail-gasused-section",
"tx-detail-gasused",
String(gasUsed),
);
}
// Show the on-chain details group if any child section is visible
const onchainGroup = $("tx-detail-onchain-group");
if (onchainGroup) {
const hasVisible = [
"tx-detail-block-section",
"tx-detail-nonce-section",
"tx-detail-fee-section",
"tx-detail-gasprice-section",
"tx-detail-gasused-section",
].some((id) => {
const el = $(id);
return el && !el.classList.contains("hidden");
});
if (hasVisible) {
onchainGroup.classList.remove("hidden");
}
}
// Bind copy handlers for newly added elements
for (const id of [
"tx-detail-block-section",
"tx-detail-nonce-section",
"tx-detail-fee-section",
"tx-detail-gasprice-section",
"tx-detail-gasused-section",
]) {
const section = $(id);
if (!section) continue;
section.querySelectorAll("[data-copy]").forEach((el) => {
el.onclick = () => {
navigator.clipboard.writeText(el.dataset.copy);
showFlash("Copied!");
flashCopyFeedback(el);
};
});
}
}
async function loadFullTxDetails(txHash, toAddress, isContractCall) {
const section = $("tx-detail-calldata-section"); const section = $("tx-detail-calldata-section");
const actionEl = $("tx-detail-calldata-action"); const actionEl = $("tx-detail-calldata-action");
const detailsEl = $("tx-detail-calldata-details"); const detailsEl = $("tx-detail-calldata-details");
@@ -192,6 +340,10 @@ async function loadCalldata(txHash, toAddress) {
); );
if (!resp.ok) return; if (!resp.ok) return;
const txData = await resp.json(); const txData = await resp.json();
// Populate on-chain detail fields (block, nonce, gas, fee)
populateOnChainDetails(txData);
const inputData = txData.raw_input || txData.input || null; const inputData = txData.raw_input || txData.input || null;
if (!inputData || inputData === "0x") return; if (!inputData || inputData === "0x") return;

View File

@@ -23,8 +23,10 @@ const DEFAULT_STATE = {
hideFraudContracts: true, hideFraudContracts: true,
hideDustTransactions: true, hideDustTransactions: true,
dustThresholdGwei: 100000, dustThresholdGwei: 100000,
utcTimestamps: false,
fraudContracts: [], fraudContracts: [],
tokenHolderCache: {}, tokenHolderCache: {},
theme: "system",
}; };
const state = { const state = {
@@ -53,8 +55,10 @@ async function saveState() {
hideFraudContracts: state.hideFraudContracts, hideFraudContracts: state.hideFraudContracts,
hideDustTransactions: state.hideDustTransactions, hideDustTransactions: state.hideDustTransactions,
dustThresholdGwei: state.dustThresholdGwei, dustThresholdGwei: state.dustThresholdGwei,
utcTimestamps: state.utcTimestamps,
fraudContracts: state.fraudContracts, fraudContracts: state.fraudContracts,
tokenHolderCache: state.tokenHolderCache, tokenHolderCache: state.tokenHolderCache,
theme: state.theme,
currentView: state.currentView, currentView: state.currentView,
selectedWallet: state.selectedWallet, selectedWallet: state.selectedWallet,
selectedAddress: state.selectedAddress, selectedAddress: state.selectedAddress,
@@ -108,8 +112,11 @@ async function loadState() {
saved.dustThresholdGwei !== undefined saved.dustThresholdGwei !== undefined
? saved.dustThresholdGwei ? saved.dustThresholdGwei
: 100000; : 100000;
state.utcTimestamps =
saved.utcTimestamps !== undefined ? saved.utcTimestamps : false;
state.fraudContracts = saved.fraudContracts || []; state.fraudContracts = saved.fraudContracts || [];
state.tokenHolderCache = saved.tokenHolderCache || {}; state.tokenHolderCache = saved.tokenHolderCache || {};
state.theme = saved.theme || "system";
state.currentView = saved.currentView || null; state.currentView = saved.currentView || null;
state.selectedWallet = state.selectedWallet =
saved.selectedWallet !== undefined ? saved.selectedWallet : null; saved.selectedWallet !== undefined ? saved.selectedWallet : null;

View File

@@ -153,24 +153,38 @@ async function fetchRecentTransactions(address, blockscoutUrl, count = 25) {
// When a token transfer shares a hash with a normal tx, the normal tx // When a token transfer shares a hash with a normal tx, the normal tx
// is the contract call (0 ETH) and the token transfer has the real // is the contract call (0 ETH) and the token transfer has the real
// amount and symbol. A single transaction (e.g. a swap) can produce // amount and symbol. For contract calls (swaps), a single transaction
// multiple token transfers (one per token involved), so we key token // can produce multiple token transfers (input, intermediates, output).
// transfers by hash + contract address to keep all of them. We also // We consolidate these into the original tx entry using the token
// preserve contract-call metadata (direction, label, method) from the // transfer where the user *receives* tokens (the swap output), so
// matching normal tx so swaps display correctly. // the transaction list shows the final result rather than confusing
// intermediate hops. We preserve the original tx's from/to so the
// user sees their own address, not a router or Permit2 contract.
for (const tt of ttJson.items || []) { for (const tt of ttJson.items || []) {
const parsed = parseTokenTransfer(tt, addrLower); const parsed = parseTokenTransfer(tt, addrLower);
const existing = txsByHash.get(parsed.hash); const existing = txsByHash.get(parsed.hash);
if (existing && existing.direction === "contract") { if (existing && existing.direction === "contract") {
parsed.direction = "contract"; // For contract calls (swaps), consolidate into the original
parsed.directionLabel = existing.directionLabel; // tx entry. Prefer the "received" transfer (swap output)
parsed.isContractCall = true; // for the display amount. If no received transfer exists,
parsed.method = existing.method; // fall back to the first "sent" transfer (swap input).
// Remove the bare-hash normal tx so it doesn't appear as a const isReceived = parsed.direction === "received";
// duplicate with empty value; token transfers replace it. const needsAmount = !existing.exactValue;
txsByHash.delete(parsed.hash); if (isReceived || needsAmount) {
existing.value = parsed.value;
existing.exactValue = parsed.exactValue;
existing.rawAmount = parsed.rawAmount;
existing.rawUnit = parsed.rawUnit;
existing.symbol = parsed.symbol;
existing.contractAddress = parsed.contractAddress;
existing.holders = parsed.holders;
}
// Keep the original tx's from/to (the user's address and the
// contract they called), not the token transfer's from/to
// which may be a router or Permit2 contract.
continue;
} }
// Use composite key so multiple token transfers per tx are kept. // Non-contract token transfers get their own entries.
const ttKey = parsed.hash + ":" + (parsed.contractAddress || ""); const ttKey = parsed.hash + ":" + (parsed.contractAddress || "");
txsByHash.set(ttKey, parsed); txsByHash.set(ttKey, parsed);
} }

View File

@@ -359,9 +359,12 @@ function decode(data, toAddress) {
const s = decodeV3SwapExactIn(inputs[i]); const s = decodeV3SwapExactIn(inputs[i]);
if (s) { if (s) {
if (!inputToken) inputToken = s.tokenIn; if (!inputToken) inputToken = s.tokenIn;
if (!outputToken) outputToken = s.tokenOut;
if (!inputAmount) inputAmount = s.amountIn; if (!inputAmount) inputAmount = s.amountIn;
if (!minOutput) minOutput = s.amountOutMin; // Always update output: in multi-step swaps (V3 → V4),
// the last swap step determines the final output token
// and minimum received amount.
outputToken = s.tokenOut;
minOutput = s.amountOutMin;
} }
} }
@@ -369,9 +372,9 @@ function decode(data, toAddress) {
const s = decodeV2SwapExactIn(inputs[i]); const s = decodeV2SwapExactIn(inputs[i]);
if (s) { if (s) {
if (!inputToken) inputToken = s.tokenIn; if (!inputToken) inputToken = s.tokenIn;
if (!outputToken) outputToken = s.tokenOut;
if (!inputAmount) inputAmount = s.amountIn; if (!inputAmount) inputAmount = s.amountIn;
if (!minOutput) minOutput = s.amountOutMin; outputToken = s.tokenOut;
minOutput = s.amountOutMin;
} }
} }
@@ -388,12 +391,11 @@ function decode(data, toAddress) {
const v4 = decodeV4Swap(inputs[i]); const v4 = decodeV4Swap(inputs[i]);
if (v4) { if (v4) {
if (!inputToken && v4.tokenIn) inputToken = v4.tokenIn; if (!inputToken && v4.tokenIn) inputToken = v4.tokenIn;
if (!outputToken && v4.tokenOut)
outputToken = v4.tokenOut;
if (!inputAmount && v4.amountIn) if (!inputAmount && v4.amountIn)
inputAmount = v4.amountIn; inputAmount = v4.amountIn;
if (!minOutput && v4.amountOutMin) // Always update output: last swap step wins
minOutput = v4.amountOutMin; if (v4.tokenOut) outputToken = v4.tokenOut;
if (v4.amountOutMin) minOutput = v4.amountOutMin;
} }
} }