use std::collections::BTreeSet;
use pepper_sync::{
keys::{
decode_address,
transparent::{self, TransparentAddressId, TransparentScope},
},
wallet::{KeyIdInterface, TransparentCoin},
};
use unified::{ReceiverSelection, UnifiedAddressId, UnifiedKeyStore};
use zcash_keys::address::UnifiedAddress;
use zcash_transparent::address::TransparentAddress;
use zcash_transparent::keys::NonHardenedChildIndex;
use zip32::DiversifierIndex;
use super::{LightWallet, error::KeyError};
pub mod legacy;
pub mod unified;
pub enum WalletAddressRef {
Unified {
account_id: zip32::AccountId,
address_index: Option<u32>,
has_orchard: bool,
has_sapling: bool,
has_transparent: bool,
encoded_address: String,
},
OrchardInternal {
account_id: zip32::AccountId,
diversifier_index: DiversifierIndex,
encoded_address: String,
},
SaplingExternal {
account_id: zip32::AccountId,
diversifier_index: DiversifierIndex,
encoded_address: String,
},
Transparent {
account_id: zip32::AccountId,
scope: TransparentScope,
address_index: NonHardenedChildIndex,
encoded_address: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WalletKind {
Mnemonic,
SpendingKey,
ViewingKey {
transparent: bool,
sapling: bool,
orchard: bool,
},
NoKeys,
}
impl LightWallet {
pub fn kind(&self) -> Result<WalletKind, KeyError> {
if self.mnemonic_phrase().is_some() {
return Ok(WalletKind::Mnemonic);
}
let kind = match self
.unified_key_store
.get(&zip32::AccountId::ZERO)
.ok_or(KeyError::NoAccountKeys)?
{
UnifiedKeyStore::Spend(_) => WalletKind::SpendingKey,
UnifiedKeyStore::View(ufvk) => WalletKind::ViewingKey {
transparent: ufvk.p2pkh().is_some(),
sapling: ufvk.sapling().is_some(),
orchard: ufvk.orchard().is_some(),
},
UnifiedKeyStore::Empty => WalletKind::NoKeys,
};
Ok(kind)
}
pub fn generate_unified_address(
&mut self,
receivers: ReceiverSelection,
account_id: zip32::AccountId,
) -> Result<(UnifiedAddressId, UnifiedAddress), KeyError> {
let address_id = UnifiedAddressId {
account_id,
address_index: self
.unified_addresses
.keys()
.filter(|&address_id| address_id.account_id == account_id)
.map(|&address_id| address_id.address_index)
.max()
.map_or(0, |address_id| address_id + 1),
};
let unified_address = self
.unified_key_store
.get(&account_id)
.ok_or(KeyError::NoAccountKeys)?
.generate_unified_address(address_id.address_index, receivers)?;
self.unified_addresses
.insert(address_id, unified_address.clone());
self.save_required = true;
Ok((address_id, unified_address))
}
pub fn generate_transparent_address(
&mut self,
account_id: zip32::AccountId,
enforce_no_gap: bool,
) -> Result<(TransparentAddressId, TransparentAddress), KeyError> {
let latest_address = self
.transparent_addresses
.iter()
.filter(|(address_id, _)| {
address_id.scope() == TransparentScope::External
&& address_id.account_id() == account_id
})
.max_by_key(|(address_id, _)| address_id.address_index());
if enforce_no_gap
&& let Some((_, address)) = latest_address
&& !self
.wallet_outputs::<TransparentCoin>()
.iter()
.any(|&output| output.address() == address.as_str())
{
return Err(KeyError::GapError);
}
let address_index =
latest_address.map_or(Ok(NonHardenedChildIndex::ZERO), |(address_index, _)| {
address_index
.address_index()
.next()
.ok_or(KeyError::InvalidNonHardenedChildIndex)
})?;
let address_id =
TransparentAddressId::new(account_id, TransparentScope::External, address_index);
let external_address = self
.unified_key_store
.get(&account_id)
.ok_or(KeyError::NoAccountKeys)?
.generate_transparent_address(address_id.address_index(), address_id.scope())?;
self.transparent_addresses.insert(
address_id,
transparent::encode_address(&self.chain_type, external_address),
);
self.save_required = true;
Ok((address_id, external_address))
}
pub fn derive_refund_addresses(
&self,
n: usize,
account_id: zip32::AccountId,
) -> Result<Vec<(TransparentAddressId, TransparentAddress)>, KeyError> {
let first_index = self
.transparent_addresses
.keys()
.filter(|&address_id| {
address_id.scope() == TransparentScope::Refund
&& address_id.account_id() == account_id
})
.map(|&address_id| address_id.address_index())
.max()
.map_or(Ok(NonHardenedChildIndex::ZERO), |address_index| {
address_index
.next()
.ok_or(KeyError::InvalidNonHardenedChildIndex)
})?
.index() as usize;
(first_index..(first_index + n))
.map(|address_index| {
let address_id = TransparentAddressId::new(
account_id,
TransparentScope::Refund,
NonHardenedChildIndex::from_index(address_index as u32)
.ok_or(KeyError::InvalidNonHardenedChildIndex)?,
);
let refund_address = self
.unified_key_store
.get(&account_id)
.ok_or(KeyError::NoAccountKeys)?
.generate_transparent_address(address_id.address_index(), address_id.scope())?;
Ok((address_id, refund_address))
})
.collect()
}
pub fn generate_refund_addresses(
&mut self,
n: usize,
account_id: zip32::AccountId,
) -> Result<Vec<(TransparentAddressId, TransparentAddress)>, KeyError> {
let refund_addresses = self.derive_refund_addresses(n, account_id)?;
for (address_id, refund_address) in &refund_addresses {
self.transparent_addresses.insert(
*address_id,
transparent::encode_address(&self.chain_type, *refund_address),
);
}
self.save_required = true;
Ok(refund_addresses)
}
pub fn is_wallet_address(
&self,
encoded_address: &str,
) -> Result<Option<WalletAddressRef>, KeyError> {
Ok(match decode_address(&self.chain_type, encoded_address)? {
zcash_keys::address::Address::Unified(address) => {
let orchard = address
.orchard()
.and_then(|address| self.is_orchard_address_in_unified_addresses(address));
let sapling = address
.sapling()
.and_then(|address| self.is_sapling_address_in_unified_addresses(address));
let transparent = address
.transparent()
.and_then(|address| self.is_transparent_wallet_address(address))
.filter(|address_id| address_id.scope() == TransparentScope::External);
if let Some((unified_address_id, _unified_address)) = orchard {
let address_index = sapling.as_ref().map_or(
Some(unified_address_id.address_index),
|(id, _address)| {
if *id == unified_address_id {
Some(unified_address_id.address_index)
} else {
None
}
},
);
Some(WalletAddressRef::Unified {
account_id: unified_address_id.account_id,
address_index,
has_orchard: true,
has_sapling: sapling.is_some(),
has_transparent: transparent.is_some(),
encoded_address: encoded_address.to_string(),
})
} else if let Some((unified_address_id, _unified_address)) = sapling {
Some(WalletAddressRef::Unified {
account_id: unified_address_id.account_id,
address_index: Some(unified_address_id.address_index),
has_orchard: false,
has_sapling: true,
has_transparent: transparent.is_some(),
encoded_address: encoded_address.to_string(),
})
} else {
None
}
}
zcash_keys::address::Address::Sapling(address) => {
self.is_sapling_address_in_unified_addresses(&address).map(
|(unified_address_id, unified_address)| WalletAddressRef::Unified {
account_id: unified_address_id.account_id,
address_index: Some(unified_address_id.address_index),
has_orchard: unified_address.has_orchard(),
has_sapling: true,
has_transparent: unified_address.has_transparent(),
encoded_address: encoded_address.to_string(),
},
)
}
zcash_keys::address::Address::Transparent(address) => self
.is_transparent_wallet_address(&address)
.map(|address_id| WalletAddressRef::Transparent {
account_id: address_id.account_id(),
scope: address_id.scope(),
address_index: address_id.address_index(),
encoded_address: encoded_address.to_string(),
}),
zcash_keys::address::Address::Tex(_) => None,
})
}
pub fn is_address_derived_by_keys(
&self,
encoded_address: &str,
) -> Result<Option<WalletAddressRef>, KeyError> {
Ok(match decode_address(&self.chain_type, encoded_address)? {
zcash_keys::address::Address::Unified(address) => {
let orchard = address
.orchard()
.and_then(|address| self.is_orchard_address_derived_from_fvks(address));
let sapling = address
.sapling()
.and_then(|address| self.is_sapling_address_derived_from_fvks(address));
let transparent = address
.transparent()
.and_then(|address| self.is_transparent_wallet_address(address))
.filter(|address_id| address_id.scope() == TransparentScope::External);
if let Some((account_id, scope, orchard_diversifier_index)) = orchard {
if scope == zip32::Scope::External {
let address_index = u32::try_from(orchard_diversifier_index).ok().and_then(
|address_index| {
self.unified_addresses()
.get(&UnifiedAddressId {
account_id,
address_index,
})
.and_then(|unified_address| {
if *unified_address == *address {
Some(address_index)
} else {
None
}
})
},
);
Some(WalletAddressRef::Unified {
account_id,
address_index,
has_orchard: true,
has_sapling: sapling.is_some(),
has_transparent: transparent.is_some(),
encoded_address: encoded_address.to_string(),
})
} else if scope == zip32::Scope::Internal {
Some(WalletAddressRef::OrchardInternal {
account_id,
diversifier_index: orchard_diversifier_index,
encoded_address: encoded_address.to_string(),
})
} else {
unreachable!("Only external and internal scopes exist!");
}
} else if let Some((account_id, diversifier_index)) = sapling {
let address_index = Some(
self.unified_key_store
.get(&account_id)
.expect("key must exist in this scope")
.determine_nth_valid_sapling_diversifier(diversifier_index)
.expect("key must exist in this scope")
- 1,
)
.and_then(|address_index| {
self.unified_addresses()
.get(&UnifiedAddressId {
account_id,
address_index,
})
.and_then(|unified_address| {
if *unified_address == *address {
Some(address_index)
} else {
None
}
})
});
Some(WalletAddressRef::Unified {
account_id,
address_index,
has_orchard: false,
has_sapling: true,
has_transparent: transparent.is_some(),
encoded_address: encoded_address.to_string(),
})
} else {
None
}
}
zcash_keys::address::Address::Sapling(address) => {
self.is_sapling_address_derived_from_fvks(&address).map(
|(account_id, diversifier_index)| WalletAddressRef::SaplingExternal {
account_id,
diversifier_index,
encoded_address: encoded_address.to_string(),
},
)
}
zcash_keys::address::Address::Transparent(address) => self
.is_transparent_wallet_address(&address)
.map(|address_id| WalletAddressRef::Transparent {
account_id: address_id.account_id(),
scope: address_id.scope(),
address_index: address_id.address_index(),
encoded_address: encoded_address.to_string(),
}),
zcash_keys::address::Address::Tex(_) => None,
})
}
#[must_use]
pub fn is_transparent_wallet_address(
&self,
address: &TransparentAddress,
) -> Option<TransparentAddressId> {
let encoded_address = transparent::encode_address(&self.chain_type, *address);
self.transparent_addresses
.iter()
.find(|(_, wallet_address)| **wallet_address == encoded_address)
.map(|(address_id, _)| *address_id)
}
#[must_use]
pub fn is_sapling_address_derived_from_fvks(
&self,
address: &sapling_crypto::PaymentAddress,
) -> Option<(zip32::AccountId, DiversifierIndex)> {
for (account_id, unified_key) in &self.unified_key_store {
if let Some((diversifier_index, _)) =
sapling_crypto::zip32::DiversifiableFullViewingKey::try_from(unified_key)
.ok()
.and_then(|fvk| fvk.decrypt_diversifier(address))
{
return Some((*account_id, diversifier_index));
}
}
None
}
#[must_use]
pub fn is_orchard_address_derived_from_fvks(
&self,
address: &orchard::Address,
) -> Option<(zip32::AccountId, zip32::Scope, DiversifierIndex)> {
for (account_id, unified_key) in &self.unified_key_store {
let Ok(fvk) = orchard::keys::FullViewingKey::try_from(unified_key) else {
continue;
};
for scope in [zip32::Scope::External, zip32::Scope::Internal] {
if let Some(diversifier_index) = fvk.to_ivk(scope).diversifier_index(address) {
return Some((*account_id, scope, diversifier_index));
}
}
}
None
}
#[must_use]
pub fn is_sapling_address_in_unified_addresses(
&self,
address: &sapling_crypto::PaymentAddress,
) -> Option<(UnifiedAddressId, UnifiedAddress)> {
self.unified_addresses
.iter()
.find(|(_, unified_address)| unified_address.sapling() == Some(address))
.map(|(id, address)| (*id, address.clone()))
}
#[must_use]
pub fn is_orchard_address_in_unified_addresses(
&self,
address: &orchard::Address,
) -> Option<(UnifiedAddressId, UnifiedAddress)> {
self.unified_addresses
.iter()
.find(|(_, unified_address)| unified_address.orchard() == Some(address))
.map(|(id, address)| (*id, address.clone()))
}
pub(crate) fn highest_refund_address_index(&self) -> Option<NonHardenedChildIndex> {
self.transparent_addresses()
.keys()
.filter(|id| id.scope() == TransparentScope::Refund)
.max_by_key(|id| id.address_index())
.map(|id| id.address_index())
}
pub(crate) fn truncate_failed_refund_addresses(&mut self) {
let accounts: BTreeSet<zip32::AccountId> = self
.transparent_addresses()
.keys()
.filter(|id| id.scope() == TransparentScope::Refund)
.map(|id| id.account_id())
.collect();
for account_id in accounts {
self.truncate_failed_refund_addresses_for_account(account_id);
}
}
fn truncate_failed_refund_addresses_for_account(&mut self, account_id: zip32::AccountId) {
loop {
let Some((id, address)) = self
.transparent_addresses()
.iter()
.filter(|(id, _)| {
id.scope() == TransparentScope::Refund && id.account_id() == account_id
})
.max_by_key(|(id, _)| id.address_index())
.map(|(id, address)| (*id, address.clone()))
else {
return;
};
let mut paid = false;
let mut live = false;
for transaction in self.wallet_transactions.values() {
if transaction
.transparent_coins()
.iter()
.any(|coin| coin.address() == address)
{
paid = true;
live |= !transaction.status().is_failed();
}
}
if !paid || live {
return;
}
self.transparent_addresses_mut().remove(&id);
}
}
pub(crate) fn truncate_refund_addresses(&mut self, index_opt: Option<NonHardenedChildIndex>) {
if let Some(current_highest_index) = self.highest_refund_address_index()
&& index_opt.is_none_or(|index| current_highest_index > index)
{
self.transparent_addresses_mut().retain(|id, _| {
if let Some(index) = index_opt {
!(id.scope() == TransparentScope::Refund && id.address_index() > index)
} else {
id.scope() != TransparentScope::Refund
}
});
}
}
}
#[cfg(any(test, feature = "testutils"))]
mod test {
use zcash_protocol::PoolType;
use crate::wallet::LightWallet;
use super::unified::UnifiedAddressId;
impl LightWallet {
#[must_use]
pub fn get_address(&self, pool: PoolType) -> String {
match pool {
PoolType::IRONWOOD | PoolType::ORCHARD => self
.unified_addresses()
.get(&UnifiedAddressId {
address_index: 0,
account_id: zip32::AccountId::ZERO,
})
.unwrap()
.encode(&self.chain_type),
PoolType::SAPLING => self
.unified_addresses()
.get(&UnifiedAddressId {
address_index: 1,
account_id: zip32::AccountId::ZERO,
})
.unwrap()
.encode(&self.chain_type),
PoolType::Transparent => {
self.transparent_addresses.values().next().unwrap().clone()
}
}
}
}
}
#[cfg(test)]
mod tests {
use bip0039::Mnemonic;
use pepper_sync::keys::transparent::{self, TransparentAddressId, TransparentScope};
use zcash_transparent::keys::NonHardenedChildIndex;
use zingo_common_components::protocol::ActivationHeights;
use zingo_test_vectors::seeds;
use crate::config::ChainType;
use crate::wallet::LightWallet;
use crate::wallet::keys::unified::{ReceiverSelection, UnifiedAddressId};
fn regtest_wallet(mnemonic_phrase: &str) -> LightWallet {
crate::testutils::synthetic_wallet::SyntheticWalletBuilder::new(mnemonic_phrase).build()
}
#[test]
fn taddrs_from_old_seeds_stay_stable() {
let wallet = regtest_wallet(seeds::HOSPITAL_MUSEUM_SEED);
assert_eq!(
wallet.transparent_addresses().values().next().unwrap(),
"tmFLszfkjgim4zoUMAXpuohnFBAKy99rr2i"
);
}
#[test]
fn address_generation_deterministic_and_coherent() {
let seed_phrase = Mnemonic::<bip0039::English>::from_entropy([1; 32])
.unwrap()
.to_string();
let mut wallet = regtest_wallet(&seed_phrase);
let network = ChainType::Regtest(ActivationHeights::default());
wallet
.generate_unified_address(ReceiverSelection::sapling_only(), zip32::AccountId::ZERO)
.unwrap();
let (new_address_id, new_address) = wallet
.generate_unified_address(ReceiverSelection::all_shielded(), zip32::AccountId::ZERO)
.unwrap();
assert_eq!(
new_address_id,
UnifiedAddressId {
account_id: zip32::AccountId::ZERO,
address_index: 2
}
);
assert!(new_address.has_orchard());
assert!(new_address.has_sapling());
assert!(!new_address.has_transparent());
assert_eq!(
new_address.encode(&network),
"\
uregtest1ds3zxwluuzmcwvdxh4wf8xsger96c5yyzqhwzwu7vt85crj4jyf7nsn258rn89g68lvelsjhkqywz8w70wxdg2cmnul4zadukwu2ywezgjwt36\
f06qvre5qdlkqp5fksyy9j5dm0fdwxwptkk04gzt84r5qv0wfdlx250n0gdcdd6e00"
);
let (sapling_address_id, sapling_address) = wallet
.generate_unified_address(ReceiverSelection::sapling_only(), zip32::AccountId::ZERO)
.unwrap();
assert_eq!(
sapling_address_id,
UnifiedAddressId {
account_id: zip32::AccountId::ZERO,
address_index: 3
}
);
assert!(!sapling_address.has_orchard());
assert!(sapling_address.has_sapling());
assert!(!sapling_address.has_transparent());
assert_eq!(
sapling_address.encode(&network),
"\
uregtest1n22mmna853578fakgx6z6adn24ey5r7wfye8ulhscqc9hvm0rf5czxjuz9te0zzc8j93y35gzw53tdmgz6dtfvlnfmjwl2a84cx5m3fq"
);
let (taddress_id, new_taddress) = wallet
.generate_transparent_address(zip32::AccountId::ZERO, false)
.unwrap();
assert_eq!(
taddress_id,
TransparentAddressId::new(
zip32::AccountId::ZERO,
TransparentScope::External,
NonHardenedChildIndex::from_index(1).unwrap()
)
);
assert_eq!(
transparent::encode_address(&network, new_taddress),
"\
tmQuMoTTjU3GFfTjrhPiBYihbTVfYmPk5Gr"
);
}
#[test]
fn deriving_a_refund_address_answers_with_the_same_index_until_apply() {
let wallet = regtest_wallet(seeds::HOSPITAL_MUSEUM_SEED);
let first = wallet
.derive_refund_addresses(1, zip32::AccountId::ZERO)
.unwrap();
let again = wallet
.derive_refund_addresses(1, zip32::AccountId::ZERO)
.unwrap();
assert_eq!(first, again);
assert_eq!(
first[0].0,
TransparentAddressId::new(
zip32::AccountId::ZERO,
TransparentScope::Refund,
NonHardenedChildIndex::ZERO
)
);
}
#[test]
fn reserving_a_refund_address_moves_the_next_derivation_on() {
let mut wallet = regtest_wallet(seeds::HOSPITAL_MUSEUM_SEED);
let reserved = wallet
.generate_refund_addresses(1, zip32::AccountId::ZERO)
.unwrap();
let next = wallet
.derive_refund_addresses(1, zip32::AccountId::ZERO)
.unwrap();
assert_eq!(
reserved[0].0.address_index(),
NonHardenedChildIndex::ZERO,
"the first reservation takes index zero"
);
assert_eq!(
next[0].0.address_index(),
NonHardenedChildIndex::from_index(1).unwrap(),
"a reserved index is spent, so the next derivation moves past it"
);
}
}