#![forbid(unsafe_code)]
#![warn(missing_docs)]
use std::{
io::{self, Error, ErrorKind},
path::{Path, PathBuf},
sync::{Arc, RwLock},
};
use log::{LevelFilter, info};
use log4rs::{
Config,
append::rolling_file::{
RollingFileAppender,
policy::compound::{
CompoundPolicy, roll::fixed_window::FixedWindowRoller, trigger::size::SizeTrigger,
},
},
config::{Appender, Root},
encode::pattern::PatternEncoder,
filter::threshold::ThresholdFilter,
};
use zcash_primitives::consensus::{
BlockHeight, MAIN_NETWORK, NetworkType, NetworkUpgrade, Parameters, TEST_NETWORK,
};
use crate::wallet::WalletSettings;
pub const DEVELOPER_DONATION_ADDRESS: &str = "u1w47nzy4z5g9zvm4h2s4ztpl8vrdmlclqz5sz02742zs5j3tz232u4safvv9kplg7g06wpk5fx0k0rx3r9gg4qk6nkg4c0ey57l0dyxtatqf8403xat7vyge7mmen7zwjcgvryg22khtg3327s6mqqkxnpwlnrt27kxhwg37qys2kpn2d2jl2zkk44l7j7hq9az82594u3qaescr3c9v";
pub const ZENNIES_FOR_ZINGO_REGTEST_ADDRESS: &str = "uregtest14emvr2anyul683p43d0ck55c04r65ld6f0shetcn77z8j7m64hm4ku3wguf60s75f0g3s7r7g89z22f3ff5tsfgr45efj4pe2gyg5krqp5vvl3afu0280zp9ru2379zat5y6nkqkwjxsvpq5900kchcgzaw8v8z3ggt5yymnuj9hymtv3p533fcrk2wnj48g5vg42vle08c2xtanq0e";
pub const ZENNIES_FOR_ZINGO_TESTNET_ADDRESS: &str = "utest19zd9laj93deq4lkay48xcfyh0tjec786x6yrng38fp6zusgm0c84h3el99fngh8eks4kxv020r2h2njku6pf69anpqmjq5c3suzcjtlyhvpse0aqje09la48xk6a2cnm822s2yhuzfr47pp4dla9rakdk90g0cee070z57d3trqk87wwj4swz6uf6ts6p5z6lep3xyvueuvt7392tww";
pub const ZENNIES_FOR_ZINGO_DONATION_ADDRESS: &str = "u1p32nu0pgev5cr0u6t4ja9lcn29kaw37xch8nyglwvp7grl07f72c46hxvw0u3q58ks43ntg324fmulc2xqf4xl3pv42s232m25vaukp05s6av9z76s3evsstax4u6f5g7tql5yqwuks9t4ef6vdayfmrsymenqtshgxzj59hdydzygesqa7pdpw463hu7afqf4an29m69kfasdwr494";
pub const ZENNIES_FOR_ZINGO_AMOUNT: u64 = 1_000_000;
pub const DEFAULT_LIGHTWALLETD_SERVER: &str = "https://zec.rocks:443";
pub const DEFAULT_WALLET_NAME: &str = "zingo-wallet.dat";
pub const DEFAULT_LOGFILE_NAME: &str = "zingo-wallet.debug.log";
pub use pepper_sync::sync::{SyncConfig, TransparentAddressDiscovery};
pub fn load_clientconfig(
lightwallet_uri: http::Uri,
data_dir: Option<PathBuf>,
chain: ChainType,
wallet_settings: WalletSettings,
) -> std::io::Result<ZingoConfig> {
use std::net::ToSocketAddrs;
let host = lightwallet_uri.host();
let port = lightwallet_uri.port();
if host.is_none() || port.is_none() {
info!("Using offline mode");
} else {
match format!(
"{}:{}",
lightwallet_uri.host().unwrap(),
lightwallet_uri.port().unwrap()
)
.to_socket_addrs()
{
Ok(_) => {
info!("Connected to {}", lightwallet_uri);
}
Err(e) => {
info!("Couldn't resolve server: {}", e);
}
}
}
let config = ZingoConfig {
lightwalletd_uri: Arc::new(RwLock::new(lightwallet_uri)),
chain,
wallet_dir: data_dir,
wallet_name: DEFAULT_WALLET_NAME.into(),
logfile_name: DEFAULT_LOGFILE_NAME.into(),
wallet_settings,
};
Ok(config)
}
pub fn construct_lightwalletd_uri(server: Option<String>) -> http::Uri {
match server {
Some(s) => match s.is_empty() {
true => {
return http::Uri::default();
}
false => {
let mut s = if s.starts_with("http") {
s
} else {
"http://".to_string() + &s
};
let uri: http::Uri = s.parse().unwrap();
if uri.port().is_none() {
s += ":9067";
}
s
}
},
None => DEFAULT_LIGHTWALLETD_SERVER.to_string(),
}
.parse()
.unwrap()
}
#[derive(Clone, Debug)]
pub struct ZingoConfigBuilder {
pub lightwalletd_uri: Option<http::Uri>,
pub chain: ChainType,
pub reorg_buffer_offset: Option<u32>,
pub monitor_mempool: Option<bool>,
pub wallet_dir: Option<PathBuf>,
pub wallet_name: Option<PathBuf>,
pub logfile_name: Option<PathBuf>,
pub wallet_settings: WalletSettings,
}
#[derive(Clone, Debug)]
pub struct ZingoConfig {
pub lightwalletd_uri: Arc<RwLock<http::Uri>>,
pub chain: ChainType,
pub wallet_dir: Option<PathBuf>,
pub wallet_name: PathBuf,
pub logfile_name: PathBuf,
pub wallet_settings: WalletSettings,
}
impl ZingoConfigBuilder {
pub fn set_lightwalletd_uri(&mut self, lightwalletd_uri: http::Uri) -> &mut Self {
self.lightwalletd_uri = Some(lightwalletd_uri);
self
}
pub fn set_chain(&mut self, chain: ChainType) -> &mut Self {
self.chain = chain;
self
}
pub fn set_wallet_dir(&mut self, dir: PathBuf) -> &mut Self {
self.wallet_dir = Some(dir);
self
}
pub fn set_wallet_settings(&mut self, wallet_settings: WalletSettings) -> &mut Self {
self.wallet_settings = wallet_settings;
self
}
pub fn create(&self) -> ZingoConfig {
let lightwalletd_uri = self.lightwalletd_uri.clone().unwrap_or_default();
ZingoConfig {
lightwalletd_uri: Arc::new(RwLock::new(lightwalletd_uri)),
chain: self.chain,
wallet_dir: self.wallet_dir.clone(),
wallet_name: DEFAULT_WALLET_NAME.into(),
logfile_name: DEFAULT_LOGFILE_NAME.into(),
wallet_settings: self.wallet_settings.clone(),
}
}
}
impl Default for ZingoConfigBuilder {
fn default() -> Self {
ZingoConfigBuilder {
lightwalletd_uri: None,
monitor_mempool: None,
reorg_buffer_offset: None,
wallet_dir: None,
wallet_name: None,
logfile_name: None,
chain: ChainType::Mainnet,
wallet_settings: WalletSettings {
sync_config: pepper_sync::sync::SyncConfig {
transparent_address_discovery:
pepper_sync::sync::TransparentAddressDiscovery::minimal(),
},
},
}
}
}
impl ZingoConfig {
pub fn build(chain: ChainType) -> ZingoConfigBuilder {
ZingoConfigBuilder {
chain,
..ZingoConfigBuilder::default()
}
}
#[cfg(any(test, feature = "test-elevation"))]
pub fn create_testnet() -> ZingoConfig {
ZingoConfig::build(ChainType::Testnet)
.set_lightwalletd_uri(
("https://lightwalletd.testnet.electriccoin.co:9067")
.parse::<http::Uri>()
.unwrap(),
)
.create()
}
#[cfg(any(test, feature = "test-elevation"))]
pub fn create_mainnet() -> ZingoConfig {
ZingoConfig::build(ChainType::Mainnet)
.set_lightwalletd_uri(("https://zec.rocks:443").parse::<http::Uri>().unwrap())
.create()
}
#[cfg(feature = "test-elevation")]
pub fn create_unconnected(chain: ChainType, dir: Option<PathBuf>) -> ZingoConfig {
if let Some(dir) = dir {
ZingoConfig::build(chain).set_wallet_dir(dir).create()
} else {
ZingoConfig::build(chain).create()
}
}
pub fn sapling_activation_height(&self) -> u64 {
self.chain
.activation_height(NetworkUpgrade::Sapling)
.unwrap()
.into()
}
pub fn orchard_activation_height(&self) -> u64 {
self.chain
.activation_height(NetworkUpgrade::Nu5)
.unwrap()
.into()
}
pub fn set_data_dir(&mut self, dir_str: String) {
self.wallet_dir = Some(PathBuf::from(dir_str));
}
pub fn get_log_config(&self) -> io::Result<Config> {
let window_size = 3; let fixed_window_roller = FixedWindowRoller::builder()
.build("zingo-wallet-log{}", window_size)
.unwrap();
let size_limit = 5 * 1024 * 1024; let size_trigger = SizeTrigger::new(size_limit);
let compound_policy =
CompoundPolicy::new(Box::new(size_trigger), Box::new(fixed_window_roller));
Config::builder()
.appender(
Appender::builder()
.filter(Box::new(ThresholdFilter::new(LevelFilter::Info)))
.build(
"logfile",
Box::new(
RollingFileAppender::builder()
.encoder(Box::new(PatternEncoder::new("{d} {l}::{m}{n}")))
.build(self.get_log_path(), Box::new(compound_policy))?,
),
),
)
.build(
Root::builder()
.appender("logfile")
.build(LevelFilter::Debug),
)
.map_err(|e| Error::new(ErrorKind::Other, format!("{}", e)))
}
pub fn get_zingo_wallet_dir(&self) -> Box<Path> {
#[cfg(any(target_os = "ios", target_os = "android"))]
{
PathBuf::from(&self.wallet_dir.as_ref().unwrap()).into_boxed_path()
}
#[cfg(not(any(target_os = "ios", target_os = "android")))]
{
let mut zcash_data_location;
if self.wallet_dir.is_some() {
zcash_data_location = PathBuf::from(&self.wallet_dir.as_ref().unwrap());
} else {
#[cfg(any(target_os = "macos", target_os = "windows"))]
{
zcash_data_location =
dirs::data_dir().expect("Couldn't determine app data directory!");
zcash_data_location.push("Zcash");
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
if dirs::home_dir().is_none() {
log::info!("Couldn't determine home dir!");
}
zcash_data_location =
dirs::home_dir().expect("Couldn't determine home directory!");
zcash_data_location.push(".zcash");
}
match &self.chain {
ChainType::Testnet => zcash_data_location.push("testnet3"),
ChainType::Regtest(_) => zcash_data_location.push("regtest"),
ChainType::Mainnet => {}
};
}
match std::fs::create_dir_all(zcash_data_location.clone()) {
Ok(_) => {}
Err(e) => {
eprintln!("Couldn't create zcash directory!\n{}", e);
panic!("Couldn't create zcash directory!");
}
};
zcash_data_location.into_boxed_path()
}
}
pub fn get_zcash_params_path(&self) -> io::Result<Box<Path>> {
#[cfg(any(target_os = "ios", target_os = "android"))]
{
Ok(PathBuf::from(&self.wallet_dir.as_ref().unwrap()).into_boxed_path())
}
#[cfg(not(any(target_os = "ios", target_os = "android")))]
{
if dirs::home_dir().is_none() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Couldn't determine Home Dir",
));
}
let zcash_params_dir = zcash_proofs::default_params_folder().unwrap();
Ok(zcash_params_dir.into_boxed_path())
}
}
pub fn get_lightwalletd_uri(&self) -> http::Uri {
self.lightwalletd_uri
.read()
.expect("Couldn't read configured server URI!")
.clone()
}
pub fn get_wallet_pathbuf(&self) -> PathBuf {
let mut wallet_location = self.get_zingo_wallet_dir().into_path_buf();
wallet_location.push(&self.wallet_name);
wallet_location
}
pub fn get_wallet_path(&self) -> Box<Path> {
self.get_wallet_pathbuf().into_boxed_path()
}
pub fn wallet_path_exists(&self) -> bool {
self.get_wallet_path().exists()
}
#[deprecated(note = "this method was renamed 'wallet_path_exists' for clarity")]
pub fn wallet_exists(&self) -> bool {
self.wallet_path_exists()
}
pub fn backup_existing_wallet(&self) -> Result<String, String> {
if !self.wallet_path_exists() {
return Err(format!(
"Couldn't find existing wallet to backup. Looked in {:?}",
self.get_wallet_path().to_str()
));
}
let mut backup_file_path = self.get_zingo_wallet_dir().into_path_buf();
backup_file_path.push(format!(
"zingo-wallet.backup.{}.dat",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
));
let backup_file_str = backup_file_path.to_string_lossy().to_string();
std::fs::copy(self.get_wallet_path(), backup_file_path).map_err(|e| format!("{}", e))?;
Ok(backup_file_str)
}
pub fn get_log_path(&self) -> Box<Path> {
let mut log_path = self.get_zingo_wallet_dir().into_path_buf();
log_path.push(&self.logfile_name);
log_path.into_boxed_path()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ChainType {
Testnet,
Regtest(RegtestNetwork),
Mainnet,
}
impl std::fmt::Display for ChainType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use ChainType::*;
let name = match self {
Testnet => "test",
Regtest(_) => "regtest",
Mainnet => "main",
};
write!(f, "{name}")
}
}
impl Parameters for ChainType {
fn network_type(&self) -> NetworkType {
match self {
ChainType::Mainnet => NetworkType::Main,
ChainType::Testnet => NetworkType::Test,
ChainType::Regtest(_) => NetworkType::Regtest,
}
}
fn activation_height(&self, nu: NetworkUpgrade) -> Option<BlockHeight> {
use ChainType::*;
match self {
Mainnet => MAIN_NETWORK.activation_height(nu),
Testnet => TEST_NETWORK.activation_height(nu),
Regtest(regtest_network) => regtest_network.activation_height(nu),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RegtestNetwork {
activation_heights: ActivationHeights,
}
impl RegtestNetwork {
pub fn new(
overwinter_activation_height: u64,
sapling_activation_height: u64,
blossom_activation_height: u64,
heartwood_activation_height: u64,
canopy_activation_height: u64,
orchard_activation_height: u64,
nu6_activation_height: u64,
) -> Self {
Self {
activation_heights: ActivationHeights::new(
overwinter_activation_height,
sapling_activation_height,
blossom_activation_height,
heartwood_activation_height,
canopy_activation_height,
orchard_activation_height,
nu6_activation_height,
),
}
}
pub fn all_upgrades_active() -> Self {
Self {
activation_heights: ActivationHeights::new(1, 1, 1, 1, 1, 1, 1),
}
}
pub fn set_orchard_and_nu6(custom_activation_height: u64) -> Self {
Self {
activation_heights: ActivationHeights::new(
1,
1,
1,
1,
1,
custom_activation_height,
custom_activation_height,
),
}
}
pub fn activation_height(&self, nu: NetworkUpgrade) -> Option<BlockHeight> {
match nu {
NetworkUpgrade::Overwinter => Some(
self.activation_heights
.get_activation_height(NetworkUpgrade::Overwinter),
),
NetworkUpgrade::Sapling => Some(
self.activation_heights
.get_activation_height(NetworkUpgrade::Sapling),
),
NetworkUpgrade::Blossom => Some(
self.activation_heights
.get_activation_height(NetworkUpgrade::Blossom),
),
NetworkUpgrade::Heartwood => Some(
self.activation_heights
.get_activation_height(NetworkUpgrade::Heartwood),
),
NetworkUpgrade::Canopy => Some(
self.activation_heights
.get_activation_height(NetworkUpgrade::Canopy),
),
NetworkUpgrade::Nu5 => Some(
self.activation_heights
.get_activation_height(NetworkUpgrade::Nu5),
),
NetworkUpgrade::Nu6 => Some(
self.activation_heights
.get_activation_height(NetworkUpgrade::Nu6),
),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ActivationHeights {
overwinter: BlockHeight,
sapling: BlockHeight,
blossom: BlockHeight,
heartwood: BlockHeight,
canopy: BlockHeight,
orchard: BlockHeight,
nu6: BlockHeight,
}
impl ActivationHeights {
pub fn new(
overwinter: u64,
sapling: u64,
blossom: u64,
heartwood: u64,
canopy: u64,
orchard: u64,
nu6: u64,
) -> Self {
Self {
overwinter: BlockHeight::from_u32(overwinter as u32),
sapling: BlockHeight::from_u32(sapling as u32),
blossom: BlockHeight::from_u32(blossom as u32),
heartwood: BlockHeight::from_u32(heartwood as u32),
canopy: BlockHeight::from_u32(canopy as u32),
orchard: BlockHeight::from_u32(orchard as u32),
nu6: BlockHeight::from_u32(nu6 as u32),
}
}
pub fn get_activation_height(&self, network_upgrade: NetworkUpgrade) -> BlockHeight {
match network_upgrade {
NetworkUpgrade::Overwinter => self.overwinter,
NetworkUpgrade::Sapling => self.sapling,
NetworkUpgrade::Blossom => self.blossom,
NetworkUpgrade::Heartwood => self.heartwood,
NetworkUpgrade::Canopy => self.canopy,
NetworkUpgrade::Nu5 => self.orchard,
NetworkUpgrade::Nu6 => self.nu6,
}
}
}
#[cfg(test)]
mod tests {
use crate::wallet::WalletSettings;
#[tokio::test]
async fn test_load_clientconfig() {
rustls::crypto::ring::default_provider()
.install_default()
.expect("Ring to work as a default");
tracing_subscriber::fmt().init();
let valid_uri = crate::config::construct_lightwalletd_uri(Some("".to_string()));
let temp_dir = tempfile::TempDir::new().unwrap();
let temp_path = temp_dir.path().to_path_buf();
let valid_config = crate::config::load_clientconfig(
valid_uri.clone(),
Some(temp_path),
crate::config::ChainType::Mainnet,
WalletSettings {
sync_config: pepper_sync::sync::SyncConfig {
transparent_address_discovery:
pepper_sync::sync::TransparentAddressDiscovery::minimal(),
},
},
);
assert!(valid_config.is_ok());
}
#[tokio::test]
async fn test_load_clientconfig_serverless() {
rustls::crypto::ring::default_provider()
.install_default()
.expect("Ring to work as a default");
tracing_subscriber::fmt().init();
let valid_uri = crate::config::construct_lightwalletd_uri(Some(
crate::config::DEFAULT_LIGHTWALLETD_SERVER.to_string(),
));
let temp_dir = tempfile::TempDir::new().unwrap();
let temp_path = temp_dir.path().to_path_buf();
let valid_config = crate::config::load_clientconfig(
valid_uri.clone(),
Some(temp_path),
crate::config::ChainType::Mainnet,
WalletSettings {
sync_config: pepper_sync::sync::SyncConfig {
transparent_address_discovery:
pepper_sync::sync::TransparentAddressDiscovery::minimal(),
},
},
)
.unwrap();
assert_eq!(valid_config.get_lightwalletd_uri(), valid_uri);
assert_eq!(valid_config.chain, crate::config::ChainType::Mainnet);
}
}