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
375 lines
10 KiB
Rust
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,
|
|
})
|
|
}
|
|
}
|