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:
Mohan 2025-08-01 12:02:07 +02:00 committed by GitHub
parent cd12d17580
commit d21baa8350
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 957 additions and 741 deletions

36
Cargo.lock generated
View file

@ -4559,9 +4559,11 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"socket2 0.6.0",
"system-configuration 0.6.1",
"tokio",
"tower-service",
"tracing",
"windows-registry",
]
[[package]]
@ -6219,25 +6221,31 @@ name = "monero-rpc-pool"
version = "0.1.0"
dependencies = [
"anyhow",
"arti-client",
"axum",
"chrono",
"clap 4.5.41",
"futures",
"http-body-util",
"hyper 1.6.0",
"hyper-util",
"monero",
"monero-rpc",
"native-tls",
"rand 0.8.5",
"regex",
"serde",
"serde_json",
"sqlx",
"tokio",
"tokio-native-tls",
"tokio-test",
"tor-rtcompat",
"tower 0.4.13",
"tower-http 0.5.2",
"tracing",
"tracing-subscriber",
"typeshare",
"ureq",
"url",
"uuid",
]
@ -12872,21 +12880,6 @@ version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "url"
version = "2.5.4"
@ -13594,6 +13587,17 @@ dependencies = [
"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]]
name = "windows-result"
version = "0.1.2"

View file

@ -27,9 +27,17 @@ tower-http = { version = "0.5", features = ["cors"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
typeshare = { workspace = true }
ureq = { version = "2.10", default-features = false, features = ["tls"] }
url = "2.0"
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]
tokio-test = "0.4"

View file

@ -1,27 +1,53 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
use crate::TorClientArc;
#[derive(Clone)]
pub struct Config {
pub host: String,
pub port: u16,
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 {
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 {
host,
port,
data_dir,
tor_client: tor_client.into(),
}
}
pub fn new_random_port(host: String, data_dir: PathBuf) -> Self {
Self {
host,
port: 0,
data_dir,
}
pub fn new_random_port(data_dir: PathBuf) -> Self {
Self::new_random_port_with_tor_client(data_dir, None)
}
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)
}
}

View file

@ -1,6 +1,7 @@
use std::sync::Arc;
use anyhow::Result;
use arti_client::TorClient;
use axum::{
routing::{any, get},
Router,
@ -8,9 +9,13 @@ use axum::{
use monero::Network;
use tokio::task::JoinHandle;
use tor_rtcompat::tokio::TokioRustlsRuntime;
use tower_http::cors::CorsLayer;
use tracing::{error, info};
/// Type alias for the Tor client used throughout the crate
pub type TorClientArc = Arc<TorClient<TokioRustlsRuntime>>;
pub trait ToNetworkString {
fn to_network_string(&self) -> String;
}
@ -39,7 +44,7 @@ use proxy::{proxy_handler, stats_handler};
#[derive(Clone)]
pub struct AppState {
pub node_pool: Arc<NodePool>,
pub http_client: ureq::Agent,
pub tor_client: Option<TorClientArc>,
}
/// Manages background tasks for the RPC pool
@ -104,17 +109,9 @@ async fn create_app_with_receiver(
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 {
node_pool,
http_client,
tor_client: config.tor_client,
};
// Build the app
@ -177,14 +174,8 @@ pub async fn start_server_with_random_port(
tokio::sync::broadcast::Receiver<PoolStatus>,
PoolHandle,
)> {
// Clone the host before moving config
let host = config.host.clone();
// 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?;
let (app, status_receiver, pool_handle) = create_app_with_receiver(config, network).await?;
// Bind to port 0 to get a random available port
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))
}
/// 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
}

View file

@ -1,3 +1,4 @@
use arti_client::{TorClient, TorClientConfig};
use clap::Parser;
use monero_rpc_pool::{config::Config, run_server};
use tracing::info;
@ -47,6 +48,11 @@ struct Args {
#[arg(short, long)]
#[arg(help = "Enable verbose logging")]
verbose: bool,
#[arg(short, long)]
#[arg(help = "Enable Tor routing")]
#[arg(default_value = "true")]
tor: bool,
}
#[tokio::main]
@ -54,16 +60,46 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::new("trace"))
.with_env_filter(EnvFilter::new("info"))
.with_target(false)
.with_file(true)
.with_line_number(true)
.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.port,
std::env::temp_dir().join("monero-rpc-pool"),
tor_client,
);
info!(

View file

@ -1,6 +1,9 @@
use anyhow::{Context, Result};
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio::sync::broadcast;
use tracing::{debug, warn};
use tracing::warn;
use typeshare::typeshare;
use crate::database::Database;
@ -16,6 +19,7 @@ pub struct PoolStatus {
#[typeshare(serialized_as = "number")]
pub unsuccessful_health_checks: u64,
pub top_reliable_nodes: Vec<ReliableNodeInfo>,
pub bandwidth_kb_per_sec: f64,
}
#[derive(Debug, Clone, serde::Serialize)]
@ -26,10 +30,69 @@ pub struct ReliableNodeInfo {
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 {
db: Database,
network: String,
status_sender: broadcast::Sender<PoolStatus>,
bandwidth_tracker: Arc<Mutex<BandwidthTracker>>,
}
impl NodePool {
@ -39,6 +102,7 @@ impl NodePool {
db,
network,
status_sender,
bandwidth_tracker: Arc::new(Mutex::new(BandwidthTracker::new())),
};
(pool, status_receiver)
}
@ -63,13 +127,19 @@ impl NodePool {
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<()> {
let status = self.get_current_status().await?;
if let Err(e) = self.status_sender.send(status.clone()) {
warn!("Failed to send status update: {}", e);
} else {
debug!(?status, "Sent status update");
tracing::debug!(?status, "Sent status update");
}
Ok(())
@ -81,6 +151,12 @@ impl NodePool {
let (successful_checks, unsuccessful_checks) =
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
.into_iter()
.take(5)
@ -97,6 +173,7 @@ impl NodePool {
successful_health_checks: successful_checks,
unsuccessful_health_checks: unsuccessful_checks,
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>> {
use rand::seq::SliceRandom;
debug!(
tracing::debug!(
"Getting top reliable nodes for network {} (target: {})",
self.network, limit
self.network,
limit
);
let available_nodes = self
@ -149,7 +227,7 @@ impl NodePool {
selected_nodes.push(node);
}
debug!(
tracing::debug!(
"Pool size: {} nodes for network {} (target: {})",
selected_nodes.len(),
self.network,
@ -158,53 +236,4 @@ impl NodePool {
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

View file

@ -75,6 +75,12 @@ export default function MoneroPoolHealthBox() {
variant="outlined"
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>
);
};

View file

@ -38,6 +38,7 @@ import {
setFiatCurrency,
setTheme,
setTorEnabled,
setEnableMoneroTor,
setUseMoneroRpcPool,
setDonateToDevelopment,
} from "store/features/settingsSlice";
@ -91,6 +92,7 @@ export default function SettingsBox() {
<Table>
<TableBody>
<TorSettings />
<MoneroTorSettings />
<DonationTipSetting />
<ElectrumRpcUrlSetting />
<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
*/

View file

@ -1,14 +1,5 @@
import {
Box,
Typography,
CircularProgress,
Button,
Card,
CardContent,
Divider,
CardHeader,
LinearProgress,
} from "@mui/material";
import { Box, Typography, Card, LinearProgress } from "@mui/material";
import { useAppSelector } from "store/hooks";
import { PiconeroAmount } from "../../../other/Units";
import { FiatPiconeroAmount } from "../../../other/Units";
import StateIndicator from "./StateIndicator";
@ -30,23 +21,77 @@ export default function WalletOverview({
balance,
syncProgress,
}: WalletOverviewProps) {
const lowestCurrentBlock = useAppSelector(
(state) => state.wallet.state.lowestCurrentBlock,
);
const poolStatus = useAppSelector((state) => state.pool.status);
const pendingBalance =
parseFloat(balance.total_balance) - parseFloat(balance.unlocked_balance);
const isSyncing = syncProgress && syncProgress.progress_percentage < 100;
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 (
<Card sx={{ p: 2, position: "relative", borderRadius: 2 }} elevation={4}>
{syncProgress && syncProgress.progress_percentage < 100 && (
<LinearProgress
value={syncProgress.progress_percentage}
variant="determinate"
value={hasDirectKnowledge ? progressPercentage : undefined}
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={{
width: "100%",
position: "absolute",
top: 0,
left: 0,
width: "100%",
}}
/>
)}
@ -54,94 +99,104 @@ export default function WalletOverview({
{/* Balance */}
<Box
sx={{
display: "grid",
gridTemplateColumns: "1.5fr 1fr max-content",
rowGap: 0.5,
columnGap: 2,
display: "flex",
justifyContent: "space-between",
alignItems: "flex-start",
mb: 1,
}}
>
<Typography
variant="body2"
color="text.secondary"
sx={{ mb: 1, gridColumn: "1", gridRow: "1" }}
{/* Left side content */}
<Box
sx={{
display: "flex",
flexDirection: "row",
gap: 4,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: 0.5,
}}
>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Available Funds
</Typography>
<Typography variant="h4" sx={{ gridColumn: "1", gridRow: "2" }}>
<Typography variant="h4">
<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 variant="body2" color="text.secondary">
<FiatPiconeroAmount
amount={parseFloat(balance.unlocked_balance)}
/>
</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",
gridColumn: "2",
gridRow: "1",
alignSelf: "end",
}}
>
Pending
</Typography>
<Typography
variant="h5"
sx={{ gridColumn: "2", gridRow: "2", alignSelf: "center" }}
>
<Typography variant="h5">
<PiconeroAmount amount={pendingBalance} fixedPrecision={4} />
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{ gridColumn: "2", gridRow: "3" }}
>
<Typography variant="body2" color="text.secondary">
<FiatPiconeroAmount amount={pendingBalance} />
</Typography>
</>
</Box>
)}
</Box>
{/* Right side - simple approach */}
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-end",
gap: 2,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 1,
}}
>
<Typography variant="body2">
{isSyncing ? "syncing" : "synced"}
</Typography>
<StateIndicator
color={isSyncing ? "primary" : "success"}
pulsating={isSyncing}
/>
</Box>
{isSyncing && (
<Box sx={{ textAlign: "right" }}>
{isSyncing && hasDirectKnowledge && (
<Typography variant="body2" color="text.secondary">
{blocksLeft.toLocaleString()} blocks left
{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>
</Card>

View file

@ -21,30 +21,29 @@ export default function WalletPageLoadingState() {
{/* Balance */}
<Box
sx={{
display: "grid",
gridTemplateColumns: "1.5fr 1fr max-content",
rowGap: 0.5,
columnGap: 2,
display: "flex",
justifyContent: "space-between",
alignItems: "flex-start",
mb: 1,
}}
>
<Typography
variant="body2"
color="text.secondary"
sx={{ mb: 1, gridColumn: "1", gridRow: "1" }}
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: 0.5,
}}
>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Available Funds
</Typography>
<Typography variant="h4" sx={{ gridColumn: "1", gridRow: "2" }}>
<Typography variant="h4">
<Skeleton variant="text" width="80%" />
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{ gridColumn: "1", gridRow: "3" }}
>
<Typography variant="body2" color="text.secondary">
<Skeleton variant="text" width="40%" />
</Typography>
</Box>
<Box
sx={{
@ -61,7 +60,6 @@ export default function WalletPageLoadingState() {
gap: 1,
}}
>
<Typography variant="body2">loading</Typography>
<StateIndicator color="primary" pulsating={true} />
</Box>
</Box>

View file

@ -319,6 +319,8 @@ export async function initializeContext() {
// For Monero nodes, determine whether to use pool or custom node
const useMoneroRpcPool = store.getState().settings.useMoneroRpcPool;
const useMoneroTor = store.getState().settings.enableMoneroTor;
const moneroNodeUrl =
store.getState().settings.nodes[network][Blockchain.Monero][0] ?? null;
@ -341,6 +343,7 @@ export async function initializeContext() {
electrum_rpc_urls: bitcoinNodes,
monero_node_config: moneroNodeConfig,
use_tor: useTor,
enable_monero_tor: useMoneroTor,
};
logger.info("Initializing context with settings", tauriSettings);

View file

@ -19,6 +19,8 @@ export interface SettingsState {
fiatCurrency: FiatCurrency;
/// Whether to enable Tor for p2p connections
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)
useMoneroRpcPool: boolean;
userHasSeenIntroduction: boolean;
@ -126,6 +128,7 @@ const initialState: SettingsState = {
fetchFiatPrices: false,
fiatCurrency: FiatCurrency.Usd,
enableTor: true,
enableMoneroTor: false, // Default to not routing Monero traffic through Tor
useMoneroRpcPool: true, // Default to using RPC pool
userHasSeenIntroduction: false,
rendezvousPoints: DEFAULT_RENDEZVOUS_POINTS,
@ -215,6 +218,9 @@ const alertsSlice = createSlice({
setTorEnabled(slice, action: PayloadAction<boolean>) {
slice.enableTor = action.payload;
},
setEnableMoneroTor(slice, action: PayloadAction<boolean>) {
slice.enableMoneroTor = action.payload;
},
setUseMoneroRpcPool(slice, action: PayloadAction<boolean>) {
slice.useMoneroRpcPool = action.payload;
},
@ -236,6 +242,7 @@ export const {
setFetchFiatPrices,
setFiatCurrency,
setTorEnabled,
setEnableMoneroTor,
setUseMoneroRpcPool,
setUserHasSeenIntroduction,
addRendezvousPoint,

View file

@ -11,6 +11,7 @@ interface WalletState {
balance: GetMoneroBalanceResponse | null;
syncProgress: GetMoneroSyncProgressResponse | null;
history: GetMoneroHistoryResponse | null;
lowestCurrentBlock: number | null;
}
export interface WalletSlice {
@ -24,6 +25,7 @@ const initialState: WalletSlice = {
balance: null,
syncProgress: null,
history: null,
lowestCurrentBlock: null,
},
};
@ -42,6 +44,16 @@ export const walletSlice = createSlice({
slice,
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;
},
setHistory(slice, action: PayloadAction<GetMoneroHistoryResponse>) {

View file

@ -440,6 +440,7 @@ async fn initialize_context(
.with_json(false)
.with_debug(true)
.with_tor(settings.use_tor)
.with_enable_monero_tor(settings.enable_monero_tor)
.with_tauri(tauri_handle.clone())
.build()
.await;

View file

@ -25,7 +25,7 @@ use structopt::clap;
use structopt::clap::ErrorKind;
use swap::asb::command::{parse_args, Arguments, Command};
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::{self, get_logs, warn_if_outdated};
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 namespace = XmrBtcNamespace::from_is_testnet(testnet);
// Initialize Tor client
let tor_client = init_tor_client(&config.data.dir, None).await?.into();
// Initialize and bootstrap Tor client
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(
&seed,
@ -495,10 +497,7 @@ async fn init_monero_wallet(
let (server_info, _status_receiver, _pool_handle) =
monero_rpc_pool::start_server_with_random_port(
monero_rpc_pool::config::Config::new_random_port(
"127.0.0.1".to_string(),
config.data.dir.join("monero-rpc-pool"),
),
monero_rpc_pool::config::Config::new_random_port(config.data.dir.join("monero-rpc-pool")),
env_config.monero_network,
)
.await

View file

@ -3,7 +3,7 @@ pub mod tauri_bindings;
use crate::cli::api::tauri_bindings::SeedChoice;
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::database::{open_db, AccessMode};
use crate::network::rendezvous::XmrBtcNamespace;
@ -204,6 +204,7 @@ pub struct ContextBuilder {
debug: bool,
json: bool,
tor: bool,
enable_monero_tor: bool,
tauri_handle: Option<TauriHandle>,
}
@ -227,6 +228,7 @@ impl ContextBuilder {
debug: false,
json: false,
tor: false,
enable_monero_tor: false,
tauri_handle: None,
}
}
@ -280,6 +282,12 @@ impl ContextBuilder {
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<Context> {
// 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) =
monero_rpc_pool::start_server_with_random_port(
monero_rpc_pool::config::Config::new_random_port(
"127.0.0.1".to_string(),
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 => crate::monero::Network::Stagenet,
@ -460,25 +485,25 @@ impl ContextBuilder {
}
};
let initialize_tor_client = async {
// Don't init a tor client unless we should use it.
if !self.tor {
tracing::warn!("Internal Tor client not enabled, skipping initialization");
return Ok(None);
}
let maybe_tor_client = init_tor_client(&data_dir, tauri_handle.clone())
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 create Tor client. We will continue without Tor");
tracing::warn!(%err, "Failed to bootstrap Tor client. It will remain unbootstrapped");
})
.ok();
Ok(maybe_tor_client)
Ok(Some(tor_client))
}
None => Ok(None),
}
};
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 let Some(wallet) = bitcoin_wallet.clone() {

View file

@ -1008,6 +1008,8 @@ pub struct TauriSettings {
pub electrum_rpc_urls: Vec<String>,
/// Whether to initialize and use a tor client.
pub use_tor: bool,
/// Whether to route Monero wallet traffic through Tor
pub enable_monero_tor: bool,
}
#[typeshare]

View file

@ -8,9 +8,9 @@ use arti_client::{config::TorClientConfigBuilder, status::BootstrapStatus, Error
use futures::StreamExt;
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,
tauri_handle: Option<TauriHandle>,
) -> Result<Arc<TorClient<TokioRustlsRuntime>>, Error> {
// We store the Tor state in the data directory
let data_dir = data_dir.join("tor");
@ -23,20 +23,28 @@ pub async fn init_tor_client(
.build()
.expect("We initialized the Tor client all required attributes");
// Start the Arti client, and let it bootstrap a connection to the Tor network.
// (This takes a while to gather the necessary directory information.
// It uses cached information when possible.)
// Create the Arti client without bootstrapping
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)
.config(config)
.create_unbootstrapped_async()
.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();
tracing::debug!("Bootstrapping Tor client");
// 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
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