1#[cfg(feature = "transparent-inputs")]
3use {
4 crate::{
5 data_api::CoinbaseFilter,
6 fees::{ChangeValue, StandardFeeRule},
7 proposal::{Step, StepOutput, StepOutputIndex},
8 },
9 std::convert::Infallible,
10 transparent::{address::TransparentAddress, bundle::OutPoint, keys::TransparentKeyScope},
11 zcash_primitives::transaction::fees::{
12 transparent as transparent_fees, transparent::InputSize, zip317::P2PKH_STANDARD_INPUT_SIZE,
13 },
14 zcash_protocol::constants::MAX_BLOCK_BYTES,
15 zip321::Payment,
16};
17
18use core::marker::PhantomData;
19use nonempty::NonEmpty;
20use std::{
21 collections::{BTreeMap, BTreeSet},
22 error,
23 fmt::{self, Debug, Display},
24};
25
26use transparent::bundle::TxOut;
27use zcash_address::{ConversionError, ZcashAddress};
28use zcash_keys::address::{Address, UnifiedAddress};
29use zcash_primitives::transaction::{
30 TxVersion,
31 fees::{FeeRule, zip317::P2PKH_STANDARD_OUTPUT_SIZE},
32};
33use zcash_protocol::{
34 PoolType, ShieldedPool,
35 consensus::{self, BlockHeight},
36 memo::MemoBytes,
37 value::{BalanceError, MAX_MONEY, Zatoshis},
38};
39use zip321::TransactionRequest;
40
41use crate::{
42 data_api::{
43 InputSource, MaxSpendMode, ReceivedNotes, SimpleNoteRetention, TargetValue,
44 anchor_retention::PoolMigrationParams, wallet::TargetHeight,
45 },
46 fees::{ChangeError, ChangeStrategy, EphemeralBalance, TransactionBalance, sapling},
47 proposal::{Proposal, ProposalError, ShieldedInputs},
48 wallet::{ReceivedNote, WalletTransparentOutput},
49};
50
51pub use crate::data_api::locking::{LockFilter, LockedInputPolicy};
52
53use super::ConfirmationsPolicy;
54
55#[cfg(feature = "orchard")]
56use crate::{data_api::wallet::ironwood_active_at, fees::orchard as orchard_fees};
57
58#[derive(Debug)]
60#[non_exhaustive]
61pub enum InputSelectorError<DbErrT, SelectorErrT, ChangeErrT, N> {
62 DataSource(DbErrT),
64 Selection(SelectorErrT),
66 Change(ChangeError<ChangeErrT, N>),
68 Proposal(ProposalError),
70 Address(ConversionError<&'static str>),
72 InsufficientFunds {
75 available: Zatoshis,
76 required: Zatoshis,
77 },
78 SyncRequired,
81}
82
83impl<DE: fmt::Display, SE: fmt::Display, CE: fmt::Display, N: fmt::Display> fmt::Display
84 for InputSelectorError<DE, SE, CE, N>
85{
86 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
87 match &self {
88 InputSelectorError::DataSource(e) => {
89 write!(
90 f,
91 "The underlying datasource produced the following error: {e}"
92 )
93 }
94 InputSelectorError::Selection(e) => {
95 write!(f, "Note selection encountered the following error: {e}")
96 }
97 InputSelectorError::Change(e) => write!(
98 f,
99 "Proposal generation failed due to an error in computing change or transaction fees: {e}"
100 ),
101 InputSelectorError::Proposal(e) => {
102 write!(
103 f,
104 "Input selection attempted to generate an invalid proposal: {e}"
105 )
106 }
107 InputSelectorError::Address(e) => {
108 write!(
109 f,
110 "An error occurred decoding the address from a payment request: {e}."
111 )
112 }
113 InputSelectorError::InsufficientFunds {
114 available,
115 required,
116 } => write!(
117 f,
118 "Insufficient balance (have {}, need {} including fee)",
119 u64::from(*available),
120 u64::from(*required)
121 ),
122 InputSelectorError::SyncRequired => {
123 write!(f, "Insufficient chain data is available, sync required.")
124 }
125 }
126 }
127}
128
129impl<DE, SE, CE, N> error::Error for InputSelectorError<DE, SE, CE, N>
130where
131 DE: Debug + Display + error::Error + 'static,
132 SE: Debug + Display + error::Error + 'static,
133 CE: Debug + Display + error::Error + 'static,
134 N: Debug + Display + 'static,
135{
136 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
137 match &self {
138 Self::DataSource(e) => Some(e),
139 Self::Selection(e) => Some(e),
140 Self::Change(e) => Some(e),
141 Self::Proposal(e) => Some(e),
142 _ => None,
143 }
144 }
145}
146
147impl<E, S, F, N> From<ConversionError<&'static str>> for InputSelectorError<E, S, F, N> {
148 fn from(value: ConversionError<&'static str>) -> Self {
149 InputSelectorError::Address(value)
150 }
151}
152
153impl<E, S, C, N> From<ChangeError<C, N>> for InputSelectorError<E, S, C, N> {
154 fn from(err: ChangeError<C, N>) -> Self {
155 InputSelectorError::Change(err)
156 }
157}
158
159impl<E, S, C, N> From<ProposalError> for InputSelectorError<E, S, C, N> {
160 fn from(err: ProposalError) -> Self {
161 InputSelectorError::Proposal(err)
162 }
163}
164
165pub trait InputSelector {
171 type Error;
173
174 type InputSource: InputSource;
180
181 #[allow(clippy::type_complexity)]
205 #[allow(clippy::too_many_arguments)]
206 fn propose_transaction<ParamsT, ChangeT>(
207 &self,
208 params: &ParamsT,
209 wallet_db: &Self::InputSource,
210 target_height: TargetHeight,
211 anchor_height: BlockHeight,
212 zip318: &PoolMigrationParams,
213 confirmations_policy: ConfirmationsPolicy,
214 account: <Self::InputSource as InputSource>::AccountId,
215 transaction_request: TransactionRequest,
216 change_strategy: &ChangeT,
217 spend_policy: &SpendPolicy,
218 proposed_version: Option<TxVersion>,
219 ) -> Result<
220 Proposal<<ChangeT as ChangeStrategy>::FeeRule, <Self::InputSource as InputSource>::NoteRef>,
221 InputSelectorError<
222 <Self::InputSource as InputSource>::Error,
223 Self::Error,
224 ChangeT::Error,
225 <Self::InputSource as InputSource>::NoteRef,
226 >,
227 >
228 where
229 ParamsT: consensus::Parameters,
230 ChangeT: ChangeStrategy<MetaSource = Self::InputSource>;
231}
232
233#[cfg(feature = "transparent-inputs")]
237pub trait ShieldingSelector {
238 type Error;
240 type InputSource: InputSource;
246
247 #[allow(clippy::type_complexity)]
259 #[allow(clippy::too_many_arguments)]
260 fn propose_shielding<ParamsT, ChangeT>(
261 &self,
262 params: &ParamsT,
263 wallet_db: &Self::InputSource,
264 change_strategy: &ChangeT,
265 shielding_threshold: Zatoshis,
266 source_addrs: &[TransparentAddress],
267 to_account: <Self::InputSource as InputSource>::AccountId,
268 target_height: TargetHeight,
269 anchor_height: BlockHeight,
270 zip318: &PoolMigrationParams,
271 confirmations_policy: ConfirmationsPolicy,
272 output_filter: CoinbaseFilter,
273 ) -> Result<
274 Proposal<<ChangeT as ChangeStrategy>::FeeRule, Infallible>,
275 InputSelectorError<
276 <Self::InputSource as InputSource>::Error,
277 Self::Error,
278 ChangeT::Error,
279 Infallible,
280 >,
281 >
282 where
283 ParamsT: consensus::Parameters,
284 ChangeT: ChangeStrategy<MetaSource = Self::InputSource>;
285
286 #[allow(clippy::type_complexity)]
324 #[allow(clippy::too_many_arguments)]
325 fn propose_shielding_coinbase<ParamsT, FeeRuleT>(
326 &self,
327 params: &ParamsT,
328 wallet_db: &Self::InputSource,
329 fee_rule: &FeeRuleT,
330 shielding_threshold: Zatoshis,
331 source_addrs: &[TransparentAddress],
332 to_address: ZcashAddress,
333 memo: Option<MemoBytes>,
334 limit: Option<usize>,
335 target_height: TargetHeight,
336 anchor_height: BlockHeight,
337 ) -> Result<
338 Proposal<FeeRuleT, Infallible>,
339 InputSelectorError<
340 <Self::InputSource as InputSource>::Error,
341 Self::Error,
342 FeeRuleT::Error,
343 Infallible,
344 >,
345 >
346 where
347 ParamsT: consensus::Parameters,
348 FeeRuleT: FeeRule + Clone;
349}
350
351#[derive(Debug, Clone, PartialEq, Eq)]
353#[non_exhaustive]
354pub enum GreedyInputSelectorError {
355 Balance(BalanceError),
357 UnsupportedAddress(Box<UnifiedAddress>),
359 UnsupportedTexAddress,
361 RequirementCovered {
364 required: Zatoshis,
366 available: Zatoshis,
368 },
369 DustInputsNotGiven,
372 ExcludedNoteSelected,
376 UnrequestedPoolSelected,
380}
381
382impl fmt::Display for GreedyInputSelectorError {
383 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
384 match &self {
385 GreedyInputSelectorError::Balance(e) => write!(
386 f,
387 "A balance calculation violated amount validity bounds: {e:?}."
388 ),
389 GreedyInputSelectorError::UnsupportedAddress(_) => {
390 write!(f, "Unified address contains no supported receivers.")
393 }
394 GreedyInputSelectorError::UnsupportedTexAddress => {
395 write!(
396 f,
397 "Support for transparent-source-only (TEX) addresses requires the transparent-inputs feature."
398 )
399 }
400 GreedyInputSelectorError::RequirementCovered {
401 required,
402 available,
403 } => write!(
404 f,
405 "The change strategy required {} of inputs worth {}, which already cover it.",
406 u64::from(*required),
407 u64::from(*available)
408 ),
409 GreedyInputSelectorError::DustInputsNotGiven => {
410 write!(
411 f,
412 "The change strategy reported as dust an input it was not given, or none at all."
413 )
414 }
415 GreedyInputSelectorError::ExcludedNoteSelected => {
416 write!(
417 f,
418 "The data source returned a note that input selection asked it to exclude."
419 )
420 }
421 GreedyInputSelectorError::UnrequestedPoolSelected => {
422 write!(
423 f,
424 "The data source returned a note from a pool that input selection did not ask for."
425 )
426 }
427 }
428 }
429}
430
431impl<DbErrT, ChangeErrT, N> From<GreedyInputSelectorError>
432 for InputSelectorError<DbErrT, GreedyInputSelectorError, ChangeErrT, N>
433{
434 fn from(err: GreedyInputSelectorError) -> Self {
435 InputSelectorError::Selection(err)
436 }
437}
438
439impl<DbErrT, ChangeErrT, N> From<BalanceError>
440 for InputSelectorError<DbErrT, GreedyInputSelectorError, ChangeErrT, N>
441{
442 fn from(err: BalanceError) -> Self {
443 InputSelectorError::Selection(GreedyInputSelectorError::Balance(err))
444 }
445}
446
447pub(crate) struct SaplingPayment(Zatoshis);
448
449#[cfg(test)]
450impl SaplingPayment {
451 pub(crate) fn new(amount: Zatoshis) -> Self {
452 SaplingPayment(amount)
453 }
454}
455
456impl sapling::OutputView for SaplingPayment {
457 fn value(&self) -> Zatoshis {
458 self.0
459 }
460}
461
462#[cfg(feature = "orchard")]
463pub(crate) struct OrchardPayment(Zatoshis);
464
465#[cfg(test)]
466#[cfg(feature = "orchard")]
467impl OrchardPayment {
468 pub(crate) fn new(amount: Zatoshis) -> Self {
469 OrchardPayment(amount)
470 }
471}
472
473#[cfg(feature = "orchard")]
474impl orchard_fees::OutputView for OrchardPayment {
475 fn value(&self) -> Zatoshis {
476 self.0
477 }
478}
479
480#[cfg(feature = "transparent-inputs")]
483const DEFAULT_SHIELDING_BLOCK_SPACE_PERCENT: u32 = 10;
484
485#[derive(Clone, Debug, PartialEq, Eq)]
490pub struct NonEmptyBTreeSet<T>(BTreeSet<T>);
491
492impl<T: Ord> NonEmptyBTreeSet<T> {
493 pub fn singleton(value: T) -> Self {
495 Self(BTreeSet::from_iter([value]))
496 }
497
498 pub fn from_nonempty(values: NonEmpty<T>) -> Self {
501 Self(values.into_iter().collect())
502 }
503
504 pub fn from_set(values: BTreeSet<T>) -> Option<Self> {
506 (!values.is_empty()).then_some(Self(values))
507 }
508}
509
510impl<T> NonEmptyBTreeSet<T> {
511 pub fn as_set(&self) -> &BTreeSet<T> {
513 &self.0
514 }
515
516 pub fn iter(&self) -> std::collections::btree_set::Iter<'_, T> {
518 self.0.iter()
519 }
520}
521
522#[derive(Clone, Debug)]
537pub struct SpendPolicy {
538 shielded: BTreeSet<ShieldedPool>,
539 #[cfg(feature = "transparent-inputs")]
540 transparent: Option<TransparentSpendPolicy>,
541 locked_input_policy: LockedInputPolicy,
542 note_selection: NoteSelection,
543}
544
545#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
547pub enum NoteSelection {
548 #[default]
550 Accumulate,
551 PreferSingle,
558}
559
560impl Default for SpendPolicy {
561 fn default() -> Self {
562 Self::shielded_pools([
563 ShieldedPool::Sapling,
564 #[cfg(feature = "orchard")]
565 ShieldedPool::Orchard,
566 #[cfg(feature = "orchard")]
567 ShieldedPool::Ironwood,
568 ])
569 }
570}
571
572impl SpendPolicy {
573 pub fn shielded_pools(pools: impl IntoIterator<Item = ShieldedPool>) -> Self {
576 Self {
577 shielded: pools.into_iter().collect(),
578 #[cfg(feature = "transparent-inputs")]
579 transparent: None,
580 locked_input_policy: LockedInputPolicy::Exclude,
581 note_selection: NoteSelection::Accumulate,
582 }
583 }
584
585 pub fn permits_shielded(&self, pool: ShieldedPool) -> bool {
587 self.shielded.contains(&pool)
588 }
589
590 pub fn shielded(&self) -> &BTreeSet<ShieldedPool> {
592 &self.shielded
593 }
594
595 #[cfg(feature = "transparent-inputs")]
597 pub fn with_transparent(mut self, transparent: TransparentSpendPolicy) -> Self {
598 self.transparent = Some(transparent);
599 self
600 }
601
602 #[cfg(feature = "transparent-inputs")]
604 pub fn transparent(&self) -> Option<&TransparentSpendPolicy> {
605 self.transparent.as_ref()
606 }
607
608 pub fn with_locked_input_policy(mut self, policy: LockedInputPolicy) -> Self {
610 self.locked_input_policy = policy;
611 self
612 }
613
614 pub fn locked_input_policy(&self) -> &LockedInputPolicy {
616 &self.locked_input_policy
617 }
618
619 pub fn with_note_selection(mut self, note_selection: NoteSelection) -> Self {
622 self.note_selection = note_selection;
623 self
624 }
625
626 pub fn note_selection(&self) -> NoteSelection {
628 self.note_selection
629 }
630}
631
632#[derive(Clone, Copy, Debug, PartialEq, Eq)]
638pub enum CoinbasePolicy {
639 OnlyCoinbase,
641 NonCoinbase,
643}
644
645#[cfg(feature = "transparent-inputs")]
646impl From<CoinbasePolicy> for CoinbaseFilter {
647 fn from(policy: CoinbasePolicy) -> Self {
648 match policy {
649 CoinbasePolicy::OnlyCoinbase => CoinbaseFilter::CoinbaseOnly,
650 CoinbasePolicy::NonCoinbase => CoinbaseFilter::NonCoinbaseOnly,
651 }
652 }
653}
654
655#[cfg(feature = "transparent-inputs")]
664#[derive(Clone, Debug)]
665pub struct TransparentSpendPolicy {
666 source: TransparentSource,
667 coinbase: CoinbasePolicy,
668}
669
670#[cfg(feature = "transparent-inputs")]
671impl TransparentSpendPolicy {
672 pub fn any_account_addr() -> Self {
676 Self {
677 source: TransparentSource::AnyAccountAddr,
678 coinbase: CoinbasePolicy::NonCoinbase,
679 }
680 }
681
682 pub fn from_addresses(taddrs: NonEmpty<TransparentAddress>) -> Self {
685 Self {
686 source: TransparentSource::FromAddresses(NonEmptyBTreeSet::from_nonempty(taddrs)),
687 coinbase: CoinbasePolicy::NonCoinbase,
688 }
689 }
690
691 pub fn from_one_address(taddr: TransparentAddress) -> Self {
693 Self {
694 source: TransparentSource::FromAddresses(NonEmptyBTreeSet::singleton(taddr)),
695 coinbase: CoinbasePolicy::NonCoinbase,
696 }
697 }
698
699 pub fn with_coinbase(mut self, coinbase: CoinbasePolicy) -> Self {
701 self.coinbase = coinbase;
702 self
703 }
704
705 pub fn source(&self) -> &TransparentSource {
707 &self.source
708 }
709
710 pub fn coinbase(&self) -> CoinbasePolicy {
712 self.coinbase
713 }
714
715 fn address_allow_list(&self) -> Option<Vec<TransparentAddress>> {
718 match &self.source {
719 TransparentSource::FromAddresses(addrs) => Some(addrs.iter().copied().collect()),
720 TransparentSource::AnyAccountAddr => None,
721 }
722 }
723}
724
725#[cfg(feature = "transparent-inputs")]
727#[derive(Clone, Debug)]
728pub enum TransparentSource {
729 AnyAccountAddr,
732 FromAddresses(NonEmptyBTreeSet<TransparentAddress>),
734}
735
736pub struct GreedyInputSelector<DbT> {
742 #[cfg(feature = "transparent-inputs")]
747 shielding_block_space_percent: u32,
748 #[cfg(feature = "transparent-inputs")]
756 locked_input_policy: LockedInputPolicy,
757 _ds_type: PhantomData<DbT>,
758}
759
760impl<DbT> GreedyInputSelector<DbT> {
761 pub fn new() -> Self {
771 GreedyInputSelector {
772 #[cfg(feature = "transparent-inputs")]
773 shielding_block_space_percent: DEFAULT_SHIELDING_BLOCK_SPACE_PERCENT,
774 #[cfg(feature = "transparent-inputs")]
775 locked_input_policy: LockedInputPolicy::Exclude,
776 _ds_type: PhantomData,
777 }
778 }
779
780 #[cfg(feature = "transparent-inputs")]
792 pub fn with_shielding_block_space_percent(mut self, percent: u32) -> Self {
793 self.shielding_block_space_percent = percent.min(100);
794 self
795 }
796
797 #[cfg(feature = "transparent-inputs")]
804 pub fn with_locked_input_policy(mut self, policy: LockedInputPolicy) -> Self {
805 self.locked_input_policy = policy;
806 self
807 }
808
809 #[cfg(feature = "transparent-inputs")]
810 #[allow(clippy::too_many_arguments)]
811 #[allow(clippy::type_complexity)]
812 fn gather_transparent<ChangeT>(
813 &self,
814 wallet_db: &DbT,
815 target_height: TargetHeight,
816 confirmations_policy: ConfirmationsPolicy,
817 account: <DbT as InputSource>::AccountId,
818 address_allow_list: Option<&[TransparentAddress]>,
821 coinbase: CoinbaseFilter,
823 transaction_request: &TransactionRequest,
824 amount_at_transparent_gather: &mut Zatoshis,
825 locked_input_policy: &LockedInputPolicy,
826 ) -> Result<
827 Vec<WalletTransparentOutput<()>>,
828 InputSelectorError<
829 <DbT as InputSource>::Error,
830 <GreedyInputSelector<DbT> as InputSelector>::Error,
831 <ChangeT as ChangeStrategy>::Error,
832 <DbT as InputSource>::NoteRef,
833 >,
834 >
835 where
836 DbT: InputSource,
837 ChangeT: ChangeStrategy<MetaSource = DbT>,
838 {
839 let max_money = Zatoshis::const_from_u64(zcash_protocol::value::MAX_MONEY);
840 let mut total_opt: Option<Zatoshis> = Some(Zatoshis::ZERO);
841 for payment in transaction_request.payments().values() {
842 let Some(payment_amount) = payment.amount() else {
843 total_opt = None;
844 break;
845 };
846 if let Some(t) = total_opt {
847 match t + payment_amount {
848 Some(sum) => total_opt = Some(sum),
849 None => {
850 return Err(InputSelectorError::InsufficientFunds {
851 available: Zatoshis::ZERO,
852 required: max_money,
853 });
854 }
855 }
856 }
857 }
858 let (target_value, amount_at_gather) = match total_opt {
859 Some(z) => (TargetValue::AtLeast(z), z),
860 None => (
861 TargetValue::AllFunds(MaxSpendMode::MaxSpendable),
862 Zatoshis::ZERO,
863 ),
864 };
865 *amount_at_transparent_gather = amount_at_gather;
866 Ok(wallet_db
872 .select_spendable_transparent_outputs(
873 account,
874 target_height,
875 confirmations_policy,
876 coinbase,
877 address_allow_list,
878 target_value,
879 shielding_max_inputs(self.shielding_block_space_percent),
880 &StandardFeeRule::Zip317,
881 LockFilter::Policy(locked_input_policy),
882 )
883 .map_err(InputSelectorError::DataSource)?
884 .into_iter()
885 .map(|utxo| utxo.redact_account_data())
886 .collect::<Vec<_>>())
887 }
888}
889
890#[cfg(feature = "transparent-inputs")]
895fn shielding_max_inputs(block_space_percent: u32) -> usize {
896 (MAX_BLOCK_BYTES.saturating_mul(block_space_percent as usize) / 100) / P2PKH_STANDARD_INPUT_SIZE
897}
898
899impl<DbT> Default for GreedyInputSelector<DbT> {
900 fn default() -> Self {
901 Self::new()
902 }
903}
904
905impl<DbT: InputSource> InputSelector for GreedyInputSelector<DbT> {
906 type Error = GreedyInputSelectorError;
907 type InputSource = DbT;
908
909 #[allow(clippy::type_complexity)]
910 fn propose_transaction<ParamsT, ChangeT>(
911 &self,
912 params: &ParamsT,
913 wallet_db: &Self::InputSource,
914 target_height: TargetHeight,
915 anchor_height: BlockHeight,
916 zip318: &PoolMigrationParams,
917 confirmations_policy: ConfirmationsPolicy,
918 account: <DbT as InputSource>::AccountId,
919 transaction_request: TransactionRequest,
920 change_strategy: &ChangeT,
921 spend_policy: &SpendPolicy,
922 proposed_version: Option<TxVersion>,
923 ) -> Result<
924 Proposal<<ChangeT as ChangeStrategy>::FeeRule, DbT::NoteRef>,
925 InputSelectorError<<DbT as InputSource>::Error, Self::Error, ChangeT::Error, DbT::NoteRef>,
926 >
927 where
928 ParamsT: consensus::Parameters,
929 Self::InputSource: InputSource,
930 ChangeT: ChangeStrategy<MetaSource = DbT>,
931 {
932 let (sapling_supported, orchard_supported) =
933 proposed_version.map_or(Ok((true, true)), |v| {
934 let branch_id =
935 consensus::BranchId::for_height(params, BlockHeight::from(target_height));
936 if v.valid_in_branch(branch_id) {
937 Ok((
938 v.has_sapling(),
939 cfg!(feature = "orchard") && v.has_orchard(),
940 ))
941 } else {
942 Err(ProposalError::IncompatibleTxVersion(branch_id))
943 }
944 })?;
945 #[cfg(not(feature = "orchard"))]
948 let _ = orchard_supported;
949
950 let mut transparent_outputs = vec![];
951 let mut sapling_outputs = vec![];
952 #[cfg(feature = "orchard")]
953 let mut orchard_outputs = vec![];
954 let mut payment_pools = BTreeMap::new();
955
956 #[cfg(feature = "transparent-inputs")]
960 let mut tr1_transparent_outputs = vec![];
961 #[cfg(feature = "transparent-inputs")]
962 let mut tr1_payments = vec![];
963 #[cfg(feature = "transparent-inputs")]
964 let mut tr1_payment_pools = BTreeMap::new();
965 #[cfg(feature = "transparent-inputs")]
969 let mut total_ephemeral = Zatoshis::ZERO;
970
971 for (idx, payment) in transaction_request.payments() {
972 let payment_amount = payment
973 .amount()
974 .ok_or(ProposalError::PaymentAmountMissing(*idx))?;
975 let recipient_address: Address = payment
976 .recipient_address()
977 .clone()
978 .convert_if_network(params.network_type())?;
979
980 match recipient_address {
981 Address::Transparent(addr) => {
982 payment_pools.insert(*idx, PoolType::TRANSPARENT);
983 transparent_outputs.push(TxOut::new(payment_amount, addr.script().into()));
984 }
985 #[cfg(feature = "transparent-inputs")]
986 Address::Tex(data) => {
987 let p2pkh_addr = TransparentAddress::PublicKeyHash(data);
988
989 tr1_payment_pools.insert(*idx, PoolType::TRANSPARENT);
990 tr1_transparent_outputs
991 .push(TxOut::new(payment_amount, p2pkh_addr.script().into()));
992 tr1_payments.push(
993 Payment::new(
994 payment.recipient_address().clone(),
995 payment.amount(),
996 None,
997 payment.label().cloned(),
998 payment.message().cloned(),
999 payment.other_params().to_vec(),
1000 )
1001 .expect("cannot fail because memo is None and amount is nonzero"),
1002 );
1003 total_ephemeral = (total_ephemeral + payment_amount)
1004 .ok_or(GreedyInputSelectorError::Balance(BalanceError::Overflow))?;
1005 }
1006 #[cfg(not(feature = "transparent-inputs"))]
1007 Address::Tex(_) => {
1008 return Err(InputSelectorError::Selection(
1009 GreedyInputSelectorError::UnsupportedTexAddress,
1010 ));
1011 }
1012 Address::Sapling(_) => {
1013 payment_pools.insert(*idx, PoolType::SAPLING);
1014 sapling_outputs.push(SaplingPayment(payment_amount));
1015 }
1016 Address::Unified(addr) => {
1017 #[cfg(feature = "orchard")]
1018 if addr.has_orchard() && orchard_supported {
1019 let pool = if ironwood_active_at(params, target_height) {
1023 if let Some(v) = proposed_version
1030 && !v.has_ironwood()
1031 {
1032 return Err(
1033 ProposalError::OrchardReceiverRequiresIronwood(v).into()
1034 );
1035 }
1036 PoolType::IRONWOOD
1037 } else {
1038 PoolType::ORCHARD
1039 };
1040 payment_pools.insert(*idx, pool);
1041 orchard_outputs.push(OrchardPayment(payment_amount));
1042 continue;
1043 }
1044
1045 if addr.has_sapling() && sapling_supported {
1046 payment_pools.insert(*idx, PoolType::SAPLING);
1047 sapling_outputs.push(SaplingPayment(payment_amount));
1048 continue;
1049 }
1050
1051 if let Some(addr) = addr.transparent() {
1052 payment_pools.insert(*idx, PoolType::TRANSPARENT);
1053 transparent_outputs.push(TxOut::new(payment_amount, addr.script().into()));
1054 continue;
1055 }
1056
1057 return Err(InputSelectorError::Selection(
1058 GreedyInputSelectorError::UnsupportedAddress(addr),
1059 ));
1060 }
1061 }
1062 }
1063
1064 #[cfg(not(feature = "transparent-inputs"))]
1065 let transparent_inputs = vec![];
1066 #[cfg(feature = "transparent-inputs")]
1067 let mut amount_at_transparent_gather = Zatoshis::ZERO;
1068 #[cfg(feature = "transparent-inputs")]
1069 let mut transparent_inputs = match spend_policy.transparent() {
1070 None => {
1071 Vec::new()
1073 }
1074 Some(transparent) => {
1075 let address_allow_list = transparent.address_allow_list();
1076 self.gather_transparent::<ChangeT>(
1077 wallet_db,
1078 target_height,
1079 confirmations_policy,
1080 account,
1081 address_allow_list.as_deref(),
1082 transparent.coinbase().into(),
1083 &transaction_request,
1084 &mut amount_at_transparent_gather,
1085 spend_policy.locked_input_policy(),
1086 )?
1087 }
1088 };
1089 #[cfg(feature = "transparent-inputs")]
1094 let mut transparent_dust: BTreeSet<OutPoint> = BTreeSet::new();
1095
1096 let mut shielded_inputs = ReceivedNotes::empty();
1097 let mut amount_required = Zatoshis::ZERO;
1098 let mut exclude: Vec<DbT::NoteRef> = vec![];
1099
1100 #[cfg(feature = "orchard")]
1109 let mut pool_preference = selectable_pool_preference(
1110 params,
1111 target_height,
1112 sapling_supported,
1113 orchard_supported,
1114 !orchard_outputs.is_empty(),
1115 );
1116 #[cfg(not(feature = "orchard"))]
1117 let mut pool_preference = {
1118 let mut pools = vec![];
1119 if sapling_supported {
1120 pools.push(ShieldedPool::Sapling);
1121 }
1122 pools
1123 };
1124
1125 pool_preference.retain(|pool| spend_policy.permits_shielded(*pool));
1130
1131 loop {
1141 #[cfg(not(feature = "orchard"))]
1145 let sapling_bundle_required = true;
1146 #[cfg(not(feature = "orchard"))]
1147 let balance_input_value = shielded_inputs.total_value()?;
1148 #[cfg(feature = "orchard")]
1149 let (
1150 sapling_bundle_required,
1151 orchard_bundle_required,
1152 ironwood_bundle_required,
1153 balance_input_value,
1154 ) = {
1155 let pool_values = [
1160 (ShieldedPool::Sapling, shielded_inputs.sapling_value()?),
1161 (ShieldedPool::Orchard, shielded_inputs.orchard_value()?),
1162 (ShieldedPool::Ironwood, shielded_inputs.ironwood_value()?),
1163 ];
1164 let value_of = |pool: ShieldedPool| {
1165 pool_values
1166 .iter()
1167 .find(|(p, _)| *p == pool)
1168 .map(|(_, v)| *v)
1169 .expect("all shielded pools are present in pool_values")
1170 };
1171
1172 let (use_pools, trimmed_value) = if let Some(single) = pool_preference
1173 .iter()
1174 .find(|p| value_of(**p) >= amount_required)
1175 {
1176 (vec![*single], value_of(*single))
1177 } else {
1178 let mut running = Zatoshis::ZERO;
1179 let mut used = vec![];
1180 for pool in &pool_preference {
1181 if running >= amount_required {
1182 break;
1183 }
1184 running = (running + value_of(*pool))
1185 .ok_or(GreedyInputSelectorError::Balance(BalanceError::Overflow))?;
1186 used.push(*pool);
1187 }
1188 (used, running)
1189 };
1190
1191 (
1192 use_pools.contains(&ShieldedPool::Sapling),
1193 use_pools.contains(&ShieldedPool::Orchard),
1194 use_pools.contains(&ShieldedPool::Ironwood),
1195 trimmed_value,
1196 )
1197 };
1198
1199 let sapling_inputs = if sapling_bundle_required {
1200 shielded_inputs
1201 .sapling()
1202 .iter()
1203 .map(|i| (*i.internal_note_id(), i.note().value()))
1204 .collect()
1205 } else {
1206 vec![]
1207 };
1208
1209 #[cfg(feature = "orchard")]
1210 let orchard_inputs = if orchard_bundle_required {
1211 shielded_inputs
1212 .orchard()
1213 .iter()
1214 .map(|i| (*i.internal_note_id(), i.note().value()))
1215 .collect()
1216 } else {
1217 vec![]
1218 };
1219
1220 #[cfg(feature = "orchard")]
1223 let ironwood_inputs = if ironwood_bundle_required {
1224 shielded_inputs
1225 .ironwood()
1226 .iter()
1227 .map(|i| (*i.internal_note_id(), i.note().value()))
1228 .collect()
1229 } else {
1230 vec![]
1231 };
1232
1233 let selected_input_ids = sapling_inputs.iter().map(|(id, _)| id);
1234 #[cfg(feature = "orchard")]
1235 let selected_input_ids =
1236 selected_input_ids.chain(orchard_inputs.iter().map(|(id, _)| id));
1237 #[cfg(feature = "orchard")]
1238 let selected_input_ids =
1239 selected_input_ids.chain(ironwood_inputs.iter().map(|(id, _)| id));
1240
1241 let selected_input_ids = selected_input_ids.cloned().collect::<Vec<_>>();
1242
1243 let wallet_meta = change_strategy
1244 .fetch_wallet_meta(wallet_db, account, target_height, &selected_input_ids)
1245 .map_err(InputSelectorError::DataSource)?;
1246
1247 #[cfg(not(feature = "transparent-inputs"))]
1248 let ephemeral_output_value = None;
1249
1250 #[cfg(feature = "transparent-inputs")]
1251 let (ephemeral_output_value, tr1_balance_opt) = {
1252 if tr1_transparent_outputs.is_empty() {
1253 (None, None)
1254 } else {
1255 #[cfg(feature = "orchard")]
1263 let empty_orchard_view = (
1264 orchard_bundle_version_for_height(params, target_height),
1265 &[] as &[Infallible],
1266 &[] as &[Infallible],
1267 );
1268 #[cfg(feature = "orchard")]
1269 let empty_ironwood_view = (
1270 ironwood_bundle_version_for_height(params, target_height),
1271 &[] as &[Infallible],
1272 &[] as &[Infallible],
1273 );
1274
1275 let tr1_required_input_value = match change_strategy
1280 .compute_balance::<_, DbT::NoteRef>(
1281 params,
1282 target_height,
1283 anchor_height,
1284 zip318,
1285 &[] as &[WalletTransparentOutput<<DbT as InputSource>::AccountId>],
1286 &tr1_transparent_outputs,
1287 &sapling::EmptyBundleView,
1288 #[cfg(feature = "orchard")]
1289 &empty_orchard_view,
1290 #[cfg(feature = "orchard")]
1291 &empty_ironwood_view,
1292 Some(EphemeralBalance::Input(Zatoshis::ZERO)),
1293 &wallet_meta,
1294 ) {
1295 Err(ChangeError::InsufficientFunds { required, .. })
1296 if required.is_positive() =>
1297 {
1298 required
1299 }
1300 Err(ChangeError::InsufficientFunds { required, .. }) => {
1302 return Err(InputSelectorError::Selection(
1303 GreedyInputSelectorError::RequirementCovered {
1304 required,
1305 available: Zatoshis::ZERO,
1306 },
1307 ));
1308 }
1309 Err(ChangeError::DustInputs { .. }) => {
1311 return Err(InputSelectorError::Selection(
1312 GreedyInputSelectorError::DustInputsNotGiven,
1313 ));
1314 }
1315 Err(other) => return Err(InputSelectorError::Change(other)),
1316 Ok(_) => Zatoshis::ZERO, };
1318
1319 let tr1_balance = change_strategy.compute_balance::<_, DbT::NoteRef>(
1322 params,
1323 target_height,
1324 anchor_height,
1325 zip318,
1326 &[] as &[WalletTransparentOutput<<DbT as InputSource>::AccountId>],
1327 &tr1_transparent_outputs,
1328 &sapling::EmptyBundleView,
1329 #[cfg(feature = "orchard")]
1330 &empty_orchard_view,
1331 #[cfg(feature = "orchard")]
1332 &empty_ironwood_view,
1333 Some(EphemeralBalance::Input(tr1_required_input_value)),
1334 &wallet_meta,
1335 )?;
1336 assert_eq!(tr1_balance.total(), tr1_balance.fee_required());
1337
1338 (Some(tr1_required_input_value), Some(tr1_balance))
1339 }
1340 };
1341
1342 #[cfg(feature = "orchard")]
1348 let orchard_view = (
1349 orchard_bundle_version_for_height(params, target_height),
1350 &orchard_inputs[..],
1351 if ironwood_active_at(params, target_height) {
1352 &[]
1353 } else {
1354 &orchard_outputs[..]
1355 },
1356 );
1357 #[cfg(feature = "orchard")]
1358 let ironwood_view = (
1359 ironwood_bundle_version_for_height(params, target_height),
1360 &ironwood_inputs[..],
1361 if ironwood_active_at(params, target_height) {
1362 &orchard_outputs[..]
1363 } else {
1364 &[]
1365 },
1366 );
1367
1368 #[cfg(not(feature = "transparent-inputs"))]
1381 let transparent_inputs_changed = false;
1382 #[cfg(feature = "transparent-inputs")]
1383 let mut transparent_inputs_changed = false;
1384 let mut shielded_dust_excluded = false;
1388
1389 let tr0_balance = change_strategy.compute_balance(
1392 params,
1393 target_height,
1394 anchor_height,
1395 zip318,
1396 &transparent_inputs,
1397 &transparent_outputs,
1398 &(
1399 ::sapling::builder::BundleType::DEFAULT,
1400 &sapling_inputs[..],
1401 &sapling_outputs[..],
1402 ),
1403 #[cfg(feature = "orchard")]
1404 &orchard_view,
1405 #[cfg(feature = "orchard")]
1406 &ironwood_view,
1407 ephemeral_output_value.map(EphemeralBalance::Output),
1408 &wallet_meta,
1409 );
1410
1411 match tr0_balance {
1412 Ok(tr0_balance) => {
1413 let shielded_inputs =
1416 NonEmpty::from_vec(shielded_inputs.into_vec(&SimpleNoteRetention {
1417 sapling: sapling_bundle_required,
1418 #[cfg(feature = "orchard")]
1419 orchard: orchard_bundle_required,
1420 #[cfg(feature = "orchard")]
1421 ironwood: ironwood_bundle_required,
1422 }))
1423 .map(ShieldedInputs::from_parts);
1424
1425 return build_proposal(
1426 change_strategy.fee_rule(),
1427 tr0_balance,
1428 target_height,
1429 anchor_height,
1430 confirmations_policy,
1431 shielded_inputs,
1432 transparent_inputs,
1433 transaction_request,
1434 payment_pools,
1435 #[cfg(feature = "orchard")]
1436 ironwood_active_at(params, target_height),
1437 #[cfg(feature = "transparent-inputs")]
1438 ephemeral_output_value.zip(tr1_balance_opt).map(
1439 |(ephemeral_output_value, tr1_balance)| EphemeralStepConfig {
1440 ephemeral_output_value,
1441 tr1_balance,
1442 tr1_payments,
1443 tr1_payment_pools,
1444 },
1445 ),
1446 )
1447 .map_err(InputSelectorError::Proposal);
1448 }
1449 Err(ChangeError::DustInputs {
1450 #[cfg(feature = "transparent-inputs")]
1451 transparent,
1452 mut sapling,
1453 #[cfg(feature = "orchard")]
1454 mut orchard,
1455 #[cfg(feature = "orchard")]
1456 mut ironwood,
1457 ..
1458 }) => {
1459 let named = sapling.iter();
1462 #[cfg(feature = "orchard")]
1463 let named = named.chain(orchard.iter()).chain(ironwood.iter());
1464 let mut other_named = false;
1465 for note in named {
1466 if selected_input_ids.contains(note) {
1467 shielded_dust_excluded = true;
1468 } else {
1469 other_named = true;
1470 }
1471 }
1472 exclude.append(&mut sapling);
1473 #[cfg(feature = "orchard")]
1474 exclude.append(&mut orchard);
1475 #[cfg(feature = "orchard")]
1476 exclude.append(&mut ironwood);
1477 #[cfg(feature = "transparent-inputs")]
1478 {
1479 let len_before = transparent_inputs.len();
1480 other_named |= transparent.iter().any(|outpoint| {
1481 !transparent_inputs
1482 .iter()
1483 .any(|input| input.outpoint() == outpoint)
1484 });
1485 transparent_dust.extend(transparent);
1486 transparent_inputs.retain(|i| !transparent_dust.contains(i.outpoint()));
1487 if transparent_inputs.len() != len_before {
1491 transparent_inputs_changed = true;
1492 }
1493 }
1494 if other_named || (!shielded_dust_excluded && !transparent_inputs_changed) {
1497 return Err(InputSelectorError::Selection(
1498 GreedyInputSelectorError::DustInputsNotGiven,
1499 ));
1500 }
1501 }
1502 Err(ChangeError::InsufficientFunds { required, .. }) => {
1503 let available = available_value(balance_input_value, &transparent_inputs)?;
1505 if required <= available {
1506 return Err(InputSelectorError::Selection(
1507 GreedyInputSelectorError::RequirementCovered {
1508 required,
1509 available,
1510 },
1511 ));
1512 }
1513 amount_required = required;
1514 #[cfg(feature = "transparent-inputs")]
1520 if let Some(transparent) = spend_policy.transparent()
1521 && required > amount_at_transparent_gather
1522 {
1523 let address_allow_list = transparent.address_allow_list();
1524 transparent_inputs = wallet_db
1527 .select_spendable_transparent_outputs(
1528 account,
1529 target_height,
1530 confirmations_policy,
1531 transparent.coinbase().into(),
1532 address_allow_list.as_deref(),
1533 TargetValue::AtLeast(required),
1534 shielding_max_inputs(self.shielding_block_space_percent),
1535 &StandardFeeRule::Zip317,
1536 LockFilter::Policy(spend_policy.locked_input_policy()),
1537 )
1538 .map_err(InputSelectorError::DataSource)?
1539 .into_iter()
1540 .filter(|utxo| !transparent_dust.contains(utxo.outpoint()))
1545 .map(|utxo| utxo.redact_account_data())
1546 .collect::<Vec<_>>();
1547 amount_at_transparent_gather = required;
1548 transparent_inputs_changed = true;
1549 }
1550 }
1551 Err(other) => return Err(InputSelectorError::Change(other)),
1552 }
1553
1554 let single_note = match spend_policy.note_selection() {
1571 NoteSelection::PreferSingle => Some(
1572 wallet_db
1573 .select_single_spendable_note(
1574 account,
1575 amount_required,
1576 &pool_preference,
1577 target_height,
1578 confirmations_policy,
1579 &exclude,
1580 LockFilter::Policy(spend_policy.locked_input_policy()),
1581 )
1582 .map_err(InputSelectorError::DataSource)?,
1583 )
1584 .filter(|notes| !notes.is_empty()),
1585 NoteSelection::Accumulate => None,
1586 };
1587 shielded_inputs = match single_note {
1588 Some(single) => single,
1589 None => wallet_db
1590 .select_spendable_notes(
1591 account,
1592 TargetValue::AtLeast(amount_required),
1593 &pool_preference,
1594 target_height,
1595 confirmations_policy,
1596 &exclude,
1597 LockFilter::Policy(spend_policy.locked_input_policy()),
1598 )
1599 .map_err(InputSelectorError::DataSource)?,
1600 };
1601
1602 check_selection(&shielded_inputs, &pool_preference, &exclude)?;
1606
1607 let new_available = shielded_inputs.total_value()?;
1611 if new_available <= balance_input_value
1612 && !transparent_inputs_changed
1613 && !shielded_dust_excluded
1614 {
1615 return Err(InputSelectorError::InsufficientFunds {
1616 required: amount_required,
1617 available: available_value(balance_input_value, &transparent_inputs)?,
1618 });
1619 }
1620 }
1621 }
1622}
1623
1624fn available_value<AccountId>(
1627 shielded: Zatoshis,
1628 transparent_inputs: &[WalletTransparentOutput<AccountId>],
1629) -> Result<Zatoshis, BalanceError> {
1630 let transparent = transparent_inputs
1631 .iter()
1632 .map(|input| input.value())
1633 .sum::<Option<Zatoshis>>()
1634 .ok_or(BalanceError::Overflow)?;
1635 (shielded + transparent).ok_or(BalanceError::Overflow)
1636}
1637
1638fn check_selection<NoteRef: PartialEq>(
1641 notes: &ReceivedNotes<NoteRef>,
1642 sources: &[ShieldedPool],
1643 exclude: &[NoteRef],
1644) -> Result<(), GreedyInputSelectorError> {
1645 fn check_pool<NoteRef: PartialEq, NoteT>(
1646 notes: &[ReceivedNote<NoteRef, NoteT>],
1647 pool: ShieldedPool,
1648 sources: &[ShieldedPool],
1649 exclude: &[NoteRef],
1650 ) -> Result<(), GreedyInputSelectorError> {
1651 if !notes.is_empty() && !sources.contains(&pool) {
1652 return Err(GreedyInputSelectorError::UnrequestedPoolSelected);
1653 }
1654 if notes
1655 .iter()
1656 .any(|note| exclude.contains(note.internal_note_id()))
1657 {
1658 return Err(GreedyInputSelectorError::ExcludedNoteSelected);
1659 }
1660 Ok(())
1661 }
1662 check_pool(notes.sapling(), ShieldedPool::Sapling, sources, exclude)?;
1663 #[cfg(feature = "orchard")]
1664 {
1665 check_pool(notes.orchard(), ShieldedPool::Orchard, sources, exclude)?;
1666 check_pool(notes.ironwood(), ShieldedPool::Ironwood, sources, exclude)?;
1667 }
1668 Ok(())
1669}
1670
1671#[cfg(feature = "orchard")]
1681fn selectable_pool_preference<ParamsT: consensus::Parameters>(
1682 params: &ParamsT,
1683 target_height: TargetHeight,
1684 sapling_supported: bool,
1685 orchard_supported: bool,
1686 prefer_orchard_family: bool,
1687) -> Vec<ShieldedPool> {
1688 let ironwood_selectable = orchard_supported && ironwood_active_at(params, target_height);
1689 let mut preference = Vec::with_capacity(3);
1690 if prefer_orchard_family {
1691 if ironwood_selectable {
1692 preference.push(ShieldedPool::Ironwood);
1693 }
1694 if orchard_supported {
1695 preference.push(ShieldedPool::Orchard);
1696 }
1697 if sapling_supported {
1698 preference.push(ShieldedPool::Sapling);
1699 }
1700 } else {
1701 if sapling_supported {
1702 preference.push(ShieldedPool::Sapling);
1703 }
1704 if ironwood_selectable {
1705 preference.push(ShieldedPool::Ironwood);
1706 }
1707 if orchard_supported {
1708 preference.push(ShieldedPool::Orchard);
1709 }
1710 }
1711 preference
1712}
1713
1714#[cfg(feature = "orchard")]
1717fn orchard_bundle_version_for_height<ParamsT: consensus::Parameters, H: Into<BlockHeight>>(
1718 params: &ParamsT,
1719 target_height: H,
1720) -> ::orchard::bundle::BundleVersion {
1721 zcash_primitives::transaction::components::orchard::bundle_version_for_branch(
1722 consensus::BranchId::for_height(params, target_height.into()),
1723 ::orchard::ValuePool::Orchard,
1724 )
1725 .unwrap_or(::orchard::bundle::BundleVersion::orchard_insecure_v1())
1729}
1730
1731#[cfg(feature = "orchard")]
1734fn ironwood_bundle_version_for_height<ParamsT: consensus::Parameters, H: Into<BlockHeight>>(
1735 params: &ParamsT,
1736 target_height: H,
1737) -> ::orchard::bundle::BundleVersion {
1738 zcash_primitives::transaction::components::orchard::bundle_version_for_branch(
1739 consensus::BranchId::for_height(params, target_height.into()),
1740 ::orchard::ValuePool::Ironwood,
1741 )
1742 .unwrap_or(::orchard::bundle::BundleVersion::ironwood_v3())
1746}
1747
1748#[allow(clippy::type_complexity, clippy::too_many_arguments)]
1749pub(crate) fn propose_send_max<ParamsT, InputSourceT, FeeRuleT>(
1750 params: &ParamsT,
1751 wallet_db: &InputSourceT,
1752 fee_rule: &FeeRuleT,
1753 source_account: InputSourceT::AccountId,
1754 spend_pools: &[ShieldedPool],
1755 target_height: TargetHeight,
1756 anchor_height: BlockHeight,
1757 mode: MaxSpendMode,
1758 confirmations_policy: ConfirmationsPolicy,
1759 recipient: ZcashAddress,
1760 memo: Option<MemoBytes>,
1761 locked_input_policy: &LockedInputPolicy,
1762) -> Result<
1763 Proposal<FeeRuleT, InputSourceT::NoteRef>,
1764 InputSelectorError<
1765 InputSourceT::Error,
1766 GreedyInputSelectorError,
1767 FeeRuleT::Error,
1768 InputSourceT::NoteRef,
1769 >,
1770>
1771where
1772 ParamsT: consensus::Parameters,
1773 InputSourceT: InputSource,
1774 FeeRuleT: FeeRule + Clone,
1775{
1776 let spendable_notes = wallet_db
1782 .select_spendable_notes(
1783 source_account,
1784 TargetValue::AllFunds(mode),
1785 spend_pools,
1786 target_height,
1787 confirmations_policy,
1788 &[],
1789 LockFilter::Policy(locked_input_policy),
1790 )
1791 .map_err(InputSelectorError::DataSource)?;
1792
1793 let input_total = spendable_notes
1794 .total_value()
1795 .map_err(|e| InputSelectorError::Selection(GreedyInputSelectorError::Balance(e)))?;
1796
1797 let mut payment_pools = BTreeMap::new();
1798
1799 #[cfg(feature = "orchard")]
1804 let orchard_receiver_payable = recipient.can_receive_as(PoolType::ORCHARD);
1805 #[cfg(not(feature = "orchard"))]
1806 let orchard_receiver_payable = false;
1807
1808 let sapling_output_count = {
1809 let requested_sapling_outputs: usize =
1812 if recipient.can_receive_as(PoolType::SAPLING) && !orchard_receiver_payable {
1813 payment_pools.insert(0, PoolType::SAPLING);
1814 1
1815 } else {
1816 0
1817 };
1818
1819 ::sapling::builder::BundleType::DEFAULT
1820 .num_outputs(spendable_notes.sapling.len(), requested_sapling_outputs)
1821 .map_err(|s| InputSelectorError::Change(ChangeError::BundleError(s)))?
1822 };
1823
1824 let sapling_bundle_required = !spendable_notes.sapling().is_empty() || sapling_output_count > 0;
1825
1826 #[cfg(feature = "orchard")]
1830 let orchard_receivers_fill_ironwood = ironwood_active_at(params, target_height);
1831 #[cfg(feature = "orchard")]
1832 if orchard_receiver_payable {
1833 payment_pools.insert(
1834 0,
1835 if orchard_receivers_fill_ironwood {
1836 PoolType::IRONWOOD
1837 } else {
1838 PoolType::ORCHARD
1839 },
1840 );
1841 }
1842
1843 #[cfg(feature = "orchard")]
1844 let orchard_action_count = orchard_fees::transactional_action_count(
1845 ::orchard::builder::BundleType::DEFAULT,
1848 orchard_bundle_version_for_height(params, target_height),
1849 spendable_notes.orchard.len(),
1850 usize::from(orchard_receiver_payable && !orchard_receivers_fill_ironwood),
1851 )
1852 .map_err(|e| InputSelectorError::Change(ChangeError::BundleError(e)))?;
1853 #[cfg(not(feature = "orchard"))]
1854 let orchard_action_count: usize = 0;
1855
1856 #[cfg(feature = "orchard")]
1857 let orchard_bundle_required = orchard_action_count > 0;
1858
1859 #[cfg(feature = "orchard")]
1860 let ironwood_action_count = orchard_fees::transactional_action_count(
1861 ::orchard::builder::BundleType::DEFAULT,
1862 ironwood_bundle_version_for_height(params, target_height),
1863 spendable_notes.ironwood.len(),
1864 usize::from(orchard_receiver_payable && orchard_receivers_fill_ironwood),
1865 )
1866 .map_err(|s| InputSelectorError::Change(ChangeError::BundleError(s)))?;
1867 #[cfg(not(feature = "orchard"))]
1868 let ironwood_action_count: usize = 0;
1869
1870 #[cfg(feature = "orchard")]
1871 let ironwood_bundle_required = ironwood_action_count > 0;
1872
1873 let recipient_address: Address = recipient
1874 .clone()
1875 .convert_if_network(params.network_type())?;
1876
1877 let pays_transparent_directly = match &recipient_address {
1883 Address::Transparent(_) => true,
1884 Address::Unified(addr) => {
1885 addr.has_transparent() && !(addr.has_sapling() || addr.has_orchard())
1886 }
1887 _ => false,
1888 };
1889 if pays_transparent_directly {
1890 payment_pools.insert(0, PoolType::Transparent);
1891 }
1892
1893 if payment_pools.is_empty()
1899 && let Address::Unified(addr) = &recipient_address
1900 {
1901 return Err(InputSelectorError::Selection(
1902 GreedyInputSelectorError::UnsupportedAddress(addr.clone()),
1903 ));
1904 }
1905
1906 let (tr0_fee, tr1_fee) = match recipient_address {
1907 Address::Sapling(_) => fee_rule
1908 .fee_required(
1909 params,
1910 BlockHeight::from(target_height),
1911 [],
1912 [],
1913 spendable_notes.sapling().len(),
1914 sapling_output_count,
1915 orchard_action_count,
1916 ironwood_action_count,
1917 )
1918 .map(|fee| (fee, None)),
1919 Address::Transparent(_) => fee_rule
1920 .fee_required(
1921 params,
1922 BlockHeight::from(target_height),
1923 [],
1924 [P2PKH_STANDARD_OUTPUT_SIZE],
1925 spendable_notes.sapling().len(),
1926 sapling_output_count,
1927 orchard_action_count,
1928 ironwood_action_count,
1929 )
1930 .map(|fee| (fee, None)),
1931 Address::Unified(_) => fee_rule
1932 .fee_required(
1933 params,
1934 BlockHeight::from(target_height),
1935 [],
1936 if pays_transparent_directly {
1937 vec![P2PKH_STANDARD_OUTPUT_SIZE]
1938 } else {
1939 vec![]
1940 },
1941 spendable_notes.sapling().len(),
1942 sapling_output_count,
1943 orchard_action_count,
1944 ironwood_action_count,
1945 )
1946 .map(|fee| (fee, None)),
1947 #[cfg(not(feature = "transparent-inputs"))]
1951 Address::Tex(_) => {
1952 return Err(InputSelectorError::Selection(
1953 GreedyInputSelectorError::UnsupportedTexAddress,
1954 ));
1955 }
1956 #[cfg(feature = "transparent-inputs")]
1957 Address::Tex(_) => fee_rule
1958 .fee_required(
1959 params,
1960 BlockHeight::from(target_height),
1961 [],
1962 [P2PKH_STANDARD_OUTPUT_SIZE],
1963 spendable_notes.sapling().len(),
1964 sapling_output_count,
1965 orchard_action_count,
1966 ironwood_action_count,
1967 )
1968 .and_then(|tr0_fee| {
1969 let tr1_fee = fee_rule.fee_required(
1970 params,
1971 BlockHeight::from(target_height),
1972 [InputSize::Known(P2PKH_STANDARD_INPUT_SIZE)],
1973 [P2PKH_STANDARD_OUTPUT_SIZE],
1974 0,
1975 0,
1976 0,
1977 0,
1978 )?;
1979
1980 Ok((tr0_fee, Some(tr1_fee)))
1981 }),
1982 }
1983 .map_err(|fee_error| InputSelectorError::Change(ChangeError::StrategyError(fee_error)))?;
1984
1985 let total_fee_required = (tr0_fee + tr1_fee.unwrap_or(Zatoshis::ZERO)).ok_or(
1991 InputSelectorError::Selection(GreedyInputSelectorError::Balance(BalanceError::Overflow)),
1992 )?;
1993
1994 let total_to_recipient = (input_total - total_fee_required)
2001 .filter(|amount| *amount > Zatoshis::ZERO)
2002 .ok_or(InputSelectorError::InsufficientFunds {
2003 available: input_total,
2004 required: (total_fee_required + Zatoshis::const_from_u64(1))
2005 .unwrap_or(Zatoshis::const_from_u64(MAX_MONEY)),
2006 })?;
2007
2008 #[cfg(feature = "transparent-inputs")]
2013 let ephemeral_output_value =
2014 tr1_fee.map(|fee| (total_to_recipient + fee).expect("overflow already checked"));
2015
2016 #[cfg(feature = "transparent-inputs")]
2017 let tr0_change = ephemeral_output_value
2018 .into_iter()
2019 .map(ChangeValue::ephemeral_transparent)
2020 .collect();
2021 #[cfg(not(feature = "transparent-inputs"))]
2022 let tr0_change = vec![];
2023
2024 let tr0_balance = TransactionBalance::new(tr0_change, tr0_fee)
2027 .expect("the sum of an single-element vector of fee values cannot overflow");
2028
2029 let payment = zip321::Payment::new(
2030 recipient,
2031 Some(total_to_recipient),
2032 memo,
2033 None,
2034 None,
2035 vec![],
2036 )
2037 .map_err(|e| InputSelectorError::Proposal(ProposalError::Zip321(e.with_index(0))))?;
2038
2039 let transaction_request =
2040 TransactionRequest::new(vec![payment.clone()]).map_err(|payment_error| {
2041 InputSelectorError::Proposal(ProposalError::Zip321(payment_error))
2042 })?;
2043
2044 let shielded_inputs = NonEmpty::from_vec(spendable_notes.into_vec(&SimpleNoteRetention {
2045 sapling: sapling_bundle_required,
2046 #[cfg(feature = "orchard")]
2047 orchard: orchard_bundle_required,
2048 #[cfg(feature = "orchard")]
2049 ironwood: ironwood_bundle_required,
2050 }))
2051 .map(ShieldedInputs::from_parts);
2052
2053 build_proposal(
2054 fee_rule,
2055 tr0_balance,
2056 target_height,
2057 anchor_height,
2058 confirmations_policy,
2059 shielded_inputs,
2060 vec![],
2061 transaction_request,
2062 payment_pools,
2063 #[cfg(feature = "orchard")]
2064 ironwood_active_at(params, target_height),
2065 #[cfg(feature = "transparent-inputs")]
2066 ephemeral_output_value
2067 .zip(tr1_fee)
2068 .map(|(ephemeral_output_value, tr1_fee)| EphemeralStepConfig {
2069 ephemeral_output_value,
2070 tr1_balance: TransactionBalance::new(vec![], tr1_fee)
2071 .expect("the sum of an empty vector of fee values cannot overflow"),
2072 tr1_payments: vec![payment],
2073 tr1_payment_pools: BTreeMap::from_iter([(0, PoolType::Transparent)]),
2074 }),
2075 )
2076 .map_err(InputSelectorError::Proposal)
2077}
2078
2079#[cfg(feature = "transparent-inputs")]
2080struct EphemeralStepConfig {
2081 ephemeral_output_value: Zatoshis,
2082 tr1_balance: TransactionBalance,
2083 tr1_payments: Vec<Payment>,
2084 tr1_payment_pools: BTreeMap<usize, PoolType>,
2085}
2086
2087#[allow(clippy::too_many_arguments)]
2088fn build_proposal<FeeRuleT: FeeRule + Clone, NoteRef>(
2089 fee_rule: &FeeRuleT,
2090 tr0_balance: TransactionBalance,
2091 target_height: TargetHeight,
2092 anchor_height: BlockHeight,
2093 confirmations_policy: ConfirmationsPolicy,
2094 shielded_inputs: Option<ShieldedInputs<NoteRef>>,
2095 transparent_inputs: Vec<WalletTransparentOutput<()>>,
2096 transaction_request: TransactionRequest,
2097 payment_pools: BTreeMap<usize, PoolType>,
2098 #[cfg(feature = "orchard")] ironwood_active: bool,
2099 #[cfg(feature = "transparent-inputs")] ephemeral_step_opt: Option<EphemeralStepConfig>,
2100) -> Result<Proposal<FeeRuleT, NoteRef>, ProposalError> {
2101 #[cfg(feature = "transparent-inputs")]
2102 if let Some(ephemeral_step) = ephemeral_step_opt {
2103 let tr1_balance = ephemeral_step.tr1_balance;
2104 let ephemeral_outputs = tr0_balance
2111 .proposed_change()
2112 .iter()
2113 .enumerate()
2114 .filter(|(_, c)| c.is_ephemeral())
2115 .collect::<Vec<_>>();
2116
2117 let ephemeral_output_index = match &ephemeral_outputs[..] {
2118 [(i, change_value)]
2119 if change_value.value() == ephemeral_step.ephemeral_output_value =>
2120 {
2121 Ok(*i)
2122 }
2123 _ => Err(ProposalError::EphemeralOutputsInvalid),
2124 }?;
2125
2126 let ephemeral_stepoutput =
2127 StepOutput::new(0, StepOutputIndex::Change(ephemeral_output_index));
2128
2129 let tr0 = TransactionRequest::from_indexed(
2130 transaction_request
2131 .payments()
2132 .iter()
2133 .filter(|(idx, _payment)| !ephemeral_step.tr1_payment_pools.contains_key(idx))
2134 .map(|(k, v)| (*k, v.clone()))
2135 .collect(),
2136 )
2137 .expect("removing payments from a TransactionRequest preserves validity");
2138
2139 let mut steps = vec![];
2140 let step0 = Step::from_parts(
2141 &[],
2142 tr0,
2143 payment_pools,
2144 transparent_inputs,
2145 shielded_inputs,
2146 Some(anchor_height),
2147 vec![],
2148 tr0_balance,
2149 false,
2150 #[cfg(feature = "orchard")]
2151 ironwood_active,
2152 )?;
2153 steps.push(step0);
2154
2155 let tr1 =
2156 TransactionRequest::new(ephemeral_step.tr1_payments).expect("valid by construction");
2157 let step1 = Step::from_parts(
2158 &steps,
2159 tr1,
2160 ephemeral_step.tr1_payment_pools,
2161 vec![],
2162 None,
2163 Some(anchor_height),
2164 vec![ephemeral_stepoutput],
2165 tr1_balance,
2166 false,
2167 #[cfg(feature = "orchard")]
2168 ironwood_active,
2169 )?;
2170 steps.push(step1);
2171
2172 return Proposal::multi_step(
2173 fee_rule.clone(),
2174 target_height,
2175 confirmations_policy,
2176 NonEmpty::from_vec(steps).expect("steps is known to be nonempty"),
2177 );
2178 }
2179
2180 Proposal::single_step(
2181 transaction_request,
2182 payment_pools,
2183 transparent_inputs,
2184 shielded_inputs,
2185 anchor_height,
2186 tr0_balance,
2187 fee_rule.clone(),
2188 target_height,
2189 confirmations_policy,
2190 false,
2191 #[cfg(feature = "orchard")]
2192 ironwood_active,
2193 )
2194}
2195
2196#[cfg(feature = "transparent-inputs")]
2197impl<DbT: InputSource> ShieldingSelector for GreedyInputSelector<DbT> {
2198 type Error = GreedyInputSelectorError;
2199 type InputSource = DbT;
2200
2201 #[allow(clippy::type_complexity)]
2202 fn propose_shielding<ParamsT, ChangeT>(
2203 &self,
2204 params: &ParamsT,
2205 wallet_db: &Self::InputSource,
2206 change_strategy: &ChangeT,
2207 shielding_threshold: Zatoshis,
2208 source_addrs: &[TransparentAddress],
2209 to_account: <Self::InputSource as InputSource>::AccountId,
2210 target_height: TargetHeight,
2211 anchor_height: BlockHeight,
2212 zip318: &PoolMigrationParams,
2213 confirmations_policy: ConfirmationsPolicy,
2214 output_filter: CoinbaseFilter,
2215 ) -> Result<
2216 Proposal<<ChangeT as ChangeStrategy>::FeeRule, Infallible>,
2217 InputSelectorError<<DbT as InputSource>::Error, Self::Error, ChangeT::Error, Infallible>,
2218 >
2219 where
2220 ParamsT: consensus::Parameters,
2221 ChangeT: ChangeStrategy<MetaSource = Self::InputSource>,
2222 {
2223 let mut transparent_inputs = gather_shielding_inputs::<DbT, ChangeT::Error>(
2224 wallet_db,
2225 source_addrs,
2226 target_height,
2227 confirmations_policy,
2228 output_filter,
2229 shielding_max_inputs(self.shielding_block_space_percent),
2230 &self.locked_input_policy,
2231 )?;
2232
2233 let wallet_meta = change_strategy
2234 .fetch_wallet_meta(wallet_db, to_account, target_height, &[])
2235 .map_err(InputSelectorError::DataSource)?;
2236
2237 let balance = compute_shielding_balance_with_dust_retry::<DbT, ChangeT, ParamsT>(
2238 change_strategy,
2239 params,
2240 target_height,
2241 anchor_height,
2242 zip318,
2243 &mut transparent_inputs,
2244 &wallet_meta,
2245 )?;
2246
2247 if balance.total() >= shielding_threshold {
2248 Proposal::single_step(
2249 TransactionRequest::empty(),
2250 BTreeMap::new(),
2251 transparent_inputs,
2252 None,
2253 anchor_height,
2254 balance,
2255 (*change_strategy.fee_rule()).clone(),
2256 target_height,
2257 confirmations_policy,
2258 true,
2259 #[cfg(feature = "orchard")]
2260 ironwood_active_at(params, target_height),
2261 )
2262 .map_err(InputSelectorError::Proposal)
2263 } else {
2264 Err(InputSelectorError::InsufficientFunds {
2265 available: balance.total(),
2266 required: shielding_threshold,
2267 })
2268 }
2269 }
2270
2271 #[allow(clippy::type_complexity)]
2272 fn propose_shielding_coinbase<ParamsT, FeeRuleT>(
2273 &self,
2274 params: &ParamsT,
2275 wallet_db: &Self::InputSource,
2276 fee_rule: &FeeRuleT,
2277 shielding_threshold: Zatoshis,
2278 source_addrs: &[TransparentAddress],
2279 to_address: ZcashAddress,
2280 memo: Option<MemoBytes>,
2281 limit: Option<usize>,
2282 target_height: TargetHeight,
2283 anchor_height: BlockHeight,
2284 ) -> Result<
2285 Proposal<FeeRuleT, Infallible>,
2286 InputSelectorError<<DbT as InputSource>::Error, Self::Error, FeeRuleT::Error, Infallible>,
2287 >
2288 where
2289 ParamsT: consensus::Parameters,
2290 FeeRuleT: FeeRule + Clone,
2291 {
2292 let transparent_inputs = gather_shielding_inputs::<DbT, FeeRuleT::Error>(
2302 wallet_db,
2303 source_addrs,
2304 target_height,
2305 ConfirmationsPolicy::MIN,
2308 CoinbaseFilter::CoinbaseOnly,
2309 limit
2310 .unwrap_or(usize::MAX)
2311 .min(shielding_max_inputs(self.shielding_block_space_percent)),
2312 &self.locked_input_policy,
2313 )?;
2314
2315 let destination_pool = resolve_shielded_destination::<DbT, FeeRuleT::Error, ParamsT>(
2316 &to_address,
2317 params,
2318 target_height,
2319 )?;
2320
2321 let (sapling_output_count, orchard_action_count, ironwood_action_count) =
2322 match destination_pool {
2323 PoolType::SAPLING => {
2324 let count = ::sapling::builder::BundleType::DEFAULT
2325 .num_outputs(0, 1)
2326 .expect("sapling DEFAULT bundle type permits any (spends, outputs) count");
2327 (count, 0usize, 0usize)
2328 }
2329 #[cfg(feature = "orchard")]
2332 PoolType::ORCHARD => {
2333 let count = orchard_fees::transactional_action_count(
2334 ::orchard::builder::BundleType::DEFAULT,
2335 orchard_bundle_version_for_height(params, target_height),
2336 0,
2337 1,
2338 )
2339 .expect("every Orchard bundle version permits spending and output creation");
2340 (0usize, count, 0usize)
2341 }
2342 #[cfg(feature = "orchard")]
2345 PoolType::IRONWOOD => {
2346 let count = orchard_fees::transactional_action_count(
2347 ::orchard::builder::BundleType::DEFAULT,
2348 ironwood_bundle_version_for_height(params, target_height),
2349 0,
2350 1,
2351 )
2352 .expect("the Ironwood bundle version permits spending and output creation");
2353 (0usize, 0usize, count)
2354 }
2355 _ => {
2358 return Err(InputSelectorError::Proposal(
2359 ProposalError::ShieldingRequiresShieldedRecipient,
2360 ));
2361 }
2362 };
2363
2364 let fee = fee_rule
2365 .fee_required(
2366 params,
2367 BlockHeight::from(target_height),
2368 transparent_inputs
2369 .iter()
2370 .map(transparent_fees::InputView::serialized_size),
2371 std::iter::empty::<usize>(),
2372 0,
2373 sapling_output_count,
2374 orchard_action_count,
2375 ironwood_action_count,
2376 )
2377 .map_err(|e| InputSelectorError::Change(ChangeError::StrategyError(e)))?;
2383
2384 let input_total = transparent_inputs
2387 .iter()
2388 .map(|utxo| utxo.value())
2389 .try_fold(Zatoshis::ZERO, |acc, v| acc + v)
2390 .ok_or(InputSelectorError::Selection(
2391 GreedyInputSelectorError::Balance(BalanceError::Overflow),
2392 ))?;
2393 let payment_amount =
2394 (input_total - fee).ok_or_else(|| InputSelectorError::InsufficientFunds {
2395 available: input_total,
2396 required: fee,
2397 })?;
2398
2399 if payment_amount < shielding_threshold {
2400 return Err(InputSelectorError::InsufficientFunds {
2401 available: payment_amount,
2402 required: shielding_threshold,
2403 });
2404 }
2405
2406 let payment = Payment::new(to_address, Some(payment_amount), memo, None, None, vec![])
2407 .map_err(|payment_error| {
2408 InputSelectorError::Proposal(ProposalError::Zip321(payment_error.with_index(0)))
2409 })?;
2410 let request = TransactionRequest::new(vec![payment]).map_err(|payment_error| {
2411 InputSelectorError::Proposal(ProposalError::Zip321(payment_error))
2412 })?;
2413 let mut payment_pools = BTreeMap::new();
2414 payment_pools.insert(0usize, destination_pool);
2415 let final_balance = TransactionBalance::new(vec![], fee).map_err(|_| {
2416 InputSelectorError::Selection(GreedyInputSelectorError::Balance(BalanceError::Overflow))
2417 })?;
2418
2419 Proposal::single_step(
2424 request,
2425 payment_pools,
2426 transparent_inputs,
2427 None,
2428 anchor_height,
2429 final_balance,
2430 fee_rule.clone(),
2431 target_height,
2432 ConfirmationsPolicy::default(),
2436 false,
2437 #[cfg(feature = "orchard")]
2438 ironwood_active_at(params, target_height),
2439 )
2440 .map_err(InputSelectorError::Proposal)
2441 }
2442}
2443
2444#[cfg(feature = "transparent-inputs")]
2450#[allow(clippy::type_complexity)]
2451fn gather_shielding_inputs<DbT, ChangeErrT>(
2452 wallet_db: &DbT,
2453 source_addrs: &[TransparentAddress],
2454 target_height: TargetHeight,
2455 confirmations_policy: ConfirmationsPolicy,
2456 output_filter: CoinbaseFilter,
2457 max_inputs: usize,
2458 locked_input_policy: &LockedInputPolicy,
2459) -> Result<
2460 Vec<WalletTransparentOutput<()>>,
2461 InputSelectorError<
2462 <DbT as InputSource>::Error,
2463 GreedyInputSelectorError,
2464 ChangeErrT,
2465 Infallible,
2466 >,
2467>
2468where
2469 DbT: InputSource,
2470{
2471 let mut utxos = wallet_db
2480 .get_spendable_transparent_outputs_for_addresses(
2481 source_addrs,
2482 target_height,
2483 confirmations_policy,
2484 output_filter,
2485 LockFilter::Policy(locked_input_policy),
2486 )
2487 .map_err(InputSelectorError::DataSource)?;
2488
2489 utxos.sort_by(|a, b| {
2496 b.value()
2497 .cmp(&a.value())
2498 .then_with(|| a.outpoint().cmp(b.outpoint()))
2499 });
2500 utxos.truncate(max_inputs);
2501
2502 let ephemeral_addrs = utxos
2506 .iter()
2507 .filter_map(|utxo| {
2508 (utxo.recipient_key_scope() == Some(TransparentKeyScope::EPHEMERAL))
2509 .then_some(utxo.recipient_address())
2510 })
2511 .collect::<BTreeSet<_>>();
2512 let input_addrs = utxos
2513 .iter()
2514 .map(|utxo| utxo.recipient_address())
2515 .collect::<BTreeSet<_>>();
2516
2517 if !ephemeral_addrs.is_empty() && input_addrs.len() > 1 {
2520 return Err(InputSelectorError::Proposal(
2521 ProposalError::EphemeralAddressLinkability,
2522 ));
2523 }
2524
2525 Ok(utxos
2526 .into_iter()
2527 .map(|utxo| utxo.redact_account_data())
2528 .collect())
2529}
2530
2531#[cfg(feature = "transparent-inputs")]
2539#[allow(clippy::type_complexity)]
2540fn resolve_shielded_destination<DbT, ChangeErrT, ParamsT>(
2541 addr: &ZcashAddress,
2542 params: &ParamsT,
2543 target_height: TargetHeight,
2544) -> Result<
2545 PoolType,
2546 InputSelectorError<
2547 <DbT as InputSource>::Error,
2548 GreedyInputSelectorError,
2549 ChangeErrT,
2550 Infallible,
2551 >,
2552>
2553where
2554 DbT: InputSource,
2555 ParamsT: consensus::Parameters,
2556{
2557 #[cfg(not(feature = "orchard"))]
2558 let _ = target_height;
2559
2560 let resolved: Address = addr
2561 .clone()
2562 .convert_if_network(params.network_type())
2563 .map_err(InputSelectorError::Address)?;
2564 match resolved {
2565 Address::Sapling(_) => Ok(PoolType::SAPLING),
2566 #[cfg(feature = "orchard")]
2570 Address::Unified(ua) if ua.has_orchard() => {
2571 Ok(if ironwood_active_at(params, target_height) {
2572 PoolType::IRONWOOD
2573 } else {
2574 PoolType::ORCHARD
2575 })
2576 }
2577 Address::Unified(ua) if ua.has_sapling() => Ok(PoolType::SAPLING),
2578 Address::Unified(ua) => Err(InputSelectorError::Selection(
2579 GreedyInputSelectorError::UnsupportedAddress(ua),
2580 )),
2581 Address::Transparent(_) | Address::Tex(_) => Err(InputSelectorError::Proposal(
2582 ProposalError::ShieldingRequiresShieldedRecipient,
2583 )),
2584 }
2585}
2586
2587#[cfg(feature = "transparent-inputs")]
2595#[allow(clippy::type_complexity)]
2596fn compute_shielding_balance_with_dust_retry<DbT, ChangeT, ParamsT>(
2597 change_strategy: &ChangeT,
2598 params: &ParamsT,
2599 target_height: TargetHeight,
2600 anchor_height: BlockHeight,
2601 zip318: &PoolMigrationParams,
2602 transparent_inputs: &mut Vec<WalletTransparentOutput<()>>,
2603 wallet_meta: &<ChangeT as ChangeStrategy>::AccountMetaT,
2604) -> Result<
2605 TransactionBalance,
2606 InputSelectorError<
2607 <DbT as InputSource>::Error,
2608 GreedyInputSelectorError,
2609 ChangeT::Error,
2610 Infallible,
2611 >,
2612>
2613where
2614 DbT: InputSource,
2615 ChangeT: ChangeStrategy<MetaSource = DbT>,
2616 ParamsT: consensus::Parameters,
2617{
2618 let trial = compute_shielding_balance::<DbT, ChangeT, ParamsT>(
2619 change_strategy,
2620 params,
2621 target_height,
2622 anchor_height,
2623 zip318,
2624 transparent_inputs,
2625 wallet_meta,
2626 );
2627
2628 match trial {
2629 Ok(balance) => Ok(balance),
2630 Err(ChangeError::DustInputs { transparent, .. }) => {
2631 let exclusions: BTreeSet<OutPoint> = transparent.into_iter().collect();
2632 transparent_inputs.retain(|i| !exclusions.contains(i.outpoint()));
2633
2634 compute_shielding_balance::<DbT, ChangeT, ParamsT>(
2635 change_strategy,
2636 params,
2637 target_height,
2638 anchor_height,
2639 zip318,
2640 transparent_inputs,
2641 wallet_meta,
2642 )
2643 .map_err(InputSelectorError::Change)
2644 }
2645 Err(other) => Err(InputSelectorError::Change(other)),
2646 }
2647}
2648
2649#[cfg(feature = "transparent-inputs")]
2659#[allow(clippy::type_complexity)]
2660fn compute_shielding_balance<DbT, ChangeT, ParamsT>(
2661 change_strategy: &ChangeT,
2662 params: &ParamsT,
2663 target_height: TargetHeight,
2664 anchor_height: BlockHeight,
2665 zip318: &PoolMigrationParams,
2666 transparent_inputs: &[WalletTransparentOutput<()>],
2667 wallet_meta: &<ChangeT as ChangeStrategy>::AccountMetaT,
2668) -> Result<TransactionBalance, ChangeError<ChangeT::Error, Infallible>>
2669where
2670 DbT: InputSource,
2671 ChangeT: ChangeStrategy<MetaSource = DbT>,
2672 ParamsT: consensus::Parameters,
2673{
2674 #[cfg(feature = "orchard")]
2675 let empty_orchard_view = (
2676 orchard_bundle_version_for_height(params, target_height),
2677 &[] as &[Infallible],
2678 &[] as &[Infallible],
2679 );
2680 #[cfg(feature = "orchard")]
2681 let empty_ironwood_view = (
2682 ironwood_bundle_version_for_height(params, target_height),
2683 &[] as &[Infallible],
2684 &[] as &[Infallible],
2685 );
2686
2687 change_strategy.compute_balance(
2688 params,
2689 target_height,
2690 anchor_height,
2691 zip318,
2692 transparent_inputs,
2693 &[] as &[TxOut],
2694 &sapling::EmptyBundleView,
2695 #[cfg(feature = "orchard")]
2696 &empty_orchard_view,
2697 #[cfg(feature = "orchard")]
2698 &empty_ironwood_view,
2699 None,
2700 wallet_meta,
2701 )
2702}
2703
2704#[cfg(all(test, feature = "transparent-inputs"))]
2705mod tests {
2706 use super::shielding_max_inputs;
2707
2708 #[test]
2709 fn shielding_max_inputs_from_block_space_percent() {
2710 assert_eq!(shielding_max_inputs(0), 0);
2713 assert_eq!(shielding_max_inputs(1), 133); assert_eq!(shielding_max_inputs(10), 1333); assert_eq!(shielding_max_inputs(100), 13333); }
2717}
2718
2719#[cfg(test)]
2720mod spend_policy_tests {
2721 use super::*;
2722 #[cfg(feature = "transparent-inputs")]
2723 use crate::data_api::CoinbaseFilter;
2724 use crate::wallet::LockOwner;
2725
2726 #[test]
2730 fn default_permits_all_shielded_pools_and_no_transparent() {
2731 let policy = SpendPolicy::default();
2732 assert!(policy.permits_shielded(ShieldedPool::Sapling));
2733 #[cfg(feature = "orchard")]
2734 {
2735 assert!(policy.permits_shielded(ShieldedPool::Orchard));
2736 assert!(policy.permits_shielded(ShieldedPool::Ironwood));
2737 }
2738 #[cfg(feature = "transparent-inputs")]
2739 assert!(policy.transparent().is_none());
2740 }
2741
2742 #[test]
2744 fn shielded_pools_restricts_the_permitted_set() {
2745 let policy = SpendPolicy::shielded_pools([ShieldedPool::Orchard]);
2746 assert!(policy.permits_shielded(ShieldedPool::Orchard));
2747 assert!(!policy.permits_shielded(ShieldedPool::Sapling));
2748 assert!(!policy.permits_shielded(ShieldedPool::Ironwood));
2749 }
2750
2751 #[cfg(feature = "transparent-inputs")]
2753 #[test]
2754 fn coinbase_policy_maps_to_filter() {
2755 assert_eq!(
2756 CoinbaseFilter::from(CoinbasePolicy::OnlyCoinbase),
2757 CoinbaseFilter::CoinbaseOnly
2758 );
2759 assert_eq!(
2760 CoinbaseFilter::from(CoinbasePolicy::NonCoinbase),
2761 CoinbaseFilter::NonCoinbaseOnly
2762 );
2763 }
2764
2765 #[cfg(feature = "transparent-inputs")]
2768 #[test]
2769 fn transparent_policy_coinbase_defaults_and_override() {
2770 let policy = TransparentSpendPolicy::any_account_addr();
2771 assert_eq!(policy.coinbase(), CoinbasePolicy::NonCoinbase);
2772
2773 let policy = policy.with_coinbase(CoinbasePolicy::OnlyCoinbase);
2774 assert_eq!(policy.coinbase(), CoinbasePolicy::OnlyCoinbase);
2775 assert!(matches!(policy.source(), TransparentSource::AnyAccountAddr));
2776 }
2777
2778 #[test]
2781 fn spend_policy_locked_input_policy_roundtrips() {
2782 let default = SpendPolicy::default();
2783 assert_eq!(default.locked_input_policy(), &LockedInputPolicy::Exclude);
2784 let owners = NonEmptyBTreeSet::singleton(LockOwner::new([9u8; 32]));
2785 let policy = SpendPolicy::default()
2786 .with_locked_input_policy(LockedInputPolicy::PreferLocked(owners.clone()));
2787 assert_eq!(
2788 policy.locked_input_policy(),
2789 &LockedInputPolicy::PreferLocked(owners)
2790 );
2791 }
2792}
2793
2794#[cfg(test)]
2795mod greedy_selection_tests {
2796 use ::sapling::zip32::ExtendedSpendingKey;
2797 use zcash_primitives::transaction::fees::zip317::FeeRule as Zip317FeeRule;
2798 use zcash_protocol::consensus::{Network, NetworkUpgrade, Parameters};
2799 use zip321::Payment;
2800
2801 use super::*;
2802 use crate::{
2803 data_api::{anchor_retention::AnchorRetentionInterval, testing::FixedNotes},
2804 fees::{DustOutputPolicy, zip317::SingleOutputChangeStrategy},
2805 };
2806
2807 const ACCOUNT: u32 = 0;
2809 const SELECTION_LIMIT: usize = 4;
2812
2813 const PAYMENT_VALUE: Zatoshis = Zatoshis::const_from_u64(47_000);
2816 const SAPLING_NOTE_VALUE: Zatoshis = Zatoshis::const_from_u64(60_000);
2818 const SAPLING_NOTE_ID: u32 = 0;
2819
2820 #[derive(Debug, PartialEq, Eq)]
2822 enum Failure {
2823 InsufficientFunds {
2824 available: Zatoshis,
2825 required: Zatoshis,
2826 },
2827 Selection(GreedyInputSelectorError),
2828 }
2829
2830 fn standard_strategy() -> SingleOutputChangeStrategy<Zip317FeeRule, FixedNotes> {
2831 SingleOutputChangeStrategy::new(
2832 Zip317FeeRule::standard(),
2833 None,
2834 ShieldedPool::Sapling,
2835 DustOutputPolicy::default(),
2836 )
2837 }
2838
2839 fn propose_payment<S>(
2843 wallet: &FixedNotes,
2844 payment: Zatoshis,
2845 change_strategy: &S,
2846 ) -> Result<Vec<u32>, Failure>
2847 where
2848 S: ChangeStrategy<MetaSource = FixedNotes>,
2849 S::Error: Debug,
2850 {
2851 propose_payment_under(wallet, payment, change_strategy, &SpendPolicy::default())
2852 }
2853
2854 fn propose_payment_under<S>(
2858 wallet: &FixedNotes,
2859 payment: Zatoshis,
2860 change_strategy: &S,
2861 spend_policy: &SpendPolicy,
2862 ) -> Result<Vec<u32>, Failure>
2863 where
2864 S: ChangeStrategy<MetaSource = FixedNotes>,
2865 S::Error: Debug,
2866 {
2867 let params = Network::TestNetwork;
2868 let anchor_height = params
2869 .activation_height(NetworkUpgrade::Nu5)
2870 .expect("NU5 is active on testnet");
2871 let (_, recipient) = ExtendedSpendingKey::master(&[])
2872 .expect("the master key of the empty seed is valid")
2873 .default_address();
2874 let recipient = Address::from(recipient).to_zcash_address(¶ms);
2875 let request = TransactionRequest::new(vec![Payment::without_memo(recipient, payment)])
2876 .expect("a single payment is a valid request");
2877
2878 GreedyInputSelector::new()
2879 .propose_transaction(
2880 ¶ms,
2881 wallet,
2882 TargetHeight::from(anchor_height + 1),
2883 anchor_height,
2884 &PoolMigrationParams::new(
2885 zcash_protocol::zip318::BlockSpacing::UNIFORM,
2886 AnchorRetentionInterval::ZIP_318,
2887 ),
2888 ConfirmationsPolicy::default(),
2889 ACCOUNT,
2890 request,
2891 change_strategy,
2892 spend_policy,
2893 None,
2894 )
2895 .map(|proposal| {
2896 proposal
2897 .steps()
2898 .iter()
2899 .flat_map(|step| step.shielded_inputs())
2900 .flat_map(|inputs| inputs.notes().iter().map(|n| *n.internal_note_id()))
2901 .collect()
2902 })
2903 .map_err(|e| match e {
2904 InputSelectorError::InsufficientFunds {
2905 available,
2906 required,
2907 } => Failure::InsufficientFunds {
2908 available,
2909 required,
2910 },
2911 InputSelectorError::Selection(e) => Failure::Selection(e),
2912 other => panic!("unexpected input selection error: {other:?}"),
2913 })
2914 }
2915
2916 #[cfg(feature = "orchard")]
2919 mod across_pools {
2920 use super::*;
2921
2922 const ORCHARD_NOTE_VALUE: Zatoshis = Zatoshis::const_from_u64(20_000);
2924 const ORCHARD_NOTE_ID: u32 = 1;
2925
2926 fn two_pool_notes() -> FixedNotes {
2929 FixedNotes::new(&[
2930 (ShieldedPool::Orchard, ORCHARD_NOTE_ID, ORCHARD_NOTE_VALUE),
2931 (ShieldedPool::Sapling, SAPLING_NOTE_ID, SAPLING_NOTE_VALUE),
2932 ])
2933 .with_selection_limit(SELECTION_LIMIT)
2934 }
2935
2936 #[test]
2937 fn a_standard_strategy_spends_both_notes_at_a_dust_shortfall() {
2938 assert_eq!(
2939 propose_payment(&two_pool_notes(), PAYMENT_VALUE, &standard_strategy()),
2940 Ok(vec![SAPLING_NOTE_ID, ORCHARD_NOTE_ID])
2941 );
2942 }
2943
2944 #[test]
2945 fn a_source_returning_a_pool_not_asked_for_is_a_contract_violation() {
2946 let wallet = two_pool_notes().ignoring_sources();
2948 let spend_policy = SpendPolicy::shielded_pools([ShieldedPool::Sapling]);
2949
2950 assert_eq!(
2951 propose_payment_under(&wallet, PAYMENT_VALUE, &standard_strategy(), &spend_policy),
2952 Err(Failure::Selection(
2953 GreedyInputSelectorError::UnrequestedPoolSelected
2954 ))
2955 );
2956 }
2957 }
2958
2959 mod dust_exclusion {
2961 use zcash_primitives::transaction::fees::zip317::MARGINAL_FEE;
2962 use zcash_protocol::value::COIN;
2963
2964 use super::*;
2965
2966 const DUST_NOTE_VALUE: Zatoshis = MARGINAL_FEE;
2970 const FUNDING_NOTE_VALUE: Zatoshis = Zatoshis::const_from_u64(COIN);
2974 const DUST_NOTE_ID: u32 = 0;
2975 const FIRST_FUNDING_NOTE_ID: u32 = 1;
2976 const SECOND_FUNDING_NOTE_ID: u32 = 2;
2977
2978 fn two_action_fee() -> Zatoshis {
2980 (MARGINAL_FEE + MARGINAL_FEE).expect("no overflow")
2981 }
2982
2983 fn dust_then_funding_notes() -> FixedNotes {
2985 FixedNotes::new(&[
2986 (ShieldedPool::Sapling, DUST_NOTE_ID, DUST_NOTE_VALUE),
2987 (
2988 ShieldedPool::Sapling,
2989 FIRST_FUNDING_NOTE_ID,
2990 FUNDING_NOTE_VALUE,
2991 ),
2992 (
2993 ShieldedPool::Sapling,
2994 SECOND_FUNDING_NOTE_ID,
2995 FUNDING_NOTE_VALUE,
2996 ),
2997 ])
2998 .with_selection_limit(SELECTION_LIMIT)
2999 }
3000
3001 #[test]
3002 fn excluding_a_dust_note_retries_the_remaining_selection() {
3003 assert_eq!(
3004 propose_payment(
3005 &dust_then_funding_notes(),
3006 FUNDING_NOTE_VALUE,
3007 &standard_strategy()
3008 ),
3009 Ok(vec![FIRST_FUNDING_NOTE_ID, SECOND_FUNDING_NOTE_ID])
3010 );
3011 }
3012
3013 #[test]
3014 fn excluding_a_dust_note_still_reports_a_real_shortfall() {
3015 let funding_value = (FUNDING_NOTE_VALUE + FUNDING_NOTE_VALUE).expect("no overflow");
3016 let payment = (funding_value - MARGINAL_FEE).expect("no underflow");
3021
3022 assert_eq!(
3023 propose_payment(&dust_then_funding_notes(), payment, &standard_strategy()),
3024 Err(Failure::InsufficientFunds {
3025 available: funding_value,
3026 required: (payment + two_action_fee()).expect("no overflow"),
3027 })
3028 );
3029 }
3030
3031 #[test]
3032 fn a_source_returning_an_excluded_note_is_a_contract_violation() {
3033 let wallet = dust_then_funding_notes().ignoring_exclude();
3036
3037 assert_eq!(
3038 propose_payment(&wallet, FUNDING_NOTE_VALUE, &standard_strategy()),
3039 Err(Failure::Selection(
3040 GreedyInputSelectorError::ExcludedNoteSelected
3041 ))
3042 );
3043 }
3044 }
3045
3046 mod contract_violations {
3048 use transparent::bundle::OutPoint;
3049 use zcash_primitives::transaction::fees::{
3050 transparent as transparent_fees,
3051 zip317::{FeeError as Zip317FeeError, FeeRule as Zip317FeeRule},
3052 };
3053
3054 use super::*;
3055 #[cfg(feature = "orchard")]
3056 use crate::fees::orchard::InputView as _;
3057 use crate::fees::{MetaSource, sapling::InputView as _};
3058
3059 fn single_sapling_note() -> FixedNotes {
3061 FixedNotes::new(&[(ShieldedPool::Sapling, SAPLING_NOTE_ID, SAPLING_NOTE_VALUE)])
3062 .with_selection_limit(SELECTION_LIMIT)
3063 }
3064
3065 fn given_value<NoteRefT>(
3067 sapling: &impl sapling::BundleView<NoteRefT>,
3068 #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
3069 #[cfg(feature = "orchard")] ironwood: &impl orchard_fees::BundleView<NoteRefT>,
3070 ) -> Zatoshis {
3071 let given = sapling.inputs().iter().map(|input| input.value());
3072 #[cfg(feature = "orchard")]
3073 let given = given
3074 .chain(orchard.inputs().iter().map(|input| input.value()))
3075 .chain(ironwood.inputs().iter().map(|input| input.value()));
3076 given.sum::<Option<Zatoshis>>().expect("no overflow")
3077 }
3078
3079 struct FeeBlindStrategy {
3083 fee_rule: Zip317FeeRule,
3084 }
3085
3086 impl ChangeStrategy for FeeBlindStrategy {
3087 type FeeRule = Zip317FeeRule;
3088 type Error = Zip317FeeError;
3089 type MetaSource = FixedNotes;
3090 type AccountMetaT = ();
3091
3092 fn fee_rule(&self) -> &Self::FeeRule {
3093 &self.fee_rule
3094 }
3095
3096 fn fetch_wallet_meta(
3097 &self,
3098 _meta_source: &Self::MetaSource,
3099 _account: <Self::MetaSource as MetaSource>::AccountId,
3100 _target_height: TargetHeight,
3101 _exclude: &[<Self::MetaSource as MetaSource>::NoteRef],
3102 ) -> Result<Self::AccountMetaT, <Self::MetaSource as MetaSource>::Error> {
3103 Ok(())
3104 }
3105
3106 fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
3107 &self,
3108 _params: &P,
3109 _target_height: TargetHeight,
3110 _anchor_height: BlockHeight,
3111 _zip318: &PoolMigrationParams,
3112 _transparent_inputs: &[impl transparent_fees::InputView],
3113 _transparent_outputs: &[impl transparent_fees::OutputView],
3114 sapling: &impl sapling::BundleView<NoteRefT>,
3115 #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
3116 #[cfg(feature = "orchard")] ironwood: &impl orchard_fees::BundleView<NoteRefT>,
3117 _ephemeral_balance: Option<EphemeralBalance>,
3118 _wallet_meta: &Self::AccountMetaT,
3119 ) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
3120 Err(ChangeError::InsufficientFunds {
3121 available: given_value(
3122 sapling,
3123 #[cfg(feature = "orchard")]
3124 orchard,
3125 #[cfg(feature = "orchard")]
3126 ironwood,
3127 ),
3128 required: PAYMENT_VALUE,
3129 })
3130 }
3131 }
3132
3133 struct DustNamingStrategy {
3138 fee_rule: Zip317FeeRule,
3139 names_given: bool,
3140 transparent: Vec<OutPoint>,
3141 }
3142
3143 impl ChangeStrategy for DustNamingStrategy {
3144 type FeeRule = Zip317FeeRule;
3145 type Error = Zip317FeeError;
3146 type MetaSource = FixedNotes;
3147 type AccountMetaT = ();
3148
3149 fn fee_rule(&self) -> &Self::FeeRule {
3150 &self.fee_rule
3151 }
3152
3153 fn fetch_wallet_meta(
3154 &self,
3155 _meta_source: &Self::MetaSource,
3156 _account: <Self::MetaSource as MetaSource>::AccountId,
3157 _target_height: TargetHeight,
3158 _exclude: &[<Self::MetaSource as MetaSource>::NoteRef],
3159 ) -> Result<Self::AccountMetaT, <Self::MetaSource as MetaSource>::Error> {
3160 Ok(())
3161 }
3162
3163 fn compute_balance<P: consensus::Parameters, NoteRefT: Clone>(
3164 &self,
3165 _params: &P,
3166 _target_height: TargetHeight,
3167 _anchor_height: BlockHeight,
3168 _zip318: &PoolMigrationParams,
3169 _transparent_inputs: &[impl transparent_fees::InputView],
3170 _transparent_outputs: &[impl transparent_fees::OutputView],
3171 sapling: &impl sapling::BundleView<NoteRefT>,
3172 #[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
3173 #[cfg(feature = "orchard")] ironwood: &impl orchard_fees::BundleView<NoteRefT>,
3174 _ephemeral_balance: Option<EphemeralBalance>,
3175 _wallet_meta: &Self::AccountMetaT,
3176 ) -> Result<TransactionBalance, ChangeError<Self::Error, NoteRefT>> {
3177 let given = given_value(
3178 sapling,
3179 #[cfg(feature = "orchard")]
3180 orchard,
3181 #[cfg(feature = "orchard")]
3182 ironwood,
3183 );
3184 if given == Zatoshis::ZERO {
3185 Err(ChangeError::InsufficientFunds {
3186 available: given,
3187 required: PAYMENT_VALUE,
3188 })
3189 } else {
3190 let sapling = if self.names_given {
3191 sapling
3192 .inputs()
3193 .iter()
3194 .map(|input| input.note_id().clone())
3195 .collect()
3196 } else {
3197 vec![]
3198 };
3199 Err(ChangeError::DustInputs {
3200 transparent: self.transparent.clone(),
3201 sapling,
3202 #[cfg(feature = "orchard")]
3203 orchard: vec![],
3204 #[cfg(feature = "orchard")]
3205 ironwood: vec![],
3206 })
3207 }
3208 }
3209 }
3210
3211 #[test]
3212 fn a_requirement_the_inputs_cover_is_a_contract_violation() {
3213 let change_strategy = FeeBlindStrategy {
3214 fee_rule: Zip317FeeRule::standard(),
3215 };
3216
3217 assert_eq!(
3218 propose_payment(&single_sapling_note(), PAYMENT_VALUE, &change_strategy),
3219 Err(Failure::Selection(
3220 GreedyInputSelectorError::RequirementCovered {
3221 required: PAYMENT_VALUE,
3222 available: SAPLING_NOTE_VALUE,
3223 }
3224 ))
3225 );
3226 }
3227
3228 #[test]
3229 fn dust_inputs_naming_nothing_is_a_contract_violation() {
3230 let change_strategy = DustNamingStrategy {
3231 fee_rule: Zip317FeeRule::standard(),
3232 names_given: false,
3233 transparent: vec![],
3234 };
3235
3236 assert_eq!(
3237 propose_payment(&single_sapling_note(), PAYMENT_VALUE, &change_strategy),
3238 Err(Failure::Selection(
3239 GreedyInputSelectorError::DustInputsNotGiven
3240 ))
3241 );
3242 }
3243
3244 #[cfg(feature = "transparent-inputs")]
3245 #[test]
3246 fn dust_inputs_naming_an_input_not_given_is_a_contract_violation() {
3247 let change_strategy = DustNamingStrategy {
3248 fee_rule: Zip317FeeRule::standard(),
3249 names_given: true,
3250 transparent: vec![OutPoint::new([0; 32], 0)],
3251 };
3252
3253 assert_eq!(
3254 propose_payment(&single_sapling_note(), PAYMENT_VALUE, &change_strategy),
3255 Err(Failure::Selection(
3256 GreedyInputSelectorError::DustInputsNotGiven
3257 ))
3258 );
3259 }
3260 }
3261}