1use incrementalmerkletree::frontier::CommitmentTree;
4use nonempty::NonEmpty;
5use std::{
6 array::TryFromSliceError,
7 collections::BTreeMap,
8 fmt::{self, Display},
9 io,
10 num::NonZeroU32,
11};
12use zcash_address::unified::{self, Encoding};
13
14use self::proposal::proposed_input;
15use self::proposal::proposed_input::Value::*;
17use self::proposal::{PriorStepChange, PriorStepOutput, ReceivedOutput};
18
19use sapling::{self, Node, note::ExtractedNoteCommitment};
20use zcash_note_encryption::EphemeralKeyBytes;
21use zcash_primitives::{
22 block::{BlockHash, BlockHeader},
23 merkle_tree::read_commitment_tree,
24 transaction::{TxId, TxVersion},
25};
26use zcash_protocol::{
27 PoolType, ShieldedPool,
28 consensus::{self, BlockHeight, NetworkType},
29 memo::{self, MemoBytes},
30 value::Zatoshis,
31};
32use zip321::{TransactionRequest, Zip321Error};
33
34use crate::{
35 data_api::{
36 InputSource,
37 chain::ChainState,
38 wallet::{ConfirmationsPolicy, TargetHeight, input_selection::LockFilter},
39 },
40 fees::{ChangeValue, DummyOutputCounts, StandardFeeRule, TransactionBalance},
41 proposal::{
42 Proposal, ProposalError, ShieldedInputs, Step, StepOutput, StepOutputIndex,
43 produces_shielded_bundle,
44 },
45};
46
47#[cfg(feature = "transparent-inputs")]
48use transparent::bundle::OutPoint;
49
50#[cfg(feature = "orchard")]
51use orchard::tree::MerkleHashOrchard;
52
53#[rustfmt::skip]
54#[allow(unknown_lints)]
55#[allow(clippy::derive_partial_eq_without_eq)]
56#[allow(clippy::doc_overindented_list_items)]
57pub mod compact_formats;
58
59#[rustfmt::skip]
60#[allow(unknown_lints)]
61#[allow(clippy::derive_partial_eq_without_eq)]
62#[allow(clippy::doc_overindented_list_items)]
63pub mod proposal;
64
65#[rustfmt::skip]
66#[allow(unknown_lints)]
67#[allow(clippy::derive_partial_eq_without_eq)]
68#[allow(clippy::doc_overindented_list_items)]
69pub mod service;
70
71impl compact_formats::CompactBlock {
72 pub fn hash(&self) -> BlockHash {
79 if let Some(header) = self.header() {
80 header.hash()
81 } else {
82 BlockHash::from_slice(&self.hash)
83 }
84 }
85
86 pub fn prev_hash(&self) -> BlockHash {
93 if let Some(header) = self.header() {
94 header.prev_block
95 } else {
96 BlockHash::from_slice(&self.prev_hash)
97 }
98 }
99
100 pub fn height(&self) -> BlockHeight {
107 self.height.try_into().unwrap()
108 }
109
110 pub fn header(&self) -> Option<BlockHeader> {
114 if self.header.is_empty() {
115 None
116 } else {
117 BlockHeader::read(&self.header[..]).ok()
118 }
119 }
120}
121
122impl compact_formats::CompactTx {
123 pub fn txid(&self) -> TxId {
125 let mut txid_bytes = [0u8; 32];
126 txid_bytes.copy_from_slice(&self.txid);
127 TxId::from_bytes(txid_bytes)
128 }
129}
130
131#[derive(Clone, Debug)]
133#[non_exhaustive]
134pub enum CompactFormatError {
135 InvalidLength(TryFromSliceError),
137 InvalidValue,
139}
140
141impl Display for CompactFormatError {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 match self {
144 CompactFormatError::InvalidLength(e) => write!(f, "Invalid compact format field: {e}"),
145 CompactFormatError::InvalidValue => {
146 write!(f, "Compact format field is not a valid protocol element")
147 }
148 }
149 }
150}
151
152impl compact_formats::CompactSaplingOutput {
153 pub fn cmu(&self) -> Result<ExtractedNoteCommitment, CompactFormatError> {
157 let mut repr = [0; 32];
158 repr.copy_from_slice(&self.cmu[..]);
159 Option::from(ExtractedNoteCommitment::from_bytes(&repr))
160 .ok_or(CompactFormatError::InvalidValue)
161 }
162
163 pub fn ephemeral_key(&self) -> Result<EphemeralKeyBytes, CompactFormatError> {
167 self.ephemeral_key[..]
168 .try_into()
169 .map(EphemeralKeyBytes)
170 .map_err(CompactFormatError::InvalidLength)
171 }
172}
173
174impl<Proof> From<&sapling::bundle::OutputDescription<Proof>>
175 for compact_formats::CompactSaplingOutput
176{
177 fn from(
178 out: &sapling::bundle::OutputDescription<Proof>,
179 ) -> compact_formats::CompactSaplingOutput {
180 compact_formats::CompactSaplingOutput {
181 cmu: out.cmu().to_bytes().to_vec(),
182 ephemeral_key: out.ephemeral_key().as_ref().to_vec(),
183 ciphertext: out.enc_ciphertext()[..sapling::note_encryption::COMPACT_NOTE_SIZE]
184 .to_vec(),
185 }
186 }
187}
188
189impl TryFrom<compact_formats::CompactSaplingOutput>
190 for sapling::note_encryption::CompactOutputDescription
191{
192 type Error = CompactFormatError;
193
194 fn try_from(value: compact_formats::CompactSaplingOutput) -> Result<Self, Self::Error> {
195 (&value).try_into()
196 }
197}
198
199impl TryFrom<&compact_formats::CompactSaplingOutput>
200 for sapling::note_encryption::CompactOutputDescription
201{
202 type Error = CompactFormatError;
203
204 fn try_from(value: &compact_formats::CompactSaplingOutput) -> Result<Self, Self::Error> {
205 Ok(sapling::note_encryption::CompactOutputDescription {
206 cmu: value.cmu()?,
207 ephemeral_key: value.ephemeral_key()?,
208 enc_ciphertext: value.ciphertext[..]
209 .try_into()
210 .map_err(CompactFormatError::InvalidLength)?,
211 })
212 }
213}
214
215impl compact_formats::CompactSaplingSpend {
216 pub fn nf(&self) -> Result<sapling::Nullifier, CompactFormatError> {
220 sapling::Nullifier::from_slice(&self.nf).map_err(CompactFormatError::InvalidLength)
221 }
222}
223
224#[cfg(feature = "transparent-inputs")]
225impl compact_formats::CompactTxIn {
226 pub fn prevout(&self) -> Result<transparent::bundle::OutPoint, CompactFormatError> {
231 Ok(transparent::bundle::OutPoint::new(
232 self.prevout_txid[..]
233 .try_into()
234 .map_err(CompactFormatError::InvalidLength)?,
235 self.prevout_index,
236 ))
237 }
238}
239
240#[cfg(feature = "transparent-inputs")]
241impl compact_formats::TxOut {
242 pub fn to_txout(&self) -> Result<transparent::bundle::TxOut, CompactFormatError> {
250 Ok(transparent::bundle::TxOut::new(
251 Zatoshis::from_u64(self.value).map_err(|_| CompactFormatError::InvalidValue)?,
252 transparent::address::Script(zcash_script::script::Code(self.script_pub_key.clone())),
253 ))
254 }
255}
256
257#[cfg(feature = "orchard")]
258impl TryFrom<&compact_formats::CompactOrchardAction> for orchard::note_encryption::CompactAction {
259 type Error = CompactFormatError;
260
261 fn try_from(value: &compact_formats::CompactOrchardAction) -> Result<Self, Self::Error> {
262 Ok(orchard::note_encryption::CompactAction::from_parts(
263 value.nf()?,
264 value.cmx()?,
265 value.ephemeral_key()?,
266 value.ciphertext[..]
267 .try_into()
268 .map_err(CompactFormatError::InvalidLength)?,
269 ))
270 }
271}
272
273#[cfg(feature = "orchard")]
274impl compact_formats::CompactOrchardAction {
275 pub fn cmx(&self) -> Result<orchard::note::ExtractedNoteCommitment, CompactFormatError> {
279 Option::from(orchard::note::ExtractedNoteCommitment::from_bytes(
280 &self.cmx[..]
281 .try_into()
282 .map_err(CompactFormatError::InvalidLength)?,
283 ))
284 .ok_or(CompactFormatError::InvalidValue)
285 }
286
287 pub fn nf(&self) -> Result<orchard::note::Nullifier, CompactFormatError> {
291 let nf_bytes: [u8; 32] = self.nullifier[..]
292 .try_into()
293 .map_err(CompactFormatError::InvalidLength)?;
294 Option::from(orchard::note::Nullifier::from_bytes(&nf_bytes))
295 .ok_or(CompactFormatError::InvalidValue)
296 }
297
298 pub fn ephemeral_key(&self) -> Result<EphemeralKeyBytes, CompactFormatError> {
302 self.ephemeral_key[..]
303 .try_into()
304 .map(EphemeralKeyBytes)
305 .map_err(CompactFormatError::InvalidLength)
306 }
307}
308
309impl<A: sapling::bundle::Authorization> From<&sapling::bundle::SpendDescription<A>>
310 for compact_formats::CompactSaplingSpend
311{
312 fn from(spend: &sapling::bundle::SpendDescription<A>) -> compact_formats::CompactSaplingSpend {
313 compact_formats::CompactSaplingSpend {
314 nf: spend.nullifier().to_vec(),
315 }
316 }
317}
318
319#[cfg(feature = "orchard")]
320impl<SpendAuth> From<&orchard::Action<SpendAuth>> for compact_formats::CompactOrchardAction {
321 fn from(action: &orchard::Action<SpendAuth>) -> compact_formats::CompactOrchardAction {
322 compact_formats::CompactOrchardAction {
323 nullifier: action.nullifier().to_bytes().to_vec(),
324 cmx: action.cmx().to_bytes().to_vec(),
325 ephemeral_key: action.encrypted_note().epk_bytes.to_vec(),
326 ciphertext: action.encrypted_note().enc_ciphertext.0
327 [..orchard::note_encryption::COMPACT_NOTE_SIZE]
328 .to_vec(),
329 }
330 }
331}
332
333impl service::LightdInfo {
334 pub fn chain_name(&self) -> Option<NetworkType> {
337 match self.chain_name.as_str() {
338 "main" => Some(NetworkType::Main),
339 "test" => Some(NetworkType::Test),
340 "regtest" => Some(NetworkType::Regtest),
341 _ => None,
342 }
343 }
344
345 pub fn sapling_activation_height(&self) -> BlockHeight {
352 self.sapling_activation_height
353 .try_into()
354 .expect("lightwalletd should provide in-range heights")
355 }
356
357 pub fn consensus_branch_id(&self) -> Option<consensus::BranchId> {
360 u32::from_str_radix(&self.consensus_branch_id, 16)
361 .ok()?
362 .try_into()
363 .ok()
364 }
365
366 pub fn block_height(&self) -> BlockHeight {
376 self.block_height
377 .try_into()
378 .expect("lightwalletd should provide in-range heights")
379 }
380
381 pub fn estimated_height(&self) -> BlockHeight {
391 self.estimated_height
392 .try_into()
393 .expect("lightwalletd should provide in-range heights")
394 }
395
396 pub fn donation_address(&self) -> Option<unified::Address> {
403 if self.donation_address.is_empty() {
404 None
405 } else {
406 let (network_type, _revision, address) =
407 unified::Address::decode(&self.donation_address).ok()?;
408 (Some(network_type) == self.chain_name()).then_some(address)
409 }
410 }
411}
412
413impl service::TreeState {
414 pub fn sapling_tree(
416 &self,
417 ) -> io::Result<CommitmentTree<Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>> {
418 if self.sapling_tree.is_empty() {
419 Ok(CommitmentTree::empty())
420 } else {
421 let sapling_tree_bytes = hex::decode(&self.sapling_tree).map_err(|e| {
422 io::Error::new(
423 io::ErrorKind::InvalidData,
424 format!("Hex decoding of Sapling tree bytes failed: {e:?}"),
425 )
426 })?;
427 read_commitment_tree::<Node, _, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>(
428 &sapling_tree_bytes[..],
429 )
430 }
431 }
432
433 #[cfg(feature = "orchard")]
435 pub fn orchard_tree(
436 &self,
437 ) -> io::Result<CommitmentTree<MerkleHashOrchard, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>>
438 {
439 if self.orchard_tree.is_empty() {
440 Ok(CommitmentTree::empty())
441 } else {
442 let orchard_tree_bytes = hex::decode(&self.orchard_tree).map_err(|e| {
443 io::Error::new(
444 io::ErrorKind::InvalidData,
445 format!("Hex decoding of Orchard tree bytes failed: {e:?}"),
446 )
447 })?;
448 read_commitment_tree::<
449 MerkleHashOrchard,
450 _,
451 { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
452 >(&orchard_tree_bytes[..])
453 }
454 }
455
456 #[cfg(feature = "orchard")]
462 pub fn ironwood_tree(
463 &self,
464 ) -> io::Result<CommitmentTree<MerkleHashOrchard, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>>
465 {
466 if self.ironwood_tree.is_empty() {
467 Ok(CommitmentTree::empty())
468 } else {
469 let ironwood_tree_bytes = hex::decode(&self.ironwood_tree).map_err(|e| {
470 io::Error::new(
471 io::ErrorKind::InvalidData,
472 format!("Hex decoding of Ironwood tree bytes failed: {e:?}"),
473 )
474 })?;
475 read_commitment_tree::<
476 MerkleHashOrchard,
477 _,
478 { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
479 >(&ironwood_tree_bytes[..])
480 }
481 }
482
483 pub fn to_chain_state(&self) -> io::Result<ChainState> {
487 let mut hash_bytes = hex::decode(&self.hash).map_err(|e| {
488 io::Error::new(
489 io::ErrorKind::InvalidData,
490 format!("Block hash is not valid hex: {e:?}"),
491 )
492 })?;
493 hash_bytes.reverse();
495
496 Ok(ChainState::new(
497 self.height
498 .try_into()
499 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid block height"))?,
500 BlockHash::try_from_slice(&hash_bytes).ok_or_else(|| {
501 io::Error::new(io::ErrorKind::InvalidData, "Invalid block hash length.")
502 })?,
503 self.sapling_tree()?.to_frontier(),
504 #[cfg(feature = "orchard")]
505 self.orchard_tree()?.to_frontier(),
506 #[cfg(feature = "orchard")]
507 self.ironwood_tree()?.to_frontier(),
508 ))
509 }
510}
511
512pub const PROPOSAL_SER_V1: u32 = 1;
514
515#[derive(Debug, Clone)]
518#[non_exhaustive]
519pub enum ProposalDecodingError<DbError> {
520 NoSteps,
522 Zip321(Zip321Error),
524 NullInput(usize),
526 TxIdInvalid(TryFromSliceError),
528 ValuePoolNotSupported(i32),
530 InputRetrieval(DbError),
532 InputNotFound(TxId, PoolType, u32),
535 BalanceInvalid,
537 MemoInvalid(memo::Error),
539 VersionInvalid(u32),
541 FeeRuleNotSupported(proposal::FeeRule),
543 ProposalInvalid(ProposalError),
545 EmptyShieldedInputs(ShieldedPool),
547 TransparentMemo,
549 InvalidChangeRecipient(PoolType),
551 InvalidEphemeralRecipient(PoolType),
553 ConfirmationsPolicyInvalid,
556 OrchardPaymentProhibited,
560 MissingShieldedAnchor,
566 ProposedVersionInvalid(u32),
569}
570
571impl<E> From<Zip321Error> for ProposalDecodingError<E> {
572 fn from(value: Zip321Error) -> Self {
573 Self::Zip321(value)
574 }
575}
576
577impl<E: Display> Display for ProposalDecodingError<E> {
578 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
579 match self {
580 ProposalDecodingError::NoSteps => write!(f, "The proposal had no steps."),
581 ProposalDecodingError::Zip321(err) => write!(f, "Transaction request invalid: {err}"),
582 ProposalDecodingError::NullInput(i) => {
583 write!(f, "Proposed input was null at index {i}")
584 }
585 ProposalDecodingError::TxIdInvalid(err) => {
586 write!(f, "Invalid transaction id: {err:?}")
587 }
588 ProposalDecodingError::ValuePoolNotSupported(id) => {
589 write!(f, "Invalid value pool identifier: {id:?}")
590 }
591 ProposalDecodingError::InputRetrieval(err) => {
592 write!(f, "An error occurred retrieving a transaction input: {err}")
593 }
594 ProposalDecodingError::InputNotFound(txid, pool, idx) => {
595 write!(f, "No {pool} input found for txid {txid}, index {idx}")
596 }
597 ProposalDecodingError::BalanceInvalid => {
598 write!(f, "An error occurred decoding the proposal balance.")
599 }
600 ProposalDecodingError::MemoInvalid(err) => {
601 write!(f, "An error occurred decoding a proposed memo: {err}")
602 }
603 ProposalDecodingError::VersionInvalid(v) => {
604 write!(f, "Unrecognized proposal version {v}")
605 }
606 ProposalDecodingError::FeeRuleNotSupported(r) => {
607 write!(
608 f,
609 "Fee calculation using the {r:?} fee rule is not supported."
610 )
611 }
612 ProposalDecodingError::ProposalInvalid(err) => write!(f, "{err}"),
613 ProposalDecodingError::EmptyShieldedInputs(protocol) => write!(
614 f,
615 "An inputs field was present for {protocol:?}, but contained no note references."
616 ),
617 ProposalDecodingError::TransparentMemo => {
618 write!(f, "Transparent outputs cannot have memos.")
619 }
620 ProposalDecodingError::InvalidChangeRecipient(pool_type) => write!(
621 f,
622 "Change outputs to the {pool_type} pool are not supported."
623 ),
624 ProposalDecodingError::InvalidEphemeralRecipient(pool_type) => write!(
625 f,
626 "Ephemeral outputs to the {pool_type} pool are not supported."
627 ),
628 ProposalDecodingError::ConfirmationsPolicyInvalid => {
629 write!(f, "The encoded confirmations policy was not valid.")
630 }
631 ProposalDecodingError::OrchardPaymentProhibited => write!(
632 f,
633 "A payment may not be directed to the Orchard pool once Ironwood is active."
634 ),
635 ProposalDecodingError::MissingShieldedAnchor => write!(
636 f,
637 "A proposal step that produces a shielded bundle must specify an anchor height."
638 ),
639 ProposalDecodingError::ProposedVersionInvalid(header) => write!(
640 f,
641 "The proposal specified an unrecognized transaction version header {header:#x}."
642 ),
643 }
644 }
645}
646
647impl<E: std::error::Error + 'static> std::error::Error for ProposalDecodingError<E> {
648 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
649 match self {
650 ProposalDecodingError::Zip321(e) => Some(e),
651 ProposalDecodingError::InputRetrieval(e) => Some(e),
652 ProposalDecodingError::MemoInvalid(e) => Some(e),
653 _ => None,
654 }
655 }
656}
657
658fn pool_type<T>(pool_id: i32) -> Result<PoolType, ProposalDecodingError<T>> {
659 match proposal::ValuePool::try_from(pool_id) {
660 Ok(proposal::ValuePool::Transparent) => Ok(PoolType::TRANSPARENT),
661 Ok(proposal::ValuePool::Sapling) => Ok(PoolType::SAPLING),
662 Ok(proposal::ValuePool::Orchard) => Ok(PoolType::ORCHARD),
663 Ok(proposal::ValuePool::Ironwood) => Ok(PoolType::IRONWOOD),
664 _ => Err(ProposalDecodingError::ValuePoolNotSupported(pool_id)),
665 }
666}
667
668impl proposal::ReceivedOutput {
669 pub fn parse_txid(&self) -> Result<TxId, TryFromSliceError> {
670 Ok(TxId::from_bytes(self.txid[..].try_into()?))
671 }
672
673 pub fn pool_type<T>(&self) -> Result<PoolType, ProposalDecodingError<T>> {
674 pool_type(self.value_pool)
675 }
676}
677
678impl proposal::ChangeValue {
679 pub fn pool_type<T>(&self) -> Result<PoolType, ProposalDecodingError<T>> {
680 pool_type(self.value_pool)
681 }
682}
683
684impl From<PoolType> for proposal::ValuePool {
685 fn from(value: PoolType) -> Self {
686 match value {
687 PoolType::Transparent => proposal::ValuePool::Transparent,
688 PoolType::Shielded(p) => p.into(),
689 }
690 }
691}
692
693impl From<ShieldedPool> for proposal::ValuePool {
694 fn from(value: ShieldedPool) -> Self {
695 match value {
696 ShieldedPool::Sapling => proposal::ValuePool::Sapling,
697 ShieldedPool::Orchard => proposal::ValuePool::Orchard,
698 ShieldedPool::Ironwood => proposal::ValuePool::Ironwood,
699 }
700 }
701}
702
703impl proposal::Proposal {
704 pub fn from_standard_proposal<NoteRef>(value: &Proposal<StandardFeeRule, NoteRef>) -> Self {
707 let steps = value
708 .steps()
709 .iter()
710 .map(|step| {
711 let transaction_request = step.transaction_request().to_uri();
712
713 let anchor_height = step.anchor_height().map_or(0, u32::from);
715
716 let inputs = step
717 .transparent_inputs()
718 .iter()
719 .map(|utxo| proposal::ProposedInput {
720 value: Some(proposed_input::Value::ReceivedOutput(ReceivedOutput {
721 txid: utxo.outpoint().hash().to_vec(),
722 value_pool: proposal::ValuePool::Transparent.into(),
723 index: utxo.outpoint().n(),
724 value: utxo.txout().value().into(),
725 })),
726 })
727 .chain(step.shielded_inputs().iter().flat_map(|s_in| {
728 s_in.notes().iter().map(|rec_note| proposal::ProposedInput {
729 value: Some(proposed_input::Value::ReceivedOutput(ReceivedOutput {
730 txid: rec_note.txid().as_ref().to_vec(),
731 value_pool: proposal::ValuePool::from(rec_note.note().pool())
732 .into(),
733 index: rec_note.output_index().into(),
734 value: rec_note.note().value().into(),
735 })),
736 })
737 }))
738 .chain(step.prior_step_inputs().iter().map(|p_in| {
739 match p_in.output_index() {
740 StepOutputIndex::Payment(i) => proposal::ProposedInput {
741 value: Some(proposed_input::Value::PriorStepOutput(
742 PriorStepOutput {
743 step_index: p_in
744 .step_index()
745 .try_into()
746 .expect("Step index fits into a u32"),
747 payment_index: i
748 .try_into()
749 .expect("Payment index fits into a u32"),
750 },
751 )),
752 },
753 StepOutputIndex::Change(i) => proposal::ProposedInput {
754 value: Some(proposed_input::Value::PriorStepChange(
755 PriorStepChange {
756 step_index: p_in
757 .step_index()
758 .try_into()
759 .expect("Step index fits into a u32"),
760 change_index: i
761 .try_into()
762 .expect("Payment index fits into a u32"),
763 },
764 )),
765 },
766 }
767 }))
768 .collect();
769
770 let payment_output_pools = step
771 .payment_pools()
772 .iter()
773 .map(|(idx, pool_type)| proposal::PaymentOutputPool {
774 payment_index: u32::try_from(*idx).expect("Payment index fits into a u32"),
775 value_pool: proposal::ValuePool::from(*pool_type).into(),
776 })
777 .collect();
778
779 let balance = Some(proposal::TransactionBalance {
780 proposed_change: step
781 .balance()
782 .proposed_change()
783 .iter()
784 .map(|change| proposal::ChangeValue {
785 value: change.value().into(),
786 value_pool: proposal::ValuePool::from(change.output_pool()).into(),
787 memo: change.memo().map(|memo_bytes| proposal::MemoBytes {
788 value: memo_bytes.as_slice().to_vec(),
789 }),
790 is_ephemeral: change.is_ephemeral(),
791 })
792 .collect(),
793 fee_required: step.balance().fee_required().into(),
794 dummy_outputs: step.balance().dummy_outputs().map(|counts| {
795 proposal::DummyOutputs {
796 sapling: counts
797 .sapling()
798 .try_into()
799 .expect("Sapling dummy-output count fits into u32"),
800 #[cfg(feature = "orchard")]
801 orchard: counts
802 .orchard()
803 .try_into()
804 .expect("Orchard dummy-output count fits into u32"),
805 #[cfg(not(feature = "orchard"))]
806 orchard: 0,
807 #[cfg(feature = "orchard")]
808 ironwood: counts
809 .ironwood()
810 .try_into()
811 .expect("Ironwood dummy-output count fits into u32"),
812 #[cfg(not(feature = "orchard"))]
813 ironwood: 0,
814 }
815 }),
816 });
817
818 proposal::ProposalStep {
819 transaction_request,
820 payment_output_pools,
821 anchor_height,
822 inputs,
823 balance,
824 is_shielding: step.is_shielding(),
825 }
826 })
827 .collect();
828
829 let confirmations_policy = value.confirmations_policy();
830 proposal::Proposal {
831 proto_version: PROPOSAL_SER_V1,
832 fee_rule: match value.fee_rule() {
833 StandardFeeRule::Zip317 => proposal::FeeRule::Zip317,
834 }
835 .into(),
836 min_target_height: value.min_target_height().into(),
837 steps,
838 confirmations_policy: Some(proposal::ConfirmationsPolicy {
839 trusted: confirmations_policy.trusted().into(),
840 untrusted: confirmations_policy.untrusted().into(),
841 #[cfg(feature = "transparent-inputs")]
842 allow_zero_conf_shielding: confirmations_policy.allow_zero_conf_shielding(),
843 #[cfg(not(feature = "transparent-inputs"))]
844 allow_zero_conf_shielding: true,
845 }),
846 proposed_version: value.proposed_version().map(|v| v.header()),
847 }
848 }
849
850 pub fn try_into_standard_proposal<ParamsT, DbT, DbError>(
853 &self,
854 params: &ParamsT,
855 wallet_db: &DbT,
856 ) -> Result<Proposal<StandardFeeRule, DbT::NoteRef>, ProposalDecodingError<DbError>>
857 where
858 ParamsT: consensus::Parameters,
859 DbT: InputSource<Error = DbError>,
860 {
861 match self.proto_version {
862 PROPOSAL_SER_V1 => {
863 let fee_rule = match self.fee_rule() {
864 proposal::FeeRule::Zip317 => StandardFeeRule::Zip317,
865 other => {
866 return Err(ProposalDecodingError::FeeRuleNotSupported(other));
867 }
868 };
869
870 let target_height = TargetHeight::from(self.min_target_height);
871
872 let lock_filter = LockFilter::Unfiltered;
878
879 #[cfg(feature = "orchard")]
882 let ironwood_active = params.is_nu_active(
883 consensus::NetworkUpgrade::Nu6_3,
884 BlockHeight::from(target_height),
885 );
886 #[cfg(not(feature = "orchard"))]
887 let _ = params;
888
889 let mut steps = Vec::with_capacity(self.steps.len());
890 for step in &self.steps {
891 let transaction_request =
892 TransactionRequest::from_uri(&step.transaction_request)?;
893
894 let payment_pools = step
895 .payment_output_pools
896 .iter()
897 .map(|pop| {
898 Ok((
899 usize::try_from(pop.payment_index)
900 .expect("Payment index fits into a usize"),
901 pool_type(pop.value_pool)?,
902 ))
903 })
904 .collect::<Result<BTreeMap<usize, PoolType>, ProposalDecodingError<DbError>>>()?;
905
906 #[cfg(feature = "orchard")]
911 if ironwood_active
912 && payment_pools
913 .values()
914 .any(|pool| *pool == PoolType::ORCHARD)
915 {
916 return Err(ProposalDecodingError::OrchardPaymentProhibited);
917 }
918
919 #[allow(unused_mut)]
920 let mut transparent_inputs = vec![];
921 let mut received_notes = vec![];
922 let mut prior_step_inputs = vec![];
923 for (i, input) in step.inputs.iter().enumerate() {
924 match input
925 .value
926 .as_ref()
927 .ok_or(ProposalDecodingError::NullInput(i))?
928 {
929 ReceivedOutput(out) => {
930 let txid = out
931 .parse_txid()
932 .map_err(ProposalDecodingError::TxIdInvalid)?;
933
934 match out.pool_type()? {
935 PoolType::Transparent => {
936 #[cfg(not(feature = "transparent-inputs"))]
937 return Err(ProposalDecodingError::ValuePoolNotSupported(
938 out.value_pool,
939 ));
940
941 #[cfg(feature = "transparent-inputs")]
942 {
943 let outpoint = OutPoint::new(txid.into(), out.index);
944 transparent_inputs.push(
945 wallet_db
946 .get_unspent_transparent_output(
947 &outpoint,
948 target_height,
949 )
950 .map_err(ProposalDecodingError::InputRetrieval)?
951 .ok_or({
952 ProposalDecodingError::InputNotFound(
953 txid,
954 PoolType::TRANSPARENT,
955 out.index,
956 )
957 })?
958 .redact_account_data(),
959 );
960 }
961 }
962 PoolType::Shielded(protocol) => received_notes.push(
963 wallet_db
964 .get_spendable_note(
965 &txid,
966 protocol,
967 out.index,
968 target_height,
969 lock_filter,
970 )
971 .map_err(ProposalDecodingError::InputRetrieval)
972 .and_then(|opt| {
973 opt.ok_or({
974 ProposalDecodingError::InputNotFound(
975 txid,
976 PoolType::Shielded(protocol),
977 out.index,
978 )
979 })
980 })?,
981 ),
982 }
983 }
984 PriorStepOutput(s_ref) => {
985 prior_step_inputs.push(StepOutput::new(
986 s_ref
987 .step_index
988 .try_into()
989 .expect("Step index fits into a usize"),
990 StepOutputIndex::Payment(
991 s_ref
992 .payment_index
993 .try_into()
994 .expect("Payment index fits into a usize"),
995 ),
996 ));
997 }
998 PriorStepChange(s_ref) => {
999 prior_step_inputs.push(StepOutput::new(
1000 s_ref
1001 .step_index
1002 .try_into()
1003 .expect("Step index fits into a usize"),
1004 StepOutputIndex::Change(
1005 s_ref
1006 .change_index
1007 .try_into()
1008 .expect("Payment index fits into a usize"),
1009 ),
1010 ));
1011 }
1012 }
1013 }
1014
1015 let shielded_inputs =
1016 NonEmpty::from_vec(received_notes).map(ShieldedInputs::from_parts);
1017
1018 let proto_balance = step
1019 .balance
1020 .as_ref()
1021 .ok_or(ProposalDecodingError::BalanceInvalid)?;
1022 let balance = TransactionBalance::new(
1023 proto_balance
1024 .proposed_change
1025 .iter()
1026 .map(|cv| -> Result<ChangeValue, ProposalDecodingError<_>> {
1027 let value = Zatoshis::from_u64(cv.value)
1028 .map_err(|_| ProposalDecodingError::BalanceInvalid)?;
1029 let memo = cv
1030 .memo
1031 .as_ref()
1032 .map(|bytes| {
1033 MemoBytes::from_bytes(&bytes.value)
1034 .map_err(ProposalDecodingError::MemoInvalid)
1035 })
1036 .transpose()?;
1037 match (cv.pool_type()?, cv.is_ephemeral) {
1038 (PoolType::Shielded(ShieldedPool::Sapling), false) => {
1039 Ok(ChangeValue::sapling(value, memo))
1040 }
1041 #[cfg(feature = "orchard")]
1042 (PoolType::Shielded(ShieldedPool::Orchard), false) => {
1043 Ok(ChangeValue::orchard(value, memo))
1044 }
1045 #[cfg(feature = "orchard")]
1046 (PoolType::Shielded(ShieldedPool::Ironwood), false) => Ok(
1047 ChangeValue::shielded(ShieldedPool::Ironwood, value, memo),
1048 ),
1049 (PoolType::Transparent, _) if memo.is_some() => {
1050 Err(ProposalDecodingError::TransparentMemo)
1051 }
1052 #[cfg(feature = "transparent-inputs")]
1053 (PoolType::Transparent, true) => {
1054 Ok(ChangeValue::ephemeral_transparent(value))
1055 }
1056 #[cfg(feature = "transparent-inputs")]
1057 (PoolType::Transparent, false) => {
1058 Ok(ChangeValue::transparent(value))
1059 }
1060 #[allow(unreachable_patterns)]
1064 (pool, false) => {
1065 Err(ProposalDecodingError::InvalidChangeRecipient(pool))
1066 }
1067 (pool, true) => {
1068 Err(ProposalDecodingError::InvalidEphemeralRecipient(pool))
1069 }
1070 }
1071 })
1072 .collect::<Result<Vec<_>, _>>()?,
1073 Zatoshis::from_u64(proto_balance.fee_required)
1074 .map_err(|_| ProposalDecodingError::BalanceInvalid)?,
1075 )
1076 .map_err(|_| ProposalDecodingError::BalanceInvalid)?;
1077 let balance = match proto_balance.dummy_outputs.as_ref() {
1078 Some(counts) => {
1079 #[cfg(feature = "orchard")]
1080 let dummy_outputs = DummyOutputCounts::new(
1081 counts.sapling as usize,
1082 counts.orchard as usize,
1083 counts.ironwood as usize,
1084 );
1085 #[cfg(not(feature = "orchard"))]
1086 let dummy_outputs = DummyOutputCounts::new(counts.sapling as usize);
1087 balance.with_dummy_outputs(dummy_outputs)
1088 }
1089 None => balance,
1091 };
1092
1093 let anchor_height = match step.anchor_height {
1100 0 if produces_shielded_bundle(
1101 shielded_inputs.is_some(),
1102 &payment_pools,
1103 &balance,
1104 ) =>
1105 {
1106 return Err(ProposalDecodingError::MissingShieldedAnchor);
1107 }
1108 0 => None,
1109 h => Some(BlockHeight::from_u32(h)),
1110 };
1111
1112 let step = Step::from_parts(
1113 &steps,
1114 transaction_request,
1115 payment_pools,
1116 transparent_inputs,
1117 shielded_inputs,
1118 anchor_height,
1119 prior_step_inputs,
1120 balance,
1121 step.is_shielding,
1122 #[cfg(feature = "orchard")]
1123 ironwood_active,
1124 )
1125 .map_err(ProposalDecodingError::ProposalInvalid)?;
1126
1127 steps.push(step);
1128 }
1129
1130 let confirmations_policy = match &self.confirmations_policy {
1134 Some(cp) => ConfirmationsPolicy::new(
1135 NonZeroU32::new(cp.trusted)
1136 .ok_or(ProposalDecodingError::ConfirmationsPolicyInvalid)?,
1137 NonZeroU32::new(cp.untrusted)
1138 .ok_or(ProposalDecodingError::ConfirmationsPolicyInvalid)?,
1139 #[cfg(feature = "transparent-inputs")]
1140 cp.allow_zero_conf_shielding,
1141 )
1142 .map_err(|_| ProposalDecodingError::ConfirmationsPolicyInvalid)?,
1143 None => ConfirmationsPolicy::default(),
1144 };
1145
1146 let proposed_version = self
1150 .proposed_version
1151 .map(|header| {
1152 if header == TxVersion::V5.header() {
1153 Ok(TxVersion::V5)
1154 } else if header == TxVersion::V6.header() {
1155 Ok(TxVersion::V6)
1156 } else {
1157 Err(ProposalDecodingError::ProposedVersionInvalid(header))
1158 }
1159 })
1160 .transpose()?;
1161
1162 Proposal::multi_step(
1163 fee_rule,
1164 target_height,
1165 confirmations_policy,
1166 NonEmpty::from_vec(steps).ok_or(ProposalDecodingError::NoSteps)?,
1167 )
1168 .map(|proposal| proposal.with_proposed_version(proposed_version))
1169 .map_err(ProposalDecodingError::ProposalInvalid)
1170 }
1171 other => Err(ProposalDecodingError::VersionInvalid(other)),
1172 }
1173 }
1174}
1175
1176#[cfg(feature = "lightwalletd-tonic-transport")]
1177impl service::compact_tx_streamer_client::CompactTxStreamerClient<tonic::transport::Channel> {
1178 pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
1180 where
1181 D: TryInto<tonic::transport::Endpoint>,
1182 D::Error: Into<tonic::codegen::StdError>,
1183 {
1184 let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
1185 Ok(Self::new(conn))
1186 }
1187}