Resolve all README FIXMEs and enforce truncation safety
All checks were successful
check / check (push) Successful in 18s

- Update Architecture tree to match actual src/ structure
- Fix settings button to have border and hover state (Clickable Affordance)
- Cap truncateMiddle to remove at most 10 chars (anti-spoofing guard)
- Raise caller floor from 10 to 32 chars for address display
- Fill in default RPC URL (ethereum-rpc.publicnode.com)
- Fix dependencies table intro (four runtime libs, not two)
- Clean up TODO section: remove all completed items
This commit is contained in:
2026-02-27 16:48:00 +07:00
parent d67023e80d
commit 6b301dee28
6 changed files with 153 additions and 145 deletions

273
README.md
View File

@@ -6,6 +6,23 @@ Chrome and Firefox. It manages HD wallets derived from BIP-39 seed phrases and
supports sending and receiving ETH and ERC-20 tokens, as well as web3 site supports sending and receiving ETH and ERC-20 tokens, as well as web3 site
connection and authentication via the EIP-1193 provider API. connection and authentication via the EIP-1193 provider API.
The most popular browser-based EVM wallet has a cute mascot, but sucks now. It
has tracking, ads, preferred placement for swaps, tx broadcast fuckery,
intercepts tx status links to their own site instead of going to Etherscan, etc.
None of the common alternatives work on Firefox.
Hence, a minimally viable ERC20 browser wallet/signer that works cross-platform.
Everything you need, nothing you don't. We import as few libraries as possible,
don't implement any crypto, and don't send user-specific data anywhere but a
(user-configurable) Ethereum RPC endpoint (which defaults to a public node). The
extension contacts precisely two external services: the configured RPC node for
blockchain interactions, and a public CoinDesk API (no API key) to get realtime
price information.
In the extension is a hardcoded list of the top ERC20 contract addresses. You
can add any ERC20 contract by contract address if you wish, but the hardcoded
list exists to detect symbol spoofing attacks and improve UX.
## Getting Started ## Getting Started
```bash ```bash
@@ -24,9 +41,9 @@ Load the extension:
## Rationale ## Rationale
MetaMask has become bloated with swap UIs, portfolio dashboards, analytics, Common popular EVM wallets have become bloated with swap UIs, portfolio
tracking, and advertisements. It is no longer a simple wallet. Most alternatives dashboards, analytics, tracking, and advertisements. It is no longer a simple
(Rabby, Rainbow, etc.) only support Chromium browsers, leaving Firefox users wallet. Most alternatives only support Chromium browsers, leaving Firefox users
without a usable option. without a usable option.
AutistMask exists to provide the absolute minimum viable Ethereum wallet AutistMask exists to provide the absolute minimum viable Ethereum wallet
@@ -47,46 +64,69 @@ separate output directories.
``` ```
src/ src/
background/ — service worker / background script background/ — service worker / background script
index.js — extension lifecycle, message routing index.js — RPC routing, approval flows, message signing
wallet.js — wallet management (create, import, derive via ethers.js) content/ — content script injected into web pages
provider.js — EIP-1193 JSON-RPC provider implementation index.js — relay between inpage provider and background
inpage.js — the window.ethereum provider object (EIP-1193)
popup/ — popup UI (the main wallet interface) popup/ — popup UI (the main wallet interface)
index.html index.html
index.js index.js — entry point, view routing, state restore
styles/ — CSS (Tailwind) styles/main.css — Tailwind source
content/content script injected into web pages views/ — one JS module per screen (home, send, approval, etc.)
index.jsinjects the provider into page context shared/ modules used by both popup and background
inpage.js — the window.ethereum provider object balances.js — ETH + ERC-20 balance fetching via RPC + Blockscout
shared/ — shared utilities constants.js — chain IDs, default RPC endpoint, ERC-20 ABI
vault.js — encrypted storage via libsodium ens.js — ENS forward/reverse resolution
constants.js — chain IDs, default RPC endpoints, ERC-20 ABI prices.js — ETH/USD and token/USD via CoinDesk API
scamlist.js — known fraud contract addresses
state.js — persisted state (extension storage)
tokenList.js — top ERC-20 tokens by market cap (hardcoded)
transactions.js — tx history fetching + anti-poisoning filters
uniswap.js — Uniswap Universal Router calldata decoder
vault.js — password-based encryption via libsodium
wallet.js — mnemonic generation, HD derivation, signing
manifest/ manifest/
chrome.json — Manifest V3 for Chrome chrome.json — Manifest V3 for Chrome
firefox.json — Manifest V2/V3 for Firefox firefox.json — Manifest V2 for Firefox
``` ```
### UI Design Philosophy ### UI Design Philosophy
The UI follows a "Universal Paperclips" aesthetic — a deliberately spartan, The UI is inspired by _Universal Paperclips_. It's deliberately minimal,
almost brutalist approach. The guiding principle is that an unskilled, monochrome, fast, and includes once-popular usability affordances that seem to
non-technical person should be able to figure out how to use it without any have fallen out of fashion in modern UI design. Clickable things look clickable.
crypto knowledge. Things don't flash or spin or move around unnecessarily. This is a tool for
getting work done, not a toy.
This is designed for a normal audience. Basic familiarity with cryptocurrency
terms is required, but you need not be a programmer or software engineer to use
this wallet.
If you _are_ basically familiar with cryptocurrency terms, you should be able to
use all of the main features of this wallet without having to read the
documentation; i.e. we wish for the primary functionality to remain easily
discoverable.
#### Visual Style #### Visual Style
- **Monochrome**: Black text on white background. No brand colors, no gradients, - **Monochrome**: Black text on white background. Color is only used when and
no color-coding. Color may be introduced later for specific semantic purposes where it is semantically meaningful and explicitly useful, such as error
(e.g. error states) but the baseline is monochrome. messages, critical warnings, or address disambiguation. (Notable exception: we
use color dots, and identicons, to help a user easily distinguish addresses.)
- **Text-first**: Every piece of information is presented as text. Balances are - **Text-first**: Every piece of information is presented as text. Balances are
numbers. Addresses are hex strings. Status is a sentence. No progress spinners numbers. Addresses are hex strings. Flash messages are sentences. All fiddly
with animations — a text status line is sufficient. bits can be clicked to copy to the clipboard, and external links to Etherscan
are provided everywhere they might be useful.
- **Monospace font**: All text is rendered in the system monospace font. - **Monospace font**: All text is rendered in the system monospace font.
Ethereum addresses, transaction hashes, and balances are inherently Ethereum addresses, transaction hashes, and balances are inherently
fixed-width data. Rather than mixing proportional and monospace fonts, we use fixed-width data. Rather than mixing proportional and monospace fonts, we use
monospace everywhere for visual consistency and alignment. monospace everywhere for visual consistency and alignment.
- **No images**: Zero image assets in the entire extension. No logos, no - **No images**: Zero image assets in the entire extension. No logos, no
illustrations, no token icons. Token identity is conveyed by symbol text (ETH, illustrations, no token icons. Token identity is conveyed by symbol text (ETH,
USDC, etc.). USDC, etc.). We display
[Blockie identicons](https://github.com/MyCryptoHQ/ethereum-blockies-base64)
on critical screens and when space is available to allow users to disambiguate
addresses visually, as a security feature.
- **Tailwind CSS**: Utility-first CSS via Tailwind. No custom CSS classes for - **Tailwind CSS**: Utility-first CSS via Tailwind. No custom CSS classes for
styling. Tailwind is configured with a minimal monochrome palette. This keeps styling. Tailwind is configured with a minimal monochrome palette. This keeps
the styling co-located with the markup and eliminates CSS file management. the styling co-located with the markup and eliminates CSS file management.
@@ -95,16 +135,19 @@ crypto knowledge.
would add bundle size, build complexity, and attack surface for no benefit at would add bundle size, build complexity, and attack surface for no benefit at
this scale. this scale.
- **360x600 popup**: Standard browser extension popup dimensions. The UI is - **360x600 popup**: Standard browser extension popup dimensions. The UI is
designed for this fixed viewport — no responsive breakpoints needed. designed for this fixed viewport.
#### No Layout Shift #### No Layout Shift
Asynchronous state changes (clipboard confirmation, transaction status, error Asynchronous state changes (clipboard confirmation, transaction status, error
messages, flash notifications) must never move existing UI elements. All dynamic messages, flash notifications, API results returning) must _never_ move around
content areas reserve their space up front using `min-height` or always-present the existing UI elements. All dynamic content areas must reserve their space up
wrapper elements. `visibility: hidden` is preferred over `display: none` when front using `min-height` or always-present wrapper elements.
the element's space must be preserved. This prevents jarring content jumps that `visibility: hidden` is preferred over `display: none` when the element's space
disorient users and avoids mis-clicks caused by shifting buttons. must be preserved. This prevents jarring content jumps that disorient users and
can cause dangerous mis-clicks. Anyone who has multi-tabled on ClubGG and
smashed the big red "all-in blind preflop" button when trying to simply "call"
on a different table knows exactly tf I am talking about.
#### Clickable Affordance #### Clickable Affordance
@@ -125,25 +168,33 @@ wherever shown. If a formatting rule applies in one place, it applies in every
place. Users should never see the same value rendered differently on two place. Users should never see the same value rendered differently on two
screens. screens.
**Exception — Transaction Detail view:** The transaction detail screen is the **Specific Exception — Truncation:** On some non-critical display locations, we
authoritative record of a specific transaction and shows the exact, untruncated may truncate _a small number_ of characters from the middle of an address solely
amount with all meaningful decimal places (e.g. "0.00498824598498216 ETH"). It due to display size constraints. Wherever possible, and, notably, **in all
also shows the native quantity (e.g. "4988245984982160 wei") below it. Both are critical contexts (transaction confirmation view before signing, transaction
click-copyable. Truncating to 4 decimals in summary views is acceptable for history detail view) addresses will _NEVER_ be truncated**. Even in places we
scannability, but the detail view must never discard precision — it is the one truncate addresses, we truncate only a maximum of 10 characters, which means
place the user goes to verify exact figures. that the portions still displayed will be more than adequate for the user to
verify addresses even in the case of address spoofing attacks. Clicking an
address will always copy the full, untruncated value.
**Specific Exception — Transaction Detail view:** The transaction detail screen
is the authoritative record of a specific transaction and shows the exact,
untruncated amount with all meaningful decimal places (e.g. "0.00498824598498216
ETH"). It also shows the native quantity (e.g. "4988245984982160 wei") below it.
Both are click-copyable. Truncating to 4 decimals in summary views is acceptable
for scannability, but the detail view must never discard precision — it is the
one place the user can always use to verify exact details.
#### Language & Labeling #### Language & Labeling
All user-facing text avoids crypto jargon wherever possible: All user-facing text avoids unnecessary jargon wherever possible:
- "Recovery phrase" instead of "seed phrase", "mnemonic", or "BIP-39 mnemonic" - "Recovery phrase" instead of "seed phrase", "mnemonic", or "BIP-39 mnemonic"
- "Address" instead of "account", "derived key", or "HD child" - "Address" instead of "account", "derived key", or "HD child"
- "Password" instead of "encryption key" or "vault passphrase" - "Password" instead of "encryption key" or "vault passphrase"
- "Private key" instead of "secret key" or "signing key"
- Buttons use plain verbs: "Send", "Receive", "Copy address", "Add", "Back", - Buttons use plain verbs: "Send", "Receive", "Copy address", "Add", "Back",
"Cancel", "Lock", "Unlock", "Allow", "Deny" "Cancel", "Lock", "Unlock", "Allow", "Deny"
- No bracket notation like `[locked]` or `[setup]` — just plain titles
- Helpful inline descriptions where needed (e.g. "This password locks the wallet - Helpful inline descriptions where needed (e.g. "This password locks the wallet
on this device. It is not the same as your recovery phrase.") on this device. It is not the same as your recovery phrase.")
- Error messages are full sentences ("Please enter your password." not "password - Error messages are full sentences ("Please enter your password." not "password
@@ -152,14 +203,14 @@ All user-facing text avoids crypto jargon wherever possible:
#### Full Identifiers Policy #### Full Identifiers Policy
Addresses, transaction hashes, contract addresses, and all other cryptographic Addresses, transaction hashes, contract addresses, and all other cryptographic
identifiers are displayed in full whenever possible — never truncated. Address identifiers are displayed in full whenever possible. We truncate only in
poisoning attacks exploit truncated displays by generating fraud addresses that specific, limited, non-critical places and even then only a small amount that
share the same prefix and suffix as a legitimate address. If a user only sees still prevents spoofing attacks. Address poisoning attacks exploit truncated
`0xAbCd...1234`, an attacker can create an address with the same visible displays by generating fraud addresses that share the same prefix and suffix as
characters and trick the user into sending funds to it. Showing the complete a legitimate address. If a user only sees `0xAbCd...1234`, an attacker can
identifier defeats this class of attack. Truncation is only acceptable in create an address with the same visible characters and trick the user into
space-constrained contexts where the full identifier is accessible one tap away sending funds to it. Showing the complete identifier defeats this class of
(e.g. a tooltip or copy action). attack.
#### Data Model #### Data Model
@@ -483,20 +534,20 @@ transitions.
### External Services ### External Services
AutistMask is not a fully self-contained offline tool. It necessarily AutistMask is not a fully self-contained offline tool. It necessarily
communicates with external services to function as a wallet: communicates with two external services to function as a wallet:
- **Ethereum JSON-RPC endpoint**: The extension needs an Ethereum node to query - **Ethereum JSON-RPC endpoint**: The extension needs an Ethereum node to query
balances (`eth_getBalance`), read ERC-20 token contracts (`eth_call`), balances (`eth_getBalance`), read ERC-20 token contracts (`eth_call`),
estimate gas (`eth_estimateGas`), fetch nonces (`eth_getTransactionCount`), estimate gas (`eth_estimateGas`), fetch nonces (`eth_getTransactionCount`),
broadcast transactions (`eth_sendRawTransaction`), and check transaction broadcast transactions (`eth_sendRawTransaction`), and check transaction
receipts. The default endpoint is a public RPC (configurable by the user to receipts. The default endpoint is a public RPC (configurable by the user to
any endpoint they prefer, including a local node). This is the only external any endpoint they prefer, including a local node). By default the extension
service the extension talks to. talks to `https://ethereum-rpc.publicnode.com`.
- **CoinDesk CADLI price API**: Used to fetch ETH/USD and token/USD prices for - **CoinDesk CADLI price API**: Used to fetch ETH/USD and token/USD prices for
displaying fiat values. The price is cached for 5 minutes to avoid excessive displaying fiat values. The price is cached for 5 minutes to avoid excessive
requests. No API key required. No user data is sent — only a list of token requests. No API key required. No user data is sent — only a list of token
symbols. symbols. Note that CoinDesk will receive your client IP.
What the extension does NOT do: What the extension does NOT do:
@@ -512,8 +563,8 @@ services. Users who want maximum privacy can point the RPC at their own node
### Dependencies ### Dependencies
AutistMask uses two runtime libraries. All cryptographic operations are AutistMask uses four runtime libraries. All cryptographic operations are
delegated to these libraries — see the Crypto Policy section below. delegated to ethers and libsodium — see the Crypto Policy section below.
| Package | Version | License | Purpose | | Package | Version | License | Purpose |
| -------------------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -------------------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -538,17 +589,11 @@ Dev dependencies (not shipped in extension):
`pbkdf`, `hmac`, `encrypt`, `decrypt`, `hash`, `cipher`, `digest`, `sign` `pbkdf`, `hmac`, `encrypt`, `decrypt`, `hash`, `cipher`, `digest`, `sign`
(case-insensitive) appear in our own source code (outside of `node_modules/`), (case-insensitive) appear in our own source code (outside of `node_modules/`),
it is almost certainly a bug. All cryptographic operations must go through it is almost certainly a bug. All cryptographic operations must go through
`ethers` or `libsodium-wrappers-sumo`. This policy exists because: `ethers` or `libsodium-wrappers-sumo`. Both are widely audited and
battle-tested.
- Rolling your own crypto is the single most common source of security
vulnerabilities in wallet software.
- Both libraries are widely audited and battle-tested.
- Keeping crypto out of application code makes security review tractable:
reviewers only need to verify that we call the libraries correctly, not that
we implemented crypto correctly.
Exceptions require explicit authorization in a code comment referencing this Exceptions require explicit authorization in a code comment referencing this
policy. policy, but as of now there are none.
### DEBUG Mode Policy ### DEBUG Mode Policy
@@ -580,12 +625,12 @@ project owner.
- **The password is NOT used in address derivation.** It exists solely to - **The password is NOT used in address derivation.** It exists solely to
protect the recovery phrase / private key on disk. Anyone with the protect the recovery phrase / private key on disk. Anyone with the
recovery phrase can restore the wallet on any device without this recovery phrase can restore the wallet on any device without this
password. This matches MetaMask's behavior. password. This matches standard EVM wallet behavior.
- **BIP-39 / BIP-44 via ethers.js**: Mnemonic generation, validation, and HD key - **BIP-39 / BIP-44 via ethers.js**: Mnemonic generation, validation, and HD key
derivation (`m/44'/60'/0'/0/n`) are handled entirely by ethers.js. The BIP-39 derivation (`m/44'/60'/0'/0/n`) are handled entirely by ethers.js. The BIP-39
passphrase is always empty (matching MetaMask and most wallet software). The passphrase is always empty (matching most wallet software). The user's
user's password is completely separate and has no effect on which addresses password is completely separate and has no effect on which addresses are
are generated. generated.
- **ethers.js for everything Ethereum**: Transaction construction, signing, gas - **ethers.js for everything Ethereum**: Transaction construction, signing, gas
estimation, RPC communication, ERC-20 contract calls, and address derivation estimation, RPC communication, ERC-20 contract calls, and address derivation
are all handled by ethers.js. This means zero hand-rolled Ethereum logic. are all handled by ethers.js. This means zero hand-rolled Ethereum logic.
@@ -730,104 +775,58 @@ Currently supported:
"Swap USDT → ETH"), token addresses, amounts, execution steps, and deadline. "Swap USDT → ETH"), token addresses, amounts, execution steps, and deadline.
Decodes Permit2, V2/V3/V4 swaps, wrap/unwrap, and balance checks. Decodes Permit2, V2/V3/V4 swaps, wrap/unwrap, and balance checks.
### Non-Goals ### Non-Goals Forever
- Token swaps (use a DEX in the browser) - Built in token swaps (use a DEX in the browser)
- NFT display or management
- Multi-chain support (Ethereum mainnet only, for now)
- Analytics, telemetry, or tracking of any kind - Analytics, telemetry, or tracking of any kind
- Advertisements or promotions - Advertisements or promotions
- Phishing detection - Obscure token list auto-discovery (user adds tokens manually)
- Hardware wallet support (maybe later) - We detect common/popular ERC20s in the basic case
- Token list auto-discovery (user adds tokens manually)
- Fiat on/off ramps - Fiat on/off ramps
- Browser notifications - Extensive transaction decoding/parsing
- Transaction history (use Etherscan) - For common ones we will do best-effort, but you should just use a block
explorer.
## TODO — 0.1.0 MVP ### Non-Goals for 1.0
Everything needed for a minimal working wallet that can send and receive ETH. - Multi-chain support (Ethereum mainnet only)
- Hardware wallet support
### Done ## TODO
- [x] Project scaffolding (Makefile, Dockerfile, CI, manifests, esbuild)
- [x] Tailwind CSS build pipeline
- [x] Popup UI shell with screen stacking (Welcome, AddWallet, Home,
AddressDetail, Send, Receive, Settings)
- [x] BIP-39 mnemonic generation via ethers.js (die button)
- [x] BIP-39 mnemonic validation on import
- [x] BIP-32/BIP-44 HD key derivation (real addresses from xpub)
- [x] Private key import (real address via ethers.Wallet)
- [x] Xpub stored unencrypted for read-only address derivation
- [x] State persistence to extension storage (survives popup close)
- [x] Live ETH balance fetching via JSON-RPC (`eth_getBalance`)
- [x] ENS reverse lookup (address → name) and forward resolution (name → address
in Send field)
- [x] ETH/USD price fetching via CoinDesk API
- [x] USD value display next to ETH balances
- [x] Full address display everywhere (no truncation)
- [x] Token list module with ~150 ERC-20 tokens ordered by market cap
### Wallet Management ### Wallet Management
- [ ] Rename wallets (tap wallet name on Home to edit)
- [ ] Rename addresses (tap address name on AddressDetail to edit)
- [ ] Delete wallet (with confirmation) - [ ] Delete wallet (with confirmation)
- [ ] Delete address from HD wallet (with confirmation) - [ ] Delete address from HD wallet (with confirmation)
- [ ] Show wallet's recovery phrase (requires password, from Settings or wallet - [ ] Show wallet's recovery phrase (requires password)
context menu)
### Sending ### Transactions
- [x] Encrypt recovery phrase / private key with password via libsodium
(Argon2id + XSalsa20-Poly1305)
- [x] Password prompt on Send (decrypt private key to construct transaction)
- [x] Transaction construction via ethers.js (to, value, gasLimit, gasPrice)
- [ ] Gas estimation and fee display before confirming - [ ] Gas estimation and fee display before confirming
- [x] Broadcast transaction via `eth_sendRawTransaction`
- [x] Transaction status feedback (pending → confirmed / failed)
### Receiving
- [x] QR code generation for address (qrcode library, renders to canvas)
### Display
- [ ] Home screen: total ETH balance summed across all addresses
- [ ] Home screen: total USD value (small text under total ETH)
- [ ] Cache ETH/USD price for 5 minutes (don't re-fetch on every popup open)
- [ ] Per-address USD value in small text under ETH balance everywhere
### Tokens (ERC-20)
- [ ] Add token by contract address (fetch name/symbol/decimals from contract)
- [ ] Display ERC-20 token balances per address
- [ ] Send ERC-20 tokens
### Testing ### Testing
- [ ] Tests for mnemonic generation and address derivation - [ ] Tests for mnemonic generation and address derivation
- [ ] Tests for xpub derivation and child address generation - [ ] Tests for xpub derivation and child address generation
- [ ] Tests for token list module (getTopTokenPrices, getTopTokenSymbols)
- [ ] Test on Chrome (Manifest V3)
- [ ] Test on Firefox (Manifest V2) - [ ] Test on Firefox (Manifest V2)
### Scam List ### Scam List
- [ ] Research and document each address in scamlist.js (what it is, why it's on - [ ] Research and document each address in scamlist.js
the list, source) - [ ] Add more known fraud addresses from Etherscan labels
- [ ] Add more known fraud addresses from Etherscan labels (drainers, phishing,
address poisoning deployers)
### Post-MVP ### Future
- [ ] EIP-1193 provider injection (window.ethereum) for web3 site connectivity
- [ ] Site connection approval flow
- [ ] Transaction signing approval flow (requests from connected sites)
- [ ] Message signing (`personal_sign`, `eth_sign`)
- [ ] Multi-currency fiat display (EUR, GBP, etc.) - [ ] Multi-currency fiat display (EUR, GBP, etc.)
- [ ] Security audit of key management - [ ] Security audit of key management
## Policies
- We don't mention "the other wallet" by name in code or documentation. We're
our own thing.
- The README is the complete authoritative technical documentation. It's ok if
it gets big.
## License ## License
GPL-3.0. See [LICENSE](LICENSE). GPL-3.0. See [LICENSE](LICENSE).

View File

@@ -23,7 +23,7 @@
</h1> </h1>
<button <button
id="btn-settings" id="btn-settings"
class="bg-transparent border-none text-fg cursor-pointer text-2xl p-0 leading-none" class="border border-border text-fg cursor-pointer text-xl px-1 leading-none hover:bg-fg hover:text-bg"
title="Settings" title="Settings"
> >
&#9881; &#9881;

View File

@@ -191,7 +191,7 @@ function renderTransactions(txs) {
const amountStr = tx.value const amountStr = tx.value
? escapeHtml(tx.value + " " + tx.symbol) ? escapeHtml(tx.value + " " + tx.symbol)
: escapeHtml(tx.symbol); : escapeHtml(tx.symbol);
const maxAddr = Math.max(10, 36 - Math.max(0, amountStr.length - 10)); const maxAddr = Math.max(32, 36 - Math.max(0, amountStr.length - 10));
const displayAddr = ensName || truncateMiddle(counterparty, maxAddr); const displayAddr = ensName || truncateMiddle(counterparty, maxAddr);
const addrStr = escapeHtml(displayAddr); const addrStr = escapeHtml(displayAddr);
const dot = addressDotHtml(counterparty); const dot = addressDotHtml(counterparty);

View File

@@ -216,7 +216,7 @@ function renderTransactions(txs) {
const amountStr = tx.value const amountStr = tx.value
? escapeHtml(tx.value + " " + tx.symbol) ? escapeHtml(tx.value + " " + tx.symbol)
: escapeHtml(tx.symbol); : escapeHtml(tx.symbol);
const maxAddr = Math.max(10, 36 - Math.max(0, amountStr.length - 10)); const maxAddr = Math.max(32, 36 - Math.max(0, amountStr.length - 10));
const displayAddr = ensName || truncateMiddle(counterparty, maxAddr); const displayAddr = ensName || truncateMiddle(counterparty, maxAddr);
const addrStr = escapeHtml(displayAddr); const addrStr = escapeHtml(displayAddr);
const dot = addressDotHtml(counterparty); const dot = addressDotHtml(counterparty);

View File

@@ -131,9 +131,18 @@ function balanceLinesForAddress(addr, trackedTokens, showZero) {
return html; return html;
} }
// Truncate the middle of a string, replacing removed characters with "…".
// Safety: refuses to truncate more than 10 characters, which is the maximum
// that still prevents address spoofing attacks (see Display Consistency in
// README). Callers that need to display less should use a different UI
// approach rather than silently making addresses insecure.
function truncateMiddle(str, maxLen) { function truncateMiddle(str, maxLen) {
if (str.length <= maxLen) return str; if (str.length <= maxLen) return str;
if (maxLen < 5) return str.slice(0, maxLen); const removed = str.length - maxLen + 1; // +1 for the ellipsis char
if (removed > 10) {
maxLen = str.length - 10 + 1;
}
if (maxLen >= str.length) return str;
const half = Math.floor((maxLen - 1) / 2); const half = Math.floor((maxLen - 1) / 2);
return str.slice(0, half) + "\u2026" + str.slice(-(maxLen - 1 - half)); return str.slice(0, half) + "\u2026" + str.slice(-(maxLen - 1 - half));
} }

View File

@@ -140,7 +140,7 @@ function renderHomeTxList(ctx) {
const amountStr = tx.value const amountStr = tx.value
? escapeHtml(tx.value + " " + tx.symbol) ? escapeHtml(tx.value + " " + tx.symbol)
: escapeHtml(tx.symbol); : escapeHtml(tx.symbol);
const maxAddr = Math.max(10, 36 - Math.max(0, amountStr.length - 10)); const maxAddr = Math.max(32, 36 - Math.max(0, amountStr.length - 10));
const displayAddr = truncateMiddle(counterparty, maxAddr); const displayAddr = truncateMiddle(counterparty, maxAddr);
const addrStr = escapeHtml(displayAddr); const addrStr = escapeHtml(displayAddr);
const dot = addressDotHtml(counterparty); const dot = addressDotHtml(counterparty);