Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bsv-desktop-electron",
"version": "2.3.0",
"version": "2.3.1",
"description": "BSV Desktop Wallet - Electron Edition",
"main": "dist-electron/main.js",
"type": "module",
Expand Down
50 changes: 24 additions & 26 deletions src/lib/WalletContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,12 @@ import React, {
} from 'react'
import { useMediaQuery } from '@mui/material'
import { DEFAULT_SETTINGS, WalletSettings } from '@bsv/wallet-toolbox-client/out/src/WalletSettingsManager'
import { WalletPermissionsManager, PrivilegedKeyManager } from '@bsv/wallet-toolbox-client'
import { WalletStorageManager, WalletAuthenticationManager } from '@bsv/wallet-toolbox-client'
import { WalletInterface } from '@bsv/sdk'
import { WalletPermissionsManager, PrivilegedKeyManager, WalletStorageManager, WalletAuthenticationManager } from '@bsv/wallet-toolbox-client'
import { WalletInterface, Utils } from '@bsv/sdk'
import { PeerPayClient, AdvertisementToken } from '@bsv/message-box-client'
import { toast } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'

import { DEFAULT_CHAIN, ADMIN_ORIGINATOR, DEFAULT_USE_WAB } from './config'
import { ADMIN_ORIGINATOR } from './config'
import { UserContext } from './UserContext'
import { useWalletService, getWalletService } from './hooks/useWalletService'
import { buildPermissionModuleRegistry } from './permissionModules/registry'
Expand All @@ -37,7 +35,6 @@ import type { GroupPermissionRequest, CounterpartyPermissionRequest } from './ty
import type { WalletProfile } from './types/WalletProfile'
import { RequestInterceptorWallet } from './RequestInterceptorWallet'
import { updateRecentApp } from './pages/Dashboard/Apps/getApps'
import { Utils } from '@bsv/sdk'

// -----
// Permission Configuration Types (preserved for backward compatibility)
Expand Down Expand Up @@ -98,7 +95,6 @@ interface ManagerState {
walletManager?: WalletAuthenticationManager;
permissionsManager?: WalletPermissionsManager;
settingsManager?: any;
wallet?: WalletInterface;
storageManager?: WalletStorageManager;
}

Expand All @@ -118,6 +114,13 @@ export interface WABConfig {
export interface WalletContextValue {
managers: ManagerState;
updateManagers: (newManagers: ManagerState) => void;
/**
* Raw, unwrapped `Wallet` from `@bsv/wallet-toolbox`. Standalone — kept
* outside `managers` so it is never confused with `permissionsManager`.
* Internal/first-party use only (e.g. diagnostic UI, BRC-103 handshake
* plumbing). App-originated requests must go through `managers.permissionsManager`.
*/
wallet?: WalletInterface;
settings: WalletSettings;
updateSettings: (newSettings: WalletSettings) => Promise<void>;
network: 'mainnet' | 'testnet';
Expand Down Expand Up @@ -161,6 +164,7 @@ export interface WalletContextValue {
addBackupStorageUrl: (url: string) => Promise<void>;
removeBackupStorageUrl: (url: string) => Promise<void>;
syncBackupStorage: (progressCallback?: (message: string) => void) => Promise<void>;
setPrimaryStorage: (target: string, progressCallback?: (message: string) => void) => Promise<void>;
updateMessageBoxUrl: (url: string) => Promise<void>;
removeMessageBoxUrl: () => Promise<void>;
initializingBackendServices: boolean;
Expand Down Expand Up @@ -221,6 +225,7 @@ export const WalletContext = createContext<WalletContextValue>({
addBackupStorageUrl: async () => {},
removeBackupStorageUrl: async () => {},
syncBackupStorage: async () => {},
setPrimaryStorage: async () => {},
updateMessageBoxUrl: async () => {},
removeMessageBoxUrl: async () => {},
initializingBackendServices: false,
Expand Down Expand Up @@ -314,18 +319,10 @@ export const WalletContextProvider: React.FC<WalletContextProps> = ({
svc.permissionQueue.setPermissionsModuleHelpers(getPermissionModuleById as any, permissionPromptHandlersRef.current)
}, [enabledPermissionModules, getPermissionModuleById, svc])

// ---- Permissions config (load from localStorage once) ----
useEffect(() => {
try {
const stored = localStorage.getItem('permissionsConfig')
if (stored) {
const merged = { ...DEFAULT_PERMISSIONS_CONFIG, ...JSON.parse(stored) }
svc.permissionQueue.permissionsConfig = merged
}
} catch (e) {
console.error('Failed to load permissions config:', e)
}
}, [svc])
// Permissions config is loaded from localStorage inside getWalletService()
// before React mounts, so the primed _queueSnapshot already reflects the
// saved value. Loading here in a useEffect creates a race against
// useSyncExternalStore's subscribe phase and the snapshot update is lost.

// ---- Dark mode for permission prompts ----
const tokenPromptPaletteMode = useMemo<import('@mui/material').PaletteMode>(() => {
Expand All @@ -344,9 +341,11 @@ export const WalletContextProvider: React.FC<WalletContextProps> = ({
const DEBOUNCE_TIME_MS = 5000

useEffect(() => {
// Use managers.wallet (set by _buildWallet) instead of walletManager.authenticated
// SimpleWalletManager (direct-key) doesn't expose an authenticated property
const walletReady = !!managers?.wallet
// External BRC-100 traffic (port 3321) hits permissionsManager so app-originated
// requests pass through permission prompts. The standalone raw `wallet` (separate
// from `managers`) is reserved for internal wallet-toolbox plumbing that
// intentionally bypasses permissions (e.g. StorageClient BRC-103 handshake).
const walletReady = !!managers?.permissionsManager
console.log('[onWalletReady effect] check:', {
walletReady,
profileId: activeProfile?.id ? `[${activeProfile.id.length} bytes]` : null,
Expand All @@ -357,7 +356,6 @@ export const WalletContextProvider: React.FC<WalletContextProps> = ({
}

console.log('[onWalletReady effect] guard passed — registering wallet ref')
const wallet = managers.wallet!

const updateRecentAppWrapper = async (profileId: string, origin: string): Promise<void> => {
try {
Expand All @@ -367,18 +365,18 @@ export const WalletContextProvider: React.FC<WalletContextProps> = ({
if (lastProcessed && (now - lastProcessed) < DEBOUNCE_TIME_MS) return
recentOriginsRef.current.set(cacheKey, now)
await updateRecentApp(profileId, origin)
window.dispatchEvent(new CustomEvent('recentAppsUpdated', { detail: { profileId, origin } }))
globalThis.dispatchEvent(new CustomEvent('recentAppsUpdated', { detail: { profileId, origin } }))
} catch (error) {
console.debug('Error tracking recent app:', error)
}
}

const interceptorWallet = new RequestInterceptorWallet(wallet, Utils.toBase64(activeProfile.id), updateRecentAppWrapper)
const interceptorWallet = new RequestInterceptorWallet(managers.permissionsManager, Utils.toBase64(activeProfile.id), updateRecentAppWrapper)
// onWalletReady registers IPC listener once, subsequent calls just swap wallet ref
onWalletReady(interceptorWallet)

// No cleanup — IPC listener is permanent, wallet ref is swapped not re-registered
}, [managers?.wallet, activeProfile?.id, onWalletReady])
}, [managers?.permissionsManager, activeProfile?.id, onWalletReady])

// ---- Context value ----
const contextValue = useMemo<WalletContextValue>(() => ({
Expand Down
16 changes: 14 additions & 2 deletions src/lib/components/WalletConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,20 @@ const WalletConfig: React.FC<WalletConfigProps> = ({ autoExpand = false, hideLog
useRemoteStorage,
useMessageBox,
})
if (valid) setShowWalletConfig(false)
}, [wabUrl, wabInfo, method, network, storageUrl, messageBoxUrl, loginType, useRemoteStorage, useMessageBox, finalizeConfig, setShowWalletConfig])
if (valid) {
// The toggle/close path calls `resetCurrentConfig()`, which re-applies `backupConfig`
// (the pre-edit snapshot). After a successful Apply we must clear `backupConfig` so
// that a subsequent close — including the parent's auto-close below — doesn't
// immediately revert the freshly-applied configuration.
setBackupConfig(undefined)
if (isControlled) {
// Parent owns visibility; signal it to close the panel automatically.
onToggle?.()
} else {
setShowWalletConfig(false)
}
}
}, [wabUrl, wabInfo, method, network, storageUrl, messageBoxUrl, loginType, useRemoteStorage, useMessageBox, finalizeConfig, setShowWalletConfig, isControlled, onToggle])

// Force the manager to use the "presentation-key-and-password" flow (only for WAB/CWIStyle managers):
useEffect(() => {
Expand Down
34 changes: 32 additions & 2 deletions src/lib/hooks/useWalletService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { UserContext } from '../UserContext'
import { WalletService, WalletServiceSnapshot } from '../services/WalletService'
import type { QueueSnapshot } from '../services/PermissionQueueManager'
import type { PeerPaySnapshot } from '../services/PeerPayManager'
import { DEFAULT_PERMISSIONS_CONFIG } from '../WalletContext'

// Module-level singleton — survives React re-renders and hot reloads
let _walletServiceInstance: WalletService | null = null
Expand All @@ -29,6 +30,19 @@ export function getWalletService(): WalletService {
_walletServiceInstance = new WalletService()
// Restore config from snapshot synchronously before first render
_walletServiceInstance.restoreConfigFromSnapshot()
// Restore permissions config from localStorage BEFORE priming snapshot cache.
// Doing this in a React useEffect creates a race: the useEffect's emit fires
// before useSyncExternalStore's subscribe has registered, so the snapshot
// update is lost and React keeps reading the DEFAULT-primed cache.
try {
const stored = typeof localStorage !== 'undefined' ? localStorage.getItem('permissionsConfig') : null
if (stored) {
const merged = { ...DEFAULT_PERMISSIONS_CONFIG, ...JSON.parse(stored) }
_walletServiceInstance.permissionQueue.permissionsConfig = merged
}
} catch (e) {
console.error('[getWalletService] Failed to load permissionsConfig from localStorage:', e)
}
// Prime the caches so getSnapshot functions never return null on first call
_walletSnapshot = _walletServiceInstance.getSnapshot()
_queueSnapshot = _walletServiceInstance.permissionQueue.getSnapshot()
Expand Down Expand Up @@ -251,12 +265,24 @@ export function useWalletService() {
const addBackupStorageUrl = useCallback((url: string) => svc.addBackupStorageUrl(url), [svc])
const removeBackupStorageUrl = useCallback((url: string) => svc.removeBackupStorageUrl(url), [svc])
const syncBackupStorage = useCallback((cb?: any) => svc.syncBackupStorage(cb), [svc])
const setPrimaryStorage = useCallback(
(target: string, cb?: (message: string) => void) => svc.setPrimaryStorage(target, cb),
[svc]
)
const updateMessageBoxUrl = useCallback((url: string) => svc.updateMessageBoxUrl(url), [svc])
const removeMessageBoxUrl = useCallback(() => svc.removeMessageBoxUrl(), [svc])
const updateSettings = useCallback((s: any) => svc.updateSettings(s), [svc])
const updatePermissionsConfig = useCallback(async (config: any) => {
svc.permissionQueue.permissionsConfig = config
localStorage.setItem('permissionsConfig', JSON.stringify(config))
// Persist first — if storage fails we don't want to silently update the
// in-memory config and have the change disappear on reload.
try {
localStorage.setItem('permissionsConfig', JSON.stringify(config))
} catch (e) {
console.error('[useWalletService] failed to persist permissionsConfig:', e)
throw e
}
// Apply to queue + live WalletPermissionsManager and re-emit snapshot.
svc.permissionQueue.setPermissionsConfig(config)
}, [svc])

const anointCurrentHost = useCallback(
Expand Down Expand Up @@ -326,6 +352,9 @@ export function useWalletService() {
// Managers
managers: walletState.managers,
updateManagers,
// Raw, unwrapped Wallet — for internal first-party use only. App-originated
// requests must go through managers.permissionsManager.
wallet: walletState.wallet,
// Settings
settings: walletState.settings,
updateSettings,
Expand Down Expand Up @@ -375,6 +404,7 @@ export function useWalletService() {
addBackupStorageUrl,
removeBackupStorageUrl,
syncBackupStorage,
setPrimaryStorage,
updateMessageBoxUrl,
removeMessageBoxUrl,
initializingBackendServices: walletState.initializingBackendServices,
Expand Down
Loading