feat(GUI): Add settings for theme, fiat currency and remote nodes (#128)

This commit is contained in:
Einliterflasche 2024-11-13 22:51:47 +01:00 committed by GitHub
parent 27d6e23b93
commit 3e79bb3712
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 1133 additions and 267 deletions

View file

@ -69,7 +69,7 @@ impl Wallet {
err => err?,
};
let client = Client::new(electrum_rpc_url, env_config.bitcoin_sync_interval())?;
let client = Client::new(electrum_rpc_url, env_config.bitcoin_sync_interval(), 5)?;
let network = wallet.network();
@ -722,9 +722,9 @@ pub struct Client {
}
impl Client {
fn new(electrum_rpc_url: Url, interval: Duration) -> Result<Self> {
pub fn new(electrum_rpc_url: Url, interval: Duration, retry_count: u8) -> Result<Self> {
let config = bdk::electrum_client::ConfigBuilder::default()
.retry(5)
.retry(retry_count)
.build();
let electrum = bdk::electrum_client::Client::from_config(electrum_rpc_url.as_str(), config)

View file

@ -324,15 +324,6 @@ impl ContextBuilder {
)
.await?;
// If we are connected to the Bitcoin blockchain and if there is a handle to Tauri present,
// we start a background task to watch for timelock changes.
if let Some(wallet) = bitcoin_wallet.clone() {
if self.tauri_handle.is_some() {
let watcher = Watcher::new(wallet, db.clone(), self.tauri_handle.clone());
tokio::spawn(watcher.run());
}
}
// We initialize the Monero wallet below
// To display the progress to the user, we emit events to the Tauri frontend
self.tauri_handle
@ -360,6 +351,15 @@ impl ContextBuilder {
let tor_socks5_port = self.tor.map_or(9050, |tor| tor.tor_socks5_port);
// If we are connected to the Bitcoin blockchain and if there is a handle to Tauri present,
// we start a background task to watch for timelock changes.
if let Some(wallet) = bitcoin_wallet.clone() {
if self.tauri_handle.is_some() {
let watcher = Watcher::new(wallet, db.clone(), self.tauri_handle.clone());
tokio::spawn(watcher.run());
}
}
let context = Context {
db,
bitcoin_wallet,
@ -473,6 +473,11 @@ async fn init_monero_wallet(
let monero_wallet_rpc = monero::WalletRpc::new(data_dir.join("monero")).await?;
tracing::debug!(
address = monero_daemon_address,
"Attempting to start monero-wallet-rpc process"
);
let monero_wallet_rpc_process = monero_wallet_rpc
.run(network, Some(monero_daemon_address))
.await

View file

@ -1,19 +1,22 @@
use super::tauri_bindings::TauriHandle;
use crate::bitcoin::{CancelTimelock, ExpiredTimelocks, PunishTimelock, TxLock};
use crate::bitcoin::{wallet, CancelTimelock, ExpiredTimelocks, PunishTimelock, TxLock};
use crate::cli::api::tauri_bindings::{TauriEmitter, TauriSwapProgressEvent};
use crate::cli::api::Context;
use crate::cli::{list_sellers as list_sellers_impl, EventLoop, Seller, SellerStatus};
use crate::common::get_logs;
use crate::libp2p_ext::MultiAddrExt;
use crate::monero::wallet_rpc::MoneroDaemon;
use crate::network::quote::{BidQuote, ZeroQuoteReceived};
use crate::network::swarm;
use crate::protocol::bob::{BobState, Swap};
use crate::protocol::{bob, State};
use crate::{bitcoin, cli, monero, rpc};
use ::bitcoin::Txid;
use ::monero::Network;
use anyhow::{bail, Context as AnyContext, Result};
use libp2p::core::Multiaddr;
use libp2p::PeerId;
use once_cell::sync::Lazy;
use qrcode::render::unicode;
use qrcode::QrCode;
use serde::{Deserialize, Serialize};
@ -25,6 +28,7 @@ use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use thiserror::Error;
use tracing::debug_span;
use tracing::Instrument;
use tracing::Span;
@ -1294,3 +1298,77 @@ where
Ok((btc_swap_amount, fees))
}
#[typeshare]
#[derive(Deserialize, Serialize)]
pub struct CheckMoneroNodeArgs {
pub url: String,
pub network: String,
}
#[typeshare]
#[derive(Deserialize, Serialize)]
pub struct CheckMoneroNodeResponse {
pub available: bool,
}
#[derive(Error, Debug)]
#[error("this is not one of the known monero networks")]
struct UnknownMoneroNetwork(String);
impl CheckMoneroNodeArgs {
pub async fn request(self) -> Result<CheckMoneroNodeResponse> {
let network = match self.network.to_lowercase().as_str() {
// When the GUI says testnet, it means monero stagenet
"mainnet" => Network::Mainnet,
"testnet" => Network::Stagenet,
otherwise => anyhow::bail!(UnknownMoneroNetwork(otherwise.to_string())),
};
static CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.https_only(false)
.build()
.expect("reqwest client to work")
});
let Ok(monero_daemon) = MoneroDaemon::from_str(self.url, network) else {
return Ok(CheckMoneroNodeResponse { available: false });
};
let Ok(available) = monero_daemon.is_available(&CLIENT).await else {
return Ok(CheckMoneroNodeResponse { available: false });
};
Ok(CheckMoneroNodeResponse { available })
}
}
#[typeshare]
#[derive(Deserialize, Clone)]
pub struct CheckElectrumNodeArgs {
pub url: String,
}
#[typeshare]
#[derive(Serialize, Clone)]
pub struct CheckElectrumNodeResponse {
pub available: bool,
}
impl CheckElectrumNodeArgs {
pub async fn request(self) -> Result<CheckElectrumNodeResponse> {
// Check if the URL is valid
let Ok(url) = self.url.parse() else {
return Ok(CheckElectrumNodeResponse { available: false });
};
// Check if the node is available
let res = wallet::Client::new(url, Duration::from_secs(10), 0);
Ok(CheckElectrumNodeResponse {
available: res.is_ok(),
})
}
}

View file

@ -1,5 +1,5 @@
pub mod wallet;
mod wallet_rpc;
pub mod wallet_rpc;
pub use ::monero::network::Network;
pub use ::monero::{Address, PrivateKey, PublicKey};

View file

@ -4,6 +4,7 @@ use big_bytes::BigByte;
use data_encoding::HEXLOWER;
use futures::{StreamExt, TryStreamExt};
use monero_rpc::wallet::{Client, MoneroWalletRpc as _};
use once_cell::sync::Lazy;
use reqwest::header::CONTENT_LENGTH;
use reqwest::Url;
use serde::Deserialize;
@ -22,24 +23,26 @@ use tokio_util::io::StreamReader;
// See: https://www.moneroworld.com/#nodes, https://monero.fail
// We don't need any testnet nodes because we don't support testnet at all
const MONERO_DAEMONS: [MoneroDaemon; 16] = [
MoneroDaemon::new("xmr-node.cakewallet.com", 18081, Network::Mainnet),
MoneroDaemon::new("nodex.monerujo.io", 18081, Network::Mainnet),
MoneroDaemon::new("nodes.hashvault.pro", 18081, Network::Mainnet),
MoneroDaemon::new("p2pmd.xmrvsbeast.com", 18081, Network::Mainnet),
MoneroDaemon::new("node.monerodevs.org", 18089, Network::Mainnet),
MoneroDaemon::new("xmr-node-usa-east.cakewallet.com", 18081, Network::Mainnet),
MoneroDaemon::new("xmr-node-uk.cakewallet.com", 18081, Network::Mainnet),
MoneroDaemon::new("node.community.rino.io", 18081, Network::Mainnet),
MoneroDaemon::new("testingjohnross.com", 20031, Network::Mainnet),
MoneroDaemon::new("xmr.litepay.ch", 18081, Network::Mainnet),
MoneroDaemon::new("node.trocador.app", 18089, Network::Mainnet),
MoneroDaemon::new("stagenet.xmr-tw.org", 38081, Network::Stagenet),
MoneroDaemon::new("node.monerodevs.org", 38089, Network::Stagenet),
MoneroDaemon::new("singapore.node.xmr.pm", 38081, Network::Stagenet),
MoneroDaemon::new("xmr-lux.boldsuck.org", 38081, Network::Stagenet),
MoneroDaemon::new("stagenet.community.rino.io", 38081, Network::Stagenet),
];
const MONERO_DAEMONS: Lazy<[MoneroDaemon; 16]> = Lazy::new(|| {
[
MoneroDaemon::new("xmr-node.cakewallet.com", 18081, Network::Mainnet),
MoneroDaemon::new("nodex.monerujo.io", 18081, Network::Mainnet),
MoneroDaemon::new("nodes.hashvault.pro", 18081, Network::Mainnet),
MoneroDaemon::new("p2pmd.xmrvsbeast.com", 18081, Network::Mainnet),
MoneroDaemon::new("node.monerodevs.org", 18089, Network::Mainnet),
MoneroDaemon::new("xmr-node-usa-east.cakewallet.com", 18081, Network::Mainnet),
MoneroDaemon::new("xmr-node-uk.cakewallet.com", 18081, Network::Mainnet),
MoneroDaemon::new("node.community.rino.io", 18081, Network::Mainnet),
MoneroDaemon::new("testingjohnross.com", 20031, Network::Mainnet),
MoneroDaemon::new("xmr.litepay.ch", 18081, Network::Mainnet),
MoneroDaemon::new("node.trocador.app", 18089, Network::Mainnet),
MoneroDaemon::new("stagenet.xmr-tw.org", 38081, Network::Stagenet),
MoneroDaemon::new("node.monerodevs.org", 38089, Network::Stagenet),
MoneroDaemon::new("singapore.node.xmr.pm", 38081, Network::Stagenet),
MoneroDaemon::new("xmr-lux.boldsuck.org", 38081, Network::Stagenet),
MoneroDaemon::new("stagenet.community.rino.io", 38081, Network::Stagenet),
]
});
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
compile_error!("unsupported operating system");
@ -87,26 +90,26 @@ pub struct WalletRpcProcess {
port: u16,
}
#[derive(Debug, Copy, Clone)]
struct MoneroDaemon {
address: &'static str,
#[derive(Debug, Clone)]
pub struct MoneroDaemon {
address: String,
port: u16,
network: Network,
}
impl MoneroDaemon {
const fn new(address: &'static str, port: u16, network: Network) -> Self {
Self {
address,
pub fn new(address: impl Into<String>, port: u16, network: Network) -> MoneroDaemon {
MoneroDaemon {
address: address.into(),
port,
network,
}
}
pub fn from_str(address: String, network: Network) -> Result<Self, Error> {
let (address, port) = extract_host_and_port(address)?;
pub fn from_str(address: impl Into<String>, network: Network) -> Result<MoneroDaemon, Error> {
let (address, port) = extract_host_and_port(address.into())?;
Ok(Self {
Ok(MoneroDaemon {
address,
port,
network,
@ -114,7 +117,7 @@ impl MoneroDaemon {
}
/// Checks if the Monero daemon is available by sending a request to its `get_info` endpoint.
async fn is_available(&self, client: &reqwest::Client) -> Result<bool, Error> {
pub async fn is_available(&self, client: &reqwest::Client) -> Result<bool, Error> {
let url = format!("http://{}:{}/get_info", self.address, self.port);
let res = client
.get(url)
@ -162,15 +165,14 @@ async fn choose_monero_daemon(network: Network) -> Result<MoneroDaemon, Error> {
.build()?;
// We only want to check for daemons that match the specified network
let network_matching_daemons = MONERO_DAEMONS
.iter()
.filter(|daemon| daemon.network == network);
let daemons = &*MONERO_DAEMONS;
let network_matching_daemons = daemons.iter().filter(|daemon| daemon.network == network);
for daemon in network_matching_daemons {
match daemon.is_available(&client).await {
Ok(true) => {
tracing::debug!(%daemon, "Found available Monero daemon");
return Ok(*daemon);
return Ok(daemon.clone());
}
Err(err) => {
tracing::debug!(%err, %daemon, "Failed to connect to Monero daemon");
@ -402,7 +404,6 @@ impl WalletRpc {
line?;
}
// Send a json rpc request to make sure monero_wallet_rpc is ready
Client::localhost(port)?.get_version().await?;
Ok(WalletRpcProcess {
@ -486,7 +487,7 @@ impl WalletRpc {
}
}
fn extract_host_and_port(address: String) -> Result<(&'static str, u16), Error> {
fn extract_host_and_port(address: String) -> Result<(String, u16), Error> {
// Strip the protocol (anything before "://")
let stripped_address = if let Some(pos) = address.find("://") {
address[(pos + 3)..].to_string()
@ -501,9 +502,7 @@ fn extract_host_and_port(address: String) -> Result<(&'static str, u16), Error>
let host = parts[0].to_string();
let port = parts[1].parse::<u16>()?;
// Leak the host string to create a 'static lifetime string
let static_str_host: &'static str = Box::leak(host.into_boxed_str());
return Ok((static_str_host, port));
return Ok((host, port));
}
bail!(