1use 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 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 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 pub fn height(&self) -> BlockHeight {
96 self.height.try_into().unwrap()
97 }
98
99 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 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 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 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 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 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 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 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 #[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 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 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
334pub const PROPOSAL_SER_V1: u32 = 1;
336
337#[derive(Debug, Clone)]
340pub enum ProposalDecodingError<DbError> {
341 NoSteps,
343 Zip321(Zip321Error),
345 NullInput(usize),
347 TxIdInvalid(TryFromSliceError),
349 ValuePoolNotSupported(i32),
351 InputRetrieval(DbError),
353 InputNotFound(TxId, PoolType, u32),
356 BalanceInvalid,
358 MemoInvalid(memo::Error),
360 VersionInvalid(u32),
362 FeeRuleNotSupported(proposal::FeeRule),
364 ProposalInvalid(ProposalError),
366 EmptyShieldedInputs(ShieldedProtocol),
368 TransparentMemo,
370 InvalidChangeRecipient(PoolType),
372 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 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 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 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}