pub mod request; pub mod tauri_bindings; use crate::cli::api::tauri_bindings::SeedChoice; use crate::cli::command::{Bitcoin, Monero}; use crate::common::tor::{bootstrap_tor_client, create_tor_client}; use crate::common::tracing_util::Format; use crate::database::{open_db, AccessMode}; use crate::network::rendezvous::XmrBtcNamespace; use crate::protocol::Database; use crate::seed::Seed; use crate::{bitcoin, common, monero}; use anyhow::{bail, Context as AnyContext, Error, Result}; use arti_client::TorClient; use futures::future::try_join_all; use std::fmt; use std::future::Future; use std::path::{Path, PathBuf}; use std::sync::{Arc, Once}; use swap_env::env::{Config as EnvConfig, GetConfig, Mainnet, Testnet}; use swap_fs::system_data_dir; 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; use tracing::level_filters::LevelFilter; use tracing::Level; use uuid::Uuid; use super::watcher::Watcher; static START: Once = Once::new(); #[derive(Clone, PartialEq, Debug)] pub struct Config { namespace: XmrBtcNamespace, pub env_config: EnvConfig, seed: Option, debug: bool, json: bool, log_dir: PathBuf, data_dir: PathBuf, is_testnet: bool, } #[derive(Default)] pub struct PendingTaskList(TokioMutex>>); impl PendingTaskList { pub async fn spawn(&self, future: F) where F: Future + Send + 'static, T: Send + 'static, { let handle = tokio::spawn(async move { let _ = future.await; }); self.0.lock().await.push(handle); } pub async fn wait_for_tasks(&self) -> Result<()> { let tasks = { // Scope for the lock, to avoid holding it for the entire duration of the async block let mut guard = self.0.lock().await; guard.drain(..).collect::>() }; try_join_all(tasks).await?; Ok(()) } } /// The `SwapLock` manages the state of the current swap, ensuring that only one swap can be active at a time. /// It includes: /// - A lock for the current swap (`current_swap`) /// - A broadcast channel for suspension signals (`suspension_trigger`) /// /// The `SwapLock` provides methods to acquire and release the swap lock, and to listen for suspension signals. /// This ensures that swap operations do not overlap and can be safely suspended if needed. pub struct SwapLock { current_swap: RwLock>, suspension_trigger: Sender<()>, } impl SwapLock { pub fn new() -> Self { let (suspension_trigger, _) = broadcast::channel(10); SwapLock { current_swap: RwLock::new(None), suspension_trigger, } } pub async fn listen_for_swap_force_suspension(&self) -> Result<(), Error> { let mut listener = self.suspension_trigger.subscribe(); let event = listener.recv().await; match event { Ok(_) => Ok(()), Err(e) => { tracing::error!("Error receiving swap suspension signal: {}", e); bail!(e) } } } pub async fn acquire_swap_lock(&self, swap_id: Uuid) -> Result<(), Error> { let mut current_swap = self.current_swap.write().await; if current_swap.is_some() { bail!("There already exists an active swap lock"); } tracing::debug!(swap_id = %swap_id, "Acquiring swap lock"); *current_swap = Some(swap_id); Ok(()) } pub async fn get_current_swap_id(&self) -> Option { *self.current_swap.read().await } /// Sends a signal to suspend all ongoing swap processes. /// /// This function performs the following steps: /// 1. Triggers the suspension by sending a unit `()` signal to all listeners via `self.suspension_trigger`. /// 2. Polls the `current_swap` state every 50 milliseconds to check if it has been set to `None`, indicating that the swap processes have been suspended and the lock released. /// 3. If the lock is not released within 10 seconds, the function returns an error. /// /// If we send a suspend signal while no swap is in progress, the function will not fail, but will return immediately. /// /// # Returns /// - `Ok(())` if the swap lock is successfully released. /// - `Err(Error)` if the function times out waiting for the swap lock to be released. /// /// # Notes /// The 50ms polling interval is considered negligible overhead compared to the typical time required to suspend ongoing swap processes. pub async fn send_suspend_signal(&self) -> Result<(), Error> { const TIMEOUT: u64 = 10_000; const INTERVAL: u64 = 50; let _ = self.suspension_trigger.send(())?; for _ in 0..(TIMEOUT / INTERVAL) { if self.get_current_swap_id().await.is_none() { return Ok(()); } tokio::time::sleep(tokio::time::Duration::from_millis(INTERVAL)).await; } bail!("Timed out waiting for swap lock to be released"); } pub async fn release_swap_lock(&self) -> Result { let mut current_swap = self.current_swap.write().await; if let Some(swap_id) = current_swap.as_ref() { tracing::debug!(swap_id = %swap_id, "Releasing swap lock"); let prev_swap_id = *swap_id; *current_swap = None; drop(current_swap); Ok(prev_swap_id) } else { bail!("There is no current swap lock to release"); } } } impl Default for SwapLock { fn default() -> Self { Self::new() } } /// Holds shared data for different parts of the CLI. /// /// Some components are optional, allowing initialization of only necessary parts. /// For example, the `history` command doesn't require wallet initialization. /// /// Many fields are wrapped in `Arc` for thread-safe sharing. #[derive(Clone)] pub struct Context { pub db: Arc, pub swap_lock: Arc, pub config: Config, pub tasks: Arc, tauri_handle: Option, bitcoin_wallet: Option>, pub monero_manager: Option>, tor_client: Option>>, #[allow(dead_code)] monero_rpc_pool_handle: Option>, } /// A conveniant builder struct for [`Context`]. #[must_use = "ContextBuilder must be built to be useful"] pub struct ContextBuilder { monero_config: Option, bitcoin: Option, data: Option, is_testnet: bool, debug: bool, json: bool, tor: bool, enable_monero_tor: bool, tauri_handle: Option, } impl ContextBuilder { /// Start building a context pub fn new(is_testnet: bool) -> Self { if is_testnet { Self::testnet() } else { Self::mainnet() } } /// Basic builder with default options for mainnet pub fn mainnet() -> Self { ContextBuilder { monero_config: None, bitcoin: None, data: None, is_testnet: false, debug: false, json: false, tor: false, enable_monero_tor: false, tauri_handle: None, } } /// Basic builder with default options for testnet pub fn testnet() -> Self { let mut builder = Self::mainnet(); builder.is_testnet = true; builder } /// Configures the Context to initialize a Monero wallet with the given configuration. pub fn with_monero(mut self, monero_config: impl Into>) -> Self { self.monero_config = monero_config.into(); self } /// Configures the Context to initialize a Bitcoin wallet with the given configuration. pub fn with_bitcoin(mut self, bitcoin: impl Into>) -> Self { self.bitcoin = bitcoin.into(); self } /// Attach a handle to Tauri to the Context for emitting events etc. pub fn with_tauri(mut self, tauri_handle: impl Into>) -> Self { self.tauri_handle = tauri_handle.into(); self } /// Configures where the data and logs are saved in the filesystem pub fn with_data_dir(mut self, data: impl Into>) -> Self { self.data = data.into(); self } /// Whether to include debug level logging messages (default false) pub fn with_debug(mut self, debug: bool) -> Self { self.debug = debug; self } /// Set logging format to json (default false) pub fn with_json(mut self, json: bool) -> Self { self.json = json; self } /// Whether to initialize a Tor client (default false) pub fn with_tor(mut self, tor: bool) -> Self { self.tor = tor; self } /// Whether to route Monero wallet traffic through Tor (default false) pub fn with_enable_monero_tor(mut self, enable_monero_tor: bool) -> Self { self.enable_monero_tor = enable_monero_tor; self } /// Takes the builder, initializes the context by initializing the wallets and other components and returns the Context. pub async fn build(self) -> Result { // This is the data directory for the eigenwallet (wallet files) let eigenwallet_data_dir = &eigenwallet_data::new(self.is_testnet)?; let base_data_dir = &data::data_dir_from(self.data, self.is_testnet)?; let log_dir = base_data_dir.join("logs"); let env_config = env_config_from(self.is_testnet); // Initialize logging let format = if self.json { Format::Json } else { Format::Raw }; let level_filter = if self.debug { LevelFilter::from_level(Level::DEBUG) } else { LevelFilter::from_level(Level::INFO) }; START.call_once(|| { let _ = common::tracing_util::init( level_filter, format, log_dir.clone(), self.tauri_handle.clone(), false, ); tracing::info!( binary = "cli", version = env!("VERGEN_GIT_DESCRIBE"), os = std::env::consts::OS, arch = std::env::consts::ARCH, "Setting up context" ); }); // Create unbootstrapped Tor client early if enabled let unbootstrapped_tor_client = if self.tor { match create_tor_client(&base_data_dir).await.inspect_err(|err| { tracing::warn!(%err, "Failed to create Tor client. We will continue without Tor"); }) { Ok(client) => Some(client), Err(_) => None, } } else { tracing::warn!("Internal Tor client not enabled, skipping initialization"); None }; // Start the rpc pool for the monero wallet with optional Tor client based on enable_monero_tor setting let (server_info, mut status_receiver, pool_handle) = monero_rpc_pool::start_server_with_random_port( monero_rpc_pool::config::Config::new_random_port_with_tor_client( base_data_dir.join("monero-rpc-pool"), if self.enable_monero_tor { unbootstrapped_tor_client.clone() } else { None }, match self.is_testnet { true => monero::Network::Stagenet, false => monero::Network::Mainnet, }, ), ) .await?; // Listen for pool status updates and forward them to frontend let pool_tauri_handle = self.tauri_handle.clone(); tokio::spawn(async move { while let Ok(status) = status_receiver.recv().await { pool_tauri_handle.emit_pool_status_update(status); } }); // Determine the monero node address to use let (monero_node_address, monero_rpc_pool_handle) = match &self.monero_config { Some(MoneroNodeConfig::Pool) => { let rpc_url = server_info.into(); (rpc_url, Some(Arc::new(pool_handle))) } Some(MoneroNodeConfig::SingleNode { url }) => (url.clone(), None), None => { // Default to pool if no monero config is provided let rpc_url = server_info.into(); (rpc_url, Some(Arc::new(pool_handle))) } }; // Create a daemon struct for the monero wallet based on the node address let daemon = monero_sys::Daemon { address: monero_node_address, ssl: false, }; // Initialize wallet database for tracking recent wallets let wallet_database = monero_sys::Database::new(eigenwallet_data_dir.clone()) .await .context("Failed to initialize wallet database")?; // Prompt the user to open/create a Monero wallet let (wallet, seed) = request_and_open_monero_wallet( self.tauri_handle.clone(), eigenwallet_data_dir, base_data_dir, env_config, &daemon, &wallet_database, ) .await?; let primary_address = wallet.main_address().await; // Derive data directory from primary address let data_dir = base_data_dir .join("identities") .join(primary_address.to_string()); // Ensure the identity directory exists swap_fs::ensure_directory_exists(&data_dir) .context("Failed to create identity directory")?; tracing::info!( primary_address = %primary_address, data_dir = %data_dir.display(), "Using wallet-specific data directory" ); let wallet_database = Some(Arc::new(wallet_database)); // Create the monero wallet manager let monero_manager = Some(Arc::new( monero::Wallets::new_with_existing_wallet( eigenwallet_data_dir.to_path_buf(), daemon.clone(), env_config.monero_network, false, self.tauri_handle.clone(), wallet, wallet_database, ) .await .context("Failed to initialize Monero wallets with existing wallet")?, )); // Create the data structure we use to manage the swap lock let swap_lock = Arc::new(SwapLock::new()); let tasks = PendingTaskList::default().into(); // Initialize the database let database_progress_handle = self .tauri_handle .new_background_process_with_initial_progress( TauriBackgroundProgress::OpeningDatabase, (), ); let db = open_db( data_dir.join("sqlite"), AccessMode::ReadWrite, self.tauri_handle.clone(), ) .await?; database_progress_handle.finish(); let tauri_handle = &self.tauri_handle.clone(); let initialize_bitcoin_wallet = async { match self.bitcoin { Some(bitcoin) => { let (urls, target_block) = bitcoin.apply_defaults(self.is_testnet)?; let bitcoin_progress_handle = tauri_handle .new_background_process_with_initial_progress( TauriBackgroundProgress::OpeningBitcoinWallet, (), ); let wallet = init_bitcoin_wallet( urls, &seed, &data_dir, env_config, target_block, self.tauri_handle.clone(), ) .await?; bitcoin_progress_handle.finish(); Ok::>, Error>(Some(Arc::new( wallet, ))) } None => Ok(None), } }; let bootstrap_tor_client_task = async { // Bootstrap the Tor client if we have one match unbootstrapped_tor_client.clone() { Some(tor_client) => { bootstrap_tor_client(tor_client.clone(), tauri_handle.clone()) .await .inspect_err(|err| { tracing::warn!(%err, "Failed to bootstrap Tor client. It will remain unbootstrapped"); }) .ok(); Ok(Some(tor_client)) } None => Ok(None), } }; let (bitcoin_wallet, tor) = tokio::try_join!(initialize_bitcoin_wallet, bootstrap_tor_client_task,)?; // If we have a bitcoin wallet and a tauri handle, we start a background task if let Some(wallet) = bitcoin_wallet.clone() { if self.tauri_handle.is_some() { let watcher = Watcher::new( wallet, db.clone(), self.tauri_handle.clone(), swap_lock.clone(), ); tokio::spawn(watcher.run()); } } let context = Context { db, bitcoin_wallet, monero_manager, config: Config { namespace: XmrBtcNamespace::from_is_testnet(self.is_testnet), env_config, seed: seed.clone().into(), debug: self.debug, json: self.json, is_testnet: self.is_testnet, data_dir: data_dir.clone(), log_dir: log_dir.clone(), }, swap_lock, tasks, tauri_handle: self.tauri_handle, tor_client: tor, monero_rpc_pool_handle, }; tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Available); Ok(context) } } impl Context { pub fn with_tauri_handle(mut self, tauri_handle: impl Into>) -> Self { self.tauri_handle = tauri_handle.into(); self } pub async fn for_harness( seed: Seed, env_config: EnvConfig, db_path: PathBuf, bob_bitcoin_wallet: Arc, bob_monero_wallet: Arc, ) -> Self { let config = Config::for_harness(seed, env_config); Self { bitcoin_wallet: Some(bob_bitcoin_wallet), monero_manager: Some(bob_monero_wallet), config, db: open_db(db_path, AccessMode::ReadWrite, None) .await .expect("Could not open sqlite database"), swap_lock: SwapLock::new().into(), tasks: PendingTaskList::default().into(), tauri_handle: None, tor_client: None, monero_rpc_pool_handle: None, } } pub fn cleanup(&self) -> Result<()> { // TODO: close all monero wallets // call store(..) on all wallets // TODO: This doesn't work because "there is no reactor running, must be called from the context of a Tokio 1.x runtime" // let monero_manager = self.monero_manager.clone(); // tokio::spawn(async move { // if let Some(monero_manager) = monero_manager { // let wallet = monero_manager.main_wallet().await; // wallet.store(None).await; // } // }); Ok(()) } pub fn bitcoin_wallet(&self) -> Option> { self.bitcoin_wallet.clone() } pub fn tauri_handle(&self) -> Option { self.tauri_handle.clone() } } impl fmt::Debug for Context { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "") } } async fn init_bitcoin_wallet( electrum_rpc_urls: Vec, seed: &Seed, data_dir: &Path, env_config: EnvConfig, bitcoin_target_block: u16, tauri_handle_option: Option, ) -> Result> { let mut builder = bitcoin::wallet::WalletBuilder::default() .seed(seed.clone()) .network(env_config.bitcoin_network) .electrum_rpc_urls(electrum_rpc_urls) .persister(bitcoin::wallet::PersisterConfig::SqliteFile { data_dir: data_dir.to_path_buf(), }) .finality_confirmations(env_config.bitcoin_finality_confirmations) .target_block(bitcoin_target_block) .sync_interval(env_config.bitcoin_sync_interval()); if let Some(handle) = tauri_handle_option { builder = builder.tauri_handle(handle.clone()); } let wallet = builder .build() .await .context("Failed to initialize Bitcoin wallet")?; Ok(wallet) } async fn request_and_open_monero_wallet_legacy( data_dir: &PathBuf, env_config: EnvConfig, daemon: &monero_sys::Daemon, ) -> Result { let wallet_path = data_dir.join("swap-tool-blockchain-monitoring-wallet"); let wallet = monero::Wallet::open_or_create( wallet_path.display().to_string(), daemon.clone(), env_config.monero_network, true, ) .await .context("Failed to create wallet")?; Ok(wallet) } /// Opens or creates a Monero wallet after asking the user via the Tauri UI. /// /// The user can: /// - Create a new wallet with a random seed. /// - Recover a wallet from a given seed phrase. /// - Open an existing wallet file (with password verification). /// /// Errors if the user aborts, provides an incorrect password, or the wallet /// fails to open/create. async fn request_and_open_monero_wallet( tauri_handle: Option, eigenwallet_data_dir: &PathBuf, legacy_data_dir: &PathBuf, env_config: EnvConfig, daemon: &monero_sys::Daemon, wallet_database: &monero_sys::Database, ) -> Result<(monero_sys::WalletHandle, Seed), Error> { let eigenwallet_wallets_dir = eigenwallet_data_dir.join("wallets"); let wallet = match tauri_handle { Some(tauri_handle) => { // Get recent wallets from database let recent_wallets: Vec = wallet_database .get_recent_wallets(5) .await .unwrap_or_default() .into_iter() .map(|w| w.wallet_path) .collect(); // This loop continually requests the user to select a wallet file // It then requests the user to provide a password. // It repeats until the user provides a valid password or rejects the password request // When the user rejects the password request, we prompt him to select a wallet again loop { let seed_choice = tauri_handle .request_seed_selection_with_recent_wallets(recent_wallets.clone()) .await?; let _monero_progress_handle = tauri_handle .new_background_process_with_initial_progress( TauriBackgroundProgress::OpeningMoneroWallet, (), ); fn new_wallet_path(eigenwallet_wallets_dir: &PathBuf) -> Result { let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); let wallet_path = eigenwallet_wallets_dir.join(format!("wallet_{}", timestamp)); if let Some(parent) = wallet_path.parent() { swap_fs::ensure_directory_exists(parent) .context("Failed to create wallet directory")?; } Ok(wallet_path) } let wallet = match seed_choice { SeedChoice::RandomSeed => { // Create wallet with Unix timestamp as name let wallet_path = new_wallet_path(&eigenwallet_wallets_dir) .context("Failed to determine path for new wallet")?; monero::Wallet::open_or_create( wallet_path.display().to_string(), daemon.clone(), env_config.monero_network, true, ) .await .context("Failed to create wallet from random seed")? } SeedChoice::FromSeed { seed: mnemonic } => { // Create wallet from provided seed let wallet_path = new_wallet_path(&eigenwallet_wallets_dir) .context("Failed to determine path for new wallet")?; monero::Wallet::open_or_create_from_seed( wallet_path.display().to_string(), mnemonic, env_config.monero_network, 0, true, daemon.clone(), ) .await .context("Failed to create wallet from provided seed")? } SeedChoice::FromWalletPath { wallet_path } => { // Helper function to verify password let verify_password = |password: String| -> Result { monero_sys::WalletHandle::verify_wallet_password( wallet_path.clone(), password, ) .map_err(|e| anyhow::anyhow!("Failed to verify wallet password: {}", e)) }; // Request and verify password before opening wallet let wallet_password: Option = { const WALLET_EMPTY_PASSWORD: &str = ""; // First try empty password if verify_password(WALLET_EMPTY_PASSWORD.to_string())? { Some(WALLET_EMPTY_PASSWORD.to_string()) } else { // If empty password fails, ask user for password loop { // Request password from user let password = tauri_handle .request_password(wallet_path.clone()) .await .inspect_err(|e| { tracing::error!( "Failed to get password from user: {}", e ); }) .ok(); // If the user rejects the password request (presses cancel) // We prompt him to select a wallet again let password = match password { Some(password) => password, None => break None, }; // Verify the password using the helper function match verify_password(password.clone()) { Ok(true) => { break Some(password); } Ok(false) => { // Continue loop to request password again continue; } Err(e) => { return Err(e); } } } } }; let password = match wallet_password { Some(password) => password, // None means the user rejected the password request // We prompt him to select a wallet again None => { continue; } }; // Open existing wallet with verified password monero::Wallet::open_or_create_with_password( wallet_path.clone(), password, daemon.clone(), env_config.monero_network, true, ) .await .context("Failed to open wallet from provided path")? } SeedChoice::Legacy => { let wallet = request_and_open_monero_wallet_legacy( legacy_data_dir, env_config, daemon, ) .await?; let seed = Seed::from_file_or_generate(legacy_data_dir) .await .context("Failed to extract seed from wallet")?; break (wallet, seed); } }; // Extract seed from the wallet tracing::info!( "Extracting seed from wallet directory: {}", legacy_data_dir.display() ); let seed = Seed::from_monero_wallet(&wallet) .await .context("Failed to extract seed from wallet")?; break (wallet, seed); } } // If we don't have a tauri handle, we use the seed.pem file // This is used for the CLI to monitor the blockchain None => { let wallet = request_and_open_monero_wallet_legacy(legacy_data_dir, env_config, daemon).await?; let seed = Seed::from_file_or_generate(legacy_data_dir) .await .context("Failed to extract seed from wallet")?; (wallet, seed) } }; Ok(wallet) } pub mod data { use super::*; pub fn data_dir_from(arg_dir: Option, testnet: bool) -> Result { let base_dir = match arg_dir { Some(custom_base_dir) => custom_base_dir, None => os_default()?, }; let sub_directory = if testnet { "testnet" } else { "mainnet" }; Ok(base_dir.join(sub_directory)) } fn os_default() -> Result { Ok(system_data_dir()?.join("cli")) } } pub mod eigenwallet_data { use swap_fs::system_data_dir_eigenwallet; use super::*; pub fn new(testnet: bool) -> Result { Ok(system_data_dir_eigenwallet(testnet)?) } } fn env_config_from(testnet: bool) -> EnvConfig { if testnet { Testnet::get_config() } else { Mainnet::get_config() } } impl Config { pub fn for_harness(seed: Seed, env_config: EnvConfig) -> Self { let data_dir = data::data_dir_from(None, false).expect("Could not find data directory"); let log_dir = data_dir.join("logs"); // not used in production Self { namespace: XmrBtcNamespace::from_is_testnet(false), env_config, seed: seed.into(), debug: false, json: false, is_testnet: false, data_dir, log_dir, } } } 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::*; pub const MULTI_ADDRESS: &str = "/ip4/127.0.0.1/tcp/9939/p2p/12D3KooWCdMKjesXMJz1SiZ7HgotrxuqhQJbP5sgBm2BwP1cqThi"; pub const MONERO_STAGENET_ADDRESS: &str = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a"; pub const BITCOIN_TESTNET_ADDRESS: &str = "tb1qr3em6k3gfnyl8r7q0v7t4tlnyxzgxma3lressv"; pub const MONERO_MAINNET_ADDRESS: &str = "44Ato7HveWidJYUAVw5QffEcEtSH1DwzSP3FPPkHxNAS4LX9CqgucphTisH978FLHE34YNEx7FcbBfQLQUU8m3NUC4VqsRa"; pub const BITCOIN_MAINNET_ADDRESS: &str = "bc1qe4epnfklcaa0mun26yz5g8k24em5u9f92hy325"; pub const SWAP_ID: &str = "ea030832-3be9-454f-bb98-5ea9a788406b"; impl Config { pub async fn default( is_testnet: bool, data_dir: Option, debug: bool, json: bool, ) -> Self { let data_dir = data::data_dir_from(data_dir, is_testnet).unwrap(); let log_dir = data_dir.clone().join("logs"); let seed = Seed::from_file_or_generate(data_dir.as_path()) .await .unwrap(); let env_config = env_config_from(is_testnet); Self { namespace: XmrBtcNamespace::from_is_testnet(is_testnet), env_config, seed: seed.into(), debug, json, is_testnet, data_dir, log_dir, } } } }