diff --git a/monero-rpc-pool/.sqlx/query-132666c849bf0db14e50ef41f429e17b7c1afd21031edf3af40fadfb79ef2597.json b/monero-rpc-pool/.sqlx/query-d32d91ca2debc4212841282533482b2ff081234c7f9f848a7223ae04234995d9.json similarity index 61% rename from monero-rpc-pool/.sqlx/query-132666c849bf0db14e50ef41f429e17b7c1afd21031edf3af40fadfb79ef2597.json rename to monero-rpc-pool/.sqlx/query-d32d91ca2debc4212841282533482b2ff081234c7f9f848a7223ae04234995d9.json index dbc4e29b..b7cd990e 100644 --- a/monero-rpc-pool/.sqlx/query-132666c849bf0db14e50ef41f429e17b7c1afd21031edf3af40fadfb79ef2597.json +++ b/monero-rpc-pool/.sqlx/query-d32d91ca2debc4212841282533482b2ff081234c7f9f848a7223ae04234995d9.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT \n CAST(SUM(CASE WHEN hc.was_successful THEN 1 ELSE 0 END) AS INTEGER) as \"successful!: i64\",\n CAST(SUM(CASE WHEN NOT hc.was_successful THEN 1 ELSE 0 END) AS INTEGER) as \"unsuccessful!: i64\"\n FROM health_checks hc\n JOIN monero_nodes n ON hc.node_id = n.id\n WHERE n.network = ?\n ", + "query": "\n SELECT \n CAST(SUM(CASE WHEN hc.was_successful THEN 1 ELSE 0 END) AS INTEGER) as \"successful!: i64\",\n CAST(SUM(CASE WHEN NOT hc.was_successful THEN 1 ELSE 0 END) AS INTEGER) as \"unsuccessful!: i64\"\n FROM (\n SELECT hc.was_successful\n FROM health_checks hc\n JOIN monero_nodes n ON hc.node_id = n.id\n WHERE n.network = ?\n ORDER BY hc.timestamp DESC\n LIMIT 100\n ) hc\n ", "describe": { "columns": [ { @@ -19,5 +19,5 @@ }, "nullable": [true, true] }, - "hash": "132666c849bf0db14e50ef41f429e17b7c1afd21031edf3af40fadfb79ef2597" + "hash": "d32d91ca2debc4212841282533482b2ff081234c7f9f848a7223ae04234995d9" } diff --git a/monero-rpc-pool/src/database.rs b/monero-rpc-pool/src/database.rs index 5c21ecb3..630d689f 100644 --- a/monero-rpc-pool/src/database.rs +++ b/monero-rpc-pool/src/database.rs @@ -481,9 +481,14 @@ impl Database { SELECT CAST(SUM(CASE WHEN hc.was_successful THEN 1 ELSE 0 END) AS INTEGER) as "successful!: i64", CAST(SUM(CASE WHEN NOT hc.was_successful THEN 1 ELSE 0 END) AS INTEGER) as "unsuccessful!: i64" - FROM health_checks hc - JOIN monero_nodes n ON hc.node_id = n.id - WHERE n.network = ? + FROM ( + SELECT hc.was_successful + FROM health_checks hc + JOIN monero_nodes n ON hc.node_id = n.id + WHERE n.network = ? + ORDER BY hc.timestamp DESC + LIMIT 100 + ) hc "#, network ) diff --git a/monero-rpc-pool/src/lib.rs b/monero-rpc-pool/src/lib.rs index 197da8b5..f03dd1f3 100644 --- a/monero-rpc-pool/src/lib.rs +++ b/monero-rpc-pool/src/lib.rs @@ -37,12 +37,12 @@ pub struct AppState { } /// Manages background tasks for the RPC pool -pub struct TaskManager { +pub struct PoolHandle { pub status_update_handle: JoinHandle<()>, pub discovery_handle: JoinHandle<()>, } -impl Drop for TaskManager { +impl Drop for PoolHandle { fn drop(&mut self) { self.status_update_handle.abort(); self.discovery_handle.abort(); @@ -62,7 +62,7 @@ async fn create_app_with_receiver( ) -> Result<( Router, tokio::sync::broadcast::Receiver, - TaskManager, + PoolHandle, )> { // Initialize database let db = Database::new_with_data_dir(config.data_dir.clone()).await?; @@ -75,17 +75,27 @@ async fn create_app_with_receiver( // Initialize discovery service let discovery = NodeDiscovery::new(db.clone())?; + // Publish initial status immediately to ensure first event is sent + { + let pool_guard = node_pool.read().await; + if let Err(e) = pool_guard.publish_status_update().await { + error!("Failed to publish initial status update: {}", e); + } + } + // Start background tasks let node_pool_for_health_check = node_pool.clone(); let status_update_handle = tokio::spawn(async move { + let mut interval = tokio::time::interval(std::time::Duration::from_secs(10)); + loop { - // Publish status update after health check + interval.tick().await; + + // Publish status update let pool_guard = node_pool_for_health_check.read().await; if let Err(e) = pool_guard.publish_status_update().await { - error!("Failed to publish status update after health check: {}", e); + error!("Failed to publish status update: {}", e); } - - tokio::time::sleep(std::time::Duration::from_secs(10)).await; } }); @@ -102,7 +112,7 @@ async fn create_app_with_receiver( } }); - let task_manager = TaskManager { + let pool_handle = PoolHandle { status_update_handle, discovery_handle, }; @@ -116,12 +126,12 @@ async fn create_app_with_receiver( .layer(CorsLayer::permissive()) .with_state(app_state); - Ok((app, status_receiver, task_manager)) + Ok((app, status_receiver, pool_handle)) } pub async fn create_app(config: Config, network: Network) -> Result { - let (app, _, _task_manager) = create_app_with_receiver(config, network).await?; - // Note: task_manager is dropped here, so tasks will be aborted when this function returns + let (app, _, _pool_handle) = create_app_with_receiver(config, network).await?; + // Note: pool_handle is dropped here, so tasks will be aborted when this function returns // This is intentional for the simple create_app use case Ok(app) } @@ -160,14 +170,14 @@ pub async fn run_server_with_data_dir( } /// Start a server with a random port for library usage -/// Returns the server info with the actual port used, a receiver for pool status updates, and task manager +/// Returns the server info with the actual port used, a receiver for pool status updates, and pool handle pub async fn start_server_with_random_port( config: Config, network: Network, ) -> Result<( ServerInfo, tokio::sync::broadcast::Receiver, - TaskManager, + PoolHandle, )> { // Clone the host before moving config let host = config.host.clone(); @@ -175,7 +185,7 @@ pub async fn start_server_with_random_port( // If port is 0, the system will assign a random available port let config_with_random_port = Config::new_random_port(config.host, config.data_dir); - let (app, status_receiver, task_manager) = + let (app, status_receiver, pool_handle) = create_app_with_receiver(config_with_random_port, network).await?; // Bind to port 0 to get a random available port @@ -199,11 +209,11 @@ pub async fn start_server_with_random_port( } }); - Ok((server_info, status_receiver, task_manager)) + Ok((server_info, status_receiver, pool_handle)) } /// Start a server with a random port and custom data directory for library usage -/// Returns the server info with the actual port used, a receiver for pool status updates, and task manager +/// Returns the server info with the actual port used, a receiver for pool status updates, and pool handle pub async fn start_server_with_random_port_and_data_dir( config: Config, network: Network, @@ -211,7 +221,7 @@ pub async fn start_server_with_random_port_and_data_dir( ) -> Result<( ServerInfo, tokio::sync::broadcast::Receiver, - TaskManager, + PoolHandle, )> { let config_with_data_dir = Config::new_random_port(config.host, data_dir); start_server_with_random_port(config_with_data_dir, network).await diff --git a/src-gui/src/renderer/components/pages/help/MoneroPoolHealthBox.tsx b/src-gui/src/renderer/components/pages/help/MoneroPoolHealthBox.tsx index 4f187534..b2f692b4 100644 --- a/src-gui/src/renderer/components/pages/help/MoneroPoolHealthBox.tsx +++ b/src-gui/src/renderer/components/pages/help/MoneroPoolHealthBox.tsx @@ -64,7 +64,7 @@ export default function MoneroPoolHealthBox() { size="small" /> 80 ? "success" @@ -182,25 +182,6 @@ export default function MoneroPoolHealthBox() { additionalContent={ {poolStatus && renderHealthSummary()} - - {poolStatus && ( - - - Health Check Statistics - - - - Successful:{" "} - {poolStatus.successful_health_checks.toLocaleString()} - - - Failed:{" "} - {poolStatus.unsuccessful_health_checks.toLocaleString()} - - - - )} - {renderTopNodes()} } diff --git a/src-gui/src/renderer/rpc.ts b/src-gui/src/renderer/rpc.ts index fe90ce66..56e2f476 100644 --- a/src-gui/src/renderer/rpc.ts +++ b/src-gui/src/renderer/rpc.ts @@ -223,21 +223,30 @@ export async function initializeContext() { const bitcoinNodes = store.getState().settings.nodes[network][Blockchain.Bitcoin]; - // For Monero nodes, get the configured node URL and pool setting + // For Monero nodes, determine whether to use pool or custom node const useMoneroRpcPool = store.getState().settings.useMoneroRpcPool; - const moneroNodes = - store.getState().settings.nodes[network][Blockchain.Monero]; - // Always pass the first configured monero node URL directly without checking availability - // The backend will handle whether to use the pool or the custom node - const moneroNode = moneroNodes.length > 0 ? moneroNodes[0] : null; + const moneroNodeUrl = + store.getState().settings.nodes[network][Blockchain.Monero][0] ?? null; + + // Check the state of the Monero node + const isMoneroNodeOnline = await getMoneroNodeStatus(moneroNodeUrl, network); + + const moneroNodeConfig = + useMoneroRpcPool || moneroNodeUrl == null || !isMoneroNodeOnline + ? { type: "Pool" as const } + : { + type: "SingleNode" as const, + content: { + url: moneroNodeUrl, + }, + }; // Initialize Tauri settings const tauriSettings: TauriSettings = { electrum_rpc_urls: bitcoinNodes, - monero_node_url: moneroNode, + monero_node_config: moneroNodeConfig, use_tor: useTor, - use_monero_rpc_pool: useMoneroRpcPool, }; logger.info("Initializing context with settings", tauriSettings); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index fb92f17c..f54ee36e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -17,7 +17,7 @@ use swap::cli::{ tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings}, Context, ContextBuilder, }, - command::{Bitcoin, Monero}, + command::Bitcoin, }; use tauri::{async_runtime::RwLock, Manager, RunEvent}; use tauri_plugin_dialog::DialogExt; @@ -367,48 +367,6 @@ async fn initialize_context( .context("Context is already being initialized") .to_string_result()?; - // Determine which Monero node to use: - // - If using RPC pool, start and use the local RPC pool - // - Otherwise, use the provided node URL directly (even if empty) - let monero_node_url = if settings.use_monero_rpc_pool { - // Start RPC pool and use it - let data_dir = data::data_dir_from(None, testnet).to_string_result()?; - match monero_rpc_pool::start_server_with_random_port( - monero_rpc_pool::config::Config::new_random_port( - "127.0.0.1".to_string(), - data_dir.join("monero-rpc-pool"), - ), - match testnet { - true => swap::monero::Network::Stagenet, - false => swap::monero::Network::Mainnet, - }, - ) - .await - { - Ok((server_info, mut status_receiver, _task_manager)) => { - let rpc_url = format!("http://{}:{}", server_info.host, server_info.port); - tracing::info!("Monero RPC Pool started on {}", rpc_url); - - // Start listening for pool status updates and forward them to frontend - let pool_tauri_handle = TauriHandle::new(app_handle.clone()); - tauri::async_runtime::spawn(async move { - while let Ok(status) = status_receiver.recv().await { - pool_tauri_handle.emit_pool_status_update(status); - } - }); - - rpc_url.parse().ok() - } - Err(e) => { - tracing::error!("Failed to start Monero RPC Pool: {}", e); - None - } - } - } else { - // Use the provided node URL directly without checking availability - settings.monero_node_url.clone() - }; - // Get app handle and create a Tauri handle let tauri_handle = TauriHandle::new(app_handle.clone()); @@ -420,9 +378,7 @@ async fn initialize_context( bitcoin_electrum_rpc_urls: settings.electrum_rpc_urls.clone(), bitcoin_target_block: None, }) - .with_monero(Monero { - monero_node_address: monero_node_url, - }) + .with_monero(settings.monero_node_config) .with_json(false) .with_debug(true) .with_tor(settings.use_tor) diff --git a/swap/src/bin/asb.rs b/swap/src/bin/asb.rs index 38c7a84d..dc13478f 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -483,7 +483,7 @@ async fn init_monero_wallet( // Start the monero-rpc-pool and use it tracing::info!("Starting Monero RPC Pool for ASB"); - let (server_info, _status_receiver, _task_manager) = + let (server_info, _status_receiver, _pool_handle) = monero_rpc_pool::start_server_with_random_port( monero_rpc_pool::config::Config::new_random_port( "127.0.0.1".to_string(), diff --git a/swap/src/cli/api.rs b/swap/src/cli/api.rs index 47521569..8740611f 100644 --- a/swap/src/cli/api.rs +++ b/swap/src/cli/api.rs @@ -20,7 +20,9 @@ use std::fmt; use std::future::Future; use std::path::{Path, PathBuf}; use std::sync::{Arc, Once}; -use tauri_bindings::{TauriBackgroundProgress, TauriContextStatusEvent, TauriEmitter, TauriHandle}; +use tauri_bindings::{ + MoneroNodeConfig, TauriBackgroundProgress, TauriContextStatusEvent, TauriEmitter, TauriHandle, +}; use tokio::sync::{broadcast, broadcast::Sender, Mutex as TokioMutex, RwLock}; use tokio::task::JoinHandle; use tor_rtcompat::tokio::TokioRustlsRuntime; @@ -188,12 +190,13 @@ pub struct Context { bitcoin_wallet: Option>, monero_manager: Option>, tor_client: Option>>, + monero_rpc_pool_handle: Option>, } /// A conveniant builder struct for [`Context`]. #[must_use = "ContextBuilder must be built to be useful"] pub struct ContextBuilder { - monero: Option, + monero_config: Option, bitcoin: Option, data: Option, is_testnet: bool, @@ -216,7 +219,7 @@ impl ContextBuilder { /// Basic builder with default options for mainnet pub fn mainnet() -> Self { ContextBuilder { - monero: None, + monero_config: None, bitcoin: None, data: None, is_testnet: false, @@ -235,8 +238,8 @@ impl ContextBuilder { } /// Configures the Context to initialize a Monero wallet with the given configuration. - pub fn with_monero(mut self, monero: impl Into>) -> Self { - self.monero = monero.into(); + pub fn with_monero(mut self, monero_config: impl Into>) -> Self { + self.monero_config = monero_config.into(); self } @@ -247,8 +250,8 @@ impl ContextBuilder { } /// Attach a handle to Tauri to the Context for emitting events etc. - pub fn with_tauri(mut self, tauri: impl Into>) -> Self { - self.tauri_handle = tauri.into(); + pub fn with_tauri(mut self, tauri_handle: impl Into>) -> Self { + self.tauri_handle = tauri_handle.into(); self } @@ -364,17 +367,61 @@ impl ContextBuilder { }; let initialize_monero_wallet = async { - match self.monero { - Some(monero) => { + match self.monero_config { + Some(monero_config) => { let monero_progress_handle = tauri_handle .new_background_process_with_initial_progress( TauriBackgroundProgress::OpeningMoneroWallet, (), ); + // Handle the different monero configurations + let (monero_node_address, rpc_pool_handle) = match monero_config { + MoneroNodeConfig::Pool => { + // Start RPC pool and use it + match monero_rpc_pool::start_server_with_random_port( + monero_rpc_pool::config::Config::new_random_port( + "127.0.0.1".to_string(), + data_dir.join("monero-rpc-pool"), + ), + match self.is_testnet { + true => crate::monero::Network::Stagenet, + false => crate::monero::Network::Mainnet, + }, + ) + .await + { + Ok((server_info, mut status_receiver, pool_handle)) => { + let rpc_url = + format!("http://{}:{}", server_info.host, server_info.port); + tracing::info!("Monero RPC Pool started on {}", rpc_url); + + // Start listening for pool status updates and forward them to frontend + if let Some(ref handle) = self.tauri_handle { + let pool_tauri_handle = handle.clone(); + tokio::spawn(async move { + while let Ok(status) = status_receiver.recv().await { + pool_tauri_handle.emit_pool_status_update(status); + } + }); + } + + (Some(rpc_url), Some(Arc::new(pool_handle))) + } + Err(e) => { + tracing::error!("Failed to start Monero RPC Pool: {}", e); + (None, None) + } + } + } + MoneroNodeConfig::SingleNode { url } => { + (if url.is_empty() { None } else { Some(url) }, None) + } + }; + let wallets = init_monero_wallet( data_dir.as_path(), - monero.monero_node_address.map(|url| url.to_string()), + monero_node_address, env_config, tauri_handle.clone(), ) @@ -382,9 +429,9 @@ impl ContextBuilder { monero_progress_handle.finish(); - Ok(Some(wallets)) + Ok((Some(wallets), rpc_pool_handle)) } - None => Ok(None), + None => Ok((None, None)), } }; @@ -405,7 +452,7 @@ impl ContextBuilder { Ok(maybe_tor_client) }; - let (bitcoin_wallet, monero_manager, tor) = tokio::try_join!( + let (bitcoin_wallet, (monero_manager, monero_rpc_pool_handle), tor) = tokio::try_join!( initialize_bitcoin_wallet, initialize_monero_wallet, initialize_tor_client, @@ -443,6 +490,7 @@ impl ContextBuilder { tasks, tauri_handle: self.tauri_handle, tor_client: tor, + monero_rpc_pool_handle, }; Ok(context) @@ -476,6 +524,7 @@ impl Context { tasks: PendingTaskList::default().into(), tauri_handle: None, tor_client: None, + monero_rpc_pool_handle: None, } } @@ -637,6 +686,23 @@ impl Config { } } +impl From for MoneroNodeConfig { + fn from(monero: Monero) -> Self { + match monero.monero_node_address { + Some(url) => MoneroNodeConfig::SingleNode { + url: url.to_string(), + }, + None => MoneroNodeConfig::Pool, + } + } +} + +impl From for Option { + fn from(monero: Monero) -> Self { + Some(MoneroNodeConfig::from(monero)) + } +} + #[cfg(test)] pub mod api_test { use super::*; diff --git a/swap/src/cli/api/tauri_bindings.rs b/swap/src/cli/api/tauri_bindings.rs index c7da6882..5ac39497 100644 --- a/swap/src/cli/api/tauri_bindings.rs +++ b/swap/src/cli/api/tauri_bindings.rs @@ -13,7 +13,6 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use strum::Display; use tokio::sync::{oneshot, Mutex as TokioMutex}; use typeshare::typeshare; -use url::Url; use uuid::Uuid; #[typeshare] @@ -703,19 +702,24 @@ pub enum BackgroundRefundState { Completed, } +#[typeshare] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "type", content = "content")] +pub enum MoneroNodeConfig { + Pool, + SingleNode { url: String }, +} + /// This struct contains the settings for the Context #[typeshare] #[derive(Debug, Serialize, Deserialize, Clone)] pub struct TauriSettings { - /// The URL of the Monero node e.g `http://xmr.node:18081` - #[typeshare(serialized_as = "Option")] - pub monero_node_url: Option, + /// Configuration for Monero node connection + pub monero_node_config: MoneroNodeConfig, /// The URLs of the Electrum RPC servers e.g `["ssl://bitcoin.com:50001", "ssl://backup.com:50001"]` pub electrum_rpc_urls: Vec, /// Whether to initialize and use a tor client. pub use_tor: bool, - /// Whether to use the Monero RPC pool instead of custom nodes. - pub use_monero_rpc_pool: bool, } #[typeshare]