diff --git a/BoldChrome/.env.example b/.env.example similarity index 100% rename from BoldChrome/.env.example rename to .env.example diff --git a/BoldChrome/.gitignore b/.gitignore similarity index 57% rename from BoldChrome/.gitignore rename to .gitignore index ebc4a77..d2a9008 100644 --- a/BoldChrome/.gitignore +++ b/.gitignore @@ -28,3 +28,21 @@ Thumbs.db # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* + +# Logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Test / coverage +coverage + +# TypeScript +*.tsbuildinfo + +# Editor (optional – remove if you commit shared settings) +.idea +*.swp +*.swo diff --git a/BoldChrome/.npmrc b/.npmrc similarity index 100% rename from BoldChrome/.npmrc rename to .npmrc diff --git a/BoldChrome/ARCHITECTURE.md b/BoldChrome/ARCHITECTURE.md deleted file mode 100644 index ac64ea7..0000000 --- a/BoldChrome/ARCHITECTURE.md +++ /dev/null @@ -1,344 +0,0 @@ -# BoldChrome Architecture - -## Overview - -BoldChrome is a Chrome extension built with **SvelteKit** that analyzes Medium articles and provides AI-powered summaries. It uses Google's Generative AI API to understand and summarize article content. - -## Extension Type - -- **Manifest Version:** 3 (latest Chrome extension format) -- **Architecture:** Popup-based extension with no background service worker -- **Build System:** SvelteKit + Vite with Chrome Extension Adapter - -## Technology Stack - -``` -┌─────────────────────────────────────────┐ -│ Chrome Extension │ -│ (Manifest v3 popup-based) │ -├─────────────────────────────────────────┤ -│ SvelteKit Framework │ -│ (Component framework + routing) │ -├─────────────────────────────────────────┤ -│ Vite Build Tool │ -│ (Fast bundling and hot reload) │ -├─────────────────────────────────────────┤ -│ Google Generative AI API │ -│ (Claude/Gemini for summarization) │ -└─────────────────────────────────────────┘ -``` - -## Project Structure - -### `/src` - Source Code - -``` -src/ -├── app.d.ts # Type definitions for SvelteKit -├── app.html # Root HTML template -│ -├── routes/ -│ ├── +layout.svelte # Root layout component -│ ├── +page.svelte # Main page (landing) -│ │ -│ └── popup/ # Popup extension route -│ ├── +page.svelte # Popup UI component -│ └── page.ts # Popup data loader (Chrome API calls) -│ -└── lib/ - ├── stores/ - │ └── index.ts # Svelte stores for state management - │ ├── onMedium # Boolean: is current tab on Medium? - │ └── summary # String: article summary or status - │ - └── assets/ - └── [icons, favicon] -``` - -### `/static` - Static Files - -``` -static/ -├── manifest.json # Chrome extension manifest -└── robots.txt -``` - -### Configuration Files - -- **svelte.config.js** - SvelteKit configuration with Chrome extension adapter -- **vite.config.ts** - Vite build configuration optimized for extensions -- **tsconfig.json** - TypeScript configuration -- **package.json** - Dependencies and build scripts -- **.env.local** - Local development environment variables (NOT in git) - -## Data Flow - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 1. User Opens Extension Popup │ -└─────────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────────┐ -│ 2. +page.ts Loader Executes (popup/page.ts) │ -│ - Uses chrome.tabs.query() to detect current tab │ -│ - Checks if URL contains "medium.com" │ -│ - Sets onMedium store accordingly │ -└─────────────────────────────────────────────────────────────┘ - ↓ - ┌─────────┴─────────┐ - ↓ ↓ - Not on Medium On Medium Article - ↓ ↓ - ┌──────────────────┐ ┌──────────────────┐ - │ Show Error Msg │ │ Extract Content │ - │ "Please visit │ │ using Chrome API │ - │ Medium article" │ └──────────────────┘ - └──────────────────┘ ↓ - ↑ ┌──────────────────┐ - │ │ Call Google API │ - │ │ with article │ - │ │ text for summary │ - │ └──────────────────┘ - │ ↓ - │ ┌──────────────────┐ - │ │ Update summary │ - │ │ store with │ - │ │ generated text │ - │ └──────────────────┘ - │ ↓ - └──────────────────┤ - ↓ - ┌──────────────────────┐ - │ +page.svelte Updates │ - │ UI to show summary │ - │ with beautiful UI │ - └──────────────────────┘ -``` - -## Component Hierarchy - -``` -app.html (Root HTML) - └─ +layout.svelte (Root Layout) - ├─ +page.svelte (Main Page - not used in popup) - │ - └─ popup/+page.svelte (Extension Popup) - ├─ Header section (Title: "Bold Medium Summary") - ├─ Main content area - │ ├─ Error state (non-Medium sites) - │ ├─ Loading state (Processing article...) - │ ├─ Summary state (Display article summary) - │ └─ Placeholder state (Waiting for article) - └─ Styled with scoped CSS (gradient purple theme) -``` - -## State Management - -### Svelte Stores (`src/lib/stores/index.ts`) - -Two main stores manage the extension's state: - -1. **`onMedium`** - Writable boolean store - - `true` when current tab is a Medium article - - `false` when on other websites - - Used to show/hide content accordingly - -2. **`summary`** - Writable string store - - Empty string: initial/idle state - - "Processing article...": API call in progress - - Article text: successfully generated summary - -### Data Flow with Stores - -``` -popup/page.ts (Loader) - ├─ Detects Medium article - ├─ Updates onMedium store - ├─ Extracts article text - ├─ Calls Google API - └─ Updates summary store - ↓ -popup/+page.svelte (Component) - ├─ Subscribes to onMedium - ├─ Subscribes to summary - └─ Re-renders UI when stores change -``` - -## Chrome Extension Integration - -### Manifest v3 Configuration - -```json -{ - "manifest_version": 3, - "name": "Bold Bitcoin Wallet", - "permissions": ["scripting", "activeTab"], - "action": { - "default_popup": "popup.html" - } -} -``` - -### Chrome APIs Used - -1. **chrome.tabs.query()** - - Query current active tab - - Check tab URL - - Located in: `popup/page.ts` - -2. **chrome.scripting.executeScript()** - - Extract DOM content from current page - - Get article text from Medium page - - Located in: `popup/page.ts` - -### Permissions - -- `scripting` - Allows content script execution on pages -- `activeTab` - Access to current active tab information - -## Google Generative AI Integration - -### Flow - -``` -popup/page.ts - └─ import GoogleGenerativeAI - └─ Initialize with PUBLIC_GOOGLE_API_KEY - └─ genAI.getGenerativeModel({ model: "gemini-pro" }) - └─ model.generateContent(articleText) - └─ Returns summary text -``` - -### Environment Configuration - -API key is passed via environment variable: -- Source: `.env.local` file -- Variable: `PUBLIC_GOOGLE_API_KEY` -- Exposed as: `$env/static/public` in SvelteKit - -## Build Process - -### Development Build - -``` -npm run build - └─ Vite + SvelteKit processor - ├─ Compiles Svelte components - ├─ Transpiles TypeScript - ├─ Generates source maps - └─ Outputs to /build directory - ├─ Preserves manifest.json - ├─ Creates popup.html - └─ Bundles all scripts -``` - -### Production Build - -``` -npm run build:prod - └─ Optimized build - ├─ Minified JavaScript - ├─ No source maps - └─ Smaller file size for distribution -``` - -### Output Structure - -``` -build/ -├── manifest.json # Chrome manifest (copied from static/) -├── popup.html # Popup entry point -├── _app/ # SvelteKit app bundle -│ ├── chunks/ # Code chunks -│ ├── immutable/ # Static files -│ └── manifest.json # Route manifest -├── index.html # Main page (not used) -└── [other assets] -``` - -## Extension Initialization - -1. **First Run:** - - Extension loads `/popup.html` - - SvelteKit mounts to DOM - - `popup/page.ts` loader runs - - Chrome API checks current tab - -2. **Subsequent Clicks:** - - Popup re-opens - - Loader re-executes - - Detects current tab again - - Updates UI accordingly - -## Key Design Decisions - -### Popup-Only Architecture -- Simpler than background service worker -- All logic runs when popup is open -- Stateless between popup opens - -### SvelteKit + Vite -- **Why SvelteKit?** - - Great TypeScript support - - Reactive stores for state - - File-based routing - - Built-in styling - -- **Why Vite?** - - Fast build times - - Hot reload during dev - - Optimized production builds - - Chrome extension support via adapter - -### Google Generative AI -- **Why?** - - Powerful summarization - - Easy API integration - - Affordable pricing - - No server needed - -## Future Enhancement Opportunities - -1. **Background Service Worker** - - Cache summaries - - Pre-fetch articles - - Schedule periodic tasks - -2. **Content Script** - - In-page summary display - - Article highlighting - - Full page context - -3. **Storage** - - Save summaries locally - - History of read articles - - User preferences - -4. **Advanced Features** - - Different summary lengths - - Multiple language support - - Custom AI models - - Share summaries - -## File Sizes (Approximate) - -- `manifest.json` - 0.5 KB -- `popup.html` - 1-2 KB -- JavaScript bundle - 200-400 KB (uncompressed) -- Total extension - 300-500 KB - -## Performance Considerations - -- **Startup:** Extension loads quickly (DOM is small) -- **API calls:** Async, doesn't block UI -- **Memory:** Minimal (simple state management) -- **CPU:** Light usage (Chrome handles compilation) - ---- - -## References - -- [Chrome Extension Manifest v3](https://developer.chrome.com/docs/extensions/mv3/) -- [SvelteKit Documentation](https://svelte.dev/docs/kit) -- [Vite Documentation](https://vitejs.dev) -- [Google Generative AI API](https://ai.google.dev) diff --git a/BoldChrome/BUILD_INSTRUCTIONS.md b/BoldChrome/BUILD_INSTRUCTIONS.md deleted file mode 100644 index 7e0689c..0000000 --- a/BoldChrome/BUILD_INSTRUCTIONS.md +++ /dev/null @@ -1,51 +0,0 @@ -# BoldChrome – Build & load - -## Prerequisites - -- **Node.js** v18+ -- **npm** (comes with Node.js) -- **Chrome** (or Chromium) - -## 1. Install - -```bash -cd BoldChrome -npm install -``` - -## 2. Build - -```bash -npm run build -``` - -Output is in the **`build`** directory (or **`extension-build`** depending on your adapter). That folder is what you load in Chrome. - -## 3. Load in Chrome - -1. Open **chrome://extensions** -2. Turn on **Developer mode** (top right) -3. Click **Load unpacked** -4. Choose the **`build`** (or `extension-build`) folder inside BoldChrome -5. The **Bold Bitcoin Wallet** extension should appear in the toolbar - -## 4. After code changes - -1. Run `npm run build` again -2. In **chrome://extensions**, click the **reload** icon on BoldChrome - -(No hot reload; rebuild and reload each time.) - -## Other commands - -- **Type check:** `npm run check` or `npm run check:watch` -- **Verify setup:** `npm run verify` -- **Production build:** `npm run build:prod` - -## Troubleshooting - -- **Extension won’t load:** Ensure you selected the **build** (or **extension-build**) folder that contains `manifest.json`, not `src` or the repo root. -- **Popup blank:** Right‑click the popup → **Inspect** and check the console for errors. -- **Build errors:** Remove `node_modules` and `.svelte-kit`, run `npm install`, then `npm run build` again. - -For architecture and pairing flow, see [ARCHITECTURE.md](ARCHITECTURE.md) and [docs/PAIRING_VIA_QR.md](docs/PAIRING_VIA_QR.md). diff --git a/BoldChrome/README.md b/BoldChrome/README.md deleted file mode 100644 index 5bb83ab..0000000 --- a/BoldChrome/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# BoldChrome – Bold Bitcoin Wallet (Chrome Extension) - -A **watch-only** Bitcoin wallet Chrome extension that pairs with the Bold mobile app via **QR codes**. Built with SvelteKit and the Chrome Extensions API. - -## Features - -- Pair with Bold mobile app via QR (no backend required) -- View balance and addresses (watch-only) -- Send / Receive flows; QR-based signing with mobile -- Light/dark theme; compact popup UI - -## Quick start - -```bash -npm install -npm run build -``` - -Then in Chrome: **chrome://extensions** → Enable **Developer mode** → **Load unpacked** → select the **`build`** folder (or `extension-build` if your build outputs there). - -## Docs - -- **[BUILD_INSTRUCTIONS.md](BUILD_INSTRUCTIONS.md)** – Build, load, and debug -- **[ARCHITECTURE.md](ARCHITECTURE.md)** – Project structure and flow -- **[docs/PAIRING_VIA_QR.md](docs/PAIRING_VIA_QR.md)** – QR pairing flow -- **[docs/DESIGN_SYSTEM.md](docs/DESIGN_SYSTEM.md)** – UI/theme - -## Commands - -| Command | Description | -|--------------------|----------------------------| -| `npm run build` | Build extension | -| `npm run build:prod` | Production build | -| `npm run check` | TypeScript + Svelte check | -| `npm run verify` | Verify build/setup | - -## Tech - -- **SvelteKit** + **Vite** + **TypeScript** -- **Chrome Extension** (Manifest v3) -- **sveltekit-adapter-chrome-extension** for build output diff --git a/BoldChrome/docs/ADDRESS_DERIVATION.md b/BoldChrome/docs/ADDRESS_DERIVATION.md deleted file mode 100644 index 000d16a..0000000 --- a/BoldChrome/docs/ADDRESS_DERIVATION.md +++ /dev/null @@ -1,41 +0,0 @@ -# Address derivation alignment with Bold Wallet app - -After importing `pubKey` and `chainCode` from the Bold mobile app (via the bind flow), the extension derives receive addresses so they **match the app**. - -## Derivation path (aligned with app) - -The app (BBMTLib `GetOutputDescriptor` / `GetDerivedPubKey`) uses: - -- **Master** = `pubKey` (33 bytes hex) + `chainCode` (32 bytes hex). -- **Account level** = non-hardened derivation from master: - `m / bipPath / coinType / 0` - - **Legacy (BIP44):** `m/44/0/0` (mainnet) or `m/44/1/0` (testnet) - - **SegWit nested (BIP49):** `m/49/0/0` or `m/49/1/0` - - **SegWit native (BIP84):** `m/84/0/0` or `m/84/1/0` -- **Receive addresses** = from account: `account / 0 / index` - So first address = `m/84/0/0/0/0`, second = `m/84/0/0/0/1`, etc. - -The extension (`src/lib/services/hdwallet.ts`) does the same: - -1. Build BIP32 root from `publicKey` + `chainCode` (hex). -2. Derive to account: `root.derive(bipPath).derive(coinType).derive(0)` with `bipPath` 44/49/84 and `coinType` 0 (mainnet) or 1 (testnet). -3. Derive receive: `accountNode.derive(0).derive(index)` for index 0, 1, 2, … - -So the extension’s first receive address (and all indices) match the app for the same `pubKey` and `chainCode`. - -## How to validate - -1. **Pair** the extension with the Bold Wallet app using the bind flow (pairing_code QR → mobile response QR). -2. In the **app**, open Device tab (or Wallet) and note the **first receive address** (e.g. the one shown as “Receive” or in the address list). -3. In the **extension**, after pairing, the first address it derives (and uses for “Receive”) should be **identical**. -4. Optionally compare a few more indices (e.g. second, third address) in both; they should match. - -If any address differs for the same keyshare, derivation is out of sync and should be fixed (paths or network/coinType). - -## Address types - -- **Legacy:** P2PKH, `1...` (mainnet) / `m...` or `n...` (testnet) -- **SegWit nested:** P2SH-P2WPKH, `3...` (mainnet) / `2...` (testnet) -- **SegWit native:** P2WPKH, `bc1q...` (mainnet) / `tb1q...` (testnet) - -The extension derives all three types with the same path rules as the app; the default “Receive” address is the first SegWit native one. diff --git a/BoldChrome/docs/DESIGN_SYSTEM.md b/BoldChrome/docs/DESIGN_SYSTEM.md deleted file mode 100644 index 56f966c..0000000 --- a/BoldChrome/docs/DESIGN_SYSTEM.md +++ /dev/null @@ -1,140 +0,0 @@ -# BoldChrome UI/UX Design System & Alignment Plan - -This document is the **single source of truth** for UI/UX in the extension. **The extension themes, styles, and fonts follow the BoldWallet app** so the experience is consistent across web and extension. - ---- - -## 1. Design tokens (canonical) - -All UI must use **CSS variables** from the theme. No hardcoded hex colors, font names, or magic-number spacing in components. - -### 1.1 Colors (BoldWallet-aligned) - -Defined in `src/lib/styles/theme.ts`. Values match BoldWallet app (gold primary `#ffd600`, green secondary `#4caf50`, navy wallet card `#14213d`, background `#f5f7fa`). Use only: - -| Token (CSS var) | Purpose | -|-----------------|--------| -| `--color-primary` | Primary CTAs (BoldWallet gold) | -| `--color-subPrimary` | Hover primary (lighter gold) | -| `--color-secondary` | Secondary actions (green) | -| `--color-accent` | Highlights, focus (gold) | -| `--color-background` | Page/popup background | -| `--color-cardBackground` | Cards, modals | -| `--color-inputBackground` | Input fields background | -| `--color-text` | Primary text | -| `--color-textSecondary` | Muted text | -| `--color-textOnPrimary` | Text on primary buttons (dark on gold) | -| `--color-border` | Borders, dividers | -| `--color-disabled` | Disabled state | -| `--color-success` | Success (green) | -| `--color-error` | Errors | -| `--color-warning` | Warnings | -| `--color-walletCard` | BoldWallet navy card (#14213d) | -| `--color-textOnDark` | Text on navy/dark | -| `--color-accentOnDark` | Gold on navy | -| `--color-labelOnDark` | Labels on navy (#bfc8e6) | -| `--color-inputOnDark` | Input bg on navy (#232946) | -| `--color-borderOnDark` | Border on navy (#3a3a4d) | - -### 1.2 Typography - -| Token | Value | Use | -|-------|--------|-----| -| `--font-sans` | System UI stack | Body, headings, labels | -| `--font-mono` | Monospace stack | Addresses, tx ids, code | - -**Font stack (sans):** -`var(--font-sans)` → `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif` - -**Font stack (mono):** -`var(--font-mono)` → `'SF Mono', Monaco, 'Fira Code', 'Courier New', monospace` - -**Scale (use rem or var where we add --text-*):** -- Title: 1.5rem–1.75rem, font-weight 700 -- Section: 1rem–1.125rem, font-weight 600 -- Body: 0.875rem–1rem, font-weight 400–500 -- Caption: 0.75rem, font-weight 500 -- Mono: 0.8125rem–0.875rem - -### 1.3 Real estate (spacing & layout) - -| Token | Value | Use | -|-------|--------|-----| -| `--space-1` | 4px | Tight gaps | -| `--space-2` | 8px | Inline spacing | -| `--space-3` | 12px | Small padding | -| `--space-4` | 16px | Default padding | -| `--space-5` | 20px | Section padding | -| `--space-6` | 24px | Large padding | -| `--radius-sm` | 6px | Inputs (BoldWallet) | -| `--radius-md` | 8px | Buttons | -| `--radius-lg` | 12px | Cards | -| `--radius-xl` | 18px | Hero/wallet card (BoldWallet) | - -Popup constraints: **width 380px**, **min-height 580px** (set in `app.html`). - -### 1.4 Themes (BoldWallet-aligned) - -- **lightPolished** – default: #f5f7fa background, gold (#ffd600) primary, green secondary, white cards. Matches BoldWallet wallet page. -- **darkPolished** – navy (#14213d) background, gold accent, same semantic tokens. - -Theme is applied in `+layout.svelte` via `applyTheme()`. All tokens come from the active theme. - ---- - -## 2. Alignment plan (extension → design system) - -### Phase 1 – Single source of truth (done / in progress) - -- [x] Document tokens in this file. -- [x] Add `inputBackground` (and any missing color) to `theme.ts`. -- [x] Add font and spacing tokens to `theme.ts` and set CSS vars in `applyTheme()`. - -### Phase 2 – Remove hardcoded values - -Replace every hardcoded color/font/spacing with a token: - -| File | What to do | -|------|------------| -| `src/routes/popup.html/+page.svelte` | Replace all `#hex`, `font-family`, `font-size` with `var(--color-*)`, `var(--font-*)`, `var(--space-*)` / `var(--text-*)` or rem scale. | -| `src/routes/+page.svelte` | Same; remove Bitcoin-orange/gray hardcodes, use theme vars. | -| `src/routes/scanner/+page.svelte` | Use theme vars for background, text, borders, buttons. | -| `src/routes/permission/+page.svelte` | Use theme vars; gradient can use `--color-primary` / `--color-subPrimary`. | -| `src/popup/SendBitcoin.svelte` | Use theme vars only. | -| `src/lib/components/SendTransaction.svelte` | Use theme vars only. | -| `src/lib/components/QRScannerPopup.svelte` | Use theme vars only. | -| `src/lib/components/QRScanner.svelte` | Use theme vars only. | -| `src/app.html` | Use `var(--font-sans)` for body; keep size constraints. | - -### Phase 3 – Consistency checks - -- [ ] No `#hex` or `rgb()` in `.svelte` or `app.html` except inside `theme.ts`. -- [ ] No raw font names; use `var(--font-sans)` or `var(--font-mono)`. -- [ ] Spacing uses `var(--space-*)` or a shared rem scale. -- [ ] Border radius uses `var(--radius-*)`. - -### Phase 4 – Align with BoldWallet app ✅ - -- **Done:** Extension theme matches BoldWallet app. Primary is gold (#ffd600), background #f5f7fa, green secondary, navy wallet card tokens available. See `theme.ts`. - ---- - -## 3. Rules for new UI - -1. **Colors** – Use only `var(--color-*)` from the theme. -2. **Fonts** – Use `var(--font-sans)` or `var(--font-mono)`. -3. **Spacing** – Prefer `var(--space-*)`; avoid arbitrary pixel values. -4. **Radii** – Use `var(--radius-sm|md|lg|xl)`. -5. **New tokens** – Add to `theme.ts` and this doc; then use the var everywhere. - ---- - -## 4. File reference - -| File | Role | -|------|------| -| `src/lib/styles/theme.ts` | Defines themes and sets all CSS variables. | -| `src/routes/+layout.svelte` | Calls `applyTheme()` on mount. | -| `docs/DESIGN_SYSTEM.md` | This document – UI/UX code of conduct. | - -Once alignment is complete, this doc plus `theme.ts` are the only places that define colors, fonts, and spacing; the rest of the extension only consumes them. diff --git a/BoldChrome/docs/PAIRING_VIA_QR.md b/BoldChrome/docs/PAIRING_VIA_QR.md deleted file mode 100644 index 0b99a95..0000000 --- a/BoldChrome/docs/PAIRING_VIA_QR.md +++ /dev/null @@ -1,70 +0,0 @@ -# Pairing via QR (Mobile -> Extension) - -This document describes the minimal QR payload format and examples for passing a public key (and optional chain code / device info) from a mobile wallet to the Bold Chrome extension via a QR code. - -## Bold bind flow (swimlanes.io, recommended) - -1. **Extension** creates a pairing code and shows a QR with `pairing_code=XXXX` (e.g. 6 digits). -2. **Mobile** (Bold Wallet app) scans that QR, user confirms, mobile builds response (cipher + checksum), shows response QR. -3. **Extension** scans the mobile response QR (base64, 67 bytes). It deciphers with the stored pairing code, extracts `pub_key` and `chain_code`, validates the checksum, and stores the wallet. - -The response format is binary (not JSON): 65-byte cipher (payload XOR sha256(pairing_code)) + 2-byte checksum, base64-encoded. The extension parses this in `extensionBind.ts`; the mobile generates it in `utils/extensionBind.ts` (BoldWallet repo). - -## Accepted QR payload formats -The extension accepts several formats. The recommended canonical format is JSON with `type: 'pairing_response'` and a `data` object containing pairing fields. - -Example (JSON): - -{ - "type": "pairing_response", - "data": { - "publicKey": "02a1633caf...", - "chainCode": "abcd1234...", // optional - "deviceId": "my-mobile-wallet", - "network": "mainnet", - "address": "bc1q...", // optional - "addresses": [] // optional - }, - "timestamp": 1630000000000, - "id": "pair-res-..." -} - -The extension also supports a simpler JSON shape (no `type`) that contains `publicKey` at the top level, and raw hex public keys (66 or 130 hex chars) as the QR payload. - -## Mobile App: generating the QR -- Web / React Native (JS) example (uses `qrcode` package or `react-native-qrcode-svg`): - -// Using `qrcode` (web/React) -import QRCode from 'qrcode'; - -const payload = { - type: 'pairing_response', - data: { - publicKey: '02a1633caf...', - chainCode: 'abcd1234...', - deviceId: 'my-mobile-wallet', - network: 'mainnet' - }, - timestamp: Date.now(), - id: `pair-res-${Date.now()}` -}; - -const dataUrl = await QRCode.toDataURL(JSON.stringify(payload), { width: 400, margin: 2 }); -// Render `dataUrl` as an or using a native QR component - -// Using `react-native-qrcode-svg` (React Native) -import QRCode from 'react-native-qrcode-svg'; - - - -## Security considerations -- Make sure the mobile app displays the QR only when the device/user intends to pair with the extension. -- The extension validates presence of `publicKey` and optionally `chainCode` — ensure your mobile wallet includes these fields intentionally. -- If possible, include a `deviceId` and display it in the extension pairing UI so the user can confirm which device they are pairing with. - -## Testing -- The extension includes `qr.generatePairingResponseQR(data)` helper that creates a data URL you can use for manual testing (or for generating a QR in a test harness). - -## Troubleshooting -- If pairing fails, check the console in the extension popup for `[QR]` logs — they show how the payload was parsed. -- Use the manual input mode in the popup to paste the JSON payload to test the pairing flow without camera scanning. diff --git a/BoldChrome/src/routes/popup.html/+page.svelte b/BoldChrome/src/routes/popup.html/+page.svelte deleted file mode 100644 index aa99994..0000000 --- a/BoldChrome/src/routes/popup.html/+page.svelte +++ /dev/null @@ -1,4134 +0,0 @@ - - - - \ No newline at end of file diff --git a/BoldChrome/static/permission-grant.html b/BoldChrome/static/permission-grant.html deleted file mode 100644 index 3c8da4b..0000000 --- a/BoldChrome/static/permission-grant.html +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - Camera Permission - - - -
-
-

Camera Permission Setup

-
-

Requesting camera permission...

-

Please click "Allow" when your browser asks for camera access.

-
-
- - - - diff --git a/README.md b/README.md index 6726950..76f7887 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ # Bold Extension - Overview + +### [On Chrome Store](https://chromewebstore.google.com/detail/bold-wallet/dpgigdojkmhknnoedgbkfdeilmlbdecf?hl=en&authuser=0) +![](https://lh3.googleusercontent.com/9mrv2g2VabIfR7efvsr2IPLGTHojthBE6b9At8amT6KC_49H1ceSdN5KBF4IyrClfjbnPMvgqmj5cD-3A6YBoAy2rw=s1600-w1600-h1000) + ### Bold Extension is a Watch-only wallet: - No private keys or keyshares imported. - Track your wallet balance and transactions diff --git a/BoldChrome/fix-build.js b/fix-build.js similarity index 100% rename from BoldChrome/fix-build.js rename to fix-build.js diff --git a/BoldChrome/package-lock.json b/package-lock.json similarity index 99% rename from BoldChrome/package-lock.json rename to package-lock.json index 943c205..b8fa261 100644 --- a/BoldChrome/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "boldchrome", - "version": "0.0.1", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "boldchrome", - "version": "0.0.1", + "version": "1.0.1", "dependencies": { "@noble/hashes": "^2.0.1", "@noble/secp256k1": "^3.0.0", diff --git a/BoldChrome/package.json b/package.json similarity index 98% rename from BoldChrome/package.json rename to package.json index b75aaec..6cbe94d 100644 --- a/BoldChrome/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "boldchrome", "private": true, - "version": "0.0.1", + "version": "1.0.3", "type": "module", "scripts": { "dev": "vite dev", diff --git a/BoldChrome/pnpm-lock.yaml b/pnpm-lock.yaml similarity index 100% rename from BoldChrome/pnpm-lock.yaml rename to pnpm-lock.yaml diff --git a/BoldChrome/pnpm-workspace.yaml b/pnpm-workspace.yaml similarity index 100% rename from BoldChrome/pnpm-workspace.yaml rename to pnpm-workspace.yaml diff --git a/BoldChrome/src/app.d.ts b/src/app.d.ts similarity index 100% rename from BoldChrome/src/app.d.ts rename to src/app.d.ts diff --git a/BoldChrome/src/app.html b/src/app.html similarity index 100% rename from BoldChrome/src/app.html rename to src/app.html diff --git a/BoldChrome/src/hooks.client.ts b/src/hooks.client.ts similarity index 100% rename from BoldChrome/src/hooks.client.ts rename to src/hooks.client.ts diff --git a/BoldChrome/src/jsqr.d.ts b/src/jsqr.d.ts similarity index 100% rename from BoldChrome/src/jsqr.d.ts rename to src/jsqr.d.ts diff --git a/BoldChrome/src/lib/assets/Icon-App-40x40@2x.png b/src/lib/assets/Icon-App-40x40@2x.png similarity index 100% rename from BoldChrome/src/lib/assets/Icon-App-40x40@2x.png rename to src/lib/assets/Icon-App-40x40@2x.png diff --git a/BoldChrome/src/lib/assets/about-icon.png b/src/lib/assets/about-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/about-icon.png rename to src/lib/assets/about-icon.png diff --git a/BoldChrome/src/lib/assets/address-type-icon.png b/src/lib/assets/address-type-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/address-type-icon.png rename to src/lib/assets/address-type-icon.png diff --git a/BoldChrome/src/lib/assets/advanced-icon.png b/src/lib/assets/advanced-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/advanced-icon.png rename to src/lib/assets/advanced-icon.png diff --git a/BoldChrome/src/lib/assets/api-icon.png b/src/lib/assets/api-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/api-icon.png rename to src/lib/assets/api-icon.png diff --git a/BoldChrome/src/lib/assets/backup-icon.png b/src/lib/assets/backup-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/backup-icon.png rename to src/lib/assets/backup-icon.png diff --git a/BoldChrome/src/lib/assets/bind-icon.png b/src/lib/assets/bind-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/bind-icon.png rename to src/lib/assets/bind-icon.png diff --git a/BoldChrome/src/lib/assets/bitcoin-icon.png b/src/lib/assets/bitcoin-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/bitcoin-icon.png rename to src/lib/assets/bitcoin-icon.png diff --git a/BoldChrome/src/lib/assets/bitcoin-logo.png b/src/lib/assets/bitcoin-logo.png similarity index 100% rename from BoldChrome/src/lib/assets/bitcoin-logo.png rename to src/lib/assets/bitcoin-logo.png diff --git a/BoldChrome/src/lib/assets/bold-bitcoin-icon.png b/src/lib/assets/bold-bitcoin-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/bold-bitcoin-icon.png rename to src/lib/assets/bold-bitcoin-icon.png diff --git a/BoldChrome/src/lib/assets/bold-icon-inverted.png b/src/lib/assets/bold-icon-inverted.png similarity index 100% rename from BoldChrome/src/lib/assets/bold-icon-inverted.png rename to src/lib/assets/bold-icon-inverted.png diff --git a/BoldChrome/src/lib/assets/bold-icon.png b/src/lib/assets/bold-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/bold-icon.png rename to src/lib/assets/bold-icon.png diff --git a/BoldChrome/src/lib/assets/bricks-icon.png b/src/lib/assets/bricks-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/bricks-icon.png rename to src/lib/assets/bricks-icon.png diff --git a/BoldChrome/src/lib/assets/bulb-icon.png b/src/lib/assets/bulb-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/bulb-icon.png rename to src/lib/assets/bulb-icon.png diff --git a/BoldChrome/src/lib/assets/capability-icon.png b/src/lib/assets/capability-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/capability-icon.png rename to src/lib/assets/capability-icon.png diff --git a/BoldChrome/src/lib/assets/check-icon.png b/src/lib/assets/check-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/check-icon.png rename to src/lib/assets/check-icon.png diff --git a/BoldChrome/src/lib/assets/clock-icon.png b/src/lib/assets/clock-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/clock-icon.png rename to src/lib/assets/clock-icon.png diff --git a/BoldChrome/src/lib/assets/consolidate-icon.png b/src/lib/assets/consolidate-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/consolidate-icon.png rename to src/lib/assets/consolidate-icon.png diff --git a/BoldChrome/src/lib/assets/copy-icon.png b/src/lib/assets/copy-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/copy-icon.png rename to src/lib/assets/copy-icon.png diff --git a/BoldChrome/src/lib/assets/cosign-icon.png b/src/lib/assets/cosign-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/cosign-icon.png rename to src/lib/assets/cosign-icon.png diff --git a/BoldChrome/src/lib/assets/currency-icon.png b/src/lib/assets/currency-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/currency-icon.png rename to src/lib/assets/currency-icon.png diff --git a/BoldChrome/src/lib/assets/dark-icon.png b/src/lib/assets/dark-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/dark-icon.png rename to src/lib/assets/dark-icon.png diff --git a/BoldChrome/src/lib/assets/delete-icon.png b/src/lib/assets/delete-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/delete-icon.png rename to src/lib/assets/delete-icon.png diff --git a/BoldChrome/src/lib/assets/descriptor-icon.png b/src/lib/assets/descriptor-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/descriptor-icon.png rename to src/lib/assets/descriptor-icon.png diff --git a/BoldChrome/src/lib/assets/dna-icon.png b/src/lib/assets/dna-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/dna-icon.png rename to src/lib/assets/dna-icon.png diff --git a/BoldChrome/src/lib/assets/extension-icon.png b/src/lib/assets/extension-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/extension-icon.png rename to src/lib/assets/extension-icon.png diff --git a/BoldChrome/src/lib/assets/eye-off-icon.png b/src/lib/assets/eye-off-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/eye-off-icon.png rename to src/lib/assets/eye-off-icon.png diff --git a/BoldChrome/src/lib/assets/eye-on-icon.png b/src/lib/assets/eye-on-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/eye-on-icon.png rename to src/lib/assets/eye-on-icon.png diff --git a/BoldChrome/src/lib/assets/favicon.svg b/src/lib/assets/favicon.svg similarity index 100% rename from BoldChrome/src/lib/assets/favicon.svg rename to src/lib/assets/favicon.svg diff --git a/BoldChrome/src/lib/assets/fingerprint.png b/src/lib/assets/fingerprint.png similarity index 100% rename from BoldChrome/src/lib/assets/fingerprint.png rename to src/lib/assets/fingerprint.png diff --git a/BoldChrome/src/lib/assets/font-icon.png b/src/lib/assets/font-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/font-icon.png rename to src/lib/assets/font-icon.png diff --git a/BoldChrome/src/lib/assets/icon copy.png b/src/lib/assets/icon copy.png similarity index 100% rename from BoldChrome/src/lib/assets/icon copy.png rename to src/lib/assets/icon copy.png diff --git a/BoldChrome/src/lib/assets/icon-inverted.png b/src/lib/assets/icon-inverted.png similarity index 100% rename from BoldChrome/src/lib/assets/icon-inverted.png rename to src/lib/assets/icon-inverted.png diff --git a/BoldChrome/src/lib/assets/icon.png b/src/lib/assets/icon.png similarity index 100% rename from BoldChrome/src/lib/assets/icon.png rename to src/lib/assets/icon.png diff --git a/BoldChrome/src/lib/assets/in-icon.png b/src/lib/assets/in-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/in-icon.png rename to src/lib/assets/in-icon.png diff --git a/BoldChrome/src/lib/assets/info-icon.png b/src/lib/assets/info-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/info-icon.png rename to src/lib/assets/info-icon.png diff --git a/BoldChrome/src/lib/assets/join-icon.png b/src/lib/assets/join-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/join-icon.png rename to src/lib/assets/join-icon.png diff --git a/BoldChrome/src/lib/assets/key-icon.png b/src/lib/assets/key-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/key-icon.png rename to src/lib/assets/key-icon.png diff --git a/BoldChrome/src/lib/assets/legal-icon.png b/src/lib/assets/legal-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/legal-icon.png rename to src/lib/assets/legal-icon.png diff --git a/BoldChrome/src/lib/assets/light-icon.png b/src/lib/assets/light-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/light-icon.png rename to src/lib/assets/light-icon.png diff --git a/BoldChrome/src/lib/assets/link-icon.png b/src/lib/assets/link-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/link-icon.png rename to src/lib/assets/link-icon.png diff --git a/BoldChrome/src/lib/assets/locker-icon.png b/src/lib/assets/locker-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/locker-icon.png rename to src/lib/assets/locker-icon.png diff --git a/BoldChrome/src/lib/assets/logo copy.png b/src/lib/assets/logo copy.png similarity index 100% rename from BoldChrome/src/lib/assets/logo copy.png rename to src/lib/assets/logo copy.png diff --git a/BoldChrome/src/lib/assets/logo.png b/src/lib/assets/logo.png similarity index 100% rename from BoldChrome/src/lib/assets/logo.png rename to src/lib/assets/logo.png diff --git a/BoldChrome/src/lib/assets/mainnet-icon.png b/src/lib/assets/mainnet-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/mainnet-icon.png rename to src/lib/assets/mainnet-icon.png diff --git a/BoldChrome/src/lib/assets/mempool-icon.png b/src/lib/assets/mempool-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/mempool-icon.png rename to src/lib/assets/mempool-icon.png diff --git a/BoldChrome/src/lib/assets/mode-icon.png b/src/lib/assets/mode-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/mode-icon.png rename to src/lib/assets/mode-icon.png diff --git a/BoldChrome/src/lib/assets/network-icon.png b/src/lib/assets/network-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/network-icon.png rename to src/lib/assets/network-icon.png diff --git a/BoldChrome/src/lib/assets/new-icon.png b/src/lib/assets/new-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/new-icon.png rename to src/lib/assets/new-icon.png diff --git a/BoldChrome/src/lib/assets/nostr-icon.png b/src/lib/assets/nostr-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/nostr-icon.png rename to src/lib/assets/nostr-icon.png diff --git a/BoldChrome/src/lib/assets/numbers-icon.png b/src/lib/assets/numbers-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/numbers-icon.png rename to src/lib/assets/numbers-icon.png diff --git a/BoldChrome/src/lib/assets/out-icon.png b/src/lib/assets/out-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/out-icon.png rename to src/lib/assets/out-icon.png diff --git a/BoldChrome/src/lib/assets/pair-icon.png b/src/lib/assets/pair-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/pair-icon.png rename to src/lib/assets/pair-icon.png diff --git a/BoldChrome/src/lib/assets/pairing-icon.png b/src/lib/assets/pairing-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/pairing-icon.png rename to src/lib/assets/pairing-icon.png diff --git a/BoldChrome/src/lib/assets/paste-icon.png b/src/lib/assets/paste-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/paste-icon.png rename to src/lib/assets/paste-icon.png diff --git a/BoldChrome/src/lib/assets/pending-icon.png b/src/lib/assets/pending-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/pending-icon.png rename to src/lib/assets/pending-icon.png diff --git a/BoldChrome/src/lib/assets/phone-icon.png b/src/lib/assets/phone-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/phone-icon.png rename to src/lib/assets/phone-icon.png diff --git a/BoldChrome/src/lib/assets/playstore-icon.png b/src/lib/assets/playstore-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/playstore-icon.png rename to src/lib/assets/playstore-icon.png diff --git a/BoldChrome/src/lib/assets/prefs-icon.png b/src/lib/assets/prefs-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/prefs-icon.png rename to src/lib/assets/prefs-icon.png diff --git a/BoldChrome/src/lib/assets/prepare-icon.png b/src/lib/assets/prepare-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/prepare-icon.png rename to src/lib/assets/prepare-icon.png diff --git a/BoldChrome/src/lib/assets/privacy-icon.png b/src/lib/assets/privacy-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/privacy-icon.png rename to src/lib/assets/privacy-icon.png diff --git a/BoldChrome/src/lib/assets/qr-icon.png b/src/lib/assets/qr-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/qr-icon.png rename to src/lib/assets/qr-icon.png diff --git a/BoldChrome/src/lib/assets/qrc-icon.png b/src/lib/assets/qrc-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/qrc-icon.png rename to src/lib/assets/qrc-icon.png diff --git a/BoldChrome/src/lib/assets/receive-icon.png b/src/lib/assets/receive-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/receive-icon.png rename to src/lib/assets/receive-icon.png diff --git a/BoldChrome/src/lib/assets/recycle-icon.png b/src/lib/assets/recycle-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/recycle-icon.png rename to src/lib/assets/recycle-icon.png diff --git a/BoldChrome/src/lib/assets/refresh-icon.png b/src/lib/assets/refresh-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/refresh-icon.png rename to src/lib/assets/refresh-icon.png diff --git a/BoldChrome/src/lib/assets/restore-icon.png b/src/lib/assets/restore-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/restore-icon.png rename to src/lib/assets/restore-icon.png diff --git a/BoldChrome/src/lib/assets/scan-icon.png b/src/lib/assets/scan-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/scan-icon.png rename to src/lib/assets/scan-icon.png diff --git a/BoldChrome/src/lib/assets/search-icon.png b/src/lib/assets/search-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/search-icon.png rename to src/lib/assets/search-icon.png diff --git a/BoldChrome/src/lib/assets/security-icon.png b/src/lib/assets/security-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/security-icon.png rename to src/lib/assets/security-icon.png diff --git a/BoldChrome/src/lib/assets/send-icon.png b/src/lib/assets/send-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/send-icon.png rename to src/lib/assets/send-icon.png diff --git a/BoldChrome/src/lib/assets/settings-icon.png b/src/lib/assets/settings-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/settings-icon.png rename to src/lib/assets/settings-icon.png diff --git a/BoldChrome/src/lib/assets/share-icon.png b/src/lib/assets/share-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/share-icon.png rename to src/lib/assets/share-icon.png diff --git a/BoldChrome/src/lib/assets/spy-icon.png b/src/lib/assets/spy-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/spy-icon.png rename to src/lib/assets/spy-icon.png diff --git a/BoldChrome/src/lib/assets/start-icon.png b/src/lib/assets/start-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/start-icon.png rename to src/lib/assets/start-icon.png diff --git a/BoldChrome/src/lib/assets/storage-icon.png b/src/lib/assets/storage-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/storage-icon.png rename to src/lib/assets/storage-icon.png diff --git a/BoldChrome/src/lib/assets/success-icon.png b/src/lib/assets/success-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/success-icon.png rename to src/lib/assets/success-icon.png diff --git a/BoldChrome/src/lib/assets/testnet-icon.png b/src/lib/assets/testnet-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/testnet-icon.png rename to src/lib/assets/testnet-icon.png diff --git a/BoldChrome/src/lib/assets/theme-icon.png b/src/lib/assets/theme-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/theme-icon.png rename to src/lib/assets/theme-icon.png diff --git a/BoldChrome/src/lib/assets/upload-icon.png b/src/lib/assets/upload-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/upload-icon.png rename to src/lib/assets/upload-icon.png diff --git a/BoldChrome/src/lib/assets/utxo-icon.png b/src/lib/assets/utxo-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/utxo-icon.png rename to src/lib/assets/utxo-icon.png diff --git a/BoldChrome/src/lib/assets/vpn-icon.png b/src/lib/assets/vpn-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/vpn-icon.png rename to src/lib/assets/vpn-icon.png diff --git a/BoldChrome/src/lib/assets/wallet-icon.png b/src/lib/assets/wallet-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/wallet-icon.png rename to src/lib/assets/wallet-icon.png diff --git a/BoldChrome/src/lib/assets/warning-icon.png b/src/lib/assets/warning-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/warning-icon.png rename to src/lib/assets/warning-icon.png diff --git a/BoldChrome/src/lib/assets/wifi-icon.png b/src/lib/assets/wifi-icon.png similarity index 100% rename from BoldChrome/src/lib/assets/wifi-icon.png rename to src/lib/assets/wifi-icon.png diff --git a/BoldChrome/src/lib/components/QRScanner.svelte b/src/lib/components/QRScanner.svelte similarity index 100% rename from BoldChrome/src/lib/components/QRScanner.svelte rename to src/lib/components/QRScanner.svelte diff --git a/BoldChrome/src/lib/components/QRScannerPopup.svelte b/src/lib/components/QRScannerPopup.svelte similarity index 100% rename from BoldChrome/src/lib/components/QRScannerPopup.svelte rename to src/lib/components/QRScannerPopup.svelte diff --git a/BoldChrome/src/lib/components/SendTransaction.svelte b/src/lib/components/SendTransaction.svelte similarity index 100% rename from BoldChrome/src/lib/components/SendTransaction.svelte rename to src/lib/components/SendTransaction.svelte diff --git a/BoldChrome/src/lib/index.ts b/src/lib/index.ts similarity index 100% rename from BoldChrome/src/lib/index.ts rename to src/lib/index.ts diff --git a/BoldChrome/src/lib/initialization.ts b/src/lib/initialization.ts similarity index 100% rename from BoldChrome/src/lib/initialization.ts rename to src/lib/initialization.ts diff --git a/BoldChrome/src/lib/services/blockchain.ts b/src/lib/services/blockchain.ts similarity index 88% rename from BoldChrome/src/lib/services/blockchain.ts rename to src/lib/services/blockchain.ts index 6083e7d..62370d5 100644 --- a/BoldChrome/src/lib/services/blockchain.ts +++ b/src/lib/services/blockchain.ts @@ -74,8 +74,26 @@ export interface FeeEstimate { [blocks: string]: number; // blocks as key, fee rate as value } +/** Mempool.space /v1/fees/recommended response (sat/vB). */ +export interface RecommendedFees { + fastestFee: number; + halfHourFee: number; + hourFee: number; + economyFee?: number; + minimumFee?: number; +} + const DEFAULT_MAINNET_API = 'https://mempool.space/api'; const DEFAULT_TESTNET_API = 'https://mempool.space/testnet/api'; +const FETCH_TIMEOUT_MS = 5000; + +function fetchWithTimeout(url: string, init?: RequestInit): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + return fetch(url, { ...init, signal: controller.signal }).finally(() => + clearTimeout(timeoutId) + ); +} class BlockchainService { private baseUrl = DEFAULT_MAINNET_API; @@ -136,7 +154,7 @@ class BlockchainService { const url = `${this.getBaseUrl()}/address/${address}`; try { - const response = await fetch(url); + const response = await fetchWithTimeout(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } @@ -188,7 +206,7 @@ class BlockchainService { const url = `${this.getBaseUrl()}/address/${address}/utxo`; try { - const response = await fetch(url); + const response = await fetchWithTimeout(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } @@ -217,7 +235,7 @@ class BlockchainService { } try { - const response = await fetch(url); + const response = await fetchWithTimeout(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } @@ -245,7 +263,7 @@ class BlockchainService { const url = `${this.getBaseUrl()}/tx/${txid}`; try { - const response = await fetch(url); + const response = await fetchWithTimeout(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } @@ -270,7 +288,7 @@ class BlockchainService { const url = `${this.getBaseUrl()}/tx/${txid}/hex`; try { - const response = await fetch(url); + const response = await fetchWithTimeout(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } @@ -284,20 +302,21 @@ class BlockchainService { } /** - * Get fee estimates + * Get recommended fee estimates (economy, 1hr, 30m, fast) in sat/vB. */ - async getFeeEstimates(): Promise { + async getFeeEstimates(): Promise { console.log('[Blockchain] Fetching fee estimates'); const url = `${this.getBaseUrl()}/v1/fees/recommended`; try { - const response = await fetch(url); + const response = await fetchWithTimeout(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); - return data; + console.log('[Blockchain] Fee estimates:', data); + return data as RecommendedFees; } catch (error) { console.error('[Blockchain] Error fetching fee estimates:', error); throw error; @@ -312,7 +331,7 @@ class BlockchainService { const url = `${this.getBaseUrl()}/tx`; try { - const response = await fetch(url, { + const response = await fetchWithTimeout(url, { method: 'POST', headers: { 'Content-Type': 'text/plain' @@ -343,7 +362,7 @@ class BlockchainService { const url = `${this.getBaseUrl()}/blocks/tip/height`; try { - const response = await fetch(url); + const response = await fetchWithTimeout(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } @@ -371,7 +390,7 @@ class BlockchainService { const url = `${this.getBaseUrl()}/v1/prices`; try { - const response = await fetch(url); + const response = await fetchWithTimeout(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } @@ -385,7 +404,7 @@ class BlockchainService { } catch (error) { console.error('[Blockchain] Error fetching Bitcoin prices:', error); try { - const fallback = await fetch('https://api.coinbase.com/v2/prices/BTC-USD/spot'); + const fallback = await fetchWithTimeout('https://api.coinbase.com/v2/prices/BTC-USD/spot'); const fallbackData = await fallback.json(); const usd = parseFloat(fallbackData?.data?.amount ?? 0); return usd ? { USD: usd } : {}; diff --git a/BoldChrome/src/lib/services/extensionBind.ts b/src/lib/services/extensionBind.ts similarity index 100% rename from BoldChrome/src/lib/services/extensionBind.ts rename to src/lib/services/extensionBind.ts diff --git a/BoldChrome/src/lib/services/hdwallet.ts b/src/lib/services/hdwallet.ts similarity index 82% rename from BoldChrome/src/lib/services/hdwallet.ts rename to src/lib/services/hdwallet.ts index 1179c6c..0bf6f37 100644 --- a/BoldChrome/src/lib/services/hdwallet.ts +++ b/src/lib/services/hdwallet.ts @@ -244,8 +244,20 @@ export interface DerivedAddress { path: string; index: number; type: 'legacy' | 'segwit-nested' | 'segwit-native'; + chain?: 'receive' | 'change'; } +export interface HdState { + externalIndex: number; + changeIndex: number; + maxUsedExternal: number; + discoveryDone: boolean; + discoveryLastAt: number; + addressType: 'segwit-native' | 'segwit-nested' | 'legacy'; +} + +export const GAP_LIMIT = 5; + export interface HDWalletConfig { publicKey: string; // Extended public key (base58) chainCode: string; // Chain code in hex @@ -264,13 +276,15 @@ class HDWalletService { config: HDWalletConfig, addressType: 'legacy' | 'segwit-nested' | 'segwit-native', count: number = 20, - startIndex: number = 0 + startIndex: number = 0, + chain: 0 | 1 = 0 ): DerivedAddress[] { const network = config.network === 'mainnet' ? bitcoin.networks.bitcoin : bitcoin.networks.testnet; const coinType = config.network === 'testnet' ? 1 : 0; const basePath = this.getBasePath(addressType); + const chainLabel: 'receive' | 'change' = chain === 0 ? 'receive' : 'change'; const addresses: DerivedAddress[] = []; @@ -283,13 +297,14 @@ class HDWalletService { const accountNode = root.derive(bipPath).derive(coinType).derive(0); for (let i = startIndex; i < startIndex + count; i++) { - const child = accountNode.derive(0).derive(i); + const child = accountNode.derive(chain).derive(i); const address = this.getAddress(child, addressType, network); addresses.push({ address, - path: `${basePath}/0/${i}`, + path: `${basePath}/${chain}/${i}`, index: i, - type: addressType + type: addressType, + chain: chainLabel }); } } catch (error) { @@ -396,6 +411,71 @@ class HDWalletService { segwitNative: this.deriveAddresses(config, 'segwit-native', countPerType) }; } + + /** + * Derive all HD addresses for a given address type up to the known indexes. + * Returns both receive (0..externalEnd) and change (0..changeEnd) addresses. + */ + deriveHdAddresses( + config: HDWalletConfig, + addressType: 'legacy' | 'segwit-nested' | 'segwit-native', + externalEnd: number, + changeEnd: number + ): DerivedAddress[] { + const receive = this.deriveAddresses(config, addressType, externalEnd + 1, 0, 0); + const change = changeEnd >= 0 + ? this.deriveAddresses(config, addressType, changeEnd + 1, 0, 1) + : []; + return [...receive, ...change]; + } + + /** + * Gap-limit discovery: scan the blockchain to find all used addresses. + * Returns discovered indexes. Matches mobile's discoverHdIndexesForNetwork. + */ + async discoverIndexes( + config: HDWalletConfig, + addressType: 'legacy' | 'segwit-nested' | 'segwit-native', + getAddressStats: (address: string) => Promise<{ tx_count: number }>, + onProgress?: (chain: 'receive' | 'change', index: number) => void + ): Promise<{ maxUsedExternal: number; externalNext: number; changeNext: number }> { + let maxUsedExternal = -1; + let maxUsedChange = -1; + + // External (receive) chain + let consecutiveUnused = 0; + for (let i = 0; consecutiveUnused < GAP_LIMIT; i++) { + const [addr] = this.deriveAddresses(config, addressType, 1, i, 0); + onProgress?.('receive', i); + const stats = await getAddressStats(addr.address); + if (stats.tx_count > 0) { + maxUsedExternal = i; + consecutiveUnused = 0; + } else { + consecutiveUnused++; + } + } + + // Internal (change) chain + consecutiveUnused = 0; + for (let i = 0; consecutiveUnused < GAP_LIMIT; i++) { + const [addr] = this.deriveAddresses(config, addressType, 1, i, 1); + onProgress?.('change', i); + const stats = await getAddressStats(addr.address); + if (stats.tx_count > 0) { + maxUsedChange = i; + consecutiveUnused = 0; + } else { + consecutiveUnused++; + } + } + + return { + maxUsedExternal, + externalNext: Math.max(0, maxUsedExternal + 1), + changeNext: Math.max(0, maxUsedChange + 1), + }; + } } export const hdWallet = new HDWalletService(); diff --git a/BoldChrome/src/lib/services/pairing.ts b/src/lib/services/pairing.ts similarity index 100% rename from BoldChrome/src/lib/services/pairing.ts rename to src/lib/services/pairing.ts diff --git a/BoldChrome/src/lib/services/pin.ts b/src/lib/services/pin.ts similarity index 100% rename from BoldChrome/src/lib/services/pin.ts rename to src/lib/services/pin.ts diff --git a/BoldChrome/src/lib/services/psbt.ts b/src/lib/services/psbt.ts similarity index 90% rename from BoldChrome/src/lib/services/psbt.ts rename to src/lib/services/psbt.ts index 30cdb4d..d843fc8 100644 --- a/BoldChrome/src/lib/services/psbt.ts +++ b/src/lib/services/psbt.ts @@ -8,7 +8,7 @@ import { Point, getPublicKey, sign as ecdsaSign, verify as ecdsaVerify, schnorr, import { sha256 } from '@noble/hashes/sha2.js'; import { hmac } from '@noble/hashes/hmac.js'; import { writable, get } from 'svelte/store'; -import { walletStore } from '../stores/wallet'; +import { walletStore, getNextChangeAddress, type TaggedUTXO } from '../stores/wallet'; import { blockchain } from './blockchain'; import { qr } from './qr'; @@ -232,9 +232,10 @@ class PsbtService { public session = { subscribe: this.currentSession.subscribe }; /** - * Create a PSBT for spending Bitcoin + * Create a PSBT for spending Bitcoin using tagged UTXOs from all HD addresses. + * Change goes to a fresh HD change address (not the sending address). */ - async createPsbt(params: CreatePsbtParams): Promise<{ psbtBase64: string; feeSats: number; psbtId: string }> { + async createPsbt(params: CreatePsbtParams): Promise<{ psbtBase64: string; feeSats: number; psbtId: string; utxosJson: string; changeAddress: string }> { const { recipientAddress, amountSats, feeRate = 5 } = params; console.log('[PSBT] Creating PSBT:', { @@ -244,35 +245,27 @@ class PsbtService { }); const wallet = get(walletStore); - if (!wallet.address) { - throw new Error('No active wallet address'); + if (!wallet.utxos || wallet.utxos.length === 0) { + throw new Error('No UTXOs available for spending'); } const network = wallet.network === 'testnet' ? bitcoin.networks.testnet : bitcoin.networks.bitcoin; - // Fetch UTXOs for the current address - const utxos = await blockchain.getUTXOs(wallet.address); - if (!utxos || utxos.length === 0) { - throw new Error('No UTXOs available for spending'); - } + console.log('[PSBT] Found', wallet.utxos.length, 'tagged UTXOs across all addresses'); - console.log('[PSBT] Found', utxos.length, 'UTXOs'); - - // Create PSBT const psbt = new bitcoin.Psbt({ network }); - // Select UTXOs to cover amount + estimated fee - const estimatedSize = 250; // Rough estimate for 1-in, 2-out transaction + const estimatedSize = 250; const estimatedFee = Math.ceil(estimatedSize * feeRate); const targetAmount = amountSats + estimatedFee; let totalInput = 0; - const selectedUtxos: UTXO[] = []; + const selectedUtxos: TaggedUTXO[] = []; - // Simple UTXO selection - use largest first - const sortedUtxos = [...utxos].sort((a, b) => b.value - a.value); + // Largest-first coin selection from the aggregated tagged UTXO set + const sortedUtxos = [...wallet.utxos].sort((a, b) => b.value - a.value); for (const utxo of sortedUtxos) { if (totalInput >= targetAmount) break; @@ -280,10 +273,8 @@ class PsbtService { selectedUtxos.push(utxo); totalInput += utxo.value; - // Fetch the full transaction hex for this UTXO const txHex = await blockchain.getTransactionHex(utxo.txid); - // Add input to PSBT (use browser-friendly Uint8Array from hex) const txBytes = (function hexToU8Local(hex: string) { const clean = (hex || '').replace(/^0x/, '').replace(/\s+/g, ''); const len = Math.ceil(clean.length / 2); @@ -303,37 +294,47 @@ class PsbtService { throw new Error(`Insufficient funds. Have ${totalInput} sats, need ${targetAmount} sats`); } - // Calculate actual fee based on selected inputs const actualSize = this.estimateTransactionSize(selectedUtxos.length, 2); const actualFee = Math.ceil(actualSize * feeRate); const changeAmount = totalInput - amountSats - actualFee; - // Add recipient output + // Recipient output psbt.addOutput({ address: recipientAddress, value: BigInt(amountSats), }); - // Add change output if significant (> dust threshold) - if (changeAmount > 546) { + // Change goes to a fresh HD change address + const changeAddr = getNextChangeAddress(); + if (changeAmount > 546 && changeAddr) { psbt.addOutput({ - address: wallet.address, + address: changeAddr.address, value: BigInt(changeAmount), }); } + // Build utxosJson matching mobile's expected format: + // [{txid, vout, value, derivationPath, address}] + const utxosJson = JSON.stringify(selectedUtxos.map(u => ({ + txid: u.txid, + vout: u.vout, + value: u.value, + derivationPath: u.derivationPath, + address: u.address, + }))); + console.log('[PSBT] Transaction details:', { inputs: selectedUtxos.length, totalInput, recipient: amountSats, change: changeAmount, + changeAddress: changeAddr?.address?.slice(0, 12), fee: actualFee, feeRate }); const psbtBase64 = psbt.toBase64(); - // Create session const psbtId = `psbt-${Date.now()}-${Math.random().toString(36).substring(7)}`; this.currentSession.set({ psbtId, @@ -345,7 +346,7 @@ class PsbtService { feeSats: actualFee }); - return { psbtBase64, feeSats: actualFee, psbtId }; + return { psbtBase64, feeSats: actualFee, psbtId, utxosJson, changeAddress: changeAddr?.address || '' }; } /** diff --git a/BoldChrome/src/lib/services/qr.ts b/src/lib/services/qr.ts similarity index 97% rename from BoldChrome/src/lib/services/qr.ts rename to src/lib/services/qr.ts index b58c3ae..9393db9 100644 --- a/BoldChrome/src/lib/services/qr.ts +++ b/src/lib/services/qr.ts @@ -46,7 +46,8 @@ export interface QRSession { } /** - * Encode send-bitcoin QR payload: toAddress|amount|fee|spendingHash|addressType|derivationPath|network + * Encode send-bitcoin QR payload (v5 format — matches mobile's decodeSendBitcoinQR). + * toAddress|amount|fee|spendingHash|addressType|derivationPath|network|utxosJson|changeAddress */ export const encodeSendBitcoinQR = ( toAddress: string, @@ -55,11 +56,13 @@ export const encodeSendBitcoinQR = ( spendingHash: string = '', addressType: string = '', derivationPath: string = '', - network: string = '' + network: string = '', + utxosJson: string = '', + changeAddress: string = '' ): string => { const amount = typeof amountSats === 'string' ? amountSats : amountSats.toString(); const fee = typeof feeSats === 'string' ? feeSats : feeSats.toString(); - return `${toAddress}|${amount}|${fee}|${spendingHash || ''}|${addressType || ''}|${derivationPath || ''}|${network || ''}`; + return `${toAddress}|${amount}|${fee}|${spendingHash || ''}|${addressType || ''}|${derivationPath || ''}|${network || ''}|${utxosJson || ''}|${changeAddress || ''}`; }; class QRService { @@ -274,14 +277,15 @@ class QRService { spendingHash: string = '', addressType: string = '', derivationPath: string = '', - network: string = '' + network: string = '', + utxosJson: string = '', + changeAddress: string = '' ): Promise<{ dataUrl: string; payload: string }> { const id = `send-${Date.now()}-${Math.random().toString(36).substring(7)}`; - // Build base payload without spendingHash, then set spendingHash = sha256(qrData + Date.now()) - const basePayload = encodeSendBitcoinQR(toAddress, amountSats, feeSats, '', addressType, derivationPath, network); + const basePayload = encodeSendBitcoinQR(toAddress, amountSats, feeSats, '', addressType, derivationPath, network, utxosJson, changeAddress); const computedHash = spendingHash || CryptoJS.SHA256(basePayload + Date.now()).toString(); - const payload = encodeSendBitcoinQR(toAddress, amountSats, feeSats, computedHash, addressType, derivationPath, network); + const payload = encodeSendBitcoinQR(toAddress, amountSats, feeSats, computedHash, addressType, derivationPath, network, utxosJson, changeAddress); try { const dataUrl = await this.generateQRCode(payload); @@ -666,7 +670,7 @@ class QRService { console.log('[QR] Attempting relay lookup:', url); const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 4000); + const timeout = setTimeout(() => controller.abort(), 5000); try { const resp = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json' }, signal: controller.signal }); diff --git a/BoldChrome/src/lib/services/socket.ts b/src/lib/services/socket.ts similarity index 100% rename from BoldChrome/src/lib/services/socket.ts rename to src/lib/services/socket.ts diff --git a/BoldChrome/src/lib/services/storage.ts b/src/lib/services/storage.ts similarity index 94% rename from BoldChrome/src/lib/services/storage.ts rename to src/lib/services/storage.ts index 44e053e..bf3bfaa 100644 --- a/BoldChrome/src/lib/services/storage.ts +++ b/src/lib/services/storage.ts @@ -28,8 +28,14 @@ export interface StorageData { /** Mempool API: undefined = not chosen (show preference after pairing), '' = default mempool.space, else custom mainnet URL */ mempoolMainnetUrl?: string | null; + // HD wallet state (gap-limit discovery results) + hdState?: string; // JSON stringified HdState + // PIN lock (extension) pinHash?: string; // SHA-256 hash of PIN, never store raw PIN + + // First-load camera permission prompt (extension) + cameraPermissionChecked?: boolean; } /** diff --git a/BoldChrome/src/lib/stores/device.ts b/src/lib/stores/device.ts similarity index 100% rename from BoldChrome/src/lib/stores/device.ts rename to src/lib/stores/device.ts diff --git a/BoldChrome/src/lib/stores/index.ts b/src/lib/stores/index.ts similarity index 100% rename from BoldChrome/src/lib/stores/index.ts rename to src/lib/stores/index.ts diff --git a/BoldChrome/src/lib/stores/wallet.ts b/src/lib/stores/wallet.ts similarity index 53% rename from BoldChrome/src/lib/stores/wallet.ts rename to src/lib/stores/wallet.ts index 8140972..c48775e 100644 --- a/BoldChrome/src/lib/stores/wallet.ts +++ b/src/lib/stores/wallet.ts @@ -1,7 +1,9 @@ import { writable, derived } from 'svelte/store'; import { storage } from '../services/storage'; import { blockchain } from '../services/blockchain'; -import { hdWallet, type DerivedAddress as HDDerivedAddress } from '../services/hdwallet'; +import { hdWallet, type DerivedAddress as HDDerivedAddress, type HdState, GAP_LIMIT } from '../services/hdwallet'; + +const HD_DISCOVERY_STALE_MS = 2 * 60 * 60 * 1000; // 2 hours export interface Transaction { txid: string; @@ -17,42 +19,54 @@ export interface Transaction { export interface DerivedAddress { address: string; - path: string; // BIP44 derivation path (e.g., m/84'/0'/0'/0/0) - index: number; // Address index - type: 'legacy' | 'segwit-nested' | 'segwit-native'; // Address type - label?: string; // Optional user-defined label - balance?: string; // Cached balance in BTC - lastUsed?: number; // Timestamp of last transaction + path: string; + index: number; + type: 'legacy' | 'segwit-nested' | 'segwit-native'; + chain?: 'receive' | 'change'; + label?: string; + balance?: string; + lastUsed?: number; +} + +export interface TaggedUTXO { + txid: string; + vout: number; + value: number; + address: string; + derivationPath: string; + status: { confirmed: boolean; block_height?: number }; } export interface WalletState { - // Identity - address: string; // Currently selected address - addresses: DerivedAddress[]; // All derived addresses from public key + address: string; + addresses: DerivedAddress[]; network: 'mainnet' | 'testnet'; - // HD Wallet Public Key (for address derivation) - publicKey?: string; // Extended public key (xpub/ypub/zpub) - chainCode?: string; // Chain code for HD derivation + publicKey?: string; + chainCode?: string; + + // HD state + hdState?: HdState; - // Balance + // Aggregate balance (across all HD addresses) btc: string; usd: string; lastBalanceUpdate: number; - // Transactions + // Merged transactions from all addresses transactions: Transaction[]; lastTxUpdate: number; hasMoreTransactions: boolean; - // UI state + // Tagged UTXOs (each knows its source address + derivation path) + utxos: TaggedUTXO[]; + isLoading: boolean; isLoadingMoreTransactions: boolean; error?: string; - // Watch-only configuration isWatchOnly: boolean; - pairedDevices?: string[]; // Mobile device IDs that can sign + pairedDevices?: string[]; } const initialState: WalletState = { @@ -65,9 +79,10 @@ const initialState: WalletState = { transactions: [], lastTxUpdate: 0, hasMoreTransactions: true, + utxos: [], isLoading: false, isLoadingMoreTransactions: false, - isWatchOnly: true, // Chrome extension is always watch-only + isWatchOnly: true, }; /** @@ -92,6 +107,7 @@ export async function resetWallet() { 'chainCode', 'address', 'addresses', + 'hdState', 'network', 'pairedDevices', 'pinHash' @@ -102,9 +118,11 @@ export async function resetWallet() { addresses: [], publicKey: undefined, chainCode: undefined, + hdState: undefined, btc: '0', usd: '0', transactions: [], + utxos: [], isLoading: false, error: undefined }); @@ -120,6 +138,8 @@ export async function initializeWalletStore() { const addressesJson = await storage.get('addresses'); const addresses: DerivedAddress[] = addressesJson ? JSON.parse(addressesJson) : []; const network = await storage.get('network') as 'mainnet' | 'testnet'; + const hdStateJson = await storage.get('hdState'); + const hdState: HdState | undefined = hdStateJson ? JSON.parse(hdStateJson) : undefined; walletStore.update(state => ({ ...state, @@ -127,6 +147,7 @@ export async function initializeWalletStore() { publicKey: publicKey ?? undefined, chainCode: chainCode ?? undefined, addresses, + hdState, network: network || 'mainnet' })); } @@ -347,6 +368,7 @@ export async function updateWalletFromPairing(data: { /** * Derive initial addresses from public key. * Exactly 3 addresses, each on first path (../0/0): native segwit, nested segwit, legacy. + * Used as a quick bootstrap before full HD discovery completes. */ export async function deriveInitialAddresses() { const publicKey = await storage.get('publicKey'); @@ -361,59 +383,192 @@ export async function deriveInitialAddresses() { try { console.log('[Wallet] Deriving 3 addresses (first derivation: native segwit, nested segwit, legacy)...'); - // One address per type at index 0 const derived = hdWallet.deriveAllTypes( { publicKey, chainCode, network }, - 1 // 1 address per type (index 0 only) + 1 ); - // Order: 1) native segwit (default), 2) nested segwit, 3) legacy const addresses: DerivedAddress[] = [ - ...derived.segwitNative.map(addr => ({ ...addr, type: 'segwit-native' as const })), - ...derived.segwitNested.map(addr => ({ ...addr, type: 'segwit-nested' as const })), - ...derived.legacy.map(addr => ({ ...addr, type: 'legacy' as const })) + ...derived.segwitNative.map(addr => ({ ...addr, type: 'segwit-native' as const, chain: 'receive' as const })), + ...derived.segwitNested.map(addr => ({ ...addr, type: 'segwit-nested' as const, chain: 'receive' as const })), + ...derived.legacy.map(addr => ({ ...addr, type: 'legacy' as const, chain: 'receive' as const })) ]; console.log('[Wallet] Derived', addresses.length, 'addresses'); - await updateAddresses(addresses); - // Default to native segwit (first in list) if (derived.segwitNative.length > 0) { await setAddress(derived.segwitNative[0].address); } + + // Kick off full HD discovery in the background + runHdDiscovery().catch(err => + console.error('[Wallet] Background HD discovery error:', err) + ); } catch (error) { console.error('[Wallet] Address derivation error:', error); throw new Error(`Failed to derive addresses: ${error instanceof Error ? error.message : 'Unknown error'}`); } } -/** Map raw mempool.space tx to our Transaction format (shared by refresh and fetchMore). */ +/** + * Run gap-limit HD discovery for the active address type. + * Scans the blockchain for used addresses, updates hdState & address list. + */ +/** + * @returns `true` if discovery actually ran (and refreshed data), `false` if skipped. + */ +export async function runHdDiscovery(force = false, overrideAddressType?: 'segwit-native' | 'segwit-nested' | 'legacy'): Promise { + const publicKey = await storage.get('publicKey'); + const chainCode = await storage.get('chainCode'); + const network = (await storage.get('network') as 'mainnet' | 'testnet') || 'mainnet'; + if (!publicKey || !chainCode) return false; + + const hdStateJson = await storage.get('hdState'); + const existing: HdState | null = hdStateJson ? JSON.parse(hdStateJson) : null; + + const addressType = overrideAddressType || existing?.addressType || 'segwit-native'; + + if (!overrideAddressType && !force && existing?.discoveryDone) { + const age = Date.now() - (existing.discoveryLastAt || 0); + if (age < HD_DISCOVERY_STALE_MS) { + console.log('[Wallet] HD discovery still fresh, skipping'); + return false; + } + } + const config = { publicKey, chainCode, network }; + + console.log('[Wallet] Running HD discovery for', addressType); + + const getStats = async (address: string) => { + const stats = await blockchain.getAddressStats(address); + return { tx_count: stats.chain_stats.tx_count + stats.mempool_stats.tx_count }; + }; + + const result = await hdWallet.discoverIndexes(config, addressType, getStats); + + const newHdState: HdState = { + externalIndex: result.externalNext, + changeIndex: result.changeNext, + maxUsedExternal: result.maxUsedExternal, + discoveryDone: true, + discoveryLastAt: Date.now(), + addressType, + }; + + await storage.set('hdState', JSON.stringify(newHdState)); + + // Derive the full address set from discovered indexes + const externalEnd = Math.max(result.externalNext, result.maxUsedExternal); + const changeEnd = result.changeNext; + const allAddrs = hdWallet.deriveHdAddresses(config, addressType, externalEnd, changeEnd > 0 ? changeEnd - 1 : -1); + + const addresses: DerivedAddress[] = allAddrs.map(a => ({ + ...a, + type: addressType, + })); + + await updateAddresses(addresses); + + // Set active address to current receive address + if (result.externalNext >= 0) { + const [receiveAddr] = hdWallet.deriveAddresses(config, addressType, 1, result.externalNext, 0); + if (receiveAddr) { + await storage.set('address', receiveAddr.address); + walletStore.update(s => ({ ...s, address: receiveAddr.address })); + } + } + + walletStore.update(s => ({ ...s, hdState: newHdState })); + console.log('[Wallet] HD discovery complete:', newHdState); + + // Re-aggregate balance/txs/UTXOs now that the full address set is known + await refreshWalletData(); + return true; +} + +/** + * Switch the active address type, re-run HD discovery, and refresh wallet data. + */ +export async function switchAddressType(newType: 'segwit-native' | 'segwit-nested' | 'legacy'): Promise { + const state = getStoreValue(); + if (state.hdState?.addressType === newType) return; + await runHdDiscovery(true, newType); +} + +/** + * Get the current receive address (at externalIndex). + */ +export function getCurrentReceiveAddress(): DerivedAddress | null { + const state = getStoreValue(); + if (!state.publicKey || !state.chainCode || !state.hdState) return null; + const config = { publicKey: state.publicKey, chainCode: state.chainCode, network: state.network }; + const [addr] = hdWallet.deriveAddresses(config, state.hdState.addressType, 1, state.hdState.externalIndex, 0); + return addr ? { ...addr, type: state.hdState.addressType, chain: 'receive' } : null; +} + +/** + * Get the next change address (at changeIndex). + */ +export function getNextChangeAddress(): DerivedAddress | null { + const state = getStoreValue(); + if (!state.publicKey || !state.chainCode || !state.hdState) return null; + const config = { publicKey: state.publicKey, chainCode: state.chainCode, network: state.network }; + const [addr] = hdWallet.deriveAddresses(config, state.hdState.addressType, 1, state.hdState.changeIndex, 1); + return addr ? { ...addr, type: state.hdState.addressType, chain: 'change' } : null; +} + +function getStoreValue(): WalletState { + let val: WalletState = initialState; + walletStore.subscribe(s => { val = s; })(); + return val; +} + +/** Map raw mempool.space tx to our Transaction format (single-address, for fetchMore). */ function mapRawTxToTransaction(tx: any, currentAddress: string): Transaction { + return mapRawTxMultiAddress(tx, new Set([currentAddress])); +} + +/** Map raw mempool.space tx considering multiple wallet addresses. */ +function mapRawTxMultiAddress(tx: any, walletAddresses: Set): Transaction { const receivedByUs = tx.vout - .filter((v: any) => v.scriptpubkey_address === currentAddress) + .filter((v: any) => walletAddresses.has(v.scriptpubkey_address)) .reduce((sum: number, v: any) => sum + v.value, 0); const sentFromUs = tx.vin - .filter((v: any) => v.prevout?.scriptpubkey_address === currentAddress) + .filter((v: any) => walletAddresses.has(v.prevout?.scriptpubkey_address)) .reduce((sum: number, v: any) => sum + (v.prevout?.value || 0), 0); const sentToOthers = tx.vout - .filter((v: any) => v.scriptpubkey_address && v.scriptpubkey_address !== currentAddress) + .filter((v: any) => v.scriptpubkey_address && !walletAddresses.has(v.scriptpubkey_address)) .reduce((sum: number, v: any) => sum + v.value, 0); const netAmount = receivedByUs - sentFromUs; - const type: Transaction['type'] = netAmount > 0 ? 'receive' : 'send'; + + // If all inputs and outputs belong to us, it's a consolidation + const allInputsOurs = tx.vin.every((v: any) => walletAddresses.has(v.prevout?.scriptpubkey_address)); + const allOutputsOurs = tx.vout.every((v: any) => walletAddresses.has(v.scriptpubkey_address)); + const isConsolidation = allInputsOurs && allOutputsOurs; + + const type: Transaction['type'] = isConsolidation + ? 'consolidation' + : netAmount > 0 + ? 'receive' + : 'send'; const amount = - type === 'receive' + type === 'consolidation' ? receivedByUs - : sentToOthers > 0 - ? sentToOthers - : Math.abs(netAmount); + : type === 'receive' + ? receivedByUs + : sentToOthers > 0 + ? sentToOthers + : Math.abs(netAmount); - const recipientVout = tx.vout?.find((v: any) => v.scriptpubkey_address && v.scriptpubkey_address !== currentAddress); - const senderVin = tx.vin?.find((v: any) => v.prevout?.scriptpubkey_address && v.prevout.scriptpubkey_address !== currentAddress); + const firstOurAddress = tx.vout?.find((v: any) => walletAddresses.has(v.scriptpubkey_address))?.scriptpubkey_address + || Array.from(walletAddresses)[0]; + const recipientVout = tx.vout?.find((v: any) => v.scriptpubkey_address && !walletAddresses.has(v.scriptpubkey_address)); + const senderVin = tx.vin?.find((v: any) => v.prevout?.scriptpubkey_address && !walletAddresses.has(v.prevout.scriptpubkey_address)); return { txid: tx.txid, @@ -422,45 +577,90 @@ function mapRawTxToTransaction(tx: any, currentAddress: string): Transaction { fee: tx.fee || 0, status: tx.status.confirmed ? 'confirmed' : 'pending', type, - address: currentAddress, - from: type === 'receive' ? senderVin?.prevout?.scriptpubkey_address : currentAddress, - to: type === 'send' ? recipientVout?.scriptpubkey_address : currentAddress, + address: firstOurAddress, + from: type === 'receive' ? senderVin?.prevout?.scriptpubkey_address : firstOurAddress, + to: type === 'send' ? recipientVout?.scriptpubkey_address : firstOurAddress, }; } /** - * Refresh wallet data from blockchain API + * Refresh wallet data from blockchain API. + * Aggregates balance, transactions, and UTXOs across all HD addresses. */ export async function refreshWalletData() { - let currentAddress = ''; + let addresses: DerivedAddress[] = []; walletStore.update(state => { - currentAddress = state.address; + addresses = state.addresses; return { ...state, isLoading: true, error: undefined, hasMoreTransactions: true }; }); - if (!currentAddress) { + if (!addresses.length) { walletStore.update(state => ({ ...state, isLoading: false, - error: 'No wallet address configured' + error: 'No wallet addresses configured' })); return; } + const allAddressStrings = new Set(addresses.map(a => a.address)); + try { - const [addressStats, txHistory] = await Promise.all([ - blockchain.getAddressStats(currentAddress), - blockchain.getTransactions(currentAddress) - ]); + // Aggregate balance across all addresses + let totalConfirmed = 0; + let totalUnconfirmed = 0; + for (const addr of addresses) { + try { + const stats = await blockchain.getAddressStats(addr.address); + totalConfirmed += stats.chain_stats.funded_txo_sum - stats.chain_stats.spent_txo_sum; + totalUnconfirmed += stats.mempool_stats.funded_txo_sum - stats.mempool_stats.spent_txo_sum; + } catch { + // Skip addresses that fail (rate limit, etc.) + } + } - const balanceSats = addressStats.chain_stats.funded_txo_sum - addressStats.chain_stats.spent_txo_sum; + const balanceSats = totalConfirmed + totalUnconfirmed; const balanceBTC = (balanceSats / 100_000_000).toFixed(8); - const price = await blockchain.getBitcoinPrice(); const balanceUSD = (parseFloat(balanceBTC) * price).toFixed(2); - const transactions: Transaction[] = txHistory.map((tx: any) => mapRawTxToTransaction(tx, currentAddress)); + // Aggregate transactions, dedup by txid + const txMap = new Map(); + for (const addr of addresses) { + try { + const txHistory = await blockchain.getTransactions(addr.address); + for (const rawTx of txHistory) { + if (!txMap.has(rawTx.txid)) { + txMap.set(rawTx.txid, mapRawTxMultiAddress(rawTx, allAddressStrings)); + } + } + } catch { + // Skip on error + } + } + const transactions = Array.from(txMap.values()) + .sort((a, b) => b.timestamp - a.timestamp); + + // Aggregate UTXOs with address/path tagging + const taggedUtxos: TaggedUTXO[] = []; + for (const addr of addresses) { + try { + const utxos = await blockchain.getUTXOs(addr.address); + for (const u of utxos) { + taggedUtxos.push({ + txid: u.txid, + vout: u.vout, + value: u.value, + address: addr.address, + derivationPath: addr.path, + status: u.status, + }); + } + } catch { + // Skip + } + } walletStore.update(state => ({ ...state, @@ -469,7 +669,8 @@ export async function refreshWalletData() { lastBalanceUpdate: Date.now(), transactions, lastTxUpdate: Date.now(), - hasMoreTransactions: txHistory.length > 0, + hasMoreTransactions: transactions.length > 0, + utxos: taggedUtxos, isLoading: false, error: undefined })); @@ -485,27 +686,43 @@ export async function refreshWalletData() { } /** - * Fetch more transactions (paging), appending to the list. Uses last txid in list and /chain/:txid. + * Fetch more transactions (paging), appending to the list. */ export async function fetchMoreTransactions() { - let currentAddress = ''; + let addresses: DerivedAddress[] = []; let lastTxid = ''; walletStore.update(state => { - currentAddress = state.address; + addresses = state.addresses; const txs = state.transactions; lastTxid = txs.length > 0 ? txs[txs.length - 1].txid : ''; return { ...state, isLoadingMoreTransactions: true }; }); - if (!currentAddress || !lastTxid) { + if (!addresses.length || !lastTxid) { walletStore.update(state => ({ ...state, isLoadingMoreTransactions: false, hasMoreTransactions: false })); return; } + const allAddressStrings = new Set(addresses.map(a => a.address)); + try { - const nextPage = await blockchain.getTransactions(currentAddress, lastTxid); - const newTransactions = nextPage.map((tx: any) => mapRawTxToTransaction(tx, currentAddress)); + const txMap = new Map(); + for (const addr of addresses) { + try { + const nextPage = await blockchain.getTransactions(addr.address, lastTxid); + for (const rawTx of nextPage) { + if (!txMap.has(rawTx.txid)) { + txMap.set(rawTx.txid, mapRawTxMultiAddress(rawTx, allAddressStrings)); + } + } + } catch { + // Skip on error + } + } + + const newTransactions = Array.from(txMap.values()) + .sort((a, b) => b.timestamp - a.timestamp); walletStore.update(state => { const existingIds = new Set(state.transactions.map(t => t.txid)); @@ -514,7 +731,7 @@ export async function fetchMoreTransactions() { ...state, transactions: [...state.transactions, ...appended], lastTxUpdate: Date.now(), - hasMoreTransactions: nextPage.length > 0, + hasMoreTransactions: newTransactions.length > 0, isLoadingMoreTransactions: false }; }); diff --git a/BoldChrome/src/lib/styles/theme.ts b/src/lib/styles/theme.ts similarity index 100% rename from BoldChrome/src/lib/styles/theme.ts rename to src/lib/styles/theme.ts diff --git a/BoldChrome/src/popup/SendBitcoin.svelte b/src/popup/SendBitcoin.svelte similarity index 100% rename from BoldChrome/src/popup/SendBitcoin.svelte rename to src/popup/SendBitcoin.svelte diff --git a/BoldChrome/src/routes/+layout.svelte b/src/routes/+layout.svelte similarity index 100% rename from BoldChrome/src/routes/+layout.svelte rename to src/routes/+layout.svelte diff --git a/BoldChrome/src/routes/+layout.ts b/src/routes/+layout.ts similarity index 100% rename from BoldChrome/src/routes/+layout.ts rename to src/routes/+layout.ts diff --git a/BoldChrome/src/routes/+page.svelte b/src/routes/+page.svelte similarity index 100% rename from BoldChrome/src/routes/+page.svelte rename to src/routes/+page.svelte diff --git a/BoldChrome/src/routes/permission/+page.svelte b/src/routes/permission/+page.svelte similarity index 100% rename from BoldChrome/src/routes/permission/+page.svelte rename to src/routes/permission/+page.svelte diff --git a/src/routes/popup.html/+page.svelte b/src/routes/popup.html/+page.svelte new file mode 100644 index 0000000..3ed56c5 --- /dev/null +++ b/src/routes/popup.html/+page.svelte @@ -0,0 +1,5143 @@ + + + + + diff --git a/BoldChrome/src/routes/popup.html/+page.ts b/src/routes/popup.html/+page.ts similarity index 100% rename from BoldChrome/src/routes/popup.html/+page.ts rename to src/routes/popup.html/+page.ts diff --git a/BoldChrome/src/routes/scanner/+page.svelte b/src/routes/scanner/+page.svelte similarity index 100% rename from BoldChrome/src/routes/scanner/+page.svelte rename to src/routes/scanner/+page.svelte diff --git a/BoldChrome/src/types/chrome.d.ts b/src/types/chrome.d.ts similarity index 100% rename from BoldChrome/src/types/chrome.d.ts rename to src/types/chrome.d.ts diff --git a/BoldChrome/static/app.css b/static/app.css similarity index 80% rename from BoldChrome/static/app.css rename to static/app.css index 3bb0f19..acf5dd7 100644 --- a/BoldChrome/static/app.css +++ b/static/app.css @@ -44,6 +44,7 @@ body[data-sveltekit-preload-data="hover"] { /* ----- Popup root & content (so layout takes effect from app.css) ----- */ html body[data-sveltekit-preload-data="hover"] .popup-root { + position: relative; display: flex; flex-direction: column; align-items: stretch; @@ -54,6 +55,31 @@ html body[data-sveltekit-preload-data="hover"] .popup-root { background: var(--color-background); overflow: hidden; } +/* Global toast – fixed at top center, always visible when shown */ +html body[data-sveltekit-preload-data="hover"] .popup-root .toast-global { + position: fixed; + top: 56px; + left: 50%; + transform: translateX(-50%); + z-index: 9999; + padding: var(--space-small) var(--space-medium); + border-radius: var(--radius-medium); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: #fff; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + max-width: calc(100% - var(--space-large)); + text-align: center; +} +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .toast-global.success { + background: var(--color-primary); +} +html body[data-sveltekit-preload-data="hover"] .popup-root .toast-global.error { + background: var(--color-bitcoinOrange); +} html body[data-sveltekit-preload-data="hover"] .app-content { flex: 1; display: flex; @@ -62,7 +88,7 @@ html body[data-sveltekit-preload-data="hover"] .app-content { overflow: auto; width: 100%; margin-top: 48px; - padding-top: var(--space-large); + padding: var(--space-small); min-height: 0; box-sizing: border-box; } @@ -74,6 +100,84 @@ html padding-top: 0; } +/* ----- Camera permission overlay (first-load) ----- */ +html body[data-sveltekit-preload-data="hover"] .camera-permission-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 100; + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-large); + background: var(--color-background); + box-sizing: border-box; +} +html body[data-sveltekit-preload-data="hover"] .camera-permission-screen { + margin-top: 0; + padding-top: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--space-large); + min-height: 100%; + box-sizing: border-box; +} +html body[data-sveltekit-preload-data="hover"] .camera-permission-card { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + max-width: 320px; + padding: var(--space-extraLarge) var(--space-large); + background: var(--color-cardBackground); + border: 1px solid var(--color-border); + border-radius: var(--radius-large); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08); + box-sizing: border-box; +} +html body[data-sveltekit-preload-data="hover"] .camera-permission-logo { + width: 64px; + height: 64px; + object-fit: contain; + margin-bottom: var(--space-large); +} +html body[data-sveltekit-preload-data="hover"] .camera-permission-title { + font-size: var(--font-size-xl); + font-weight: var(--font-weight-bold); + color: var(--color-text); + margin: 0 0 var(--space-small) 0; +} +html body[data-sveltekit-preload-data="hover"] .camera-permission-hint { + font-size: var(--font-size-base); + color: var(--color-textSecondary); + line-height: 1.5; + margin: 0 0 var(--space-large) 0; +} +html body[data-sveltekit-preload-data="hover"] .camera-permission-actions { + display: flex; + flex-direction: column; + gap: var(--space-medium); + width: 100%; + max-width: 260px; +} +html body[data-sveltekit-preload-data="hover"] .camera-permission-grant { + width: 100%; +} +html body[data-sveltekit-preload-data="hover"] .camera-permission-skip { + width: 100%; + background: var(--color-cardBackground); + border: 1px solid var(--color-border); + color: var(--color-textSecondary); +} +html body[data-sveltekit-preload-data="hover"] .camera-permission-skip:hover { + background: var(--color-border); + color: var(--color-text); +} + /* ----- Header: logo left, theme right ----- */ html body[data-sveltekit-preload-data="hover"] .app-header { position: fixed !important; @@ -665,7 +769,8 @@ html body[data-sveltekit-preload-data="hover"] .popup-root .wallet-content { flex: 1; overflow-x: hidden; overflow-y: auto; - padding: 0 var(--space-medium) var(--space-medium); + padding: 0; + margin-top: var(--space-medium); box-sizing: border-box; } @@ -892,14 +997,12 @@ html html body[data-sveltekit-preload-data="hover"] .popup-root .address-text { color: var(--color-text); } -html body[data-sveltekit-preload-data="hover"] .popup-root .address-value, -html body[data-sveltekit-preload-data="hover"] .popup-root .address-path { +html body[data-sveltekit-preload-data="hover"] .popup-root .address-prefix { color: var(--color-textSecondary); font-family: var(--font-mono); } -html body[data-sveltekit-preload-data="hover"] .popup-root .address-balance { - color: var(--color-success); - font-family: var(--font-mono); +html body[data-sveltekit-preload-data="hover"] .popup-root .address-check { + color: var(--color-primary); } html body[data-sveltekit-preload-data="hover"] .popup-root .actions { background: var(--color-cardBackground); @@ -974,7 +1077,10 @@ html body[data-sveltekit-preload-data="hover"] .popup-root .tx-list-footer { margin-top: var(--space-small); border-top: 1px solid var(--color-border); } -html body[data-sveltekit-preload-data="hover"] .popup-root .tx-list-footer-loading, +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .tx-list-footer-loading, html body[data-sveltekit-preload-data="hover"] .popup-root .tx-list-footer-end { margin: 0; font-size: var(--font-size-sm); @@ -989,9 +1095,14 @@ html body[data-sveltekit-preload-data="hover"] .popup-root .tx-list-load-more { font-size: var(--font-size-sm); font-weight: var(--font-weight-medium); cursor: pointer; - transition: background 0.2s, border-color 0.2s; + transition: + background 0.2s, + border-color 0.2s; } -html body[data-sveltekit-preload-data="hover"] .popup-root .tx-list-load-more:hover { +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .tx-list-load-more:hover { background: var(--color-background); border-color: var(--color-primary); } @@ -1007,7 +1118,10 @@ html body[data-sveltekit-preload-data="hover"] .popup-root .pin-screen { box-sizing: border-box; flex: 1; } -html body[data-sveltekit-preload-data="hover"] .popup-root .pin-screen.pin-loading { +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .pin-screen.pin-loading { gap: var(--space-medium); } html body[data-sveltekit-preload-data="hover"] .popup-root .pin-loading-text { @@ -1051,7 +1165,10 @@ html body[data-sveltekit-preload-data="hover"] .popup-root .pin-screen-hint { line-height: 1.45; max-width: 100%; } -html body[data-sveltekit-preload-data="hover"] .popup-root .pin-screen-hint.pin-hint-long { +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .pin-screen-hint.pin-hint-long { max-width: 36ch; margin-left: auto; margin-right: auto; @@ -1063,7 +1180,10 @@ html body[data-sveltekit-preload-data="hover"] .popup-root .pin-form { width: 100%; gap: var(--space-small); } -html body[data-sveltekit-preload-data="hover"] .popup-root .pin-form.pin-form-confirm { +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .pin-form.pin-form-confirm { gap: var(--space-medium); } html body[data-sveltekit-preload-data="hover"] .popup-root .pin-input { @@ -1079,9 +1199,14 @@ html body[data-sveltekit-preload-data="hover"] .popup-root .pin-input { text-align: center; letter-spacing: 0.25em; box-sizing: border-box; - transition: border-color 0.2s, box-shadow 0.2s; + transition: + border-color 0.2s, + box-shadow 0.2s; } -html body[data-sveltekit-preload-data="hover"] .popup-root .pin-input::placeholder { +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .pin-input::placeholder { color: var(--color-textSecondary); opacity: 0.8; letter-spacing: 0; @@ -1091,10 +1216,16 @@ html body[data-sveltekit-preload-data="hover"] .popup-root .pin-input:focus { border-color: var(--color-primary); box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.08); } -html body[data-sveltekit-preload-data="hover"] .popup-root .pin-input[aria-invalid="true"] { +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .pin-input[aria-invalid="true"] { border-color: var(--color-error); } -html body[data-sveltekit-preload-data="hover"] .popup-root .pin-input[aria-invalid="true"]:focus { +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .pin-input[aria-invalid="true"]:focus { box-shadow: 0 0 0 2px rgba(231, 76, 60, 0.2); } html body[data-sveltekit-preload-data="hover"] .popup-root .pin-error { @@ -1115,10 +1246,16 @@ html body[data-sveltekit-preload-data="hover"] .popup-root .pin-submit { border-radius: var(--radius-medium); } /* ----- Mempool preference screen (after pairing) ----- */ -html body[data-sveltekit-preload-data="hover"] .popup-root .mempool-preference-card { +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .mempool-preference-card { max-width: 320px; } -html body[data-sveltekit-preload-data="hover"] .popup-root .mempool-preference-form { +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .mempool-preference-form { width: 100%; display: flex; flex-direction: column; @@ -1133,7 +1270,10 @@ html body[data-sveltekit-preload-data="hover"] .popup-root .mempool-label { html body[data-sveltekit-preload-data="hover"] .popup-root .mempool-input { letter-spacing: 0; } -html body[data-sveltekit-preload-data="hover"] .popup-root .mempool-preference-actions { +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .mempool-preference-actions { display: flex; gap: var(--space-medium); margin-top: var(--space-small); @@ -1158,13 +1298,19 @@ html body[data-sveltekit-preload-data="hover"] .popup-root .mempool-modal-card { flex-direction: column; gap: var(--space-medium); } -html body[data-sveltekit-preload-data="hover"] .popup-root .mempool-modal-title { +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .mempool-modal-title { margin: 0 0 var(--space-small); font-size: var(--font-size-xl); font-weight: var(--font-weight-bold); color: var(--color-text); } -html body[data-sveltekit-preload-data="hover"] .popup-root .mempool-modal-input { +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .mempool-modal-input { width: 100%; min-height: 44px; padding: var(--space-medium) var(--space-large); @@ -1176,24 +1322,41 @@ html body[data-sveltekit-preload-data="hover"] .popup-root .mempool-modal-input box-sizing: border-box; transition: border-color 0.2s; } -html body[data-sveltekit-preload-data="hover"] .popup-root .mempool-modal-input:focus { +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .mempool-modal-input:focus { outline: none; border-color: var(--color-primary); } -html body[data-sveltekit-preload-data="hover"] .popup-root .mempool-modal-actions { +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .mempool-modal-actions { display: flex; flex-wrap: wrap; gap: var(--space-small); justify-content: space-between; align-items: center; } -html body[data-sveltekit-preload-data="hover"] .popup-root .mempool-modal-actions .btn-secondary, -html body[data-sveltekit-preload-data="hover"] .popup-root .mempool-modal-actions .btn-primary { +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .mempool-modal-actions + .btn-secondary, +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .mempool-modal-actions + .btn-primary { min-height: 44px; padding: var(--space-medium) var(--space-large); width: 30%; } -html body[data-sveltekit-preload-data="hover"] .popup-root .mempool-modal-reset-btn { +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .mempool-modal-reset-btn { min-height: 44px; padding: var(--space-medium) var(--space-large); } @@ -1217,7 +1380,7 @@ html body[data-sveltekit-preload-data="hover"] .popup-root .footer { align-items: center; justify-content: space-between; gap: var(--space-small) var(--space-medium); - padding: var(--space-medium); + padding-top: var(--space-medium); min-height: 40px; color: var(--color-textSecondary); background: var(--color-background); @@ -1232,11 +1395,17 @@ html body[data-sveltekit-preload-data="hover"] .popup-root .footer-left { flex: 1 1 auto; min-width: 0; } -html body[data-sveltekit-preload-data="hover"] .popup-root .footer-mempool-label { +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .footer-mempool-label { color: var(--color-textSecondary); flex-shrink: 0; } -html body[data-sveltekit-preload-data="hover"] .popup-root .footer-mempool-value { +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .footer-mempool-value { color: var(--color-text); font-family: var(--font-mono); font-size: var(--font-size-xs); @@ -1259,7 +1428,9 @@ html body[data-sveltekit-preload-data="hover"] .popup-root .footer-btn { background: var(--color-cardBackground); color: var(--color-text); cursor: pointer; - transition: background 0.2s, border-color 0.2s; + transition: + background 0.2s, + border-color 0.2s; } html body[data-sveltekit-preload-data="hover"] .popup-root .footer-btn:hover { background: var(--color-border); @@ -1418,6 +1589,160 @@ html font-size: var(--font-size-sm); color: var(--color-textSecondary); } +html body[data-sveltekit-preload-data="hover"] .popup-root .send-amount-row { + display: flex; + gap: var(--space-small); + align-items: stretch; + margin-bottom: var(--space-small); +} +html body[data-sveltekit-preload-data="hover"] .popup-root .send-amount-input { + flex: 1 1 0; + min-width: 0; +} +html body[data-sveltekit-preload-data="hover"] .popup-root .send-max-btn { + flex-shrink: 0; + padding: var(--space-small) var(--space-medium); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + border-radius: var(--radius-small); + border: 1px solid var(--color-border); + background: var(--color-cardBackground); + color: var(--color-text); + cursor: pointer; + transition: + background 0.2s, + border-color 0.2s; + height: 38px; +} +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .send-max-btn:hover:not(:disabled) { + background: var(--color-border); + border-color: var(--color-primary); +} +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .send-max-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} +html body[data-sveltekit-preload-data="hover"] .popup-root .send-fee-strategy { + margin-bottom: var(--space-small); +} +html body[data-sveltekit-preload-data="hover"] .popup-root .send-fee-label { + display: block; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + color: var(--color-textSecondary); + margin-bottom: var(--space-small); +} +html body[data-sveltekit-preload-data="hover"] .popup-root .send-fee-buttons { + display: flex; + flex-wrap: wrap; + gap: var(--space-small); +} +html body[data-sveltekit-preload-data="hover"] .popup-root .fee-strategy-name { + font-size: var(--font-size-md); + font-weight: var(--font-weight-bold); +} +html body[data-sveltekit-preload-data="hover"] .popup-root .fee-strategy-rate { + font-size: var(--font-size-xs); + font-weight: var(--font-weight-normal); +} +html body[data-sveltekit-preload-data="hover"] .popup-root .send-fee-btn { + padding: var(--space-small) var(--space-medium); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + border-radius: var(--radius-small); + border: 1px solid var(--color-border); + background: var(--color-cardBackground); + color: var(--color-primary); + cursor: pointer; + width: 22%; + transition: + background 0.2s, + border-color 0.2s, + color 0.2s; +} +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .send-fee-btn:hover:not(:disabled) { + background: var(--color-border); + color: var(--color-text); +} +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .send-fee-btn.selected { + background: var(--color-primary); + border-color: var(--color-primary); + color: var(--color-textOnPrimary); +} +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .send-fee-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} +html body[data-sveltekit-preload-data="hover"] .popup-root .send-fee-row { + font-size: var(--font-size-sm); + color: var(--color-textSecondary); + margin-bottom: var(--space-small); +} +html body[data-sveltekit-preload-data="hover"] .popup-root .send-summary { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: var(--space-small); + font-size: var(--font-size-sm); +} +html body[data-sveltekit-preload-data="hover"] .popup-root .send-summary-line { + display: flex; + justify-content: space-between; + align-items: baseline; +} +html body[data-sveltekit-preload-data="hover"] .popup-root .send-summary-label { + color: var(--color-textSecondary); + flex-shrink: 0; +} +html body[data-sveltekit-preload-data="hover"] .popup-root .send-summary-value { + text-align: right; + font-family: var(--font-mono); + color: var(--color-text); +} +html body[data-sveltekit-preload-data="hover"] .popup-root .send-summary-fiat { + font-family: var(--font-body, sans-serif); + color: var(--color-textSecondary); + font-size: var(--font-size-xs); + margin-left: 4px; +} +html body[data-sveltekit-preload-data="hover"] .popup-root .send-summary-total { + border-top: 1px solid var(--color-border); + padding-top: 6px; + margin-top: 2px; +} +html body[data-sveltekit-preload-data="hover"] .popup-root .send-summary-total .send-summary-label { + font-weight: var(--font-weight-semibold); + color: var(--color-text); +} +html body[data-sveltekit-preload-data="hover"] .popup-root .send-summary-total .send-summary-value { + font-weight: var(--font-weight-semibold); +} +html + body[data-sveltekit-preload-data="hover"] + .popup-root + .send-summary-value.invalid { + color: var(--color-error); +} +html body[data-sveltekit-preload-data="hover"] .popup-root .send-total-warn { + margin: 0 0 var(--space-small); + font-size: var(--font-size-sm); + color: var(--color-error); +} html body[data-sveltekit-preload-data="hover"] .popup-root .send-address-row { display: flex; gap: var(--space-small); diff --git a/BoldChrome/static/icons/icon128.png b/static/icons/icon128.png similarity index 100% rename from BoldChrome/static/icons/icon128.png rename to static/icons/icon128.png diff --git a/BoldChrome/static/icons/icon16.png b/static/icons/icon16.png similarity index 100% rename from BoldChrome/static/icons/icon16.png rename to static/icons/icon16.png diff --git a/BoldChrome/static/icons/icon48.png b/static/icons/icon48.png similarity index 100% rename from BoldChrome/static/icons/icon48.png rename to static/icons/icon48.png diff --git a/BoldChrome/static/manifest.json b/static/manifest.json similarity index 96% rename from BoldChrome/static/manifest.json rename to static/manifest.json index 212708f..e6a8168 100644 --- a/BoldChrome/static/manifest.json +++ b/static/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Bold Wallet", - "version": "1.0.0", + "version": "1.0.3", "description": "Bold Bitcoin Wallet - Watch-only wallet with QR-based mobile signing", "permissions": ["storage"], "host_permissions": ["https://mempool.space/*"], diff --git a/static/permission-grant.html b/static/permission-grant.html new file mode 100644 index 0000000..521b312 --- /dev/null +++ b/static/permission-grant.html @@ -0,0 +1,164 @@ + + + + + + Camera Permission + + + +
+
+

Camera Permission Setup

+
+

Requesting camera permission...

+

Please click "Allow" when your browser asks for camera access.

+ +
+
+ + + + diff --git a/BoldChrome/static/robots.txt b/static/robots.txt similarity index 100% rename from BoldChrome/static/robots.txt rename to static/robots.txt diff --git a/BoldChrome/svelte.config.js b/svelte.config.js similarity index 100% rename from BoldChrome/svelte.config.js rename to svelte.config.js diff --git a/BoldChrome/tsconfig.json b/tsconfig.json similarity index 100% rename from BoldChrome/tsconfig.json rename to tsconfig.json diff --git a/BoldChrome/verify-setup.cjs b/verify-setup.cjs similarity index 100% rename from BoldChrome/verify-setup.cjs rename to verify-setup.cjs diff --git a/BoldChrome/verify-setup.js b/verify-setup.js similarity index 100% rename from BoldChrome/verify-setup.js rename to verify-setup.js diff --git a/BoldChrome/vite.config.ts b/vite.config.ts similarity index 100% rename from BoldChrome/vite.config.ts rename to vite.config.ts