//! Synor Desktop Wallet - Tauri Backend //! //! Provides native functionality for the desktop wallet: //! - Secure key storage using OS keychain //! - Direct RPC communication with Synor nodes //! - Transaction signing with Dilithium3 post-quantum signatures //! - File system access for wallet backups //! - System tray for background operation //! - Auto-updates for seamless upgrades mod commands; mod crypto; mod error; mod keychain; mod wallet; use tauri::{ menu::{Menu, MenuItem}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, Emitter, Manager, Runtime, }; pub use error::{Error, Result}; /// Build the system tray menu fn build_tray_menu(app: &tauri::AppHandle) -> tauri::Result> { let show = MenuItem::with_id(app, "show", "Show Wallet", true, None::<&str>)?; let hide = MenuItem::with_id(app, "hide", "Hide to Tray", true, None::<&str>)?; let separator1 = MenuItem::with_id(app, "sep1", "─────────────", false, None::<&str>)?; let lock = MenuItem::with_id(app, "lock", "Lock Wallet", true, None::<&str>)?; let separator2 = MenuItem::with_id(app, "sep2", "─────────────", false, None::<&str>)?; let check_updates = MenuItem::with_id(app, "check_updates", "Check for Updates", true, None::<&str>)?; let separator3 = MenuItem::with_id(app, "sep3", "─────────────", false, None::<&str>)?; let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; Menu::with_items( app, &[ &show, &hide, &separator1, &lock, &separator2, &check_updates, &separator3, &quit, ], ) } /// Handle system tray menu events fn handle_tray_event(app: &tauri::AppHandle, event: TrayIconEvent) { match event { TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } => { // Left click: show and focus the window if let Some(window) = app.get_webview_window("main") { let _ = window.show(); let _ = window.set_focus(); } } _ => {} } } /// Handle tray menu item clicks fn handle_menu_event(app: &tauri::AppHandle, event: tauri::menu::MenuEvent) { match event.id().as_ref() { "show" => { if let Some(window) = app.get_webview_window("main") { let _ = window.show(); let _ = window.set_focus(); } } "hide" => { if let Some(window) = app.get_webview_window("main") { let _ = window.hide(); } } "lock" => { // Emit lock event to frontend let _ = app.emit("wallet-lock", ()); } "check_updates" => { // Emit update check event to frontend let _ = app.emit("check-updates", ()); } "quit" => { // Emit quit event to allow cleanup let _ = app.emit("app-quit", ()); std::process::exit(0); } _ => {} } } /// Initialize the Tauri application with all plugins and commands #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() // Core plugins .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_store::Builder::default().build()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_clipboard_manager::init()) // New plugins for desktop features .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_process::init()) .setup(|app| { // Initialize wallet state let wallet_state = wallet::WalletState::new(); app.manage(wallet_state); // Build and set up system tray let menu = build_tray_menu(app.handle())?; let _tray = TrayIconBuilder::new() .icon(app.default_window_icon().unwrap().clone()) .menu(&menu) .show_menu_on_left_click(false) .on_tray_icon_event(|tray, event| { handle_tray_event(tray.app_handle(), event); }) .on_menu_event(|app, event| { handle_menu_event(app, event); }) .tooltip("Synor Wallet") .build(app)?; // Check for updates on startup (non-blocking) let handle = app.handle().clone(); tauri::async_runtime::spawn(async move { check_for_updates(handle).await; }); #[cfg(debug_assertions)] { // Open devtools in development if let Some(window) = app.get_webview_window("main") { window.open_devtools(); } } Ok(()) }) .invoke_handler(tauri::generate_handler![ // Wallet management commands::create_wallet, commands::import_wallet, commands::unlock_wallet, commands::lock_wallet, commands::get_wallet_info, commands::export_mnemonic, // Addresses & UTXOs commands::get_addresses, commands::generate_address, commands::get_balance, commands::get_utxos, // Transactions commands::create_transaction, commands::sign_transaction, commands::broadcast_transaction, commands::get_transaction_history, // Network commands::connect_node, commands::disconnect_node, commands::get_network_status, // Updates check_update, install_update, // Keychain / Biometric keychain_is_available, keychain_enable_biometric, keychain_disable_biometric, keychain_is_biometric_enabled, keychain_unlock_with_biometric, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } /// Check for updates on startup async fn check_for_updates(app: tauri::AppHandle) { use tauri_plugin_updater::UpdaterExt; // Wait a few seconds before checking for updates tokio::time::sleep(std::time::Duration::from_secs(5)).await; let updater = match app.updater() { Ok(u) => u, Err(e) => { eprintln!("Updater not available: {}", e); return; } }; match updater.check().await { Ok(Some(update)) => { // Notify frontend about available update let _ = app.emit("update-available", serde_json::json!({ "version": update.version, "body": update.body, "date": update.date })); } Ok(None) => { // No update available } Err(e) => { eprintln!("Failed to check for updates: {}", e); } } } /// Tauri command: Check for updates #[tauri::command] async fn check_update(app: tauri::AppHandle) -> std::result::Result, String> { use tauri_plugin_updater::UpdaterExt; let updater = app.updater().map_err(|e| format!("Updater not configured: {}", e))?; match updater.check().await { Ok(Some(update)) => { Ok(Some(serde_json::json!({ "version": update.version, "body": update.body, "date": update.date }))) } Ok(None) => Ok(None), Err(e) => Err(format!("Update check failed: {}", e)), } } /// Tauri command: Install update #[tauri::command] async fn install_update(app: tauri::AppHandle) -> std::result::Result<(), String> { use tauri_plugin_updater::UpdaterExt; let updater = app.updater().map_err(|e| format!("Updater not configured: {}", e))?; match updater.check().await { Ok(Some(update)) => { // Download and install with progress callbacks let downloaded: u64 = 0; let total_size: u64 = 0; update.download_and_install( move |chunk_len, content_len| { // Progress callback - could emit progress events here let _ = (chunk_len, content_len, downloaded, total_size); }, || { // Ready to install callback }, ).await.map_err(|e| format!("Update installation failed: {}", e))?; // Notify user to restart let _ = app.emit("update-installed", ()); Ok(()) } Ok(None) => Err("No update available".to_string()), Err(e) => Err(format!("Update check failed: {}", e)), } } // ============================================================================ // Keychain / Biometric Commands // ============================================================================ /// Check if OS keychain is available on this system #[tauri::command] fn keychain_is_available() -> bool { keychain::KeychainService::is_available() } /// Enable biometric unlock for the current wallet #[tauri::command] async fn keychain_enable_biometric( password: String, state: tauri::State<'_, wallet::WalletState>, ) -> std::result::Result<(), String> { // Get wallet address to create keychain service let addresses = state.addresses.read().await; let first_address = addresses.first() .ok_or("No wallet loaded")? .address.clone(); drop(addresses); // Verify wallet is unlocked and password is correct if !state.is_unlocked().await { return Err("Wallet is locked".to_string()); } // Create keychain service and enable biometric let keychain_service = keychain::KeychainService::from_address(&first_address); keychain_service.enable_biometric_unlock(&password) .map_err(|e| e.to_string())?; Ok(()) } /// Disable biometric unlock for the current wallet #[tauri::command] async fn keychain_disable_biometric( state: tauri::State<'_, wallet::WalletState>, ) -> std::result::Result<(), String> { let addresses = state.addresses.read().await; let first_address = addresses.first() .ok_or("No wallet loaded")? .address.clone(); drop(addresses); let keychain_service = keychain::KeychainService::from_address(&first_address); keychain_service.disable_biometric_unlock() .map_err(|e| e.to_string())?; Ok(()) } /// Check if biometric unlock is enabled for the current wallet #[tauri::command] async fn keychain_is_biometric_enabled( state: tauri::State<'_, wallet::WalletState>, ) -> std::result::Result { let addresses = state.addresses.read().await; let first_address = match addresses.first() { Some(addr) => addr.address.clone(), None => return Ok(false), }; drop(addresses); let keychain_service = keychain::KeychainService::from_address(&first_address); Ok(keychain_service.is_biometric_enabled()) } /// Attempt to unlock wallet using biometric authentication /// The OS will prompt for TouchID/FaceID/Windows Hello #[tauri::command] async fn keychain_unlock_with_biometric( state: tauri::State<'_, wallet::WalletState>, ) -> std::result::Result { // Load wallet metadata if not loaded if state.metadata.read().await.is_none() { state.load_metadata().await.map_err(|e| e.to_string())?; } let addresses = state.addresses.read().await; let first_address = addresses.first() .ok_or("No wallet loaded")? .address.clone(); drop(addresses); let keychain_service = keychain::KeychainService::from_address(&first_address); // Check if biometric is enabled if !keychain_service.is_biometric_enabled() { return Err("Biometric unlock not enabled".to_string()); } // Retrieve the biometric key from OS keychain // This will trigger the OS biometric prompt (TouchID/FaceID/Windows Hello) let _biometric_key = keychain_service.get_biometric_unlock_key() .map_err(|e| format!("Biometric authentication failed: {}", e))?; // Biometric authentication succeeded // The frontend can now proceed with the unlock flow // In a full implementation, we would use the biometric_key to derive // the decryption key for the wallet Ok(true) }