1#![cfg_attr(
25 feature = "transparent-inputs",
26 doc = "
27Another important high-level operation provided by this module is [`propose_shielding`], which
28takes a set of transparent source addresses, and constructs a [`Proposal`] to send those funds
29to a wallet-internal shielded address, as described in [ZIP 316](https://zips.z.cash/zip-0316).
30
31[`propose_shielding`]: crate::data_api::wallet::propose_shielding
32"
33)]
34use nonempty::NonEmpty;
38use rand_core::CryptoRng;
39use std::{
40 num::NonZeroU32,
41 ops::{Add, Sub},
42 time::SystemTime,
43};
44
45use shardtree::error::{QueryError, ShardTreeError};
46
47pub use super::locking::{LockRequest, unlock_proposal_inputs};
48use super::{InputSource, locking::lock_proposal_inputs};
49use crate::{
50 data_api::{
51 Account, MaxSpendMode, NoteCommitmentTree, SentTransaction, SentTransactionOutput,
52 WalletCommitmentTrees, WalletRead, WalletWrite,
53 error::{AddressExpiryError, Error},
54 wallet::input_selection::propose_send_max,
55 },
56 decrypt_transaction,
57 fees::{
58 ChangeStrategy, DustOutputPolicy, StandardFeeRule, standard::SingleOutputChangeStrategy,
59 },
60 proposal::{Proposal, ProposalError, Step, StepOutputIndex},
61 util::Clock,
62 wallet::{Note, OvkPolicy, Recipient},
63};
64use sapling::{
65 note_encryption::{PreparedIncomingViewingKey, try_sapling_note_decryption},
66 prover::{OutputProver, SpendProver},
67};
68use transparent::{address::TransparentAddress, builder::TransparentSigningSet, bundle::OutPoint};
69use zcash_address::ZcashAddress;
70use zcash_keys::{
71 address::{Address, UnifiedAddress},
72 keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
73};
74use zcash_primitives::transaction::{
75 Transaction, TxId, TxVersion,
76 builder::{BuildConfig, BuildResult, Builder, BundlePadding},
77 components::sapling::zip212_enforcement,
78 fees::FeeRule,
79};
80use zcash_protocol::{
81 PoolType, ShieldedPool,
82 consensus::{self, BlockHeight},
83 memo::MemoBytes,
84 value::Zatoshis,
85 zip318::AnchorGrid,
86};
87use zip32::Scope;
88use zip321::Payment;
89#[cfg(feature = "orchard")]
90use {
91 crate::data_api::anchor_retention::PoolMigrationParams,
92 zcash_protocol::{consensus::NetworkUpgrade, zip318::PoolMigrationConstants},
93};
94
95#[cfg(feature = "transparent-inputs")]
96use {
97 super::CoinbaseFilter,
98 crate::{
99 fees::ChangeValue,
100 proposal::StepOutput,
101 wallet::{TransparentAddressMetadata, TransparentAddressSource},
102 },
103 core::convert::Infallible,
104 input_selection::ShieldingSelector,
105 std::collections::HashMap,
106 transparent::bundle::TxOut,
107};
108
109#[cfg(feature = "transparent-key-import")]
110use zcash_script::script::{self as zs_script, Evaluable};
111
112#[cfg(feature = "pczt")]
113use {
114 crate::data_api::error::PcztError,
115 bip32::ChildNumber,
116 orchard::note_encryption::{IronwoodDomain, OrchardDomain},
117 pczt::roles::{
118 creator::Creator, io_finalizer::IoFinalizer, redactor::Redactor,
119 spend_finalizer::SpendFinalizer, tx_extractor::TransactionExtractor, updater::Updater,
120 },
121 sapling::note_encryption::SaplingDomain,
122 serde::{Deserialize, Serialize},
123 std::collections::BTreeMap,
124 transparent::pczt::Bip32Derivation,
125 zcash_note_encryption::{Domain, ShieldedOutput, try_output_recovery_with_pkd_esk},
126 zcash_protocol::{consensus::NetworkConstants, value::BalanceError},
127};
128
129pub mod input_selection;
130use input_selection::{
131 GreedyInputSelector, GreedyInputSelectorError, InputSelector, InputSelectorError,
132};
133
134#[cfg(feature = "pczt")]
135const PROPRIETARY_PROPOSAL_INFO: &str = "zcash_client_backend:proposal_info";
136#[cfg(feature = "pczt")]
137const PROPRIETARY_OUTPUT_INFO: &str = "zcash_client_backend:output_info";
138
139#[cfg(feature = "orchard")]
140fn ironwood_active_at<ParamsT: consensus::Parameters, H: Into<BlockHeight>>(
141 params: &ParamsT,
142 target_height: H,
143) -> bool {
144 params.is_nu_active(NetworkUpgrade::Nu6_3, target_height.into())
145}
146
147#[cfg(feature = "pczt")]
148fn serialize_target_height<S>(
149 target_height: &TargetHeight,
150 serializer: S,
151) -> Result<S::Ok, S::Error>
152where
153 S: serde::Serializer,
154{
155 let u: u32 = BlockHeight::from(*target_height).into();
156 u.serialize(serializer)
157}
158
159#[cfg(feature = "pczt")]
160fn deserialize_target_height<'de, D>(deserializer: D) -> Result<TargetHeight, D::Error>
161where
162 D: serde::Deserializer<'de>,
163{
164 let u = u32::deserialize(deserializer)?;
165 Ok(BlockHeight::from_u32(u).into())
166}
167
168#[cfg(feature = "pczt")]
172#[derive(Serialize, Deserialize)]
173struct ProposalInfo<AccountId> {
174 from_account: AccountId,
175 #[serde(
176 serialize_with = "serialize_target_height",
177 deserialize_with = "deserialize_target_height"
178 )]
179 target_height: TargetHeight,
180}
181
182#[cfg(feature = "pczt")]
186#[derive(Serialize, Deserialize)]
187enum PcztRecipient<AccountId> {
188 External,
189 #[cfg(feature = "transparent-inputs")]
190 EphemeralTransparent {
191 receiving_account: AccountId,
192 },
193 InternalShielded {
194 receiving_account: AccountId,
195 },
196 #[cfg(feature = "transparent-inputs")]
199 InternalTransparent {
200 receiving_account: AccountId,
201 },
202}
203
204#[cfg(feature = "pczt")]
205impl<AccountId: Copy> PcztRecipient<AccountId> {
206 fn from_shielded_recipient(
207 recipient: ShieldedBuildRecipient<AccountId>,
208 ) -> (Self, Option<ZcashAddress>) {
209 match recipient {
210 ShieldedBuildRecipient::External {
211 recipient_address, ..
212 } => (PcztRecipient::External, Some(recipient_address)),
213 ShieldedBuildRecipient::InternalShielded {
214 receiving_account,
215 external_address,
216 } => (
217 PcztRecipient::InternalShielded { receiving_account },
218 external_address,
219 ),
220 }
221 }
222
223 fn from_transparent_recipient(
224 recipient: TransparentBuildRecipient<AccountId>,
225 ) -> (Self, Option<ZcashAddress>) {
226 match recipient {
227 TransparentBuildRecipient::External {
228 recipient_address, ..
229 } => (PcztRecipient::External, Some(recipient_address)),
230 #[cfg(feature = "transparent-inputs")]
231 TransparentBuildRecipient::EphemeralTransparent {
232 receiving_account, ..
233 } => (
234 PcztRecipient::EphemeralTransparent { receiving_account },
235 None,
236 ),
237 #[cfg(feature = "transparent-inputs")]
238 TransparentBuildRecipient::InternalTransparent {
239 receiving_account, ..
240 } => (
241 PcztRecipient::InternalTransparent { receiving_account },
242 None,
243 ),
244 }
245 }
246}
247
248pub fn decrypt_and_store_transaction<ParamsT, DbT>(
251 params: &ParamsT,
252 data: &mut DbT,
253 tx: &Transaction,
254 mined_height: Option<BlockHeight>,
255) -> Result<(), <DbT as WalletRead>::Error>
256where
257 ParamsT: consensus::Parameters,
258 DbT: WalletWrite,
259{
260 let ufvks = data.get_unified_full_viewing_keys()?;
262
263 data.store_decrypted_tx(decrypt_transaction(
264 params,
265 mined_height.map_or_else(|| data.get_tx_height(tx.txid()), |h| Ok(Some(h)))?,
266 data.chain_height()?,
267 tx,
268 &ufvks,
269 ))?;
270
271 Ok(())
272}
273
274pub type ProposeTransferErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT> = Error<
277 <DbT as WalletRead>::Error,
278 CommitmentTreeErrT,
279 <InputsT as InputSelector>::Error,
280 <<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
281 <ChangeT as ChangeStrategy>::Error,
282 <<InputsT as InputSelector>::InputSource as InputSource>::NoteRef,
283>;
284
285pub type ProposeSendMaxErrT<DbT, CommitmentTreeErrT, FeeRuleT> = Error<
289 <DbT as WalletRead>::Error,
290 CommitmentTreeErrT,
291 GreedyInputSelectorError,
292 <FeeRuleT as FeeRule>::Error,
293 <FeeRuleT as FeeRule>::Error,
294 <DbT as InputSource>::NoteRef,
295>;
296
297#[cfg(feature = "transparent-inputs")]
300pub type ProposeShieldingErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT> = Error<
301 <DbT as WalletRead>::Error,
302 CommitmentTreeErrT,
303 <InputsT as ShieldingSelector>::Error,
304 <<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
305 <ChangeT as ChangeStrategy>::Error,
306 Infallible,
307>;
308
309pub type CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N> = Error<
311 <DbT as WalletRead>::Error,
312 <DbT as WalletCommitmentTrees>::Error,
313 InputsErrT,
314 <FeeRuleT as FeeRule>::Error,
315 ChangeErrT,
316 N,
317>;
318
319pub type TransferErrT<DbT, InputsT, ChangeT> = Error<
321 <DbT as WalletRead>::Error,
322 <DbT as WalletCommitmentTrees>::Error,
323 <InputsT as InputSelector>::Error,
324 <<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
325 <ChangeT as ChangeStrategy>::Error,
326 <<InputsT as InputSelector>::InputSource as InputSource>::NoteRef,
327>;
328
329#[cfg(feature = "transparent-inputs")]
331pub type ShieldErrT<DbT, InputsT, ChangeT> = Error<
332 <DbT as WalletRead>::Error,
333 <DbT as WalletCommitmentTrees>::Error,
334 <InputsT as ShieldingSelector>::Error,
335 <<ChangeT as ChangeStrategy>::FeeRule as FeeRule>::Error,
336 <ChangeT as ChangeStrategy>::Error,
337 Infallible,
338>;
339
340#[cfg(feature = "pczt")]
342pub type ExtractErrT<DbT, N> = Error<
343 <DbT as WalletRead>::Error,
344 <DbT as WalletCommitmentTrees>::Error,
345 Infallible,
346 Infallible,
347 Infallible,
348 N,
349>;
350
351#[repr(transparent)]
355#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
356pub struct TargetHeight(BlockHeight);
357
358impl TargetHeight {
359 pub fn saturating_sub(self, value: u32) -> BlockHeight {
362 self.0.saturating_sub(value)
363 }
364}
365
366impl From<BlockHeight> for TargetHeight {
367 fn from(value: BlockHeight) -> Self {
368 TargetHeight(value)
369 }
370}
371
372impl From<TargetHeight> for BlockHeight {
373 fn from(value: TargetHeight) -> Self {
374 value.0
375 }
376}
377
378impl From<TargetHeight> for u32 {
379 fn from(value: TargetHeight) -> Self {
380 u32::from(value.0)
381 }
382}
383
384impl From<u32> for TargetHeight {
385 fn from(value: u32) -> Self {
386 TargetHeight(BlockHeight::from_u32(value))
387 }
388}
389
390impl<I> Add<I> for TargetHeight
391where
392 BlockHeight: Add<I>,
393{
394 type Output = <BlockHeight as Add<I>>::Output;
395
396 fn add(self, rhs: I) -> Self::Output {
397 self.0 + rhs
398 }
399}
400
401impl<I> Sub<I> for TargetHeight
402where
403 BlockHeight: Sub<I>,
404{
405 type Output = <BlockHeight as Sub<I>>::Output;
406
407 fn sub(self, rhs: I) -> Self::Output {
408 self.0 - rhs
409 }
410}
411
412#[derive(Clone, Copy, Debug, PartialEq, Eq)]
421pub struct ConfirmationsPolicyError;
422
423impl core::fmt::Display for ConfirmationsPolicyError {
424 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
425 write!(
426 f,
427 "Trusted confirmations must not exceed untrusted confirmations"
428 )
429 }
430}
431
432#[derive(Clone, Copy, Debug, PartialEq, Eq)]
434pub struct ConfirmationsPolicy {
435 trusted: NonZeroU32,
436 untrusted: NonZeroU32,
437 #[cfg(feature = "transparent-inputs")]
438 allow_zero_conf_shielding: bool,
439}
440
441impl Default for ConfirmationsPolicy {
452 fn default() -> Self {
453 ConfirmationsPolicy {
454 trusted: NonZeroU32::MIN.saturating_add(2),
456 untrusted: NonZeroU32::MIN.saturating_add(9),
458 #[cfg(feature = "transparent-inputs")]
459 allow_zero_conf_shielding: true,
460 }
461 }
462}
463
464impl ConfirmationsPolicy {
465 pub const MIN: Self = ConfirmationsPolicy {
469 trusted: NonZeroU32::MIN,
470 untrusted: NonZeroU32::MIN,
471 #[cfg(feature = "transparent-inputs")]
472 allow_zero_conf_shielding: true,
473 };
474
475 pub fn new(
485 trusted: NonZeroU32,
486 untrusted: NonZeroU32,
487 #[cfg(feature = "transparent-inputs")] allow_zero_conf_shielding: bool,
488 ) -> Result<Self, ConfirmationsPolicyError> {
489 if trusted > untrusted {
490 Err(ConfirmationsPolicyError)
491 } else {
492 Ok(Self {
493 trusted,
494 untrusted,
495 #[cfg(feature = "transparent-inputs")]
496 allow_zero_conf_shielding,
497 })
498 }
499 }
500
501 pub fn new_symmetrical(
507 min_confirmations: NonZeroU32,
508 #[cfg(feature = "transparent-inputs")] allow_zero_conf_shielding: bool,
509 ) -> Self {
510 Self {
511 trusted: min_confirmations,
512 untrusted: min_confirmations,
513 #[cfg(feature = "transparent-inputs")]
514 allow_zero_conf_shielding,
515 }
516 }
517
518 #[cfg(any(test, feature = "test-dependencies"))]
525 pub fn new_unchecked(
526 trusted: u32,
527 untrusted: u32,
528 #[cfg(feature = "transparent-inputs")] allow_zero_conf_shielding: bool,
529 ) -> Self {
530 Self::new(
531 NonZeroU32::new(trusted).expect("trusted must be nonzero"),
532 NonZeroU32::new(untrusted).expect("untrusted must be nonzero"),
533 #[cfg(feature = "transparent-inputs")]
534 allow_zero_conf_shielding,
535 )
536 .expect("trusted must be <= untrusted")
537 }
538
539 #[cfg(any(test, feature = "test-dependencies"))]
545 pub fn new_symmetrical_unchecked(
546 min_confirmations: u32,
547 #[cfg(feature = "transparent-inputs")] allow_zero_conf_shielding: bool,
548 ) -> Self {
549 Self::new_symmetrical(
550 NonZeroU32::new(min_confirmations).expect("min_confirmations must be nonzero"),
551 #[cfg(feature = "transparent-inputs")]
552 allow_zero_conf_shielding,
553 )
554 }
555
556 pub fn trusted(&self) -> NonZeroU32 {
562 self.trusted
563 }
564
565 pub fn untrusted(&self) -> NonZeroU32 {
571 self.untrusted
572 }
573
574 pub fn anchor_height(&self, target_height: TargetHeight) -> BlockHeight {
581 target_height.saturating_sub(u32::from(self.trusted()))
582 }
583
584 pub fn bucketed(
614 &self,
615 grid: AnchorGrid,
616 target_height: TargetHeight,
617 activation_height: BlockHeight,
618 ) -> Option<Self> {
619 let most_recent = grid.boundary_at_or_below(self.anchor_height(target_height));
625 let boundary = grid.boundary_before(most_recent)?;
626
627 if boundary <= activation_height {
633 return None;
634 }
635 let bucketed = u32::from(target_height).checked_sub(u32::from(boundary))?;
636 let trusted = NonZeroU32::new(bucketed)?;
637 Self::new(
638 trusted,
639 core::cmp::max(self.untrusted(), trusted),
640 #[cfg(feature = "transparent-inputs")]
641 self.allow_zero_conf_shielding(),
642 )
643 .ok()
644 }
645
646 #[cfg(feature = "transparent-inputs")]
649 pub fn allow_zero_conf_shielding(&self) -> bool {
650 self.allow_zero_conf_shielding
651 }
652
653 #[allow(clippy::too_many_arguments)]
673 pub fn confirmations_until_spendable(
674 &self,
675 target_height: TargetHeight,
676 pool_type: PoolType,
677 receiving_key_scope: Option<Scope>,
678 mined_height: Option<BlockHeight>,
679 tx_trusted: bool,
680 max_shielding_input_height: Option<BlockHeight>,
681 tx_shielding_inputs_trusted: bool,
682 ) -> u32 {
683 let trusted_height = target_height.saturating_sub(u32::from(self.trusted));
686 let untrusted_height = target_height.saturating_sub(u32::from(self.untrusted));
689
690 let confs_for_trusted =
696 mined_height.map_or(u32::from(self.trusted), |h| h - trusted_height);
697 let confs_for_untrusted =
698 mined_height.map_or(u32::from(self.untrusted), |h| h - untrusted_height);
699 match pool_type {
700 PoolType::Transparent => {
701 #[cfg(feature = "transparent-inputs")]
702 let zc_shielding = self.allow_zero_conf_shielding;
703 #[cfg(not(feature = "transparent-inputs"))]
704 let zc_shielding = false;
705
706 if zc_shielding {
707 0
708 } else if tx_trusted || receiving_key_scope == Some(Scope::Internal) {
709 confs_for_trusted
710 } else {
711 confs_for_untrusted
712 }
713 }
714 PoolType::Shielded(_) => {
715 if tx_trusted {
716 confs_for_trusted
717 } else if receiving_key_scope == Some(Scope::Internal) {
718 if let Some(h) = max_shielding_input_height {
722 if tx_shielding_inputs_trusted {
723 h - trusted_height
724 } else {
725 h - untrusted_height
726 }
727 } else {
728 confs_for_trusted
729 }
730 } else {
731 confs_for_untrusted
732 }
733 }
734 }
735 }
736}
737
738#[allow(clippy::too_many_arguments)]
776#[allow(clippy::type_complexity)]
777pub fn propose_transfer<DbT, ParamsT, InputsT, ChangeT, CommitmentTreeErrT>(
778 wallet_db: &mut DbT,
779 params: &ParamsT,
780 spend_from_account: <DbT as InputSource>::AccountId,
781 input_selector: &InputsT,
782 change_strategy: &ChangeT,
783 request: zip321::TransactionRequest,
784 confirmations_policy: ConfirmationsPolicy,
785 spend_policy: &input_selection::SpendPolicy,
786 lock_inputs: Option<LockRequest>,
787 proposed_version: Option<TxVersion>,
788) -> Result<
789 Proposal<ChangeT::FeeRule, <DbT as InputSource>::NoteRef>,
790 ProposeTransferErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT>,
791>
792where
793 DbT: WalletWrite + InputSource<Error = <DbT as WalletRead>::Error>,
794 <DbT as InputSource>::NoteRef: Copy + Eq + Ord,
795 ParamsT: consensus::Parameters + Clone,
796 InputsT: InputSelector<InputSource = DbT>,
797 ChangeT: ChangeStrategy<MetaSource = DbT>,
798{
799 let maybe_intial_heights = wallet_db
804 .get_target_and_anchor_heights(confirmations_policy.trusted)
805 .map_err(InputSelectorError::DataSource)?;
806 let (target_height, anchor_height) =
807 maybe_intial_heights.ok_or_else(|| InputSelectorError::SyncRequired)?;
808
809 let zip318 = wallet_db.pool_migration_params(params);
814
815 #[cfg(feature = "orchard")]
838 let canonical_fee = crate::fees::canonical_crossing_fee(params, target_height.into()).ok();
839
840 #[cfg(feature = "orchard")]
843 let bucketed_policy = canonical_crossing_candidate(params, &zip318, &request, target_height)
844 .then(|| params.activation_height(NetworkUpgrade::Nu6_3))
845 .flatten()
846 .and_then(|activation| {
847 confirmations_policy.bucketed(zip318.anchor_grid(), target_height, activation)
848 })
849 .filter(|_| spend_policy.permits_shielded(ShieldedPool::Orchard));
852
853 #[cfg(feature = "orchard")]
863 let bucketed_policy = match bucketed_policy {
864 Some(policy) => wallet_db
865 .anchor_computable(ShieldedPool::Orchard, policy.anchor_height(target_height))
866 .map_err(|e| Error::from(InputSelectorError::DataSource(e)))?
867 .then_some(policy),
868 None => None,
869 };
870
871 #[cfg(feature = "orchard")]
872 let canonical_attempt = bucketed_policy.map(|bucketed_policy| {
873 let orchard_only = input_selection::SpendPolicy::shielded_pools([ShieldedPool::Orchard])
881 .with_locked_input_policy(spend_policy.locked_input_policy().clone())
882 .with_note_selection(input_selection::NoteSelection::PreferSingle);
883
884 input_selector.propose_transaction(
885 params,
886 wallet_db,
887 target_height,
888 bucketed_policy.anchor_height(target_height),
889 &zip318,
890 bucketed_policy,
891 spend_from_account,
892 request.clone(),
893 change_strategy,
894 &orchard_only,
895 proposed_version,
896 )
897 });
898
899 #[cfg(feature = "orchard")]
906 let canonical_proposal = match canonical_attempt {
907 Some(Ok(proposal)) => {
908 let is_canonical = proposal.steps().len() == 1
913 && canonical_fee.is_some_and(|fee| {
914 proposal.steps().first().is_canonical_crossing(&zip318, fee)
915 });
916 is_canonical.then_some(proposal)
917 }
918 Some(Err(InputSelectorError::InsufficientFunds { .. })) | None => None,
919 Some(Err(other)) => return Err(other.into()),
920 };
921
922 #[cfg(not(feature = "orchard"))]
923 let canonical_proposal = None;
924
925 let proposal = match canonical_proposal {
926 Some(proposal) => proposal,
927 None => input_selector.propose_transaction(
928 params,
929 wallet_db,
930 target_height,
931 anchor_height,
932 &zip318,
933 confirmations_policy,
934 spend_from_account,
935 request,
936 change_strategy,
937 spend_policy,
938 proposed_version,
939 )?,
940 };
941 proposal.check_transaction_size()?;
942 if let Some(request) = lock_inputs {
943 let lock_expiry_height = target_height + request.for_blocks();
944 lock_proposal_inputs(wallet_db, &proposal, request.owner(), lock_expiry_height)?;
945 }
946
947 Ok(proposal.with_proposed_version(proposed_version))
950}
951
952#[cfg(feature = "orchard")]
962fn canonical_crossing_candidate<ParamsT: consensus::Parameters>(
963 params: &ParamsT,
964 zip318: &PoolMigrationParams,
965 request: &zip321::TransactionRequest,
966 target_height: TargetHeight,
967) -> bool {
968 params.is_nu_active(NetworkUpgrade::Nu6_3, target_height.into())
969 && match request.payments().values().collect::<Vec<_>>()[..] {
970 [payment] => payment
971 .amount()
972 .is_some_and(|amount| zip318.is_canonical_denomination(amount)),
973 _ => false,
974 }
975}
976
977#[cfg(feature = "orchard")]
984fn step_is_canonical_crossing<DbT, ParamsT, N>(
985 wallet_db: &DbT,
986 params: &ParamsT,
987 step: &Step<N>,
988 target_height: TargetHeight,
989) -> bool
990where
991 DbT: WalletRead,
992 ParamsT: consensus::Parameters,
993{
994 crate::fees::canonical_crossing_fee(params, target_height.into())
995 .is_ok_and(|fee| step.is_canonical_crossing(&wallet_db.pool_migration_params(params), fee))
996}
997
998#[allow(clippy::too_many_arguments)]
1028#[allow(clippy::type_complexity)]
1029pub fn propose_standard_transfer_to_address<DbT, ParamsT, CommitmentTreeErrT>(
1030 wallet_db: &mut DbT,
1031 params: &ParamsT,
1032 fee_rule: StandardFeeRule,
1033 spend_from_account: <DbT as InputSource>::AccountId,
1034 confirmations_policy: ConfirmationsPolicy,
1035 to: &Address,
1036 amount: Zatoshis,
1037 memo: Option<MemoBytes>,
1038 change_memo: Option<MemoBytes>,
1039 fallback_change_pool: ShieldedPool,
1040 lock_inputs: Option<LockRequest>,
1041 proposed_version: Option<TxVersion>,
1042) -> Result<
1043 Proposal<StandardFeeRule, DbT::NoteRef>,
1044 ProposeTransferErrT<
1045 DbT,
1046 CommitmentTreeErrT,
1047 GreedyInputSelector<DbT>,
1048 SingleOutputChangeStrategy<DbT>,
1049 >,
1050>
1051where
1052 ParamsT: consensus::Parameters + Clone,
1053 DbT: InputSource,
1054 DbT: WalletWrite,
1055 DbT: WalletRead<Error = <DbT as InputSource>::Error, AccountId = <DbT as InputSource>::AccountId>,
1056 DbT::NoteRef: Copy + Eq + Ord,
1057{
1058 let request = zip321::TransactionRequest::new(vec![
1059 Payment::new(
1060 to.to_zcash_address(params),
1061 Some(amount),
1062 memo,
1063 None,
1064 None,
1065 vec![],
1066 )
1067 .map_err(Error::Payment)?,
1068 ])
1069 .expect(
1070 "It should not be possible for this to violate ZIP 321 request construction invariants.",
1071 );
1072
1073 let input_selector = GreedyInputSelector::<DbT>::new();
1074 let change_strategy = SingleOutputChangeStrategy::<DbT>::new(
1075 fee_rule,
1076 change_memo,
1077 fallback_change_pool,
1078 DustOutputPolicy::default(),
1079 );
1080
1081 propose_transfer(
1082 wallet_db,
1083 params,
1084 spend_from_account,
1085 &input_selector,
1086 &change_strategy,
1087 request,
1088 confirmations_policy,
1089 &input_selection::SpendPolicy::default(),
1090 lock_inputs,
1091 proposed_version,
1092 )
1093}
1094
1095#[allow(clippy::too_many_arguments)]
1143#[allow(clippy::type_complexity)]
1144pub fn propose_send_max_transfer<DbT, ParamsT, FeeRuleT, CommitmentTreeErrT>(
1145 wallet_db: &mut DbT,
1146 params: &ParamsT,
1147 spend_from_account: <DbT as InputSource>::AccountId,
1148 spend_pools: &[ShieldedPool],
1149 fee_rule: &FeeRuleT,
1150 recipient: ZcashAddress,
1151 memo: Option<MemoBytes>,
1152 mode: MaxSpendMode,
1153 confirmations_policy: ConfirmationsPolicy,
1154 locked_input_policy: &input_selection::LockedInputPolicy,
1155 lock_inputs: Option<LockRequest>,
1156) -> Result<
1157 Proposal<FeeRuleT, <DbT as InputSource>::NoteRef>,
1158 ProposeSendMaxErrT<DbT, CommitmentTreeErrT, FeeRuleT>,
1159>
1160where
1161 DbT: WalletWrite + InputSource<Error = <DbT as WalletRead>::Error>,
1162 <DbT as InputSource>::NoteRef: Copy + Eq + Ord,
1163 ParamsT: consensus::Parameters + Clone,
1164 FeeRuleT: FeeRule + Clone,
1165{
1166 let (target_height, anchor_height) = wallet_db
1167 .get_target_and_anchor_heights(confirmations_policy.trusted())
1168 .map_err(|e| Error::from(InputSelectorError::DataSource(e)))?
1169 .ok_or_else(|| Error::from(InputSelectorError::SyncRequired))?;
1170
1171 if memo.is_some() && !recipient.can_receive_memo() {
1172 return Err(Error::Payment(zip321::PaymentError::TransparentMemo));
1173 }
1174
1175 let proposal = propose_send_max(
1176 params,
1177 wallet_db,
1178 fee_rule,
1179 spend_from_account,
1180 spend_pools,
1181 target_height,
1182 anchor_height,
1183 mode,
1184 confirmations_policy,
1185 recipient,
1186 memo,
1187 locked_input_policy,
1188 )?;
1189
1190 if let Some(request) = lock_inputs {
1191 let lock_expiry_height = target_height + request.for_blocks();
1192 lock_proposal_inputs(wallet_db, &proposal, request.owner(), lock_expiry_height)?;
1193 }
1194
1195 Ok(proposal)
1196}
1197
1198#[cfg(feature = "transparent-inputs")]
1209#[allow(clippy::too_many_arguments)]
1210#[allow(clippy::type_complexity)]
1211pub fn propose_shielding<DbT, ParamsT, InputsT, ChangeT, CommitmentTreeErrT>(
1212 wallet_db: &mut DbT,
1213 params: &ParamsT,
1214 input_selector: &InputsT,
1215 change_strategy: &ChangeT,
1216 shielding_threshold: Zatoshis,
1217 from_addrs: &[TransparentAddress],
1218 to_account: <DbT as InputSource>::AccountId,
1219 confirmations_policy: ConfirmationsPolicy,
1220 output_filter: CoinbaseFilter,
1221 lock_inputs: Option<LockRequest>,
1222) -> Result<
1223 Proposal<ChangeT::FeeRule, Infallible>,
1224 ProposeShieldingErrT<DbT, CommitmentTreeErrT, InputsT, ChangeT>,
1225>
1226where
1227 ParamsT: consensus::Parameters,
1228 DbT: WalletWrite + InputSource<Error = <DbT as WalletRead>::Error>,
1229 InputsT: ShieldingSelector<InputSource = DbT>,
1230 ChangeT: ChangeStrategy<MetaSource = DbT>,
1231{
1232 let (target_height, anchor_height) = wallet_db
1233 .get_target_and_anchor_heights(confirmations_policy.trusted)
1234 .map_err(|e| Error::from(InputSelectorError::DataSource(e)))?
1235 .ok_or_else(|| Error::from(InputSelectorError::SyncRequired))?;
1236
1237 let proposal = input_selector
1238 .propose_shielding(
1239 params,
1240 wallet_db,
1241 change_strategy,
1242 shielding_threshold,
1243 from_addrs,
1244 to_account,
1245 target_height,
1246 anchor_height,
1247 &wallet_db.pool_migration_params(params),
1248 confirmations_policy,
1249 output_filter,
1250 )
1251 .map_err(Error::from)?;
1252
1253 proposal.check_transaction_size()?;
1254
1255 if let Some(request) = lock_inputs {
1256 let lock_expiry_height = target_height + request.for_blocks();
1257 lock_proposal_inputs(wallet_db, &proposal, request.owner(), lock_expiry_height)?;
1258 }
1259
1260 Ok(proposal)
1261}
1262
1263#[cfg(feature = "transparent-inputs")]
1267pub type ProposeShieldingCoinbaseErrT<DbT, CommitmentTreeErrT, InputsT, FeeRuleT> = Error<
1268 <DbT as WalletRead>::Error,
1269 CommitmentTreeErrT,
1270 <InputsT as ShieldingSelector>::Error,
1271 <FeeRuleT as FeeRule>::Error,
1272 <FeeRuleT as FeeRule>::Error,
1273 Infallible,
1274>;
1275
1276#[cfg(feature = "transparent-inputs")]
1309#[allow(clippy::too_many_arguments)]
1310#[allow(clippy::type_complexity)]
1311pub fn propose_shielding_coinbase<DbT, ParamsT, InputsT, FeeRuleT, CommitmentTreeErrT>(
1312 wallet_db: &mut DbT,
1313 params: &ParamsT,
1314 input_selector: &InputsT,
1315 fee_rule: &FeeRuleT,
1316 shielding_threshold: Zatoshis,
1317 from_addrs: &[TransparentAddress],
1318 to_address: ZcashAddress,
1319 memo: Option<MemoBytes>,
1320 limit: Option<usize>,
1321 lock_inputs: Option<LockRequest>,
1322) -> Result<
1323 Proposal<FeeRuleT, Infallible>,
1324 ProposeShieldingCoinbaseErrT<DbT, CommitmentTreeErrT, InputsT, FeeRuleT>,
1325>
1326where
1327 ParamsT: consensus::Parameters,
1328 DbT: WalletWrite + InputSource<Error = <DbT as WalletRead>::Error>,
1329 InputsT: ShieldingSelector<InputSource = DbT>,
1330 FeeRuleT: FeeRule + Clone,
1331{
1332 let (target_height, anchor_height) = wallet_db
1333 .get_target_and_anchor_heights(ConfirmationsPolicy::default().trusted)
1334 .map_err(|e| Error::from(InputSelectorError::DataSource(e)))?
1335 .ok_or_else(|| Error::from(InputSelectorError::SyncRequired))?;
1336
1337 let proposal = input_selector
1338 .propose_shielding_coinbase(
1339 params,
1340 wallet_db,
1341 fee_rule,
1342 shielding_threshold,
1343 from_addrs,
1344 to_address,
1345 memo,
1346 limit,
1347 target_height,
1348 anchor_height,
1349 )
1350 .map_err(Error::from)?;
1351
1352 if let Some(request) = lock_inputs {
1353 let lock_expiry_height = target_height + request.for_blocks();
1354 lock_proposal_inputs(wallet_db, &proposal, request.owner(), lock_expiry_height)?;
1355 }
1356
1357 Ok(proposal)
1358}
1359
1360struct StepResult<AccountId> {
1361 build_result: BuildResult,
1362 outputs: Vec<SentTransactionOutput<AccountId>>,
1363 fee_amount: Zatoshis,
1364 #[cfg(feature = "transparent-inputs")]
1365 utxos_spent: Vec<OutPoint>,
1366}
1367
1368pub struct SpendingKeys {
1378 usk: UnifiedSpendingKey,
1379 #[cfg(feature = "transparent-key-import")]
1380 standalone_transparent_keys: HashMap<TransparentAddress, Vec<secp256k1::SecretKey>>,
1381}
1382
1383impl SpendingKeys {
1384 pub fn new(
1386 usk: UnifiedSpendingKey,
1387 #[cfg(feature = "transparent-key-import")] standalone_transparent_keys: HashMap<
1388 TransparentAddress,
1389 Vec<secp256k1::SecretKey>,
1390 >,
1391 ) -> Self {
1392 Self {
1393 usk,
1394 #[cfg(feature = "transparent-key-import")]
1395 standalone_transparent_keys,
1396 }
1397 }
1398
1399 pub fn from_unified_spending_key(usk: UnifiedSpendingKey) -> Self {
1402 Self {
1403 usk,
1404 #[cfg(feature = "transparent-key-import")]
1405 standalone_transparent_keys: HashMap::new(),
1406 }
1407 }
1408}
1409
1410#[cfg(feature = "transparent-key-import")]
1411impl Drop for SpendingKeys {
1412 fn drop(&mut self) {
1413 for secret_key in self.standalone_transparent_keys.values_mut().flatten() {
1414 secret_key.non_secure_erase();
1415 }
1416 }
1417}
1418
1419#[allow(clippy::too_many_arguments)]
1441#[allow(clippy::type_complexity)]
1442pub fn create_proposed_transactions<DbT, ParamsT, InputsErrT, FeeRuleT, ChangeErrT, N>(
1443 wallet_db: &mut DbT,
1444 params: &ParamsT,
1445 clock: &impl Clock,
1446 rng: &mut impl CryptoRng,
1447 spend_prover: &impl SpendProver,
1448 output_prover: &impl OutputProver,
1449 spending_keys: &SpendingKeys,
1450 ovk_policy: OvkPolicy,
1451 proposal: &Proposal<FeeRuleT, N>,
1452 expiry_height: Option<BlockHeight>,
1453) -> Result<NonEmpty<TxId>, CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>>
1454where
1455 DbT: WalletWrite + WalletCommitmentTrees,
1456 ParamsT: consensus::Parameters + Clone,
1457 FeeRuleT: FeeRule,
1458{
1459 let proposed_version = proposal.proposed_version();
1462
1463 proposal.check_transaction_size()?;
1467
1468 if let Some(expiry_height) = expiry_height {
1469 let min_target_height = BlockHeight::from(proposal.min_target_height());
1470 if expiry_height != consensus::H0 && expiry_height < min_target_height {
1471 return Err(Error::ExpiryHeightBelowTargetHeight {
1472 expiry_height,
1473 min_target_height,
1474 });
1475 }
1476 }
1477
1478 #[cfg(feature = "transparent-inputs")]
1482 let mut unused_transparent_outputs = HashMap::new();
1483
1484 let account_id = wallet_db
1485 .get_account_for_ufvk(&spending_keys.usk.to_unified_full_viewing_key())
1486 .map_err(Error::DataSource)?
1487 .ok_or(Error::KeyNotRecognized)?
1488 .id();
1489
1490 let now = clock.now();
1491
1492 let mut step_results = Vec::with_capacity(proposal.steps().len());
1493 for step in proposal.steps() {
1494 let step_result: StepResult<_> = create_proposed_transaction(
1495 wallet_db,
1496 params,
1497 &mut *rng,
1498 spend_prover,
1499 output_prover,
1500 spending_keys,
1501 account_id,
1502 ovk_policy.clone(),
1503 proposal.fee_rule(),
1504 proposal.min_target_height(),
1505 proposal.confirmations_policy(),
1506 &step_results,
1507 step,
1508 #[cfg(feature = "transparent-inputs")]
1509 &mut unused_transparent_outputs,
1510 proposed_version,
1511 expiry_height,
1512 now,
1513 )?;
1514 step_results.push((step, step_result));
1515 }
1516
1517 #[cfg(feature = "transparent-inputs")]
1519 for so in unused_transparent_outputs.into_keys() {
1520 if let StepOutputIndex::Change(i) = so.output_index() {
1521 if step_results[so.step_index()].0.balance().proposed_change()[i].is_ephemeral() {
1523 return Err(ProposalError::EphemeralOutputLeftUnspent(so).into());
1524 }
1525 }
1526 }
1527
1528 let created = time::OffsetDateTime::from(now);
1529
1530 let mut transactions = Vec::with_capacity(step_results.len());
1534 let mut txids = Vec::with_capacity(step_results.len());
1535 #[allow(unused_variables)]
1536 for (_, step_result) in step_results.iter() {
1537 let tx = step_result.build_result.transaction();
1538 transactions.push(SentTransaction::new(
1539 tx,
1540 created,
1541 proposal.min_target_height(),
1542 account_id,
1543 &step_result.outputs,
1544 step_result.fee_amount,
1545 #[cfg(feature = "transparent-inputs")]
1546 &step_result.utxos_spent,
1547 ));
1548 txids.push(tx.txid());
1549 }
1550
1551 wallet_db
1552 .store_transactions_to_be_sent(&transactions)
1553 .map_err(Error::DataSource)?;
1554
1555 Ok(NonEmpty::from_vec(txids).expect("proposal.steps is NonEmpty"))
1556}
1557
1558#[derive(Debug, Clone)]
1561enum ShieldedBuildRecipient<AccountId> {
1562 External {
1563 recipient_address: ZcashAddress,
1564 output_pool: PoolType,
1565 },
1566 InternalShielded {
1567 receiving_account: AccountId,
1568 external_address: Option<ZcashAddress>,
1569 },
1570}
1571
1572impl<AccountId> ShieldedBuildRecipient<AccountId> {
1573 fn into_recipient(self, note: impl FnOnce() -> Note) -> Recipient<AccountId> {
1574 match self {
1575 ShieldedBuildRecipient::External {
1576 recipient_address,
1577 output_pool,
1578 } => Recipient::External {
1579 recipient_address,
1580 output_pool,
1581 },
1582 ShieldedBuildRecipient::InternalShielded {
1583 receiving_account,
1584 external_address,
1585 } => Recipient::InternalShielded {
1586 receiving_account,
1587 external_address,
1588 note: Box::new(note()),
1589 },
1590 }
1591 }
1592}
1593
1594#[derive(Debug, Clone)]
1598enum TransparentBuildRecipient<AccountId> {
1599 External {
1600 recipient_address: ZcashAddress,
1601 output_pool: PoolType,
1602 },
1603 #[cfg(feature = "transparent-inputs")]
1604 EphemeralTransparent {
1605 receiving_account: AccountId,
1606 ephemeral_address: TransparentAddress,
1607 },
1608 #[cfg(feature = "transparent-inputs")]
1609 InternalTransparent {
1610 receiving_account: AccountId,
1611 recipient_address: TransparentAddress,
1612 },
1613 #[cfg(not(feature = "transparent-inputs"))]
1619 #[doc(hidden)]
1620 #[allow(dead_code)]
1621 Unconstructible(
1622 core::marker::PhantomData<AccountId>,
1623 core::convert::Infallible,
1624 ),
1625}
1626
1627impl<AccountId> TransparentBuildRecipient<AccountId> {
1628 fn into_recipient(
1629 self,
1630 #[cfg(feature = "transparent-inputs")] outpoint: OutPoint,
1631 ) -> Recipient<AccountId> {
1632 match self {
1633 TransparentBuildRecipient::External {
1634 recipient_address,
1635 output_pool,
1636 } => Recipient::External {
1637 recipient_address,
1638 output_pool,
1639 },
1640 #[cfg(feature = "transparent-inputs")]
1641 TransparentBuildRecipient::EphemeralTransparent {
1642 receiving_account,
1643 ephemeral_address,
1644 } => Recipient::EphemeralTransparent {
1645 receiving_account,
1646 ephemeral_address,
1647 outpoint,
1648 },
1649 #[cfg(feature = "transparent-inputs")]
1650 TransparentBuildRecipient::InternalTransparent {
1651 receiving_account,
1652 recipient_address,
1653 } => Recipient::InternalTransparent {
1654 receiving_account,
1655 recipient_address,
1656 },
1657 #[cfg(not(feature = "transparent-inputs"))]
1658 TransparentBuildRecipient::Unconstructible(_, absurd) => match absurd {},
1659 }
1660 }
1661}
1662
1663#[allow(clippy::type_complexity)]
1664struct BuildState<P, AccountId> {
1665 #[cfg(feature = "transparent-inputs")]
1666 step_index: usize,
1667 builder: Builder<P, ()>,
1668 #[cfg(feature = "transparent-inputs")]
1669 transparent_input_addresses: HashMap<TransparentAddress, TransparentAddressMetadata>,
1670 #[cfg(feature = "orchard")]
1671 orchard_output_meta: Vec<(
1672 ShieldedBuildRecipient<AccountId>,
1673 Zatoshis,
1674 Option<MemoBytes>,
1675 )>,
1676 #[cfg(feature = "orchard")]
1677 ironwood_output_meta: Vec<(
1678 ShieldedBuildRecipient<AccountId>,
1679 Zatoshis,
1680 Option<MemoBytes>,
1681 )>,
1682 sapling_output_meta: Vec<(
1683 ShieldedBuildRecipient<AccountId>,
1684 Zatoshis,
1685 Option<MemoBytes>,
1686 )>,
1687 transparent_output_meta: Vec<(
1688 TransparentBuildRecipient<AccountId>,
1689 TransparentAddress,
1690 Zatoshis,
1691 StepOutputIndex,
1692 )>,
1693 #[cfg(feature = "transparent-inputs")]
1694 utxos_spent: Vec<OutPoint>,
1695}
1696
1697fn check_recipient_expiry(
1707 ua: &UnifiedAddress,
1708 min_target_height: TargetHeight,
1709 tx_expiry_height: Option<BlockHeight>,
1710 now: SystemTime,
1711) -> Result<(), AddressExpiryError> {
1712 if let Some(address_expiry) = ua.expiry_height() {
1713 let chain_height = min_target_height.saturating_sub(1);
1714 if chain_height > address_expiry {
1715 return Err(AddressExpiryError::HeightExpired {
1716 address_expiry,
1717 chain_height,
1718 });
1719 }
1720
1721 match tx_expiry_height {
1722 None => {
1723 return Err(AddressExpiryError::TransactionExpiryDisabled { address_expiry });
1724 }
1725 Some(transaction_expiry) if transaction_expiry > address_expiry => {
1726 return Err(AddressExpiryError::ExpiryHeightConflict {
1727 address_expiry,
1728 transaction_expiry,
1729 });
1730 }
1731 Some(_) => {}
1732 }
1733 }
1734
1735 if let Some(expiry_time) = ua.expiry_time() {
1736 let now_secs = now
1739 .duration_since(SystemTime::UNIX_EPOCH)
1740 .map(|d| d.as_secs())
1741 .unwrap_or(0);
1742 if now_secs > expiry_time {
1743 return Err(AddressExpiryError::TimeExpired { expiry_time });
1744 }
1745 }
1746
1747 Ok(())
1748}
1749
1750#[allow(clippy::too_many_arguments)]
1754#[allow(clippy::type_complexity)]
1755fn build_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, ChangeErrT, N>(
1756 wallet_db: &mut DbT,
1757 params: &ParamsT,
1758 ufvk: &UnifiedFullViewingKey,
1759 account_id: <DbT as WalletRead>::AccountId,
1760 ovk_policy: OvkPolicy,
1761 min_target_height: TargetHeight,
1762 confirmations_policy: ConfirmationsPolicy,
1763 prior_step_results: &[(&Step<N>, StepResult<<DbT as WalletRead>::AccountId>)],
1764 proposal_step: &Step<N>,
1765 #[cfg(feature = "transparent-inputs")] unused_transparent_outputs: &mut HashMap<
1766 StepOutput,
1767 (TransparentAddress, OutPoint),
1768 >,
1769 proposed_version: Option<TxVersion>,
1770 orchard_pool_padding: BundlePadding,
1774 expiry_height: Option<BlockHeight>,
1777 now: SystemTime,
1779) -> Result<
1780 BuildState<ParamsT, <DbT as WalletRead>::AccountId>,
1781 CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>,
1782>
1783where
1784 DbT: WalletWrite + WalletCommitmentTrees,
1785 ParamsT: consensus::Parameters + Clone,
1786 FeeRuleT: FeeRule,
1787{
1788 #[cfg(feature = "transparent-inputs")]
1789 let step_index = prior_step_results.len();
1790
1791 #[allow(clippy::never_loop)]
1794 for input_ref in proposal_step.prior_step_inputs() {
1795 let (prior_step, _) = prior_step_results
1796 .get(input_ref.step_index())
1797 .ok_or(ProposalError::ReferenceError(*input_ref))?;
1798
1799 #[allow(unused_variables)]
1800 let output_pool = match input_ref.output_index() {
1801 StepOutputIndex::Payment(i) => prior_step.payment_pools().get(&i).cloned(),
1802 StepOutputIndex::Change(i) => match prior_step.balance().proposed_change().get(i) {
1803 Some(change) if !change.is_ephemeral() => {
1804 return Err(ProposalError::SpendsChange(*input_ref).into());
1805 }
1806 other => other.map(|change| change.output_pool()),
1807 },
1808 }
1809 .ok_or(ProposalError::ReferenceError(*input_ref))?;
1810
1811 #[cfg(feature = "transparent-inputs")]
1813 if output_pool != PoolType::TRANSPARENT {
1814 return Err(Error::ProposalNotSupported);
1815 }
1816 #[cfg(not(feature = "transparent-inputs"))]
1817 return Err(Error::ProposalNotSupported);
1818 }
1819
1820 let anchor_height = proposal_step
1827 .anchor_height()
1828 .unwrap_or_else(|| confirmations_policy.anchor_height(min_target_height));
1829
1830 let (sapling_anchor, sapling_inputs) = if proposal_step
1831 .involves(PoolType::Shielded(ShieldedPool::Sapling))
1832 {
1833 wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _, _, _>>(|sapling_tree| {
1834 let anchor = sapling_tree
1835 .root_at_checkpoint_id(&anchor_height)?
1836 .ok_or(ProposalError::AnchorNotFound(anchor_height))?
1837 .into();
1838
1839 let sapling_inputs = proposal_step
1840 .shielded_inputs()
1841 .map(|inputs| {
1842 inputs
1843 .notes()
1844 .iter()
1845 .filter_map(|selected| match selected.note() {
1846 Note::Sapling(note) => sapling_tree
1847 .witness_at_checkpoint_id_caching(
1848 selected.note_commitment_tree_position(),
1849 &anchor_height,
1850 )
1851 .and_then(|witness| {
1852 witness
1853 .ok_or(ShardTreeError::Query(QueryError::CheckpointPruned))
1854 })
1855 .map(|merkle_path| {
1856 Some((selected.spending_key_scope(), note, merkle_path))
1857 })
1858 .map_err(Error::from)
1859 .transpose(),
1860 #[cfg(feature = "orchard")]
1861 Note::Orchard { .. } => None,
1862 })
1863 .collect::<Result<Vec<_>, Error<_, _, _, _, _, _>>>()
1864 })
1865 .transpose()?
1866 .unwrap_or_default();
1867
1868 Ok((Some(anchor), sapling_inputs))
1869 })?
1870 } else {
1871 (None, vec![])
1872 };
1873
1874 #[cfg(feature = "orchard")]
1875 let (orchard_anchor, orchard_inputs) = if proposal_step
1876 .involves(PoolType::Shielded(ShieldedPool::Orchard))
1877 {
1878 wallet_db.with_orchard_tree_mut::<_, _, Error<_, _, _, _, _, _>>(|orchard_tree| {
1879 let anchor = orchard_tree
1880 .root_at_checkpoint_id(&anchor_height)?
1881 .ok_or(ProposalError::AnchorNotFound(anchor_height))?
1882 .into();
1883
1884 let orchard_inputs = proposal_step
1885 .shielded_inputs()
1886 .map(|inputs| {
1887 inputs
1888 .notes()
1889 .iter()
1890 .filter_map(|selected| match selected.note() {
1891 Note::Orchard {
1892 note,
1893 pool: orchard::ValuePool::Orchard,
1894 } => orchard_tree
1895 .witness_at_checkpoint_id_caching(
1896 selected.note_commitment_tree_position(),
1897 &anchor_height,
1898 )
1899 .and_then(|witness| {
1900 witness
1901 .ok_or(ShardTreeError::Query(QueryError::CheckpointPruned))
1902 })
1903 .map(|merkle_path| Some((note, merkle_path.into())))
1904 .map_err(Error::from)
1905 .transpose(),
1906 _ => None,
1907 })
1908 .collect::<Result<Vec<_>, Error<_, _, _, _, _, _>>>()
1909 })
1910 .transpose()?
1911 .unwrap_or_default();
1912
1913 Ok((Some(anchor), orchard_inputs))
1914 })?
1915 } else {
1916 (None, vec![])
1917 };
1918 #[cfg(not(feature = "orchard"))]
1919 let orchard_anchor = None;
1920
1921 #[cfg(feature = "orchard")]
1928 let (ironwood_anchor, ironwood_inputs) =
1929 if proposal_step.involves(PoolType::Shielded(ShieldedPool::Ironwood)) {
1930 wallet_db
1931 .with_ironwood_tree_mut::<_, _, Error<_, _, _, _, _, _>>(|ironwood_tree| {
1932 let anchor = ironwood_tree
1933 .root_at_checkpoint_id(&anchor_height)?
1934 .ok_or(ProposalError::AnchorNotFound(anchor_height))?
1935 .into();
1936
1937 let ironwood_inputs = proposal_step
1938 .shielded_inputs()
1939 .map(|inputs| {
1940 inputs
1941 .notes()
1942 .iter()
1943 .filter_map(|selected| match selected.note() {
1944 Note::Orchard {
1945 note,
1946 pool: orchard::ValuePool::Ironwood,
1947 } => ironwood_tree
1948 .witness_at_checkpoint_id_caching(
1949 selected.note_commitment_tree_position(),
1950 &anchor_height,
1951 )
1952 .and_then(|witness| {
1953 witness.ok_or(ShardTreeError::Query(
1954 QueryError::CheckpointPruned,
1955 ))
1956 })
1957 .map(|merkle_path| Some((note, merkle_path.into())))
1958 .map_err(Error::from)
1959 .transpose(),
1960 _ => None,
1961 })
1962 .collect::<Result<Vec<_>, Error<_, _, _, _, _, _>>>()
1963 })
1964 .transpose()?
1965 .unwrap_or_default();
1966
1967 Ok((Some(anchor), ironwood_inputs))
1968 })?
1969 .ok_or(Error::ProposalNotSupported)?
1970 } else {
1971 (None, vec![])
1972 };
1973
1974 #[cfg(not(feature = "orchard"))]
1975 let ironwood_anchor = None;
1976
1977 #[cfg(feature = "orchard")]
1986 let ironwood_padding = proposal_step.ironwood_bundle_padding();
1987 #[cfg(not(feature = "orchard"))]
1988 let ironwood_padding = orchard_pool_padding;
1989
1990 let mut builder = Builder::new(
1992 params.clone(),
1993 BlockHeight::from(min_target_height),
1994 BuildConfig::Standard {
1995 sapling_anchor,
1996 orchard_anchor,
1997 ironwood_anchor,
1998 orchard_padding: orchard_pool_padding,
1999 ironwood_padding,
2000 },
2001 );
2002 #[cfg(feature = "orchard")]
2009 let expiry_height = {
2010 if step_is_canonical_crossing(wallet_db, params, proposal_step, min_target_height) {
2011 match expiry_height {
2012 Some(requested) => {
2013 return Err(Error::ExpiryHeightConflictsWithCanonicalCrossing { requested });
2014 }
2015 None => Some(zcash_protocol::zip318::expiry_height(
2016 &zcash_protocol::zip318::BlockSpacing::for_network(params),
2017 min_target_height.into(),
2018 )),
2019 }
2020 } else {
2021 expiry_height
2022 }
2023 };
2024
2025 if let Some(expiry_height) = expiry_height {
2026 builder = builder.with_expiry_height(expiry_height);
2027 }
2028
2029 let tx_expiry_height = Some(builder.expiry_height()).filter(|h| *h != consensus::H0);
2031
2032 if let Some(version) = proposed_version {
2033 builder.propose_version(version)?;
2034 }
2035
2036 #[cfg(all(feature = "transparent-inputs", not(feature = "orchard")))]
2037 let has_shielded_inputs = !sapling_inputs.is_empty();
2038 #[cfg(all(feature = "transparent-inputs", feature = "orchard"))]
2039 let has_shielded_inputs =
2040 !(sapling_inputs.is_empty() && orchard_inputs.is_empty() && ironwood_inputs.is_empty());
2041
2042 let input_sources = NonEmpty::from_vec({
2043 let mut sources = vec![];
2044 if !sapling_inputs.is_empty() {
2045 sources.push(PoolType::SAPLING);
2046 }
2047 #[cfg(feature = "orchard")]
2048 if !orchard_inputs.is_empty() || !ironwood_inputs.is_empty() {
2049 sources.push(PoolType::ORCHARD);
2050 }
2051 #[cfg(feature = "transparent-inputs")]
2054 if !(proposal_step.transparent_inputs().is_empty()
2055 && proposal_step.prior_step_inputs().is_empty())
2056 {
2057 sources.push(PoolType::Transparent);
2058 }
2059 sources
2060 })
2061 .ok_or(Error::ProposalNotSupported)?;
2062
2063 for (_sapling_key_scope, sapling_note, merkle_path) in sapling_inputs.into_iter() {
2064 let key = match _sapling_key_scope {
2065 Scope::External => ufvk.sapling().map(|k| k.fvk().clone()),
2066 Scope::Internal => ufvk.sapling().map(|k| k.to_internal_fvk()),
2067 };
2068
2069 builder.add_sapling_spend(
2070 key.ok_or(Error::KeyNotAvailable(PoolType::SAPLING))?,
2071 sapling_note.clone(),
2072 merkle_path,
2073 )?;
2074 }
2075
2076 #[cfg(feature = "orchard")]
2079 for (orchard_note, merkle_path) in orchard_inputs.into_iter() {
2080 builder.add_orchard_spend(
2081 ufvk.orchard()
2082 .cloned()
2083 .ok_or(Error::KeyNotAvailable(PoolType::ORCHARD))?,
2084 *orchard_note,
2085 merkle_path,
2086 )?;
2087 }
2088
2089 #[cfg(feature = "orchard")]
2090 for (ironwood_note, merkle_path) in ironwood_inputs.into_iter() {
2091 builder.add_ironwood_spend(
2092 ufvk.orchard()
2093 .cloned()
2094 .ok_or(Error::KeyNotAvailable(PoolType::ORCHARD))?,
2095 *ironwood_note,
2096 merkle_path,
2097 )?;
2098 }
2099
2100 #[cfg(feature = "transparent-inputs")]
2101 let mut transparent_input_addresses =
2102 HashMap::<TransparentAddress, TransparentAddressMetadata>::new();
2103
2104 #[cfg(feature = "transparent-inputs")]
2105 let mut metadata_from_address = |addr: &TransparentAddress| -> Result<
2106 TransparentAddressMetadata,
2107 CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>,
2108 > {
2109 match transparent_input_addresses.get(addr) {
2110 Some(result) => Ok(result.clone()),
2111 None => {
2112 let result = wallet_db
2121 .get_transparent_address_metadata(account_id, addr)
2122 .map_err(InputSelectorError::DataSource)?
2123 .ok_or(Error::AddressNotRecognized(*addr))?;
2124
2125 transparent_input_addresses.insert(*addr, result.clone());
2126 Ok(result)
2127 }
2128 }
2129 };
2130
2131 #[cfg(feature = "transparent-inputs")]
2132 let utxos_spent = {
2133 let mut utxos_spent: Vec<OutPoint> = vec![];
2134 let mut add_transparent_input =
2135 |builder: &mut Builder<_, _>,
2136 utxos_spent: &mut Vec<_>,
2137 recipient_address: &TransparentAddress,
2138 outpoint: OutPoint,
2139 txout: TxOut|
2140 -> Result<(), CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>> {
2141 let metadata = metadata_from_address(recipient_address)?;
2142 match metadata.source() {
2143 TransparentAddressSource::Derived {
2144 scope,
2145 address_index,
2146 } => {
2147 let pubkey = ufvk
2148 .p2pkh()
2149 .ok_or(Error::KeyNotAvailable(PoolType::Transparent))?
2150 .derive_address_pubkey(*scope, *address_index)
2151 .expect("spending key derivation should not fail");
2152 utxos_spent.push(outpoint.clone());
2153 builder.add_transparent_p2pkh_input(pubkey, outpoint, txout)?;
2154 }
2155 #[cfg(feature = "transparent-key-import")]
2156 TransparentAddressSource::StandalonePubkey(pubkey) => {
2157 utxos_spent.push(outpoint.clone());
2158 builder.add_transparent_p2pkh_input(*pubkey, outpoint, txout)?;
2159 }
2160 #[cfg(feature = "transparent-key-import")]
2161 TransparentAddressSource::StandaloneScript(redeem_script) => {
2162 let from_chain =
2163 zs_script::FromChain::parse(&zs_script::Code(redeem_script.to_bytes()))
2164 .map_err(|_| ::transparent::builder::Error::UnsupportedScript)?;
2165 utxos_spent.push(outpoint.clone());
2166 builder.add_transparent_p2sh_input(from_chain, outpoint, txout)?;
2167 }
2168 #[cfg(feature = "transparent-key-import")]
2171 TransparentAddressSource::StandaloneAddress => {
2172 return Err(Error::KeyNotAvailable(PoolType::Transparent));
2173 }
2174 }
2175
2176 Ok(())
2177 };
2178
2179 for utxo in proposal_step.transparent_inputs() {
2180 add_transparent_input(
2181 &mut builder,
2182 &mut utxos_spent,
2183 utxo.recipient_address(),
2184 utxo.outpoint().clone(),
2185 utxo.txout().clone(),
2186 )?;
2187 }
2188 for input_ref in proposal_step.prior_step_inputs() {
2189 let (address, outpoint) = unused_transparent_outputs
2192 .remove(input_ref)
2193 .ok_or(Error::Proposal(ProposalError::ReferenceError(*input_ref)))?;
2194
2195 let txout = &prior_step_results[input_ref.step_index()]
2196 .1
2197 .build_result
2198 .transaction()
2199 .transparent_bundle()
2200 .ok_or(ProposalError::ReferenceError(*input_ref))?
2201 .vout[outpoint.n() as usize];
2202
2203 add_transparent_input(
2204 &mut builder,
2205 &mut utxos_spent,
2206 &address,
2207 outpoint,
2208 txout.clone(),
2209 )?;
2210 }
2211 utxos_spent
2212 };
2213
2214 let (external_ovk, internal_ovk) = match ovk_policy {
2215 OvkPolicy::Sender => (
2216 Some(
2217 ufvk.select_ovk(zip32::Scope::External, &input_sources)
2218 .ok_or(Error::KeyNotAvailable(input_sources.head))?,
2219 ),
2220 None,
2221 ),
2222 OvkPolicy::Custom {
2223 external_ovk,
2224 internal_ovk,
2225 } => (Some(external_ovk), internal_ovk),
2226 OvkPolicy::Discard => (None, None),
2227 };
2228
2229 #[cfg(feature = "orchard")]
2230 let mut orchard_output_meta: Vec<(ShieldedBuildRecipient<_>, Zatoshis, Option<MemoBytes>)> =
2231 vec![];
2232 #[cfg(feature = "orchard")]
2233 let mut ironwood_output_meta: Vec<(
2234 ShieldedBuildRecipient<_>,
2235 Zatoshis,
2236 Option<MemoBytes>,
2237 )> = vec![];
2238 let mut sapling_output_meta: Vec<(ShieldedBuildRecipient<_>, Zatoshis, Option<MemoBytes>)> =
2239 vec![];
2240 let mut transparent_output_meta: Vec<(
2241 TransparentBuildRecipient<_>,
2242 TransparentAddress,
2243 Zatoshis,
2244 StepOutputIndex,
2245 )> = vec![];
2246
2247 for (&payment_index, output_pool) in proposal_step.payment_pools() {
2248 let payment = proposal_step
2249 .transaction_request()
2250 .payments()
2251 .get(&payment_index)
2252 .expect(
2253 "The mapping between payment index and payment is checked in step construction",
2254 );
2255 let payment_amount = payment
2256 .amount()
2257 .ok_or(ProposalError::PaymentAmountMissing(payment_index))?;
2258 let recipient_address = payment.recipient_address();
2259
2260 let add_sapling_output =
2261 |builder: &mut Builder<_, _>,
2262 sapling_output_meta: &mut Vec<_>,
2263 to: sapling::PaymentAddress|
2264 -> Result<(), CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>> {
2265 let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
2266 builder.add_sapling_output(
2267 external_ovk.map(|k| k.into()),
2268 to,
2269 payment_amount,
2270 memo.clone(),
2271 )?;
2272 sapling_output_meta.push((
2273 ShieldedBuildRecipient::External {
2274 recipient_address: recipient_address.clone(),
2275 output_pool: PoolType::SAPLING,
2276 },
2277 payment_amount,
2278 Some(memo),
2279 ));
2280 Ok(())
2281 };
2282
2283 #[cfg(feature = "orchard")]
2284 let add_orchard_output =
2285 |builder: &mut Builder<_, _>,
2286 orchard_output_meta: &mut Vec<_>,
2287 to: orchard::Address|
2288 -> Result<(), CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>> {
2289 let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
2290 builder.add_orchard_output(
2291 external_ovk.map(|k| k.into()),
2292 to,
2293 payment_amount,
2294 memo.clone(),
2295 )?;
2296 orchard_output_meta.push((
2297 ShieldedBuildRecipient::External {
2298 recipient_address: recipient_address.clone(),
2299 output_pool: PoolType::ORCHARD,
2300 },
2301 payment_amount,
2302 Some(memo),
2303 ));
2304 Ok(())
2305 };
2306
2307 #[cfg(feature = "orchard")]
2308 let add_ironwood_output =
2309 |builder: &mut Builder<_, _>,
2310 ironwood_output_meta: &mut Vec<_>,
2311 to: orchard::Address|
2312 -> Result<(), CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>> {
2313 let memo = payment.memo().map_or_else(MemoBytes::empty, |m| m.clone());
2314 builder.add_ironwood_output(
2315 external_ovk.map(|k| k.into()),
2316 to,
2317 payment_amount,
2318 memo.clone(),
2319 )?;
2320 ironwood_output_meta.push((
2321 ShieldedBuildRecipient::External {
2322 recipient_address: recipient_address.clone(),
2323 output_pool: PoolType::IRONWOOD,
2327 },
2328 payment_amount,
2329 Some(memo),
2330 ));
2331 Ok(())
2332 };
2333
2334 let add_transparent_output =
2335 |builder: &mut Builder<_, _>,
2336 transparent_output_meta: &mut Vec<_>,
2337 to: TransparentAddress|
2338 -> Result<(), CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>> {
2339 if payment.memo().is_some() {
2340 return Err(Error::Payment(zip321::PaymentError::TransparentMemo));
2341 }
2342 builder.add_transparent_output(&to, payment_amount)?;
2343 transparent_output_meta.push((
2344 TransparentBuildRecipient::External {
2345 recipient_address: recipient_address.clone(),
2346 output_pool: PoolType::TRANSPARENT,
2347 },
2348 to,
2349 payment_amount,
2350 StepOutputIndex::Payment(payment_index),
2351 ));
2352 Ok(())
2353 };
2354
2355 let recipient = recipient_address
2356 .clone()
2357 .convert_if_network(params.network_type())?;
2358
2359 if let Address::Unified(ua) = &recipient {
2360 check_recipient_expiry(ua, min_target_height, tx_expiry_height, now).map_err(
2361 |error| Error::RecipientAddressExpiry {
2362 payment_index,
2363 error,
2364 },
2365 )?;
2366 }
2367
2368 match recipient {
2369 Address::Unified(ua) => match output_pool {
2370 #[cfg(not(feature = "orchard"))]
2371 PoolType::Shielded(ShieldedPool::Orchard) => {
2372 return Err(Error::ProposalNotSupported);
2373 }
2374 #[cfg(feature = "orchard")]
2375 PoolType::Shielded(ShieldedPool::Orchard) => {
2376 let to = *ua.orchard().expect("The mapping between payment pool and receiver is checked in step construction");
2380 add_orchard_output(&mut builder, &mut orchard_output_meta, to)?;
2381 }
2382 PoolType::Shielded(ShieldedPool::Sapling) => {
2383 let to = *ua.sapling().expect("The mapping between payment pool and receiver is checked in step construction");
2384 add_sapling_output(&mut builder, &mut sapling_output_meta, to)?;
2385 }
2386 PoolType::Transparent => {
2387 let to = *ua.transparent().expect("The mapping between payment pool and receiver is checked in step construction");
2388 add_transparent_output(&mut builder, &mut transparent_output_meta, to)?;
2389 }
2390 #[cfg(feature = "orchard")]
2391 PoolType::Shielded(ShieldedPool::Ironwood) => {
2392 let to = *ua.orchard().expect("The mapping between payment pool and receiver is checked in step construction");
2395 add_ironwood_output(&mut builder, &mut ironwood_output_meta, to)?;
2396 }
2397 #[cfg(not(feature = "orchard"))]
2398 PoolType::Shielded(ShieldedPool::Ironwood) => {
2399 return Err(Error::ProposalNotSupported);
2400 }
2401 },
2402 Address::Sapling(to) => {
2403 add_sapling_output(&mut builder, &mut sapling_output_meta, *to)?;
2404 }
2405 Address::Transparent(to) => {
2406 add_transparent_output(&mut builder, &mut transparent_output_meta, to)?;
2407 }
2408 #[cfg(not(feature = "transparent-inputs"))]
2409 Address::Tex(_) => {
2410 return Err(Error::ProposalNotSupported);
2411 }
2412 #[cfg(feature = "transparent-inputs")]
2413 Address::Tex(data) => {
2414 if has_shielded_inputs {
2415 return Err(ProposalError::PaysTexFromShielded.into());
2416 }
2417 let to = TransparentAddress::PublicKeyHash(data);
2418 add_transparent_output(&mut builder, &mut transparent_output_meta, to)?;
2419 }
2420 }
2421 }
2422
2423 for change_value in proposal_step.balance().proposed_change() {
2424 let memo = change_value
2425 .memo()
2426 .map_or_else(MemoBytes::empty, |m| m.clone());
2427 let output_pool = change_value.output_pool();
2428 match output_pool {
2429 PoolType::Shielded(ShieldedPool::Sapling) => {
2430 builder.add_sapling_output(
2431 internal_ovk.map(|k| k.into()),
2432 ufvk.sapling()
2433 .ok_or(Error::KeyNotAvailable(PoolType::SAPLING))?
2434 .change_address()
2435 .1,
2436 change_value.value(),
2437 memo.clone(),
2438 )?;
2439 sapling_output_meta.push((
2440 ShieldedBuildRecipient::InternalShielded {
2441 receiving_account: account_id,
2442 external_address: None,
2443 },
2444 change_value.value(),
2445 Some(memo),
2446 ))
2447 }
2448 PoolType::Shielded(ShieldedPool::Orchard) => {
2449 #[cfg(not(feature = "orchard"))]
2450 return Err(Error::UnsupportedChangeType(output_pool));
2451
2452 #[cfg(feature = "orchard")]
2453 {
2454 let orchard_fvk = ufvk
2455 .orchard()
2456 .ok_or(Error::KeyNotAvailable(PoolType::ORCHARD))?;
2457
2458 let change_address =
2459 orchard_fvk.address_at(0u32, orchard::keys::Scope::Internal);
2460
2461 if ironwood_active_at(params, min_target_height) {
2462 builder.add_orchard_change_output(
2468 orchard_fvk.clone(),
2469 internal_ovk.map(|k| k.into()),
2470 change_address,
2471 change_value.value(),
2472 memo.clone(),
2473 )?;
2474 orchard_output_meta.push((
2475 ShieldedBuildRecipient::InternalShielded {
2476 receiving_account: account_id,
2477 external_address: None,
2478 },
2479 change_value.value(),
2480 Some(memo),
2481 ))
2482 } else {
2483 builder.add_orchard_output(
2484 internal_ovk.map(|k| k.into()),
2485 change_address,
2486 change_value.value(),
2487 memo.clone(),
2488 )?;
2489 orchard_output_meta.push((
2490 ShieldedBuildRecipient::InternalShielded {
2491 receiving_account: account_id,
2492 external_address: None,
2493 },
2494 change_value.value(),
2495 Some(memo),
2496 ))
2497 }
2498 }
2499 }
2500 PoolType::Transparent => {
2501 #[cfg(not(feature = "transparent-inputs"))]
2505 return Err(Error::UnsupportedChangeType(output_pool));
2506 }
2507 PoolType::Shielded(ShieldedPool::Ironwood) => {
2508 #[cfg(not(feature = "orchard"))]
2509 return Err(Error::UnsupportedChangeType(output_pool));
2510
2511 #[cfg(feature = "orchard")]
2512 {
2513 let orchard_fvk = ufvk
2514 .orchard()
2515 .ok_or(Error::KeyNotAvailable(PoolType::ORCHARD))?;
2516
2517 let change_address =
2518 orchard_fvk.address_at(0u32, orchard::keys::Scope::Internal);
2519
2520 builder.add_ironwood_output(
2521 internal_ovk.map(|k| k.into()),
2522 change_address,
2523 change_value.value(),
2524 memo.clone(),
2525 )?;
2526 ironwood_output_meta.push((
2527 ShieldedBuildRecipient::InternalShielded {
2528 receiving_account: account_id,
2529 external_address: None,
2530 },
2531 change_value.value(),
2532 Some(memo),
2533 ))
2534 }
2535 }
2536 }
2537 }
2538
2539 #[cfg(feature = "transparent-inputs")]
2543 {
2544 let ephemeral_outputs: Vec<(usize, &ChangeValue)> = proposal_step
2545 .balance()
2546 .proposed_change()
2547 .iter()
2548 .enumerate()
2549 .filter(|(_, change_value)| {
2550 change_value.is_ephemeral() && change_value.output_pool() == PoolType::Transparent
2551 })
2552 .collect();
2553
2554 let addresses_and_metadata = wallet_db
2555 .reserve_next_n_ephemeral_addresses(account_id, ephemeral_outputs.len())
2556 .map_err(Error::DataSource)?;
2557 assert_eq!(addresses_and_metadata.len(), ephemeral_outputs.len());
2558
2559 for ((change_index, change_value), (ephemeral_address, _)) in
2561 ephemeral_outputs.iter().zip(addresses_and_metadata)
2562 {
2563 builder.add_transparent_output(&ephemeral_address, change_value.value())?;
2566 transparent_output_meta.push((
2567 TransparentBuildRecipient::EphemeralTransparent {
2568 receiving_account: account_id,
2569 ephemeral_address,
2570 },
2571 ephemeral_address,
2572 change_value.value(),
2573 StepOutputIndex::Change(*change_index),
2574 ))
2575 }
2576 }
2577
2578 #[cfg(feature = "transparent-inputs")]
2582 {
2583 let transparent_change_outputs: Vec<(usize, &ChangeValue)> = proposal_step
2584 .balance()
2585 .proposed_change()
2586 .iter()
2587 .enumerate()
2588 .filter(|(_, change_value)| {
2589 !change_value.is_ephemeral() && change_value.output_pool() == PoolType::Transparent
2590 })
2591 .collect();
2592
2593 if !transparent_change_outputs.is_empty() {
2594 let addresses_and_metadata = wallet_db
2595 .reserve_next_n_internal_addresses(account_id, transparent_change_outputs.len())
2596 .map_err(Error::DataSource)?;
2597 assert_eq!(
2598 addresses_and_metadata.len(),
2599 transparent_change_outputs.len()
2600 );
2601
2602 for ((change_index, change_value), (change_address, _)) in transparent_change_outputs
2603 .iter()
2604 .zip(addresses_and_metadata)
2605 {
2606 builder.add_transparent_output(&change_address, change_value.value())?;
2607 transparent_output_meta.push((
2608 TransparentBuildRecipient::InternalTransparent {
2609 receiving_account: account_id,
2610 recipient_address: change_address,
2611 },
2612 change_address,
2613 change_value.value(),
2614 StepOutputIndex::Change(*change_index),
2615 ))
2616 }
2617 }
2618 }
2619
2620 Ok(BuildState {
2621 #[cfg(feature = "transparent-inputs")]
2622 step_index,
2623 builder,
2624 #[cfg(feature = "transparent-inputs")]
2625 transparent_input_addresses,
2626 #[cfg(feature = "orchard")]
2627 orchard_output_meta,
2628 #[cfg(feature = "orchard")]
2629 ironwood_output_meta,
2630 sapling_output_meta,
2631 transparent_output_meta,
2632 #[cfg(feature = "transparent-inputs")]
2633 utxos_spent,
2634 })
2635}
2636
2637#[allow(clippy::too_many_arguments)]
2641#[allow(clippy::type_complexity)]
2642fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, ChangeErrT, N>(
2643 wallet_db: &mut DbT,
2644 params: &ParamsT,
2645 rng: &mut impl CryptoRng,
2646 spend_prover: &impl SpendProver,
2647 output_prover: &impl OutputProver,
2648 spending_keys: &SpendingKeys,
2649 account_id: <DbT as WalletRead>::AccountId,
2650 ovk_policy: OvkPolicy,
2651 fee_rule: &FeeRuleT,
2652 min_target_height: TargetHeight,
2653 confirmations_policy: ConfirmationsPolicy,
2654 prior_step_results: &[(&Step<N>, StepResult<<DbT as WalletRead>::AccountId>)],
2655 proposal_step: &Step<N>,
2656 #[cfg(feature = "transparent-inputs")] unused_transparent_outputs: &mut HashMap<
2657 StepOutput,
2658 (TransparentAddress, OutPoint),
2659 >,
2660 proposed_version: Option<TxVersion>,
2661 expiry_height: Option<BlockHeight>,
2662 now: SystemTime,
2663) -> Result<
2664 StepResult<<DbT as WalletRead>::AccountId>,
2665 CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>,
2666>
2667where
2668 DbT: WalletWrite + WalletCommitmentTrees,
2669 ParamsT: consensus::Parameters + Clone,
2670 FeeRuleT: FeeRule,
2671{
2672 let build_state = build_proposed_transaction::<_, _, _, FeeRuleT, _, _>(
2673 wallet_db,
2674 params,
2675 &spending_keys.usk.to_unified_full_viewing_key(),
2676 account_id,
2677 ovk_policy,
2678 min_target_height,
2679 confirmations_policy,
2680 prior_step_results,
2681 proposal_step,
2682 #[cfg(feature = "transparent-inputs")]
2683 unused_transparent_outputs,
2684 proposed_version,
2685 BundlePadding::DEFAULT,
2687 expiry_height,
2688 now,
2689 )?;
2690
2691 #[cfg_attr(not(feature = "transparent-inputs"), allow(unused_mut))]
2693 let mut transparent_signing_set = TransparentSigningSet::new();
2694 #[cfg(feature = "transparent-inputs")]
2695 for (_address, address_metadata) in build_state.transparent_input_addresses {
2696 match address_metadata.source() {
2697 TransparentAddressSource::Derived {
2698 scope,
2699 address_index,
2700 } => {
2701 transparent_signing_set.add_key(
2702 spending_keys
2703 .usk
2704 .transparent()
2705 .derive_secret_key(*scope, *address_index)
2706 .expect("spending key derivation should not fail"),
2707 );
2708 }
2709 #[cfg(feature = "transparent-key-import")]
2710 TransparentAddressSource::StandalonePubkey(_)
2711 | TransparentAddressSource::StandaloneScript(_) => {
2712 let keys = spending_keys
2713 .standalone_transparent_keys
2714 .get(&_address)
2715 .ok_or(Error::AddressNotRecognized(_address))?;
2716 for key in keys {
2717 transparent_signing_set.add_key(*key);
2718 }
2719 }
2720 #[cfg(feature = "transparent-key-import")]
2723 TransparentAddressSource::StandaloneAddress => {
2724 return Err(Error::AddressNotRecognized(_address));
2725 }
2726 }
2727 }
2728 let sapling_extsks = &[
2729 spending_keys.usk.sapling().clone(),
2730 spending_keys
2731 .usk
2732 .sapling()
2733 .derive_internal()
2734 .ok_or(Error::KeyNotAvailable(PoolType::SAPLING))?,
2735 ];
2736 #[cfg(feature = "orchard")]
2737 let orchard_saks = &[spending_keys.usk.orchard().into()];
2738 #[cfg(not(feature = "orchard"))]
2739 let orchard_saks = &[];
2740 let build_result = build_state.builder.build(
2741 &transparent_signing_set,
2742 sapling_extsks,
2743 orchard_saks,
2744 rng,
2745 spend_prover,
2746 output_prover,
2747 fee_rule,
2748 )?;
2749
2750 #[cfg(feature = "orchard")]
2751 let orchard_fvk: orchard::keys::FullViewingKey = spending_keys.usk.orchard().into();
2752 #[cfg(feature = "orchard")]
2753 let orchard_internal_ivk = orchard_fvk.to_ivk(orchard::keys::Scope::Internal);
2754 #[cfg(feature = "orchard")]
2758 let orchard_external_ivk = orchard_fvk.to_ivk(orchard::keys::Scope::External);
2759 #[cfg(feature = "orchard")]
2760 let orchard_outputs = build_state.orchard_output_meta.into_iter().enumerate().map(
2761 |(i, (recipient, value, memo))| {
2762 let output_index = build_result
2763 .orchard_meta()
2764 .output_action_index(i)
2765 .expect("An action should exist in the transaction for each Orchard output.");
2766
2767 let recipient = recipient.into_recipient(|| {
2768 build_result
2769 .transaction()
2770 .orchard_bundle()
2771 .and_then(|bundle| {
2772 bundle
2773 .decrypt_output_with_key(output_index, &orchard_internal_ivk)
2774 .or_else(|| {
2775 bundle.decrypt_output_with_key(output_index, &orchard_external_ivk)
2776 })
2777 .map(|(note, _, _)| Note::Orchard {
2778 note,
2779 pool: orchard::ValuePool::Orchard,
2780 })
2781 })
2782 .expect("Wallet-internal outputs must be decryptable with the wallet's IVK")
2783 });
2784
2785 SentTransactionOutput::from_parts_in_tree(
2786 Some(NoteCommitmentTree::Orchard),
2787 output_index,
2788 recipient,
2789 value,
2790 memo,
2791 )
2792 },
2793 );
2794
2795 #[cfg(feature = "orchard")]
2796 let ironwood_outputs = build_state
2797 .ironwood_output_meta
2798 .into_iter()
2799 .enumerate()
2800 .map(|(i, (recipient, value, memo))| {
2801 let raw_output_index = build_result
2802 .ironwood_meta()
2803 .output_action_index(i)
2804 .expect("An action should exist in the transaction for each Ironwood output.");
2805
2806 let recipient = recipient.into_recipient(|| {
2807 build_result
2808 .transaction()
2809 .ironwood_bundle()
2810 .and_then(|bundle| {
2811 bundle
2812 .decrypt_output_with_key(raw_output_index, &orchard_internal_ivk)
2813 .map(|(note, _, _)| Note::Orchard { note, pool: orchard::ValuePool::Ironwood })
2814 })
2815 .expect(
2816 "Wallet-internal Ironwood outputs must be decryptable with the wallet's IVK",
2817 )
2818 });
2819
2820 SentTransactionOutput::from_parts_in_tree(
2825 Some(NoteCommitmentTree::Ironwood),
2826 raw_output_index,
2827 recipient,
2828 value,
2829 memo,
2830 )
2831 });
2832
2833 let sapling_dfvk = spending_keys
2834 .usk
2835 .sapling()
2836 .to_diversifiable_full_viewing_key();
2837 let sapling_internal_ivk =
2838 PreparedIncomingViewingKey::new(&sapling_dfvk.to_ivk(Scope::Internal));
2839 let sapling_outputs = build_state.sapling_output_meta.into_iter().enumerate().map(
2840 |(i, (recipient, value, memo))| {
2841 let output_index = build_result
2842 .sapling_meta()
2843 .output_index(i)
2844 .expect("An output should exist in the transaction for each Sapling payment.");
2845
2846 let recipient = recipient.into_recipient(|| {
2847 build_result
2848 .transaction()
2849 .sapling_bundle()
2850 .and_then(|bundle| {
2851 try_sapling_note_decryption(
2852 &sapling_internal_ivk,
2853 &bundle.shielded_outputs()[output_index],
2854 zip212_enforcement(params, min_target_height.into()),
2855 )
2856 .map(|(note, _, _)| Note::Sapling(note))
2857 })
2858 .expect("Wallet-internal outputs must be decryptable with the wallet's IVK")
2859 });
2860
2861 SentTransactionOutput::from_parts_in_tree(
2862 Some(NoteCommitmentTree::Sapling),
2863 output_index,
2864 recipient,
2865 value,
2866 memo,
2867 )
2868 },
2869 );
2870
2871 let txid: [u8; 32] = build_result.transaction().txid().into();
2872 assert_eq!(
2873 build_state.transparent_output_meta.len(),
2874 build_result
2875 .transaction()
2876 .transparent_bundle()
2877 .map_or(0, |b| b.vout.len()),
2878 );
2879
2880 #[allow(unused_variables)]
2881 let transparent_outputs = build_state
2882 .transparent_output_meta
2883 .into_iter()
2884 .enumerate()
2885 .map(|(n, (recipient, address, value, step_output_index))| {
2886 let outpoint = OutPoint::new(txid, n as u32);
2891
2892 let recipient = recipient.into_recipient(
2893 #[cfg(feature = "transparent-inputs")]
2894 outpoint.clone(),
2895 );
2896
2897 #[cfg(feature = "transparent-inputs")]
2898 unused_transparent_outputs.insert(
2899 StepOutput::new(build_state.step_index, step_output_index),
2900 (address, outpoint),
2901 );
2902 SentTransactionOutput::from_parts(n, recipient, value, None)
2903 });
2904
2905 let mut outputs: Vec<SentTransactionOutput<_>> = vec![];
2906 #[cfg(feature = "orchard")]
2907 outputs.extend(orchard_outputs);
2908 #[cfg(feature = "orchard")]
2909 outputs.extend(ironwood_outputs);
2910 outputs.extend(sapling_outputs);
2911 outputs.extend(transparent_outputs);
2912
2913 Ok(StepResult {
2914 build_result,
2915 outputs,
2916 fee_amount: proposal_step.balance().fee_required(),
2917 #[cfg(feature = "transparent-inputs")]
2918 utxos_spent: build_state.utxos_spent,
2919 })
2920}
2921
2922#[allow(clippy::too_many_arguments)]
2961#[allow(clippy::type_complexity)]
2962#[cfg(feature = "pczt")]
2963pub fn create_pczt_from_proposal<DbT, ParamsT, InputsErrT, FeeRuleT, ChangeErrT, N>(
2964 wallet_db: &mut DbT,
2965 params: &ParamsT,
2966 clock: &impl Clock,
2967 rng: &mut impl CryptoRng,
2968 account_id: <DbT as WalletRead>::AccountId,
2969 ovk_policy: OvkPolicy,
2970 proposal: &Proposal<FeeRuleT, N>,
2971 expiry_height: Option<BlockHeight>,
2972 orchard_pool_padding: BundlePadding,
2973) -> Result<pczt::Pczt, CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>>
2974where
2975 DbT: WalletWrite + WalletCommitmentTrees,
2976 ParamsT: consensus::Parameters + Clone,
2977 FeeRuleT: FeeRule,
2978 <DbT as WalletRead>::AccountId: serde::Serialize,
2979{
2980 let account = wallet_db
2981 .get_account(account_id)
2982 .map_err(Error::DataSource)?
2983 .ok_or(Error::AccountIdNotRecognized)?;
2984 let ufvk = account.ufvk().ok_or(Error::AccountCannotSpend)?;
2985 let account_derivation = account.source().key_derivation();
2986
2987 if proposal.steps().len() > 1 {
2989 return Err(Error::ProposalNotSupported);
2990 }
2991
2992 proposal.check_transaction_size()?;
2994
2995 let fee_rule = proposal.fee_rule();
2996 let min_target_height = proposal.min_target_height();
2997
2998 if let Some(expiry_height) = expiry_height {
2999 let min_target_height = BlockHeight::from(min_target_height);
3000 if expiry_height != consensus::H0 && expiry_height < min_target_height {
3001 return Err(Error::ExpiryHeightBelowTargetHeight {
3002 expiry_height,
3003 min_target_height,
3004 });
3005 }
3006 }
3007
3008 let prior_step_results = &[];
3009 let proposal_step = proposal.steps().first();
3010
3011 let unused_transparent_outputs = &mut HashMap::new();
3012 let proposed_version = proposal.proposed_version();
3016
3017 let build_state = build_proposed_transaction::<_, _, _, FeeRuleT, _, _>(
3018 wallet_db,
3019 params,
3020 ufvk,
3021 account_id,
3022 ovk_policy,
3023 min_target_height,
3024 proposal.confirmations_policy(),
3025 prior_step_results,
3026 proposal_step,
3027 #[cfg(feature = "transparent-inputs")]
3028 unused_transparent_outputs,
3029 proposed_version,
3030 orchard_pool_padding,
3031 expiry_height,
3032 clock.now(),
3033 )?;
3034
3035 let build_result = build_state.builder.build_for_pczt(&mut *rng, fee_rule)?;
3039
3040 let created = Creator::build_from_parts(build_result.pczt_parts).ok_or(PcztError::Build)?;
3041
3042 let io_finalized = IoFinalizer::new(created).finalize_io(rng)?;
3043
3044 #[cfg(feature = "orchard")]
3045 let orchard_outputs = build_state
3046 .orchard_output_meta
3047 .into_iter()
3048 .enumerate()
3049 .map(|(i, (recipient, _, _))| {
3050 let output_index = build_result
3051 .orchard_meta
3052 .output_action_index(i)
3053 .expect("An action should exist in the transaction for each Orchard output.");
3054
3055 (
3056 output_index,
3057 PcztRecipient::from_shielded_recipient(recipient),
3058 )
3059 })
3060 .collect::<HashMap<_, _>>();
3061
3062 #[cfg(feature = "orchard")]
3063 let ironwood_outputs = build_state
3064 .ironwood_output_meta
3065 .into_iter()
3066 .enumerate()
3067 .map(|(i, (recipient, _, _))| {
3068 let output_index = build_result
3069 .ironwood_meta
3070 .output_action_index(i)
3071 .expect("An action should exist in the transaction for each Ironwood output.");
3072
3073 (
3074 output_index,
3075 PcztRecipient::from_shielded_recipient(recipient),
3076 )
3077 })
3078 .collect::<HashMap<_, _>>();
3079
3080 let sapling_outputs = build_state
3081 .sapling_output_meta
3082 .into_iter()
3083 .enumerate()
3084 .map(|(i, (recipient, _, _))| {
3085 let output_index = build_result
3086 .sapling_meta
3087 .output_index(i)
3088 .expect("An output should exist in the transaction for each Sapling output.");
3089
3090 (
3091 output_index,
3092 PcztRecipient::from_shielded_recipient(recipient),
3093 )
3094 })
3095 .collect::<HashMap<_, _>>();
3096
3097 let pczt = Updater::new(io_finalized)
3098 .update_global_with(|mut updater| {
3099 updater.set_proprietary(
3100 PROPRIETARY_PROPOSAL_INFO.into(),
3101 postcard::to_allocvec(&ProposalInfo::<<DbT as WalletRead>::AccountId> {
3102 from_account: account_id,
3103 target_height: proposal.min_target_height(),
3104 })
3105 .expect("postcard encoding of PCZT proposal metadata should not fail"),
3106 )
3107 })
3108 .update_orchard_with(|mut updater| {
3109 let spend_needs_derivation = updater
3115 .bundle()
3116 .actions()
3117 .iter()
3118 .map(|action| action.spend().spend_auth_sig().is_none())
3119 .collect::<Vec<_>>();
3120
3121 for (index, needs_derivation) in spend_needs_derivation.iter().enumerate() {
3122 updater.update_action_with(index, |mut action_updater| {
3123 if let Some(derivation) = account_derivation
3128 && *needs_derivation
3129 {
3130 action_updater.set_spend_zip32_derivation(
3132 orchard::pczt::Zip32Derivation::parse(
3133 derivation.seed_fingerprint().to_bytes(),
3134 vec![
3135 zip32::ChildIndex::hardened(32).index(),
3136 zip32::ChildIndex::hardened(params.network_type().coin_type())
3137 .index(),
3138 zip32::ChildIndex::hardened(u32::from(
3139 derivation.account_index(),
3140 ))
3141 .index(),
3142 ],
3143 )
3144 .expect("valid"),
3145 );
3146 }
3147
3148 if let Some((pczt_recipient, external_address)) = orchard_outputs.get(&index) {
3149 if let Some(user_address) = external_address {
3150 action_updater.set_output_user_address(user_address.encode());
3151 }
3152 action_updater.set_output_proprietary(
3153 PROPRIETARY_OUTPUT_INFO.into(),
3154 postcard::to_allocvec(pczt_recipient).expect(
3155 "postcard encoding of PCZT recipient metadata should not fail",
3156 ),
3157 );
3158 }
3159
3160 Ok(())
3161 })?;
3162 }
3163 Ok(())
3164 })?
3165 .update_ironwood_with(|mut updater| {
3166 let spend_needs_derivation = updater
3172 .bundle()
3173 .actions()
3174 .iter()
3175 .map(|action| action.spend().spend_auth_sig().is_none())
3176 .collect::<Vec<_>>();
3177
3178 for (index, needs_derivation) in spend_needs_derivation.iter().enumerate() {
3179 updater.update_action_with(index, |mut action_updater| {
3180 if let Some(derivation) = account_derivation
3186 && *needs_derivation
3187 {
3188 action_updater.set_spend_zip32_derivation(
3190 orchard::pczt::Zip32Derivation::parse(
3191 derivation.seed_fingerprint().to_bytes(),
3192 vec![
3193 zip32::ChildIndex::hardened(32).index(),
3194 zip32::ChildIndex::hardened(params.network_type().coin_type())
3195 .index(),
3196 zip32::ChildIndex::hardened(u32::from(
3197 derivation.account_index(),
3198 ))
3199 .index(),
3200 ],
3201 )
3202 .expect("valid"),
3203 );
3204 }
3205
3206 if let Some((pczt_recipient, external_address)) = ironwood_outputs.get(&index) {
3207 if let Some(user_address) = external_address {
3208 action_updater.set_output_user_address(user_address.encode());
3209 }
3210 action_updater.set_output_proprietary(
3211 PROPRIETARY_OUTPUT_INFO.into(),
3212 postcard::to_allocvec(pczt_recipient).expect(
3213 "postcard encoding of PCZT recipient metadata should not fail",
3214 ),
3215 );
3216 }
3217
3218 Ok(())
3219 })?;
3220 }
3221 Ok(())
3222 })?
3223 .update_sapling_with(|mut updater| {
3224 if let Some(derivation) = account_derivation {
3226 let non_dummy_spends = updater
3227 .bundle()
3228 .spends()
3229 .iter()
3230 .enumerate()
3231 .filter_map(|(index, spend)| {
3232 spend.proof_generation_key().is_none().then_some(index)
3234 })
3235 .collect::<Vec<_>>();
3236
3237 for index in non_dummy_spends {
3238 updater.update_spend_with(index, |mut spend_updater| {
3239 spend_updater.set_zip32_derivation(
3241 sapling::pczt::Zip32Derivation::parse(
3242 derivation.seed_fingerprint().to_bytes(),
3243 vec![
3244 zip32::ChildIndex::hardened(32).index(),
3245 zip32::ChildIndex::hardened(params.network_type().coin_type())
3246 .index(),
3247 zip32::ChildIndex::hardened(u32::from(
3248 derivation.account_index(),
3249 ))
3250 .index(),
3251 ],
3252 )
3253 .expect("valid"),
3254 );
3255 Ok(())
3256 })?;
3257 }
3258 }
3259
3260 for index in 0..updater.bundle().outputs().len() {
3261 if let Some((pczt_recipient, external_address)) = sapling_outputs.get(&index) {
3262 updater.update_output_with(index, |mut output_updater| {
3263 if let Some(user_address) = external_address {
3264 output_updater.set_user_address(user_address.encode());
3265 }
3266 output_updater.set_proprietary(
3267 PROPRIETARY_OUTPUT_INFO.into(),
3268 postcard::to_allocvec(pczt_recipient).expect(
3269 "postcard encoding of PCZT recipient metadata should not fail",
3270 ),
3271 );
3272 Ok(())
3273 })?;
3274 }
3275 }
3276
3277 Ok(())
3278 })?
3279 .update_transparent_with(|mut updater| {
3280 if let Some(derivation) = account_derivation {
3282 let inputs_to_update = updater
3284 .bundle()
3285 .inputs()
3286 .iter()
3287 .enumerate()
3288 .filter_map(|(index, input)| {
3289 let address_metadata = build_state.transparent_input_addresses.get(
3290 &TransparentAddress::from_script_from_chain(input.script_pubkey())
3291 .expect("we created this with a supported transparent address"),
3292 )?;
3293 match address_metadata.source() {
3294 TransparentAddressSource::Derived {
3295 scope,
3296 address_index,
3297 } => Some((index, *scope, *address_index)),
3298 #[cfg(feature = "transparent-key-import")]
3299 TransparentAddressSource::StandalonePubkey(_)
3300 | TransparentAddressSource::StandaloneScript(_)
3301 | TransparentAddressSource::StandaloneAddress => None,
3302 }
3303 })
3304 .collect::<Vec<_>>();
3305
3306 for (index, scope, address_index) in inputs_to_update {
3307 updater.update_input_with(index, |mut input_updater| {
3308 let pubkey = ufvk
3309 .p2pkh()
3310 .expect("we derived this successfully in build_proposed_transaction")
3311 .derive_address_pubkey(scope, address_index)
3312 .expect("spending key derivation should not fail");
3313
3314 input_updater.set_bip32_derivation(
3315 pubkey.serialize(),
3316 Bip32Derivation::parse(
3317 derivation.seed_fingerprint().to_bytes(),
3318 vec![
3319 44 | ChildNumber::HARDENED_FLAG,
3321 params.network_type().coin_type() | ChildNumber::HARDENED_FLAG,
3322 u32::from(derivation.account_index())
3323 | ChildNumber::HARDENED_FLAG,
3324 ChildNumber::from(scope).into(),
3325 ChildNumber::from(address_index).into(),
3326 ],
3327 )
3328 .expect("valid"),
3329 );
3330 Ok(())
3331 })?;
3332 }
3333 }
3334
3335 assert_eq!(
3336 build_state.transparent_output_meta.len(),
3337 updater.bundle().outputs().len(),
3338 );
3339 for (index, (recipient, _, _, _)) in
3340 build_state.transparent_output_meta.into_iter().enumerate()
3341 {
3342 updater.update_output_with(index, |mut output_updater| {
3343 let (pczt_recipient, external_address) =
3344 PcztRecipient::from_transparent_recipient(recipient);
3345 if let Some(user_address) = external_address {
3346 output_updater.set_user_address(user_address.encode());
3347 }
3348 output_updater.set_proprietary(
3349 PROPRIETARY_OUTPUT_INFO.into(),
3350 postcard::to_allocvec(&pczt_recipient)
3351 .expect("postcard encoding of pczt recipient metadata should not fail"),
3352 );
3353 Ok(())
3354 })?;
3355 }
3356
3357 Ok(())
3358 })?
3359 .finish();
3360
3361 Ok(pczt)
3362}
3363
3364#[cfg(feature = "pczt")]
3367#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3368pub enum SignerView {
3369 Full,
3381 Compact,
3391}
3392
3393#[cfg(feature = "pczt")]
3413pub fn redact_pczt_for_signer(pczt: &pczt::Pczt, view: SignerView) -> pczt::Pczt {
3414 match view {
3415 SignerView::Full => full_signer_view(pczt),
3416 SignerView::Compact => compact_signer_view(pczt),
3417 }
3418}
3419
3420#[cfg(feature = "pczt")]
3421fn full_signer_view(pczt: &pczt::Pczt) -> pczt::Pczt {
3422 fn redact_orchard_bundle(mut redactor: pczt::roles::redactor::orchard::OrchardRedactor<'_>) {
3423 redactor.redact_actions(|mut action| {
3424 action.clear_spend_witness();
3425 action.clear_spend_dummy_sk();
3426 action.redact_output_proprietary(PROPRIETARY_OUTPUT_INFO);
3427 });
3428 }
3429
3430 Redactor::new(pczt.clone())
3431 .redact_global_with(|mut redactor| {
3432 redactor.redact_proprietary(PROPRIETARY_PROPOSAL_INFO);
3433 })
3434 .redact_transparent_with(|mut redactor| {
3435 redactor.redact_outputs(|mut output| {
3436 output.redact_proprietary(PROPRIETARY_OUTPUT_INFO);
3437 });
3438 })
3439 .redact_sapling_with(|mut redactor| {
3440 redactor.redact_spends(|mut spend| {
3441 spend.clear_witness();
3442 spend.clear_dummy_ask();
3443 });
3444 redactor.redact_outputs(|mut output| {
3445 output.redact_proprietary(PROPRIETARY_OUTPUT_INFO);
3446 });
3447 })
3448 .redact_orchard_with(redact_orchard_bundle)
3449 .redact_ironwood_with(redact_orchard_bundle)
3450 .finish()
3451}
3452
3453#[cfg(feature = "pczt")]
3454fn compact_signer_view(pczt: &pczt::Pczt) -> pczt::Pczt {
3455 let redact_v6_anchors = *pczt.global().tx_version() == zcash_protocol::constants::V6_TX_VERSION;
3456
3457 fn redact_orchard_bundle(
3458 mut redactor: pczt::roles::redactor::orchard::OrchardRedactor<'_>,
3459 redact_v6_anchor: bool,
3460 ) {
3461 redactor.clear_zkproof();
3462 redactor.clear_bsk();
3463 redactor.redact_actions(|mut action| {
3464 action.clear_spend_witness();
3465 action.clear_spend_dummy_sk();
3466 action.redact_output_proprietary(PROPRIETARY_OUTPUT_INFO);
3467 });
3468
3469 redactor.compact_resolvable_fields();
3470
3471 if redact_v6_anchor {
3472 redactor.clear_anchor();
3473 }
3474 }
3475
3476 Redactor::new(pczt.clone())
3477 .redact_global_with(|mut redactor| {
3478 redactor.redact_proprietary(PROPRIETARY_PROPOSAL_INFO);
3479 })
3480 .redact_transparent_with(|mut redactor| {
3481 redactor.redact_outputs(|mut output| {
3482 output.redact_proprietary(PROPRIETARY_OUTPUT_INFO);
3483 });
3484 })
3485 .redact_sapling_with(|mut redactor| {
3486 redactor.clear_bsk();
3487 redactor.redact_spends(|mut spend| {
3488 spend.clear_zkproof();
3489 spend.clear_dummy_ask();
3490 });
3491 redactor.redact_outputs(|mut output| {
3492 output.clear_zkproof();
3493 output.redact_proprietary(PROPRIETARY_OUTPUT_INFO);
3494 });
3495 if redact_v6_anchors {
3496 redactor.clear_anchor();
3497 }
3498 })
3499 .redact_orchard_with(|redactor| {
3500 redact_orchard_bundle(redactor, redact_v6_anchors);
3501 })
3502 .redact_ironwood_with(|redactor| {
3503 redact_orchard_bundle(redactor, redact_v6_anchors);
3504 })
3505 .finish()
3506}
3507
3508#[cfg(feature = "pczt")]
3523pub fn redact_pczt_for_batch_signer(pczt: &pczt::Pczt) -> pczt::Pczt {
3524 fn preauthorized_action_indices(bundle: &pczt::orchard::Bundle) -> Vec<usize> {
3526 bundle
3527 .actions()
3528 .iter()
3529 .enumerate()
3530 .filter_map(|(index, action)| {
3531 action.spend().spend_auth_sig().is_some().then_some(index)
3532 })
3533 .collect()
3534 }
3535
3536 fn redact_orchard_bundle(
3537 mut redactor: pczt::roles::redactor::orchard::OrchardRedactor<'_>,
3538 preauthorized_action_indices: &[usize],
3539 ) {
3540 redactor.redact_actions(|mut action| {
3546 action.clear_spend_fvk();
3547 action.clear_spend_auth_sig();
3548 });
3549 for &index in preauthorized_action_indices {
3551 redactor.redact_action(index, |mut action| action.clear_spend_alpha());
3552 }
3553 }
3554
3555 let orchard_preauthorized = preauthorized_action_indices(pczt.orchard());
3556 let ironwood_preauthorized = preauthorized_action_indices(pczt.ironwood());
3557
3558 Redactor::new(redact_pczt_for_signer(pczt, SignerView::Compact))
3560 .redact_orchard_with(|redactor| {
3561 redact_orchard_bundle(redactor, &orchard_preauthorized);
3562 })
3563 .redact_ironwood_with(|redactor| {
3564 redact_orchard_bundle(redactor, &ironwood_preauthorized);
3565 })
3566 .finish()
3567}
3568
3569#[cfg(feature = "pczt")]
3587pub fn extract_and_store_transaction_from_pczt<DbT, N>(
3588 wallet_db: &mut DbT,
3589 clock: &impl Clock,
3590 rng: &mut impl CryptoRng,
3591 pczt: pczt::Pczt,
3592 sapling_vk: Option<(
3593 &sapling::circuit::SpendVerifyingKey,
3594 &sapling::circuit::OutputVerifyingKey,
3595 )>,
3596 #[cfg(feature = "orchard")] orchard_vk: Option<&orchard::circuit::VerifyingKey>,
3597) -> Result<TxId, ExtractErrT<DbT, N>>
3598where
3599 DbT: WalletWrite + WalletCommitmentTrees,
3600 <DbT as WalletRead>::AccountId: serde::de::DeserializeOwned,
3601{
3602 let finalized = SpendFinalizer::new(pczt).finalize_spends()?;
3603
3604 let proposal_info = finalized
3605 .global()
3606 .proprietary()
3607 .get(PROPRIETARY_PROPOSAL_INFO)
3608 .ok_or_else(|| PcztError::Invalid("PCZT missing proprietary proposal info field".into()))
3609 .and_then(|v| {
3610 postcard::from_bytes::<ProposalInfo<<DbT as WalletRead>::AccountId>>(v).map_err(|e| {
3611 PcztError::Invalid(format!(
3612 "Postcard decoding of proprietary proposal info failed: {e}"
3613 ))
3614 })
3615 })?;
3616
3617 #[allow(clippy::type_complexity)]
3622 fn orchard_protocol_output_info<AccountId: serde::de::DeserializeOwned>(
3623 actions: &[pczt::orchard::Action],
3624 note_version: orchard::note::NoteVersion,
3625 ) -> Result<
3626 Vec<
3627 Option<(
3628 (PcztRecipient<AccountId>, Option<ZcashAddress>),
3629 orchard::Note,
3630 )>,
3631 >,
3632 PcztError,
3633 > {
3634 actions
3635 .iter()
3636 .map(|act| {
3637 let note = || {
3638 let recipient = act.output().recipient().as_ref().and_then(|b| {
3639 ::orchard::Address::from_raw_address_bytes(b).into_option()
3640 })?;
3641 let value = act
3642 .output()
3643 .value()
3644 .map(orchard::value::NoteValue::from_raw)?;
3645 let rho =
3646 orchard::note::Rho::from_bytes(act.spend().nullifier()).into_option()?;
3647 let rseed = act.output().rseed().as_ref().and_then(|rseed| {
3648 orchard::note::RandomSeed::from_bytes(*rseed, &rho).into_option()
3649 })?;
3650
3651 orchard::Note::from_parts(recipient, value, rho, rseed, note_version)
3652 .into_option()
3653 };
3654
3655 let external_address = act
3656 .output()
3657 .user_address()
3658 .as_deref()
3659 .map(ZcashAddress::try_from_encoded)
3660 .transpose()
3661 .map_err(|e| PcztError::Invalid(format!("Invalid user_address: {e}")))?;
3662
3663 let pczt_recipient = act
3664 .output()
3665 .proprietary()
3666 .get(PROPRIETARY_OUTPUT_INFO)
3667 .map(|v| postcard::from_bytes::<PcztRecipient<AccountId>>(v))
3668 .transpose()
3669 .map_err(|e: postcard::Error| {
3670 PcztError::Invalid(format!(
3671 "Postcard decoding of proprietary output info failed: {e}"
3672 ))
3673 })?
3674 .map(|pczt_recipient| (pczt_recipient, external_address));
3675
3676 Ok(pczt_recipient.zip(note()))
3680 })
3681 .collect::<Result<Vec<_>, PcztError>>()
3682 }
3683
3684 let orchard_output_info = orchard_protocol_output_info::<<DbT as WalletRead>::AccountId>(
3685 finalized.orchard().actions(),
3686 orchard::note::NoteVersion::V2,
3687 )?;
3688
3689 let ironwood_output_info = orchard_protocol_output_info::<<DbT as WalletRead>::AccountId>(
3690 finalized.ironwood().actions(),
3691 orchard::note::NoteVersion::V3,
3692 )?;
3693
3694 let sapling_output_info = finalized
3695 .sapling()
3696 .outputs()
3697 .iter()
3698 .map(|out| {
3699 let note = || {
3700 let recipient = out
3701 .recipient()
3702 .as_ref()
3703 .and_then(::sapling::PaymentAddress::from_bytes)?;
3704 let value = out.value().map(::sapling::value::NoteValue::from_raw)?;
3705 let rseed = out
3706 .rseed()
3707 .as_ref()
3708 .cloned()
3709 .map(::sapling::note::Rseed::AfterZip212)?;
3710
3711 Some(::sapling::Note::from_parts(recipient, value, rseed))
3712 };
3713
3714 let external_address = out
3715 .user_address()
3716 .as_deref()
3717 .map(ZcashAddress::try_from_encoded)
3718 .transpose()
3719 .map_err(|e| PcztError::Invalid(format!("Invalid user_address: {e}")))?;
3720
3721 let pczt_recipient = out
3722 .proprietary()
3723 .get(PROPRIETARY_OUTPUT_INFO)
3724 .map(|v| postcard::from_bytes::<PcztRecipient<<DbT as WalletRead>::AccountId>>(v))
3725 .transpose()
3726 .map_err(|e: postcard::Error| {
3727 PcztError::Invalid(format!(
3728 "Postcard decoding of proprietary output info failed: {e}"
3729 ))
3730 })?
3731 .map(|pczt_recipient| (pczt_recipient, external_address));
3732
3733 Ok(pczt_recipient.zip(note()))
3737 })
3738 .collect::<Result<Vec<_>, PcztError>>()?;
3739
3740 let transparent_output_info = finalized
3741 .transparent()
3742 .outputs()
3743 .iter()
3744 .map(|out| {
3745 let external_address = out
3746 .user_address()
3747 .as_deref()
3748 .map(ZcashAddress::try_from_encoded)
3749 .transpose()
3750 .map_err(|e| PcztError::Invalid(format!("Invalid user_address: {e}")))?;
3751
3752 let pczt_recipient = out
3753 .proprietary()
3754 .get(PROPRIETARY_OUTPUT_INFO)
3755 .map(|v| postcard::from_bytes::<PcztRecipient<<DbT as WalletRead>::AccountId>>(v))
3756 .transpose()
3757 .map_err(|e: postcard::Error| {
3758 PcztError::Invalid(format!(
3759 "Postcard decoding of proprietary output info failed: {e}"
3760 ))
3761 })?
3762 .map(|pczt_recipient| (pczt_recipient, external_address));
3763
3764 Ok(pczt_recipient)
3765 })
3766 .collect::<Result<Vec<_>, PcztError>>()?;
3767
3768 let utxos_map = finalized
3769 .transparent()
3770 .inputs()
3771 .iter()
3772 .map(|input| {
3773 Zatoshis::from_u64(*input.value()).map(|value| {
3774 (
3775 OutPoint::new(*input.prevout_txid(), *input.prevout_index()),
3776 value,
3777 )
3778 })
3779 })
3780 .collect::<Result<BTreeMap<_, _>, _>>()?;
3781
3782 let mut tx_extractor = TransactionExtractor::new(finalized);
3783 if let Some((spend_vk, output_vk)) = sapling_vk {
3784 tx_extractor = tx_extractor.with_sapling(spend_vk, output_vk);
3785 }
3786 if let Some(orchard_vk) = orchard_vk {
3787 tx_extractor = tx_extractor.with_orchard(orchard_vk);
3788 }
3789 let transaction = tx_extractor.extract(rng)?;
3790 let txid = transaction.txid();
3791
3792 #[allow(clippy::too_many_arguments)]
3793 fn to_sent_transaction_output<
3794 AccountId: Copy,
3795 D: Domain,
3796 O: ShieldedOutput<D>,
3797 DbT: WalletRead + WalletCommitmentTrees,
3798 N,
3799 >(
3800 domain: D,
3801 note: D::Note,
3802 output: &O,
3803 output_pool: ShieldedPool,
3804 output_index: usize,
3805 pczt_recipient: PcztRecipient<AccountId>,
3806 external_address: Option<ZcashAddress>,
3807 note_value: impl Fn(&D::Note) -> u64,
3808 memo_bytes: impl Fn(&D::Memo) -> &[u8; 512],
3809 wallet_note: impl Fn(D::Note) -> Note,
3810 ) -> Result<SentTransactionOutput<AccountId>, ExtractErrT<DbT, N>> {
3811 let pk_d = D::get_pk_d(¬e);
3812 let esk = D::derive_esk(¬e).expect("notes are post-ZIP 212");
3813 let memo = try_output_recovery_with_pkd_esk(&domain, pk_d, esk, output).map(|(_, _, m)| {
3814 MemoBytes::from_bytes(memo_bytes(&m)).expect("Memo is the correct length.")
3815 });
3816
3817 let note_commitment_tree = match output_pool {
3821 ShieldedPool::Sapling => NoteCommitmentTree::Sapling,
3822 ShieldedPool::Orchard => NoteCommitmentTree::Orchard,
3823 ShieldedPool::Ironwood => NoteCommitmentTree::Ironwood,
3824 };
3825
3826 let note_value = Zatoshis::try_from(note_value(¬e))?;
3827 let recipient = match (pczt_recipient, external_address) {
3828 (PcztRecipient::External, Some(addr)) => Ok(Recipient::External {
3829 recipient_address: addr,
3830 output_pool: PoolType::Shielded(output_pool),
3831 }),
3832 (PcztRecipient::External, None) => Err(PcztError::Invalid(
3833 "external recipient needs to have its user_address field set".into(),
3834 )),
3835 #[cfg(feature = "transparent-inputs")]
3836 (PcztRecipient::EphemeralTransparent { .. }, _) => Err(PcztError::Invalid(
3837 "shielded output cannot be EphemeralTransparent".into(),
3838 )),
3839 #[cfg(feature = "transparent-inputs")]
3840 (PcztRecipient::InternalTransparent { .. }, _) => Err(PcztError::Invalid(
3841 "shielded output cannot be InternalTransparent".into(),
3842 )),
3843 (PcztRecipient::InternalShielded { receiving_account }, external_address) => {
3844 Ok(Recipient::InternalShielded {
3845 receiving_account,
3846 external_address,
3847 note: Box::new(wallet_note(note)),
3848 })
3849 }
3850 }?;
3851
3852 Ok(SentTransactionOutput::from_parts_in_tree(
3853 Some(note_commitment_tree),
3854 output_index,
3855 recipient,
3856 note_value,
3857 memo,
3858 ))
3859 }
3860
3861 #[cfg(feature = "orchard")]
3862 let orchard_outputs = transaction
3863 .orchard_bundle()
3864 .map(|bundle| {
3865 assert_eq!(bundle.actions().len(), orchard_output_info.len());
3866 bundle
3867 .actions()
3868 .iter()
3869 .zip(orchard_output_info)
3870 .enumerate()
3871 .filter_map(|(output_index, (action, output_info))| {
3872 output_info.map(|((pczt_recipient, external_address), note)| {
3873 let domain = OrchardDomain::for_action(action);
3874 to_sent_transaction_output::<_, _, _, DbT, _>(
3875 domain,
3876 note,
3877 action,
3878 ShieldedPool::Orchard,
3879 output_index,
3880 pczt_recipient,
3881 external_address,
3882 |note| note.value().inner(),
3883 |memo| memo,
3884 |note| Note::Orchard {
3885 note,
3886 pool: orchard::ValuePool::Orchard,
3887 },
3888 )
3889 })
3890 })
3891 .collect::<Result<Vec<_>, _>>()
3892 })
3893 .transpose()?;
3894
3895 #[cfg(feature = "orchard")]
3896 let ironwood_outputs = transaction
3897 .ironwood_bundle()
3898 .map(|bundle| {
3899 assert_eq!(bundle.actions().len(), ironwood_output_info.len());
3900 bundle
3901 .actions()
3902 .iter()
3903 .zip(ironwood_output_info)
3904 .enumerate()
3905 .filter_map(|(output_index, (action, output_info))| {
3906 output_info.map(|((pczt_recipient, external_address), note)| {
3907 let domain = IronwoodDomain::for_action(action);
3908 to_sent_transaction_output::<_, _, _, DbT, _>(
3909 domain,
3910 note,
3911 action,
3912 ShieldedPool::Ironwood,
3913 output_index,
3914 pczt_recipient,
3915 external_address,
3916 |note| note.value().inner(),
3917 |memo| memo,
3918 |note| Note::Orchard {
3919 note,
3920 pool: orchard::ValuePool::Ironwood,
3921 },
3922 )
3923 })
3924 })
3925 .collect::<Result<Vec<_>, _>>()
3926 })
3927 .transpose()?;
3928
3929 let sapling_outputs = transaction
3930 .sapling_bundle()
3931 .map(|bundle| {
3932 assert_eq!(bundle.shielded_outputs().len(), sapling_output_info.len());
3933 bundle
3934 .shielded_outputs()
3935 .iter()
3936 .zip(sapling_output_info)
3937 .enumerate()
3938 .filter_map(|(output_index, (action, output_info))| {
3939 output_info.map(|((pczt_recipient, external_address), note)| {
3940 let domain =
3941 SaplingDomain::new(sapling::note_encryption::Zip212Enforcement::On);
3942 to_sent_transaction_output::<_, _, _, DbT, _>(
3943 domain,
3944 note,
3945 action,
3946 ShieldedPool::Sapling,
3947 output_index,
3948 pczt_recipient,
3949 external_address,
3950 |note| note.value().inner(),
3951 |memo| memo,
3952 Note::Sapling,
3953 )
3954 })
3955 })
3956 .collect::<Result<Vec<_>, _>>()
3957 })
3958 .transpose()?;
3959
3960 #[allow(unused_variables)]
3961 let transparent_outputs = transaction
3962 .transparent_bundle()
3963 .map(|bundle| {
3964 assert_eq!(bundle.vout.len(), transparent_output_info.len());
3965 bundle
3966 .vout
3967 .iter()
3968 .zip(transparent_output_info)
3969 .enumerate()
3970 .filter_map(|(output_index, (output, output_info))| {
3971 output_info.map(|(pczt_recipient, external_address)| {
3972 let outpoint = OutPoint::new(txid.into(), output_index as u32);
3977
3978 let recipient = match (pczt_recipient, external_address) {
3979 (PcztRecipient::External, Some(addr)) => {
3980 Ok(Recipient::External {
3981 recipient_address: addr,
3982 output_pool: PoolType::Transparent,
3983 })
3984 }
3985 (PcztRecipient::External, None) => Err(PcztError::Invalid(
3986 "external recipient needs to have its user_address field set".into(),
3987 )),
3988 #[cfg(feature = "transparent-inputs")]
3989 (PcztRecipient::EphemeralTransparent { receiving_account }, _) => output
3990 .recipient_address()
3991 .ok_or(PcztError::Invalid(
3992 "Ephemeral outputs cannot have a non-standard script_pubkey"
3993 .into(),
3994 ))
3995 .map(|ephemeral_address| Recipient::EphemeralTransparent {
3996 receiving_account,
3997 ephemeral_address,
3998 outpoint,
3999 }),
4000 #[cfg(feature = "transparent-inputs")]
4001 (PcztRecipient::InternalTransparent { receiving_account }, _) => output
4002 .recipient_address()
4003 .ok_or(PcztError::Invalid(
4004 "Transparent change outputs cannot have a non-standard script_pubkey"
4005 .into(),
4006 ))
4007 .map(|recipient_address| Recipient::InternalTransparent {
4008 receiving_account,
4009 recipient_address,
4010 }),
4011 (
4012 PcztRecipient::InternalShielded {
4013 receiving_account,
4014 },
4015 _,
4016 ) => Err(PcztError::Invalid(
4017 "Transparent output cannot be InternalShielded".into(),
4018 )),
4019 }?;
4020
4021 Ok(SentTransactionOutput::from_parts(
4022 output_index,
4023 recipient,
4024 output.value(),
4025 None,
4026 ))
4027 })
4028 })
4029 .collect::<Result<Vec<_>, ExtractErrT<DbT, _>>>()
4030 })
4031 .transpose()?;
4032
4033 let mut outputs: Vec<SentTransactionOutput<_>> = vec![];
4034 #[cfg(feature = "orchard")]
4035 outputs.extend(orchard_outputs.into_iter().flatten());
4036 #[cfg(feature = "orchard")]
4037 outputs.extend(ironwood_outputs.into_iter().flatten());
4038 outputs.extend(sapling_outputs.into_iter().flatten());
4039 outputs.extend(transparent_outputs.into_iter().flatten());
4040
4041 let fee_amount = transaction
4042 .fee_paid(|outpoint| Ok::<_, BalanceError>(utxos_map.get(outpoint).copied()))?
4043 .expect("input map was constructed correctly");
4046
4047 let utxos_spent = utxos_map.into_keys().collect::<Vec<_>>();
4049
4050 let created = time::OffsetDateTime::from(clock.now());
4051
4052 let transactions = vec![SentTransaction::new(
4053 &transaction,
4054 created,
4055 proposal_info.target_height,
4056 proposal_info.from_account,
4057 &outputs,
4058 fee_amount,
4059 #[cfg(feature = "transparent-inputs")]
4060 &utxos_spent,
4061 )];
4062
4063 wallet_db
4064 .store_transactions_to_be_sent(&transactions)
4065 .map_err(Error::DataSource)?;
4066
4067 Ok(txid)
4068}
4069
4070#[cfg(feature = "transparent-inputs")]
4105#[allow(clippy::too_many_arguments)]
4106#[allow(clippy::type_complexity)]
4107pub fn shield_transparent_funds<DbT, ParamsT, InputsT, ChangeT>(
4108 wallet_db: &mut DbT,
4109 params: &ParamsT,
4110 clock: &impl Clock,
4111 rng: &mut impl CryptoRng,
4112 spend_prover: &impl SpendProver,
4113 output_prover: &impl OutputProver,
4114 input_selector: &InputsT,
4115 change_strategy: &ChangeT,
4116 shielding_threshold: Zatoshis,
4117 spending_keys: &SpendingKeys,
4118 from_addrs: &[TransparentAddress],
4119 to_account: <DbT as InputSource>::AccountId,
4120 confirmations_policy: ConfirmationsPolicy,
4121) -> Result<NonEmpty<TxId>, ShieldErrT<DbT, InputsT, ChangeT>>
4122where
4123 ParamsT: consensus::Parameters,
4124 DbT: WalletWrite + WalletCommitmentTrees + InputSource<Error = <DbT as WalletRead>::Error>,
4125 InputsT: ShieldingSelector<InputSource = DbT>,
4126 ChangeT: ChangeStrategy<MetaSource = DbT>,
4127{
4128 let proposal = propose_shielding(
4129 wallet_db,
4130 params,
4131 input_selector,
4132 change_strategy,
4133 shielding_threshold,
4134 from_addrs,
4135 to_account,
4136 confirmations_policy,
4137 CoinbaseFilter::AllTransparentOutputs,
4138 None,
4139 )?;
4140
4141 create_proposed_transactions(
4142 wallet_db,
4143 params,
4144 clock,
4145 rng,
4146 spend_prover,
4147 output_prover,
4148 spending_keys,
4149 OvkPolicy::Sender,
4150 &proposal,
4151 None,
4152 )
4153}
4154
4155#[cfg(test)]
4156mod tests {
4157 use super::*;
4158
4159 use zcash_protocol::zip318::{AnchorBucketInterval, BlockSpacing};
4160
4161 fn policy(confirmations: u32) -> ConfirmationsPolicy {
4162 ConfirmationsPolicy::new_symmetrical_unchecked(
4163 confirmations,
4164 #[cfg(feature = "transparent-inputs")]
4165 false,
4166 )
4167 }
4168
4169 fn target(height: u32) -> TargetHeight {
4170 TargetHeight::from(BlockHeight::from_u32(height))
4171 }
4172
4173 #[test]
4177 fn bucketed_policy_lands_the_anchor_on_a_boundary() {
4178 let interval = AnchorGrid::new(AnchorBucketInterval::ZIP_318, BlockSpacing::UNIFORM);
4179 let policy = policy(10);
4180
4181 for height in [2_000_000u32, 2_000_143, 2_000_144, 1_000_000] {
4182 let target = target(height);
4183 let bucketed = policy
4184 .bucketed(interval, target, BlockHeight::from_u32(1))
4185 .expect("a boundary is reachable well above the grid's origin");
4186 let anchor = bucketed.anchor_height(target);
4187
4188 assert!(
4189 interval.is_boundary(anchor),
4190 "anchor {anchor:?} off the grid"
4191 );
4192 let most_recent = interval.boundary_at_or_below(policy.anchor_height(target));
4194 assert_eq!(
4195 u32::from(anchor),
4196 u32::from(most_recent) - interval.interval().block_count().get(),
4197 "the chosen boundary must be exactly one interval below the most recent"
4198 );
4199 assert!(anchor < most_recent, "an age of 0 is not admissible");
4200 assert!(bucketed.trusted() >= policy.trusted());
4202 assert!(bucketed.untrusted() >= bucketed.trusted());
4203 assert!(
4206 u32::from(bucketed.trusted()) - u32::from(policy.trusted())
4207 < 2 * interval.interval().block_count().get()
4208 );
4209 }
4210 }
4211
4212 #[test]
4215 fn bucketing_never_chooses_the_most_recent_boundary() {
4216 let interval = AnchorGrid::new(AnchorBucketInterval::ZIP_318, BlockSpacing::UNIFORM);
4217 let policy = policy(10);
4218 let aligned_anchor = interval.boundary_at_or_below(BlockHeight::from_u32(2_000_000));
4219 let target = target(u32::from(aligned_anchor) + 10);
4220
4221 let bucketed = policy
4222 .bucketed(interval, target, BlockHeight::from_u32(1))
4223 .expect("reachable");
4224 assert_eq!(
4225 u32::from(bucketed.anchor_height(target)),
4226 u32::from(aligned_anchor) - interval.interval().block_count().get()
4227 );
4228 assert!(bucketed.trusted() > policy.trusted());
4229 }
4230
4231 #[test]
4234 fn bucketing_declines_below_the_first_boundary() {
4235 let interval = AnchorGrid::new(AnchorBucketInterval::ZIP_318, BlockSpacing::UNIFORM);
4236 let policy = policy(10);
4237 assert!(
4240 policy
4241 .bucketed(interval, target(100), BlockHeight::from_u32(1))
4242 .is_none()
4243 );
4244 }
4245
4246 #[test]
4248 fn bucketing_honours_a_custom_interval() {
4249 let interval = AnchorGrid::new(
4250 AnchorBucketInterval::custom(NonZeroU32::new(12).expect("nonzero")),
4251 BlockSpacing::UNIFORM,
4252 );
4253 let policy = policy(3);
4254 let target = target(1_000);
4255
4256 let bucketed = policy
4257 .bucketed(interval, target, BlockHeight::from_u32(1))
4258 .expect("reachable");
4259 let anchor = bucketed.anchor_height(target);
4260 assert!(interval.is_boundary(anchor));
4261 assert_eq!(u32::from(anchor), 984);
4263 }
4264
4265 #[test]
4268 fn bucketing_follows_the_scaled_grid_from_nu7() {
4269 let activation = 1_000_000u32;
4270 let grid = AnchorGrid::new(
4271 AnchorBucketInterval::ZIP_318,
4272 BlockSpacing::with_nu7_activation(BlockHeight::from_u32(activation)),
4273 );
4274 let scaled_interval = consensus::NU7_POW_TARGET_SPACING_RATIO
4275 * AnchorBucketInterval::ZIP_318.block_count().get();
4276 let policy = policy(10);
4277 let target = target(2_000_000);
4278
4279 let bucketed = policy
4280 .bucketed(grid, target, BlockHeight::from_u32(1))
4281 .expect("reachable");
4282 let anchor = u32::from(bucketed.anchor_height(target));
4283 let most_recent = u32::from(grid.boundary_at_or_below(policy.anchor_height(target)));
4284 assert_eq!(most_recent % scaled_interval, 0);
4285 assert_eq!(anchor, most_recent - scaled_interval);
4286 }
4287}