use std::{
collections::BTreeMap,
num::NonZeroU32,
path::{Path, PathBuf},
};
use bip0039::{English, Mnemonic};
use http::uri::InvalidUri;
use zcash_protocol::consensus::{BlockHeight, Parameters};
use pepper_sync::config::{SyncConfig, TransparentAddressDiscovery};
use zingo_common_components::protocol::ActivationHeights;
pub use crate::destination::servers::{IndexerConfig, Location, Role, Trust};
use crate::wallet::{
WalletBase, WalletSettings,
error::{KeyError, WalletError},
keys::unified::UnifiedKeyStore,
};
pub const DEFAULT_WALLET_NAME: &str = "zingo-wallet.dat";
pub const LIB_BIRTHDAY_MAINNET: u32 = 3_411_499;
pub const LIB_BIRTHDAY_TESTNET: u32 = 4_134_000;
pub fn lib_birthday(chain: ChainType) -> u32 {
match chain {
ChainType::Mainnet => LIB_BIRTHDAY_MAINNET,
ChainType::Testnet => LIB_BIRTHDAY_TESTNET,
ChainType::Regtest(_) => 1,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ChainType {
Mainnet,
Testnet,
Regtest(ActivationHeights),
}
impl std::fmt::Display for ChainType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let chain = match self {
ChainType::Mainnet => "mainnet",
ChainType::Testnet => "testnet",
ChainType::Regtest(_) => "regtest",
};
write!(f, "{chain}")
}
}
impl TryFrom<&str> for ChainType {
type Error = InvalidChainType;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"mainnet" | "main" => Ok(ChainType::Mainnet),
"testnet" | "test" => Ok(ChainType::Testnet),
"regtest" => Ok(ChainType::Regtest(ActivationHeights::default())),
_ => Err(InvalidChainType(value.to_string())),
}
}
}
pub(crate) mod consealed {
use zcash_protocol::consensus::{
BlockHeight, MAIN_NETWORK, NetworkType, NetworkUpgrade, Parameters, TEST_NETWORK,
};
use super::ChainType;
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> {
match self {
ChainType::Mainnet => MAIN_NETWORK.activation_height(nu),
ChainType::Testnet => TEST_NETWORK.activation_height(nu),
ChainType::Regtest(activation_heights) => match nu {
NetworkUpgrade::Overwinter => {
activation_heights.overwinter().map(BlockHeight::from_u32)
}
NetworkUpgrade::Sapling => {
activation_heights.sapling().map(BlockHeight::from_u32)
}
NetworkUpgrade::Blossom => {
activation_heights.blossom().map(BlockHeight::from_u32)
}
NetworkUpgrade::Heartwood => {
activation_heights.heartwood().map(BlockHeight::from_u32)
}
NetworkUpgrade::Canopy => {
activation_heights.canopy().map(BlockHeight::from_u32)
}
NetworkUpgrade::Nu5 => activation_heights.nu5().map(BlockHeight::from_u32),
NetworkUpgrade::Nu6 => activation_heights.nu6().map(BlockHeight::from_u32),
NetworkUpgrade::Nu6_1 => activation_heights.nu6_1().map(BlockHeight::from_u32),
NetworkUpgrade::Nu6_2 => activation_heights.nu6_2().map(BlockHeight::from_u32),
NetworkUpgrade::Nu6_3 => activation_heights.nu6_3().map(BlockHeight::from_u32),
NetworkUpgrade::Nu7 => activation_heights.nu7().map(BlockHeight::from_u32),
},
}
}
}
}
#[derive(thiserror::Error, Debug)]
#[error("Invalid chain type '{0}'. Expected one of: 'mainnet', 'testnet' or 'regtest'.")]
pub struct InvalidChainType(String);
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum WalletConfig {
NewSeed {
no_of_accounts: NonZeroU32,
chain_height: u32,
wallet_settings: WalletSettings,
},
MnemonicPhrase {
mnemonic_phrase: String,
no_of_accounts: NonZeroU32,
birthday: u32,
wallet_settings: WalletSettings,
},
Ufvk {
ufvk: String,
birthday: u32,
wallet_settings: WalletSettings,
},
Usk {
usk: Vec<u8>,
birthday: u32,
wallet_settings: WalletSettings,
},
Read,
}
impl WalletConfig {
#[allow(clippy::result_large_err)]
pub(crate) fn resolve(self, chain_type: ChainType) -> Result<WalletBase, WalletError> {
match self {
WalletConfig::NewSeed {
no_of_accounts,
chain_height,
wallet_settings,
} => {
let sapling_activation_height = chain_type
.activation_height(zcash_protocol::consensus::NetworkUpgrade::Sapling)
.expect("should have some sapling activation height");
let birthday =
sapling_activation_height.max(BlockHeight::from_u32(chain_height) - 100);
WalletConfig::MnemonicPhrase {
mnemonic_phrase: Mnemonic::<English>::generate(bip0039::Count::Words24)
.into_phrase(),
no_of_accounts,
birthday: u32::from(birthday),
wallet_settings,
}
.resolve(chain_type)
}
WalletConfig::MnemonicPhrase {
mnemonic_phrase: mnemonic,
no_of_accounts,
birthday,
wallet_settings,
} => {
let mnemonic = Mnemonic::from_phrase(mnemonic)?;
let no_of_accounts = u32::from(no_of_accounts);
let unified_key_store = (0..no_of_accounts)
.map(|account_index| {
let account_id = zip32::AccountId::try_from(account_index)?;
Ok((
account_id,
UnifiedKeyStore::new_from_mnemonic(chain_type, &mnemonic, account_id)?,
))
})
.collect::<Result<BTreeMap<_, _>, KeyError>>()?;
Ok(WalletBase {
unified_key_store,
mnemonic: Some(mnemonic),
birthday: BlockHeight::from_u32(birthday),
wallet_settings,
})
}
WalletConfig::Ufvk {
ufvk,
birthday,
wallet_settings,
} => {
let mut unified_key_store = BTreeMap::new();
unified_key_store.insert(
zip32::AccountId::ZERO,
UnifiedKeyStore::new_from_ufvk(chain_type, ufvk)?,
);
Ok(WalletBase {
unified_key_store,
mnemonic: None,
birthday: BlockHeight::from_u32(birthday),
wallet_settings,
})
}
WalletConfig::Usk {
usk,
birthday,
wallet_settings,
} => {
let mut unified_key_store = BTreeMap::new();
unified_key_store.insert(
zip32::AccountId::ZERO,
UnifiedKeyStore::new_from_usk(usk.as_slice())?,
);
Ok(WalletBase {
unified_key_store,
mnemonic: None,
birthday: BlockHeight::from_u32(birthday),
wallet_settings,
})
}
WalletConfig::Read => Err(WalletError::WalletAlreadyCreated),
}
}
}
pub fn construct_indexer_uri(server: String) -> Result<http::Uri, InvalidUri> {
if server.is_empty() {
return Ok(http::Uri::default());
}
let mut s = if server.starts_with("http") {
server
} else {
"http://".to_string() + &server
};
let uri: http::Uri = s.parse()?;
if uri.port().is_none() {
s += ":9067";
}
s.parse()
}
#[derive(Clone, Debug)]
pub struct ClientConfig {
indexer_uri: Option<http::Uri>,
migration_transmission_uri: Option<http::Uri>,
indexers: Vec<IndexerConfig>,
remote_indexer_trust: Option<Trust>,
chain_type: ChainType,
wallet_dir: PathBuf,
wallet_name: String,
wallet_config: WalletConfig,
}
impl ClientConfig {
#[must_use]
pub fn builder() -> ClientConfigBuilder {
ClientConfigBuilder::default()
}
#[must_use]
pub fn indexer_uri(&self) -> Option<http::Uri> {
self.indexer_uri.clone()
}
#[must_use]
pub fn migration_transmission_uri(&self) -> Option<http::Uri> {
self.migration_transmission_uri.clone()
}
#[must_use]
pub fn indexers(&self) -> &[IndexerConfig] {
&self.indexers
}
#[must_use]
pub fn remote_indexer_trust(&self) -> Option<Trust> {
self.remote_indexer_trust
}
#[must_use]
pub fn chain_type(&self) -> ChainType {
self.chain_type
}
#[must_use]
pub fn wallet_dir(&self) -> PathBuf {
self.wallet_dir.clone()
}
#[must_use]
pub fn wallet_name(&self) -> &str {
&self.wallet_name
}
#[must_use]
pub fn wallet_config(&self) -> WalletConfig {
self.wallet_config.clone()
}
#[must_use]
pub fn get_wallet_path(&self) -> Box<Path> {
let mut wallet_path = self.wallet_dir();
wallet_path.push(self.wallet_name());
wallet_path.into_boxed_path()
}
}
#[derive(Clone, Debug)]
pub struct ClientConfigBuilder {
indexer_uri: Option<http::Uri>,
migration_transmission_uri: Option<http::Uri>,
indexers: Vec<IndexerConfig>,
remote_indexer_trust: Option<Trust>,
chain_type: ChainType,
wallet_dir: Option<PathBuf>,
wallet_name: Option<String>,
wallet_config: WalletConfig,
}
impl ClientConfigBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn set_indexer_uri(mut self, indexer_uri: http::Uri) -> Self {
self.indexer_uri = Some(indexer_uri);
self
}
pub fn set_migration_transmission_uri(mut self, migration_transmission_uri: http::Uri) -> Self {
self.migration_transmission_uri = Some(migration_transmission_uri);
self
}
pub fn add_indexer(mut self, indexer: IndexerConfig) -> Self {
self.indexers.push(indexer);
self
}
pub fn set_remote_indexer_trust(mut self, trust: Trust) -> Self {
self.remote_indexer_trust = Some(trust);
self
}
pub fn set_chain_type(mut self, chain_type: ChainType) -> Self {
self.chain_type = chain_type;
self
}
pub fn set_wallet_dir(mut self, dir: PathBuf) -> Self {
self.wallet_dir = Some(dir);
self
}
pub fn set_wallet_name(mut self, wallet_name: String) -> Self {
self.wallet_name = Some(wallet_name);
self
}
pub fn set_wallet_config(mut self, wallet_config: WalletConfig) -> Self {
self.wallet_config = wallet_config;
self
}
pub fn build(self) -> Result<ClientConfig, ClientConfigError> {
let wallet_dir = wallet_dir_or_default(self.wallet_dir, self.chain_type)?;
let wallet_name = wallet_name_or_default(self.wallet_name);
Ok(ClientConfig {
indexer_uri: self.indexer_uri,
migration_transmission_uri: self.migration_transmission_uri,
indexers: self.indexers,
remote_indexer_trust: self.remote_indexer_trust,
chain_type: self.chain_type,
wallet_dir,
wallet_name,
wallet_config: self.wallet_config,
})
}
}
impl Default for ClientConfigBuilder {
fn default() -> Self {
Self {
indexer_uri: None,
migration_transmission_uri: None,
indexers: Vec::new(),
remote_indexer_trust: None,
wallet_dir: None,
wallet_name: None,
chain_type: ChainType::Mainnet,
wallet_config: WalletConfig::NewSeed {
no_of_accounts: NonZeroU32::try_from(1).expect("hard coded non-zero integer"),
chain_height: 1,
wallet_settings: WalletSettings {
sync_config: SyncConfig {
transparent_address_discovery: TransparentAddressDiscovery::default(),
performance_level: pepper_sync::config::PerformanceLevel::High,
shutdown_on_completion: false,
},
min_confirmations: NonZeroU32::try_from(3)
.expect("hard coded non-zero integer"),
},
},
}
}
}
fn wallet_name_or_default(opt_wallet_name: Option<String>) -> String {
let wallet_name = opt_wallet_name.unwrap_or_else(|| DEFAULT_WALLET_NAME.into());
if wallet_name.is_empty() {
DEFAULT_WALLET_NAME.into()
} else {
wallet_name
}
}
fn wallet_dir_or_default(
opt_wallet_dir: Option<PathBuf>,
#[cfg_attr(
any(target_os = "ios", target_os = "android"),
expect(
unused_variables,
reason = "a mobile caller names the wallet directory, so no default is derived from the chain"
)
)]
chain: ChainType,
) -> Result<PathBuf, ClientConfigError> {
let wallet_dir: PathBuf;
#[cfg(any(target_os = "ios", target_os = "android"))]
{
wallet_dir = opt_wallet_dir.ok_or_else(|| ClientConfigError::WalletDirNotSpecified)?;
}
#[cfg(not(any(target_os = "ios", target_os = "android")))]
{
wallet_dir = opt_wallet_dir.clone().map_or_else(
|| {
let mut dir = dirs::data_dir().ok_or(ClientConfigError::UsersDataDirNotFound)?;
#[cfg(any(target_os = "macos", target_os = "windows"))]
{
dir.push("Zcash");
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
dir.push(".zcash");
}
match chain {
ChainType::Mainnet => {}
ChainType::Testnet => dir.push("testnet3"),
ChainType::Regtest(_) => dir.push("regtest"),
}
Ok(dir)
},
Ok,
)?;
std::fs::create_dir_all(wallet_dir.clone())
.map_err(|e| ClientConfigError::FileError(e.to_string()))?;
}
Ok(wallet_dir)
}
#[derive(thiserror::Error, Debug, Clone)]
pub enum ClientConfigError {
#[error("Wallet directory must be specified for iOS and Android platforms.")]
WalletDirNotSpecified,
#[error("User's default data directory not found.")]
UsersDataDirNotFound,
#[error("Failed to create wallet directory. {0}")]
FileError(String),
}
#[cfg(test)]
mod tests {
use crate::config::ChainType;
use crate::config::ClientConfig;
use zingo_common_components::protocol::ActivationHeights;
#[tokio::test]
async fn test_load_clientconfig() {
let valid_uri =
crate::config::construct_indexer_uri("https://zec.rocks:443".to_string()).unwrap();
let temp_dir = tempfile::TempDir::new().unwrap();
let temp_path = temp_dir.path().to_path_buf();
let valid_config = ClientConfig::builder()
.set_indexer_uri(valid_uri.clone())
.set_chain_type(ChainType::Mainnet)
.set_wallet_dir(temp_path)
.build()
.unwrap();
assert_eq!(valid_config.indexer_uri(), Some(valid_uri));
assert_eq!(valid_config.chain_type(), ChainType::Mainnet);
}
#[test]
fn test_chaintypes_from_strings() {
assert_eq!(ChainType::try_from("mainnet").unwrap(), ChainType::Mainnet);
assert_eq!(ChainType::try_from("testnet").unwrap(), ChainType::Testnet);
assert_eq!(ChainType::try_from("main").unwrap(), ChainType::Mainnet);
assert_eq!(ChainType::try_from("test").unwrap(), ChainType::Testnet);
assert_eq!(
ChainType::try_from("regtest").unwrap(),
ChainType::Regtest(ActivationHeights::default())
);
assert!(ChainType::try_from("invalid").is_err());
let err = ChainType::try_from("badvalue").unwrap_err();
assert_eq!(err.0, "badvalue");
assert!(ChainType::try_from("").is_err());
assert!(ChainType::try_from("Mainnet").is_err());
assert!(ChainType::try_from("MAINNET").is_err());
assert!(ChainType::try_from("Testnet").is_err());
assert!(ChainType::try_from("Regtest").is_err());
assert!(ChainType::try_from("mainnet ").is_err()); assert!(ChainType::try_from(" mainnet").is_err()); assert!(ChainType::try_from("regtест").is_err()); }
#[test]
fn lib_birthday_exceeds_sapling_activation() {
use zcash_protocol::consensus::{NetworkUpgrade, Parameters};
for chain in [ChainType::Mainnet, ChainType::Testnet] {
let sapling_activation = chain
.activation_height(NetworkUpgrade::Sapling)
.expect("public chains have a sapling activation height");
assert!(
crate::config::lib_birthday(chain) > u32::from(sapling_activation),
"lib_birthday({chain}) must exceed sapling activation"
);
}
}
}