zcash_client_backend/data_api/
wallet.rs

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