mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-12-21 03:15:28 -05:00
feat(gui): Change Monero node while running (#502)
* feat(gui): Change daemon during runtime * feat(swap-controller): Add `monero-seed` RPC command * nitpicks * amend changelog
This commit is contained in:
parent
6861f38f16
commit
7e6138570f
11 changed files with 213 additions and 17 deletions
|
|
@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
- ASB + CONTROLLER: Add a `monero_seed` command to the controller shell. You can use it to export the seed and restore height of the internal Monero wallet. You can use those to import the wallet into a wallet software of your own choosing.
|
||||
- GUI: You can now change the Monero Node without having to restart.
|
||||
|
||||
## [3.0.0-beta.8] - 2025-08-10
|
||||
|
||||
- GUI: Speedup startup by concurrently bootstrapping Tor and requesting the user to select a wallet
|
||||
|
|
|
|||
|
|
@ -37,6 +37,14 @@ pub struct AppState {
|
|||
/// Manages background tasks for the RPC pool
|
||||
pub struct PoolHandle {
|
||||
pub status_update_handle: JoinHandle<()>,
|
||||
pub server_info: ServerInfo,
|
||||
}
|
||||
|
||||
impl PoolHandle {
|
||||
/// Get the current server info for the pool
|
||||
pub fn server_info(&self) -> &ServerInfo {
|
||||
&self.server_info
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PoolHandle {
|
||||
|
|
@ -92,6 +100,10 @@ pub async fn create_app_with_receiver(
|
|||
|
||||
let pool_handle = PoolHandle {
|
||||
status_update_handle,
|
||||
server_info: ServerInfo {
|
||||
port: config.port,
|
||||
host: config.host.clone(),
|
||||
},
|
||||
};
|
||||
|
||||
let app_state = AppState {
|
||||
|
|
@ -145,7 +157,7 @@ pub async fn start_server_with_random_port(
|
|||
PoolHandle,
|
||||
)> {
|
||||
let host = config.host.clone();
|
||||
let (app, status_receiver, pool_handle) = create_app_with_receiver(config).await?;
|
||||
let (app, status_receiver, mut pool_handle) = create_app_with_receiver(config).await?;
|
||||
|
||||
// Bind to port 0 to get a random available port
|
||||
let listener = tokio::net::TcpListener::bind(format!("{}:0", host)).await?;
|
||||
|
|
@ -156,6 +168,9 @@ pub async fn start_server_with_random_port(
|
|||
host: host.clone(),
|
||||
};
|
||||
|
||||
// Update the pool handle with the actual server info
|
||||
pool_handle.server_info = server_info.clone();
|
||||
|
||||
info!(
|
||||
"Started server on {}:{} (random port)",
|
||||
server_info.host, server_info.port
|
||||
|
|
|
|||
|
|
@ -200,9 +200,10 @@ namespace Monero
|
|||
subtract_fee_indices); // Subtract fee from all outputs
|
||||
}
|
||||
|
||||
inline bool setWalletDaemon(Wallet &wallet, const std::string &daemon_address)
|
||||
inline bool setWalletDaemon(Wallet &wallet, const std::string &daemon_address, bool try_ssl)
|
||||
{
|
||||
return wallet.setDaemon(daemon_address);
|
||||
std::string ssl = try_ssl ? "autodetect" : "disabled";
|
||||
return wallet.setDaemon(daemon_address, ssl);
|
||||
}
|
||||
|
||||
inline std::unique_ptr<std::string> pendingTransactionTxId(const PendingTransaction &tx)
|
||||
|
|
|
|||
|
|
@ -191,7 +191,11 @@ pub mod ffi {
|
|||
fn refreshAsync(self: Pin<&mut Wallet>) -> Result<()>;
|
||||
|
||||
/// Set the daemon address.
|
||||
fn setWalletDaemon(wallet: Pin<&mut Wallet>, daemon_address: &CxxString) -> Result<bool>;
|
||||
fn setWalletDaemon(
|
||||
wallet: Pin<&mut Wallet>,
|
||||
daemon_address: &CxxString,
|
||||
try_ssl: bool,
|
||||
) -> Result<bool>;
|
||||
|
||||
/// Set whether the daemon is trusted.
|
||||
fn setTrustedDaemon(self: Pin<&mut Wallet>, trusted: bool) -> Result<()>;
|
||||
|
|
|
|||
|
|
@ -1394,7 +1394,7 @@ impl FfiWallet {
|
|||
.map_err(|e| anyhow!("Failed to initialize wallet: {e}"))?;
|
||||
tracing::debug!("Initialized wallet, setting daemon address");
|
||||
|
||||
wallet.set_daemon_address(&daemon.address)?;
|
||||
wallet.set_daemon(&daemon)?;
|
||||
|
||||
if background_sync {
|
||||
tracing::debug!("Background sync enabled, starting refresh thread");
|
||||
|
|
@ -1438,13 +1438,16 @@ impl FfiWallet {
|
|||
monero::Address::from_str(&address.to_string()).expect("wallet's own address to be valid")
|
||||
}
|
||||
|
||||
pub fn set_daemon_address(&mut self, address: &str) -> anyhow::Result<()> {
|
||||
tracing::debug!(%address, "Setting daemon address");
|
||||
pub fn set_daemon(&mut self, daemon: &Daemon) -> anyhow::Result<()> {
|
||||
let ssl = daemon.ssl;
|
||||
let address = daemon.address.clone();
|
||||
|
||||
tracing::debug!(%address, %ssl, "Setting daemon address");
|
||||
|
||||
let_cxx_string!(address = address);
|
||||
let raw_wallet = &mut self.inner;
|
||||
|
||||
let success = ffi::setWalletDaemon(raw_wallet.pinned(), &address)
|
||||
let success = ffi::setWalletDaemon(raw_wallet.pinned(), &address, ssl)
|
||||
.context("Failed to set daemon address: FFI call failed with exception")?;
|
||||
|
||||
if !success {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import {
|
|||
SetRestoreHeightArgs,
|
||||
SetRestoreHeightResponse,
|
||||
GetRestoreHeightResponse,
|
||||
MoneroNodeConfig,
|
||||
} from "models/tauriModel";
|
||||
import {
|
||||
rpcSetBalance,
|
||||
|
|
@ -641,3 +642,33 @@ export async function saveFilesInDialog(files: Record<string, string>) {
|
|||
export async function dfxAuthenticate(): Promise<DfxAuthenticateResponse> {
|
||||
return await invokeNoArgs<DfxAuthenticateResponse>("dfx_authenticate");
|
||||
}
|
||||
|
||||
export async function changeMoneroNode(
|
||||
nodeConfig: MoneroNodeConfig,
|
||||
): Promise<void> {
|
||||
await invoke<{ node_config: MoneroNodeConfig }, void>("change_monero_node", {
|
||||
node_config: nodeConfig,
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to create MoneroNodeConfig from current settings
|
||||
export async function getCurrentMoneroNodeConfig(): Promise<MoneroNodeConfig> {
|
||||
const network = getNetwork();
|
||||
const useMoneroRpcPool = store.getState().settings.useMoneroRpcPool;
|
||||
const moneroNodeUrl =
|
||||
store.getState().settings.nodes[network][Blockchain.Monero][0] ?? null;
|
||||
|
||||
const moneroNodeConfig =
|
||||
useMoneroRpcPool ||
|
||||
moneroNodeUrl == null ||
|
||||
!(await getMoneroNodeStatus(moneroNodeUrl, network))
|
||||
? { type: "Pool" as const }
|
||||
: {
|
||||
type: "SingleNode" as const,
|
||||
content: {
|
||||
url: moneroNodeUrl,
|
||||
},
|
||||
};
|
||||
|
||||
return moneroNodeConfig;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import {
|
|||
fetchSellersAtPresetRendezvousPoints,
|
||||
getSwapInfo,
|
||||
initializeMoneroWallet,
|
||||
changeMoneroNode,
|
||||
getCurrentMoneroNodeConfig,
|
||||
} from "renderer/rpc";
|
||||
import logger from "utils/logger";
|
||||
import { contextStatusEventReceived } from "store/features/rpcSlice";
|
||||
|
|
@ -14,6 +16,9 @@ import {
|
|||
addNode,
|
||||
setFetchFiatPrices,
|
||||
setFiatCurrency,
|
||||
setUseMoneroRpcPool,
|
||||
Blockchain,
|
||||
Network,
|
||||
} from "store/features/settingsSlice";
|
||||
import { fetchFeedbackMessagesViaHttp, updateRates } from "renderer/api";
|
||||
import { store } from "renderer/store/storeRenderer";
|
||||
|
|
@ -22,7 +27,8 @@ import {
|
|||
addFeedbackId,
|
||||
setConversation,
|
||||
} from "store/features/conversationsSlice";
|
||||
import { TauriContextStatusEvent } from "models/tauriModel";
|
||||
import { TauriContextStatusEvent, MoneroNodeConfig } from "models/tauriModel";
|
||||
import { getNetwork } from "store/config";
|
||||
|
||||
// Create a Map to store throttled functions per swap_id
|
||||
const throttledGetSwapInfoFunctions = new Map<
|
||||
|
|
@ -128,6 +134,25 @@ export function createMainListeners() {
|
|||
},
|
||||
});
|
||||
|
||||
// Listener for Monero node configuration changes
|
||||
listener.startListening({
|
||||
actionCreator: setUseMoneroRpcPool,
|
||||
effect: async (action) => {
|
||||
const usePool = action.payload;
|
||||
logger.info(
|
||||
`Monero node setting changed to: ${usePool ? "Pool" : "Single Node"}`,
|
||||
);
|
||||
|
||||
try {
|
||||
const nodeConfig = await getCurrentMoneroNodeConfig();
|
||||
await changeMoneroNode(nodeConfig);
|
||||
logger.info("Changed Monero node configuration to: ", nodeConfig);
|
||||
} catch (error) {
|
||||
logger.error("Failed to change Monero node configuration:", error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Listener for when a feedback id is added
|
||||
listener.startListening({
|
||||
actionCreator: addFeedbackId,
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ use swap::cli::{
|
|||
api::{
|
||||
data,
|
||||
request::{
|
||||
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, CheckElectrumNodeArgs,
|
||||
CheckElectrumNodeResponse, CheckMoneroNodeArgs, CheckMoneroNodeResponse, CheckSeedArgs,
|
||||
CheckSeedResponse, DfxAuthenticateResponse, ExportBitcoinWalletArgs,
|
||||
GetCurrentSwapArgs, GetDataDirArgs, GetHistoryArgs, GetLogsArgs,
|
||||
GetMoneroAddressesArgs, GetMoneroBalanceArgs, GetMoneroHistoryArgs,
|
||||
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, ChangeMoneroNodeArgs,
|
||||
CheckElectrumNodeArgs, CheckElectrumNodeResponse, CheckMoneroNodeArgs,
|
||||
CheckMoneroNodeResponse, CheckSeedArgs, CheckSeedResponse, DfxAuthenticateResponse,
|
||||
ExportBitcoinWalletArgs, GetCurrentSwapArgs, GetDataDirArgs, GetHistoryArgs,
|
||||
GetLogsArgs, GetMoneroAddressesArgs, GetMoneroBalanceArgs, GetMoneroHistoryArgs,
|
||||
GetMoneroMainAddressArgs, GetMoneroSyncProgressArgs, GetPendingApprovalsResponse,
|
||||
GetRestoreHeightArgs, GetSwapInfoArgs, GetSwapInfosAllArgs, ListSellersArgs,
|
||||
MoneroRecoveryArgs, RedactArgs, RejectApprovalArgs, RejectApprovalResponse,
|
||||
|
|
@ -212,6 +212,7 @@ pub fn run() {
|
|||
reject_approval_request,
|
||||
get_restore_height,
|
||||
dfx_authenticate,
|
||||
change_monero_node,
|
||||
])
|
||||
.setup(setup)
|
||||
.build(tauri::generate_context!())
|
||||
|
|
@ -253,6 +254,7 @@ tauri_command!(list_sellers, ListSellersArgs);
|
|||
tauri_command!(cancel_and_refund, CancelAndRefundArgs);
|
||||
tauri_command!(redact, RedactArgs);
|
||||
tauri_command!(send_monero, SendMoneroArgs);
|
||||
tauri_command!(change_monero_node, ChangeMoneroNodeArgs);
|
||||
|
||||
// These commands require no arguments
|
||||
tauri_command!(get_wallet_descriptor, ExportBitcoinWalletArgs, no_args);
|
||||
|
|
|
|||
|
|
@ -644,6 +644,65 @@ impl Context {
|
|||
pub fn tauri_handle(&self) -> Option<TauriHandle> {
|
||||
self.tauri_handle.clone()
|
||||
}
|
||||
|
||||
/// Change the Monero node configuration for all wallets
|
||||
pub async fn change_monero_node(&self, node_config: MoneroNodeConfig) -> Result<()> {
|
||||
let monero_manager = self
|
||||
.monero_manager
|
||||
.as_ref()
|
||||
.context("Monero wallet manager not available")?;
|
||||
|
||||
// Determine the daemon configuration based on the node config
|
||||
let daemon = match node_config {
|
||||
MoneroNodeConfig::Pool => {
|
||||
// Use the pool handle to get server info
|
||||
let pool_handle = self
|
||||
.monero_rpc_pool_handle
|
||||
.as_ref()
|
||||
.context("Pool handle not available")?;
|
||||
|
||||
let server_info = pool_handle.server_info();
|
||||
let pool_url: String = server_info.clone().into();
|
||||
tracing::info!("Switching to Monero RPC pool: {}", pool_url);
|
||||
|
||||
monero_sys::Daemon {
|
||||
address: pool_url,
|
||||
ssl: false,
|
||||
}
|
||||
}
|
||||
MoneroNodeConfig::SingleNode { url } => {
|
||||
tracing::info!("Switching to single Monero node: {}", url);
|
||||
|
||||
// Parse the URL to determine SSL and address
|
||||
let (address, ssl) = if url.starts_with("https://") {
|
||||
(
|
||||
url.strip_prefix("https://").unwrap_or(&url).to_string(),
|
||||
true,
|
||||
)
|
||||
} else if url.starts_with("http://") {
|
||||
(
|
||||
url.strip_prefix("http://").unwrap_or(&url).to_string(),
|
||||
false,
|
||||
)
|
||||
} else {
|
||||
// Default to HTTP if no protocol specified
|
||||
(url, false)
|
||||
};
|
||||
|
||||
monero_sys::Daemon { address, ssl }
|
||||
}
|
||||
};
|
||||
|
||||
// Update the wallet manager's daemon configuration
|
||||
monero_manager
|
||||
.change_monero_node(daemon.clone())
|
||||
.await
|
||||
.context("Failed to change Monero node in wallet manager")?;
|
||||
|
||||
tracing::info!(?daemon, "Switched Monero node");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Context {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use super::tauri_bindings::TauriHandle;
|
||||
use crate::bitcoin::{wallet, CancelTimelock, ExpiredTimelocks, PunishTimelock};
|
||||
use crate::cli::api::tauri_bindings::{
|
||||
ApprovalRequestType, SelectMakerDetails, SendMoneroDetails, TauriEmitter,
|
||||
ApprovalRequestType, MoneroNodeConfig, SelectMakerDetails, SendMoneroDetails, TauriEmitter,
|
||||
TauriSwapProgressEvent,
|
||||
};
|
||||
use crate::cli::api::Context;
|
||||
|
|
@ -2006,3 +2006,37 @@ pub struct DfxAuthenticateResponse {
|
|||
pub access_token: String,
|
||||
pub kyc_url: String,
|
||||
}
|
||||
|
||||
// ChangeMoneroNode
|
||||
#[typeshare]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ChangeMoneroNodeArgs {
|
||||
pub node_config: MoneroNodeConfig,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ChangeMoneroNodeResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
impl Request for ChangeMoneroNodeArgs {
|
||||
type Response = ChangeMoneroNodeResponse;
|
||||
|
||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||
change_monero_node(self, ctx).await
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(fields(method = "change_monero_node"), skip(context))]
|
||||
pub async fn change_monero_node(
|
||||
args: ChangeMoneroNodeArgs,
|
||||
context: Arc<Context>,
|
||||
) -> Result<ChangeMoneroNodeResponse> {
|
||||
context
|
||||
.change_monero_node(args.node_config)
|
||||
.await
|
||||
.context("Failed to change Monero node")?;
|
||||
|
||||
Ok(ChangeMoneroNodeResponse { success: true })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use anyhow::{Context, Result};
|
|||
use monero::{Address, Network};
|
||||
use monero_sys::WalletEventListener;
|
||||
pub use monero_sys::{Daemon, WalletHandle as Wallet, WalletHandleListener};
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::cli::api::{
|
||||
|
|
@ -29,7 +30,7 @@ pub struct Wallets {
|
|||
/// The network we're on.
|
||||
network: Network,
|
||||
/// The monero node we connect to.
|
||||
daemon: Daemon,
|
||||
daemon: Arc<RwLock<Daemon>>,
|
||||
/// Keep the main wallet open and synced.
|
||||
main_wallet: Arc<Wallet>,
|
||||
/// Since Network::Regtest isn't a thing we have to use an extra flag.
|
||||
|
|
@ -275,6 +276,8 @@ impl Wallets {
|
|||
.await;
|
||||
}
|
||||
|
||||
let daemon = Arc::new(RwLock::new(daemon));
|
||||
|
||||
let wallets = Self {
|
||||
wallet_dir,
|
||||
network,
|
||||
|
|
@ -323,6 +326,8 @@ impl Wallets {
|
|||
.await;
|
||||
}
|
||||
|
||||
let daemon = Arc::new(RwLock::new(daemon));
|
||||
|
||||
let wallets = Self {
|
||||
wallet_dir,
|
||||
network,
|
||||
|
|
@ -367,6 +372,8 @@ impl Wallets {
|
|||
.await
|
||||
.context("Couldn't fetch blockchain height")?;
|
||||
|
||||
let daemon = self.daemon.read().await.clone();
|
||||
|
||||
let wallet = Wallet::open_or_create_from_keys(
|
||||
wallet_path.clone(),
|
||||
None,
|
||||
|
|
@ -376,7 +383,7 @@ impl Wallets {
|
|||
spend_key,
|
||||
blockheight,
|
||||
false, // We don't sync the swap wallet, just import the transaction
|
||||
self.daemon.clone(),
|
||||
daemon,
|
||||
)
|
||||
.await
|
||||
.context(format!(
|
||||
|
|
@ -460,6 +467,18 @@ impl Wallets {
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn change_monero_node(&self, new_daemon: Daemon) -> Result<()> {
|
||||
{
|
||||
let mut daemon = self.daemon.write().await;
|
||||
*daemon = new_daemon.clone();
|
||||
}
|
||||
|
||||
self.main_wallet
|
||||
.call(move |wallet| wallet.set_daemon(&new_daemon))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the last 5 recently used wallets
|
||||
pub async fn get_recent_wallets(&self) -> Result<Vec<String>> {
|
||||
if let Some(db) = &self.wallet_database {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue