diff --git a/README.md b/README.md index 5b605d2..3a8fe8a 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,24 @@ crypto knowledge. - **360x600 popup**: Standard browser extension popup dimensions. The UI is designed for this fixed viewport — no responsive breakpoints needed. +#### No Layout Shift + +Asynchronous state changes (clipboard confirmation, transaction status, error +messages, flash notifications) must never move existing UI elements. All dynamic +content areas reserve their space up front using `min-height` or always-present +wrapper elements. `visibility: hidden` is preferred over `display: none` when +the element's space must be preserved. This prevents jarring content jumps that +disorient users and avoids mis-clicks caused by shifting buttons. + +#### Clickable Affordance + +Every interactive element must visually indicate that it is clickable. Buttons +use a visible border, padding, and a hover state (invert to white-on-black). +Text that triggers an action (e.g. "Import private key") uses an underline. No +invisible hit targets, no bare text that happens to have a click handler. If it +does something when you click it, it must look like it does something when you +click it. + #### Language & Labeling All user-facing text avoids crypto jargon wherever possible: @@ -509,6 +527,13 @@ Everything needed for a minimal working wallet that can send and receive ETH. - [ ] Test on Chrome (Manifest V3) - [ ] Test on Firefox (Manifest V2) +### Scam List + +- [ ] Research and document each address in scamlist.js (what it is, why it's on + the list, source) +- [ ] Add more known fraud addresses from Etherscan labels (drainers, phishing, + address poisoning deployers) + ### Post-MVP - [ ] EIP-1193 provider injection (window.ethereum) for web3 site connectivity diff --git a/manifest/chrome.json b/manifest/chrome.json index 9921717..c589225 100644 --- a/manifest/chrome.json +++ b/manifest/chrome.json @@ -4,6 +4,7 @@ "version": "0.1.0", "description": "Minimal Ethereum wallet for Chrome", "permissions": ["storage", "activeTab"], + "host_permissions": [""], "action": { "default_popup": "src/popup/index.html" }, @@ -11,6 +12,12 @@ "service_worker": "src/background/index.js" }, "content_scripts": [ + { + "matches": [""], + "js": ["src/content/inpage.js"], + "run_at": "document_start", + "world": "MAIN" + }, { "matches": [""], "js": ["src/content/index.js"], diff --git a/manifest/firefox.json b/manifest/firefox.json index 758ae5d..830f5c3 100644 --- a/manifest/firefox.json +++ b/manifest/firefox.json @@ -3,7 +3,7 @@ "name": "AutistMask", "version": "0.1.0", "description": "Minimal Ethereum wallet for Firefox", - "permissions": ["storage", "activeTab"], + "permissions": ["storage", "activeTab", ""], "browser_action": { "default_popup": "src/popup/index.html" }, diff --git a/src/background/index.js b/src/background/index.js index fbf9591..9d3b820 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -2,8 +2,12 @@ // Handles EIP-1193 RPC requests from content scripts and proxies // non-sensitive calls to the configured Ethereum JSON-RPC endpoint. -const CHAIN_ID = "0x1"; -const DEFAULT_RPC = "https://eth.llamarpc.com"; +const { + ETHEREUM_MAINNET_CHAIN_ID, + DEFAULT_RPC_URL, +} = require("../shared/constants"); +const { state, loadState, saveState } = require("../shared/state"); +const { refreshBalances } = require("../shared/balances"); const storageApi = typeof browser !== "undefined" @@ -17,7 +21,7 @@ const connectedSites = {}; async function getState() { const result = await storageApi.get("autistmask"); - return result.autistmask || { wallets: [], rpcUrl: DEFAULT_RPC }; + return result.autistmask || { wallets: [], rpcUrl: DEFAULT_RPC_URL }; } async function getAccounts() { @@ -33,7 +37,7 @@ async function getAccounts() { async function getRpcUrl() { const state = await getState(); - return state.rpcUrl || DEFAULT_RPC; + return state.rpcUrl || DEFAULT_RPC_URL; } // Proxy an RPC call to the Ethereum node @@ -94,7 +98,7 @@ async function handleRpc(method, params, origin) { } if (method === "eth_chainId") { - return { result: CHAIN_ID }; + return { result: ETHEREUM_MAINNET_CHAIN_ID }; } if (method === "net_version") { @@ -103,7 +107,7 @@ async function handleRpc(method, params, origin) { if (method === "wallet_switchEthereumChain") { const chainId = params?.[0]?.chainId; - if (chainId === CHAIN_ID) { + if (chainId === ETHEREUM_MAINNET_CHAIN_ID) { return { result: null }; } return { @@ -205,6 +209,24 @@ async function handleRpc(method, params, origin) { return { error: { message: "Unsupported method: " + method } }; } +// Background balance refresh: every 60 seconds when the popup isn't open. +// When the popup IS open, its 10-second interval keeps lastBalanceRefresh +// fresh, so this naturally skips. +const BACKGROUND_REFRESH_INTERVAL = 60000; + +async function backgroundRefresh() { + await loadState(); + const now = Date.now(); + if (now - (state.lastBalanceRefresh || 0) < BACKGROUND_REFRESH_INTERVAL) + return; + if (state.wallets.length === 0) return; + await refreshBalances(state.wallets, state.rpcUrl, state.blockscoutUrl); + state.lastBalanceRefresh = now; + await saveState(); +} + +setInterval(backgroundRefresh, BACKGROUND_REFRESH_INTERVAL); + // Listen for messages from content scripts runtime.onMessage.addListener((msg, sender, sendResponse) => { if (msg.type !== "AUTISTMASK_RPC") return; diff --git a/src/content/index.js b/src/content/index.js index cc708eb..b66b44c 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -1,15 +1,17 @@ // AutistMask content script — bridges between inpage (window.ethereum) // and the background service worker via extension messaging. -// Inject the inpage script into the page's JS context -const script = document.createElement("script"); -script.src = (typeof browser !== "undefined" ? browser : chrome).runtime.getURL( - "src/content/inpage.js", -); -script.onload = function () { - this.remove(); -}; -(document.head || document.documentElement).appendChild(script); +// In Chrome (MV3), inpage.js runs as a MAIN-world content script declared +// in the manifest, so no injection is needed here. In Firefox (MV2), the +// "world" key is not supported, so we inject via a