This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
bsv-desktop is an Electron-based desktop wallet for BSV blockchain built with React. It provides a complete wallet interface with support for both self-custody (local SQLite) and remote storage (WAB) modes. The project implements the BRC-100 wallet interface and exposes an HTTP server on port 3321 for external app integration.
Architecture: Electron (Node.js backend) + React (TypeScript frontend) + SQLite (local storage)
npm run dev # Start dev server with hot reload
npm run dev:vite # Start Vite only (port 5173)
npm run dev:electron # Build Electron and launchnpm run build # Build both renderer and Electron
npm run build:renderer # Vite build → dist/
npm run build:electron # TypeScript build → dist-electron/npm run package # Package for current platform
npm run package:mac # macOS (DMG + ZIP)
npm run package:win # Windows (NSIS + Portable)
npm run package:linux # Linux (AppImage + DEB)Output: release/ directory
npm run lint:ci # Currently no-op
npm run test # Currently no tests1. Electron Main Process (electron/)
- Window lifecycle and IPC handlers (
main.ts) - HTTP server on port 3321 for BRC-100 interface (
httpServer.ts) - Storage manager with SQLite backend (
storage.ts) - Monitor worker process spawner (
storage.ts→monitor-worker.ts) - IPC security bridge (
preload.ts)
2. Renderer Process (src/)
- React application entry (
main.tsx) - Wallet HTTP request handler (
onWalletReady.ts) - Native function wrappers (
electronFunctions.ts) - Storage IPC proxy (
StorageElectronIPC.ts)
3. Monitor Worker Process (electron/monitor-worker.ts)
- Separate Node.js process for background tasks
- Runs
Monitorfrom@bsv/wallet-toolbox - Monitors transactions, proofs, UTXO state
- SQLite WAL mode for concurrent access
The React app uses two primary contexts:
1. WalletContext (src/lib/WalletContext.tsx)
- Wallet Managers:
WalletAuthenticationManager(WAB mode with phone/DevConsole auth)CWIStyleWalletManager(self-custody mode)- Both create
WalletPermissionsManagerfor permission handling
- Permission Queues: basket, certificate, protocol, spending, grouped
- Group Permission Gating: Batches related permission requests
- Configuration: Network (main/test), WAB URL, storage URL, auth method
- Snapshot Management: Version 3 format with config persistence
2. UserContext (src/lib/UserContext.tsx)
- Platform-agnostic native handlers (focus, download, dialogs)
- Modal visibility state for permission handlers
- App metadata (version, name)
New Users:
WalletConfigcomponent shows → user selects network, auth, storagefinalizeConfig()validates and stores config in WalletContext state- Wallet manager created based on
useWabflag - User provides password →
providePassword()→ authenticated - Snapshot saved to
localStorage.snap(Version 3 format)
Returning Users:
- Snapshot detected in localStorage
- Config restored before wallet manager creation (critical for preventing duplicates)
- Wallet manager created with restored config
- Snapshot loaded into wallet manager
- User authenticated automatically
[version byte: 3]
[varint: config_length]
[config JSON bytes]
[wallet snapshot bytes from WalletManager]
Config includes: wabUrl, network, storageUrl, messageBoxUrl, authMethod, useWab, useRemoteStorage, useMessageBox
Critical Implementation:
- Config restoration happens in early useEffect (before wallet manager creation)
- Prevents cascading state updates and duplicate wallet managers
- Backward compatible with Version 1/2 snapshots
- Saved on: authentication, password change, profile switch, logout
Local Storage (useRemoteStorage: false):
Renderer (WalletContext)
↓ buildWallet()
StorageElectronIPC (IPC wrapper)
↓ electron.storage.callMethod()
Main Process (storage.ts)
↓ StorageManager.callMethod()
StorageKnex
↓ Knex queries
SQLite (~/.bsv-desktop/wallet.db)
Remote Storage (useRemoteStorage: true):
Renderer (WalletContext)
↓ buildWallet()
StorageClient (HTTP)
↓ Fetch requests
Remote Storage Server
Storage Manager (electron/storage.ts):
- Maintains Map of storage instances by
identityKey-chain - IPC handlers:
isAvailable,makeAvailable,callMethod,initializeServices - Spawns Monitor worker per identity/chain
- SQLite WAL mode enabled for concurrent access
- Request Reception: External app calls BRC-100 method → permission needed
- Callback Invoked:
WalletPermissionsManagercalls bound callback (e.g.,basketAccessCallback) - Queueing: Request added to type-specific queue in WalletContext
- Modal Display: Handler component (e.g.,
BasketAccessHandler) shows modal for first queued item - User Decision: User approves/denies → calls permission manager method
- Queue Advancement:
advance*Queue()removes handled request, shows next
Group Permissions:
groupPhasestate: 'idle' | 'pending'- Individual requests buffered when grouped request arrives
- After grouped decision, buffered requests evaluated against decision
- Uncovered requests re-queued for individual approval
Flow (electron/httpServer.ts):
- External app →
POST http://127.0.0.1:3321/createAction - Express server receives request
- Main process → IPC
http-request→ Renderer - Renderer's
onWalletReadyhandler →WalletInterfacemethod - Renderer → IPC
http-response→ Main process - Main process returns HTTP response to external app
CORS: Enabled for all origins
Lifecycle:
Main Process (storage.ts)
↓ fork('monitor-worker.js')
Worker Process (monitor-worker.ts)
↓ sends 'ready' message
Main Process
↓ sends 'start' with {identityKey, chain}
Worker Process
↓ creates DB connection (WAL mode)
↓ creates Monitor
↓ startTasks()
↓ sends 'monitor-started'
[Background monitoring runs]
Main Process (on shutdown)
↓ sends 'stop'
Worker Process
↓ stopTasks()
↓ exit
Why Separate Process:
- Prevents blocking main thread during
Monitor.startTasks() - Isolates errors (worker crash doesn't affect app)
- Concurrent SQLite access via WAL mode
Problem: Config restoration during snapshot load triggers re-renders → duplicate wallet managers
Solution:
- Early useEffect restores config before wallet manager creation useEffect
- Only depends on
loadEnhancedSnapshot, runs once on mount - Sets
configStatus: 'configured'to prevent second useEffect from re-running - Wallet manager useEffect checks
configStatus !== 'editing'
Problem: useRemoteStorage wasn't being saved to snapshots or set during config finalization
Solution:
finalizeConfig()callssetUseRemoteStorage()andsetUseMessageBox()saveEnhancedSnapshot()includes these flags in config JSONloadEnhancedSnapshot()infersuseRemoteStoragefromstorageUrlif not explicitly set (backward compatibility)
Problem: Renderer and Monitor both accessing database caused locks
Solution:
- Enable SQLite WAL (Write-Ahead Logging) mode
- Monitor runs in separate process with own connection
- Both can read/write without blocking
- Permission requests call
onFocusRequested()to bring app to foreground onFocusRelinquished()called when queues empty- Platform-specific implementations in
electron/main.ts
RequestInterceptorWalletwraps wallet interface- Intercepts method calls to record originator domain
updateRecentApp()fetches favicon/manifest- Debounced (5s) to prevent duplicate tracking
- Stored in localStorage as
brc100_recent_apps_{profileId}
Permission Handlers (in src/lib/components/):
BasketAccessHandler,CertificateAccessHandler,ProtocolPermissionHandlerGroupPermissionHandler,SpendingAuthorizationHandler- Modal-based UI with approve/deny actions
Dashboard Pages (in src/lib/pages/Dashboard/):
/dashboard- Apps, recent actions, balance/dashboard/apps- App catalog/dashboard/my-identity- Identity certificates/dashboard/trust- Trusted entities/dashboard/settings- Password, recovery key
UI Components:
UserInterface- Main router with permission handlersWalletConfig- WAB/storage/network configurationAmountDisplay- Currency display with exchange rates- Chips:
AppChip,BasketChip,CertificateChip, etc.
- Add queue state to
WalletContext:const [newRequests, setNewRequests] = useState<NewRequest[]>([]) - Create callback:
const newCallback = useCallback((request) => { setNewRequests(q => [...q, request]) }, []) - Bind in
buildWallet():permissionsManager.bindCallback('newPermission', newCallback) - Create advance function:
const advanceNewQueue = () => { setNewRequests(q => q.slice(1)) } - Create handler component:
NewPermissionHandler.tsx - Add to
UserInterface.tsxwith modal visibility state
Local (Electron):
- Modify
electron/storage.tsfor IPC handlers - Update
src/StorageElectronIPC.tsfor proxy methods - Database at
~/.bsv-desktop/wallet.dborwallet-test.db
Remote (WAB):
- Uses
StorageClientfrom@bsv/wallet-toolbox - Configure via
WalletConfigcomponent
- Defaults in
src/lib/config.ts - User config via
WalletConfigcomponent - Finalized by
finalizeConfig()in WalletContext - Stored in snapshot (Version 3)
strict: false- existing code not fully type-safe- Renderer:
tsconfig.json→dist/+dist/types/ - Electron:
tsconfig.electron.json→dist-electron/ - Shared types in
src/global.d.ts
@bsv/wallet-toolbox- Wallet managers, storage, permissions, Monitor@bsv/sdk- Transactions, keys, signingelectron- Desktop frameworkexpress- HTTP server (port 3321)better-sqlite3+knex- Local SQLite storagereact+react-router-domv5 - UI framework@mui/material- Material-UI components
- React UI library:
src/lib/(reusable components, contexts, pages) - Electron app entry:
src/(main.tsx, onWalletReady.ts, electronFunctions.ts) - Electron backend:
electron/(main.ts, httpServer.ts, storage.ts, monitor-worker.ts) - Build output:
dist/(renderer),dist-electron/(main),release/(packaged apps)
# Check if authenticated
curl http://127.0.0.1:3321/isAuthenticated
# Test wallet method
curl -X POST http://127.0.0.1:3321/listOutputs \
-H "Content-Type: application/json" \
-d '{"args": [{"basket": "default"}]}'- Main process: Logs in terminal where
npm run devruns - Renderer: DevTools auto-open in dev mode (Cmd+Opt+I / Ctrl+Shift+I)
- Monitor worker: stdout/stderr piped to main process console
- Location:
~/.bsv-desktop/wallet.db(mainnet) orwallet-test.db(testnet) - WAL mode files:
wallet.db-wal,wallet.db-shm - Delete database:
rm -rf ~/.bsv-desktop/(forces re-initialization)
- Config restoration: Only
loadEnhancedSnapshot - Wallet manager creation: All config state +
passwordRetriever+recoveryKeySaver - Snapshot loading: Happens inside wallet manager creation (async/await)
Renderer → Main:
const result = await window.electron.storage.callMethod(identityKey, chain, 'listOutputs', [args])Main → Renderer (HTTP requests):
mainWindow.webContents.send('http-request', { id, method, args })
ipcMain.once(`http-response-${id}`, (event, response) => { /* ... */ })- Toast errors via
react-toastify - Console errors for debugging
- Try/catch in async wallet operations
- Permission callbacks should not throw (breaks wallet flow)