use ::monero::Network; use anyhow::{Context, Result}; use big_bytes::BigByte; use futures::{StreamExt, TryStreamExt}; use monero_rpc::wallet::{Client, MoneroWalletRpc as _}; use reqwest::header::CONTENT_LENGTH; use reqwest::Url; use std::io::ErrorKind; use std::path::{Path, PathBuf}; use std::process::Stdio; use tokio::fs::{remove_file, OpenOptions}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::process::{Child, Command}; use tokio_util::codec::{BytesCodec, FramedRead}; use tokio_util::io::StreamReader; #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] compile_error!("unsupported operating system"); #[cfg(target_os = "macos")] const DOWNLOAD_URL: &str = "http://downloads.getmonero.org/cli/monero-mac-x64-v0.17.3.0.tar.bz2"; #[cfg(all(target_os = "linux", target_arch = "x86_64"))] const DOWNLOAD_URL: &str = "https://downloads.getmonero.org/cli/monero-linux-x64-v0.17.3.0.tar.bz2"; #[cfg(all(target_os = "linux", target_arch = "arm"))] const DOWNLOAD_URL: &str = "https://downloads.getmonero.org/cli/monero-linux-armv7-v0.17.3.0.tar.bz2"; #[cfg(target_os = "windows")] const DOWNLOAD_URL: &str = "https://downloads.getmonero.org/cli/monero-win-x64-v0.17.3.0.zip"; #[cfg(any(target_os = "macos", target_os = "linux"))] const PACKED_FILE: &str = "monero-wallet-rpc"; #[cfg(target_os = "windows")] const PACKED_FILE: &str = "monero-wallet-rpc.exe"; #[derive(Debug, Clone, Copy, thiserror::Error)] #[error("monero wallet rpc executable not found in downloaded archive")] pub struct ExecutableNotFoundInArchive; pub struct WalletRpcProcess { _child: Child, port: u16, } impl WalletRpcProcess { pub fn endpoint(&self) -> Url { Url::parse(&format!("http://127.0.0.1:{}/json_rpc", self.port)) .expect("Static url template is always valid") } } pub struct WalletRpc { working_dir: PathBuf, } impl WalletRpc { pub async fn new(working_dir: impl AsRef) -> Result { let working_dir = working_dir.as_ref(); if !working_dir.exists() { tokio::fs::create_dir(working_dir).await?; } let monero_wallet_rpc = WalletRpc { working_dir: working_dir.to_path_buf(), }; if monero_wallet_rpc.archive_path().exists() { remove_file(monero_wallet_rpc.archive_path()).await?; } if !monero_wallet_rpc.exec_path().exists() { let mut options = OpenOptions::new(); let mut file = options .read(true) .write(true) .create_new(true) .open(monero_wallet_rpc.archive_path()) .await?; let response = reqwest::get(DOWNLOAD_URL).await?; let content_length = response.headers()[CONTENT_LENGTH] .to_str() .context("Failed to convert content-length to string")? .parse::()?; tracing::info!( "Downloading monero-wallet-rpc ({}) from {}", content_length.big_byte(2), DOWNLOAD_URL ); let byte_stream = response .bytes_stream() .map_err(|err| std::io::Error::new(ErrorKind::Other, err)); #[cfg(not(target_os = "windows"))] let mut stream = FramedRead::new( async_compression::tokio::bufread::BzDecoder::new(StreamReader::new(byte_stream)), BytesCodec::new(), ) .map_ok(|bytes| bytes.freeze()); #[cfg(target_os = "windows")] let mut stream = FramedRead::new(StreamReader::new(byte_stream), BytesCodec::new()) .map_ok(|bytes| bytes.freeze()); while let Some(chunk) = stream.next().await { file.write(&chunk?).await?; } file.flush().await?; Self::extract_archive(&monero_wallet_rpc).await?; } Ok(monero_wallet_rpc) } pub async fn run(&self, network: Network, daemon_address: &str) -> Result { let port = tokio::net::TcpListener::bind("127.0.0.1:0") .await? .local_addr()? .port(); tracing::debug!( %port, "Starting monero-wallet-rpc" ); let network_flag = match network { Network::Mainnet => { vec![] } Network::Stagenet => { vec!["--stagenet"] } Network::Testnet => { vec!["--testnet"] } }; let mut child = Command::new(self.exec_path()) .env("LANG", "en_AU.UTF-8") .stdout(Stdio::piped()) .kill_on_drop(true) .args(network_flag) .arg("--daemon-address") .arg(daemon_address) .arg("--rpc-bind-port") .arg(format!("{}", port)) .arg("--disable-rpc-login") .arg("--wallet-dir") .arg(self.working_dir.join("monero-data")) .spawn()?; let stdout = child .stdout .take() .expect("monero wallet rpc stdout was not piped parent process"); let mut reader = BufReader::new(stdout).lines(); #[cfg(not(target_os = "windows"))] while let Some(line) = reader.next_line().await? { if line.contains("Starting wallet RPC server") { break; } } // If we do not hear from the monero_wallet_rpc process for 3 seconds we assume // it is is ready #[cfg(target_os = "windows")] while let Ok(line) = tokio::time::timeout(std::time::Duration::from_secs(3), reader.next_line()).await { line?; } // Send a json rpc request to make sure monero_wallet_rpc is ready Client::localhost(port)?.get_version().await?; Ok(WalletRpcProcess { _child: child, port, }) } fn archive_path(&self) -> PathBuf { self.working_dir.join("monero-cli-wallet.archive") } fn exec_path(&self) -> PathBuf { self.working_dir.join(PACKED_FILE) } #[cfg(not(target_os = "windows"))] async fn extract_archive(monero_wallet_rpc: &Self) -> Result<()> { use anyhow::bail; use tokio_tar::Archive; let mut options = OpenOptions::new(); let file = options .read(true) .open(monero_wallet_rpc.archive_path()) .await?; let mut ar = Archive::new(file); let mut entries = ar.entries()?; loop { match entries.next().await { Some(file) => { let mut f = file?; if f.path()? .to_str() .context("Could not find convert path to str in tar ball")? .contains(PACKED_FILE) { f.unpack(monero_wallet_rpc.exec_path()).await?; break; } } None => bail!(ExecutableNotFoundInArchive), } } remove_file(monero_wallet_rpc.archive_path()).await?; Ok(()) } #[cfg(target_os = "windows")] async fn extract_archive(monero_wallet_rpc: &Self) -> Result<()> { use std::fs::File; use tokio::task::JoinHandle; use zip::ZipArchive; let archive_path = monero_wallet_rpc.archive_path(); let exec_path = monero_wallet_rpc.exec_path(); let extract: JoinHandle> = tokio::task::spawn_blocking(|| { let file = File::open(archive_path)?; let mut zip = ZipArchive::new(file)?; let name = zip .file_names() .find(|name| name.contains(PACKED_FILE)) .context(ExecutableNotFoundInArchive)? .to_string(); let mut rpc = zip.by_name(&name)?; let mut file = File::create(exec_path)?; std::io::copy(&mut rpc, &mut file)?; Ok(()) }); extract.await??; remove_file(monero_wallet_rpc.archive_path()).await?; Ok(()) } }