use std::collections::{BTreeMap, HashMap};
use std::num::NonZeroU32;
use bip0039::Mnemonic;
use pepper_sync::sync::MAX_REORG_ALLOWANCE;
use zcash_keys::address::UnifiedAddress;
use zcash_primitives::transaction::TxId;
use zcash_protocol::consensus::{BlockHeight, Parameters};
use zcash_transparent::keys::NonHardenedChildIndex;
use pepper_sync::keys::transparent::{TransparentScope, encode_address};
use pepper_sync::wallet::{KeyIdInterface, ScanTarget, ShardTrees};
use pepper_sync::{
keys::transparent::TransparentAddressId,
wallet::{NullifierMap, OutputId, SyncState, WalletBlock, WalletTransaction},
};
use zingo_price::PriceList;
use crate::config::{ChainType, WalletConfig};
use crate::data::proposal::ZingoProposal;
use error::{KeyError, WalletError};
use keys::unified::{UnifiedAddressId, UnifiedKeyStore};
pub mod error;
pub mod expiry;
pub(crate) mod legacy;
pub mod traits;
pub mod utils;
pub mod balance;
pub mod disk;
pub mod keys;
pub mod locks;
pub mod migration;
pub mod output;
pub mod propose;
pub mod send;
pub mod summary;
pub mod sync;
pub mod transaction;
pub mod transparent;
mod zcb_traits;
pub use pepper_sync::config::{
PerformanceLevel, SyncConfig, TransparentAddressDiscovery, TransparentAddressDiscoveryScopes,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WalletSettings {
pub sync_config: pepper_sync::config::SyncConfig,
pub min_confirmations: NonZeroU32,
}
impl Default for WalletSettings {
fn default() -> Self {
Self {
sync_config: SyncConfig::default(),
min_confirmations: NonZeroU32::try_from(3).expect("hard-coded non-zero integer"),
}
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
pub struct RecoveryInfo {
pub seed_phrase: String,
pub birthday: u64,
pub no_of_accounts: u32,
}
impl std::fmt::Display for RecoveryInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Wallet backup info:
{{
seed phrase: {}
birthday: {}
no_of_accounts: {}
}}",
self.seed_phrase, self.birthday, self.no_of_accounts,
)
}
}
pub(crate) struct WalletBase {
pub(crate) unified_key_store: BTreeMap<zip32::AccountId, UnifiedKeyStore>,
pub(crate) mnemonic: Option<Mnemonic>,
pub(crate) birthday: BlockHeight,
pub(crate) wallet_settings: WalletSettings,
}
#[derive(Debug)]
pub struct LightWallet {
current_version: u64,
read_version: u64,
chain_type: ChainType,
mnemonic: Option<Mnemonic>,
birthday: BlockHeight,
pub unified_key_store: BTreeMap<zip32::AccountId, UnifiedKeyStore>,
unified_addresses: BTreeMap<UnifiedAddressId, UnifiedAddress>,
transparent_addresses: BTreeMap<TransparentAddressId, String>,
pub wallet_blocks: BTreeMap<BlockHeight, WalletBlock>,
pub wallet_transactions: HashMap<TxId, WalletTransaction>,
pub nullifier_map: NullifierMap,
pub outpoint_map: BTreeMap<OutputId, ScanTarget>,
pub shard_trees: ShardTrees,
pub sync_state: SyncState,
pub wallet_settings: WalletSettings,
pub price_list: PriceList,
pub migration: Option<migration::MigrationState>,
send_proposal: Option<ZingoProposal>,
output_locks: locks::OutputLocks,
pub(crate) save_required: bool,
}
impl LightWallet {
#[allow(clippy::result_large_err)]
pub fn new(chain_type: ChainType, wallet_config: WalletConfig) -> Result<Self, WalletError> {
let wallet_base = wallet_config.resolve(chain_type)?;
Self::from_base(chain_type, wallet_base)
}
#[allow(clippy::result_large_err)]
pub(crate) fn from_base(
chain_type: ChainType,
wallet_base: WalletBase,
) -> Result<Self, WalletError> {
let WalletBase {
unified_key_store,
mnemonic,
birthday,
wallet_settings,
} = wallet_base;
let sapling_activation_height = chain_type
.activation_height(zcash_protocol::consensus::NetworkUpgrade::Sapling)
.expect("should have some sapling activation height");
if birthday < sapling_activation_height {
return Err(WalletError::BirthdayBelowSapling(
u32::from(birthday),
u32::from(sapling_activation_height),
));
}
let unified_key = unified_key_store
.get(&zip32::AccountId::ZERO)
.expect("account 0 must exist");
let mut unified_addresses = BTreeMap::new();
if let Some(receivers) = unified_key.default_receivers() {
let unified_address_id = UnifiedAddressId {
account_id: zip32::AccountId::ZERO,
address_index: 0,
};
let first_unified_address = unified_key
.generate_unified_address(unified_address_id.address_index, receivers)?;
unified_addresses.insert(unified_address_id, first_unified_address.clone());
}
let mut transparent_addresses = BTreeMap::new();
let transparent_address_id = TransparentAddressId::new(
zip32::AccountId::ZERO,
TransparentScope::External,
NonHardenedChildIndex::ZERO,
);
match unified_key.generate_transparent_address(
transparent_address_id.address_index(),
transparent_address_id.scope(),
) {
Ok(first_transparent_address) => {
transparent_addresses.insert(
transparent_address_id,
encode_address(&chain_type, first_transparent_address),
);
}
Err(KeyError::NoViewCapability) => (),
Err(e) => return Err(e.into()),
}
Ok(Self {
current_version: LightWallet::serialized_version(),
read_version: LightWallet::serialized_version(),
chain_type,
mnemonic,
birthday: BlockHeight::from_u32(birthday.into()),
unified_key_store,
unified_addresses,
transparent_addresses,
wallet_blocks: BTreeMap::new(),
wallet_transactions: HashMap::new(),
nullifier_map: NullifierMap::new(),
outpoint_map: BTreeMap::new(),
shard_trees: ShardTrees::new(),
sync_state: SyncState::new(),
wallet_settings,
price_list: PriceList::new(),
migration: None,
save_required: true,
send_proposal: None,
output_locks: locks::OutputLocks::default(),
})
}
#[must_use]
pub fn current_version(&self) -> u64 {
self.current_version
}
#[must_use]
pub fn read_version(&self) -> u64 {
self.read_version
}
#[must_use]
pub fn birthday(&self) -> BlockHeight {
self.birthday
}
#[must_use]
pub fn chain_type(&self) -> ChainType {
self.chain_type
}
#[must_use]
pub(crate) fn mnemonic(&self) -> Option<&Mnemonic> {
self.mnemonic.as_ref()
}
#[must_use]
pub fn mnemonic_phrase(&self) -> Option<String> {
self.mnemonic().map(|m| m.phrase().to_string())
}
#[must_use]
pub fn unified_addresses(&self) -> &BTreeMap<UnifiedAddressId, UnifiedAddress> {
&self.unified_addresses
}
#[must_use]
pub fn unified_addresses_json(&self) -> json::JsonValue {
json::JsonValue::Array(
self.unified_addresses
.iter()
.map(|(id, unified_address)| {
json::object! {
"account" => u32::from(id.account_id),
"address_index" => id.address_index,
"has_orchard" => unified_address.has_orchard(),
"has_sapling" => unified_address.has_sapling(),
"has_transparent" => unified_address.has_transparent(),
"encoded_address" => unified_address.encode(&self.chain_type),
}
})
.collect::<Vec<_>>(),
)
}
#[must_use]
pub fn transparent_addresses(&self) -> &BTreeMap<TransparentAddressId, String> {
&self.transparent_addresses
}
#[must_use]
pub(crate) fn transparent_addresses_mut(
&mut self,
) -> &mut BTreeMap<TransparentAddressId, String> {
&mut self.transparent_addresses
}
#[must_use]
pub fn transparent_addresses_json(&self) -> json::JsonValue {
json::JsonValue::Array(
self.transparent_addresses
.iter()
.map(|(id, transparent_address)| {
json::object! {
"account" => u32::from(id.account_id()),
"address_index" => id.address_index().index(),
"scope" => id.scope().to_string(),
"encoded_address" => transparent_address.clone(),
}
})
.collect::<Vec<_>>(),
)
}
pub fn clear_proposal(&mut self) {
self.send_proposal = None;
}
pub fn mark_dirty(&mut self) {
self.save_required = true;
}
#[must_use]
pub fn recovery_info(&self) -> Option<RecoveryInfo> {
Some(RecoveryInfo {
seed_phrase: self.mnemonic_phrase()?,
birthday: self.birthday.into(),
no_of_accounts: self.unified_key_store.len() as u32,
})
}
#[allow(clippy::result_large_err)]
pub fn create_new_account(&mut self) -> Result<(), WalletError> {
let last_account = self.unified_key_store.keys().copied().max();
let account_id = last_account.map_or(Ok(zip32::AccountId::ZERO), |last_account| {
last_account
.next()
.ok_or(WalletError::AccountCreationFailed)
})?;
self.unified_key_store.insert(
account_id,
UnifiedKeyStore::new_from_mnemonic(
self.chain_type(),
self.mnemonic().ok_or(WalletError::MnemonicNotFound)?,
account_id,
)?,
);
self.save_required = true;
Ok(())
}
pub fn save(&mut self) -> std::io::Result<Option<Vec<u8>>> {
if self.save_required {
let chain_type = self.chain_type;
let mut wallet_bytes: Vec<u8> = vec![];
self.write(&mut wallet_bytes, &chain_type)?;
Ok(Some(wallet_bytes))
} else {
Ok(None)
}
}
pub fn prune_price_list(&mut self) {
let Some(fully_scanned_height) = self.sync_state.fully_scanned_height() else {
return;
};
let transaction_times = self
.wallet_transactions
.values()
.filter(|transaction| {
transaction
.status()
.get_confirmed_height()
.is_some_and(|height| height <= fully_scanned_height)
})
.map(pepper_sync::wallet::WalletTransaction::datetime)
.collect();
let prune_below = self
.wallet_blocks
.get(&fully_scanned_height)
.expect("fully scanned height should always be on a scan range boundary")
.time();
self.price_list.prune(transaction_times, prune_below);
}
pub fn clear_all(&mut self) {
let chain_height_opt = self.sync_state.last_known_chain_height();
self.sync_state = SyncState::new();
if let Some(chain_height) = chain_height_opt {
pepper_sync::add_scan_targets(
&mut self.sync_state,
&self
.wallet_transactions
.values()
.filter(|transaction| {
transaction
.status()
.is_confirmed_before(&(chain_height - MAX_REORG_ALLOWANCE))
})
.filter_map(|transaction| {
transaction
.status()
.get_confirmed_height()
.map(|height| ScanTarget::new(height, Some(transaction.txid())))
})
.collect::<Vec<_>>(),
);
}
self.wallet_blocks.clear();
self.wallet_transactions.clear();
self.nullifier_map.clear();
self.outpoint_map.clear();
self.shard_trees = ShardTrees::new();
self.price_list = PriceList::new();
self.save_required = true;
}
}
#[cfg(test)]
mod tests {
use incrementalmerkletree::frontier::CommitmentTree;
use orchard::tree::MerkleHashOrchard;
#[test]
fn anchor_from_tree_works() {
let commitments = [
[
0x68, 0x13, 0x5c, 0xf4, 0x99, 0x33, 0x22, 0x90, 0x99, 0xa4, 0x4e, 0xc9, 0x9a, 0x75,
0xe1, 0xe1, 0xcb, 0x46, 0x40, 0xf9, 0xb5, 0xbd, 0xec, 0x6b, 0x32, 0x23, 0x85, 0x6f,
0xea, 0x16, 0x39, 0x0a,
],
[
0x78, 0x31, 0x50, 0x08, 0xfb, 0x29, 0x98, 0xb4, 0x30, 0xa5, 0x73, 0x1d, 0x67, 0x26,
0x20, 0x7d, 0xc0, 0xf0, 0xec, 0x81, 0xea, 0x64, 0xaf, 0x5c, 0xf6, 0x12, 0x95, 0x69,
0x01, 0xe7, 0x2f, 0x0e,
],
[
0xee, 0x94, 0x88, 0x05, 0x3a, 0x30, 0xc5, 0x96, 0xb4, 0x30, 0x14, 0x10, 0x5d, 0x34,
0x77, 0xe6, 0xf5, 0x78, 0xc8, 0x92, 0x40, 0xd1, 0xd1, 0xee, 0x17, 0x43, 0xb7, 0x7b,
0xb6, 0xad, 0xc4, 0x0a,
],
[
0x9d, 0xdc, 0xe7, 0xf0, 0x65, 0x01, 0xf3, 0x63, 0x76, 0x8c, 0x5b, 0xca, 0x3f, 0x26,
0x46, 0x60, 0x83, 0x4d, 0x4d, 0xf4, 0x46, 0xd1, 0x3e, 0xfc, 0xd7, 0xc6, 0xf1, 0x7b,
0x16, 0x7a, 0xac, 0x1a,
],
[
0xbd, 0x86, 0x16, 0x81, 0x1c, 0x6f, 0x5f, 0x76, 0x9e, 0xa4, 0x53, 0x9b, 0xba, 0xff,
0x0f, 0x19, 0x8a, 0x6c, 0xdf, 0x3b, 0x28, 0x0d, 0xd4, 0x99, 0x26, 0x16, 0x3b, 0xd5,
0x3f, 0x53, 0xa1, 0x21,
],
];
let mut orchard_tree: CommitmentTree<MerkleHashOrchard, 32> = CommitmentTree::empty();
for commitment in commitments {
orchard_tree
.append(MerkleHashOrchard::from_bytes(&commitment).unwrap())
.unwrap();
}
let anchor = [
0xc8, 0x75, 0xbe, 0x2d, 0x60, 0x87, 0x3f, 0x8b, 0xcd, 0xeb, 0x91, 0x28, 0x2e, 0x64,
0x2e, 0x0c, 0xc6, 0x5f, 0xf7, 0xd0, 0x64, 0x2d, 0x13, 0x7b, 0x28, 0xcf, 0x28, 0xcc,
0x9c, 0x52, 0x7f, 0x0e,
];
let anchor = orchard::Anchor::from(MerkleHashOrchard::from_bytes(&anchor).unwrap());
assert_eq!(orchard::Anchor::from(orchard_tree.root()), anchor);
}
}