1#![cfg_attr(
24 feature = "transparent-inputs",
25 doc = "
26Another important high-level operation provided by this module is [`propose_shielding`], which
27takes a set of transparent source addresses, and constructs a [`Proposal`] to send those funds
28to a wallet-internal shielded address, as described in [ZIP 316](https://zips.z.cash/zip-0316).
29
30[`propose_shielding`]: crate::data_api::wallet::propose_shielding
31"
32)]
33use nonempty::NonEmpty;
37use rand_core::OsRng;
38use std::num::NonZeroU32;
39
40use shardtree::error::{QueryError, ShardTreeError};
41
42use super::InputSource;
43use crate::{
44 data_api::{
45 error::Error, Account, SentTransaction, SentTransactionOutput, WalletCommitmentTrees,
46 WalletRead, WalletWrite,
47 },
48 decrypt_transaction,
49 fees::{
50 standard::SingleOutputChangeStrategy, ChangeStrategy, DustOutputPolicy, StandardFeeRule,
51 },
52 proposal::{Proposal, ProposalError, Step, StepOutputIndex},
53 wallet::{Note, OvkPolicy, Recipient},
54};
55use ::sapling::{
56 note_encryption::{try_sapling_note_decryption, PreparedIncomingViewingKey},
57 prover::{OutputProver, SpendProver},
58};
59use ::transparent::{
60 address::TransparentAddress, builder::TransparentSigningSet, bundle::OutPoint,
61};
62use zcash_address::ZcashAddress;
63use zcash_keys::{
64 address::Address,
65 keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
66};
67use zcash_primitives::transaction::{
68 builder::{BuildConfig, BuildResult, Builder},
69 components::sapling::zip212_enforcement,
70 fees::FeeRule,
71 Transaction, TxId,
72};
73use zcash_protocol::{
74 consensus::{self, BlockHeight},
75 memo::MemoBytes,
76 value::Zatoshis,
77 PoolType, ShieldedProtocol,
78};
79use zip32::Scope;
80use zip321::Payment;
81
82#[cfg(feature = "transparent-inputs")]
83use {
84 crate::{fees::ChangeValue, proposal::StepOutput, wallet::TransparentAddressMetadata},
85 ::transparent::bundle::TxOut,
86 core::convert::Infallible,
87 input_selection::ShieldingSelector,
88 std::collections::HashMap,
89 zcash_keys::encoding::AddressCodec,
90};
91
92#[cfg(feature = "pczt")]
93use {
94 crate::data_api::error::PcztError,
95 ::transparent::pczt::Bip32Derivation,
96 bip32::ChildNumber,
97 orchard::note_encryption::OrchardDomain,
98 pczt::roles::{
99 creator::Creator, io_finalizer::IoFinalizer, spend_finalizer::SpendFinalizer,
100 tx_extractor::TransactionExtractor, updater::Updater,
101 },
102 sapling::note_encryption::SaplingDomain,
103 serde::{Deserialize, Serialize},
104 zcash_note_encryption::try_output_recovery_with_pkd_esk,
105 zcash_protocol::{
106 consensus::NetworkConstants,
107 value::{BalanceError, ZatBalance},
108 },
109};
110
111pub mod input_selection;
112use input_selection::{GreedyInputSelector, InputSelector, InputSelectorError};
113
114#[cfg(feature = "pczt")]
115const PROPRIETARY_PROPOSAL_INFO: &str = "zcash_client_backend:proposal_info";
116#[cfg(feature = "pczt")]
117const PROPRIETARY_OUTPUT_INFO: &str = "zcash_client_backend:output_info";
118
119#[cfg(feature = "pczt")]
123#[derive(Serialize, Deserialize)]
124struct ProposalInfo<AccountId> {
125 from_account: AccountId,
126 target_height: u32,
127}
128
129#[cfg(feature = "pczt")]
133#[derive(Serialize, Deserialize)]
134enum PcztRecipient<AccountId> {
135 External,
136 #[cfg(feature = "transparent-inputs")]
137 EphemeralTransparent {
138 receiving_account: AccountId,
139 },
140 InternalAccount {
141 receiving_account: AccountId,
142 },
143}
144
145#[cfg(feature = "pczt")]
146impl<AccountId: Copy> PcztRecipient<AccountId> {
147 fn from_recipient(recipient: BuildRecipient<AccountId>) -> (Self, Option<ZcashAddress>) {
148 match recipient {
149 BuildRecipient::External {
150 recipient_address, ..
151 } => (PcztRecipient::External, Some(recipient_address)),
152 #[cfg(feature = "transparent-inputs")]
153 BuildRecipient::EphemeralTransparent {
154 receiving_account, ..
155 } => (
156 PcztRecipient::EphemeralTransparent { receiving_account },
157 None,
158 ),
159 BuildRecipient::InternalAccount {
160 receiving_account,
161 external_address,
162 } => (
163 PcztRecipient::InternalAccount { receiving_account },
164 external_address,
165 ),
166 }
167 }
168}
169
170pub fn decrypt_and_store_transaction<ParamsT, DbT>(
173 params: &ParamsT,
174 data: &mut DbT,
175 tx: &Transaction,
176 mined_height: Option<BlockHeight>,
177) -> Result<(), DbT::Error>
178where
179 ParamsT: consensus::Parameters,
180 DbT: WalletWrite,
181{
182 let ufvks = data.get_unified_full_viewing_keys()?;
184
185 data.store_decrypted_tx(decrypt_transaction(
186 params,
187 mined_height.map_or_else(|| data.get_tx_height(tx.txid()), |h| Ok(Some(h)))?,
188 data.chain_height()?,
189 tx,
190 &ufvks,
191 ))?;
192
193 Ok(())
194}
195
196pub type ProposeTransferErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT> = Error<
199 <DbT as WalletRead>::Error,
200 CommitmentTreeErrT,
201 <InputsT as InputSelector>::Error,
202 <<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
203 <ChangeT as ChangeStrategy>::Error,
204 <<InputsT as InputSelector>::InputSource as InputSource>::NoteRef,
205>;
206
207#[cfg(feature = "transparent-inputs")]
210pub type ProposeShieldingErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT> = Error<
211 <DbT as WalletRead>::Error,
212 CommitmentTreeErrT,
213 <InputsT as ShieldingSelector>::Error,
214 <<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
215 <ChangeT as ChangeStrategy>::Error,
216 Infallible,
217>;
218
219pub type CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N> = Error<
221 <DbT as WalletRead>::Error,
222 <DbT as WalletCommitmentTrees>::Error,
223 InputsErrT,
224 <FeeRuleT as FeeRule>::Error,
225 ChangeErrT,
226 N,
227>;
228
229pub type TransferErrT<DbT, InputsT, ChangeT> = Error<
231 <DbT as WalletRead>::Error,
232 <DbT as WalletCommitmentTrees>::Error,
233 <InputsT as InputSelector>::Error,
234 <<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
235 <ChangeT as ChangeStrategy>::Error,
236 <<InputsT as InputSelector>::InputSource as InputSource>::NoteRef,
237>;
238
239#[cfg(feature = "transparent-inputs")]
241pub type ShieldErrT<DbT, InputsT, ChangeT> = Error<
242 <DbT as WalletRead>::Error,
243 <DbT as WalletCommitmentTrees>::Error,
244 <InputsT as ShieldingSelector>::Error,
245 <<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
246 <ChangeT as ChangeStrategy>::Error,
247 Infallible,
248>;
249
250#[cfg(feature = "pczt")]
252pub type ExtractErrT<DbT, N> = Error<
253 <DbT as WalletRead>::Error,
254 <DbT as WalletCommitmentTrees>::Error,
255 Infallible,
256 Infallible,
257 Infallible,
258 N,
259>;
260
261#[allow(clippy::too_many_arguments)]
265#[allow(clippy::type_complexity)]
266pub fn propose_transfer<DbT, ParamsT, InputsT, ChangeT, CommitmentTreeErrT>(
267 wallet_db: &mut DbT,
268 params: &ParamsT,
269 spend_from_account: <DbT as InputSource>::AccountId,
270 input_selector: &InputsT,
271 change_strategy: &ChangeT,
272 request: zip321::TransactionRequest,
273 min_confirmations: NonZeroU32,
274) -> Result<
275 Proposal<ChangeT::FeeRule, <DbT as InputSource>::NoteRef>,
276 ProposeTransferErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT>,
277>
278where
279 DbT: WalletRead + InputSource<Error = <DbT as WalletRead>::Error>,
280 <DbT as InputSource>::NoteRef: Copy + Eq + Ord,
281 ParamsT: consensus::Parameters + Clone,
282 InputsT: InputSelector<InputSource = DbT>,
283 ChangeT: ChangeStrategy<MetaSource = DbT>,
284{
285 let (target_height, anchor_height) = wallet_db
286 .get_target_and_anchor_heights(min_confirmations)
287 .map_err(|e| Error::from(InputSelectorError::DataSource(e)))?
288 .ok_or_else(|| Error::from(InputSelectorError::SyncRequired))?;
289
290 input_selector
291 .propose_transaction(
292 params,
293 wallet_db,
294 target_height,
295 anchor_height,
296 spend_from_account,
297 request,
298 change_strategy,
299 )
300 .map_err(Error::from)
301}
302
303#[allow(clippy::too_many_arguments)]
329#[allow(clippy::type_complexity)]
330pub fn propose_standard_transfer_to_address<DbT, ParamsT, CommitmentTreeErrT>(
331 wallet_db: &mut DbT,
332 params: &ParamsT,
333 fee_rule: StandardFeeRule,
334 spend_from_account: <DbT as InputSource>::AccountId,
335 min_confirmations: NonZeroU32,
336 to: &Address,
337 amount: Zatoshis,
338 memo: Option<MemoBytes>,
339 change_memo: Option<MemoBytes>,
340 fallback_change_pool: ShieldedProtocol,
341) -> Result<
342 Proposal<StandardFeeRule, DbT::NoteRef>,
343 ProposeTransferErrT<
344 DbT,
345 CommitmentTreeErrT,
346 GreedyInputSelector<DbT>,
347 SingleOutputChangeStrategy<DbT>,
348 >,
349>
350where
351 ParamsT: consensus::Parameters + Clone,
352 DbT: InputSource,
353 DbT: WalletRead<
354 Error = <DbT as InputSource>::Error,
355 AccountId = <DbT as InputSource>::AccountId,
356 >,
357 DbT::NoteRef: Copy + Eq + Ord,
358{
359 let request = zip321::TransactionRequest::new(vec![Payment::new(
360 to.to_zcash_address(params),
361 amount,
362 memo,
363 None,
364 None,
365 vec![],
366 )
367 .ok_or(Error::MemoForbidden)?])
368 .expect(
369 "It should not be possible for this to violate ZIP 321 request construction invariants.",
370 );
371
372 let input_selector = GreedyInputSelector::<DbT>::new();
373 let change_strategy = SingleOutputChangeStrategy::<DbT>::new(
374 fee_rule,
375 change_memo,
376 fallback_change_pool,
377 DustOutputPolicy::default(),
378 );
379
380 propose_transfer(
381 wallet_db,
382 params,
383 spend_from_account,
384 &input_selector,
385 &change_strategy,
386 request,
387 min_confirmations,
388 )
389}
390
391#[cfg(feature = "transparent-inputs")]
394#[allow(clippy::too_many_arguments)]
395#[allow(clippy::type_complexity)]
396pub fn propose_shielding<DbT, ParamsT, InputsT, ChangeT, CommitmentTreeErrT>(
397 wallet_db: &mut DbT,
398 params: &ParamsT,
399 input_selector: &InputsT,
400 change_strategy: &ChangeT,
401 shielding_threshold: Zatoshis,
402 from_addrs: &[TransparentAddress],
403 to_account: <DbT as InputSource>::AccountId,
404 min_confirmations: u32,
405) -> Result<
406 Proposal<ChangeT::FeeRule, Infallible>,
407 ProposeShieldingErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT>,
408>
409where
410 ParamsT: consensus::Parameters,
411 DbT: WalletRead + InputSource<Error = <DbT as WalletRead>::Error>,
412 InputsT: ShieldingSelector<InputSource = DbT>,
413 ChangeT: ChangeStrategy<MetaSource = DbT>,
414{
415 let chain_tip_height = wallet_db
416 .chain_height()
417 .map_err(|e| Error::from(InputSelectorError::DataSource(e)))?
418 .ok_or_else(|| Error::from(InputSelectorError::SyncRequired))?;
419
420 input_selector
421 .propose_shielding(
422 params,
423 wallet_db,
424 change_strategy,
425 shielding_threshold,
426 from_addrs,
427 to_account,
428 chain_tip_height + 1,
429 min_confirmations,
430 )
431 .map_err(Error::from)
432}
433
434struct StepResult<AccountId> {
435 build_result: BuildResult,
436 outputs: Vec<SentTransactionOutput<AccountId>>,
437 fee_amount: Zatoshis,
438 #[cfg(feature = "transparent-inputs")]
439 utxos_spent: Vec<OutPoint>,
440}
441
442#[allow(clippy::too_many_arguments)]
454#[allow(clippy::type_complexity)]
455pub fn create_proposed_transactions<DbT, ParamsT, InputsErrT, FeeRuleT, ChangeErrT, N>(
456 wallet_db: &mut DbT,
457 params: &ParamsT,
458 spend_prover: &impl SpendProver,
459 output_prover: &impl OutputProver,
460 usk: &UnifiedSpendingKey,
461 ovk_policy: OvkPolicy,
462 proposal: &Proposal<FeeRuleT, N>,
463) -> Result<NonEmpty<TxId>, CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>>
464where
465 DbT: WalletWrite + WalletCommitmentTrees,
466 ParamsT: consensus::Parameters + Clone,
467 FeeRuleT: FeeRule,
468{
469 #[cfg(feature = "transparent-inputs")]
473 let mut unused_transparent_outputs = HashMap::new();
474
475 let account_id = wallet_db
476 .get_account_for_ufvk(&usk.to_unified_full_viewing_key())
477 .map_err(Error::DataSource)?
478 .ok_or(Error::KeyNotRecognized)?
479 .id();
480
481 let mut step_results = Vec::with_capacity(proposal.steps().len());
482 for step in proposal.steps() {
483 let step_result: StepResult<_> = create_proposed_transaction(
484 wallet_db,
485 params,
486 spend_prover,
487 output_prover,
488 usk,
489 account_id,
490 ovk_policy.clone(),
491 proposal.fee_rule(),
492 proposal.min_target_height(),
493 &step_results,
494 step,
495 #[cfg(feature = "transparent-inputs")]
496 &mut unused_transparent_outputs,
497 )?;
498 step_results.push((step, step_result));
499 }
500
501 #[cfg(feature = "transparent-inputs")]
503 for so in unused_transparent_outputs.into_keys() {
504 if let StepOutputIndex::Change(i) = so.output_index() {
505 if step_results[so.step_index()].0.balance().proposed_change()[i].is_ephemeral() {
507 return Err(ProposalError::EphemeralOutputLeftUnspent(so).into());
508 }
509 }
510 }
511
512 let created = time::OffsetDateTime::now_utc();
513
514 let mut transactions = Vec::with_capacity(step_results.len());
518 let mut txids = Vec::with_capacity(step_results.len());
519 #[allow(unused_variables)]
520 for (_, step_result) in step_results.iter() {
521 let tx = step_result.build_result.transaction();
522 transactions.push(SentTransaction::new(
523 tx,
524 created,
525 proposal.min_target_height(),
526 account_id,
527 &step_result.outputs,
528 step_result.fee_amount,
529 #[cfg(feature = "transparent-inputs")]
530 &step_result.utxos_spent,
531 ));
532 txids.push(tx.txid());
533 }
534
535 wallet_db
536 .store_transactions_to_be_sent(&transactions)
537 .map_err(Error::DataSource)?;
538
539 Ok(NonEmpty::from_vec(txids).expect("proposal.steps is NonEmpty"))
540}
541
542#[derive(Debug, Clone)]
543enum BuildRecipient<AccountId> {
544 External {
545 recipient_address: ZcashAddress,
546 output_pool: PoolType,
547 },
548 #[cfg(feature = "transparent-inputs")]
549 EphemeralTransparent {
550 receiving_account: AccountId,
551 ephemeral_address: TransparentAddress,
552 },
553 InternalAccount {
554 receiving_account: AccountId,
555 external_address: Option<ZcashAddress>,
556 },
557}
558
559impl<AccountId> BuildRecipient<AccountId> {
560 fn into_recipient_with_note(self, note: impl FnOnce() -> Note) -> Recipient<AccountId> {
561 match self {
562 BuildRecipient::External {
563 recipient_address,
564 output_pool,
565 } => Recipient::External {
566 recipient_address,
567 output_pool,
568 },
569 #[cfg(feature = "transparent-inputs")]
570 BuildRecipient::EphemeralTransparent { .. } => unreachable!(),
571 BuildRecipient::InternalAccount {
572 receiving_account,
573 external_address,
574 } => Recipient::InternalAccount {
575 receiving_account,
576 external_address,
577 note: Box::new(note()),
578 },
579 }
580 }
581
582 fn into_recipient_with_outpoint(
583 self,
584 #[cfg(feature = "transparent-inputs")] outpoint: OutPoint,
585 ) -> Recipient<AccountId> {
586 match self {
587 BuildRecipient::External {
588 recipient_address,
589 output_pool,
590 } => Recipient::External {
591 recipient_address,
592 output_pool,
593 },
594 #[cfg(feature = "transparent-inputs")]
595 BuildRecipient::EphemeralTransparent {
596 receiving_account,
597 ephemeral_address,
598 } => Recipient::EphemeralTransparent {
599 receiving_account,
600 ephemeral_address,
601 outpoint,
602 },
603 BuildRecipient::InternalAccount { .. } => unreachable!(),
604 }
605 }
606}
607
608#[allow(clippy::type_complexity)]
609struct BuildState<'a, P, AccountId> {
610 #[cfg(feature = "transparent-inputs")]
611 step_index: usize,
612 builder: Builder<'a, P, ()>,
613 #[cfg(feature = "transparent-inputs")]
614 transparent_input_addresses: HashMap<TransparentAddress, TransparentAddressMetadata>,
615 #[cfg(feature = "orchard")]
616 orchard_output_meta: Vec<(BuildRecipient<AccountId>, Zatoshis, Option<MemoBytes>)>,
617 sapling_output_meta: Vec<(BuildRecipient<AccountId>, Zatoshis, Option<MemoBytes>)>,
618 transparent_output_meta: Vec<(
619 BuildRecipient<AccountId>,
620 TransparentAddress,
621 Zatoshis,
622 StepOutputIndex,
623 )>,
624 #[cfg(feature = "transparent-inputs")]
625 utxos_spent: Vec<OutPoint>,
626}
627
628#[allow(clippy::too_many_arguments)]
632#[allow(clippy::type_complexity)]
633fn build_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, ChangeErrT, N>(
634 wallet_db: &mut DbT,
635 params: &ParamsT,
636 ufvk: &UnifiedFullViewingKey,
637 account_id: <DbT as WalletRead>::AccountId,
638 ovk_policy: OvkPolicy,
639 min_target_height: BlockHeight,
640 prior_step_results: &[(&Step<N>, StepResult<<DbT as WalletRead>::AccountId>)],
641 proposal_step: &Step<N>,
642 #[cfg(feature = "transparent-inputs")] unused_transparent_outputs: &mut HashMap<
643 StepOutput,
644 (TransparentAddress, OutPoint),
645 >,
646) -> Result<
647 BuildState<'static, ParamsT, DbT::AccountId>,
648 CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>,
649>
650where
651 DbT: WalletWrite + WalletCommitmentTrees,
652 ParamsT: consensus::Parameters + Clone,
653 FeeRuleT: FeeRule,
654{
655 #[cfg(feature = "transparent-inputs")]
656 let step_index = prior_step_results.len();
657
658 #[allow(clippy::never_loop)]
665 for input_ref in proposal_step.prior_step_inputs() {
666 let (prior_step, _) = prior_step_results
667 .get(input_ref.step_index())
668 .ok_or(ProposalError::ReferenceError(*input_ref))?;
669
670 #[allow(unused_variables)]
671 let output_pool = match input_ref.output_index() {
672 StepOutputIndex::Payment(i) => prior_step.payment_pools().get(&i).cloned(),
673 StepOutputIndex::Change(i) => match prior_step.balance().proposed_change().get(i) {
674 Some(change) if !change.is_ephemeral() => {
675 return Err(ProposalError::SpendsChange(*input_ref).into());
676 }
677 other => other.map(|change| change.output_pool()),
678 },
679 }
680 .ok_or(ProposalError::ReferenceError(*input_ref))?;
681
682 #[cfg(feature = "transparent-inputs")]
684 if output_pool != PoolType::TRANSPARENT {
685 return Err(Error::ProposalNotSupported);
686 }
687 #[cfg(not(feature = "transparent-inputs"))]
688 return Err(Error::ProposalNotSupported);
689 }
690
691 let (sapling_anchor, sapling_inputs) = if proposal_step
692 .involves(PoolType::Shielded(ShieldedProtocol::Sapling))
693 {
694 proposal_step.shielded_inputs().map_or_else(
695 || Ok((Some(sapling::Anchor::empty_tree()), vec![])),
696 |inputs| {
697 wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _, _, _>>(|sapling_tree| {
698 let anchor = sapling_tree
699 .root_at_checkpoint_id(&inputs.anchor_height())?
700 .ok_or(ProposalError::AnchorNotFound(inputs.anchor_height()))?
701 .into();
702
703 let sapling_inputs = inputs
704 .notes()
705 .iter()
706 .filter_map(|selected| match selected.note() {
707 Note::Sapling(note) => sapling_tree
708 .witness_at_checkpoint_id_caching(
709 selected.note_commitment_tree_position(),
710 &inputs.anchor_height(),
711 )
712 .and_then(|witness| {
713 witness
714 .ok_or(ShardTreeError::Query(QueryError::CheckpointPruned))
715 })
716 .map(|merkle_path| {
717 Some((selected.spending_key_scope(), note, merkle_path))
718 })
719 .map_err(Error::from)
720 .transpose(),
721 #[cfg(feature = "orchard")]
722 Note::Orchard(_) => None,
723 })
724 .collect::<Result<Vec<_>, Error<_, _, _, _, _, _>>>()?;
725
726 Ok((Some(anchor), sapling_inputs))
727 })
728 },
729 )?
730 } else {
731 (None, vec![])
732 };
733
734 #[cfg(feature = "orchard")]
735 let (orchard_anchor, orchard_inputs) = if proposal_step
736 .involves(PoolType::Shielded(ShieldedProtocol::Orchard))
737 {
738 proposal_step.shielded_inputs().map_or_else(
739 || Ok((Some(orchard::Anchor::empty_tree()), vec![])),
740 |inputs| {
741 wallet_db.with_orchard_tree_mut::<_, _, Error<_, _, _, _, _, _>>(|orchard_tree| {
742 let anchor = orchard_tree
743 .root_at_checkpoint_id(&inputs.anchor_height())?
744 .ok_or(ProposalError::AnchorNotFound(inputs.anchor_height()))?
745 .into();
746
747 let orchard_inputs = inputs
748 .notes()
749 .iter()
750 .filter_map(|selected| match selected.note() {
751 #[cfg(feature = "orchard")]
752 Note::Orchard(note) => orchard_tree
753 .witness_at_checkpoint_id_caching(
754 selected.note_commitment_tree_position(),
755 &inputs.anchor_height(),
756 )
757 .and_then(|witness| {
758 witness
759 .ok_or(ShardTreeError::Query(QueryError::CheckpointPruned))
760 })
761 .map(|merkle_path| Some((note, merkle_path)))
762 .map_err(Error::from)
763 .transpose(),
764 Note::Sapling(_) => None,
765 })
766 .collect::<Result<Vec<_>, Error<_, _, _, _, _, _>>>()?;
767
768 Ok((Some(anchor), orchard_inputs))
769 })
770 },
771 )?
772 } else {
773 (None, vec![])
774 };
775 #[cfg(not(feature = "orchard"))]
776 let orchard_anchor = None;
777
778 let mut builder = Builder::new(
781 params.clone(),
782 min_target_height,
783 BuildConfig::Standard {
784 sapling_anchor,
785 orchard_anchor,
786 },
787 );
788
789 #[cfg(all(feature = "transparent-inputs", not(feature = "orchard")))]
790 let has_shielded_inputs = !sapling_inputs.is_empty();
791 #[cfg(all(feature = "transparent-inputs", feature = "orchard"))]
792 let has_shielded_inputs = !(sapling_inputs.is_empty() && orchard_inputs.is_empty());
793
794 for (_sapling_key_scope, sapling_note, merkle_path) in sapling_inputs.into_iter() {
795 let key = match _sapling_key_scope {
796 Scope::External => ufvk.sapling().map(|k| k.fvk().clone()),
797 Scope::Internal => ufvk.sapling().map(|k| k.to_internal_fvk()),
798 };
799
800 builder.add_sapling_spend(
801 key.ok_or(Error::KeyNotAvailable(PoolType::SAPLING))?,
802 sapling_note.clone(),
803 merkle_path,
804 )?;
805 }
806
807 #[cfg(feature = "orchard")]
808 for (orchard_note, merkle_path) in orchard_inputs.into_iter() {
809 builder.add_orchard_spend(
810 ufvk.orchard()
811 .cloned()
812 .ok_or(Error::KeyNotAvailable(PoolType::ORCHARD))?,
813 *orchard_note,
814 merkle_path.into(),
815 )?;
816 }
817
818 #[cfg(feature = "transparent-inputs")]
819 let mut cache = HashMap::<TransparentAddress, TransparentAddressMetadata>::new();
820
821 #[cfg(feature = "transparent-inputs")]
822 let mut metadata_from_address = |addr: TransparentAddress| -> Result<
823 TransparentAddressMetadata,
824 CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>,
825 > {
826 match cache.get(&addr) {
827 Some(result) => Ok(result.clone()),
828 None => {
829 let result = wallet_db
838 .get_transparent_address_metadata(account_id, &addr)
839 .map_err(InputSelectorError::DataSource)?
840 .ok_or(Error::AddressNotRecognized(addr))?;
841 cache.insert(addr, result.clone());
842 Ok(result)
843 }
844 }
845 };
846
847 #[cfg(feature = "transparent-inputs")]
848 let utxos_spent = {
849 let mut utxos_spent: Vec<OutPoint> = vec![];
850 let add_transparent_input = |builder: &mut Builder<_, _>,
851 utxos_spent: &mut Vec<_>,
852 address_metadata: &TransparentAddressMetadata,
853 outpoint: OutPoint,
854 txout: TxOut|
855 -> Result<
856 (),
857 CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>,
858 > {
859 let pubkey = ufvk
860 .transparent()
861 .ok_or(Error::KeyNotAvailable(PoolType::Transparent))?
862 .derive_address_pubkey(address_metadata.scope(), address_metadata.address_index())
863 .expect("spending key derivation should not fail");
864
865 utxos_spent.push(outpoint.clone());
866 builder.add_transparent_input(pubkey, outpoint, txout)?;
867
868 Ok(())
869 };
870
871 for utxo in proposal_step.transparent_inputs() {
872 add_transparent_input(
873 &mut builder,
874 &mut utxos_spent,
875 &metadata_from_address(*utxo.recipient_address())?,
876 utxo.outpoint().clone(),
877 utxo.txout().clone(),
878 )?;
879 }
880 for input_ref in proposal_step.prior_step_inputs() {
881 let (address, outpoint) = unused_transparent_outputs
884 .remove(input_ref)
885 .ok_or(Error::Proposal(ProposalError::ReferenceError(*input_ref)))?;
886
887 let address_metadata = metadata_from_address(address)?;
888
889 let txout = &prior_step_results[input_ref.step_index()]
890 .1
891 .build_result
892 .transaction()
893 .transparent_bundle()
894 .ok_or(ProposalError::ReferenceError(*input_ref))?
895 .vout[outpoint.n() as usize];
896
897 add_transparent_input(
898 &mut builder,
899 &mut utxos_spent,
900 &address_metadata,
901 outpoint,
902 txout.clone(),
903 )?;
904 }
905 utxos_spent
906 };
907
908 #[cfg(feature = "orchard")]
909 let orchard_external_ovk = match &ovk_policy {
910 OvkPolicy::Sender => ufvk
911 .orchard()
912 .map(|fvk| fvk.to_ovk(orchard::keys::Scope::External)),
913 OvkPolicy::Custom { orchard, .. } => Some(orchard.clone()),
914 OvkPolicy::Discard => None,
915 };
916
917 #[cfg(feature = "orchard")]
918 let orchard_internal_ovk = || {
919 #[cfg(feature = "transparent-inputs")]
920 if proposal_step.is_shielding() {
921 return ufvk
922 .transparent()
923 .map(|k| orchard::keys::OutgoingViewingKey::from(k.internal_ovk().as_bytes()));
924 }
925
926 ufvk.orchard().map(|k| k.to_ovk(Scope::Internal))
927 };
928
929 let sapling_external_ovk = match &ovk_policy {
931 OvkPolicy::Sender => ufvk.sapling().map(|k| k.to_ovk(Scope::External)),
932 OvkPolicy::Custom { sapling, .. } => Some(*sapling),
933 OvkPolicy::Discard => None,
934 };
935
936 let sapling_internal_ovk = || {
937 #[cfg(feature = "transparent-inputs")]
938 if proposal_step.is_shielding() {
939 return ufvk
940 .transparent()
941 .map(|k| sapling::keys::OutgoingViewingKey(k.internal_ovk().as_bytes()));
942 }
943
944 ufvk.sapling().map(|k| k.to_ovk(Scope::Internal))
945 };
946
947 #[cfg(feature = "orchard")]
948 let mut orchard_output_meta: Vec<(BuildRecipient<_>, Zatoshis, Option<MemoBytes>)> = vec![];
949 let mut sapling_output_meta: Vec<(BuildRecipient<_>, Zatoshis, Option<MemoBytes>)> = vec![];
950 let mut transparent_output_meta: Vec<(
951 BuildRecipient<_>,
952 TransparentAddress,
953 Zatoshis,
954 StepOutputIndex,
955 )> = vec![];
956
957 for (&payment_index, output_pool) in proposal_step.payment_pools() {
958 let payment = proposal_step
959 .transaction_request()
960 .payments()
961 .get(&payment_index)
962 .expect(
963 "The mapping between payment index and payment is checked in step construction",
964 );
965 let recipient_address = payment.recipient_address();
966
967 let add_sapling_output = |builder: &mut Builder<_, _>,
968 sapling_output_meta: &mut Vec<_>,
969 to: sapling::PaymentAddress|
970 -> Result<
971 (),
972 CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>,
973 > {
974 let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
975 builder.add_sapling_output(sapling_external_ovk, to, payment.amount(), memo.clone())?;
976 sapling_output_meta.push((
977 BuildRecipient::External {
978 recipient_address: recipient_address.clone(),
979 output_pool: PoolType::SAPLING,
980 },
981 payment.amount(),
982 Some(memo),
983 ));
984 Ok(())
985 };
986
987 #[cfg(feature = "orchard")]
988 let add_orchard_output =
989 |builder: &mut Builder<_, _>,
990 orchard_output_meta: &mut Vec<_>,
991 to: orchard::Address|
992 -> Result<(), CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>> {
993 let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
994 builder.add_orchard_output(
995 orchard_external_ovk.clone(),
996 to,
997 payment.amount().into(),
998 memo.clone(),
999 )?;
1000 orchard_output_meta.push((
1001 BuildRecipient::External {
1002 recipient_address: recipient_address.clone(),
1003 output_pool: PoolType::ORCHARD,
1004 },
1005 payment.amount(),
1006 Some(memo),
1007 ));
1008 Ok(())
1009 };
1010
1011 let add_transparent_output =
1012 |builder: &mut Builder<_, _>,
1013 transparent_output_meta: &mut Vec<_>,
1014 to: TransparentAddress|
1015 -> Result<(), CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>> {
1016 #[cfg(feature = "transparent-inputs")]
1018 if wallet_db
1019 .find_account_for_ephemeral_address(&to)
1020 .map_err(Error::DataSource)?
1021 .is_some()
1022 {
1023 return Err(Error::PaysEphemeralTransparentAddress(to.encode(params)));
1024 }
1025 if payment.memo().is_some() {
1026 return Err(Error::MemoForbidden);
1027 }
1028 builder.add_transparent_output(&to, payment.amount())?;
1029 transparent_output_meta.push((
1030 BuildRecipient::External {
1031 recipient_address: recipient_address.clone(),
1032 output_pool: PoolType::TRANSPARENT,
1033 },
1034 to,
1035 payment.amount(),
1036 StepOutputIndex::Payment(payment_index),
1037 ));
1038 Ok(())
1039 };
1040
1041 match recipient_address
1042 .clone()
1043 .convert_if_network(params.network_type())?
1044 {
1045 Address::Unified(ua) => match output_pool {
1046 #[cfg(not(feature = "orchard"))]
1047 PoolType::Shielded(ShieldedProtocol::Orchard) => {
1048 return Err(Error::ProposalNotSupported);
1049 }
1050 #[cfg(feature = "orchard")]
1051 PoolType::Shielded(ShieldedProtocol::Orchard) => {
1052 let to = *ua.orchard().expect("The mapping between payment pool and receiver is checked in step construction");
1053 add_orchard_output(&mut builder, &mut orchard_output_meta, to)?;
1054 }
1055 PoolType::Shielded(ShieldedProtocol::Sapling) => {
1056 let to = *ua.sapling().expect("The mapping between payment pool and receiver is checked in step construction");
1057 add_sapling_output(&mut builder, &mut sapling_output_meta, to)?;
1058 }
1059 PoolType::Transparent => {
1060 let to = *ua.transparent().expect("The mapping between payment pool and receiver is checked in step construction");
1061 add_transparent_output(&mut builder, &mut transparent_output_meta, to)?;
1062 }
1063 },
1064 Address::Sapling(to) => {
1065 add_sapling_output(&mut builder, &mut sapling_output_meta, to)?;
1066 }
1067 Address::Transparent(to) => {
1068 add_transparent_output(&mut builder, &mut transparent_output_meta, to)?;
1069 }
1070 #[cfg(not(feature = "transparent-inputs"))]
1071 Address::Tex(_) => {
1072 return Err(Error::ProposalNotSupported);
1073 }
1074 #[cfg(feature = "transparent-inputs")]
1075 Address::Tex(data) => {
1076 if has_shielded_inputs {
1077 return Err(ProposalError::PaysTexFromShielded.into());
1078 }
1079 let to = TransparentAddress::PublicKeyHash(data);
1080 add_transparent_output(&mut builder, &mut transparent_output_meta, to)?;
1081 }
1082 }
1083 }
1084
1085 for change_value in proposal_step.balance().proposed_change() {
1086 let memo = change_value
1087 .memo()
1088 .map_or_else(MemoBytes::empty, |m| m.clone());
1089 let output_pool = change_value.output_pool();
1090 match output_pool {
1091 PoolType::Shielded(ShieldedProtocol::Sapling) => {
1092 builder.add_sapling_output(
1093 sapling_internal_ovk(),
1094 ufvk.sapling()
1095 .ok_or(Error::KeyNotAvailable(PoolType::SAPLING))?
1096 .change_address()
1097 .1,
1098 change_value.value(),
1099 memo.clone(),
1100 )?;
1101 sapling_output_meta.push((
1102 BuildRecipient::InternalAccount {
1103 receiving_account: account_id,
1104 external_address: None,
1105 },
1106 change_value.value(),
1107 Some(memo),
1108 ))
1109 }
1110 PoolType::Shielded(ShieldedProtocol::Orchard) => {
1111 #[cfg(not(feature = "orchard"))]
1112 return Err(Error::UnsupportedChangeType(output_pool));
1113
1114 #[cfg(feature = "orchard")]
1115 {
1116 builder.add_orchard_output(
1117 orchard_internal_ovk(),
1118 ufvk.orchard()
1119 .ok_or(Error::KeyNotAvailable(PoolType::ORCHARD))?
1120 .address_at(0u32, orchard::keys::Scope::Internal),
1121 change_value.value().into(),
1122 memo.clone(),
1123 )?;
1124 orchard_output_meta.push((
1125 BuildRecipient::InternalAccount {
1126 receiving_account: account_id,
1127 external_address: None,
1128 },
1129 change_value.value(),
1130 Some(memo),
1131 ))
1132 }
1133 }
1134 PoolType::Transparent => {
1135 #[cfg(not(feature = "transparent-inputs"))]
1136 return Err(Error::UnsupportedChangeType(output_pool));
1137 }
1138 }
1139 }
1140
1141 #[cfg(feature = "transparent-inputs")]
1145 {
1146 let ephemeral_outputs: Vec<(usize, &ChangeValue)> = proposal_step
1147 .balance()
1148 .proposed_change()
1149 .iter()
1150 .enumerate()
1151 .filter(|(_, change_value)| {
1152 change_value.is_ephemeral() && change_value.output_pool() == PoolType::Transparent
1153 })
1154 .collect();
1155
1156 let addresses_and_metadata = wallet_db
1157 .reserve_next_n_ephemeral_addresses(account_id, ephemeral_outputs.len())
1158 .map_err(Error::DataSource)?;
1159 assert_eq!(addresses_and_metadata.len(), ephemeral_outputs.len());
1160
1161 for ((change_index, change_value), (ephemeral_address, _)) in
1163 ephemeral_outputs.iter().zip(addresses_and_metadata)
1164 {
1165 builder.add_transparent_output(&ephemeral_address, change_value.value())?;
1168 transparent_output_meta.push((
1169 BuildRecipient::EphemeralTransparent {
1170 receiving_account: account_id,
1171 ephemeral_address,
1172 },
1173 ephemeral_address,
1174 change_value.value(),
1175 StepOutputIndex::Change(*change_index),
1176 ))
1177 }
1178 }
1179
1180 Ok(BuildState {
1181 #[cfg(feature = "transparent-inputs")]
1182 step_index,
1183 builder,
1184 #[cfg(feature = "transparent-inputs")]
1185 transparent_input_addresses: cache,
1186 #[cfg(feature = "orchard")]
1187 orchard_output_meta,
1188 sapling_output_meta,
1189 transparent_output_meta,
1190 #[cfg(feature = "transparent-inputs")]
1191 utxos_spent,
1192 })
1193}
1194
1195#[allow(clippy::too_many_arguments)]
1199#[allow(clippy::type_complexity)]
1200fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, ChangeErrT, N>(
1201 wallet_db: &mut DbT,
1202 params: &ParamsT,
1203 spend_prover: &impl SpendProver,
1204 output_prover: &impl OutputProver,
1205 usk: &UnifiedSpendingKey,
1206 account_id: <DbT as WalletRead>::AccountId,
1207 ovk_policy: OvkPolicy,
1208 fee_rule: &FeeRuleT,
1209 min_target_height: BlockHeight,
1210 prior_step_results: &[(&Step<N>, StepResult<<DbT as WalletRead>::AccountId>)],
1211 proposal_step: &Step<N>,
1212 #[cfg(feature = "transparent-inputs")] unused_transparent_outputs: &mut HashMap<
1213 StepOutput,
1214 (TransparentAddress, OutPoint),
1215 >,
1216) -> Result<
1217 StepResult<<DbT as WalletRead>::AccountId>,
1218 CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>,
1219>
1220where
1221 DbT: WalletWrite + WalletCommitmentTrees,
1222 ParamsT: consensus::Parameters + Clone,
1223 FeeRuleT: FeeRule,
1224{
1225 let build_state = build_proposed_transaction::<_, _, _, FeeRuleT, _, _>(
1226 wallet_db,
1227 params,
1228 &usk.to_unified_full_viewing_key(),
1229 account_id,
1230 ovk_policy,
1231 min_target_height,
1232 prior_step_results,
1233 proposal_step,
1234 #[cfg(feature = "transparent-inputs")]
1235 unused_transparent_outputs,
1236 )?;
1237
1238 #[cfg_attr(not(feature = "transparent-inputs"), allow(unused_mut))]
1240 let mut transparent_signing_set = TransparentSigningSet::new();
1241 #[cfg(feature = "transparent-inputs")]
1242 for (_, address_metadata) in build_state.transparent_input_addresses {
1243 transparent_signing_set.add_key(
1244 usk.transparent()
1245 .derive_secret_key(address_metadata.scope(), address_metadata.address_index())
1246 .expect("spending key derivation should not fail"),
1247 );
1248 }
1249 let sapling_extsks = &[usk.sapling().clone(), usk.sapling().derive_internal()];
1250 #[cfg(feature = "orchard")]
1251 let orchard_saks = &[usk.orchard().into()];
1252 #[cfg(not(feature = "orchard"))]
1253 let orchard_saks = &[];
1254 let build_result = build_state.builder.build(
1255 &transparent_signing_set,
1256 sapling_extsks,
1257 orchard_saks,
1258 OsRng,
1259 spend_prover,
1260 output_prover,
1261 fee_rule,
1262 )?;
1263
1264 #[cfg(feature = "orchard")]
1265 let orchard_fvk: orchard::keys::FullViewingKey = usk.orchard().into();
1266 #[cfg(feature = "orchard")]
1267 let orchard_internal_ivk = orchard_fvk.to_ivk(orchard::keys::Scope::Internal);
1268 #[cfg(feature = "orchard")]
1269 let orchard_outputs = build_state.orchard_output_meta.into_iter().enumerate().map(
1270 |(i, (recipient, value, memo))| {
1271 let output_index = build_result
1272 .orchard_meta()
1273 .output_action_index(i)
1274 .expect("An action should exist in the transaction for each Orchard output.");
1275
1276 let recipient = recipient.into_recipient_with_note(|| {
1277 build_result
1278 .transaction()
1279 .orchard_bundle()
1280 .and_then(|bundle| {
1281 bundle
1282 .decrypt_output_with_key(output_index, &orchard_internal_ivk)
1283 .map(|(note, _, _)| Note::Orchard(note))
1284 })
1285 .expect("Wallet-internal outputs must be decryptable with the wallet's IVK")
1286 });
1287
1288 SentTransactionOutput::from_parts(output_index, recipient, value, memo)
1289 },
1290 );
1291
1292 let sapling_dfvk = usk.sapling().to_diversifiable_full_viewing_key();
1293 let sapling_internal_ivk =
1294 PreparedIncomingViewingKey::new(&sapling_dfvk.to_ivk(Scope::Internal));
1295 let sapling_outputs = build_state.sapling_output_meta.into_iter().enumerate().map(
1296 |(i, (recipient, value, memo))| {
1297 let output_index = build_result
1298 .sapling_meta()
1299 .output_index(i)
1300 .expect("An output should exist in the transaction for each Sapling payment.");
1301
1302 let recipient = recipient.into_recipient_with_note(|| {
1303 build_result
1304 .transaction()
1305 .sapling_bundle()
1306 .and_then(|bundle| {
1307 try_sapling_note_decryption(
1308 &sapling_internal_ivk,
1309 &bundle.shielded_outputs()[output_index],
1310 zip212_enforcement(params, min_target_height),
1311 )
1312 .map(|(note, _, _)| Note::Sapling(note))
1313 })
1314 .expect("Wallet-internal outputs must be decryptable with the wallet's IVK")
1315 });
1316
1317 SentTransactionOutput::from_parts(output_index, recipient, value, memo)
1318 },
1319 );
1320
1321 let txid: [u8; 32] = build_result.transaction().txid().into();
1322 assert_eq!(
1323 build_state.transparent_output_meta.len(),
1324 build_result
1325 .transaction()
1326 .transparent_bundle()
1327 .map_or(0, |b| b.vout.len()),
1328 );
1329
1330 #[allow(unused_variables)]
1331 let transparent_outputs = build_state
1332 .transparent_output_meta
1333 .into_iter()
1334 .enumerate()
1335 .map(|(n, (recipient, address, value, step_output_index))| {
1336 let outpoint = OutPoint::new(txid, n as u32);
1341
1342 let recipient = recipient.into_recipient_with_outpoint(
1343 #[cfg(feature = "transparent-inputs")]
1344 outpoint.clone(),
1345 );
1346
1347 #[cfg(feature = "transparent-inputs")]
1348 unused_transparent_outputs.insert(
1349 StepOutput::new(build_state.step_index, step_output_index),
1350 (address, outpoint),
1351 );
1352 SentTransactionOutput::from_parts(n, recipient, value, None)
1353 });
1354
1355 let mut outputs: Vec<SentTransactionOutput<_>> = vec![];
1356 #[cfg(feature = "orchard")]
1357 outputs.extend(orchard_outputs);
1358 outputs.extend(sapling_outputs);
1359 outputs.extend(transparent_outputs);
1360
1361 Ok(StepResult {
1362 build_result,
1363 outputs,
1364 fee_amount: proposal_step.balance().fee_required(),
1365 #[cfg(feature = "transparent-inputs")]
1366 utxos_spent: build_state.utxos_spent,
1367 })
1368}
1369
1370#[allow(clippy::too_many_arguments)]
1385#[allow(clippy::type_complexity)]
1386#[cfg(feature = "pczt")]
1387pub fn create_pczt_from_proposal<DbT, ParamsT, InputsErrT, FeeRuleT, ChangeErrT, N>(
1388 wallet_db: &mut DbT,
1389 params: &ParamsT,
1390 account_id: <DbT as WalletRead>::AccountId,
1391 ovk_policy: OvkPolicy,
1392 proposal: &Proposal<FeeRuleT, N>,
1393) -> Result<pczt::Pczt, CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>>
1394where
1395 DbT: WalletWrite + WalletCommitmentTrees,
1396 ParamsT: consensus::Parameters + Clone,
1397 FeeRuleT: FeeRule,
1398 DbT::AccountId: serde::Serialize,
1399{
1400 use std::collections::HashSet;
1401
1402 let account = wallet_db
1403 .get_account(account_id)
1404 .map_err(Error::DataSource)?
1405 .ok_or(Error::AccountIdNotRecognized)?;
1406 let ufvk = account.ufvk().ok_or(Error::AccountCannotSpend)?;
1407 let account_derivation = account.source().key_derivation();
1408
1409 if proposal.steps().len() > 1 {
1411 return Err(Error::ProposalNotSupported);
1412 }
1413 let fee_rule = proposal.fee_rule();
1414 let min_target_height = proposal.min_target_height();
1415 let prior_step_results = &[];
1416 let proposal_step = proposal.steps().first();
1417 let unused_transparent_outputs = &mut HashMap::new();
1418
1419 let build_state = build_proposed_transaction::<_, _, _, FeeRuleT, _, _>(
1420 wallet_db,
1421 params,
1422 ufvk,
1423 account_id,
1424 ovk_policy,
1425 min_target_height,
1426 prior_step_results,
1427 proposal_step,
1428 #[cfg(feature = "transparent-inputs")]
1429 unused_transparent_outputs,
1430 )?;
1431
1432 let build_result = build_state.builder.build_for_pczt(OsRng, fee_rule)?;
1434
1435 let created = Creator::build_from_parts(build_result.pczt_parts).ok_or(PcztError::Build)?;
1436
1437 let io_finalized = IoFinalizer::new(created).finalize_io()?;
1438
1439 #[cfg(feature = "orchard")]
1440 let orchard_outputs = build_state
1441 .orchard_output_meta
1442 .into_iter()
1443 .enumerate()
1444 .map(|(i, (recipient, _, _))| {
1445 let output_index = build_result
1446 .orchard_meta
1447 .output_action_index(i)
1448 .expect("An action should exist in the transaction for each Orchard output.");
1449
1450 (output_index, PcztRecipient::from_recipient(recipient))
1451 })
1452 .collect::<HashMap<_, _>>();
1453
1454 #[cfg(feature = "orchard")]
1455 let orchard_spends = (0..)
1456 .map(|i| build_result.orchard_meta.spend_action_index(i))
1457 .take_while(|item| item.is_some())
1458 .flatten()
1459 .collect::<HashSet<_>>();
1460
1461 let sapling_outputs = build_state
1462 .sapling_output_meta
1463 .into_iter()
1464 .enumerate()
1465 .map(|(i, (recipient, _, _))| {
1466 let output_index = build_result
1467 .sapling_meta
1468 .output_index(i)
1469 .expect("An output should exist in the transaction for each Sapling output.");
1470
1471 (output_index, PcztRecipient::from_recipient(recipient))
1472 })
1473 .collect::<HashMap<_, _>>();
1474
1475 let pczt = Updater::new(io_finalized)
1476 .update_global_with(|mut updater| {
1477 updater.set_proprietary(
1478 PROPRIETARY_PROPOSAL_INFO.into(),
1479 postcard::to_allocvec(&ProposalInfo::<DbT::AccountId> {
1480 from_account: account_id,
1481 target_height: proposal.min_target_height().into(),
1482 })
1483 .expect("postcard encoding of PCZT proposal metadata should not fail"),
1484 )
1485 })
1486 .update_orchard_with(|mut updater| {
1487 for index in 0..updater.bundle().actions().len() {
1488 updater.update_action_with(index, |mut action_updater| {
1489 if let Some(derivation) = account_derivation {
1491 if orchard_spends.contains(&index) {
1494 action_updater.set_spend_zip32_derivation(
1496 orchard::pczt::Zip32Derivation::parse(
1497 derivation.seed_fingerprint().to_bytes(),
1498 vec![
1499 zip32::ChildIndex::hardened(32).index(),
1500 zip32::ChildIndex::hardened(
1501 params.network_type().coin_type(),
1502 )
1503 .index(),
1504 zip32::ChildIndex::hardened(u32::from(
1505 derivation.account_index(),
1506 ))
1507 .index(),
1508 ],
1509 )
1510 .expect("valid"),
1511 );
1512 }
1513 }
1514
1515 if let Some((pczt_recipient, external_address)) = orchard_outputs.get(&index) {
1516 if let Some(user_address) = external_address {
1517 action_updater.set_output_user_address(user_address.encode());
1518 }
1519 action_updater.set_output_proprietary(
1520 PROPRIETARY_OUTPUT_INFO.into(),
1521 postcard::to_allocvec(pczt_recipient).expect(
1522 "postcard encoding of PCZT recipient metadata should not fail",
1523 ),
1524 );
1525 }
1526
1527 Ok(())
1528 })?;
1529 }
1530 Ok(())
1531 })?
1532 .update_sapling_with(|mut updater| {
1533 if let Some(derivation) = account_derivation {
1535 let non_dummy_spends = updater
1536 .bundle()
1537 .spends()
1538 .iter()
1539 .enumerate()
1540 .filter_map(|(index, spend)| {
1541 spend.proof_generation_key().is_none().then_some(index)
1543 })
1544 .collect::<Vec<_>>();
1545
1546 for index in non_dummy_spends {
1547 updater.update_spend_with(index, |mut spend_updater| {
1548 spend_updater.set_zip32_derivation(
1550 sapling::pczt::Zip32Derivation::parse(
1551 derivation.seed_fingerprint().to_bytes(),
1552 vec![
1553 zip32::ChildIndex::hardened(32).index(),
1554 zip32::ChildIndex::hardened(params.network_type().coin_type())
1555 .index(),
1556 zip32::ChildIndex::hardened(u32::from(
1557 derivation.account_index(),
1558 ))
1559 .index(),
1560 ],
1561 )
1562 .expect("valid"),
1563 );
1564 Ok(())
1565 })?;
1566 }
1567 }
1568
1569 for index in 0..updater.bundle().outputs().len() {
1570 if let Some((pczt_recipient, external_address)) = sapling_outputs.get(&index) {
1571 updater.update_output_with(index, |mut output_updater| {
1572 if let Some(user_address) = external_address {
1573 output_updater.set_user_address(user_address.encode());
1574 }
1575 output_updater.set_proprietary(
1576 PROPRIETARY_OUTPUT_INFO.into(),
1577 postcard::to_allocvec(pczt_recipient).expect(
1578 "postcard encoding of PCZT recipient metadata should not fail",
1579 ),
1580 );
1581 Ok(())
1582 })?;
1583 }
1584 }
1585
1586 Ok(())
1587 })?
1588 .update_transparent_with(|mut updater| {
1589 if let Some(derivation) = account_derivation {
1591 let inputs_to_update = updater
1593 .bundle()
1594 .inputs()
1595 .iter()
1596 .enumerate()
1597 .filter_map(|(index, input)| {
1598 build_state
1599 .transparent_input_addresses
1600 .get(
1601 &input
1602 .script_pubkey()
1603 .address()
1604 .expect("we created this with a supported transparent address"),
1605 )
1606 .map(|address_metadata| {
1607 (
1608 index,
1609 address_metadata.scope(),
1610 address_metadata.address_index(),
1611 )
1612 })
1613 })
1614 .collect::<Vec<_>>();
1615
1616 for (index, scope, address_index) in inputs_to_update {
1617 updater.update_input_with(index, |mut input_updater| {
1618 let pubkey = ufvk
1619 .transparent()
1620 .expect("we derived this successfully in build_proposed_transaction")
1621 .derive_address_pubkey(scope, address_index)
1622 .expect("spending key derivation should not fail");
1623
1624 input_updater.set_bip32_derivation(
1625 pubkey.serialize(),
1626 Bip32Derivation::parse(
1627 derivation.seed_fingerprint().to_bytes(),
1628 vec![
1629 44 | ChildNumber::HARDENED_FLAG,
1631 params.network_type().coin_type() | ChildNumber::HARDENED_FLAG,
1632 u32::from(derivation.account_index())
1633 | ChildNumber::HARDENED_FLAG,
1634 ChildNumber::from(scope).into(),
1635 ChildNumber::from(address_index).into(),
1636 ],
1637 )
1638 .expect("valid"),
1639 );
1640 Ok(())
1641 })?;
1642 }
1643 }
1644
1645 assert_eq!(
1646 build_state.transparent_output_meta.len(),
1647 updater.bundle().outputs().len(),
1648 );
1649 for (index, (recipient, _, _, _)) in
1650 build_state.transparent_output_meta.into_iter().enumerate()
1651 {
1652 updater.update_output_with(index, |mut output_updater| {
1653 let (pczt_recipient, external_address) =
1654 PcztRecipient::from_recipient(recipient);
1655 if let Some(user_address) = external_address {
1656 output_updater.set_user_address(user_address.encode());
1657 }
1658 output_updater.set_proprietary(
1659 PROPRIETARY_OUTPUT_INFO.into(),
1660 postcard::to_allocvec(&pczt_recipient)
1661 .expect("postcard encoding of pczt recipient metadata should not fail"),
1662 );
1663 Ok(())
1664 })?;
1665 }
1666
1667 Ok(())
1668 })?
1669 .finish();
1670
1671 Ok(pczt)
1672}
1673
1674#[cfg(feature = "pczt")]
1689pub fn extract_and_store_transaction_from_pczt<DbT, N>(
1690 wallet_db: &mut DbT,
1691 pczt: pczt::Pczt,
1692 sapling_vk: Option<(
1693 &sapling::circuit::SpendVerifyingKey,
1694 &sapling::circuit::OutputVerifyingKey,
1695 )>,
1696 #[cfg(feature = "orchard")] orchard_vk: Option<&orchard::circuit::VerifyingKey>,
1697) -> Result<TxId, ExtractErrT<DbT, N>>
1698where
1699 DbT: WalletWrite + WalletCommitmentTrees,
1700 DbT::AccountId: serde::de::DeserializeOwned,
1701{
1702 use std::collections::BTreeMap;
1703 use zcash_note_encryption::{Domain, ShieldedOutput, ENC_CIPHERTEXT_SIZE};
1704
1705 let finalized = SpendFinalizer::new(pczt).finalize_spends()?;
1706
1707 let proposal_info = finalized
1708 .global()
1709 .proprietary()
1710 .get(PROPRIETARY_PROPOSAL_INFO)
1711 .ok_or_else(|| PcztError::Invalid("PCZT missing proprietary proposal info field".into()))
1712 .and_then(|v| {
1713 postcard::from_bytes::<ProposalInfo<DbT::AccountId>>(v).map_err(|e| {
1714 PcztError::Invalid(format!(
1715 "Postcard decoding of proprietary proposal info failed: {}",
1716 e
1717 ))
1718 })
1719 })?;
1720
1721 let orchard_output_info = finalized
1722 .orchard()
1723 .actions()
1724 .iter()
1725 .map(|act| {
1726 let note = || {
1727 let recipient =
1728 act.output().recipient().as_ref().and_then(|b| {
1729 ::orchard::Address::from_raw_address_bytes(b).into_option()
1730 })?;
1731 let value = act
1732 .output()
1733 .value()
1734 .map(orchard::value::NoteValue::from_raw)?;
1735 let rho = orchard::note::Rho::from_bytes(act.spend().nullifier()).into_option()?;
1736 let rseed = act.output().rseed().as_ref().and_then(|rseed| {
1737 orchard::note::RandomSeed::from_bytes(*rseed, &rho).into_option()
1738 })?;
1739
1740 orchard::Note::from_parts(recipient, value, rho, rseed).into_option()
1741 };
1742
1743 let external_address = act
1744 .output()
1745 .user_address()
1746 .as_deref()
1747 .map(ZcashAddress::try_from_encoded)
1748 .transpose()
1749 .map_err(|e| PcztError::Invalid(format!("Invalid user_address: {}", e)))?;
1750
1751 let pczt_recipient = act
1752 .output()
1753 .proprietary()
1754 .get(PROPRIETARY_OUTPUT_INFO)
1755 .map(|v| postcard::from_bytes::<PcztRecipient<DbT::AccountId>>(v))
1756 .transpose()
1757 .map_err(|e: postcard::Error| {
1758 PcztError::Invalid(format!(
1759 "Postcard decoding of proprietary output info failed: {}",
1760 e
1761 ))
1762 })?
1763 .map(|pczt_recipient| (pczt_recipient, external_address));
1764
1765 Ok(pczt_recipient.zip(note()))
1769 })
1770 .collect::<Result<Vec<_>, PcztError>>()?;
1771
1772 let sapling_output_info = finalized
1773 .sapling()
1774 .outputs()
1775 .iter()
1776 .map(|out| {
1777 let note = || {
1778 let recipient = out
1779 .recipient()
1780 .as_ref()
1781 .and_then(::sapling::PaymentAddress::from_bytes)?;
1782 let value = out.value().map(::sapling::value::NoteValue::from_raw)?;
1783 let rseed = out
1784 .rseed()
1785 .as_ref()
1786 .cloned()
1787 .map(::sapling::note::Rseed::AfterZip212)?;
1788
1789 Some(::sapling::Note::from_parts(recipient, value, rseed))
1790 };
1791
1792 let external_address = out
1793 .user_address()
1794 .as_deref()
1795 .map(ZcashAddress::try_from_encoded)
1796 .transpose()
1797 .map_err(|e| PcztError::Invalid(format!("Invalid user_address: {}", e)))?;
1798
1799 let pczt_recipient = out
1800 .proprietary()
1801 .get(PROPRIETARY_OUTPUT_INFO)
1802 .map(|v| postcard::from_bytes::<PcztRecipient<DbT::AccountId>>(v))
1803 .transpose()
1804 .map_err(|e: postcard::Error| {
1805 PcztError::Invalid(format!(
1806 "Postcard decoding of proprietary output info failed: {}",
1807 e
1808 ))
1809 })?
1810 .map(|pczt_recipient| (pczt_recipient, external_address));
1811
1812 Ok(pczt_recipient.zip(note()))
1816 })
1817 .collect::<Result<Vec<_>, PcztError>>()?;
1818
1819 let transparent_output_info = finalized
1820 .transparent()
1821 .outputs()
1822 .iter()
1823 .map(|out| {
1824 let external_address = out
1825 .user_address()
1826 .as_deref()
1827 .map(ZcashAddress::try_from_encoded)
1828 .transpose()
1829 .map_err(|e| PcztError::Invalid(format!("Invalid user_address: {}", e)))?;
1830
1831 let pczt_recipient = out
1832 .proprietary()
1833 .get(PROPRIETARY_OUTPUT_INFO)
1834 .map(|v| postcard::from_bytes::<PcztRecipient<DbT::AccountId>>(v))
1835 .transpose()
1836 .map_err(|e: postcard::Error| {
1837 PcztError::Invalid(format!(
1838 "Postcard decoding of proprietary output info failed: {}",
1839 e
1840 ))
1841 })?
1842 .map(|pczt_recipient| (pczt_recipient, external_address));
1843
1844 Ok(pczt_recipient)
1845 })
1846 .collect::<Result<Vec<_>, PcztError>>()?;
1847
1848 let utxos_map = finalized
1849 .transparent()
1850 .inputs()
1851 .iter()
1852 .map(|input| {
1853 ZatBalance::from_u64(*input.value()).map(|value| {
1854 (
1855 OutPoint::new(*input.prevout_txid(), *input.prevout_index()),
1856 value,
1857 )
1858 })
1859 })
1860 .collect::<Result<BTreeMap<_, _>, _>>()?;
1861
1862 let mut tx_extractor = TransactionExtractor::new(finalized);
1863 if let Some((spend_vk, output_vk)) = sapling_vk {
1864 tx_extractor = tx_extractor.with_sapling(spend_vk, output_vk);
1865 }
1866 if let Some(orchard_vk) = orchard_vk {
1867 tx_extractor = tx_extractor.with_orchard(orchard_vk);
1868 }
1869 let transaction = tx_extractor.extract()?;
1870 let txid = transaction.txid();
1871
1872 #[allow(clippy::too_many_arguments)]
1873 fn to_sent_transaction_output<
1874 AccountId: Copy,
1875 D: Domain,
1876 O: ShieldedOutput<D, { ENC_CIPHERTEXT_SIZE }>,
1877 DbT: WalletRead + WalletCommitmentTrees,
1878 N,
1879 >(
1880 domain: D,
1881 note: D::Note,
1882 output: &O,
1883 output_pool: ShieldedProtocol,
1884 output_index: usize,
1885 pczt_recipient: PcztRecipient<AccountId>,
1886 external_address: Option<ZcashAddress>,
1887 note_value: impl Fn(&D::Note) -> u64,
1888 memo_bytes: impl Fn(&D::Memo) -> &[u8; 512],
1889 wallet_note: impl Fn(D::Note) -> Note,
1890 ) -> Result<SentTransactionOutput<AccountId>, ExtractErrT<DbT, N>> {
1891 let pk_d = D::get_pk_d(¬e);
1892 let esk = D::derive_esk(¬e).expect("notes are post-ZIP 212");
1893 let memo = try_output_recovery_with_pkd_esk(&domain, pk_d, esk, output).map(|(_, _, m)| {
1894 MemoBytes::from_bytes(memo_bytes(&m)).expect("Memo is the correct length.")
1895 });
1896
1897 let note_value = Zatoshis::try_from(note_value(¬e))?;
1898 let recipient = match (pczt_recipient, external_address) {
1899 (PcztRecipient::External, Some(addr)) => Ok(Recipient::External {
1900 recipient_address: addr,
1901 output_pool: PoolType::Shielded(output_pool),
1902 }),
1903 (PcztRecipient::External, None) => Err(PcztError::Invalid(
1904 "external recipient needs to have its user_address field set".into(),
1905 )),
1906 #[cfg(feature = "transparent-inputs")]
1907 (PcztRecipient::EphemeralTransparent { .. }, _) => Err(PcztError::Invalid(
1908 "shielded output cannot be EphemeralTransparent".into(),
1909 )),
1910 (PcztRecipient::InternalAccount { receiving_account }, external_address) => {
1911 Ok(Recipient::InternalAccount {
1912 receiving_account,
1913 external_address,
1914 note: Box::new(wallet_note(note)),
1915 })
1916 }
1917 }?;
1918
1919 Ok(SentTransactionOutput::from_parts(
1920 output_index,
1921 recipient,
1922 note_value,
1923 memo,
1924 ))
1925 }
1926
1927 #[cfg(feature = "orchard")]
1928 let orchard_outputs = transaction
1929 .orchard_bundle()
1930 .map(|bundle| {
1931 assert_eq!(bundle.actions().len(), orchard_output_info.len());
1932 bundle
1933 .actions()
1934 .iter()
1935 .zip(orchard_output_info)
1936 .enumerate()
1937 .filter_map(|(output_index, (action, output_info))| {
1938 output_info.map(|((pczt_recipient, external_address), note)| {
1939 let domain = OrchardDomain::for_action(action);
1940 to_sent_transaction_output::<_, _, _, DbT, _>(
1941 domain,
1942 note,
1943 action,
1944 ShieldedProtocol::Orchard,
1945 output_index,
1946 pczt_recipient,
1947 external_address,
1948 |note| note.value().inner(),
1949 |memo| memo,
1950 Note::Orchard,
1951 )
1952 })
1953 })
1954 .collect::<Result<Vec<_>, _>>()
1955 })
1956 .transpose()?;
1957
1958 let sapling_outputs = transaction
1959 .sapling_bundle()
1960 .map(|bundle| {
1961 assert_eq!(bundle.shielded_outputs().len(), sapling_output_info.len());
1962 bundle
1963 .shielded_outputs()
1964 .iter()
1965 .zip(sapling_output_info)
1966 .enumerate()
1967 .filter_map(|(output_index, (action, output_info))| {
1968 output_info.map(|((pczt_recipient, external_address), note)| {
1969 let domain =
1970 SaplingDomain::new(sapling::note_encryption::Zip212Enforcement::On);
1971 to_sent_transaction_output::<_, _, _, DbT, _>(
1972 domain,
1973 note,
1974 action,
1975 ShieldedProtocol::Sapling,
1976 output_index,
1977 pczt_recipient,
1978 external_address,
1979 |note| note.value().inner(),
1980 |memo| memo,
1981 Note::Sapling,
1982 )
1983 })
1984 })
1985 .collect::<Result<Vec<_>, _>>()
1986 })
1987 .transpose()?;
1988
1989 #[allow(unused_variables)]
1990 let transparent_outputs = transaction
1991 .transparent_bundle()
1992 .map(|bundle| {
1993 assert_eq!(bundle.vout.len(), transparent_output_info.len());
1994 bundle
1995 .vout
1996 .iter()
1997 .zip(transparent_output_info)
1998 .enumerate()
1999 .filter_map(|(output_index, (output, output_info))| {
2000 output_info.map(|(pczt_recipient, external_address)| {
2001 let outpoint = OutPoint::new(txid.into(), output_index as u32);
2006
2007 let recipient = match (pczt_recipient, external_address) {
2008 (PcztRecipient::External, Some(addr)) => {
2009 Ok(Recipient::External {
2010 recipient_address: addr,
2011 output_pool: PoolType::Transparent,
2012 })
2013 }
2014 (PcztRecipient::External, None) => Err(PcztError::Invalid(
2015 "external recipient needs to have its user_address field set".into(),
2016 )),
2017 #[cfg(feature = "transparent-inputs")]
2018 (PcztRecipient::EphemeralTransparent { receiving_account }, _) => output
2019 .recipient_address()
2020 .ok_or(PcztError::Invalid(
2021 "Ephemeral outputs cannot have a non-standard script_pubkey"
2022 .into(),
2023 ))
2024 .map(|ephemeral_address| Recipient::EphemeralTransparent {
2025 receiving_account,
2026 ephemeral_address,
2027 outpoint,
2028 }),
2029 (
2030 PcztRecipient::InternalAccount {
2031 receiving_account,
2032 },
2033 _,
2034 ) => Err(PcztError::Invalid(
2035 "Transparent output cannot be InternalAccount".into(),
2036 )),
2037 }?;
2038
2039 Ok(SentTransactionOutput::from_parts(
2040 output_index,
2041 recipient,
2042 output.value,
2043 None,
2044 ))
2045 })
2046 })
2047 .collect::<Result<Vec<_>, ExtractErrT<DbT, _>>>()
2048 })
2049 .transpose()?;
2050
2051 let mut outputs: Vec<SentTransactionOutput<_>> = vec![];
2052 #[cfg(feature = "orchard")]
2053 outputs.extend(orchard_outputs.into_iter().flatten());
2054 outputs.extend(sapling_outputs.into_iter().flatten());
2055 outputs.extend(transparent_outputs.into_iter().flatten());
2056
2057 let fee_amount = Zatoshis::try_from(transaction.fee_paid(|outpoint| {
2058 utxos_map
2059 .get(outpoint)
2060 .copied()
2061 .ok_or(BalanceError::Overflow)
2064 })?)?;
2065
2066 let utxos_spent = utxos_map.into_keys().collect::<Vec<_>>();
2068
2069 let created = time::OffsetDateTime::now_utc();
2070
2071 let transactions = vec![SentTransaction::new(
2072 &transaction,
2073 created,
2074 BlockHeight::from_u32(proposal_info.target_height),
2075 proposal_info.from_account,
2076 &outputs,
2077 fee_amount,
2078 #[cfg(feature = "transparent-inputs")]
2079 &utxos_spent,
2080 )];
2081
2082 wallet_db
2083 .store_transactions_to_be_sent(&transactions)
2084 .map_err(Error::DataSource)?;
2085
2086 Ok(txid)
2087}
2088
2089#[cfg(feature = "transparent-inputs")]
2123#[allow(clippy::too_many_arguments)]
2124#[allow(clippy::type_complexity)]
2125pub fn shield_transparent_funds<DbT, ParamsT, InputsT, ChangeT>(
2126 wallet_db: &mut DbT,
2127 params: &ParamsT,
2128 spend_prover: &impl SpendProver,
2129 output_prover: &impl OutputProver,
2130 input_selector: &InputsT,
2131 change_strategy: &ChangeT,
2132 shielding_threshold: Zatoshis,
2133 usk: &UnifiedSpendingKey,
2134 from_addrs: &[TransparentAddress],
2135 to_account: <DbT as InputSource>::AccountId,
2136 min_confirmations: u32,
2137) -> Result<NonEmpty<TxId>, ShieldErrT<DbT, InputsT, ChangeT>>
2138where
2139 ParamsT: consensus::Parameters,
2140 DbT: WalletWrite + WalletCommitmentTrees + InputSource<Error = <DbT as WalletRead>::Error>,
2141 InputsT: ShieldingSelector<InputSource = DbT>,
2142 ChangeT: ChangeStrategy<MetaSource = DbT>,
2143{
2144 let proposal = propose_shielding(
2145 wallet_db,
2146 params,
2147 input_selector,
2148 change_strategy,
2149 shielding_threshold,
2150 from_addrs,
2151 to_account,
2152 min_confirmations,
2153 )?;
2154
2155 create_proposed_transactions(
2156 wallet_db,
2157 params,
2158 spend_prover,
2159 output_prover,
2160 usk,
2161 OvkPolicy::Sender,
2162 &proposal,
2163 )
2164}