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