Integrated Tor arti Client (#750)

* tor: integrated client for listener, update tokio to last version

* tor: send transaction with integrated client

* build: code format

* tor: optimize bootstrap percent check

* tor: handle config build error

* tor: do not show slatepack address if tor not launched

* tor: handle keystore errors

* tor: keep service launched

* tor: bootstrap timeout, clean cache before client launch

* tor: post request timeout

* fix: post timeout

* build: update deps after merge

* tor: store arti runtime

* tor: do not send over tor if `manual` arg presents or `skip_send_attempt` set at config

* fix: arg name

* tor: handle arti runtime creation error

* tor: handle connection error

* tor: handle bootstrap error

* tor: handle start service name error

* tor: handle service config creation error

* tor: service proxy errors

* tor: handle service key creation emptiness

* build: format code

* build: update arti to 0.43, hyper to 0.10.1

* api: start foreign listener before tor connection

* tor: handle json parse errors

* tor: prevent to create several client runtimes, ability to recreate

* tor: return bootstrap error

* tor: handle host parse error

* tor: restart proxy on error

* tor: do not use integrated client when config value use_integrated is missing, add logging

* build: update lock file
This commit is contained in:
ardocrat
2026-06-21 14:20:34 +02:00
committed by GitHub
parent ca5686aa04
commit 411bcff003
17 changed files with 5070 additions and 828 deletions
Generated
+4296 -507
View File
File diff suppressed because it is too large Load Diff
+18 -20
View File
@@ -23,8 +23,8 @@ use uuid::Uuid;
use crate::config::{TorConfig, WalletConfig};
use crate::core::core::OutputFeatures;
use crate::core::global;
use crate::impls::HttpSlateSender;
use crate::impls::SlateSender as _;
use crate::impls::TorSlateSender;
use crate::keychain::{Identifier, Keychain};
use crate::libwallet::api_impl::owner_updater::{start_updater_log_thread, StatusMessage};
use crate::libwallet::api_impl::{owner, owner_updater};
@@ -669,17 +669,15 @@ where
};
// Helper functionality. If send arguments exist, attempt to send sync and
// finalize
let skip_tor = match send_args.as_ref() {
None => false,
Some(sa) => sa.skip_tor,
};
match send_args {
Some(sa) => {
let tor_config_lock = self.tor_config.lock();
let tc = tor_config_lock.clone();
let tc = match tc {
Some(mut c) => {
c.skip_send_attempt = Some(skip_tor);
if let Some(skip_tor) = sa.skip_tor {
c.skip_send_attempt = Some(skip_tor);
}
Some(c)
}
None => None,
@@ -845,7 +843,9 @@ where
let tc = tor_config_lock.clone();
let tc = match tc {
Some(mut c) => {
c.skip_send_attempt = Some(sa.skip_tor);
if let Some(skip_tor) = sa.skip_tor {
c.skip_send_attempt = Some(skip_tor);
}
Some(c)
}
None => None,
@@ -2495,7 +2495,7 @@ pub fn try_slatepack_sync_workflow(
slate: &Slate,
dest: &str,
tor_config: Option<TorConfig>,
tor_sender: Option<HttpSlateSender>,
tor_sender: Option<TorSlateSender>,
send_to_finalize: bool,
test_mode: bool,
) -> Result<Option<Slate>, Error> {
@@ -2505,7 +2505,7 @@ pub fn try_slatepack_sync_workflow(
}
}
let mut ret_slate = Slate::blank(2, false);
let mut send_sync = |mut sender: HttpSlateSender, method_str: &str| match sender
let mut send_sync = |mut sender: TorSlateSender, method_str: &str| match sender
.send_tx(&slate, send_to_finalize)
{
Ok(s) => {
@@ -2531,18 +2531,16 @@ pub fn try_slatepack_sync_workflow(
if test_mode {
None
} else {
match HttpSlateSender::with_socks_proxy(
&tor_addr.to_http_str(),
&tor_config.as_ref().unwrap().socks_proxy_addr,
&tor_config.as_ref().unwrap().send_config_dir,
tor_config.as_ref().unwrap().bridge.clone(),
tor_config.as_ref().unwrap().proxy.clone(),
) {
Ok(s) => Some(s),
Err(e) => {
debug!("Send (TOR): Cannot create TOR Slate sender {:?}", e);
None
if let Some(tc) = tor_config {
match TorSlateSender::new(&tor_addr.to_http_str(), tc) {
Ok(s) => Some(s),
Err(e) => {
debug!("Send (TOR): Cannot create TOR Slate sender {:?}", e);
None
}
}
} else {
return Err(Error::TorConfig("Tor config is not set".to_string()));
}
}
}
+15 -4
View File
@@ -206,16 +206,24 @@ fn comments() -> HashMap<String, String> {
"[tor]".to_string(),
"
#########################################
### TOR CONFIGURATION (Experimental) ###
### TOR CONFIGURATION ###
#########################################
"
.to_string(),
);
retval.insert(
"use_integrated".to_string(),
"
#Whether to use integrated Tor library
"
.to_string(),
);
retval.insert(
"skip_send_attempt".to_string(),
"
#Whether to skip send attempts (used for debugging)
#Whether to skip send attempts (default false)
"
.to_string(),
);
@@ -259,8 +267,11 @@ fn comments() -> HashMap<String, String> {
"
#Tor bridge relay: allow to send and receive via TOR in a country where it is censored.
#Enable it by entering a single bridge line. To disable it, you must comment it.
#Support of the transport: obfs4, meek and snowflake.
#obfs4proxy or snowflake client binary must be installed and on your path.
#Support of the transport: webtunnel, obfs4, and snowflake.
#webtunnel, obfs4proxy or snowflake client binary must be installed and on your path.
#Custom path for client binary
#bridge_bin_path = \"\"
#For example, the bridge line must be in the following format for obfs4 transport: \"obfs4 [IP:PORT] [FINGERPRINT] cert=[CERT] iat-mode=[IAT-MODE]\"
#bridge_line = \"\"
+6
View File
@@ -162,6 +162,8 @@ impl From<io::Error> for ConfigError {
/// Tor configuration
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TorConfig {
/// whether to use integrated Tor library
pub use_integrated: Option<bool>,
/// whether to skip any attempts to send via TOR
pub skip_send_attempt: Option<bool>,
/// Whether to start tor listener on listener startup (default true)
@@ -181,6 +183,7 @@ pub struct TorConfig {
impl Default for TorConfig {
fn default() -> TorConfig {
TorConfig {
use_integrated: Some(true),
skip_send_attempt: Some(false),
use_tor_listener: true,
socks_proxy_addr: "127.0.0.1:59050".to_owned(),
@@ -194,6 +197,8 @@ impl Default for TorConfig {
/// Tor Bridge Config
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TorBridgeConfig {
/// Path to bridge binary to use with integrated Tor library
pub bridge_bin_path: Option<String>,
/// Bridge Line
pub bridge_line: Option<String>,
/// Client Option
@@ -203,6 +208,7 @@ pub struct TorBridgeConfig {
impl Default for TorBridgeConfig {
fn default() -> TorBridgeConfig {
TorBridgeConfig {
bridge_bin_path: None,
bridge_line: None,
client_option: None,
}
+1 -1
View File
@@ -21,7 +21,7 @@ log = "0.4"
prettytable-rs = "0.10"
ring = "0.16"
term = "0.6"
tokio = { version = "0.2", features = ["full"] }
tokio = { version = "1.52.3", features = ["full"] }
uuid = { version = "0.8", features = ["serde", "v4"] }
url = "2.1"
chrono = { version = "0.4.11", features = ["serde"] }
+14 -8
View File
@@ -224,7 +224,7 @@ where
g_args.tls_conf.clone(),
tor_config.use_tor_listener,
test_mode,
Some(tor_config.clone()),
tor_config,
);
if let Err(e) = res {
error!("Error starting listener: {}", e);
@@ -335,7 +335,7 @@ pub struct SendArgs {
pub target_slate_version: Option<u16>,
pub payment_proof_address: Option<SlatepackAddress>,
pub ttl_blocks: Option<u64>,
pub skip_tor: bool,
pub skip_tor: Option<bool>,
pub outfile: Option<String>,
pub bridge: Option<String>,
pub slatepack_qr: bool,
@@ -431,7 +431,9 @@ where
if let Some(b) = args.bridge.clone() {
c.bridge.bridge_line = Some(b);
}
c.skip_send_attempt = Some(args.skip_tor);
if let Some(s) = args.skip_tor {
c.skip_send_attempt = Some(s);
}
Some(c)
}
None => None,
@@ -630,7 +632,7 @@ where
pub struct ReceiveArgs {
pub input_file: Option<String>,
pub input_slatepack_message: Option<String>,
pub skip_tor: bool,
pub skip_tor: Option<bool>,
pub outfile: Option<String>,
pub bridge: Option<String>,
pub slatepack_qr: bool,
@@ -666,7 +668,9 @@ where
if let Some(b) = args.bridge {
c.bridge.bridge_line = Some(b);
}
c.skip_send_attempt = Some(args.skip_tor);
if let Some(s) = args.skip_tor {
c.skip_send_attempt = Some(s);
}
Some(c)
}
None => None,
@@ -688,7 +692,7 @@ where
Ok(Some(_)) => {
println!();
println!(
"Transaction recieved and sent back to sender at {} for finalization.",
"Transaction received and sent back to sender at {} for finalization.",
dest
);
println!();
@@ -925,7 +929,7 @@ pub struct ProcessInvoiceArgs {
pub slate: Slate,
pub estimate_selection_strategies: bool,
pub ttl_blocks: Option<u64>,
pub skip_tor: bool,
pub skip_tor: Option<bool>,
pub outfile: Option<String>,
pub bridge: Option<String>,
pub slatepack_qr: bool,
@@ -1008,7 +1012,9 @@ where
if let Some(b) = args.bridge {
c.bridge.bridge_line = Some(b);
}
c.skip_send_attempt = Some(args.skip_tor);
if let Some(skip_tor) = args.skip_tor {
c.skip_send_attempt = Some(skip_tor);
}
Some(c)
}
None => None,
+75 -65
View File
@@ -48,6 +48,8 @@ use crate::apiwallet::{
};
use easy_jsonrpc_mw;
use easy_jsonrpc_mw::{Handler, MaybeReply};
use grin_wallet_impls::tor::arti::start_tor_service;
use grin_wallet_impls::tor::Tor;
lazy_static! {
pub static ref GRIN_OWNER_BASIC_REALM: HeaderValue =
@@ -82,32 +84,16 @@ fn check_middleware(
}
/// initiate the tor listener
fn init_tor_listener<L, C, K>(
wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
keychain_mask: Arc<Mutex<Option<SecretKey>>>,
fn init_tor_listener(
sec_key: SecretKey,
tor_dir: String,
addr: &str,
bridge: TorBridgeConfig,
tor_proxy: TorProxyConfig,
) -> Result<(tor_process::TorProcess, SlatepackAddress), Error>
where
L: WalletLCProvider<'static, C, K> + 'static,
C: NodeClient + 'static,
K: Keychain + 'static,
{
) -> Result<Tor, Error> {
info!("Starting external Tor Process listener.");
let mut process = tor_process::TorProcess::new();
let mask = keychain_mask.lock();
// eventually want to read a list of service config keys
let mut w_lock = wallet.lock();
let lc = w_lock.lc_provider()?;
let w_inst = lc.wallet_inst()?;
let k = w_inst.keychain((&mask).as_ref())?;
let parent_key_id = w_inst.parent_key_id();
let tor_dir = format!("{}/tor/listener", lc.get_top_level_directory()?);
let sec_key = address::address_from_derivation_path(&k, &parent_key_id, 0)
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
let onion_address = OnionV3Address::from_private(&sec_key.0)
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
let sp_address = SlatepackAddress::try_from(onion_address.clone())?;
let mut hm_tor_bridge: HashMap<String, String> = HashMap::new();
let mut tor_timeout = 20;
@@ -129,10 +115,6 @@ where
.map_err(|e| Error::TorConfig(format!("{}", e).into()))?;
}
warn!(
"Starting Tor Hidden Service for API listener at address {}, binding to {}",
onion_address, addr
);
tor_config::output_tor_listener_config(
&tor_dir,
addr,
@@ -149,7 +131,11 @@ where
.completion_percent(100)
.launch()
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
Ok((process, sp_address))
Ok(Tor {
process: Some(process),
service: None,
client: None,
})
}
/// Instantiate wallet Owner API for a single-use (command line) call
@@ -285,7 +271,7 @@ pub fn foreign_listener<L, C, K>(
tls_config: Option<TLSConfig>,
use_tor: bool,
test_mode: bool,
tor_config: Option<TorConfig>,
tor_config: TorConfig,
) -> Result<(), Error>
where
L: WalletLCProvider<'static, C, K> + 'static,
@@ -293,41 +279,27 @@ where
K: Keychain + 'static,
{
// Check if wallet has been opened first
{
let (sec_key, tor_dir, onion_address) = {
let mask = keychain_mask.lock();
let mut w_lock = wallet.lock();
let lc = w_lock.lc_provider()?;
let _ = lc.wallet_inst()?;
}
let (tor_bridge, tor_proxy) = match tor_config.clone() {
Some(s) => (s.bridge, s.proxy),
None => (TorBridgeConfig::default(), TorProxyConfig::default()),
let w_inst = lc.wallet_inst()?;
let k = w_inst.keychain((&mask).as_ref())?;
let parent_key_id = w_inst.parent_key_id();
let sec_key = address::address_from_derivation_path(&k, &parent_key_id, 0)
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
let tor_dir = format!("{}/tor/listener", lc.get_top_level_directory()?);
let onion_address = OnionV3Address::from_private(&sec_key.0)
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
(sec_key, tor_dir, onion_address)
};
// need to keep in scope while the main listener is running
let (_tor_process, address) = match use_tor {
true => {
match init_tor_listener(
wallet.clone(),
keychain_mask.clone(),
addr,
tor_bridge,
tor_proxy,
) {
Ok((tp, addr)) => (Some(tp), Some(addr)),
Err(e) => {
warn!("Unable to start TOR listener; Check that TOR executable is installed and on your path");
error!("Tor Error: {}", e);
warn!("Listener will be available via HTTP only");
(None, None)
}
}
}
false => (None, None),
};
let api_handler_v2 =
ForeignAPIHandlerV2::new(wallet, keychain_mask, test_mode, Mutex::new(tor_config));
let api_handler_v2 = ForeignAPIHandlerV2::new(
wallet,
keychain_mask,
test_mode,
Mutex::new(Some(tor_config.clone())),
);
let mut router = Router::new();
router
@@ -343,14 +315,52 @@ where
let api_thread = apis
.start(socket_addr, router, tls_config, api_chan)
.map_err(|_| Error::GenericError("API thread failed to start".to_string()))?;
warn!("HTTP Foreign listener started.");
if let Some(a) = address {
let qr_string = match QrCode::new(a.to_string()) {
Ok(qr) => qr.to_string(false, 3),
Err(_) => "Failed to generate QR code!".to_string(),
// Need to keep external process in scope while the listener is running.
let tor_service = if use_tor {
let use_integrated = tor_config.use_integrated.unwrap_or(false);
let res = if use_integrated {
start_tor_service(sec_key, &tor_dir, addr, tor_config.clone())
} else {
init_tor_listener(
sec_key,
tor_dir,
addr,
tor_config.bridge.clone(),
tor_config.proxy.clone(),
)
};
warn!("Slatepack Address is: {}\n{}", a, qr_string);
match res {
Ok(service) => {
warn!(
"Starting Tor Hidden Service for API listener at address {}, binding to {}",
onion_address, addr
);
Ok(Some(service))
}
Err(e) => {
warn!("Unable to start TOR listener");
error!("Tor Error: {}", e);
warn!("Listener is available on {}", addr);
Err(e)
}
}
} else {
Ok(None)
};
if tor_service.is_ok() {
if let Some(_) = tor_service.as_ref().unwrap() {
let sp_address = SlatepackAddress::try_from(onion_address.clone())?;
let qr_string = match QrCode::new(sp_address.to_string()) {
Ok(qr) => qr.to_string(false, 3),
Err(_) => "Failed to generate QR code!".to_string(),
};
warn!("Slatepack Address is: {}\n{}", sp_address, qr_string);
} else {
warn!("Listener is available on {}", addr);
}
}
api_thread
+18 -5
View File
@@ -22,20 +22,33 @@ ring = "0.16"
uuid = { version = "0.8", features = ["serde", "v4"] }
chrono = { version = "0.4.11", features = ["serde"] }
lazy_static = "1"
tokio = { version = "0.2", features = ["full"] }
reqwest = { version = "0.10", features = ["rustls-tls", "socks"] }
tokio = { version = "1.52.3", features = ["full"] }
reqwest = { version = "0.13.3", features = ["socks"] }
#Socks/Tor/Bridge/Proxy
byteorder = "1"
ed25519-dalek = "1.0.0-pre.4"
x25519-dalek = "0.6"
data-encoding = "2"
arti-ed25519-dalek = { version = "2.1.1", package = "ed25519-dalek" }
regex = "1.3"
timer = "0.2"
sysinfo = "0.29"
base64 = "0.12.0"
url = "2.1"
arti-client = { version = "0.43.0", features = ["static", "pt-client", "onion-service-service", "onion-service-client"] }
tor-rtcompat = { version = "0.43.0", features = ["static"] }
fs-mistrust = "0.14.2"
tor-hsservice = "0.43.0"
tor-hsrproxy = "0.43.0"
tor-keymgr = "0.43.0"
tor-llcrypto = "0.43.0"
tor-hscrypto = "0.43.0"
sha2 = "0.10.8"
curve25519-dalek = "4.1.3"
hyper = { version = "1.10.1", features = ["http1", "client"] }
hyper-util = { version = "0.1.20", features = ["tokio"] }
http-body-util = "0.1.3"
bytes = "1.11.1"
grin_wallet_util = { path = "../util", version = "5.4.0-alpha.1" }
grin_wallet_config = { path = "../config", version = "5.4.0-alpha.1" }
grin_wallet_libwallet = { path = "../libwallet", version = "5.4.0-alpha.1" }
+2 -2
View File
@@ -13,12 +13,12 @@
// limitations under the License.
mod file;
pub mod http;
mod slatepack;
pub mod tor;
pub use self::file::PathToSlate;
pub use self::http::HttpSlateSender;
pub use self::slatepack::PathToSlatepack;
pub use self::tor::TorSlateSender;
use crate::config::WalletConfig;
use crate::libwallet::{Error, Slate};
@@ -12,128 +12,111 @@
// See the License for the specific language governing permissions and
// limitations under the License.
/// HTTP Wallet 'plugin' implementation
use crate::client_utils::{Client, ClientError};
use crate::libwallet::slate_versions::{SlateVersion, VersionedSlate};
use crate::libwallet::{Error, Slate};
use crate::tor::bridge::TorBridge;
use crate::tor::proxy::TorProxy;
use crate::SlateSender;
use grin_wallet_config::types::{TorBridgeConfig, TorProxyConfig};
use grin_wallet_config::TorConfig;
use serde::Serialize;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::convert::TryFrom;
use std::net::SocketAddr;
use std::path::MAIN_SEPARATOR;
use std::path::PathBuf;
use std::sync::Arc;
use crate::tor::config as tor_config;
use crate::tor::process as tor_process;
const TOR_CONFIG_PATH: &str = "tor/sender";
use crate::client_utils::{Client, ClientError};
use crate::libwallet::slate_versions::{SlateVersion, VersionedSlate};
use crate::libwallet::{Error, Slate};
use crate::tor::arti::{start_tor_client, tor_post};
use crate::tor::bridge::TorBridge;
use crate::tor::process::TorProcess;
use crate::tor::proxy::TorProxy;
use crate::tor::{config as tor_config, Tor};
use crate::SlateSender;
#[derive(Clone)]
pub struct HttpSlateSender {
pub struct TorSlateSender {
base_url: String,
use_socks: bool,
socks_proxy_addr: Option<SocketAddr>,
tor_config_dir: String,
process: Option<Arc<tor_process::TorProcess>>,
bridge: TorBridgeConfig,
proxy: TorProxyConfig,
config: TorConfig,
tor: Arc<Tor>,
}
impl HttpSlateSender {
impl TorSlateSender {
/// Create, return Err if scheme is not "http"
fn new(base_url: &str) -> Result<HttpSlateSender, SchemeNotHttp> {
pub fn new(base_url: &str, config: TorConfig) -> Result<TorSlateSender, Error> {
if !base_url.starts_with("http") && !base_url.starts_with("https") {
Err(SchemeNotHttp)
Err(Error::GenericError("Scheme must be http".to_string()))
} else {
Ok(HttpSlateSender {
let tor_dir = {
let mut path = PathBuf::from(&config.send_config_dir);
path.push("tor");
path.push("sender");
path
};
let tor = if config.use_integrated.unwrap_or(false) {
start_tor_client(tor_dir.to_str().unwrap(), config.clone())?
} else {
Self::launch_tor_process(&config, &tor_dir)?
};
Ok(TorSlateSender {
base_url: base_url.to_owned(),
use_socks: false,
socks_proxy_addr: None,
tor_config_dir: String::from(""),
process: None,
bridge: TorBridgeConfig::default(),
proxy: TorProxyConfig::default(),
config,
tor: Arc::new(tor),
})
}
}
/// Switch to using socks proxy
pub fn with_socks_proxy(
base_url: &str,
proxy_addr: &str,
tor_config_dir: &str,
tor_bridge: TorBridgeConfig,
tor_proxy: TorProxyConfig,
) -> Result<HttpSlateSender, SchemeNotHttp> {
let mut ret = Self::new(base_url)?;
ret.use_socks = true;
//TODO: Unwrap
ret.socks_proxy_addr = Some(SocketAddr::V4(proxy_addr.parse().unwrap()));
ret.tor_config_dir = tor_config_dir.into();
ret.bridge = tor_bridge;
ret.proxy = tor_proxy;
Ok(ret)
}
/// Launch external Tor process.
fn launch_tor_process(config: &TorConfig, tor_dir: &PathBuf) -> Result<Tor, Error> {
let mut tor = TorProcess::new();
let socks_proxy_addr = SocketAddr::V4(
config
.socks_proxy_addr
.parse()
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?,
);
info!("Starting TOR Process for send at {:?}", socks_proxy_addr);
/// launch TOR process
pub fn launch_tor(&mut self) -> Result<(), Error> {
// set up tor send process if needed
let mut tor = tor_process::TorProcess::new();
if self.use_socks && self.process.is_none() {
let tor_dir = format!(
"{}{}{}",
&self.tor_config_dir, MAIN_SEPARATOR, TOR_CONFIG_PATH
);
info!(
"Starting TOR Process for send at {:?}",
self.socks_proxy_addr
);
let mut hm_tor_bridge: HashMap<String, String> = HashMap::new();
if self.bridge.bridge_line.is_some() {
let bridge_struct = TorBridge::try_from(self.bridge.clone())
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
hm_tor_bridge = bridge_struct
.to_hashmap()
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
}
let mut hm_tor_proxy: HashMap<String, String> = HashMap::new();
if self.proxy.transport.is_some() || self.proxy.allowed_port.is_some() {
let proxy = TorProxy::try_from(self.proxy.clone())
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
hm_tor_proxy = proxy
.to_hashmap()
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
}
tor_config::output_tor_sender_config(
&tor_dir,
&self.socks_proxy_addr.unwrap().to_string(),
hm_tor_bridge,
hm_tor_proxy,
)
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
// Start TOR process
tor.torrc_path(&format!("{}/torrc", &tor_dir))
.working_dir(&tor_dir)
.timeout(20)
.completion_percent(100)
.launch()
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
self.process = Some(Arc::new(tor));
let mut hm_tor_bridge: HashMap<String, String> = HashMap::new();
if config.bridge.bridge_line.is_some() {
let bridge_struct = TorBridge::try_from(config.bridge.clone())
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
hm_tor_bridge = bridge_struct
.to_hashmap()
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
}
Ok(())
let mut hm_tor_proxy: HashMap<String, String> = HashMap::new();
if config.proxy.transport.is_some() || config.proxy.allowed_port.is_some() {
let proxy = TorProxy::try_from(config.proxy.clone())
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
hm_tor_proxy = proxy
.to_hashmap()
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
}
tor_config::output_tor_sender_config(
tor_dir.to_str().unwrap(),
socks_proxy_addr.to_string().as_str(),
hm_tor_bridge,
hm_tor_proxy,
)
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
// Start TOR process
let mut path = tor_dir.clone();
path.push("torrc");
tor.torrc_path(path.to_str().unwrap())
.working_dir(tor_dir.to_str().unwrap())
.timeout(20)
.completion_percent(100)
.launch()
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
Ok(Tor {
process: Some(tor),
service: None,
client: None,
})
}
/// Check version of the listening wallet
pub fn check_other_version(&mut self, url: &str) -> Result<SlateVersion, Error> {
self.launch_tor()?;
let req = json!({
"jsonrpc": "2.0",
"method": "check_version",
@@ -141,7 +124,7 @@ impl HttpSlateSender {
"params": []
});
let res: String = self.post(url, None, req).map_err(|e| {
let res: String = self.post(url, req).map_err(|e| {
let mut report = format!("Performing version check (is recipient listening?): {}", e);
let err_string = format!("{}", e);
if err_string.contains("404") {
@@ -154,7 +137,8 @@ impl HttpSlateSender {
Error::ClientCallback(report)
})?;
let res: Value = serde_json::from_str(&res).unwrap();
let res: Value = serde_json::from_str(&res)
.map_err(|_| Error::ClientCallback(format!("Can not parse response {:?}", res)))?;
trace!("Response: {}", res);
if res["error"] != json!(null) {
let report = format!(
@@ -168,9 +152,11 @@ impl HttpSlateSender {
let resp_value = res["result"]["Ok"].clone();
trace!("resp_value: {}", resp_value.clone());
let foreign_api_version: u16 =
serde_json::from_value(resp_value["foreign_api_version"].clone()).unwrap();
serde_json::from_value(resp_value["foreign_api_version"].clone())
.map_err(|_| Error::ClientCallback(format!("Can not parse response {:?}", res)))?;
let supported_slate_versions: Vec<String> =
serde_json::from_value(resp_value["supported_slate_versions"].clone()).unwrap();
serde_json::from_value(resp_value["supported_slate_versions"].clone())
.map_err(|_| Error::ClientCallback(format!("Can not parse response {:?}", res)))?;
// trivial tests for now, but will be expanded later
if foreign_api_version < 2 {
@@ -188,32 +174,33 @@ impl HttpSlateSender {
Err(Error::ClientCallback(report))
}
fn post<IN>(
&self,
url: &str,
api_secret: Option<String>,
input: IN,
) -> Result<String, ClientError>
fn post<IN>(&self, url: &str, input: IN) -> Result<String, ClientError>
where
IN: Serialize,
{
let client = if !self.use_socks {
Client::new()
let res = if self.tor.process.is_some() {
let socks_proxy_addr =
SocketAddr::V4(self.config.socks_proxy_addr.parse().map_err(|_| {
ClientError::Internal("Socks proxy address is not set".to_string())
})?);
let client = Client::with_proxy(socks_proxy_addr, "socks5h://")
.map_err(|_| ClientError::Internal("Unable to create http client".into()))?;
let req = client.create_post_request(url, None, &input)?;
let res = client.send_request(req)?;
res
} else {
Client::with_proxy(
self.socks_proxy_addr
.ok_or_else(|| ClientError::Internal("No socks proxy address set".into()))?,
"socks5h://",
)
}
.map_err(|_| ClientError::Internal("Unable to create http client".into()))?;
let req = client.create_post_request(url, api_secret, &input)?;
let res = client.send_request(req)?;
if let Some(client) = &self.tor.client {
tor_post(client.clone(), &input, url)
.map_err(|e| ClientError::RequestError(format!("{:?}", e)))?
} else {
return Err(ClientError::Internal("Tor is not configured".to_string()));
}
};
Ok(res)
}
}
impl SlateSender for HttpSlateSender {
impl SlateSender for TorSlateSender {
fn send_tx(&mut self, slate: &Slate, finalize: bool) -> Result<Slate, Error> {
let trailing = match self.base_url.ends_with('/') {
true => "",
@@ -221,8 +208,6 @@ impl SlateSender for HttpSlateSender {
};
let url_str = format!("{}{}v2/foreign", self.base_url, trailing);
self.launch_tor()?;
let slate_send = match self.check_other_version(&url_str)? {
SlateVersion::V4 => VersionedSlate::into_version(slate.clone(), SlateVersion::V4)?,
};
@@ -250,7 +235,7 @@ impl SlateSender for HttpSlateSender {
trace!("Sending receive_tx request: {}", req);
let res: String = self.post(&url_str, None, req).map_err(|e| {
let res: String = self.post(&url_str, req).map_err(|e| {
let report = format!(
"Sending transaction slate to other wallet (is recipient listening?): {}",
e
@@ -258,7 +243,8 @@ impl SlateSender for HttpSlateSender {
Error::ClientCallback(report)
})?;
let res: Value = serde_json::from_str(&res).unwrap();
let res: Value = serde_json::from_str(&res)
.map_err(|_| Error::ClientCallback(format!("Can not parse response {:?}", res)))?;
trace!("Response: {}", res);
if res["error"] != json!(null) {
let report = format!(
@@ -272,22 +258,15 @@ impl SlateSender for HttpSlateSender {
let slate_value = res["result"]["Ok"].clone();
trace!("slate_value: {}", slate_value);
let slate = Slate::deserialize_upgrade(&serde_json::to_string(&slate_value).unwrap())
.map_err(|e| {
if let Ok(slate_value) = serde_json::to_string(&slate_value) {
let slate = Slate::deserialize_upgrade(&slate_value).map_err(|e| {
error!("Error deserializing response slate: {}", e);
Error::SlateDeser
})?;
Ok(slate)
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub struct SchemeNotHttp;
impl Into<Error> for SchemeNotHttp {
fn into(self) -> Error {
let err_str = "url scheme must be http".to_string();
Error::GenericError(err_str)
Ok(slate)
} else {
error!("Error deserializing response slate: {}", slate_value);
Err(Error::SlateDeser)
}
}
}
+7 -23
View File
@@ -21,23 +21,14 @@ use reqwest::{ClientBuilder, Method, Proxy, RequestBuilder};
use serde::{Deserialize, Serialize};
use serde_json;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
use std::sync::Arc;
use std::time::Duration;
use tokio::runtime::{Builder, Handle, Runtime};
// Global Tokio runtime.
// Needs a `Mutex` because `Runtime::block_on` requires mutable access.
// Tokio v0.3 requires immutable self, but we are waiting on upstream
// updates before we can upgrade.
// See: https://github.com/seanmonstar/reqwest/pull/1076
lazy_static! {
pub static ref RUNTIME: Arc<Mutex<Runtime>> = Arc::new(Mutex::new(
Builder::new()
.threaded_scheduler()
.enable_all()
.build()
.unwrap()
));
pub static ref RUNTIME: Arc<Runtime> =
Arc::new(Builder::new_multi_thread().enable_all().build().unwrap());
}
#[derive(Clone, Eq, thiserror::Error, PartialEq, Debug)]
@@ -273,18 +264,11 @@ impl Client {
if Handle::try_current().is_ok() {
let rt = RUNTIME.clone();
let client = self.clone();
std::thread::spawn(move || {
rt.lock()
.unwrap()
.block_on(async { client.send_request_async(req).await })
})
.join()
.unwrap()
} else {
RUNTIME
.lock()
std::thread::spawn(move || rt.block_on(async { client.send_request_async(req).await }))
.join()
.unwrap()
.block_on(self.send_request_async(req))
} else {
RUNTIME.block_on(self.send_request_async(req))
}
}
}
+2 -2
View File
@@ -42,8 +42,8 @@ pub mod test_framework;
pub mod tor;
pub use crate::adapters::{
HttpSlateSender, PathToSlate, PathToSlatepack, SlateGetter, SlatePutter, SlateReceiver,
SlateSender,
PathToSlate, PathToSlatepack, SlateGetter, SlatePutter, SlateReceiver, SlateSender,
TorSlateSender,
};
pub use crate::error::Error;
pub use crate::lifecycle::DefaultLCProvider;
+1 -1
View File
@@ -272,7 +272,7 @@ impl NodeClient for HTTPNodeClient {
let rt = RUNTIME.clone();
let res: Result<Vec<_>, _> =
std::thread::spawn(move || rt.lock().unwrap().block_on(async move { task.await }))
std::thread::spawn(move || rt.block_on(async move { task.await }))
.join()
.unwrap();
+402
View File
@@ -0,0 +1,402 @@
// Copyright 2026 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::tor::{ArtiRuntimeWrapper, Tor};
use arti_client::config::pt::TransportConfigBuilder;
use arti_client::config::{BridgeConfigBuilder, TorClientConfigBuilder};
use arti_client::{TorClient, TorClientConfig};
use arti_ed25519_dalek::hazmat::ExpandedSecretKey;
use bytes::Bytes;
use curve25519_dalek::digest::Digest;
use fs_mistrust::Mistrust;
use grin_util::secp::SecretKey;
use grin_wallet_config::TorConfig;
use grin_wallet_libwallet::Error;
use grin_wallet_util::OnionV3Address;
use http_body_util::{BodyExt, Full};
use hyper::{Request, Uri};
use hyper_util::rt::TokioIo;
use lazy_static::lazy_static;
use serde::Serialize;
use sha2::Sha512;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::sync::{Arc, LazyLock, Mutex};
use std::thread;
use std::time::Duration;
use tor_hscrypto::pk::{HsIdKey, HsIdKeypair};
use tor_hsrproxy::config::{
Encapsulation, ProxyAction, ProxyConfigBuilder, ProxyPattern, ProxyRule, TargetAddr,
};
use tor_hsrproxy::OnionServiceReverseProxy;
use tor_hsservice::config::OnionServiceConfigBuilder;
use tor_hsservice::{HsIdKeypairSpecifier, HsIdPublicKeySpecifier, HsNickname};
use tor_keymgr::config::CfgPath;
use tor_keymgr::{ArtiNativeKeystore, KeyMgrBuilder, KeystoreSelector};
use tor_llcrypto::pk::ed25519::ExpandedKeypair;
use tor_rtcompat::tokio::TokioNativeTlsRuntime;
use tor_rtcompat::{SleepProviderExt, ToplevelBlockOn};
// Arti Tokio runtime.
lazy_static! {
pub static ref ARTI_RUNTIME: LazyLock<Mutex<Option<ArtiRuntimeWrapper>>> =
LazyLock::new(|| Mutex::new(ArtiRuntimeWrapper::create().ok()));
}
/// Get Tor client runtime.
fn runtime() -> Result<TokioNativeTlsRuntime, Error> {
let mut runtime = ARTI_RUNTIME.lock().unwrap();
let r = match runtime.as_ref() {
None => runtime.insert(ArtiRuntimeWrapper::create()?),
Some(r) => r
};
Ok(r.runtime.clone())
}
/// Start Tor service from provided key.
pub fn start_tor_service(
key: SecretKey,
tor_dir: &str,
addr: &str,
config: TorConfig,
) -> Result<Tor, Error> {
info!("Starting integrated Tor listener.");
let use_proxy = config.proxy.transport.is_some() && config.proxy.address.is_some();
if use_proxy {
info!("Proxy configuration will be ignored.");
}
let state_path = Path::new(&tor_dir).join("state");
let cache_path = Path::new(&tor_dir).join("cache");
let (client, config) = init_client(&state_path, &cache_path, config)?;
// Add service key to keystore.
let onion_address =
OnionV3Address::from_private(&key.0).map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
let hs = HsNickname::new(onion_address.to_string())
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
let keystore_path = Path::new(&state_path).join("keystore");
let _ = add_service_key(config.fs_mistrust(), &key, &hs, keystore_path)?;
// Launch Onion service.
let service_config = OnionServiceConfigBuilder::default()
.nickname(hs.clone())
.build()
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
let running_onion_service = match client.launch_onion_service(service_config) {
Ok(res) => {
if let Some((service, mut request)) = res {
let addr: SocketAddr = addr
.parse()
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
let c = client.clone();
thread::spawn(move || {
c.clone().runtime().block_on(async move {
// Launch service proxy.
async fn run_proxy<S> (c: Arc<TorClient<TokioNativeTlsRuntime>>,
addr: SocketAddr,
request: &mut S,
hs: HsNickname)
where
S: futures::Stream<Item = tor_hsservice::RendRequest> + Unpin + Send + 'static,
{
match run_service_proxy(c.clone(), addr, request, hs.clone()).await {
Ok(_) => {
info!("Tor proxy stopped");
},
Err(e) => {
error!("Tor proxy error: {:?}, restarting", e);
tokio::time::sleep(Duration::from_millis(1000)).await;
Box::pin(run_proxy(c, addr, request, hs)).await;
}
}
}
run_proxy(c.clone(), addr, &mut request, hs.clone()).await;
})
});
service
} else {
return Err(Error::TorProcess("Can not launch onion service".to_owned()));
}
}
Err(e) => return Err(Error::TorProcess(format!("{:?}", e))),
};
Ok(Tor {
process: None,
service: Some(running_onion_service),
client: Some(client),
})
}
/// Start Tor client to send requests.
pub fn start_tor_client(tor_dir: &str, config: TorConfig) -> Result<Tor, Error> {
let state_path = Path::new(tor_dir).join("state");
let cache_path = Path::new(tor_dir).join("cache");
let (client, _) = init_client(&state_path, &cache_path, config)?;
Ok(Tor {
process: None,
service: None,
client: Some(client),
})
}
/// Tor request timeout in milliseconds.
const REQUEST_TIMEOUT_MS: u64 = 60000;
/// Make POST request with provided client.
pub fn tor_post<IN>(
client: Arc<TorClient<TokioNativeTlsRuntime>>,
input: &IN,
url: &str,
) -> Result<String, Error>
where
IN: Serialize,
{
let json = serde_json::to_string(input)
.map_err(|_| Error::GenericError("Could not serialize data to JSON".to_owned()))?;
let url = url.to_string();
let url: Uri = url
.parse()
.map_err(|_| Error::GenericError(format!("Bad URL: {}", url)))?;
let host = match url.host() {
None => return Err(Error::GenericError(format!("URL {} has bad host", url))),
Some(h) => h
}.to_string();
let res: Result<String, Error> = thread::spawn(move || {
let c = client.clone();
client.runtime().block_on(async move {
let res = c
.runtime()
.timeout(Duration::from_millis(REQUEST_TIMEOUT_MS), async {
let stream = c
.connect((host, url.port_u16().unwrap_or(80)))
.await
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
let (mut request_sender, connection) =
hyper::client::conn::http1::handshake(TokioIo::new(stream))
.await
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
// Spawn a task to poll the connection and drive the HTTP state.
tokio::spawn(async move {
if let Err(e) = connection.await {
error!("Tor connection error: {}", e);
}
});
let resp = request_sender
.send_request(
Request::builder()
.uri(url)
.method("POST")
.body::<Full<Bytes>>(Full::from(json))
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?,
)
.await
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
let body_resp = resp
.into_body()
.collect()
.await
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
let body = body_resp.to_bytes().into();
let body_text = String::from_utf8(body)
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
Ok(body_text)
})
.await;
match res {
Err(e) => Err(Error::TorProcess(format!("{:?}", e))),
Ok(body) => Ok(body),
}
})
})
.join()
.unwrap_or_else(|e| return Err(Error::TorProcess(format!("{:?}", e))))?;
res
}
/// Client Bootstrap timeout in milliseconds.
const BOOTSTRAP_TIMEOUT_MS: u64 = 60000;
/// Create Tor client.
fn init_client(
state_path: &PathBuf,
cache_path: &PathBuf,
config: TorConfig,
) -> Result<(Arc<TorClient<TokioNativeTlsRuntime>>, TorClientConfig), Error> {
let mut builder = TorClientConfigBuilder::from_directories(&state_path, cache_path);
builder.address_filter().allow_onion_addrs(true);
// Configure bridge.
if let Some(bridge_line) = config.bridge.bridge_line {
let bridge: BridgeConfigBuilder = bridge_line
.parse()
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
builder.bridges().bridges().push(bridge.clone());
match bridge.get_transport() {
None => {
return Err(Error::TorConfig(format!(
"No transport found at {}",
bridge_line
)))
}
Some(t) => {
// Now configure bridge transport. (Requires the "pt-client" feature)
let bin_path = config.bridge.bridge_bin_path.unwrap_or(t.to_owned());
let mut transport = TransportConfigBuilder::default();
transport
.protocols(vec![t
.parse()
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?])
.path(CfgPath::new(bin_path))
.run_on_startup(true);
builder.bridges().transports().push(transport);
}
}
}
let config = builder
.build()
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
// Launch client.
let r = runtime()?;
let client = TorClient::with_runtime(r)
.config(config.clone())
.create_unbootstrapped()
.map_err(|e| Error::TorProcess(format!("{:?}", e)))?;
let c = client.clone();
let res = client.runtime().block_on(async move {
let bootstrap = async || {
return match c.bootstrap().await {
Ok(_) => {
let mut percent = 0.0;
let mut prev_percent = 0.0;
while percent < 1.0 {
percent = c.bootstrap_status().as_frac();
if percent != prev_percent {
info!("Starting Tor {}%", percent * 100.0);
}
prev_percent = percent;
tokio::time::sleep(Duration::from_millis(1000)).await;
}
Ok(())
}
Err(e) => Err(e),
};
};
match c
.runtime()
.timeout(Duration::from_millis(BOOTSTRAP_TIMEOUT_MS), bootstrap())
.await
{
Ok(r) => {
match r {
Err(e) => Err(Error::TorProcess(format!("{:?}", e))),
Ok(_) => Ok(())
}
},
Err(e) => Err(Error::TorProcess(format!("{:?}", e))),
}
});
match res {
Ok(_) => {
info!("Tor client bootstrapped successfully");
Ok((client, config))
}
Err(e) => Err(e),
}
}
/// Launch Onion service proxy.
async fn run_service_proxy<S>(
client: Arc<TorClient<TokioNativeTlsRuntime>>,
addr: SocketAddr,
request: &mut S,
nickname: HsNickname,
) -> Result<(), Error>
where
S: futures::Stream<Item = tor_hsservice::RendRequest> + Unpin + Send + 'static,
{
let runtime = client.runtime().clone();
// Setup proxy to forward request from Tor address to local address.
let proxy_rule = ProxyRule::new(
ProxyPattern::one_port(80).map_err(|e| Error::TorConfig(format!("{}", e)))?,
ProxyAction::Forward(Encapsulation::Simple, TargetAddr::Inet(addr)),
);
let mut proxy_cfg_builder = ProxyConfigBuilder::default();
proxy_cfg_builder.set_proxy_ports(vec![proxy_rule]);
let proxy_cfg = proxy_cfg_builder
.build()
.map_err(|e| Error::TorConfig(format!("{}", e)))?;
let proxy = OnionServiceReverseProxy::new(proxy_cfg);
// Start proxy for launched service.
proxy
.handle_requests(runtime, nickname, request)
.await
.map_err(|e| Error::TorProcess(format!("{:?}", e)))
}
/// Save Onion service key to keystore.
fn add_service_key(
mistrust: &Mistrust,
key: &SecretKey,
hs_nickname: &HsNickname,
path: PathBuf,
) -> Result<(), Error> {
let arti_store = ArtiNativeKeystore::from_path_and_mistrust(path, mistrust)
.map_err(|e| Error::TorProcess(format!("{}", e)))?;
let key_manager = KeyMgrBuilder::default()
.primary_store(Box::new(arti_store))
.build()
.map_err(|e| Error::TorProcess(format!("{}", e)))?;
let expanded_sk =
ExpandedSecretKey::from_bytes(Sha512::default().chain_update(key).finalize().as_ref());
let mut sk_bytes = [0_u8; 64];
sk_bytes[0..32].copy_from_slice(&expanded_sk.scalar.to_bytes());
sk_bytes[32..64].copy_from_slice(&expanded_sk.hash_prefix);
match ExpandedKeypair::from_secret_key_bytes(sk_bytes) {
None => {
return Err(Error::TorProcess(
"Hidden service key can not be created".into(),
))
}
Some(expanded_kp) => {
key_manager
.insert(
HsIdKey::from(expanded_kp.public().clone()),
&HsIdPublicKeySpecifier::new(hs_nickname.clone()),
KeystoreSelector::Primary,
true,
)
.map_err(|e| Error::TorProcess(format!("{}", e)))?;
key_manager
.insert(
HsIdKeypair::from(expanded_kp),
&HsIdKeypairSpecifier::new(hs_nickname.clone()),
KeystoreSelector::Primary,
true,
)
.map_err(|e| Error::TorProcess(format!("{}", e)))?;
}
}
Ok(())
}
+26
View File
@@ -12,7 +12,33 @@
// See the License for the specific language governing permissions and
// limitations under the License.
pub mod arti;
pub mod bridge;
pub mod config;
pub mod process;
pub mod proxy;
/// Running Tor instance control.
pub struct Tor {
/// External to process control.
pub process: Option<process::TorProcess>,
/// Integrated service.
pub service: Option<std::sync::Arc<tor_hsservice::RunningOnionService>>,
/// Integrated client.
pub client:
Option<std::sync::Arc<arti_client::TorClient<tor_rtcompat::tokio::TokioNativeTlsRuntime>>>,
}
/// Arti client runtime wrapper.
#[derive(Clone)]
pub struct ArtiRuntimeWrapper {
pub runtime: tor_rtcompat::tokio::TokioNativeTlsRuntime,
}
impl ArtiRuntimeWrapper {
fn create() -> Result<ArtiRuntimeWrapper, std::io::Error> {
Ok(Self {
runtime: tor_rtcompat::tokio::TokioNativeTlsRuntime::create()?,
})
}
}
+1 -1
View File
@@ -102,7 +102,7 @@ pub struct InitTxSendArgs {
/// Whether to use dandelion when posting. If false, skip the dandelion relay
pub fluff: bool,
/// If set, skip the Slatepack TOR send attempt
pub skip_tor: bool,
pub skip_tor: Option<bool>,
}
impl Default for InitTxArgs {
+71 -53
View File
@@ -309,7 +309,7 @@ pub fn parse_global_args(
let key = match config.tls_certificate_key.clone() {
Some(k) => k,
None => {
let msg = format!("Private key for certificate is not set");
let msg = "Private key for certificate is not set".to_string();
return Err(ParseError::ArgumentError(msg));
}
};
@@ -319,11 +319,11 @@ pub fn parse_global_args(
Ok(command::GlobalArgs {
account: account.to_owned(),
show_spent: show_spent,
api_secret: api_secret,
node_api_secret: node_api_secret,
password: password,
tls_conf: tls_conf,
show_spent,
api_secret,
node_api_secret,
password,
tls_conf,
})
}
@@ -415,7 +415,7 @@ pub fn parse_scan_rewind_hash_args(
let start_height = parse_u64_or_none(args.value_of("start_height"));
let backwards_from_tip = parse_u64_or_none(args.value_of("backwards_from_tip"));
if backwards_from_tip.is_some() && start_height.is_some() {
let msg = format!("backwards_from tip and start_height cannot both be present");
let msg = "backwards_from tip and start_height cannot both be present".to_string();
return Err(ParseError::ArgumentError(msg));
}
Ok(command::ViewWalletScanArgs {
@@ -430,7 +430,7 @@ pub fn parse_account_args(account_args: &ArgMatches) -> Result<command::AccountA
None => None,
Some(s) => Some(s.to_owned()),
};
Ok(command::AccountArgs { create: create })
Ok(command::AccountArgs { create })
}
pub fn parse_send_args(args: &ArgMatches) -> Result<command::SendArgs, ParseError> {
@@ -466,10 +466,7 @@ pub fn parse_send_args(args: &ArgMatches) -> Result<command::SendArgs, ParseErro
let late_lock = args.is_present("late_lock");
// dest
let dest = match args.value_of("dest") {
Some(d) => d,
None => "default",
};
let dest = args.value_of("dest").unwrap_or_else(|| "default");
// change_outputs
let change_outputs = parse_required(args, "change_outputs")?;
@@ -512,6 +509,12 @@ pub fn parse_send_args(args: &ArgMatches) -> Result<command::SendArgs, ParseErro
let outfile = parse_optional(args, "outfile")?;
let skip_tor = if args.is_present("manual") {
Some(true)
} else {
None
};
let bridge = match args.value_of("bridge") {
Some(b) => Some(b.to_string()),
None => None,
@@ -520,24 +523,24 @@ pub fn parse_send_args(args: &ArgMatches) -> Result<command::SendArgs, ParseErro
let slatepack_qr = args.is_present("slatepack_qr");
Ok(command::SendArgs {
amount: amount,
amount_includes_fee: amount_includes_fee,
amount,
amount_includes_fee,
use_max_amount: spend_max,
minimum_confirmations: min_c,
selection_strategy: selection_strategy.to_owned(),
estimate_selection_strategies,
late_lock,
dest: dest.to_owned(),
change_outputs: change_outputs,
fluff: fluff,
max_outputs: max_outputs,
change_outputs,
fluff,
max_outputs,
payment_proof_address,
ttl_blocks,
target_slate_version: target_slate_version,
target_slate_version,
outfile,
skip_tor: args.is_present("manual"),
bridge: bridge,
slatepack_qr: slatepack_qr,
skip_tor,
bridge,
slatepack_qr,
})
}
@@ -563,6 +566,12 @@ pub fn parse_receive_args(args: &ArgMatches) -> Result<command::ReceiveArgs, Par
let outfile = parse_optional(args, "outfile")?;
let skip_tor = if args.is_present("manual") {
Some(true)
} else {
None
};
let bridge = parse_optional(args, "bridge")?;
let slatepack_qr = args.is_present("slatepack_qr");
@@ -570,10 +579,10 @@ pub fn parse_receive_args(args: &ArgMatches) -> Result<command::ReceiveArgs, Par
Ok(command::ReceiveArgs {
input_file,
input_slatepack_message,
skip_tor: args.is_present("manual"),
skip_tor,
outfile,
bridge,
slatepack_qr: slatepack_qr,
slatepack_qr,
})
}
@@ -599,6 +608,12 @@ pub fn parse_unpack_args(args: &ArgMatches) -> Result<command::ReceiveArgs, Pars
let outfile = parse_optional(args, "outfile")?;
let skip_tor = if args.is_present("manual") {
Some(true)
} else {
None
};
let bridge = parse_optional(args, "bridge")?;
let slatepack_qr = args.is_present("slatepack_qr");
@@ -606,10 +621,10 @@ pub fn parse_unpack_args(args: &ArgMatches) -> Result<command::ReceiveArgs, Pars
Ok(command::ReceiveArgs {
input_file,
input_slatepack_message,
skip_tor: args.is_present("manual"),
skip_tor,
outfile,
bridge,
slatepack_qr: slatepack_qr,
slatepack_qr,
})
}
@@ -642,10 +657,10 @@ pub fn parse_finalize_args(args: &ArgMatches) -> Result<command::FinalizeArgs, P
Ok(command::FinalizeArgs {
input_file,
input_slatepack_message,
fluff: fluff,
nopost: nopost,
fluff,
nopost,
outfile,
slatepack_qr: slatepack_qr,
slatepack_qr,
})
}
@@ -677,10 +692,7 @@ pub fn parse_issue_invoice_args(
};
// dest, for encryption
let dest = match args.value_of("dest") {
Some(d) => d,
None => "default",
};
let dest = args.value_of("dest").unwrap_or_else(|| "default");
let outfile = parse_optional(args, "outfile")?;
@@ -694,7 +706,7 @@ pub fn parse_issue_invoice_args(
target_slate_version,
},
outfile,
slatepack_qr: slatepack_qr,
slatepack_qr,
})
}
@@ -770,6 +782,12 @@ pub fn parse_process_invoice_args(
let outfile = parse_optional(args, "outfile")?;
let skip_tor = if args.is_present("manual") {
Some(true)
} else {
None
};
let bridge = parse_optional(args, "bridge")?;
let slatepack_qr = args.is_present("slatepack_qr");
@@ -782,10 +800,10 @@ pub fn parse_process_invoice_args(
slate,
max_outputs,
ttl_blocks,
skip_tor: args.is_present("manual"),
skip_tor,
outfile,
bridge,
slatepack_qr: slatepack_qr,
slatepack_qr,
})
}
@@ -803,7 +821,7 @@ pub fn parse_check_args(args: &ArgMatches) -> Result<command::CheckArgs, ParseEr
let start_height = parse_u64_or_none(args.value_of("start_height"));
let backwards_from_tip = parse_u64_or_none(args.value_of("backwards_from_tip"));
if backwards_from_tip.is_some() && start_height.is_some() {
let msg = format!("backwards_from tip and start_height cannot both be present");
let msg = "backwards_from tip and start_height cannot both be present".to_string();
return Err(ParseError::ArgumentError(msg));
}
Ok(command::CheckArgs {
@@ -829,7 +847,7 @@ pub fn parse_txs_args(args: &ArgMatches) -> Result<command::TxsArgs, ParseError>
},
};
if tx_id.is_some() && tx_slate_id.is_some() {
let msg = format!("At most one of 'id' (-i) or 'txid' (-t) may be provided.");
let msg = "At most one of 'id' (-i) or 'txid' (-t) may be provided.".to_string();
return Err(ParseError::ArgumentError(msg));
}
let count = match args.value_of("count") {
@@ -838,8 +856,8 @@ pub fn parse_txs_args(args: &ArgMatches) -> Result<command::TxsArgs, ParseError>
};
Ok(command::TxsArgs {
id: tx_id,
tx_slate_id: tx_slate_id,
count: count,
tx_slate_id,
count,
})
}
@@ -886,8 +904,8 @@ pub fn parse_repost_args(args: &ArgMatches) -> Result<command::RepostArgs, Parse
Ok(command::RepostArgs {
id: tx_id.unwrap(),
dump_file: dump_file,
fluff: fluff,
dump_file,
fluff,
})
}
@@ -911,12 +929,12 @@ pub fn parse_cancel_args(args: &ArgMatches) -> Result<command::CancelArgs, Parse
},
};
if (tx_id.is_none() && tx_slate_id.is_none()) || (tx_id.is_some() && tx_slate_id.is_some()) {
let msg = format!("'id' (-i) or 'txid' (-t) argument is required.");
let msg = "'id' (-i) or 'txid' (-t) argument is required.".to_string();
return Err(ParseError::ArgumentError(msg));
}
Ok(command::CancelArgs {
tx_id: tx_id,
tx_slate_id: tx_slate_id,
tx_id,
tx_slate_id,
tx_id_string: tx_id_string.to_owned(),
})
}
@@ -938,17 +956,17 @@ pub fn parse_export_proof_args(args: &ArgMatches) -> Result<command::ProofExport
},
};
if tx_id.is_some() && tx_slate_id.is_some() {
let msg = format!("At most one of 'id' (-i) or 'txid' (-t) may be provided.");
let msg = "At most one of 'id' (-i) or 'txid' (-t) may be provided.".to_string();
return Err(ParseError::ArgumentError(msg));
}
if tx_id.is_none() && tx_slate_id.is_none() {
let msg = format!("Either 'id' (-i) or 'txid' (-t) must be provided.");
let msg = "Either 'id' (-i) or 'txid' (-t) must be provided.".to_string();
return Err(ParseError::ArgumentError(msg));
}
Ok(command::ProofExportArgs {
output_file: output_file.to_owned(),
id: tx_id,
tx_slate_id: tx_slate_id,
tx_slate_id,
})
}
@@ -1026,12 +1044,12 @@ where
{
let mut wallet_lock = wallet.lock();
let lc = wallet_lock.lc_provider().unwrap();
let lc = wallet_lock.lc_provider()?;
let _ = lc.set_top_level_directory(&wallet_config.data_file_dir);
}
// provide wallet instance back to the caller (handy for testing with
// local wallet proxy, etc)
// local wallet proxy, etc.)
wallet_inst_cb(wallet.clone());
// don't open wallet for certain lifecycle commands
@@ -1043,7 +1061,7 @@ where
("owner_api", _) => {
// If wallet exists and password is present then open it. Otherwise, that's fine too.
let mut wallet_lock = wallet.lock();
let lc = wallet_lock.lc_provider().unwrap();
let lc = wallet_lock.lc_provider()?;
open_wallet = wallet_args.is_present("pass") && lc.wallet_exists(None)?;
}
_ => {}
@@ -1052,7 +1070,7 @@ where
let keychain_mask = match open_wallet {
true => {
let mut wallet_lock = wallet.lock();
let lc = wallet_lock.lc_provider().unwrap();
let lc = wallet_lock.lc_provider()?;
let mask = lc.open_wallet(
None,
prompt_password(&global_wallet_args.password),
@@ -1290,8 +1308,8 @@ where
Ok(())
}
_ => {
let msg = format!("Unknown wallet command, use 'grin-wallet help' for details");
return Err(Error::ArgumentError(msg));
let msg = "Unknown wallet command, use 'grin-wallet help' for details".to_string();
Err(Error::ArgumentError(msg))
}
}
}