mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-12-21 11:25:50 -05:00
refactor(monero-rpc-pool): ureq -> raw hyper (#487)
* refactor(monero-rpc-pool): ureq -> raw hyper * whitelist "getblocks.bin", refactor config constructors, use arti-client, record lowerst seen block height, small style changes * display effective bandwidth * compact wallet overview page a bit * record latencies correctly * add setting for monero tor routing, add ssl support for hyper, lengthen window duration for bandwidth tracker * remove unwrap * refactor ui * dont fail silently tor bootstrap * some workarounds for buggy wallet2 stuff
This commit is contained in:
parent
cd12d17580
commit
d21baa8350
19 changed files with 957 additions and 741 deletions
36
Cargo.lock
generated
36
Cargo.lock
generated
|
|
@ -4559,9 +4559,11 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2 0.6.0",
|
"socket2 0.6.0",
|
||||||
|
"system-configuration 0.6.1",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"windows-registry",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -6219,25 +6221,31 @@ name = "monero-rpc-pool"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"arti-client",
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap 4.5.41",
|
"clap 4.5.41",
|
||||||
"futures",
|
"futures",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper 1.6.0",
|
||||||
|
"hyper-util",
|
||||||
"monero",
|
"monero",
|
||||||
"monero-rpc",
|
"monero-rpc",
|
||||||
|
"native-tls",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
"tokio-test",
|
"tokio-test",
|
||||||
|
"tor-rtcompat",
|
||||||
"tower 0.4.13",
|
"tower 0.4.13",
|
||||||
"tower-http 0.5.2",
|
"tower-http 0.5.2",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"typeshare",
|
"typeshare",
|
||||||
"ureq",
|
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
@ -12872,21 +12880,6 @@ version = "0.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
|
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ureq"
|
|
||||||
version = "2.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
|
|
||||||
dependencies = [
|
|
||||||
"base64 0.22.1",
|
|
||||||
"log",
|
|
||||||
"once_cell",
|
|
||||||
"rustls 0.23.29",
|
|
||||||
"rustls-pki-types",
|
|
||||||
"url",
|
|
||||||
"webpki-roots 0.26.11",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.4"
|
version = "2.5.4"
|
||||||
|
|
@ -13594,6 +13587,17 @@ dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-registry"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
"windows-result 0.3.4",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,17 @@ tower-http = { version = "0.5", features = ["cors"] }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true }
|
tracing-subscriber = { workspace = true }
|
||||||
typeshare = { workspace = true }
|
typeshare = { workspace = true }
|
||||||
ureq = { version = "2.10", default-features = false, features = ["tls"] }
|
|
||||||
url = "2.0"
|
url = "2.0"
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
|
|
||||||
|
arti-client = { workspace = true, features = ["tokio"] }
|
||||||
|
tor-rtcompat = { workspace = true, features = ["tokio", "rustls"] }
|
||||||
|
|
||||||
|
http-body-util = "0.1"
|
||||||
|
hyper = { version = "1", features = ["full"] }
|
||||||
|
hyper-util = { version = "0.1", features = ["full"] }
|
||||||
|
native-tls = "0.2"
|
||||||
|
tokio-native-tls = "0.3"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio-test = "0.4"
|
tokio-test = "0.4"
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,53 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
use crate::TorClientArc;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub host: String,
|
pub host: String,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub data_dir: PathBuf,
|
pub data_dir: PathBuf,
|
||||||
|
pub tor_client: Option<TorClientArc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Config {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("Config")
|
||||||
|
.field("host", &self.host)
|
||||||
|
.field("port", &self.port)
|
||||||
|
.field("data_dir", &self.data_dir)
|
||||||
|
.field("tor_client", &self.tor_client.is_some())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn new_with_port(host: String, port: u16, data_dir: PathBuf) -> Self {
|
pub fn new_with_port(host: String, port: u16, data_dir: PathBuf) -> Self {
|
||||||
|
Self::new_with_port_and_tor_client(host, port, data_dir, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_port_and_tor_client(
|
||||||
|
host: String,
|
||||||
|
port: u16,
|
||||||
|
data_dir: PathBuf,
|
||||||
|
tor_client: impl Into<Option<TorClientArc>>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
data_dir,
|
data_dir,
|
||||||
|
tor_client: tor_client.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_random_port(host: String, data_dir: PathBuf) -> Self {
|
pub fn new_random_port(data_dir: PathBuf) -> Self {
|
||||||
Self {
|
Self::new_random_port_with_tor_client(data_dir, None)
|
||||||
host,
|
}
|
||||||
port: 0,
|
|
||||||
data_dir,
|
pub fn new_random_port_with_tor_client(
|
||||||
}
|
data_dir: PathBuf,
|
||||||
|
tor_client: impl Into<Option<TorClientArc>>,
|
||||||
|
) -> Self {
|
||||||
|
Self::new_with_port_and_tor_client("127.0.0.1".to_string(), 0, data_dir, tor_client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use arti_client::TorClient;
|
||||||
use axum::{
|
use axum::{
|
||||||
routing::{any, get},
|
routing::{any, get},
|
||||||
Router,
|
Router,
|
||||||
|
|
@ -8,9 +9,13 @@ use axum::{
|
||||||
use monero::Network;
|
use monero::Network;
|
||||||
|
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
|
use tor_rtcompat::tokio::TokioRustlsRuntime;
|
||||||
use tower_http::cors::CorsLayer;
|
use tower_http::cors::CorsLayer;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
/// Type alias for the Tor client used throughout the crate
|
||||||
|
pub type TorClientArc = Arc<TorClient<TokioRustlsRuntime>>;
|
||||||
|
|
||||||
pub trait ToNetworkString {
|
pub trait ToNetworkString {
|
||||||
fn to_network_string(&self) -> String;
|
fn to_network_string(&self) -> String;
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +44,7 @@ use proxy::{proxy_handler, stats_handler};
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub node_pool: Arc<NodePool>,
|
pub node_pool: Arc<NodePool>,
|
||||||
pub http_client: ureq::Agent,
|
pub tor_client: Option<TorClientArc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manages background tasks for the RPC pool
|
/// Manages background tasks for the RPC pool
|
||||||
|
|
@ -104,17 +109,9 @@ async fn create_app_with_receiver(
|
||||||
status_update_handle,
|
status_update_handle,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create shared HTTP client with connection pooling and keep-alive
|
|
||||||
// TODO: Add dangerous certificate acceptance equivalent to reqwest's danger_accept_invalid_certs(true)
|
|
||||||
let http_client = ureq::AgentBuilder::new()
|
|
||||||
.timeout(std::time::Duration::from_secs(30))
|
|
||||||
.max_idle_connections(100)
|
|
||||||
.max_idle_connections_per_host(10)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let app_state = AppState {
|
let app_state = AppState {
|
||||||
node_pool,
|
node_pool,
|
||||||
http_client,
|
tor_client: config.tor_client,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build the app
|
// Build the app
|
||||||
|
|
@ -177,14 +174,8 @@ pub async fn start_server_with_random_port(
|
||||||
tokio::sync::broadcast::Receiver<PoolStatus>,
|
tokio::sync::broadcast::Receiver<PoolStatus>,
|
||||||
PoolHandle,
|
PoolHandle,
|
||||||
)> {
|
)> {
|
||||||
// Clone the host before moving config
|
|
||||||
let host = config.host.clone();
|
let host = config.host.clone();
|
||||||
|
let (app, status_receiver, pool_handle) = create_app_with_receiver(config, network).await?;
|
||||||
// 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, pool_handle) =
|
|
||||||
create_app_with_receiver(config_with_random_port, network).await?;
|
|
||||||
|
|
||||||
// Bind to port 0 to get a random available port
|
// Bind to port 0 to get a random available port
|
||||||
let listener = tokio::net::TcpListener::bind(format!("{}:0", host)).await?;
|
let listener = tokio::net::TcpListener::bind(format!("{}:0", host)).await?;
|
||||||
|
|
@ -209,18 +200,3 @@ pub async fn start_server_with_random_port(
|
||||||
|
|
||||||
Ok((server_info, status_receiver, pool_handle))
|
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 pool handle
|
|
||||||
pub async fn start_server_with_random_port_and_data_dir(
|
|
||||||
config: Config,
|
|
||||||
network: Network,
|
|
||||||
data_dir: std::path::PathBuf,
|
|
||||||
) -> Result<(
|
|
||||||
ServerInfo,
|
|
||||||
tokio::sync::broadcast::Receiver<PoolStatus>,
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use arti_client::{TorClient, TorClientConfig};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use monero_rpc_pool::{config::Config, run_server};
|
use monero_rpc_pool::{config::Config, run_server};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
@ -47,6 +48,11 @@ struct Args {
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
#[arg(help = "Enable verbose logging")]
|
#[arg(help = "Enable verbose logging")]
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
#[arg(help = "Enable Tor routing")]
|
||||||
|
#[arg(default_value = "true")]
|
||||||
|
tor: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|
@ -54,16 +60,46 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_env_filter(EnvFilter::new("trace"))
|
.with_env_filter(EnvFilter::new("info"))
|
||||||
.with_target(false)
|
.with_target(false)
|
||||||
.with_file(true)
|
.with_file(true)
|
||||||
.with_line_number(true)
|
.with_line_number(true)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let config = Config::new_with_port(
|
let tor_client = if args.tor {
|
||||||
|
let config = TorClientConfig::default();
|
||||||
|
let runtime = tor_rtcompat::tokio::TokioRustlsRuntime::current()
|
||||||
|
.expect("We are always running with tokio");
|
||||||
|
|
||||||
|
let client = TorClient::with_runtime(runtime)
|
||||||
|
.config(config)
|
||||||
|
.create_unbootstrapped_async()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let client = std::sync::Arc::new(client);
|
||||||
|
|
||||||
|
let client_clone = client.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match client_clone.bootstrap().await {
|
||||||
|
Ok(()) => {
|
||||||
|
info!("Tor client successfully bootstrapped");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to bootstrap Tor client: {}. Tor functionality will be unavailable.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(client)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = Config::new_with_port_and_tor_client(
|
||||||
args.host,
|
args.host,
|
||||||
args.port,
|
args.port,
|
||||||
std::env::temp_dir().join("monero-rpc-pool"),
|
std::env::temp_dir().join("monero-rpc-pool"),
|
||||||
|
tor_client,
|
||||||
);
|
);
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use tracing::{debug, warn};
|
use tracing::warn;
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
|
|
||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
|
|
@ -16,6 +19,7 @@ pub struct PoolStatus {
|
||||||
#[typeshare(serialized_as = "number")]
|
#[typeshare(serialized_as = "number")]
|
||||||
pub unsuccessful_health_checks: u64,
|
pub unsuccessful_health_checks: u64,
|
||||||
pub top_reliable_nodes: Vec<ReliableNodeInfo>,
|
pub top_reliable_nodes: Vec<ReliableNodeInfo>,
|
||||||
|
pub bandwidth_kb_per_sec: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize)]
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
|
|
@ -26,10 +30,69 @@ pub struct ReliableNodeInfo {
|
||||||
pub avg_latency_ms: Option<f64>,
|
pub avg_latency_ms: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct BandwidthEntry {
|
||||||
|
timestamp: Instant,
|
||||||
|
bytes: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct BandwidthTracker {
|
||||||
|
entries: VecDeque<BandwidthEntry>,
|
||||||
|
window_duration: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BandwidthTracker {
|
||||||
|
const WINDOW_DURATION: Duration = Duration::from_secs(60 * 3);
|
||||||
|
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
entries: VecDeque::new(),
|
||||||
|
window_duration: Self::WINDOW_DURATION,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_bytes(&mut self, bytes: u64) {
|
||||||
|
let now = Instant::now();
|
||||||
|
self.entries.push_back(BandwidthEntry {
|
||||||
|
timestamp: now,
|
||||||
|
bytes,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up old entries
|
||||||
|
let cutoff = now - self.window_duration;
|
||||||
|
while let Some(front) = self.entries.front() {
|
||||||
|
if front.timestamp < cutoff {
|
||||||
|
self.entries.pop_front();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_kb_per_sec(&self) -> f64 {
|
||||||
|
if self.entries.len() < 5 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_bytes: u64 = self.entries.iter().map(|e| e.bytes).sum();
|
||||||
|
let now = Instant::now();
|
||||||
|
let oldest_time = self.entries.front().unwrap().timestamp;
|
||||||
|
let duration_secs = (now - oldest_time).as_secs_f64();
|
||||||
|
|
||||||
|
if duration_secs > 0.0 {
|
||||||
|
(total_bytes as f64 / 1024.0) / duration_secs
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct NodePool {
|
pub struct NodePool {
|
||||||
db: Database,
|
db: Database,
|
||||||
network: String,
|
network: String,
|
||||||
status_sender: broadcast::Sender<PoolStatus>,
|
status_sender: broadcast::Sender<PoolStatus>,
|
||||||
|
bandwidth_tracker: Arc<Mutex<BandwidthTracker>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NodePool {
|
impl NodePool {
|
||||||
|
|
@ -39,6 +102,7 @@ impl NodePool {
|
||||||
db,
|
db,
|
||||||
network,
|
network,
|
||||||
status_sender,
|
status_sender,
|
||||||
|
bandwidth_tracker: Arc::new(Mutex::new(BandwidthTracker::new())),
|
||||||
};
|
};
|
||||||
(pool, status_receiver)
|
(pool, status_receiver)
|
||||||
}
|
}
|
||||||
|
|
@ -63,13 +127,19 @@ impl NodePool {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn record_bandwidth(&self, bytes: u64) {
|
||||||
|
if let Ok(mut tracker) = self.bandwidth_tracker.lock() {
|
||||||
|
tracker.record_bytes(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn publish_status_update(&self) -> Result<()> {
|
pub async fn publish_status_update(&self) -> Result<()> {
|
||||||
let status = self.get_current_status().await?;
|
let status = self.get_current_status().await?;
|
||||||
|
|
||||||
if let Err(e) = self.status_sender.send(status.clone()) {
|
if let Err(e) = self.status_sender.send(status.clone()) {
|
||||||
warn!("Failed to send status update: {}", e);
|
warn!("Failed to send status update: {}", e);
|
||||||
} else {
|
} else {
|
||||||
debug!(?status, "Sent status update");
|
tracing::debug!(?status, "Sent status update");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -81,6 +151,12 @@ impl NodePool {
|
||||||
let (successful_checks, unsuccessful_checks) =
|
let (successful_checks, unsuccessful_checks) =
|
||||||
self.db.get_health_check_stats(&self.network).await?;
|
self.db.get_health_check_stats(&self.network).await?;
|
||||||
|
|
||||||
|
let bandwidth_kb_per_sec = if let Ok(tracker) = self.bandwidth_tracker.lock() {
|
||||||
|
tracker.get_kb_per_sec()
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
let top_reliable_nodes = reliable_nodes
|
let top_reliable_nodes = reliable_nodes
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.take(5)
|
.take(5)
|
||||||
|
|
@ -97,6 +173,7 @@ impl NodePool {
|
||||||
successful_health_checks: successful_checks,
|
successful_health_checks: successful_checks,
|
||||||
unsuccessful_health_checks: unsuccessful_checks,
|
unsuccessful_health_checks: unsuccessful_checks,
|
||||||
top_reliable_nodes,
|
top_reliable_nodes,
|
||||||
|
bandwidth_kb_per_sec,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,9 +182,10 @@ impl NodePool {
|
||||||
pub async fn get_top_reliable_nodes(&self, limit: usize) -> Result<Vec<NodeAddress>> {
|
pub async fn get_top_reliable_nodes(&self, limit: usize) -> Result<Vec<NodeAddress>> {
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
|
|
||||||
debug!(
|
tracing::debug!(
|
||||||
"Getting top reliable nodes for network {} (target: {})",
|
"Getting top reliable nodes for network {} (target: {})",
|
||||||
self.network, limit
|
self.network,
|
||||||
|
limit
|
||||||
);
|
);
|
||||||
|
|
||||||
let available_nodes = self
|
let available_nodes = self
|
||||||
|
|
@ -149,7 +227,7 @@ impl NodePool {
|
||||||
selected_nodes.push(node);
|
selected_nodes.push(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!(
|
tracing::debug!(
|
||||||
"Pool size: {} nodes for network {} (target: {})",
|
"Pool size: {} nodes for network {} (target: {})",
|
||||||
selected_nodes.len(),
|
selected_nodes.len(),
|
||||||
self.network,
|
self.network,
|
||||||
|
|
@ -158,53 +236,4 @@ impl NodePool {
|
||||||
|
|
||||||
Ok(selected_nodes)
|
Ok(selected_nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_pool_stats(&self) -> Result<PoolStats> {
|
|
||||||
let (total, reachable, reliable) = self.db.get_node_stats(&self.network).await?;
|
|
||||||
let reliable_nodes = self.db.get_reliable_nodes(&self.network).await?;
|
|
||||||
|
|
||||||
let avg_reliable_latency = if reliable_nodes.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let total_latency: f64 = reliable_nodes
|
|
||||||
.iter()
|
|
||||||
.filter_map(|node| node.health.avg_latency_ms)
|
|
||||||
.sum();
|
|
||||||
let count = reliable_nodes
|
|
||||||
.iter()
|
|
||||||
.filter(|node| node.health.avg_latency_ms.is_some())
|
|
||||||
.count();
|
|
||||||
|
|
||||||
if count > 0 {
|
|
||||||
Some(total_latency / count as f64)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(PoolStats {
|
|
||||||
total_nodes: total,
|
|
||||||
reachable_nodes: reachable,
|
|
||||||
reliable_nodes: reliable,
|
|
||||||
avg_reliable_latency_ms: avg_reliable_latency,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct PoolStats {
|
|
||||||
pub total_nodes: i64,
|
|
||||||
pub reachable_nodes: i64,
|
|
||||||
pub reliable_nodes: i64,
|
|
||||||
pub avg_reliable_latency_ms: Option<f64>, // TOOD: Why is this an Option, we hate Options
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PoolStats {
|
|
||||||
pub fn health_percentage(&self) -> f64 {
|
|
||||||
if self.total_nodes == 0 {
|
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
(self.reachable_nodes as f64 / self.total_nodes as f64) * 100.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -75,6 +75,12 @@ export default function MoneroPoolHealthBox() {
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
|
<Chip
|
||||||
|
label={`${poolStatus.bandwidth_kb_per_sec?.toFixed(1) ?? '0.0'} KB/s Bandwidth`}
|
||||||
|
color={poolStatus.bandwidth_kb_per_sec != null && poolStatus.bandwidth_kb_per_sec > 10 ? "info" : "default"}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import {
|
||||||
setFiatCurrency,
|
setFiatCurrency,
|
||||||
setTheme,
|
setTheme,
|
||||||
setTorEnabled,
|
setTorEnabled,
|
||||||
|
setEnableMoneroTor,
|
||||||
setUseMoneroRpcPool,
|
setUseMoneroRpcPool,
|
||||||
setDonateToDevelopment,
|
setDonateToDevelopment,
|
||||||
} from "store/features/settingsSlice";
|
} from "store/features/settingsSlice";
|
||||||
|
|
@ -91,6 +92,7 @@ export default function SettingsBox() {
|
||||||
<Table>
|
<Table>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TorSettings />
|
<TorSettings />
|
||||||
|
<MoneroTorSettings />
|
||||||
<DonationTipSetting />
|
<DonationTipSetting />
|
||||||
<ElectrumRpcUrlSetting />
|
<ElectrumRpcUrlSetting />
|
||||||
<MoneroRpcPoolSetting />
|
<MoneroRpcPoolSetting />
|
||||||
|
|
@ -715,6 +717,42 @@ export function TorSettings() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A setting that allows you to enable or disable routing Monero wallet traffic through Tor.
|
||||||
|
* This setting is only visible when Tor is enabled.
|
||||||
|
*/
|
||||||
|
function MoneroTorSettings() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const torEnabled = useSettings((settings) => settings.enableTor);
|
||||||
|
const enableMoneroTor = useSettings((settings) => settings.enableMoneroTor);
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
dispatch(setEnableMoneroTor(event.target.checked));
|
||||||
|
|
||||||
|
// Hide this setting if Tor is disabled entirely
|
||||||
|
if (!torEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<SettingLabel
|
||||||
|
label="Route Monero traffic through Tor"
|
||||||
|
tooltip="When enabled, Monero wallet traffic will be routed through Tor for additional privacy. Requires main Tor setting to be enabled."
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Switch
|
||||||
|
checked={enableMoneroTor}
|
||||||
|
onChange={handleChange}
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A setting that allows you to manage rendezvous points for maker discovery
|
* A setting that allows you to manage rendezvous points for maker discovery
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,5 @@
|
||||||
import {
|
import { Box, Typography, Card, LinearProgress } from "@mui/material";
|
||||||
Box,
|
import { useAppSelector } from "store/hooks";
|
||||||
Typography,
|
|
||||||
CircularProgress,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
Divider,
|
|
||||||
CardHeader,
|
|
||||||
LinearProgress,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { PiconeroAmount } from "../../../other/Units";
|
import { PiconeroAmount } from "../../../other/Units";
|
||||||
import { FiatPiconeroAmount } from "../../../other/Units";
|
import { FiatPiconeroAmount } from "../../../other/Units";
|
||||||
import StateIndicator from "./StateIndicator";
|
import StateIndicator from "./StateIndicator";
|
||||||
|
|
@ -30,23 +21,77 @@ export default function WalletOverview({
|
||||||
balance,
|
balance,
|
||||||
syncProgress,
|
syncProgress,
|
||||||
}: WalletOverviewProps) {
|
}: WalletOverviewProps) {
|
||||||
|
const lowestCurrentBlock = useAppSelector(
|
||||||
|
(state) => state.wallet.state.lowestCurrentBlock,
|
||||||
|
);
|
||||||
|
|
||||||
|
const poolStatus = useAppSelector((state) => state.pool.status);
|
||||||
|
|
||||||
const pendingBalance =
|
const pendingBalance =
|
||||||
parseFloat(balance.total_balance) - parseFloat(balance.unlocked_balance);
|
parseFloat(balance.total_balance) - parseFloat(balance.unlocked_balance);
|
||||||
|
|
||||||
const isSyncing = syncProgress && syncProgress.progress_percentage < 100;
|
const isSyncing = syncProgress && syncProgress.progress_percentage < 100;
|
||||||
const blocksLeft = syncProgress?.target_block - syncProgress?.current_block;
|
const blocksLeft = syncProgress?.target_block - syncProgress?.current_block;
|
||||||
|
|
||||||
|
// Treat blocksLeft = 1 as if we have no direct knowledge
|
||||||
|
const hasDirectKnowledge = blocksLeft != null && blocksLeft > 1;
|
||||||
|
|
||||||
|
// syncProgress.progress_percentage is not good to display
|
||||||
|
// assuming we have an old wallet, eventually we will always only use the last few cm of the progress bar
|
||||||
|
//
|
||||||
|
// We calculate our own progress percentage
|
||||||
|
// lowestCurrentBlock is the lowest block we have seen
|
||||||
|
// currentBlock is the current block we are on (how war we've synced)
|
||||||
|
// targetBlock is the target block we are syncing to
|
||||||
|
//
|
||||||
|
// The progressPercentage below is the progress on that path
|
||||||
|
// If the lowestCurrentBlock is null, we fallback to the syncProgress.progress_percentage
|
||||||
|
const progressPercentage =
|
||||||
|
lowestCurrentBlock === null || !syncProgress
|
||||||
|
? syncProgress?.progress_percentage || 0
|
||||||
|
: syncProgress.target_block === lowestCurrentBlock
|
||||||
|
? 100 // Fully synced when target equals lowest current block
|
||||||
|
: Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
100,
|
||||||
|
((syncProgress.current_block - lowestCurrentBlock) /
|
||||||
|
(syncProgress.target_block - lowestCurrentBlock)) *
|
||||||
|
100,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isStuck = poolStatus?.bandwidth_kb_per_sec != null && poolStatus.bandwidth_kb_per_sec < 0.01;
|
||||||
|
|
||||||
|
// Calculate estimated time remaining for sync
|
||||||
|
const formatTimeRemaining = (seconds: number): string => {
|
||||||
|
if (seconds < 60) return `${Math.round(seconds)}s`;
|
||||||
|
if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
|
||||||
|
if (seconds < 86400) return `${Math.round(seconds / 3600)}h`;
|
||||||
|
return `${Math.round(seconds / 86400)}d`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const estimatedTimeRemaining =
|
||||||
|
hasDirectKnowledge && poolStatus?.bandwidth_kb_per_sec != null && poolStatus.bandwidth_kb_per_sec > 0
|
||||||
|
? (blocksLeft * 130) / poolStatus.bandwidth_kb_per_sec // blocks * 130kb / kb_per_sec = seconds
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card sx={{ p: 2, position: "relative", borderRadius: 2 }} elevation={4}>
|
<Card sx={{ p: 2, position: "relative", borderRadius: 2 }} elevation={4}>
|
||||||
{syncProgress && syncProgress.progress_percentage < 100 && (
|
{syncProgress && syncProgress.progress_percentage < 100 && (
|
||||||
<LinearProgress
|
<LinearProgress
|
||||||
value={syncProgress.progress_percentage}
|
value={hasDirectKnowledge ? progressPercentage : undefined}
|
||||||
variant="determinate"
|
valueBuffer={
|
||||||
|
// If the bandwidth is low, we may not be making progress
|
||||||
|
// We don't show the buffer in this case
|
||||||
|
hasDirectKnowledge && !isStuck ? progressPercentage : undefined
|
||||||
|
}
|
||||||
|
variant={hasDirectKnowledge ? "buffer" : "indeterminate"}
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -54,95 +99,105 @@ export default function WalletOverview({
|
||||||
{/* Balance */}
|
{/* Balance */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "grid",
|
display: "flex",
|
||||||
gridTemplateColumns: "1.5fr 1fr max-content",
|
justifyContent: "space-between",
|
||||||
rowGap: 0.5,
|
alignItems: "flex-start",
|
||||||
columnGap: 2,
|
|
||||||
mb: 1,
|
mb: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
{/* Left side content */}
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{ mb: 1, gridColumn: "1", gridRow: "1" }}
|
|
||||||
>
|
|
||||||
Available Funds
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="h4" sx={{ gridColumn: "1", gridRow: "2" }}>
|
|
||||||
<PiconeroAmount
|
|
||||||
amount={parseFloat(balance.unlocked_balance)}
|
|
||||||
fixedPrecision={4}
|
|
||||||
disableTooltip
|
|
||||||
/>
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{ gridColumn: "1", gridRow: "3" }}
|
|
||||||
>
|
|
||||||
<FiatPiconeroAmount amount={parseFloat(balance.unlocked_balance)} />
|
|
||||||
</Typography>
|
|
||||||
{pendingBalance > 0 && (
|
|
||||||
<>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="warning"
|
|
||||||
sx={{
|
|
||||||
mb: 1,
|
|
||||||
animation: "pulse 2s infinite",
|
|
||||||
gridColumn: "2",
|
|
||||||
gridRow: "1",
|
|
||||||
alignSelf: "end",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Pending
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography
|
|
||||||
variant="h5"
|
|
||||||
sx={{ gridColumn: "2", gridRow: "2", alignSelf: "center" }}
|
|
||||||
>
|
|
||||||
<PiconeroAmount amount={pendingBalance} fixedPrecision={4} />
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{ gridColumn: "2", gridRow: "3" }}
|
|
||||||
>
|
|
||||||
<FiatPiconeroAmount amount={pendingBalance} />
|
|
||||||
</Typography>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "row",
|
||||||
alignItems: "flex-end",
|
gap: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
flexDirection: "column",
|
||||||
alignItems: "center",
|
gap: 0.5,
|
||||||
gap: 1,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||||
{isSyncing ? "syncing" : "synced"}
|
Available Funds
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h4">
|
||||||
|
<PiconeroAmount
|
||||||
|
amount={parseFloat(balance.unlocked_balance)}
|
||||||
|
fixedPrecision={4}
|
||||||
|
disableTooltip
|
||||||
|
/>
|
||||||
</Typography>
|
</Typography>
|
||||||
<StateIndicator
|
|
||||||
color={isSyncing ? "primary" : "success"}
|
|
||||||
pulsating={isSyncing}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
{isSyncing && (
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{blocksLeft.toLocaleString()} blocks left
|
<FiatPiconeroAmount
|
||||||
|
amount={parseFloat(balance.unlocked_balance)}
|
||||||
|
/>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{pendingBalance > 0 && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="warning"
|
||||||
|
sx={{
|
||||||
|
mb: 1,
|
||||||
|
animation: "pulse 2s infinite",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Pending
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h5">
|
||||||
|
<PiconeroAmount amount={pendingBalance} fixedPrecision={4} />
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
<FiatPiconeroAmount amount={pendingBalance} />
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Right side - simple approach */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StateIndicator
|
||||||
|
color={isSyncing ? "primary" : "success"}
|
||||||
|
pulsating={isSyncing}
|
||||||
|
/>
|
||||||
|
<Box sx={{ textAlign: "right" }}>
|
||||||
|
{isSyncing && hasDirectKnowledge && (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{blocksLeft?.toLocaleString()} blocks left
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{poolStatus && isSyncing && !isStuck && (
|
||||||
|
<>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ mt: 0.5, fontSize: "0.7rem", display: "block" }}
|
||||||
|
>
|
||||||
|
{estimatedTimeRemaining && !isStuck && (
|
||||||
|
<>{formatTimeRemaining(estimatedTimeRemaining)} left</>
|
||||||
|
)} / {poolStatus.bandwidth_kb_per_sec?.toFixed(1) ?? '0.0'} KB/s
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -21,30 +21,29 @@ export default function WalletPageLoadingState() {
|
||||||
{/* Balance */}
|
{/* Balance */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "grid",
|
display: "flex",
|
||||||
gridTemplateColumns: "1.5fr 1fr max-content",
|
justifyContent: "space-between",
|
||||||
rowGap: 0.5,
|
alignItems: "flex-start",
|
||||||
columnGap: 2,
|
|
||||||
mb: 1,
|
mb: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Box
|
||||||
variant="body2"
|
sx={{
|
||||||
color="text.secondary"
|
display: "flex",
|
||||||
sx={{ mb: 1, gridColumn: "1", gridRow: "1" }}
|
flexDirection: "column",
|
||||||
|
gap: 0.5,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Available Funds
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||||
</Typography>
|
Available Funds
|
||||||
<Typography variant="h4" sx={{ gridColumn: "1", gridRow: "2" }}>
|
</Typography>
|
||||||
<Skeleton variant="text" width="80%" />
|
<Typography variant="h4">
|
||||||
</Typography>
|
<Skeleton variant="text" width="80%" />
|
||||||
<Typography
|
</Typography>
|
||||||
variant="body2"
|
<Typography variant="body2" color="text.secondary">
|
||||||
color="text.secondary"
|
<Skeleton variant="text" width="40%" />
|
||||||
sx={{ gridColumn: "1", gridRow: "3" }}
|
</Typography>
|
||||||
>
|
</Box>
|
||||||
<Skeleton variant="text" width="40%" />
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|
@ -61,7 +60,6 @@ export default function WalletPageLoadingState() {
|
||||||
gap: 1,
|
gap: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="body2">loading</Typography>
|
|
||||||
<StateIndicator color="primary" pulsating={true} />
|
<StateIndicator color="primary" pulsating={true} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -319,6 +319,8 @@ export async function initializeContext() {
|
||||||
// For Monero nodes, determine whether to use pool or custom node
|
// For Monero nodes, determine whether to use pool or custom node
|
||||||
const useMoneroRpcPool = store.getState().settings.useMoneroRpcPool;
|
const useMoneroRpcPool = store.getState().settings.useMoneroRpcPool;
|
||||||
|
|
||||||
|
const useMoneroTor = store.getState().settings.enableMoneroTor;
|
||||||
|
|
||||||
const moneroNodeUrl =
|
const moneroNodeUrl =
|
||||||
store.getState().settings.nodes[network][Blockchain.Monero][0] ?? null;
|
store.getState().settings.nodes[network][Blockchain.Monero][0] ?? null;
|
||||||
|
|
||||||
|
|
@ -341,6 +343,7 @@ export async function initializeContext() {
|
||||||
electrum_rpc_urls: bitcoinNodes,
|
electrum_rpc_urls: bitcoinNodes,
|
||||||
monero_node_config: moneroNodeConfig,
|
monero_node_config: moneroNodeConfig,
|
||||||
use_tor: useTor,
|
use_tor: useTor,
|
||||||
|
enable_monero_tor: useMoneroTor,
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.info("Initializing context with settings", tauriSettings);
|
logger.info("Initializing context with settings", tauriSettings);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ export interface SettingsState {
|
||||||
fiatCurrency: FiatCurrency;
|
fiatCurrency: FiatCurrency;
|
||||||
/// Whether to enable Tor for p2p connections
|
/// Whether to enable Tor for p2p connections
|
||||||
enableTor: boolean;
|
enableTor: boolean;
|
||||||
|
/// Whether to route Monero wallet traffic through Tor
|
||||||
|
enableMoneroTor: boolean;
|
||||||
/// Whether to use the Monero RPC pool for load balancing (true) or custom nodes (false)
|
/// Whether to use the Monero RPC pool for load balancing (true) or custom nodes (false)
|
||||||
useMoneroRpcPool: boolean;
|
useMoneroRpcPool: boolean;
|
||||||
userHasSeenIntroduction: boolean;
|
userHasSeenIntroduction: boolean;
|
||||||
|
|
@ -126,6 +128,7 @@ const initialState: SettingsState = {
|
||||||
fetchFiatPrices: false,
|
fetchFiatPrices: false,
|
||||||
fiatCurrency: FiatCurrency.Usd,
|
fiatCurrency: FiatCurrency.Usd,
|
||||||
enableTor: true,
|
enableTor: true,
|
||||||
|
enableMoneroTor: false, // Default to not routing Monero traffic through Tor
|
||||||
useMoneroRpcPool: true, // Default to using RPC pool
|
useMoneroRpcPool: true, // Default to using RPC pool
|
||||||
userHasSeenIntroduction: false,
|
userHasSeenIntroduction: false,
|
||||||
rendezvousPoints: DEFAULT_RENDEZVOUS_POINTS,
|
rendezvousPoints: DEFAULT_RENDEZVOUS_POINTS,
|
||||||
|
|
@ -215,6 +218,9 @@ const alertsSlice = createSlice({
|
||||||
setTorEnabled(slice, action: PayloadAction<boolean>) {
|
setTorEnabled(slice, action: PayloadAction<boolean>) {
|
||||||
slice.enableTor = action.payload;
|
slice.enableTor = action.payload;
|
||||||
},
|
},
|
||||||
|
setEnableMoneroTor(slice, action: PayloadAction<boolean>) {
|
||||||
|
slice.enableMoneroTor = action.payload;
|
||||||
|
},
|
||||||
setUseMoneroRpcPool(slice, action: PayloadAction<boolean>) {
|
setUseMoneroRpcPool(slice, action: PayloadAction<boolean>) {
|
||||||
slice.useMoneroRpcPool = action.payload;
|
slice.useMoneroRpcPool = action.payload;
|
||||||
},
|
},
|
||||||
|
|
@ -236,6 +242,7 @@ export const {
|
||||||
setFetchFiatPrices,
|
setFetchFiatPrices,
|
||||||
setFiatCurrency,
|
setFiatCurrency,
|
||||||
setTorEnabled,
|
setTorEnabled,
|
||||||
|
setEnableMoneroTor,
|
||||||
setUseMoneroRpcPool,
|
setUseMoneroRpcPool,
|
||||||
setUserHasSeenIntroduction,
|
setUserHasSeenIntroduction,
|
||||||
addRendezvousPoint,
|
addRendezvousPoint,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ interface WalletState {
|
||||||
balance: GetMoneroBalanceResponse | null;
|
balance: GetMoneroBalanceResponse | null;
|
||||||
syncProgress: GetMoneroSyncProgressResponse | null;
|
syncProgress: GetMoneroSyncProgressResponse | null;
|
||||||
history: GetMoneroHistoryResponse | null;
|
history: GetMoneroHistoryResponse | null;
|
||||||
|
lowestCurrentBlock: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WalletSlice {
|
export interface WalletSlice {
|
||||||
|
|
@ -24,6 +25,7 @@ const initialState: WalletSlice = {
|
||||||
balance: null,
|
balance: null,
|
||||||
syncProgress: null,
|
syncProgress: null,
|
||||||
history: null,
|
history: null,
|
||||||
|
lowestCurrentBlock: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -42,6 +44,16 @@ export const walletSlice = createSlice({
|
||||||
slice,
|
slice,
|
||||||
action: PayloadAction<GetMoneroSyncProgressResponse>,
|
action: PayloadAction<GetMoneroSyncProgressResponse>,
|
||||||
) {
|
) {
|
||||||
|
slice.state.lowestCurrentBlock = Math.min(
|
||||||
|
// We ignore anything below 10 blocks as this may be something like wallet2
|
||||||
|
// sending a wrong value when it hasn't initialized yet
|
||||||
|
slice.state.lowestCurrentBlock < 10 ||
|
||||||
|
slice.state.lowestCurrentBlock === null
|
||||||
|
? Infinity
|
||||||
|
: slice.state.lowestCurrentBlock,
|
||||||
|
action.payload.current_block,
|
||||||
|
);
|
||||||
|
|
||||||
slice.state.syncProgress = action.payload;
|
slice.state.syncProgress = action.payload;
|
||||||
},
|
},
|
||||||
setHistory(slice, action: PayloadAction<GetMoneroHistoryResponse>) {
|
setHistory(slice, action: PayloadAction<GetMoneroHistoryResponse>) {
|
||||||
|
|
|
||||||
|
|
@ -440,6 +440,7 @@ async fn initialize_context(
|
||||||
.with_json(false)
|
.with_json(false)
|
||||||
.with_debug(true)
|
.with_debug(true)
|
||||||
.with_tor(settings.use_tor)
|
.with_tor(settings.use_tor)
|
||||||
|
.with_enable_monero_tor(settings.enable_monero_tor)
|
||||||
.with_tauri(tauri_handle.clone())
|
.with_tauri(tauri_handle.clone())
|
||||||
.build()
|
.build()
|
||||||
.await;
|
.await;
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ use structopt::clap;
|
||||||
use structopt::clap::ErrorKind;
|
use structopt::clap::ErrorKind;
|
||||||
use swap::asb::command::{parse_args, Arguments, Command};
|
use swap::asb::command::{parse_args, Arguments, Command};
|
||||||
use swap::asb::{cancel, punish, redeem, refund, safely_abort, EventLoop, Finality, KrakenRate};
|
use swap::asb::{cancel, punish, redeem, refund, safely_abort, EventLoop, Finality, KrakenRate};
|
||||||
use swap::common::tor::init_tor_client;
|
use swap::common::tor::{bootstrap_tor_client, create_tor_client};
|
||||||
use swap::common::tracing_util::Format;
|
use swap::common::tracing_util::Format;
|
||||||
use swap::common::{self, get_logs, warn_if_outdated};
|
use swap::common::{self, get_logs, warn_if_outdated};
|
||||||
use swap::database::{open_db, AccessMode};
|
use swap::database::{open_db, AccessMode};
|
||||||
|
|
@ -201,8 +201,10 @@ pub async fn main() -> Result<()> {
|
||||||
let kraken_rate = KrakenRate::new(config.maker.ask_spread, kraken_price_updates);
|
let kraken_rate = KrakenRate::new(config.maker.ask_spread, kraken_price_updates);
|
||||||
let namespace = XmrBtcNamespace::from_is_testnet(testnet);
|
let namespace = XmrBtcNamespace::from_is_testnet(testnet);
|
||||||
|
|
||||||
// Initialize Tor client
|
// Initialize and bootstrap Tor client
|
||||||
let tor_client = init_tor_client(&config.data.dir, None).await?.into();
|
let tor_client = create_tor_client(&config.data.dir).await?;
|
||||||
|
bootstrap_tor_client(tor_client.clone(), None).await?;
|
||||||
|
let tor_client = tor_client.into();
|
||||||
|
|
||||||
let (mut swarm, onion_addresses) = swarm::asb(
|
let (mut swarm, onion_addresses) = swarm::asb(
|
||||||
&seed,
|
&seed,
|
||||||
|
|
@ -495,10 +497,7 @@ async fn init_monero_wallet(
|
||||||
|
|
||||||
let (server_info, _status_receiver, _pool_handle) =
|
let (server_info, _status_receiver, _pool_handle) =
|
||||||
monero_rpc_pool::start_server_with_random_port(
|
monero_rpc_pool::start_server_with_random_port(
|
||||||
monero_rpc_pool::config::Config::new_random_port(
|
monero_rpc_pool::config::Config::new_random_port(config.data.dir.join("monero-rpc-pool")),
|
||||||
"127.0.0.1".to_string(),
|
|
||||||
config.data.dir.join("monero-rpc-pool"),
|
|
||||||
),
|
|
||||||
env_config.monero_network,
|
env_config.monero_network,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ pub mod tauri_bindings;
|
||||||
|
|
||||||
use crate::cli::api::tauri_bindings::SeedChoice;
|
use crate::cli::api::tauri_bindings::SeedChoice;
|
||||||
use crate::cli::command::{Bitcoin, Monero};
|
use crate::cli::command::{Bitcoin, Monero};
|
||||||
use crate::common::tor::init_tor_client;
|
use crate::common::tor::{bootstrap_tor_client, create_tor_client};
|
||||||
use crate::common::tracing_util::Format;
|
use crate::common::tracing_util::Format;
|
||||||
use crate::database::{open_db, AccessMode};
|
use crate::database::{open_db, AccessMode};
|
||||||
use crate::network::rendezvous::XmrBtcNamespace;
|
use crate::network::rendezvous::XmrBtcNamespace;
|
||||||
|
|
@ -204,6 +204,7 @@ pub struct ContextBuilder {
|
||||||
debug: bool,
|
debug: bool,
|
||||||
json: bool,
|
json: bool,
|
||||||
tor: bool,
|
tor: bool,
|
||||||
|
enable_monero_tor: bool,
|
||||||
tauri_handle: Option<TauriHandle>,
|
tauri_handle: Option<TauriHandle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,6 +228,7 @@ impl ContextBuilder {
|
||||||
debug: false,
|
debug: false,
|
||||||
json: false,
|
json: false,
|
||||||
tor: false,
|
tor: false,
|
||||||
|
enable_monero_tor: false,
|
||||||
tauri_handle: None,
|
tauri_handle: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -280,6 +282,12 @@ impl ContextBuilder {
|
||||||
self
|
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.
|
/// Takes the builder, initializes the context by initializing the wallets and other components and returns the Context.
|
||||||
pub async fn build(self) -> Result<Context> {
|
pub async fn build(self) -> Result<Context> {
|
||||||
// This is the data directory for the eigenwallet (wallet files)
|
// This is the data directory for the eigenwallet (wallet files)
|
||||||
|
|
@ -314,12 +322,29 @@ impl ContextBuilder {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start the rpc pool for the monero wallet
|
// 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) =
|
let (server_info, mut status_receiver, pool_handle) =
|
||||||
monero_rpc_pool::start_server_with_random_port(
|
monero_rpc_pool::start_server_with_random_port(
|
||||||
monero_rpc_pool::config::Config::new_random_port(
|
monero_rpc_pool::config::Config::new_random_port_with_tor_client(
|
||||||
"127.0.0.1".to_string(),
|
|
||||||
base_data_dir.join("monero-rpc-pool"),
|
base_data_dir.join("monero-rpc-pool"),
|
||||||
|
if self.enable_monero_tor {
|
||||||
|
unbootstrapped_tor_client.clone()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
),
|
),
|
||||||
match self.is_testnet {
|
match self.is_testnet {
|
||||||
true => crate::monero::Network::Stagenet,
|
true => crate::monero::Network::Stagenet,
|
||||||
|
|
@ -460,25 +485,25 @@ impl ContextBuilder {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let initialize_tor_client = async {
|
let bootstrap_tor_client_task = async {
|
||||||
// Don't init a tor client unless we should use it.
|
// Bootstrap the Tor client if we have one
|
||||||
if !self.tor {
|
match unbootstrapped_tor_client.clone() {
|
||||||
tracing::warn!("Internal Tor client not enabled, skipping initialization");
|
Some(tor_client) => {
|
||||||
return Ok(None);
|
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 maybe_tor_client = init_tor_client(&data_dir, tauri_handle.clone())
|
|
||||||
.await
|
|
||||||
.inspect_err(|err| {
|
|
||||||
tracing::warn!(%err, "Failed to create Tor client. We will continue without Tor");
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
Ok(maybe_tor_client)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let (bitcoin_wallet, tor) =
|
let (bitcoin_wallet, tor) =
|
||||||
tokio::try_join!(initialize_bitcoin_wallet, initialize_tor_client,)?;
|
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 we have a bitcoin wallet and a tauri handle, we start a background task
|
||||||
if let Some(wallet) = bitcoin_wallet.clone() {
|
if let Some(wallet) = bitcoin_wallet.clone() {
|
||||||
|
|
|
||||||
|
|
@ -1008,6 +1008,8 @@ pub struct TauriSettings {
|
||||||
pub electrum_rpc_urls: Vec<String>,
|
pub electrum_rpc_urls: Vec<String>,
|
||||||
/// Whether to initialize and use a tor client.
|
/// Whether to initialize and use a tor client.
|
||||||
pub use_tor: bool,
|
pub use_tor: bool,
|
||||||
|
/// Whether to route Monero wallet traffic through Tor
|
||||||
|
pub enable_monero_tor: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ use arti_client::{config::TorClientConfigBuilder, status::BootstrapStatus, Error
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use tor_rtcompat::tokio::TokioRustlsRuntime;
|
use tor_rtcompat::tokio::TokioRustlsRuntime;
|
||||||
|
|
||||||
pub async fn init_tor_client(
|
/// Creates an unbootstrapped Tor client
|
||||||
|
pub async fn create_tor_client(
|
||||||
data_dir: &Path,
|
data_dir: &Path,
|
||||||
tauri_handle: Option<TauriHandle>,
|
|
||||||
) -> Result<Arc<TorClient<TokioRustlsRuntime>>, Error> {
|
) -> Result<Arc<TorClient<TokioRustlsRuntime>>, Error> {
|
||||||
// We store the Tor state in the data directory
|
// We store the Tor state in the data directory
|
||||||
let data_dir = data_dir.join("tor");
|
let data_dir = data_dir.join("tor");
|
||||||
|
|
@ -23,20 +23,28 @@ pub async fn init_tor_client(
|
||||||
.build()
|
.build()
|
||||||
.expect("We initialized the Tor client all required attributes");
|
.expect("We initialized the Tor client all required attributes");
|
||||||
|
|
||||||
// Start the Arti client, and let it bootstrap a connection to the Tor network.
|
// Create the Arti client without bootstrapping
|
||||||
// (This takes a while to gather the necessary directory information.
|
|
||||||
// It uses cached information when possible.)
|
|
||||||
let runtime = TokioRustlsRuntime::current().expect("We are always running with tokio");
|
let runtime = TokioRustlsRuntime::current().expect("We are always running with tokio");
|
||||||
|
|
||||||
tracing::debug!("Bootstrapping Tor client");
|
tracing::debug!("Creating unbootstrapped Tor client");
|
||||||
|
|
||||||
let tor_client = TorClient::with_runtime(runtime)
|
let tor_client = TorClient::with_runtime(runtime)
|
||||||
.config(config)
|
.config(config)
|
||||||
.create_unbootstrapped_async()
|
.create_unbootstrapped_async()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
Ok(Arc::new(tor_client))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bootstraps an existing Tor client
|
||||||
|
pub async fn bootstrap_tor_client(
|
||||||
|
tor_client: Arc<TorClient<TokioRustlsRuntime>>,
|
||||||
|
tauri_handle: Option<TauriHandle>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
let mut bootstrap_events = tor_client.bootstrap_events();
|
let mut bootstrap_events = tor_client.bootstrap_events();
|
||||||
|
|
||||||
|
tracing::debug!("Bootstrapping Tor client");
|
||||||
|
|
||||||
// Create a background progress handle for the Tor bootstrap process
|
// Create a background progress handle for the Tor bootstrap process
|
||||||
// The handle manages the TauriHandle internally, so we don't need to worry about it anymore
|
// The handle manages the TauriHandle internally, so we don't need to worry about it anymore
|
||||||
let progress_handle =
|
let progress_handle =
|
||||||
|
|
@ -67,7 +75,7 @@ pub async fn init_tor_client(
|
||||||
},
|
},
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
Ok(Arc::new(tor_client))
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// A trait to convert the Tor bootstrap event into a TauriBootstrapStatus
|
// A trait to convert the Tor bootstrap event into a TauriBootstrapStatus
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue