Skip to main content

paralock_cli/commands/
mod.rs

1//! The commands, and what they share: connecting to Zcash and the EVM chains, prices.
2
3pub mod doctor;
4pub mod maker;
5pub mod taker;
6
7use std::sync::Arc;
8use std::time::Duration;
9
10use paralock::builder::{EvmChainOptions, connect_evm_chain};
11use paralock::price::{CoinGecko, CoinGeckoOptions};
12use paralock::{
13    AdaptorEvmClient, EvmChain, EvmClient, FixedPrice, Lightwalletd, LightwalletdOptions, PriceSource, ZcashNode,
14    ZcashRpc,
15};
16
17use crate::config::{Backend, Chain, KeySource, Price, Setup};
18use crate::error::{CliError, CliResult};
19
20/// The Zcash backend of `setup`.
21pub fn zcash_node(setup: &Setup) -> CliResult<Arc<dyn ZcashNode>> {
22    Ok(match &setup.zcash {
23        Backend::Rpc { url, auth } => {
24            let rpc = ZcashRpc::new(url.clone())?;
25            match auth {
26                Some((user, secret)) => {
27                    let (cookie_user, password) = secret.load()?;
28                    let user = cookie_user.unwrap_or_else(|| user.clone());
29                    Arc::new(rpc.with_auth(user, password.as_str()))
30                }
31                None => Arc::new(rpc),
32            }
33        }
34        Backend::Lightwalletd { url } => Arc::new(Lightwalletd::with_options(url, LightwalletdOptions::default())?),
35    })
36}
37
38/// A description of the backend for messages (no credentials).
39pub fn backend_label(setup: &Setup) -> String {
40    match &setup.zcash {
41        Backend::Rpc { url, .. } => format!("Zcash RPC {}", redact(url)),
42        Backend::Lightwalletd { url } => format!("lightwalletd {}", redact(url)),
43    }
44}
45
46/// A URL without user info or query string (which may hold credentials or API keys).
47pub fn redact(url: &str) -> String {
48    let (scheme, rest) = url.split_once("://").unwrap_or(("", url));
49    let host_and_path = rest.rsplit_once('@').map_or(rest, |(_, h)| h);
50    let host_and_path = host_and_path.split(['?', '#']).next().unwrap_or("");
51    if scheme.is_empty() { host_and_path.to_owned() } else { format!("{scheme}://{host_and_path}") }
52}
53
54/// The USD/ZEC price source of `setup`.
55pub fn price_source(setup: &Setup) -> CliResult<Arc<dyn PriceSource>> {
56    Ok(match &setup.price {
57        Price::Fixed(price) => Arc::new(FixedPrice(*price)),
58        Price::CoinGecko { url, fallback } => {
59            let mut options = CoinGeckoOptions { fallback: *fallback, ..CoinGeckoOptions::default() };
60            if let Some(url) = url {
61                options.url = url.clone();
62            }
63            Arc::new(CoinGecko::with_options(options)?)
64        }
65    })
66}
67
68/// A description of the price source.
69pub fn price_label(setup: &Setup) -> String {
70    match &setup.price {
71        Price::Fixed(price) => format!("fixed ${price}/ZEC (not for real funds)"),
72        Price::CoinGecko { url: Some(url), .. } => format!("{} (CoinGecko format)", redact(url)),
73        Price::CoinGecko { url: None, fallback: Some(f) } => format!("CoinGecko (devnet fallback ${f}/ZEC)"),
74        Price::CoinGecko { url: None, fallback: None } => "CoinGecko".to_owned(),
75    }
76}
77
78/// The SDK's options for one chain with `account` (a `0x` private key).
79pub fn evm_options(chain: &Chain, account: &str) -> EvmChainOptions {
80    let mut o = EvmChainOptions::new(chain.rpc.clone(), chain.chain_id, chain.htlc.clone(), account);
81    o.adaptor_escrow = chain.adaptor_escrow.clone();
82    o.from_block = chain.from_block;
83    o.fee_token = chain.fee_token.clone();
84    o.rpc_timeout = chain.rpc_timeout;
85    o
86}
87
88/// Connects both escrows of `chain` with the key from `key`, through `rpc` (default: the chain's RPC). The SDK checks
89/// the chain id and that each escrow holds the Paralock contract's code.
90pub async fn connect(
91    chain: &Chain,
92    key: &KeySource,
93    rpc: Option<&str>,
94    receipt_timeout: Option<Duration>,
95) -> CliResult<(Arc<dyn EvmClient>, Option<Arc<dyn AdaptorEvmClient>>)> {
96    let account = key.load_0x()?;
97    let mut options = evm_options(chain, &account);
98    if let Some(rpc) = rpc {
99        options.rpc_url = rpc.to_owned();
100    }
101    options.receipt_timeout = receipt_timeout;
102    connect_evm_chain(chain.chain, &options, &account).await.map_err(|e| {
103        CliError::from(e)
104            .context(format!("{} ({})", chain.chain.display_name(), redact(&options.rpc_url)))
105            .hint("is the RPC reachable, on the configured chain id, with the Paralock contracts? `paralock doctor`")
106    })
107}
108
109/// The chain to use: `--chain`, else tempo when configured, else the only configured chain.
110pub fn pick_chain(setup: &Setup, chain: Option<EvmChain>) -> CliResult<EvmChain> {
111    if let Some(chain) = chain {
112        setup.chain(chain)?;
113        return Ok(chain);
114    }
115    if setup.chains.contains_key(&EvmChain::Tempo) {
116        return Ok(EvmChain::Tempo);
117    }
118    let mut chains = setup.chains.keys();
119    match (chains.next(), chains.next()) {
120        (Some(only), None) => Ok(*only),
121        _ => {
122            let names: Vec<&str> = setup.chains.keys().map(|c| c.as_str()).collect();
123            Err(CliError::usage(format!("pick a chain with --chain ({})", names.join(", "))))
124        }
125    }
126}
127
128/// Waits for Ctrl-C or SIGTERM.
129pub async fn shutdown_signal() {
130    #[cfg(unix)]
131    {
132        use tokio::signal::unix::{SignalKind, signal};
133        let mut term = match signal(SignalKind::terminate()) {
134            Ok(term) => term,
135            Err(_) => {
136                let _ = tokio::signal::ctrl_c().await;
137                return;
138            }
139        };
140        tokio::select! {
141            _ = tokio::signal::ctrl_c() => {}
142            _ = term.recv() => {}
143        }
144    }
145    #[cfg(not(unix))]
146    let _ = tokio::signal::ctrl_c().await;
147}
148
149/// A short timeout for doctor-style probes.
150pub const PROBE_TIMEOUT: Duration = Duration::from_secs(5);