zcash_client_backend/
proto.rs

1//! This module contains generated code for handling light client protobuf structs.
2
3use incrementalmerkletree::frontier::CommitmentTree;
4use nonempty::NonEmpty;
5use std::{
6    array::TryFromSliceError,
7    collections::BTreeMap,
8    fmt::{self, Display},
9    io,
10};
11
12use sapling::{self, note::ExtractedNoteCommitment, Node};
13use zcash_note_encryption::{EphemeralKeyBytes, COMPACT_NOTE_SIZE};
14use zcash_primitives::{
15    block::{BlockHash, BlockHeader},
16    merkle_tree::read_commitment_tree,
17    transaction::TxId,
18};
19use zcash_protocol::{
20    consensus::BlockHeight,
21    memo::{self, MemoBytes},
22    value::Zatoshis,
23    PoolType, ShieldedProtocol,
24};
25use zip321::{TransactionRequest, Zip321Error};
26
27use crate::{
28    data_api::{chain::ChainState, InputSource},
29    fees::{ChangeValue, StandardFeeRule, TransactionBalance},
30    proposal::{Proposal, ProposalError, ShieldedInputs, Step, StepOutput, StepOutputIndex},
31};
32
33#[cfg(feature = "transparent-inputs")]
34use transparent::bundle::OutPoint;
35
36#[cfg(feature = "orchard")]
37use orchard::tree::MerkleHashOrchard;
38
39#[rustfmt::skip]
40#[allow(unknown_lints)]
41#[allow(clippy::derive_partial_eq_without_eq)]
42pub mod compact_formats;
43
44#[rustfmt::skip]
45#[allow(unknown_lints)]
46#[allow(clippy::derive_partial_eq_without_eq)]
47pub mod proposal;
48
49#[rustfmt::skip]
50#[allow(unknown_lints)]
51#[allow(clippy::derive_partial_eq_without_eq)]
52pub mod service;
53
54impl compact_formats::CompactBlock {
55    /// Returns the [`BlockHash`] for this block.
56    ///
57    /// # Panics
58    ///
59    /// This function will panic if [`CompactBlock.header`] is not set and
60    /// [`CompactBlock.hash`] is not exactly 32 bytes.
61    ///
62    /// [`CompactBlock.header`]: #structfield.header
63    /// [`CompactBlock.hash`]: #structfield.hash
64    pub fn hash(&self) -> BlockHash {
65        if let Some(header) = self.header() {
66            header.hash()
67        } else {
68            BlockHash::from_slice(&self.hash)
69        }
70    }
71
72    /// Returns the [`BlockHash`] for this block's parent.
73    ///
74    /// # Panics
75    ///
76    /// This function will panic if [`CompactBlock.header`] is not set and
77    /// [`CompactBlock.prevHash`] is not exactly 32 bytes.
78    ///
79    /// [`CompactBlock.header`]: #structfield.header
80    /// [`CompactBlock.prevHash`]: #structfield.prevHash
81    pub fn prev_hash(&self) -> BlockHash {
82        if let Some(header) = self.header() {
83            header.prev_block
84        } else {
85            BlockHash::from_slice(&self.prev_hash)
86        }
87    }
88
89    /// Returns the [`BlockHeight`] value for this block
90    ///
91    /// # Panics
92    ///
93    /// This function will panic if [`CompactBlock.height`] is not
94    /// representable within a u32.
95    pub fn height(&self) -> BlockHeight {
96        self.height.try_into().unwrap()
97    }
98
99    /// Returns the [`BlockHeader`] for this block if present.
100    ///
101    /// A convenience method that parses [`CompactBlock.header`] if present.
102    ///
103    /// [`CompactBlock.header`]: #structfield.header
104    pub fn header(&self) -> Option<BlockHeader> {
105        if self.header.is_empty() {
106            None
107        } else {
108            BlockHeader::read(&self.header[..]).ok()
109        }
110    }
111}
112
113impl compact_formats::CompactTx {
114    /// Returns the transaction Id
115    pub fn txid(&self) -> TxId {
116        let mut hash = [0u8; 32];
117        hash.copy_from_slice(&self.hash);
118        TxId::from_bytes(hash)
119    }
120}
121
122impl compact_formats::CompactSaplingOutput {
123    /// Returns the note commitment for this output.
124    ///
125    /// A convenience method that parses [`CompactOutput.cmu`].
126    ///
127    /// [`CompactOutput.cmu`]: #structfield.cmu
128    pub fn cmu(&self) -> Result<ExtractedNoteCommitment, ()> {
129        let mut repr = [0; 32];
130        repr.copy_from_slice(&self.cmu[..]);
131        Option::from(ExtractedNoteCommitment::from_bytes(&repr)).ok_or(())
132    }
133
134    /// Returns the ephemeral public key for this output.
135    ///
136    /// A convenience method that parses [`CompactOutput.epk`].
137    ///
138    /// [`CompactOutput.epk`]: #structfield.epk
139    pub fn ephemeral_key(&self) -> Result<EphemeralKeyBytes, ()> {
140        self.ephemeral_key[..]
141            .try_into()
142            .map(EphemeralKeyBytes)
143            .map_err(|_| ())
144    }
145}
146
147impl<Proof> From<&sapling::bundle::OutputDescription<Proof>>
148    for compact_formats::CompactSaplingOutput
149{
150    fn from(
151        out: &sapling::bundle::OutputDescription<Proof>,
152    ) -> compact_formats::CompactSaplingOutput {
153        compact_formats::CompactSaplingOutput {
154            cmu: out.cmu().to_bytes().to_vec(),
155            ephemeral_key: out.ephemeral_key().as_ref().to_vec(),
156            ciphertext: out.enc_ciphertext()[..COMPACT_NOTE_SIZE].to_vec(),
157        }
158    }
159}
160
161impl TryFrom<compact_formats::CompactSaplingOutput>
162    for sapling::note_encryption::CompactOutputDescription
163{
164    type Error = ();
165
166    fn try_from(value: compact_formats::CompactSaplingOutput) -> Result<Self, Self::Error> {
167        (&value).try_into()
168    }
169}
170
171impl TryFrom<&compact_formats::CompactSaplingOutput>
172    for sapling::note_encryption::CompactOutputDescription
173{
174    type Error = ();
175
176    fn try_from(value: &compact_formats::CompactSaplingOutput) -> Result<Self, Self::Error> {
177        Ok(sapling::note_encryption::CompactOutputDescription {
178            cmu: value.cmu()?,
179            ephemeral_key: value.ephemeral_key()?,
180            enc_ciphertext: value.ciphertext[..].try_into().map_err(|_| ())?,
181        })
182    }
183}
184
185impl compact_formats::CompactSaplingSpend {
186    pub fn nf(&self) -> Result<sapling::Nullifier, ()> {
187        sapling::Nullifier::from_slice(&self.nf).map_err(|_| ())
188    }
189}
190
191#[cfg(feature = "orchard")]
192impl TryFrom<&compact_formats::CompactOrchardAction> for orchard::note_encryption::CompactAction {
193    type Error = ();
194
195    fn try_from(value: &compact_formats::CompactOrchardAction) -> Result<Self, Self::Error> {
196        Ok(orchard::note_encryption::CompactAction::from_parts(
197            value.nf()?,
198            value.cmx()?,
199            value.ephemeral_key()?,
200            value.ciphertext[..].try_into().map_err(|_| ())?,
201        ))
202    }
203}
204
205#[cfg(feature = "orchard")]
206impl compact_formats::CompactOrchardAction {
207    /// Returns the note commitment for the output of this action.
208    ///
209    /// A convenience method that parses [`CompactOrchardAction.cmx`].
210    ///
211    /// [`CompactOrchardAction.cmx`]: #structfield.cmx
212    pub fn cmx(&self) -> Result<orchard::note::ExtractedNoteCommitment, ()> {
213        Option::from(orchard::note::ExtractedNoteCommitment::from_bytes(
214            &self.cmx[..].try_into().map_err(|_| ())?,
215        ))
216        .ok_or(())
217    }
218
219    /// Returns the nullifier for the spend of this action.
220    ///
221    /// A convenience method that parses [`CompactOrchardAction.nullifier`].
222    ///
223    /// [`CompactOrchardAction.nullifier`]: #structfield.nullifier
224    pub fn nf(&self) -> Result<orchard::note::Nullifier, ()> {
225        let nf_bytes: [u8; 32] = self.nullifier[..].try_into().map_err(|_| ())?;
226        Option::from(orchard::note::Nullifier::from_bytes(&nf_bytes)).ok_or(())
227    }
228
229    /// Returns the ephemeral public key for the output of this action.
230    ///
231    /// A convenience method that parses [`CompactOrchardAction.ephemeral_key`].
232    ///
233    /// [`CompactOrchardAction.ephemeral_key`]: #structfield.ephemeral_key
234    pub fn ephemeral_key(&self) -> Result<EphemeralKeyBytes, ()> {
235        self.ephemeral_key[..]
236            .try_into()
237            .map(EphemeralKeyBytes)
238            .map_err(|_| ())
239    }
240}
241
242impl<A: sapling::bundle::Authorization> From<&sapling::bundle::SpendDescription<A>>
243    for compact_formats::CompactSaplingSpend
244{
245    fn from(spend: &sapling::bundle::SpendDescription<A>) -> compact_formats::CompactSaplingSpend {
246        compact_formats::CompactSaplingSpend {
247            nf: spend.nullifier().to_vec(),
248        }
249    }
250}
251
252#[cfg(feature = "orchard")]
253impl<SpendAuth> From<&orchard::Action<SpendAuth>> for compact_formats::CompactOrchardAction {
254    fn from(action: &orchard::Action<SpendAuth>) -> compact_formats::CompactOrchardAction {
255        compact_formats::CompactOrchardAction {
256            nullifier: action.nullifier().to_bytes().to_vec(),
257            cmx: action.cmx().to_bytes().to_vec(),
258            ephemeral_key: action.encrypted_note().epk_bytes.to_vec(),
259            ciphertext: action.encrypted_note().enc_ciphertext[..COMPACT_NOTE_SIZE].to_vec(),
260        }
261    }
262}
263
264impl service::TreeState {
265    /// Deserializes and returns the Sapling note commitment tree field of the tree state.
266    pub fn sapling_tree(
267        &self,
268    ) -> io::Result<CommitmentTree<Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>> {
269        if self.sapling_tree.is_empty() {
270            Ok(CommitmentTree::empty())
271        } else {
272            let sapling_tree_bytes = hex::decode(&self.sapling_tree).map_err(|e| {
273                io::Error::new(
274                    io::ErrorKind::InvalidData,
275                    format!("Hex decoding of Sapling tree bytes failed: {:?}", e),
276                )
277            })?;
278            read_commitment_tree::<Node, _, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>(
279                &sapling_tree_bytes[..],
280            )
281        }
282    }
283
284    /// Deserializes and returns the Sapling note commitment tree field of the tree state.
285    #[cfg(feature = "orchard")]
286    pub fn orchard_tree(
287        &self,
288    ) -> io::Result<CommitmentTree<MerkleHashOrchard, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>>
289    {
290        if self.orchard_tree.is_empty() {
291            Ok(CommitmentTree::empty())
292        } else {
293            let orchard_tree_bytes = hex::decode(&self.orchard_tree).map_err(|e| {
294                io::Error::new(
295                    io::ErrorKind::InvalidData,
296                    format!("Hex decoding of Orchard tree bytes failed: {:?}", e),
297                )
298            })?;
299            read_commitment_tree::<
300                MerkleHashOrchard,
301                _,
302                { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
303            >(&orchard_tree_bytes[..])
304        }
305    }
306
307    /// Parses this tree state into a [`ChainState`] for use with [`scan_cached_blocks`].
308    ///
309    /// [`scan_cached_blocks`]: crate::data_api::chain::scan_cached_blocks
310    pub fn to_chain_state(&self) -> io::Result<ChainState> {
311        let mut hash_bytes = hex::decode(&self.hash).map_err(|e| {
312            io::Error::new(
313                io::ErrorKind::InvalidData,
314                format!("Block hash is not valid hex: {:?}", e),
315            )
316        })?;
317        // Zcashd hex strings for block hashes are byte-reversed.
318        hash_bytes.reverse();
319
320        Ok(ChainState::new(
321            self.height
322                .try_into()
323                .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid block height"))?,
324            BlockHash::try_from_slice(&hash_bytes).ok_or_else(|| {
325                io::Error::new(io::ErrorKind::InvalidData, "Invalid block hash length.")
326            })?,
327            self.sapling_tree()?.to_frontier(),
328            #[cfg(feature = "orchard")]
329            self.orchard_tree()?.to_frontier(),
330        ))
331    }
332}
333
334/// Constant for the V1 proposal serialization version.
335pub const PROPOSAL_SER_V1: u32 = 1;
336
337/// Errors that can occur in the process of decoding a [`Proposal`] from its protobuf
338/// representation.
339#[derive(Debug, Clone)]
340pub enum ProposalDecodingError<DbError> {
341    /// The encoded proposal contained no steps.
342    NoSteps,
343    /// The ZIP 321 transaction request URI was invalid.
344    Zip321(Zip321Error),
345    /// A proposed input was null.
346    NullInput(usize),
347    /// A transaction identifier string did not decode to a valid transaction ID.
348    TxIdInvalid(TryFromSliceError),
349    /// An invalid value pool identifier was encountered.
350    ValuePoolNotSupported(i32),
351    /// A failure occurred trying to retrieve an unspent note or UTXO from the wallet database.
352    InputRetrieval(DbError),
353    /// The unspent note or UTXO corresponding to a proposal input was not found in the wallet
354    /// database.
355    InputNotFound(TxId, PoolType, u32),
356    /// The transaction balance, or a component thereof, failed to decode correctly.
357    BalanceInvalid,
358    /// Failed to decode a ZIP-302-compliant memo from the provided memo bytes.
359    MemoInvalid(memo::Error),
360    /// The serialization version returned by the protobuf was not recognized.
361    VersionInvalid(u32),
362    /// The fee rule specified by the proposal is not supported by the wallet.
363    FeeRuleNotSupported(proposal::FeeRule),
364    /// The proposal violated balance or structural constraints.
365    ProposalInvalid(ProposalError),
366    /// An inputs field for the given protocol was present, but contained no input note references.
367    EmptyShieldedInputs(ShieldedProtocol),
368    /// A memo field was provided for a transparent output.
369    TransparentMemo,
370    /// Change outputs to the specified pool are not supported.
371    InvalidChangeRecipient(PoolType),
372    /// Ephemeral outputs to the specified pool are not supported.
373    InvalidEphemeralRecipient(PoolType),
374}
375
376impl<E> From<Zip321Error> for ProposalDecodingError<E> {
377    fn from(value: Zip321Error) -> Self {
378        Self::Zip321(value)
379    }
380}
381
382impl<E: Display> Display for ProposalDecodingError<E> {
383    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384        match self {
385            ProposalDecodingError::NoSteps => write!(f, "The proposal had no steps."),
386            ProposalDecodingError::Zip321(err) => write!(f, "Transaction request invalid: {}", err),
387            ProposalDecodingError::NullInput(i) => {
388                write!(f, "Proposed input was null at index {}", i)
389            }
390            ProposalDecodingError::TxIdInvalid(err) => {
391                write!(f, "Invalid transaction id: {:?}", err)
392            }
393            ProposalDecodingError::ValuePoolNotSupported(id) => {
394                write!(f, "Invalid value pool identifier: {:?}", id)
395            }
396            ProposalDecodingError::InputRetrieval(err) => write!(
397                f,
398                "An error occurred retrieving a transaction input: {}",
399                err
400            ),
401            ProposalDecodingError::InputNotFound(txid, pool, idx) => write!(
402                f,
403                "No {} input found for txid {}, index {}",
404                pool, txid, idx
405            ),
406            ProposalDecodingError::BalanceInvalid => {
407                write!(f, "An error occurred decoding the proposal balance.")
408            }
409            ProposalDecodingError::MemoInvalid(err) => {
410                write!(f, "An error occurred decoding a proposed memo: {}", err)
411            }
412            ProposalDecodingError::VersionInvalid(v) => {
413                write!(f, "Unrecognized proposal version {}", v)
414            }
415            ProposalDecodingError::FeeRuleNotSupported(r) => {
416                write!(
417                    f,
418                    "Fee calculation using the {:?} fee rule is not supported.",
419                    r
420                )
421            }
422            ProposalDecodingError::ProposalInvalid(err) => write!(f, "{}", err),
423            ProposalDecodingError::EmptyShieldedInputs(protocol) => write!(
424                f,
425                "An inputs field was present for {:?}, but contained no note references.",
426                protocol
427            ),
428            ProposalDecodingError::TransparentMemo => {
429                write!(f, "Transparent outputs cannot have memos.")
430            }
431            ProposalDecodingError::InvalidChangeRecipient(pool_type) => write!(
432                f,
433                "Change outputs to the {} pool are not supported.",
434                pool_type
435            ),
436            ProposalDecodingError::InvalidEphemeralRecipient(pool_type) => write!(
437                f,
438                "Ephemeral outputs to the {} pool are not supported.",
439                pool_type
440            ),
441        }
442    }
443}
444
445impl<E: std::error::Error + 'static> std::error::Error for ProposalDecodingError<E> {
446    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
447        match self {
448            ProposalDecodingError::Zip321(e) => Some(e),
449            ProposalDecodingError::InputRetrieval(e) => Some(e),
450            ProposalDecodingError::MemoInvalid(e) => Some(e),
451            _ => None,
452        }
453    }
454}
455
456fn pool_type<T>(pool_id: i32) -> Result<PoolType, ProposalDecodingError<T>> {
457    match proposal::ValuePool::try_from(pool_id) {
458        Ok(proposal::ValuePool::Transparent) => Ok(PoolType::TRANSPARENT),
459        Ok(proposal::ValuePool::Sapling) => Ok(PoolType::SAPLING),
460        Ok(proposal::ValuePool::Orchard) => Ok(PoolType::ORCHARD),
461        _ => Err(ProposalDecodingError::ValuePoolNotSupported(pool_id)),
462    }
463}
464
465impl proposal::ReceivedOutput {
466    pub fn parse_txid(&self) -> Result<TxId, TryFromSliceError> {
467        Ok(TxId::from_bytes(self.txid[..].try_into()?))
468    }
469
470    pub fn pool_type<T>(&self) -> Result<PoolType, ProposalDecodingError<T>> {
471        pool_type(self.value_pool)
472    }
473}
474
475impl proposal::ChangeValue {
476    pub fn pool_type<T>(&self) -> Result<PoolType, ProposalDecodingError<T>> {
477        pool_type(self.value_pool)
478    }
479}
480
481impl From<PoolType> for proposal::ValuePool {
482    fn from(value: PoolType) -> Self {
483        match value {
484            PoolType::Transparent => proposal::ValuePool::Transparent,
485            PoolType::Shielded(p) => p.into(),
486        }
487    }
488}
489
490impl From<ShieldedProtocol> for proposal::ValuePool {
491    fn from(value: ShieldedProtocol) -> Self {
492        match value {
493            ShieldedProtocol::Sapling => proposal::ValuePool::Sapling,
494            ShieldedProtocol::Orchard => proposal::ValuePool::Orchard,
495        }
496    }
497}
498
499impl proposal::Proposal {
500    /// Serializes a [`Proposal`] based upon a supported [`StandardFeeRule`] to its protobuf
501    /// representation.
502    pub fn from_standard_proposal<NoteRef>(value: &Proposal<StandardFeeRule, NoteRef>) -> Self {
503        use proposal::proposed_input;
504        use proposal::{PriorStepChange, PriorStepOutput, ReceivedOutput};
505        let steps = value
506            .steps()
507            .iter()
508            .map(|step| {
509                let transaction_request = step.transaction_request().to_uri();
510
511                let anchor_height = step
512                    .shielded_inputs()
513                    .map_or_else(|| 0, |i| u32::from(i.anchor_height()));
514
515                let inputs = step
516                    .transparent_inputs()
517                    .iter()
518                    .map(|utxo| proposal::ProposedInput {
519                        value: Some(proposed_input::Value::ReceivedOutput(ReceivedOutput {
520                            txid: utxo.outpoint().hash().to_vec(),
521                            value_pool: proposal::ValuePool::Transparent.into(),
522                            index: utxo.outpoint().n(),
523                            value: utxo.txout().value.into(),
524                        })),
525                    })
526                    .chain(step.shielded_inputs().iter().flat_map(|s_in| {
527                        s_in.notes().iter().map(|rec_note| proposal::ProposedInput {
528                            value: Some(proposed_input::Value::ReceivedOutput(ReceivedOutput {
529                                txid: rec_note.txid().as_ref().to_vec(),
530                                value_pool: proposal::ValuePool::from(rec_note.note().protocol())
531                                    .into(),
532                                index: rec_note.output_index().into(),
533                                value: rec_note.note().value().into(),
534                            })),
535                        })
536                    }))
537                    .chain(step.prior_step_inputs().iter().map(|p_in| {
538                        match p_in.output_index() {
539                            StepOutputIndex::Payment(i) => proposal::ProposedInput {
540                                value: Some(proposed_input::Value::PriorStepOutput(
541                                    PriorStepOutput {
542                                        step_index: p_in
543                                            .step_index()
544                                            .try_into()
545                                            .expect("Step index fits into a u32"),
546                                        payment_index: i
547                                            .try_into()
548                                            .expect("Payment index fits into a u32"),
549                                    },
550                                )),
551                            },
552                            StepOutputIndex::Change(i) => proposal::ProposedInput {
553                                value: Some(proposed_input::Value::PriorStepChange(
554                                    PriorStepChange {
555                                        step_index: p_in
556                                            .step_index()
557                                            .try_into()
558                                            .expect("Step index fits into a u32"),
559                                        change_index: i
560                                            .try_into()
561                                            .expect("Payment index fits into a u32"),
562                                    },
563                                )),
564                            },
565                        }
566                    }))
567                    .collect();
568
569                let payment_output_pools = step
570                    .payment_pools()
571                    .iter()
572                    .map(|(idx, pool_type)| proposal::PaymentOutputPool {
573                        payment_index: u32::try_from(*idx).expect("Payment index fits into a u32"),
574                        value_pool: proposal::ValuePool::from(*pool_type).into(),
575                    })
576                    .collect();
577
578                let balance = Some(proposal::TransactionBalance {
579                    proposed_change: step
580                        .balance()
581                        .proposed_change()
582                        .iter()
583                        .map(|change| proposal::ChangeValue {
584                            value: change.value().into(),
585                            value_pool: proposal::ValuePool::from(change.output_pool()).into(),
586                            memo: change.memo().map(|memo_bytes| proposal::MemoBytes {
587                                value: memo_bytes.as_slice().to_vec(),
588                            }),
589                            is_ephemeral: change.is_ephemeral(),
590                        })
591                        .collect(),
592                    fee_required: step.balance().fee_required().into(),
593                });
594
595                proposal::ProposalStep {
596                    transaction_request,
597                    payment_output_pools,
598                    anchor_height,
599                    inputs,
600                    balance,
601                    is_shielding: step.is_shielding(),
602                }
603            })
604            .collect();
605
606        proposal::Proposal {
607            proto_version: PROPOSAL_SER_V1,
608            fee_rule: match value.fee_rule() {
609                StandardFeeRule::Zip317 => proposal::FeeRule::Zip317,
610            }
611            .into(),
612            min_target_height: value.min_target_height().into(),
613            steps,
614        }
615    }
616
617    /// Attempts to parse a [`Proposal`] based upon a supported [`StandardFeeRule`] from its
618    /// protobuf representation.
619    pub fn try_into_standard_proposal<DbT, DbError>(
620        &self,
621        wallet_db: &DbT,
622    ) -> Result<Proposal<StandardFeeRule, DbT::NoteRef>, ProposalDecodingError<DbError>>
623    where
624        DbT: InputSource<Error = DbError>,
625    {
626        use self::proposal::proposed_input::Value::*;
627        match self.proto_version {
628            PROPOSAL_SER_V1 => {
629                let fee_rule = match self.fee_rule() {
630                    proposal::FeeRule::Zip317 => StandardFeeRule::Zip317,
631                    other => {
632                        return Err(ProposalDecodingError::FeeRuleNotSupported(other));
633                    }
634                };
635
636                let mut steps = Vec::with_capacity(self.steps.len());
637                for step in &self.steps {
638                    let transaction_request =
639                        TransactionRequest::from_uri(&step.transaction_request)?;
640
641                    let payment_pools = step
642                        .payment_output_pools
643                        .iter()
644                        .map(|pop| {
645                            Ok((
646                                usize::try_from(pop.payment_index)
647                                    .expect("Payment index fits into a usize"),
648                                pool_type(pop.value_pool)?,
649                            ))
650                        })
651                        .collect::<Result<BTreeMap<usize, PoolType>, ProposalDecodingError<DbError>>>()?;
652
653                    #[allow(unused_mut)]
654                    let mut transparent_inputs = vec![];
655                    let mut received_notes = vec![];
656                    let mut prior_step_inputs = vec![];
657                    for (i, input) in step.inputs.iter().enumerate() {
658                        match input
659                            .value
660                            .as_ref()
661                            .ok_or(ProposalDecodingError::NullInput(i))?
662                        {
663                            ReceivedOutput(out) => {
664                                let txid = out
665                                    .parse_txid()
666                                    .map_err(ProposalDecodingError::TxIdInvalid)?;
667
668                                match out.pool_type()? {
669                                    PoolType::Transparent => {
670                                        #[cfg(not(feature = "transparent-inputs"))]
671                                        return Err(ProposalDecodingError::ValuePoolNotSupported(
672                                            out.value_pool,
673                                        ));
674
675                                        #[cfg(feature = "transparent-inputs")]
676                                        {
677                                            let outpoint = OutPoint::new(txid.into(), out.index);
678                                            transparent_inputs.push(
679                                                wallet_db
680                                                    .get_unspent_transparent_output(&outpoint)
681                                                    .map_err(ProposalDecodingError::InputRetrieval)?
682                                                    .ok_or({
683                                                        ProposalDecodingError::InputNotFound(
684                                                            txid,
685                                                            PoolType::TRANSPARENT,
686                                                            out.index,
687                                                        )
688                                                    })?,
689                                            );
690                                        }
691                                    }
692                                    PoolType::Shielded(protocol) => received_notes.push(
693                                        wallet_db
694                                            .get_spendable_note(&txid, protocol, out.index)
695                                            .map_err(ProposalDecodingError::InputRetrieval)
696                                            .and_then(|opt| {
697                                                opt.ok_or({
698                                                    ProposalDecodingError::InputNotFound(
699                                                        txid,
700                                                        PoolType::Shielded(protocol),
701                                                        out.index,
702                                                    )
703                                                })
704                                            })?,
705                                    ),
706                                }
707                            }
708                            PriorStepOutput(s_ref) => {
709                                prior_step_inputs.push(StepOutput::new(
710                                    s_ref
711                                        .step_index
712                                        .try_into()
713                                        .expect("Step index fits into a usize"),
714                                    StepOutputIndex::Payment(
715                                        s_ref
716                                            .payment_index
717                                            .try_into()
718                                            .expect("Payment index fits into a usize"),
719                                    ),
720                                ));
721                            }
722                            PriorStepChange(s_ref) => {
723                                prior_step_inputs.push(StepOutput::new(
724                                    s_ref
725                                        .step_index
726                                        .try_into()
727                                        .expect("Step index fits into a usize"),
728                                    StepOutputIndex::Change(
729                                        s_ref
730                                            .change_index
731                                            .try_into()
732                                            .expect("Payment index fits into a usize"),
733                                    ),
734                                ));
735                            }
736                        }
737                    }
738
739                    let shielded_inputs = NonEmpty::from_vec(received_notes)
740                        .map(|notes| ShieldedInputs::from_parts(step.anchor_height.into(), notes));
741
742                    let proto_balance = step
743                        .balance
744                        .as_ref()
745                        .ok_or(ProposalDecodingError::BalanceInvalid)?;
746                    let balance = TransactionBalance::new(
747                        proto_balance
748                            .proposed_change
749                            .iter()
750                            .map(|cv| -> Result<ChangeValue, ProposalDecodingError<_>> {
751                                let value = Zatoshis::from_u64(cv.value)
752                                    .map_err(|_| ProposalDecodingError::BalanceInvalid)?;
753                                let memo = cv
754                                    .memo
755                                    .as_ref()
756                                    .map(|bytes| {
757                                        MemoBytes::from_bytes(&bytes.value)
758                                            .map_err(ProposalDecodingError::MemoInvalid)
759                                    })
760                                    .transpose()?;
761                                match (cv.pool_type()?, cv.is_ephemeral) {
762                                    (PoolType::Shielded(ShieldedProtocol::Sapling), false) => {
763                                        Ok(ChangeValue::sapling(value, memo))
764                                    }
765                                    #[cfg(feature = "orchard")]
766                                    (PoolType::Shielded(ShieldedProtocol::Orchard), false) => {
767                                        Ok(ChangeValue::orchard(value, memo))
768                                    }
769                                    (PoolType::Transparent, _) if memo.is_some() => {
770                                        Err(ProposalDecodingError::TransparentMemo)
771                                    }
772                                    #[cfg(feature = "transparent-inputs")]
773                                    (PoolType::Transparent, true) => {
774                                        Ok(ChangeValue::ephemeral_transparent(value))
775                                    }
776                                    (pool, false) => {
777                                        Err(ProposalDecodingError::InvalidChangeRecipient(pool))
778                                    }
779                                    (pool, true) => {
780                                        Err(ProposalDecodingError::InvalidEphemeralRecipient(pool))
781                                    }
782                                }
783                            })
784                            .collect::<Result<Vec<_>, _>>()?,
785                        Zatoshis::from_u64(proto_balance.fee_required)
786                            .map_err(|_| ProposalDecodingError::BalanceInvalid)?,
787                    )
788                    .map_err(|_| ProposalDecodingError::BalanceInvalid)?;
789
790                    let step = Step::from_parts(
791                        &steps,
792                        transaction_request,
793                        payment_pools,
794                        transparent_inputs,
795                        shielded_inputs,
796                        prior_step_inputs,
797                        balance,
798                        step.is_shielding,
799                    )
800                    .map_err(ProposalDecodingError::ProposalInvalid)?;
801
802                    steps.push(step);
803                }
804
805                Proposal::multi_step(
806                    fee_rule,
807                    self.min_target_height.into(),
808                    NonEmpty::from_vec(steps).ok_or(ProposalDecodingError::NoSteps)?,
809                )
810                .map_err(ProposalDecodingError::ProposalInvalid)
811            }
812            other => Err(ProposalDecodingError::VersionInvalid(other)),
813        }
814    }
815}
816
817#[cfg(feature = "lightwalletd-tonic-transport")]
818impl service::compact_tx_streamer_client::CompactTxStreamerClient<tonic::transport::Channel> {
819    /// Attempt to create a new client by connecting to a given endpoint.
820    pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
821    where
822        D: TryInto<tonic::transport::Endpoint>,
823        D::Error: Into<tonic::codegen::StdError>,
824    {
825        let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
826        Ok(Self::new(conn))
827    }
828}