diff --git a/src/commands/init.rs b/src/commands/init.rs index 56f58ba..4902f63 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -17,7 +17,7 @@ use zcash_primitives::{ use crate::{ data::{get_db_paths, init_wallet_keys, Network}, error, - remote::connect_to_lightwalletd, + remote::{connect_to_lightwalletd, Servers}, }; // Options accepted for the `init` command @@ -34,6 +34,13 @@ pub(crate) struct Command { parse(try_from_str = "Network::parse") )] network: Network, + + #[options( + help = "the server to initialize with (default is \"ecc\")", + default = "ecc", + parse(try_from_str = "Servers::parse") + )] + server: Servers, } impl Command { @@ -42,7 +49,7 @@ impl Command { let params = consensus::Network::from(opts.network); // Get the current chain height (for the wallet's birthday). - let mut client = connect_to_lightwalletd(¶ms).await?; + let mut client = connect_to_lightwalletd(opts.server.pick(params)?).await?; let birthday = if let Some(birthday) = opts.birthday { birthday } else { diff --git a/src/commands/reset.rs b/src/commands/reset.rs index c8ac5b9..1ed1c24 100644 --- a/src/commands/reset.rs +++ b/src/commands/reset.rs @@ -2,12 +2,19 @@ use gumdrop::Options; use crate::{ data::{erase_wallet_state, read_keys}, - remote::connect_to_lightwalletd, + remote::{connect_to_lightwalletd, Servers}, }; // Options accepted for the `reset` command #[derive(Debug, Options)] -pub(crate) struct Command {} +pub(crate) struct Command { + #[options( + help = "the server to re-initialize with (default is \"ecc\")", + default = "ecc", + parse(try_from_str = "Servers::parse") + )] + server: Servers, +} impl Command { pub(crate) async fn run(self, wallet_dir: Option) -> Result<(), anyhow::Error> { @@ -16,7 +23,7 @@ impl Command { let params = keys.network(); // Connect to the client (for re-initializing the wallet). - let client = connect_to_lightwalletd(¶ms).await?; + let client = connect_to_lightwalletd(self.server.pick(params)?).await?; // Erase the wallet state (excluding key material). erase_wallet_state(wallet_dir.as_ref()).await; diff --git a/src/commands/send.rs b/src/commands/send.rs index 2223b79..36a90cb 100644 --- a/src/commands/send.rs +++ b/src/commands/send.rs @@ -24,7 +24,7 @@ use crate::{ commands::propose::{parse_fee_rule, FeeRule}, data::{get_db_paths, read_keys}, error, - remote::connect_to_lightwalletd, + remote::{connect_to_lightwalletd, Servers}, MIN_CONFIRMATIONS, }; @@ -46,6 +46,13 @@ pub(crate) struct Command { parse(try_from_str = "parse_fee_rule") )] fee_rule: FeeRule, + + #[options( + help = "the server to send via (default is \"ecc\")", + default = "ecc", + parse(try_from_str = "Servers::parse") + )] + server: Servers, } impl Command { @@ -71,7 +78,7 @@ impl Command { UnifiedSpendingKey::from_seed(¶ms, keys.seed().expose_secret(), account_index) .map_err(error::Error::from)?; - let mut client = connect_to_lightwalletd(¶ms).await?; + let mut client = connect_to_lightwalletd(self.server.pick(params)?).await?; // Create the transaction. println!("Creating transaction..."); diff --git a/src/commands/sync.rs b/src/commands/sync.rs index af37ab3..87e505b 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -27,7 +27,7 @@ use zcash_protocol::consensus::{BlockHeight, Parameters}; use crate::{ data::{get_block_path, get_db_paths, get_wallet_network}, error, - remote::connect_to_lightwalletd, + remote::{connect_to_lightwalletd, Servers}, }; #[cfg(feature = "tui")] @@ -41,6 +41,13 @@ const BATCH_SIZE: u32 = 10_000; // Options accepted for the `sync` command #[derive(Debug, Options)] pub(crate) struct Command { + #[options( + help = "the server to sync with (default is \"ecc\")", + default = "ecc", + parse(try_from_str = "Servers::parse") + )] + server: Servers, + #[cfg(feature = "tui")] pub(crate) defrag: bool, } @@ -57,7 +64,7 @@ impl Command { let fsblockdb_root = fsblockdb_root.as_path(); let mut db_cache = FsBlockDb::for_path(fsblockdb_root).map_err(error::Error::from)?; let mut db_data = WalletDb::for_path(db_data, params)?; - let mut client = connect_to_lightwalletd(¶ms).await?; + let mut client = connect_to_lightwalletd(self.server.pick(params)?).await?; #[cfg(feature = "tui")] let tui_handle = if self.defrag { diff --git a/src/main.rs b/src/main.rs index 9948005..46b65da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -70,7 +70,7 @@ fn main() -> Result<(), anyhow::Error> { let log_configured = false; #[cfg(feature = "tui")] let log_configured = - if let Some(Command::Sync(commands::sync::Command { defrag: true })) = opts.command { + if let Some(Command::Sync(commands::sync::Command { defrag: true, .. })) = opts.command { use tracing_subscriber::layer::SubscriberExt; tracing::subscriber::set_global_default( diff --git a/src/remote.rs b/src/remote.rs index 69c9b11..0fb25ce 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -1,35 +1,136 @@ +use std::{borrow::Cow, fmt}; + +use anyhow::anyhow; use tonic::transport::{Channel, ClientTlsConfig}; use tracing::info; use zcash_client_backend::proto::service::compact_tx_streamer_client::CompactTxStreamerClient; -use zcash_primitives::consensus; +use zcash_protocol::consensus::Network; -pub(crate) trait Lightwalletd { - fn host(&self) -> &str; - fn port(&self) -> u16; +const ECC_TESTNET: &[Server<'_>] = &[Server::fixed("lightwalletd.testnet.electriccoin.co", 9067)]; + +const YWALLET_MAINNET: &[Server<'_>] = &[ + Server::fixed("lwd1.zcash-infra.com", 9067), + Server::fixed("lwd2.zcash-infra.com", 9067), + Server::fixed("lwd3.zcash-infra.com", 9067), + Server::fixed("lwd4.zcash-infra.com", 9067), + Server::fixed("lwd5.zcash-infra.com", 9067), + Server::fixed("lwd6.zcash-infra.com", 9067), + Server::fixed("lwd7.zcash-infra.com", 9067), + Server::fixed("lwd8.zcash-infra.com", 9067), +]; + +const ZEC_ROCKS_MAINNET: &[Server<'_>] = &[ + Server::fixed("zec.rocks", 443), + Server::fixed("ap.zec.rocks", 443), + Server::fixed("eu.zec.rocks", 443), + Server::fixed("na.zec.rocks", 443), + Server::fixed("sa.zec.rocks", 443), +]; +const ZEC_ROCKS_TESTNET: &[Server<'_>] = &[Server::fixed("testnet.zec.rocks", 443)]; + +#[derive(Debug)] +pub(crate) enum ServerOperator { + Ecc, + YWallet, + ZecRocks, } -impl Lightwalletd for consensus::Network { - fn host(&self) -> &str { - match self { - consensus::Network::MainNetwork => "mainnet.lightwalletd.com", - consensus::Network::TestNetwork => "lightwalletd.testnet.electriccoin.co", +impl ServerOperator { + fn servers(&self, network: Network) -> &[Server<'_>] { + match (self, network) { + (ServerOperator::Ecc, Network::MainNetwork) => &[], + (ServerOperator::Ecc, Network::TestNetwork) => ECC_TESTNET, + (ServerOperator::YWallet, Network::MainNetwork) => YWALLET_MAINNET, + (ServerOperator::YWallet, Network::TestNetwork) => &[], + (ServerOperator::ZecRocks, Network::MainNetwork) => ZEC_ROCKS_MAINNET, + (ServerOperator::ZecRocks, Network::TestNetwork) => ZEC_ROCKS_TESTNET, + } + } +} + +#[derive(Debug)] +pub(crate) enum Servers { + Hosted(ServerOperator), + Custom(Vec>), +} + +impl Servers { + pub(crate) fn parse(s: &str) -> anyhow::Result { + match s { + "ecc" => Ok(Self::Hosted(ServerOperator::Ecc)), + "ywallet" => Ok(Self::Hosted(ServerOperator::YWallet)), + "zecrocks" => Ok(Self::Hosted(ServerOperator::ZecRocks)), + _ => s + .split(',') + .map(|sub| { + sub.split_once(':').and_then(|(host, port_str)| { + port_str + .parse() + .ok() + .map(|port| Server::custom(host.into(), port)) + }) + }) + .collect::>() + .map(Self::Custom) + .ok_or(anyhow!("'{}' must be one of ['ecc', 'ywallet', 'zecrocks'], or a comma-separated host:port string", s)), } } - fn port(&self) -> u16 { - 9067 + pub(crate) fn pick(&self, network: Network) -> anyhow::Result<&Server<'_>> { + // For now just use the first server in the list. + match self { + Servers::Hosted(server_operator) => server_operator + .servers(network) + .first() + .ok_or(anyhow!("{:?} doesn't serve {:?}", server_operator, network)), + Servers::Custom(servers) => Ok(servers.first().expect("not empty")), + } + } +} + +#[derive(Debug)] +pub(crate) struct Server<'a> { + host: Cow<'a, str>, + port: u16, +} + +impl<'a> fmt::Display for Server<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.host, self.port) + } +} + +impl Server<'static> { + const fn fixed(host: &'static str, port: u16) -> Self { + Self { + host: Cow::Borrowed(host), + port, + } + } +} + +impl<'a> Server<'a> { + fn custom(host: String, port: u16) -> Self { + Self { + host: Cow::Owned(host), + port, + } + } + + fn endpoint(&self) -> String { + format!("https://{}:{}", self.host, self.port) } } pub(crate) async fn connect_to_lightwalletd( - network: &impl Lightwalletd, + server: &Server<'_>, ) -> Result, anyhow::Error> { - info!("Connecting to {}:{}", network.host(), network.port()); + info!("Connecting to {}", server); - let tls = ClientTlsConfig::new().domain_name(network.host()); + let tls = ClientTlsConfig::new().domain_name(server.host.to_string()); - let channel = Channel::from_shared(format!("https://{}:{}", network.host(), network.port()))? + let channel = Channel::from_shared(server.endpoint())? .tls_config(tls)? .connect() .await?;