1use std::collections::HashMap;
2
3use sapling::note_encryption::{PreparedIncomingViewingKey, SaplingDomain};
4use zcash_keys::keys::UnifiedFullViewingKey;
5use zcash_note_encryption::{try_note_decryption, try_output_recovery_with_ovk};
6use zcash_primitives::{
7 transaction::components::sapling::zip212_enforcement, transaction::Transaction,
8};
9use zcash_protocol::{
10 consensus::{self, BlockHeight, NetworkUpgrade},
11 memo::MemoBytes,
12 value::Zatoshis,
13};
14use zip32::Scope;
15
16use crate::data_api::DecryptedTransaction;
17
18#[cfg(feature = "orchard")]
19use orchard::note_encryption::OrchardDomain;
20
21#[derive(Debug, Copy, Clone, PartialEq, Eq)]
23pub enum TransferType {
24 Incoming,
27 WalletInternal,
30 Outgoing,
33}
34
35pub struct DecryptedOutput<Note, AccountId> {
37 index: usize,
38 note: Note,
39 account: AccountId,
40 memo: MemoBytes,
41 transfer_type: TransferType,
42}
43
44impl<Note, AccountId> DecryptedOutput<Note, AccountId> {
45 pub fn new(
46 index: usize,
47 note: Note,
48 account: AccountId,
49 memo: MemoBytes,
50 transfer_type: TransferType,
51 ) -> Self {
52 Self {
53 index,
54 note,
55 account,
56 memo,
57 transfer_type,
58 }
59 }
60
61 pub fn index(&self) -> usize {
64 self.index
65 }
66
67 pub fn note(&self) -> &Note {
69 &self.note
70 }
71
72 pub fn account(&self) -> &AccountId {
74 &self.account
75 }
76
77 pub fn memo(&self) -> &MemoBytes {
79 &self.memo
80 }
81
82 pub fn transfer_type(&self) -> TransferType {
85 self.transfer_type
86 }
87}
88
89impl<A> DecryptedOutput<sapling::Note, A> {
90 pub fn note_value(&self) -> Zatoshis {
91 Zatoshis::from_u64(self.note.value().inner())
92 .expect("Sapling note value is expected to have been validated by consensus.")
93 }
94}
95
96#[cfg(feature = "orchard")]
97impl<A> DecryptedOutput<orchard::note::Note, A> {
98 pub fn note_value(&self) -> Zatoshis {
99 Zatoshis::from_u64(self.note.value().inner())
100 .expect("Orchard note value is expected to have been validated by consensus.")
101 }
102}
103
104pub fn decrypt_transaction<'a, P: consensus::Parameters, AccountId: Copy>(
118 params: &P,
119 mined_height: Option<BlockHeight>,
120 chain_tip_height: Option<BlockHeight>,
121 tx: &'a Transaction,
122 ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
123) -> DecryptedTransaction<'a, AccountId> {
124 let zip212_enforcement = zip212_enforcement(
125 params,
126 mined_height.unwrap_or_else(|| {
130 chain_tip_height
131 .map(|max_height| max_height + 1) .or_else(|| params.activation_height(NetworkUpgrade::Sapling))
133 .expect("Sapling activation height must be known.")
134 }),
135 );
136 let sapling_bundle = tx.sapling_bundle();
137 let sapling_outputs = sapling_bundle
138 .iter()
139 .flat_map(|bundle| {
140 ufvks
141 .iter()
142 .flat_map(|(account, ufvk)| ufvk.sapling().into_iter().map(|dfvk| (*account, dfvk)))
143 .flat_map(|(account, dfvk)| {
144 let sapling_domain = SaplingDomain::new(zip212_enforcement);
145 let ivk_external =
146 PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::External));
147 let ivk_internal =
148 PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal));
149 let ovk = dfvk.fvk().ovk;
150
151 bundle
152 .shielded_outputs()
153 .iter()
154 .enumerate()
155 .flat_map(move |(index, output)| {
156 try_note_decryption(&sapling_domain, &ivk_external, output)
157 .map(|ret| (ret, TransferType::Incoming))
158 .or_else(|| {
159 try_note_decryption(&sapling_domain, &ivk_internal, output)
160 .map(|ret| (ret, TransferType::WalletInternal))
161 })
162 .or_else(|| {
163 try_output_recovery_with_ovk(
164 &sapling_domain,
165 &ovk,
166 output,
167 output.cv(),
168 output.out_ciphertext(),
169 )
170 .map(|ret| (ret, TransferType::Outgoing))
171 })
172 .into_iter()
173 .map(move |((note, _, memo), transfer_type)| {
174 DecryptedOutput::new(
175 index,
176 note,
177 account,
178 MemoBytes::from_bytes(&memo).expect("correct length"),
179 transfer_type,
180 )
181 })
182 })
183 })
184 })
185 .collect();
186
187 #[cfg(feature = "orchard")]
188 let orchard_bundle = tx.orchard_bundle();
189 #[cfg(feature = "orchard")]
190 let orchard_outputs = orchard_bundle
191 .iter()
192 .flat_map(|bundle| {
193 ufvks
194 .iter()
195 .flat_map(|(account, ufvk)| ufvk.orchard().into_iter().map(|fvk| (*account, fvk)))
196 .flat_map(|(account, fvk)| {
197 let ivk_external = orchard::keys::PreparedIncomingViewingKey::new(
198 &fvk.to_ivk(Scope::External),
199 );
200 let ivk_internal = orchard::keys::PreparedIncomingViewingKey::new(
201 &fvk.to_ivk(Scope::Internal),
202 );
203 let ovk = fvk.to_ovk(Scope::External);
204
205 bundle
206 .actions()
207 .iter()
208 .enumerate()
209 .flat_map(move |(index, action)| {
210 let domain = OrchardDomain::for_action(action);
211 try_note_decryption(&domain, &ivk_external, action)
212 .map(|ret| (ret, TransferType::Incoming))
213 .or_else(|| {
214 try_note_decryption(&domain, &ivk_internal, action)
215 .map(|ret| (ret, TransferType::WalletInternal))
216 })
217 .or_else(|| {
218 try_output_recovery_with_ovk(
219 &domain,
220 &ovk,
221 action,
222 action.cv_net(),
223 &action.encrypted_note().out_ciphertext,
224 )
225 .map(|ret| (ret, TransferType::Outgoing))
226 })
227 .into_iter()
228 .map(move |((note, _, memo), transfer_type)| {
229 DecryptedOutput::new(
230 index,
231 note,
232 account,
233 MemoBytes::from_bytes(&memo).expect("correct length"),
234 transfer_type,
235 )
236 })
237 })
238 })
239 })
240 .collect();
241
242 DecryptedTransaction::new(
243 mined_height,
244 tx,
245 sapling_outputs,
246 #[cfg(feature = "orchard")]
247 orchard_outputs,
248 )
249}