import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { invoke } from '@tauri-apps/api/core'; /** * Sanitized error logging - prevents sensitive data from being logged * Only logs generic error types in production, full errors in development */ function logError(context: string, error: unknown): void { // In production, only log the context and error type, not the full message // which might contain sensitive information like addresses or amounts if (import.meta.env.PROD) { const errorType = error instanceof Error ? error.name : 'Unknown'; console.error(`[Wallet] ${context}: ${errorType}`); } else { // In development, log full error for debugging (but still sanitize) const safeError = error instanceof Error ? error.message : 'Unknown error'; // Remove any potential sensitive patterns (addresses, keys, etc.) const sanitized = safeError .replace(/synor1[a-z0-9]{38,}/gi, '[ADDRESS]') .replace(/tsynor1[a-z0-9]{38,}/gi, '[ADDRESS]') .replace(/[a-f0-9]{64}/gi, '[HASH]') .replace(/\b[a-z]{3,}\s+[a-z]{3,}\s+[a-z]{3,}/gi, '[POSSIBLE_MNEMONIC]'); console.error(`[Wallet] ${context}:`, sanitized); } } export interface WalletAddress { address: string; index: number; isChange: boolean; label?: string; } export interface Balance { balance: number; balanceHuman: string; pending: number; } export interface NetworkStatus { connected: boolean; network?: string; blockHeight?: number; peerCount?: number; synced?: boolean; } interface WalletState { // State isInitialized: boolean; isUnlocked: boolean; addresses: WalletAddress[]; balance: Balance | null; networkStatus: NetworkStatus; // Actions setInitialized: (initialized: boolean) => void; setUnlocked: (unlocked: boolean) => void; setAddresses: (addresses: WalletAddress[]) => void; setBalance: (balance: Balance | null) => void; setNetworkStatus: (status: NetworkStatus) => void; // Async actions (invoke Tauri commands) createWallet: (password: string) => Promise<{ mnemonic: string; address: string }>; importWallet: (mnemonic: string, password: string) => Promise; unlockWallet: (password: string) => Promise; lockWallet: () => Promise; refreshBalance: () => Promise; refreshAddresses: () => Promise; connectNode: (rpcUrl: string, wsUrl?: string) => Promise; } export const useWalletStore = create()( persist( (set, get) => ({ // Initial state isInitialized: false, isUnlocked: false, addresses: [], balance: null, networkStatus: { connected: false }, // Sync setters setInitialized: (initialized) => set({ isInitialized: initialized }), setUnlocked: (unlocked) => set({ isUnlocked: unlocked }), setAddresses: (addresses) => set({ addresses }), setBalance: (balance) => set({ balance }), setNetworkStatus: (status) => set({ networkStatus: status }), // Async actions createWallet: async (password: string) => { const result = await invoke<{ mnemonic: string; address: string }>( 'create_wallet', { password } ); set({ isInitialized: true, isUnlocked: true, addresses: [{ address: result.address, index: 0, isChange: false }], }); return result; }, importWallet: async (mnemonic: string, password: string) => { const address = await invoke('import_wallet', { request: { mnemonic, password }, }); set({ isInitialized: true, isUnlocked: true, addresses: [{ address, index: 0, isChange: false }], }); return address; }, unlockWallet: async (password: string) => { const success = await invoke('unlock_wallet', { password }); if (success) { set({ isUnlocked: true }); // Refresh data after unlock get().refreshBalance(); get().refreshAddresses(); } return success; }, lockWallet: async () => { await invoke('lock_wallet'); set({ isUnlocked: false, balance: null, }); }, refreshBalance: async () => { try { const balance = await invoke('get_balance'); set({ balance }); } catch (error) { logError('refreshBalance', error); } }, refreshAddresses: async () => { try { const addresses = await invoke('get_addresses'); set({ addresses }); } catch (error) { logError('refreshAddresses', error); } }, connectNode: async (rpcUrl: string, wsUrl?: string) => { try { const connection = await invoke('connect_node', { rpcUrl, wsUrl, }); set({ networkStatus: { ...connection, connected: true } }); } catch (error) { logError('connectNode', error); set({ networkStatus: { connected: false } }); } }, }), { name: 'synor-wallet-storage', partialize: (state) => ({ isInitialized: state.isInitialized, // Don't persist sensitive state like isUnlocked }), } ) );