synor/apps/desktop-wallet/src-tauri/src/lib.rs
Gulshan Yadav d81b5fe81b feat(desktop-wallet): add NFT support
Add complete NFT (non-fungible token) functionality:

Backend (Rust/Tauri):
- nft_create_collection: Deploy new NFT collection contract
- nft_mint, nft_batch_mint: Mint single or multiple NFTs
- nft_transfer: Transfer NFT ownership
- nft_burn: Permanently destroy NFT
- nft_list_owned: List all NFTs owned by address
- nft_get_collection_info, nft_get_token_info: Query metadata
- nft_set_approval_for_all, nft_set_base_uri: Collection management

Frontend (React/TypeScript):
- NFT Zustand store with collection tracking
- NftsDashboard page with 5 tabs:
  - Gallery: Visual grid of owned NFTs with modal details
  - Collections: Track and manage NFT collections
  - Create: Deploy new collection with royalties and soulbound options
  - Mint: Mint new NFTs with metadata URIs
  - Transfer: Send NFTs to other addresses
- Navigation sidebar updated with NFTs link
- Route added for /nfts path

Features:
- Royalty configuration in basis points (e.g., 250 = 2.5%)
- Soulbound token support (non-transferable)
- Batch minting up to 100 NFTs
- Collection import by contract address
- NFT burn with confirmation dialog
2026-02-02 09:23:07 +05:30

446 lines
15 KiB
Rust

//! 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 node;
mod rpc_client;
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<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<Menu<R>> {
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<R: Runtime>(app: &tauri::AppHandle<R>, 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<R: Runtime>(app: &tauri::AppHandle<R>, 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);
// Initialize node manager with app handle for events
let node_manager = std::sync::Arc::new(
node::NodeManager::with_app_handle(app.handle().clone())
);
// Initialize RPC client
let rpc_client = std::sync::Arc::new(
rpc_client::RpcClient::new(node_manager.clone())
);
// Initialize app state (node + RPC)
let app_state = commands::AppState {
node_manager,
rpc_client,
};
app.manage(app_state);
// Initialize mining state
let mining_state = commands::MiningState::new();
app.manage(mining_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 (legacy)
commands::connect_node,
commands::disconnect_node,
commands::get_network_status,
// Node management (new)
commands::node_connect_external,
commands::node_start_embedded,
commands::node_stop,
commands::node_get_status,
commands::node_get_connection_mode,
commands::node_get_peers,
commands::node_get_sync_progress,
// Mining
commands::mining_start,
commands::mining_stop,
commands::mining_pause,
commands::mining_resume,
commands::mining_get_status,
commands::mining_get_stats,
commands::mining_set_threads,
// Enhanced wallet (using RPC client)
commands::wallet_get_balance,
commands::wallet_get_utxos,
commands::wallet_get_network_info,
commands::wallet_get_fee_estimate,
// Smart contracts
commands::contract_deploy,
commands::contract_call,
commands::contract_read,
commands::contract_get_info,
// Tokens
commands::token_create,
commands::token_get_info,
commands::token_transfer,
commands::token_get_balance,
commands::token_list_balances,
commands::token_mint,
commands::token_burn,
// NFTs
commands::nft_create_collection,
commands::nft_get_collection_info,
commands::nft_mint,
commands::nft_batch_mint,
commands::nft_get_token_info,
commands::nft_transfer,
commands::nft_burn,
commands::nft_list_owned,
commands::nft_list_owned_in_collection,
commands::nft_set_approval_for_all,
commands::nft_set_base_uri,
// 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<R: Runtime>(app: tauri::AppHandle<R>) {
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<R: Runtime>(app: tauri::AppHandle<R>) -> std::result::Result<Option<serde_json::Value>, 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<R: Runtime>(app: tauri::AppHandle<R>) -> 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<bool, String> {
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<bool, String> {
// 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)
}