synor/apps/desktop-wallet/src-tauri/src/commands.rs
Gulshan Yadav 6b5a232a5e feat: Desktop wallet, gas estimator UI, and 30-day monitoring stack
Security (Desktop Wallet):
- Implement BIP39 mnemonic generation with cryptographic RNG
- Add Argon2id password-based key derivation (64MB, 3 iterations)
- Add ChaCha20-Poly1305 authenticated encryption for seed storage
- Add mnemonic auto-clear (60s timeout) and clipboard auto-clear (30s)
- Add sanitized error logging to prevent credential leaks
- Strengthen CSP with object-src, base-uri, form-action, frame-ancestors
- Clear sensitive state on component unmount

Explorer (Gas Estimator):
- Add Gas Estimation page with from/to/amount/data inputs
- Add bech32 address validation (synor1/tsynor1 prefix)
- Add BigInt-based amount parsing to avoid floating point errors
- Add production guard for mock mode (cannot enable in prod builds)

Monitoring (30-day Testnet):
- Add Prometheus config with 30-day retention
- Add comprehensive alert rules for node health, consensus, network, mempool
- Add Alertmanager with severity-based routing and inhibition rules
- Add Grafana with auto-provisioned datasource and dashboard
- Add Synor testnet dashboard with uptime SLA tracking

Docker:
- Update docker-compose.testnet.yml with monitoring profile
- Fix node-exporter for macOS Docker Desktop compatibility
- Change Grafana port to 3001 to avoid conflict
2026-01-10 04:38:09 +05:30

375 lines
10 KiB
Rust

//! Tauri commands for the desktop wallet
//!
//! All commands are async and return Result<T, Error> which Tauri
//! serializes as { ok: T } or { error: string } to the frontend.
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Manager, State};
use crate::wallet::{WalletState, WalletAddress, NetworkConnection};
use crate::{Error, Result};
// ============================================================================
// Wallet Management Commands
// ============================================================================
/// Response from wallet creation
#[derive(Debug, Serialize)]
pub struct CreateWalletResponse {
pub mnemonic: String,
pub address: String,
}
/// Create a new wallet with a random mnemonic
#[tauri::command]
pub async fn create_wallet(
app: AppHandle,
state: State<'_, WalletState>,
password: String,
) -> Result<CreateWalletResponse> {
// Validate password strength
if password.len() < 8 {
return Err(Error::Crypto("Password must be at least 8 characters".to_string()));
}
// Set up data directory
let app_data_dir = app.path().app_data_dir()
.map_err(|e| Error::Internal(format!("Failed to get app data dir: {}", e)))?;
state.set_data_dir(app_data_dir).await?;
// Check if wallet already exists
if state.wallet_exists().await {
return Err(Error::Internal("Wallet already exists. Import or unlock instead.".to_string()));
}
// Create wallet with encryption (testnet by default for safety)
let (mnemonic, address) = state.create(&password, true).await?;
Ok(CreateWalletResponse { mnemonic, address })
}
/// Import request
#[derive(Debug, Deserialize)]
pub struct ImportWalletRequest {
pub mnemonic: String,
pub password: String,
}
/// Import a wallet from mnemonic phrase
#[tauri::command]
pub async fn import_wallet(
app: AppHandle,
state: State<'_, WalletState>,
request: ImportWalletRequest,
) -> Result<String> {
// Validate password strength
if request.password.len() < 8 {
return Err(Error::Crypto("Password must be at least 8 characters".to_string()));
}
// Set up data directory
let app_data_dir = app.path().app_data_dir()
.map_err(|e| Error::Internal(format!("Failed to get app data dir: {}", e)))?;
state.set_data_dir(app_data_dir).await?;
// Import wallet with encryption (testnet by default for safety)
let address = state.import(&request.mnemonic, &request.password, true).await?;
Ok(address)
}
/// Unlock an existing wallet
#[tauri::command]
pub async fn unlock_wallet(
app: AppHandle,
state: State<'_, WalletState>,
password: String,
) -> Result<bool> {
// Set up data directory
let app_data_dir = app.path().app_data_dir()
.map_err(|e| Error::Internal(format!("Failed to get app data dir: {}", e)))?;
state.set_data_dir(app_data_dir).await?;
// Load wallet metadata from file
state.load_metadata().await?;
// Decrypt and unlock
state.unlock(&password).await?;
Ok(true)
}
/// Lock the wallet (clear sensitive data from memory)
#[tauri::command]
pub async fn lock_wallet(state: State<'_, WalletState>) -> Result<()> {
state.lock().await;
Ok(())
}
/// Wallet info returned to frontend
#[derive(Debug, Serialize)]
pub struct WalletInfo {
pub locked: bool,
pub address_count: usize,
pub network: Option<String>,
}
/// Get wallet information
#[tauri::command]
pub async fn get_wallet_info(state: State<'_, WalletState>) -> Result<WalletInfo> {
let locked = !state.is_unlocked().await;
let addresses = state.addresses.read().await;
let connection = state.connection.read().await;
Ok(WalletInfo {
locked,
address_count: addresses.len(),
network: connection.as_ref().map(|c| c.network.clone()),
})
}
/// Export the mnemonic phrase (requires unlock)
#[tauri::command]
pub async fn export_mnemonic(
state: State<'_, WalletState>,
password: String,
) -> Result<String> {
if !state.is_unlocked().await {
return Err(Error::WalletLocked);
}
// TODO: Re-verify password and return mnemonic
Err(Error::Internal("Not implemented".to_string()))
}
// ============================================================================
// Address & Balance Commands
// ============================================================================
/// Get all addresses in the wallet
#[tauri::command]
pub async fn get_addresses(state: State<'_, WalletState>) -> Result<Vec<WalletAddress>> {
let addresses = state.addresses.read().await;
Ok(addresses.clone())
}
/// Generate a new receive address
#[tauri::command]
pub async fn generate_address(
state: State<'_, WalletState>,
label: Option<String>,
) -> Result<WalletAddress> {
// Use wallet state's generate_address which handles crypto properly
state.generate_address(label, false).await
}
/// Balance response
#[derive(Debug, Serialize)]
pub struct BalanceResponse {
/// Balance in sompi (1 SYN = 100_000_000 sompi)
pub balance: u64,
/// Human-readable balance
pub balance_human: String,
/// Pending incoming
pub pending: u64,
}
/// Get wallet balance
#[tauri::command]
pub async fn get_balance(state: State<'_, WalletState>) -> Result<BalanceResponse> {
// TODO: Query node for UTXOs and sum balance
Ok(BalanceResponse {
balance: 0,
balance_human: "0 SYN".to_string(),
pending: 0,
})
}
/// UTXO response
#[derive(Debug, Serialize)]
pub struct UtxoResponse {
pub txid: String,
pub vout: u32,
pub amount: u64,
pub confirmations: u64,
}
/// Get UTXOs for the wallet
#[tauri::command]
pub async fn get_utxos(state: State<'_, WalletState>) -> Result<Vec<UtxoResponse>> {
// TODO: Query node for UTXOs
Ok(vec![])
}
// ============================================================================
// Transaction Commands
// ============================================================================
/// Transaction creation request
#[derive(Debug, Deserialize)]
pub struct CreateTransactionRequest {
pub to: String,
pub amount: u64,
pub fee: Option<u64>,
pub use_dilithium: bool,
}
/// Unsigned transaction response
#[derive(Debug, Serialize)]
pub struct UnsignedTransaction {
pub tx_hex: String,
pub fee: u64,
pub inputs: Vec<UtxoResponse>,
}
/// Create an unsigned transaction
#[tauri::command]
pub async fn create_transaction(
state: State<'_, WalletState>,
request: CreateTransactionRequest,
) -> Result<UnsignedTransaction> {
if !state.is_unlocked().await {
return Err(Error::WalletLocked);
}
// TODO: Select UTXOs, build transaction
Err(Error::Internal("Not implemented".to_string()))
}
/// Signed transaction response
#[derive(Debug, Serialize)]
pub struct SignedTransaction {
pub tx_hex: String,
pub txid: String,
}
/// Sign a transaction
#[tauri::command]
pub async fn sign_transaction(
state: State<'_, WalletState>,
tx_hex: String,
) -> Result<SignedTransaction> {
if !state.is_unlocked().await {
return Err(Error::WalletLocked);
}
// TODO: Sign transaction with appropriate key
Err(Error::Internal("Not implemented".to_string()))
}
/// Broadcast response
#[derive(Debug, Serialize)]
pub struct BroadcastResponse {
pub txid: String,
pub accepted: bool,
}
/// Broadcast a signed transaction
#[tauri::command]
pub async fn broadcast_transaction(
state: State<'_, WalletState>,
tx_hex: String,
) -> Result<BroadcastResponse> {
let connection = state.connection.read().await;
if connection.is_none() {
return Err(Error::Network("Not connected to node".to_string()));
}
// TODO: Submit transaction via RPC
Err(Error::Internal("Not implemented".to_string()))
}
/// Transaction history entry
#[derive(Debug, Serialize)]
pub struct TransactionHistoryEntry {
pub txid: String,
pub direction: String, // "sent" or "received"
pub amount: u64,
pub fee: Option<u64>,
pub timestamp: i64,
pub confirmations: u64,
pub counterparty: Option<String>,
}
/// Get transaction history
#[tauri::command]
pub async fn get_transaction_history(
state: State<'_, WalletState>,
limit: Option<usize>,
) -> Result<Vec<TransactionHistoryEntry>> {
// TODO: Query indexed transactions
Ok(vec![])
}
// ============================================================================
// Network Commands
// ============================================================================
/// Connect to a Synor node
#[tauri::command]
pub async fn connect_node(
state: State<'_, WalletState>,
rpc_url: String,
ws_url: Option<String>,
) -> Result<NetworkConnection> {
// TODO: Test connection, get network info
let network = if rpc_url.contains("testnet") || rpc_url.contains("17110") {
"testnet"
} else {
"mainnet"
};
let connection = NetworkConnection {
rpc_url,
ws_url,
connected: true,
network: network.to_string(),
};
let mut conn = state.connection.write().await;
*conn = Some(connection.clone());
Ok(connection)
}
/// Disconnect from the current node
#[tauri::command]
pub async fn disconnect_node(state: State<'_, WalletState>) -> Result<()> {
let mut connection = state.connection.write().await;
*connection = None;
Ok(())
}
/// Network status
#[derive(Debug, Serialize)]
pub struct NetworkStatus {
pub connected: bool,
pub network: Option<String>,
pub block_height: Option<u64>,
pub peer_count: Option<u32>,
pub synced: Option<bool>,
}
/// Get network status
#[tauri::command]
pub async fn get_network_status(state: State<'_, WalletState>) -> Result<NetworkStatus> {
let connection = state.connection.read().await;
if let Some(conn) = connection.as_ref() {
// TODO: Query node for actual status
Ok(NetworkStatus {
connected: conn.connected,
network: Some(conn.network.clone()),
block_height: None,
peer_count: None,
synced: None,
})
} else {
Ok(NetworkStatus {
connected: false,
network: None,
block_height: None,
peer_count: None,
synced: None,
})
}
}