zcash_client_backend/data_api/defaults.rs
1//! Reference implementations of [`WalletRead`] and [`WalletWrite`] trait methods.
2//!
3//! This module provides standalone free functions that implement the canonical semantics of
4//! selected trait methods in terms of other, more primitive operations. Wallet backend authors
5//! who do not have a more efficient implementation available can delegate the corresponding
6//! trait method to the function provided here; backends that can do better (e.g. via an
7//! indexed database lookup) should implement the trait method directly.
8//!
9//! [`WalletRead`]: super::WalletRead
10//! [`WalletWrite`]: super::WalletWrite
11
12use zcash_keys::address::{Address, UnifiedAddress};
13use zcash_protocol::consensus;
14
15use super::{Account, WalletRead, error::FindAccountForAddressError};
16
17/// Returns `true` if `address` contains any receiver that matches one of the receivers of the
18/// given [`UnifiedAddress`].
19pub fn address_receiver_matches_ua<P: consensus::Parameters>(
20 address: &Address,
21 ua: &UnifiedAddress,
22 params: &P,
23) -> bool {
24 let zcash_address = address.to_zcash_address(params);
25 ua.as_understood_receivers()
26 .iter()
27 .any(|r| r.corresponds(&zcash_address))
28}
29
30/// Reference implementation of [`WalletRead::find_account_for_address`].
31///
32/// Any address whose shielded component can be represented as a [`UnifiedAddress`] — either
33/// directly, or by synthesizing a single-receiver UA around a bare Sapling address — is
34/// first resolved via UIVK algebra: each account's
35/// [`UnifiedIncomingViewingKey::decrypt_diversifiers`] is asked whether it could have derived
36/// any shielded receiver of the (possibly synthesized) UA. An account matches if at least
37/// one shielded receiver is attributable to it, which finds addresses the account could
38/// produce even if they have never been exposed. If more than one account matches,
39/// [`FindAccountForAddressError::UnifiedAddressConflict`] is returned.
40///
41/// If the algebraic step does not resolve a match (or the address has no shielded receiver,
42/// as is the case for transparent and TEX addresses), this falls back to a linear scan over
43/// each account's tracked addresses via [`WalletRead::list_addresses`], returning the first
44/// account that holds a matching address (as an exact match, or as a receiver embedded in a
45/// stored UA).
46///
47/// Backends that can answer this query more efficiently (e.g. via an indexed database
48/// lookup) should implement [`WalletRead::find_account_for_address`] directly rather than
49/// delegating to this function.
50///
51/// See [`WalletRead::find_account_for_address`] for the semantics of the return value.
52///
53/// [`UnifiedIncomingViewingKey`]: zcash_keys::keys::UnifiedIncomingViewingKey
54/// [`UnifiedIncomingViewingKey::decrypt_diversifiers`]: zcash_keys::keys::UnifiedIncomingViewingKey::decrypt_diversifiers
55pub fn find_account_for_address<W: WalletRead, P: consensus::Parameters>(
56 wallet: &W,
57 params: &P,
58 address: &Address,
59) -> Result<Option<W::AccountId>, FindAccountForAddressError<W::Error>> {
60 // Promote bare shielded addresses to a synthesized single-receiver UA so that every
61 // address with a shielded component is resolved through the same UIVK algebra. The
62 // synthesized UA is owned by this local binding; `shielded_ua` borrows from either the
63 // original address or this binding.
64 let synthesized_ua;
65 let shielded_ua: Option<&UnifiedAddress> = match address {
66 Address::Unified(ua) => Some(ua),
67 Address::Sapling(pa) => {
68 synthesized_ua = UnifiedAddress::from_receivers(
69 #[cfg(feature = "orchard")]
70 None,
71 Some(**pa),
72 None,
73 None,
74 None,
75 );
76 synthesized_ua.as_ref()
77 }
78 Address::Transparent(_) | Address::Tex(_) => None,
79 };
80
81 if let Some(ua) = shielded_ua {
82 let mut found_acc_id: Option<W::AccountId> = None;
83 for acc_id in wallet.get_account_ids()? {
84 let Some(account) = wallet.get_account(acc_id)? else {
85 continue;
86 };
87 if !account.uivk().decrypt_diversifiers(ua).is_empty() {
88 match found_acc_id {
89 None => found_acc_id = Some(acc_id),
90 Some(prev) if prev == acc_id => {}
91 Some(_) => return Err(FindAccountForAddressError::UnifiedAddressConflict),
92 }
93 }
94 }
95 if found_acc_id.is_some() {
96 return Ok(found_acc_id);
97 }
98 }
99
100 // Fall back to a linear scan over each account's tracked addresses. This handles
101 // addresses with no shielded component (transparent and TEX) as well as UAs whose
102 // shielded receivers are not derivable from any account's UIVK but whose transparent
103 // receiver matches a stored address.
104 let zcash_address = address.to_zcash_address(params);
105 for acc_id in wallet.get_account_ids()? {
106 for addr_info in wallet.list_addresses(acc_id)? {
107 let stored = addr_info.address();
108 if stored == address {
109 return Ok(Some(acc_id));
110 }
111 if let Address::Unified(stored_ua) = stored
112 && stored_ua
113 .as_understood_receivers()
114 .iter()
115 .any(|r| r.corresponds(&zcash_address))
116 {
117 return Ok(Some(acc_id));
118 }
119 }
120 }
121 Ok(None)
122}