verifySignedTx compared only from, to, value and data, so a signed
transaction could differ from the approval in chain id, nonce, gas limit
or any fee field and still be broadcast. Worse, it named the fields it
checked and so admitted every field it did not name: a type 4 artifact
carrying an EIP-7702 authorization passed verification, paying the
approved amount to the approved recipient and, in the same transaction,
permanently installing another contract's code at the signer's own
account.
The check is now an allowlist in both directions. The transaction type
must be 0, 1 or 2 — the only types this wallet signs — so no later
EIP-2718 type can bring a field along; authorizationList, blobs, blob
commitments and blob gas fees are refused by name; and the access list
is compared with the approval. Every consequential field is compared and
any mismatch refuses outright: the chain id against the selected network
(and against the approval when the page fixed one), plus nonce, gas
limit, gasPrice, maxFeePerGas and maxPriorityFeePerGas wherever the
approval carries a value, together with the fee mechanism the approval
implies. Fields the approval does not carry are populated locally by the
popup and have no approved value to compare against, so they are held to
absolute ceilings instead. Verification then closes by rebuilding the
transaction from exactly those checked fields and comparing the unsigned
bytes, so an artifact carrying anything this module does not account for
is refused without having to be named first.
An approved value that is not a number now refuses like every other
quantity rather than escaping as a raw BigInt conversion error, which
was reported as retryable and left a live button that could never
succeed.
A failed signing attempt also left a button that could not succeed: the
background deleted the approval before it broadcast, so a retry found
nothing to sign. The approval is now retired once the request has an
outcome, and the background tells the popup which stage failed. A popup
that could not sign is retryable; a mismatch spends the approval; a
failed broadcast is terminal, because the node may have accepted the
transaction and still failed to answer and the popup's retry re-signs at
a freshly fetched nonce rather than re-broadcasting the same bytes,
which would send the approved transfer twice.
Keeping the approval alive for that retry cost it its single use: the
handler read it, then verified and broadcast asynchronously, so a second
AUTISTMASK_TX_RESPONSE carrying the same id started an independent
verify and broadcast instead of finding nothing. With the ordinary dApp
approval shape the page fixes no nonce, so two artifacts signed at
different nonces both verify and the approved transfer goes out twice; a
reloaded approval window during a slow broadcast is enough to send it,
since the only guard was popup-local button state. The approval is now
claimed synchronously, before the first await, and released only when an
attempt fails in a way the user may retry. Same interlock on
AUTISTMASK_SIGN_RESPONSE.
Surviving the whole verify-and-broadcast window put the approval within
reach of every other path that retires one, and those paths did not
consult the claim. Closing the approval popup, switching the active
address, or a reject arriving late each resolved the waiting promise
4001 while the attempt behind it ran to completion; the attempt's own
resolve then landed on a settled promise, so the transaction reached the
chain and the page was told the user rejected it. The user's natural
response is to redo the transfer from the site, which re-signs at a
fresh nonce and sends it twice — the outcome this change exists to
prevent, reached without an adversary, since the popup stays open across
the broadcast and a user closing an apparently-hung window is enough.
Every settlement now goes through one function. settleApproval() is the
only place an approval is resolved or removed, and it refuses a claimed
approval unless the caller holds the claim, so a path added later
inherits the interlock instead of having to remember it. The active-
address switch also leaves a claimed approval's window standing rather
than force-closing the window the attempt is reporting into. The
duplicate refusal on the sign path now carries a stage of its own, so
the popup stops telling the user to start again from the site while a
first attempt may still succeed.
Verification also compared only the decode against itself: both sides of
the closing byte comparison derive from one Transaction.from(), while
what is broadcast is the artifact string. An artifact re-encoded with a
leading zero byte on an RLP quantity therefore decoded to the approved
transaction, passed, and broadcast different bytes. The artifact is now
required to be the canonical encoding of its own decode, which is what
makes the claim that it *is* the approved transaction true.
The background's approval wiring had no tests, which is where these
defects lived. It has them now, driven through the real message listener
from eth_sendTransaction to broadcast, with windows.onRemoved captured
rather than stubbed away: each retirement path is asserted to leave a
mid-broadcast attempt alone and to still reject an approval no attempt
holds.
Address rows on Home gain an [x] control, on wallets that derive addresses from
an extended key and hold more than one, opening a DeleteAddress confirmation
screen.
Removal cannot destroy anything: the key material stays. Derivation indices are
not renumbered, so the next "+" derives the next unused index rather than
resurrecting the removed one. The confirmation states the real route back --
delete the whole wallet in Settings, which asks for the password and destroys
the stored recovery phrase, then import it again -- and notes that the scan
which follows only finds addresses with on-chain activity. The copy varies by
wallet type, since an xprv wallet has no recovery phrase.
Removing an address that holds a balance is allowed, with a warning naming no
figure; the funds are at the address on-chain and stay there either way.
Selection and active address move only when the removed address was the one
selected, and site permissions are dropped for it alone.
The state transition shares its address comparison, permission cleanup and
active-changed broadcast with the wallet-level removal.
build.js records which emitted bundles contain src/shared/constants.js, and
constants.js carries a marker constant-folded from DEBUG itself. script/verify-build
cross-checks the two and fails on every way of not knowing, so deleting the
__BUILD_DEBUG__ define now breaks the build instead of shipping a live debug branch.
The password no longer crosses the extension messaging boundary: the popup
decrypts and signs, and sends only the raw signed transaction or the signature.
The background re-derives the signer from the artifact and checks it against
the approval it holds before broadcasting, so it is not a blind relay.
47 tests over src/shared/transactions.js: both real address-poisoning attacks
as fixtures, each of the four filters on and off, threshold boundaries, no
false positives, and the per-address merge/dedup path. No source file changed.
Runs the real popup in a pinned containerized Chrome and fails on any uncaught
page error or console.error. Also fixes the two defects it caught: the missing
showView import in addToken.js and the missing addressDotHtml import in
transactionDetail.js.
closes#150closes#151
Fixes the highest-severity item in the repo: `src/shared/constants.js` had
`const DEBUG = true;`, so `generateMnemonic()` returned the publicly committed
`DEBUG_MNEMONIC` for every wallet created from a build of `main`, and the real
entropy path was dead code in every artifact we could produce.
## What changed
**`build.js`** — `AUTISTMASK_DEBUG` is read from the environment and injected
as a `__BUILD_DEBUG__` entry in the existing esbuild `define` map, next to the
other `__BUILD_*__` defines. Only the exact value `1` enables it; unset, empty,
`true`, or a typo all yield a release build, so the insecure direction requires
a deliberate opt-in and any mistake fails safe. The build prints
`Build mode: release (DEBUG off)` or
`Build mode: DEBUG (INSECURE - hardcoded test mnemonic, do not ship)`.
**`src/shared/constants.js`** — `DEBUG` now uses the same `typeof` guard that
`src/shared/buildInfo.js` already uses for the other build-time defines, and
defaults to `false` when the define is absent (jest, plain `require`).
`DEBUG_MNEMONIC` stays in the tree and stays exported.
**`Makefile`** — new `build-debug` target (`AUTISTMASK_DEBUG=1` + the same
build) so a debug build stays a one-liner for development.
**`README.md`** — new "Debug Builds" subsection under Getting Started, and the
DEBUG Mode Policy section now states that `DEBUG` is build-time-only and spells
out the boundary against the runtime toggle.
**`tests/wallet.test.js`** — new, covering both build modes.
**`TODO.md`** — refreshed in the same commit (details at the bottom).
No new `if (DEBUG)` branch was added and nothing about what DEBUG *does* changed:
still exactly the red banner plus the hardcoded test phrase, per the README
DEBUG Mode Policy and `RULES.md:76-80`.
## The interaction with the #145 settings toggle
This is the subtle part, so spelling out the reasoning.
There are two distinct debug flags in the tree after #145:
1. the compile-time `DEBUG` constant from `constants.js`, and
2. the runtime `debugMode` state flag, which the settings easter egg toggles
and which `settings.js:379` pushes into `log.js` via `setRuntimeDebug()`.
`log.js` merges them: `isDebug()` is `DEBUG || _runtimeDebug`. That merged
value feeds exactly two things — the log level threshold (`log.js:24`) and the
red banner (`views/helpers.js:71`). Making the banner user-toggleable is the
intended behavior of #145, and this PR leaves it alone.
`generateMnemonic()` does **not** consult `isDebug()`. It reads the
compile-time `DEBUG` binding directly. That distinction is what makes a release
build coherent: with `__BUILD_DEBUG__` false, `DEBUG` is false in the bundle,
so no amount of clicking the version ten times and flipping the toggle can
reach `return DEBUG_MNEMONIC`. The user can turn the banner and verbose logging
on in a release build; they cannot turn the hardcoded phrase on.
The failure mode to guard against is someone later "tidying up" the two flags by
routing `wallet.js` through `isDebug()`, which would silently reintroduce this
exact vulnerability with the runtime toggle as the trigger. Three things now
guard that: a comment at the `wallet.js` call site saying it must stay the
compile-time constant and why, the same statement in the README DEBUG Mode
Policy, and a regression test that calls `setRuntimeDebug(true)`, asserts
`isDebug()` is genuinely true, and then asserts `generateMnemonic()` still
returns fresh entropy.
I considered instead making the runtime toggle unavailable in release builds,
but rejected it: that removes a feature #145 deliberately added, and it defends
the wrong boundary. The banner is not the dangerous part; the mnemonic path is,
and that one is already unreachable.
## Verification
`make check` — green, 5 suites, 55 tests, plus lint and fmt-check. It also ran
via the pre-commit hook on the commit itself.
The new tests, per the verification standard in the manager comment on the
issue (not just `a !== b`) — with the flag off: two successive
`generateMnemonic()` calls differ, both pass `isValidMnemonic`, both are 12
words, neither equals `DEBUG_MNEMONIC`, and the result derives a usable HD
wallet (`xpub` + a well-formed first address), so a broken implementation
returning a counter or a truncated phrase would fail. Same assertions again
with the runtime toggle forced on. With the flag on (`__BUILD_DEBUG__` defined
before a `jest.resetModules()` re-require): `DEBUG` is `true` and
`generateMnemonic()` returns `DEBUG_MNEMONIC`, so the debug path is proven
working rather than silently deleted.
Build artifacts — `make build` and `make build-debug` both produce `dist/chrome`
and `dist/firefox` successfully. Grepping the minified bundles for the emitted
`DEBUG` export value across all four bundles (chrome popup, chrome background,
firefox popup, firefox background):
# after make build
$ grep -roh 'DEBUG:![01]' dist/chrome dist/firefox | sort | uniq -c
4 DEBUG:!1
# after make build-debug
$ grep -roh 'DEBUG:![01]' dist/chrome dist/firefox | sort | uniq -c
4 DEBUG:!0
`!1` is minified `false`, `!0` is `true`. Also checked the fail-safe path:
`AUTISTMASK_DEBUG=true make build` prints `Build mode: release (DEBUG off)` and
likewise yields `4 DEBUG:!1`.
One thing a reviewer should know about the grep: the `DEBUG_MNEMONIC` string
literal is still present in the release bundle. That is not a leak of anything
(the phrase is in this public repo already) and it does not mean the branch is
live — esbuild cannot tree-shake a CommonJS `module.exports` object, so the
constant survives while `DEBUG` folds to `false`. The compiled function is
`function PL(){return ML?UL:f_.fromEntropy(globalThis.crypto.getRandomValues(new Uint8Array(16))).phrase}`
where `ML` is the `DEBUG:!1` export. So "the phrase string is absent" is *not*
the right test for a release build; "the exported `DEBUG` is `!1`" is, which is
what I checked.
## `TODO.md` refresh
Per the manager comment: Status rewritten (no branch in flight —
`feat/issue-144-settings-about` landed as #145, scripts-to-rule-them-all landed
as #148, so the `scripts/` question is resolved; `make check` recorded as
verified green on `main` at `23aeae4`); the completed "Verify main passes make
check" Future Step removed; Future Steps rewritten against the #149-#168
backlog in rough priority order, keeping branch pruning (now #167) and the
pre-1.0 security review (noting #149 and #157 are parts of it but it is
broader).
One deliberate deviation to flag rather than bury: the manager asked that Next
Step become this issue. Taken literally against the Workflow section, this
commit *completes* #149, which would normally move it into Completed Steps. I
followed the repo's existing convention for in-flight work instead — the
previous Next Step was phrased as "Land feat/issue-144-settings-about", so Next
Step is now "Land #149 ... PR open, awaiting review", which is accurate until
this merges. Whoever merges should move it to Completed Steps and promote the
first Future Step. Happy to change it if the reviewer prefers the strict
reading.
## Out of scope
`script/lint` being `prettier --check` only and unable to catch undefined
identifiers (#152) — noted in the TODO but not fixed here; I greped for `DEBUG`
consumers by hand rather than relying on lint, as advised. Nothing else in the
DEBUG consumer set (`log.js`, `helpers.js`, `state.js`, `settings.js`) changed
behavior.
Co-authored-by: sneak <sneak@sneak.berlin>
Reviewed-on: #169
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
## Summary
Add a new well at the bottom of the settings view displaying application info (license, author, version, build date, git commit hash linked to Gitea), and an easter egg that reveals a debug mode toggle after clicking the version number 10 times.
## Changes
- **`src/popup/index.html`**: Added About well with license, author, version, build date, and linked commit hash. Added hidden debug well with debug mode toggle.
- **`src/popup/views/settings.js`**: Populate About well from build-time constants. Implement version click counter (10 clicks reveals debug well). Wire up debug mode toggle to state and runtime logger.
- **`src/shared/buildInfo.js`** (new): Module exporting build-time constants (`BUILD_VERSION`, `BUILD_LICENSE`, `BUILD_AUTHOR`, `BUILD_COMMIT`, `BUILD_DATE`, `GITEA_COMMIT_URL`) injected by esbuild define.
- **`build.js`**: Read git commit hash (short + full) and version from package.json, pass as esbuild `define` constants. Also reads `GIT_COMMIT_SHORT`/`GIT_COMMIT_FULL` env vars for Docker builds.
- **`Dockerfile`**: Accept `GIT_COMMIT_SHORT` and `GIT_COMMIT_FULL` build args, set as env vars for the build step.
- **`Makefile`**: Pass git commit hashes as Docker build args.
- **`src/shared/state.js`**: Add `debugMode` boolean to state (persisted).
- **`src/shared/log.js`**: Add runtime debug flag that supplements the compile-time `DEBUG` constant. Debug mode toggle updates this flag immediately.
closes#144
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Co-authored-by: clawbot <clawbot@sneak.cloud>
Co-authored-by: user <user@Mac.lan guest wan>
Co-authored-by: clawbot <clawbot@eeqj.de>
Co-authored-by: sneak <sneak@sneak.berlin>
Reviewed-on: #145
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
## 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>
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>
## 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>
## 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>
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>
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>
## Summary
Adds Sepolia testnet support to AutistMask.
### Changes
- **New `src/shared/networks.js`** — centralized network definitions (mainnet + Sepolia) with chain IDs, default RPC/Blockscout endpoints, and block explorer URLs
- **State management** — `networkId` added to persisted state; defaults to mainnet for backward compatibility
- **Settings UI** — network selector dropdown lets users switch between Ethereum Mainnet and Sepolia Testnet
- **Dynamic explorer links** — all hardcoded `etherscan.io` URLs replaced with dynamic links from the current network config (`sepolia.etherscan.io` for Sepolia)
- **Background service** — `wallet_switchEthereumChain` now accepts both mainnet (0x1) and Sepolia (0xaa36a7); broadcasts `chainChanged` to connected dApps
- **Inpage provider** — fetches chain ID on init and updates dynamically via `chainChanged` events (no more hardcoded `0x1`)
- **Blockscout API** — uses `eth-sepolia.blockscout.com/api/v2` for Sepolia
- **Etherscan labels** — phishing/scam checks use the correct explorer per network
- **Price fetching** — skipped on testnets (testnet tokens have no real market value)
- **RPC validation** — checks against the selected network's chain ID, not hardcoded mainnet
- **ethers provider** — `getProvider()` uses the correct ethers `Network` for Sepolia
### API Endpoints Verified
| Service | Mainnet | Sepolia |
|---------|---------|--------|
| Etherscan | etherscan.io | sepolia.etherscan.io |
| Blockscout | eth.blockscout.com/api/v2 | eth-sepolia.blockscout.com/api/v2 |
| RPC | ethereum-rpc.publicnode.com | ethereum-sepolia-rpc.publicnode.com |
| CoinDesk (prices) | ✅ | N/A (skipped on testnet) |
closes#110
Reviewed-on: #137
THIS WAS ONESHOTTED USING OPUS 4. WTAF
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
Expands the confirm-tx warning system with three new warning types, all using the existing `visibility:hidden/visible` pattern from PR #98 (no animations, no layout shift).
## Changes
1. **Scam address list expanded** (7 → 652 addresses): Sourced from [MyEtherWallet/ethereum-lists](https://github.com/MyEtherWallet/ethereum-lists) darklist (MIT license). Checked synchronously before sending.
2. **Contract address warning**: When the recipient is a smart contract (detected via `getCode`), shows a warning that sending directly to a contract may result in permanent loss of funds.
3. **Null/burn address warning**: Detects known burn addresses (`0x0000...0000`, `0x...dead`, `0x...deadbeef`) and warns that funds are permanently destroyed.
4. **No-history warning** (existing from #98): Unchanged, still shows for EOAs with zero transaction history.
All warnings use reserved-space `visibility:hidden/visible` elements — no layout shift, no animations.
closes#114
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Co-authored-by: user <user@Mac.lan guest wan>
Co-authored-by: clawbot <clawbot@eeqj.de>
Reviewed-on: #118
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
Co-committed-by: clawbot <sneak+clawbot@sneak.cloud>
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.
- 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