diff --git a/README.md b/README.md index 8b89104..0030313 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,10 @@ 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. +extension contacts exactly three external services: the configured RPC node for +blockchain interactions, a public CoinDesk API (no API key) for realtime price +information, and a Blockscout block-explorer API for transaction history and +token balances. All three endpoints are user-configurable. 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 @@ -534,7 +535,7 @@ transitions. ### External Services AutistMask is not a fully self-contained offline tool. It necessarily -communicates with two external services to function as a wallet: +communicates with three external services to function as a wallet: - **Ethereum JSON-RPC endpoint**: The extension needs an Ethereum node to query balances (`eth_getBalance`), read ERC-20 token contracts (`eth_call`), @@ -543,11 +544,24 @@ communicates with two external services to function as a wallet: receipts. The default endpoint is a public RPC (configurable by the user to any endpoint they prefer, including a local node). By default the extension talks to `https://ethereum-rpc.publicnode.com`. + - **Data sent**: Ethereum addresses, transaction data, contract call + parameters. The RPC endpoint can see all on-chain queries and submitted + transactions. - **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 requests. No API key required. No user data is sent — only a list of token symbols. Note that CoinDesk will receive your client IP. + - **Data sent**: Token symbol strings only (e.g. "ETH", "USDC"). No + addresses or user-specific data. + +- **Blockscout block-explorer API**: Used to fetch transaction history (normal + transactions and ERC-20 token transfers), ERC-20 token balances, and token + holder counts (for spam filtering). The default endpoint is + `https://eth.blockscout.com/api/v2` (configurable by the user in Settings). + - **Data sent**: Ethereum addresses. Blockscout receives the user's + addresses to query their transaction history and token balances. No + private keys, passwords, or signing operations are sent. What the extension does NOT do: @@ -557,9 +571,10 @@ What the extension does NOT do: - No Infura/Alchemy dependency (any JSON-RPC endpoint works) - No backend servers operated by the developer -The user's RPC endpoint and the CoinDesk price API are the only external -services. Users who want maximum privacy can point the RPC at their own node -(price fetching can be disabled in a future version). +These three services (RPC endpoint, CoinDesk price API, and Blockscout API) are +the only external services. All three endpoints are user-configurable. Users who +want maximum privacy can point the RPC and Blockscout URLs at their own +self-hosted instances (price fetching can be disabled in a future version). ### Dependencies diff --git a/RULES.md b/RULES.md index 6c4e4a8..9644a38 100644 --- a/RULES.md +++ b/RULES.md @@ -1,3 +1,8 @@ +> **⚠️ THIS FILE MUST NEVER BE MODIFIED BY AGENTS.** RULES.md is maintained +> exclusively by the project owner. AI agents, bots, and automated tools must +> treat this file as read-only. If an audit finds a divergence between the code +> and this file, the code must be changed to match — never the other way around. + # AutistMask Rules Checklist This file is derived from README.md and REPO_POLICIES.md for use as an audit @@ -17,8 +22,8 @@ contradicts either, the originals govern. ## External Communication -- [ ] Extension contacts exactly two external services: configured RPC endpoint - and CoinDesk price API +- [ ] Extension contacts exactly three external services: configured RPC + endpoint, CoinDesk price API, and Blockscout block-explorer API - [ ] No analytics, telemetry, or tracking - [ ] No user-specific data sent except to the configured RPC endpoint - [ ] No Infura/Alchemy hard dependency diff --git a/src/popup/views/helpers.js b/src/popup/views/helpers.js index de33fee..5d9daa8 100644 --- a/src/popup/views/helpers.js +++ b/src/popup/views/helpers.js @@ -85,7 +85,7 @@ function showFlash(msg, duration = 2000) { function balanceLine(symbol, amount, price, tokenId) { const qty = amount.toFixed(4); - const usd = price ? formatUsd(amount * price) : ""; + const usd = price ? formatUsd(amount * price) || " " : " "; const tokenAttr = tokenId ? ` data-token="${tokenId}"` : ""; const clickClass = tokenId ? " cursor-pointer hover:bg-hover balance-row" @@ -222,6 +222,41 @@ function formatAddressHtml(address, ensName, maxLen, title) { return `
${dot}${escapeHtml(displayAddr)}
`; } +function isoDate(timestamp) { + const d = new Date(timestamp * 1000); + const pad = (n) => String(n).padStart(2, "0"); + return ( + d.getFullYear() + + "-" + + pad(d.getMonth() + 1) + + "-" + + pad(d.getDate()) + + " " + + pad(d.getHours()) + + ":" + + pad(d.getMinutes()) + + ":" + + pad(d.getSeconds()) + ); +} + +function timeAgo(timestamp) { + const seconds = Math.floor(Date.now() / 1000 - timestamp); + if (seconds < 60) return seconds + " seconds ago"; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) + return minutes + " minute" + (minutes !== 1 ? "s" : "") + " ago"; + const hours = Math.floor(minutes / 60); + if (hours < 24) return hours + " hour" + (hours !== 1 ? "s" : "") + " ago"; + const days = Math.floor(hours / 24); + if (days < 30) return days + " day" + (days !== 1 ? "s" : "") + " ago"; + const months = Math.floor(days / 30); + if (months < 12) + return months + " month" + (months !== 1 ? "s" : "") + " ago"; + const years = Math.floor(days / 365); + return years + " year" + (years !== 1 ? "s" : "") + " ago"; +} + module.exports = { $, showError, @@ -236,4 +271,6 @@ module.exports = { addressTitle, formatAddressHtml, truncateMiddle, + isoDate, + timeAgo, }; diff --git a/src/popup/views/home.js b/src/popup/views/home.js index 7fd2262..62b352b 100644 --- a/src/popup/views/home.js +++ b/src/popup/views/home.js @@ -3,6 +3,8 @@ const { showView, showFlash, balanceLinesForAddress, + isoDate, + timeAgo, addressDotHtml, escapeHtml, truncateMiddle, @@ -87,41 +89,6 @@ function renderActiveAddress() { } } -function timeAgo(timestamp) { - const seconds = Math.floor(Date.now() / 1000 - timestamp); - if (seconds < 60) return seconds + " seconds ago"; - const minutes = Math.floor(seconds / 60); - if (minutes < 60) - return minutes + " minute" + (minutes !== 1 ? "s" : "") + " ago"; - const hours = Math.floor(minutes / 60); - if (hours < 24) return hours + " hour" + (hours !== 1 ? "s" : "") + " ago"; - const days = Math.floor(hours / 24); - if (days < 30) return days + " day" + (days !== 1 ? "s" : "") + " ago"; - const months = Math.floor(days / 30); - if (months < 12) - return months + " month" + (months !== 1 ? "s" : "") + " ago"; - const years = Math.floor(days / 365); - return years + " year" + (years !== 1 ? "s" : "") + " ago"; -} - -function isoDate(timestamp) { - const d = new Date(timestamp * 1000); - const pad = (n) => String(n).padStart(2, "0"); - return ( - d.getFullYear() + - "-" + - pad(d.getMonth() + 1) + - "-" + - pad(d.getDate()) + - " " + - pad(d.getHours()) + - ":" + - pad(d.getMinutes()) + - ":" + - pad(d.getSeconds()) - ); -} - let homeTxs = []; function renderHomeTxList(ctx) { diff --git a/src/popup/views/transactionDetail.js b/src/popup/views/transactionDetail.js index 0ec1dc3..8ecbfe3 100644 --- a/src/popup/views/transactionDetail.js +++ b/src/popup/views/transactionDetail.js @@ -8,6 +8,8 @@ const { addressDotHtml, addressTitle, escapeHtml, + isoDate, + timeAgo, } = require("./helpers"); const { state } = require("../../shared/state"); const makeBlockie = require("ethereum-blockies-base64"); @@ -21,41 +23,6 @@ const EXT_ICON = let ctx; -function isoDate(timestamp) { - const d = new Date(timestamp * 1000); - const pad = (n) => String(n).padStart(2, "0"); - return ( - d.getFullYear() + - "-" + - pad(d.getMonth() + 1) + - "-" + - pad(d.getDate()) + - " " + - pad(d.getHours()) + - ":" + - pad(d.getMinutes()) + - ":" + - pad(d.getSeconds()) - ); -} - -function timeAgo(timestamp) { - const seconds = Math.floor(Date.now() / 1000 - timestamp); - if (seconds < 60) return seconds + " seconds ago"; - const minutes = Math.floor(seconds / 60); - if (minutes < 60) - return minutes + " minute" + (minutes !== 1 ? "s" : "") + " ago"; - const hours = Math.floor(minutes / 60); - if (hours < 24) return hours + " hour" + (hours !== 1 ? "s" : "") + " ago"; - const days = Math.floor(hours / 24); - if (days < 30) return days + " day" + (days !== 1 ? "s" : "") + " ago"; - const months = Math.floor(days / 30); - if (months < 12) - return months + " month" + (months !== 1 ? "s" : "") + " ago"; - const years = Math.floor(days / 365); - return years + " year" + (years !== 1 ? "s" : "") + " ago"; -} - function copyableHtml(text, extraClass) { const cls = "underline decoration-dashed cursor-pointer" +