1use 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 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 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 pub fn height(&self) -> BlockHeight {
97 self.height.try_into().unwrap()
98 }
99
100 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 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 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 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 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 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 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 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 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 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 pub fn block_height(&self) -> BlockHeight {
308 self.block_height
309 .try_into()
310 .expect("lightwalletd should provide in-range heights")
311 }
312
313 pub fn estimated_height(&self) -> BlockHeight {
323 self.estimated_height
324 .try_into()
325 .expect("lightwalletd should provide in-range heights")
326 }
327
328 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 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 #[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 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 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
414pub const PROPOSAL_SER_V1: u32 = 1;
416
417#[derive(Debug, Clone)]
420pub enum ProposalDecodingError<DbError> {
421 NoSteps,
423 Zip321(Zip321Error),
425 NullInput(usize),
427 TxIdInvalid(TryFromSliceError),
429 ValuePoolNotSupported(i32),
431 InputRetrieval(DbError),
433 InputNotFound(TxId, PoolType, u32),
436 BalanceInvalid,
438 MemoInvalid(memo::Error),
440 VersionInvalid(u32),
442 FeeRuleNotSupported(proposal::FeeRule),
444 ProposalInvalid(ProposalError),
446 EmptyShieldedInputs(ShieldedProtocol),
448 TransparentMemo,
450 InvalidChangeRecipient(PoolType),
452 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 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 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 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}