Skip to main content

zcash_client_backend/
wallet.rs

1//! Structs representing transaction data scanned from the block chain by a wallet or
2//! light client.
3use std::fmt::Debug;
4
5use incrementalmerkletree::Position;
6
7use ::transparent::{
8    address::TransparentAddress,
9    bundle::{OutPoint, TxOut},
10    keys::TransparentKeyScope,
11};
12use zcash_address::ZcashAddress;
13use zcash_keys::{address::Receiver, keys::OutgoingViewingKey};
14use zcash_note_encryption::EphemeralKeyBytes;
15use zcash_primitives::transaction::{TxId, fees::transparent as transparent_fees};
16use zcash_protocol::{
17    PoolType, ShieldedPool,
18    consensus::{BlockHeight, TxIndex},
19    value::{BalanceError, Zatoshis},
20};
21#[cfg(feature = "transparent-key-import")]
22use zcash_script::script;
23use zip32::Scope;
24
25use crate::{TransferType, fees::sapling as sapling_fees};
26
27#[cfg(feature = "orchard")]
28use crate::fees::orchard as orchard_fees;
29
30#[cfg(feature = "transparent-inputs")]
31use {::transparent::keys::NonHardenedChildIndex, std::time::SystemTime};
32
33/// A unique identifier for a shielded transaction output
34#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
35pub struct NoteId {
36    txid: TxId,
37    protocol: ShieldedPool,
38    output_index: u16,
39}
40
41impl NoteId {
42    /// Constructs a new `NoteId` from its parts.
43    pub fn new(txid: TxId, protocol: ShieldedPool, output_index: u16) -> Self {
44        Self {
45            txid,
46            protocol,
47            output_index,
48        }
49    }
50
51    /// Returns the ID of the transaction containing this note.
52    pub fn txid(&self) -> &TxId {
53        &self.txid
54    }
55
56    /// Returns the shielded protocol used by this note.
57    pub fn protocol(&self) -> ShieldedPool {
58        self.protocol
59    }
60
61    /// Returns the index of this note within its transaction's corresponding list of
62    /// shielded outputs.
63    pub fn output_index(&self) -> u16 {
64        self.output_index
65    }
66}
67
68/// A reference to a transaction output received by the wallet, across all pools.
69#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
70pub struct OutputRef {
71    txid: TxId,
72    pool: PoolType,
73    output_index: u32,
74}
75
76impl OutputRef {
77    /// Constructs a new `OutputRef` from its parts.
78    pub fn new(txid: TxId, pool: PoolType, output_index: u32) -> Self {
79        Self {
80            txid,
81            pool,
82            output_index,
83        }
84    }
85
86    /// Returns the ID of the transaction containing this output.
87    pub fn txid(&self) -> &TxId {
88        &self.txid
89    }
90
91    /// Returns the pool type of this output.
92    pub fn pool(&self) -> PoolType {
93        self.pool
94    }
95
96    /// Returns the index of this output within its transaction.
97    pub fn output_index(&self) -> u32 {
98        self.output_index
99    }
100}
101
102impl From<NoteId> for OutputRef {
103    fn from(note_id: NoteId) -> Self {
104        Self {
105            txid: note_id.txid,
106            pool: PoolType::Shielded(note_id.protocol),
107            output_index: note_id.output_index.into(),
108        }
109    }
110}
111
112pub use crate::data_api::locking::LockOwner;
113
114/// A type that represents the recipient of a transaction output.
115///
116/// Variants vary along two independent axes:
117///
118/// * **Relationship to the wallet**: whether the recipient address is [`Self::External`] to
119///   the wallet, an [`Self::EphemeralTransparent`] address of a wallet account (used
120///   transiently as a middle hop), or otherwise internal to a wallet account (recorded as
121///   [`Self::InternalShielded`] or [`Self::InternalTransparent`], depending on payload
122///   domain).
123/// * **Payload domain**: whether the output is shielded (in which case what is recorded is
124///   the decrypted [`Note`], since the recipient address is not itself externally
125///   meaningful) or transparent (in which case what is recorded is the on-chain-observable
126///   recipient address, since transparent outputs carry no analogous decryptable payload).
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub enum Recipient<AccountId> {
129    /// An output sent to a recipient external to the wallet.
130    External {
131        recipient_address: ZcashAddress,
132        output_pool: PoolType,
133    },
134    /// A transparent output sent to an ephemeral address of a wallet account
135    /// (e.g. the middle hop of a ZIP 320 / TEX flow). The `outpoint` is
136    /// recorded so the wallet can later detect when this output is spent
137    /// without relying on a continuous address watch.
138    #[cfg(feature = "transparent-inputs")]
139    EphemeralTransparent {
140        receiving_account: AccountId,
141        ephemeral_address: TransparentAddress,
142        outpoint: OutPoint,
143    },
144    /// A transparent output sent to a non-ephemeral transparent address belonging to
145    /// a wallet account. Used to record the send side of a transparent output that
146    /// the wallet both funded and received.
147    ///
148    /// Distinct from [`Self::InternalShielded`] because for transparent outputs
149    /// the recipient address is observable on chain and must be recorded;
150    /// additionally, the receiving account may not be known at the point the
151    /// send is recorded. For shielded outputs the recipient address is not
152    /// externally meaningful, so wallet-internal sends are recorded against
153    /// the receiving account alone.
154    #[cfg(feature = "transparent-inputs")]
155    InternalTransparent {
156        receiving_account: AccountId,
157        recipient_address: TransparentAddress,
158    },
159    /// A shielded output recorded against a wallet account. Used for
160    /// same-account outputs such as change (`external_address` is `None`) and
161    /// for outputs received via an external IVK but funded by another wallet
162    /// account, in which case `external_address` is the address that was paid.
163    InternalShielded {
164        receiving_account: AccountId,
165        external_address: Option<ZcashAddress>,
166        note: Box<Note>,
167    },
168}
169
170/// The shielded subset of a [`Transaction`]'s data that is relevant to a particular wallet.
171///
172/// [`Transaction`]: zcash_primitives::transaction::Transaction
173#[derive(Clone)]
174pub struct WalletTx<AccountId> {
175    txid: TxId,
176    block_index: TxIndex,
177    transparent_spends: Vec<WalletTransparentSpend<AccountId>>,
178    transparent_outputs: Vec<WalletTransparentOutput<AccountId>>,
179    sapling_spends: Vec<WalletSaplingSpend<AccountId>>,
180    sapling_outputs: Vec<WalletSaplingOutput<AccountId>>,
181    #[cfg(feature = "orchard")]
182    orchard_spends: Vec<WalletOrchardSpend<AccountId>>,
183    #[cfg(feature = "orchard")]
184    orchard_outputs: Vec<WalletOrchardOutput<AccountId>>,
185    #[cfg(feature = "orchard")]
186    ironwood_spends: Vec<WalletIronwoodSpend<AccountId>>,
187    #[cfg(feature = "orchard")]
188    ironwood_outputs: Vec<WalletIronwoodOutput<AccountId>>,
189}
190
191impl<AccountId> WalletTx<AccountId> {
192    /// Constructs a new [`WalletTx`] from its constituent parts.
193    #[allow(clippy::too_many_arguments)]
194    pub fn new(
195        txid: TxId,
196        block_index: TxIndex,
197        transparent_spends: Vec<WalletTransparentSpend<AccountId>>,
198        transparent_outputs: Vec<WalletTransparentOutput<AccountId>>,
199        sapling_spends: Vec<WalletSaplingSpend<AccountId>>,
200        sapling_outputs: Vec<WalletSaplingOutput<AccountId>>,
201        #[cfg(feature = "orchard")] orchard_spends: Vec<
202            WalletSpend<orchard::note::Nullifier, AccountId>,
203        >,
204        #[cfg(feature = "orchard")] orchard_outputs: Vec<WalletOrchardOutput<AccountId>>,
205        #[cfg(feature = "orchard")] ironwood_spends: Vec<
206            WalletSpend<orchard::note::Nullifier, AccountId>,
207        >,
208        #[cfg(feature = "orchard")] ironwood_outputs: Vec<WalletIronwoodOutput<AccountId>>,
209    ) -> Self {
210        Self {
211            txid,
212            block_index,
213            transparent_spends,
214            transparent_outputs,
215            sapling_spends,
216            sapling_outputs,
217            #[cfg(feature = "orchard")]
218            orchard_spends,
219            #[cfg(feature = "orchard")]
220            orchard_outputs,
221            #[cfg(feature = "orchard")]
222            ironwood_spends,
223            #[cfg(feature = "orchard")]
224            ironwood_outputs,
225        }
226    }
227
228    /// Returns the [`TxId`] for the corresponding [`Transaction`].
229    ///
230    /// [`Transaction`]: zcash_primitives::transaction::Transaction
231    pub fn txid(&self) -> TxId {
232        self.txid
233    }
234
235    /// Returns the index of the transaction in the containing block.
236    pub fn block_index(&self) -> TxIndex {
237        self.block_index
238    }
239
240    /// Returns a record for each transparent output belonging to the wallet that was spent in
241    /// the transaction.
242    pub fn transparent_spends(&self) -> &[WalletTransparentSpend<AccountId>] {
243        &self.transparent_spends
244    }
245
246    /// Returns a record for each transparent coin received or produced by the wallet.
247    pub fn transparent_outputs(&self) -> &[WalletTransparentOutput<AccountId>] {
248        &self.transparent_outputs
249    }
250
251    /// Returns a record for each Sapling note belonging to the wallet that was spent in the
252    /// transaction.
253    pub fn sapling_spends(&self) -> &[WalletSaplingSpend<AccountId>] {
254        self.sapling_spends.as_ref()
255    }
256
257    /// Returns a record for each Sapling note received or produced by the wallet in the
258    /// transaction.
259    pub fn sapling_outputs(&self) -> &[WalletSaplingOutput<AccountId>] {
260        self.sapling_outputs.as_ref()
261    }
262
263    /// Returns a record for each Orchard note belonging to the wallet that was spent in the
264    /// transaction.
265    #[cfg(feature = "orchard")]
266    pub fn orchard_spends(&self) -> &[WalletOrchardSpend<AccountId>] {
267        self.orchard_spends.as_ref()
268    }
269
270    /// Returns a record for each Orchard note received or produced by the wallet in the
271    /// transaction.
272    #[cfg(feature = "orchard")]
273    pub fn orchard_outputs(&self) -> &[WalletOrchardOutput<AccountId>] {
274        self.orchard_outputs.as_ref()
275    }
276
277    /// Returns a record for each Ironwood note belonging to the wallet that was spent in the
278    /// transaction.
279    #[cfg(feature = "orchard")]
280    pub fn ironwood_spends(&self) -> &[WalletIronwoodSpend<AccountId>] {
281        self.ironwood_spends.as_ref()
282    }
283
284    /// Returns a record for each Ironwood note received or produced by the wallet in the
285    /// transaction.
286    #[cfg(feature = "orchard")]
287    pub fn ironwood_outputs(&self) -> &[WalletIronwoodOutput<AccountId>] {
288        self.ironwood_outputs.as_ref()
289    }
290}
291
292/// A transparent output controlled by the wallet.
293#[derive(Debug, Clone, PartialEq, Eq)]
294pub struct WalletTransparentOutput<AccountId> {
295    outpoint: OutPoint,
296    txout: TxOut,
297    mined_height: Option<BlockHeight>,
298    recipient_account: Option<AccountId>,
299    recipient_key_scope: Option<TransparentKeyScope>,
300    recipient_address: TransparentAddress,
301    funding_account: Option<AccountId>,
302    /// The known serialized input size for this output, if available.
303    /// This is set for P2SH outputs where the redeem script is known.
304    known_input_size: Option<usize>,
305}
306
307impl<AccountId> WalletTransparentOutput<AccountId> {
308    /// Constructs a new [`WalletTransparentOutput`] from its constituent parts.
309    ///
310    /// Returns `None` if the recipient address for the provided [`TxOut`] cannot be
311    /// determined based on the set of output script patterns understood by this wallet.
312    pub fn from_parts(
313        outpoint: OutPoint,
314        txout: TxOut,
315        mined_height: Option<BlockHeight>,
316        recipient_account: Option<AccountId>,
317        recipient_key_scope: Option<TransparentKeyScope>,
318        funding_account: Option<AccountId>,
319    ) -> Option<Self> {
320        txout
321            .recipient_address()
322            .map(|recipient_address| WalletTransparentOutput {
323                outpoint,
324                txout,
325                mined_height,
326                recipient_account,
327                recipient_key_scope,
328                recipient_address,
329                funding_account,
330                known_input_size: None,
331            })
332    }
333
334    /// Returns a copy of this output with account-identifying data redacted,
335    /// for inclusion in a [`Proposal`].
336    ///
337    /// Specifically:
338    /// - The `AccountId` type parameter is replaced with `()`, erasing the value
339    ///   of `recipient_account` while preserving whether the output is
340    ///   wallet-owned (the `Some` / `None` distinction is retained).
341    /// - `funding_account` is cleared to `None`, since a proposal does not
342    ///   carry information about which account funded prior outputs.
343    ///
344    /// Used when constructing or reconstructing a [`Proposal`], whose
345    /// transparent inputs are deliberately account-agnostic so that proposals
346    /// can be wire-encoded and shared without revealing wallet account
347    /// structure.
348    ///
349    /// [`Proposal`]: crate::proposal::Proposal
350    #[cfg(feature = "transparent-inputs")]
351    pub(crate) fn redact_account_data(self) -> WalletTransparentOutput<()> {
352        WalletTransparentOutput {
353            outpoint: self.outpoint,
354            txout: self.txout,
355            mined_height: self.mined_height,
356            recipient_account: self.recipient_account.map(|_| ()),
357            recipient_key_scope: self.recipient_key_scope,
358            recipient_address: self.recipient_address,
359            funding_account: None,
360            known_input_size: self.known_input_size,
361        }
362    }
363
364    /// Sets the known serialized input size for this output.
365    ///
366    /// This should be used for P2SH outputs where the wallet knows the redeem script
367    /// and can compute the expected input size for fee calculation.
368    pub fn with_known_input_size(mut self, size: usize) -> Self {
369        self.known_input_size = Some(size);
370        self
371    }
372
373    /// Returns the [`OutPoint`] corresponding to the output.
374    pub fn outpoint(&self) -> &OutPoint {
375        &self.outpoint
376    }
377
378    /// The index of the output in the transaction that created this output.
379    pub fn index(&self) -> usize {
380        self.outpoint.n() as usize
381    }
382
383    /// Returns the transaction output itself.
384    pub fn txout(&self) -> &TxOut {
385        &self.txout
386    }
387
388    /// Returns the height at which the UTXO was mined, if any.
389    pub fn mined_height(&self) -> Option<BlockHeight> {
390        self.mined_height
391    }
392
393    /// Returns the transparent key scope at which this address was derived, if known.
394    ///
395    /// This metadata MUST be returned for any transparent address derived by the wallet;
396    /// this metadata is used by `propose_shielding` to ensure that shielding transactions
397    /// do not inadvertently link ephemeral addresses to other wallet activity on-chain.
398    pub fn recipient_key_scope(&self) -> Option<TransparentKeyScope> {
399        self.recipient_key_scope
400    }
401
402    /// Returns the [`TransferType`] for this output, derived from the recipient,
403    /// recipient-key-scope, and funding-account information stored on the output:
404    ///
405    /// - [`TransferType::Outgoing`] when [`recipient_account`](Self::recipient_account)
406    ///   is `None` (the recipient is external to the wallet).
407    /// - [`TransferType::AccountInternal`] when the recipient is a wallet account and
408    ///   the output is a same-account self-transfer. This is detected either
409    ///   structurally, when [`recipient_key_scope`](Self::recipient_key_scope) is
410    ///   `INTERNAL` or `EPHEMERAL` (those key scopes exist only within a single
411    ///   account), or by observation, when the recipient account is also the
412    ///   [`funding_account`](Self::funding_account). The latter case also covers
413    ///   standalone addresses, which have no key scope.
414    /// - [`TransferType::WalletInternal`] when the recipient is a wallet account and
415    ///   the [`funding_account`](Self::funding_account) is a different wallet account
416    ///   (a cross-account transfer within the wallet).
417    /// - [`TransferType::Incoming`] when the recipient is a wallet account and no
418    ///   wallet funding account is known.
419    pub fn transfer_type(&self) -> TransferType
420    where
421        AccountId: PartialEq,
422    {
423        match (
424            self.recipient_account.as_ref(),
425            self.recipient_key_scope,
426            self.funding_account.as_ref(),
427        ) {
428            (None, _, _) => TransferType::Outgoing,
429            (Some(_), Some(TransparentKeyScope::INTERNAL | TransparentKeyScope::EPHEMERAL), _) => {
430                TransferType::AccountInternal
431            }
432            (Some(r), _, Some(r0)) if r == r0 => TransferType::AccountInternal,
433            (Some(_), _, Some(_)) => TransferType::WalletInternal,
434            (Some(_), _, _) => TransferType::Incoming,
435        }
436    }
437
438    /// The identifier for the account that received this output, if known to belong to the
439    /// wallet. Returns `None` for outputs sent to addresses outside the wallet.
440    pub fn recipient_account(&self) -> Option<&AccountId> {
441        self.recipient_account.as_ref()
442    }
443
444    /// Returns the wallet address that received the UTXO.
445    pub fn recipient_address(&self) -> &TransparentAddress {
446        &self.recipient_address
447    }
448
449    /// The identifier for the wallet account that provided funds in the transaction
450    /// that created the output, if known.
451    ///
452    /// Note: the Zcash protocol permits construction of transactions where multiple distinct
453    /// accounts provide funds; however, `zcash_client_backend` does not currently support the
454    /// construction of transactions of this form. In cases where multiple funding accounts are
455    /// detected, the account that provided the most significant source of funds should be selected
456    /// if possible; in the future, this should be either expanded to support a set of funding
457    /// accounts (which will require potentially invasive storage backend changes).
458    pub fn funding_account(&self) -> Option<&AccountId> {
459        self.funding_account.as_ref()
460    }
461
462    /// Returns the value of the UTXO
463    pub fn value(&self) -> Zatoshis {
464        self.txout.value()
465    }
466}
467
468impl<AccountId: Debug> transparent_fees::InputView for WalletTransparentOutput<AccountId> {
469    fn outpoint(&self) -> &OutPoint {
470        &self.outpoint
471    }
472    fn coin(&self) -> &TxOut {
473        &self.txout
474    }
475    fn serialized_size(&self) -> transparent_fees::InputSize {
476        match self.known_input_size {
477            Some(size) => transparent_fees::InputSize::Known(size),
478            None => {
479                match zcash_script::script::PubKey::parse(&self.txout.script_pubkey().0)
480                    .ok()
481                    .as_ref()
482                    .and_then(zcash_script::solver::standard)
483                {
484                    Some(zcash_script::solver::ScriptKind::PubKeyHash { .. }) => {
485                        transparent_fees::InputSize::STANDARD_P2PKH
486                    }
487                    Some(zcash_script::solver::ScriptKind::ScriptHash { .. }) => {
488                        // P2SH input size depends on the redeem script, which the
489                        // wallet must set via `with_known_input_size` (the SQLite
490                        // backend does this by reading the redeem script from the DB
491                        // and computing the exact size via
492                        // `p2sh_input_serialized_len`). Without it we cannot
493                        // determine the exact input size; return `Unknown` so the
494                        // estimator falls back to the consensus-maximum input size
495                        // rather than guessing.
496                        transparent_fees::InputSize::Unknown(self.outpoint.clone())
497                    }
498                    _ => transparent_fees::InputSize::Unknown(self.outpoint.clone()),
499                }
500            }
501        }
502    }
503}
504
505/// A reference to a spent note belonging to the wallet within a transaction.
506#[derive(Clone)]
507pub struct WalletSpend<Nf, AccountId> {
508    index: usize,
509    nf: Nf,
510    account_id: AccountId,
511}
512
513impl<Nf, AccountId> WalletSpend<Nf, AccountId> {
514    /// Constructs a `WalletSpend` from its constituent parts.
515    pub fn from_parts(index: usize, nf: Nf, account_id: AccountId) -> Self {
516        Self {
517            index,
518            nf,
519            account_id,
520        }
521    }
522
523    /// Returns the index of the Sapling spend or Orchard action within the transaction that
524    /// created this spend.
525    pub fn index(&self) -> usize {
526        self.index
527    }
528    /// Returns the nullifier of the spent note.
529    pub fn nf(&self) -> &Nf {
530        &self.nf
531    }
532    /// Returns the identifier to the account_id to which the note belonged.
533    pub fn account_id(&self) -> &AccountId {
534        &self.account_id
535    }
536}
537
538/// A type alias for Sapling [`WalletSpend`]s.
539pub type WalletSaplingSpend<AccountId> = WalletSpend<sapling::Nullifier, AccountId>;
540
541/// A type alias for Orchard [`WalletSpend`]s.
542#[cfg(feature = "orchard")]
543pub type WalletOrchardSpend<AccountId> = WalletSpend<orchard::note::Nullifier, AccountId>;
544
545/// A type alias for Ironwood [`WalletSpend`]s.
546///
547/// Ironwood notes are Orchard-shaped and therefore share the Orchard nullifier type, but Ironwood
548/// is a distinct pool from Orchard.
549#[cfg(feature = "orchard")]
550pub type WalletIronwoodSpend<AccountId> = WalletSpend<orchard::note::Nullifier, AccountId>;
551
552/// A reference to a transparent output belonging to the wallet that is spent within a
553/// transaction.
554///
555/// This is the transparent counterpart of [`WalletSpend`]. A transparent output is identified by
556/// the [`OutPoint`] that names it rather than by a nullifier, so unlike a shielded spend it can be
557/// recognized only by a wallet that already knows of the output being spent.
558#[derive(Clone, Debug)]
559pub struct WalletTransparentSpend<AccountId> {
560    index: usize,
561    outpoint: OutPoint,
562    account_id: AccountId,
563}
564
565impl<AccountId> WalletTransparentSpend<AccountId> {
566    /// Constructs a `WalletTransparentSpend` from its constituent parts.
567    pub fn from_parts(index: usize, outpoint: OutPoint, account_id: AccountId) -> Self {
568        Self {
569            index,
570            outpoint,
571            account_id,
572        }
573    }
574
575    /// Returns the index of the transparent input within the spending transaction.
576    pub fn index(&self) -> usize {
577        self.index
578    }
579
580    /// Returns the outpoint of the output being spent.
581    pub fn outpoint(&self) -> &OutPoint {
582        &self.outpoint
583    }
584
585    /// Returns the identifier for the account to which the spent output belonged.
586    pub fn account_id(&self) -> &AccountId {
587        &self.account_id
588    }
589}
590
591/// An output that was successfully decrypted in the process of wallet scanning.
592#[derive(Clone)]
593pub struct WalletOutput<Note, Nullifier, AccountId> {
594    index: usize,
595    ephemeral_key: EphemeralKeyBytes,
596    note: Note,
597    is_change: bool,
598    note_commitment_tree_position: Position,
599    nf: Option<Nullifier>,
600    account_id: AccountId,
601    recipient_key_scope: Option<zip32::Scope>,
602}
603
604impl<Note, Nullifier, AccountId> WalletOutput<Note, Nullifier, AccountId> {
605    /// Constructs a new `WalletOutput` value from its constituent parts.
606    #[allow(clippy::too_many_arguments)]
607    pub fn from_parts(
608        index: usize,
609        ephemeral_key: EphemeralKeyBytes,
610        note: Note,
611        is_change: bool,
612        note_commitment_tree_position: Position,
613        nf: Option<Nullifier>,
614        account_id: AccountId,
615        recipient_key_scope: Option<zip32::Scope>,
616    ) -> Self {
617        Self {
618            index,
619            ephemeral_key,
620            note,
621            is_change,
622            note_commitment_tree_position,
623            nf,
624            account_id,
625            recipient_key_scope,
626        }
627    }
628
629    /// The index of the output or action in the transaction that created this output.
630    pub fn index(&self) -> usize {
631        self.index
632    }
633    /// The [`EphemeralKeyBytes`] used in the decryption of the note.
634    pub fn ephemeral_key(&self) -> &EphemeralKeyBytes {
635        &self.ephemeral_key
636    }
637    /// The note.
638    pub fn note(&self) -> &Note {
639        &self.note
640    }
641    /// A flag indicating whether the process of note decryption determined that this
642    /// output should be classified as change.
643    pub fn is_change(&self) -> bool {
644        self.is_change
645    }
646    /// The position of the note in the global note commitment tree.
647    pub fn note_commitment_tree_position(&self) -> Position {
648        self.note_commitment_tree_position
649    }
650    /// The nullifier for the note, if the key used to decrypt the note was able to compute it.
651    pub fn nf(&self) -> Option<&Nullifier> {
652        self.nf.as_ref()
653    }
654    /// The identifier for the account to which the output belongs.
655    pub fn account_id(&self) -> &AccountId {
656        &self.account_id
657    }
658    /// The ZIP 32 scope for which the viewing key that decrypted this output was derived, if
659    /// known.
660    pub fn recipient_key_scope(&self) -> Option<zip32::Scope> {
661        self.recipient_key_scope
662    }
663}
664
665/// A subset of an [`OutputDescription`] relevant to wallets and light clients.
666///
667/// [`OutputDescription`]: sapling::bundle::OutputDescription
668pub type WalletSaplingOutput<AccountId> =
669    WalletOutput<sapling::Note, sapling::Nullifier, AccountId>;
670
671/// The output part of an Orchard [`Action`] that was decrypted in the process of scanning.
672///
673/// [`Action`]: orchard::Action
674#[cfg(feature = "orchard")]
675pub type WalletOrchardOutput<AccountId> =
676    WalletOutput<(orchard::note::Note, orchard::ValuePool), orchard::note::Nullifier, AccountId>;
677
678/// The output part of an Ironwood [`Action`] that was decrypted in the process of scanning.
679///
680/// [`Action`]: orchard::Action
681#[cfg(feature = "orchard")]
682pub type WalletIronwoodOutput<AccountId> =
683    WalletOutput<(orchard::note::Note, orchard::ValuePool), orchard::note::Nullifier, AccountId>;
684
685/// An enumeration of supported shielded note types for use in [`ReceivedNote`]
686#[derive(Debug, Clone, PartialEq, Eq)]
687pub enum Note {
688    Sapling(sapling::Note),
689    #[cfg(feature = "orchard")]
690    Orchard {
691        note: orchard::Note,
692        pool: orchard::ValuePool,
693    },
694}
695
696impl Note {
697    /// Returns the receiver of this note.
698    pub fn receiver(&self) -> Receiver {
699        match self {
700            Note::Sapling(n) => Receiver::Sapling(n.recipient()),
701            #[cfg(feature = "orchard")]
702            Note::Orchard { note, .. } => Receiver::Orchard(note.recipient()),
703        }
704    }
705
706    pub fn value(&self) -> Zatoshis {
707        match self {
708            Note::Sapling(n) => n.value().inner().try_into().expect(
709                "Sapling notes must have values in the range of valid non-negative ZEC values.",
710            ),
711            #[cfg(feature = "orchard")]
712            Note::Orchard { note, .. } => Zatoshis::from_u64(note.value().inner()).expect(
713                "Orchard notes must have values in the range of valid non-negative ZEC values.",
714            ),
715        }
716    }
717
718    /// Returns the shielded value pool to which this note belongs.
719    pub fn pool(&self) -> ShieldedPool {
720        match self {
721            Note::Sapling(_) => ShieldedPool::Sapling,
722            #[cfg(feature = "orchard")]
723            Note::Orchard { pool, .. } => shielded_pool_for_value_pool(*pool),
724        }
725    }
726}
727
728/// Returns the shielded pool corresponding to an Orchard-protocol value pool. The Orchard protocol
729/// serves both the Orchard pool (version-2 notes) and the Ironwood pool (version-3 notes); this is
730/// the single point at which that classification is made.
731#[cfg(feature = "orchard")]
732pub(crate) fn shielded_pool_for_value_pool(pool: orchard::ValuePool) -> ShieldedPool {
733    match pool {
734        orchard::ValuePool::Orchard => ShieldedPool::Orchard,
735        orchard::ValuePool::Ironwood => ShieldedPool::Ironwood,
736    }
737}
738
739/// A note that was received by the wallet, along with contextual information about the output that
740/// generated the note and the key that is required to spend it.
741#[derive(Clone, PartialEq, Eq)]
742pub struct ReceivedNote<NoteRef, NoteT> {
743    note_id: NoteRef,
744    txid: TxId,
745    output_index: u16,
746    note: NoteT,
747    spending_key_scope: Scope,
748    note_commitment_tree_position: Position,
749    mined_height: Option<BlockHeight>,
750    max_shielding_input_height: Option<BlockHeight>,
751}
752
753impl<NoteRef, NoteT> ReceivedNote<NoteRef, NoteT> {
754    /// Constructs a new [`ReceivedNote`] from its constituent parts.
755    #[allow(clippy::too_many_arguments)]
756    pub fn from_parts(
757        note_id: NoteRef,
758        txid: TxId,
759        output_index: u16,
760        note: NoteT,
761        spending_key_scope: Scope,
762        note_commitment_tree_position: Position,
763        mined_height: Option<BlockHeight>,
764        max_shielding_input_height: Option<BlockHeight>,
765    ) -> Self {
766        ReceivedNote {
767            note_id,
768            txid,
769            output_index,
770            note,
771            spending_key_scope,
772            note_commitment_tree_position,
773            mined_height,
774            max_shielding_input_height,
775        }
776    }
777
778    /// Returns the storage backend's internal identifier for the note.
779    pub fn internal_note_id(&self) -> &NoteRef {
780        &self.note_id
781    }
782    /// Returns the txid of the transaction that constructed the note.
783    pub fn txid(&self) -> &TxId {
784        &self.txid
785    }
786    /// Returns the output index of the note within the transaction, according to the note's
787    /// shielded protocol.
788    pub fn output_index(&self) -> u16 {
789        self.output_index
790    }
791    /// Returns the note data.
792    pub fn note(&self) -> &NoteT {
793        &self.note
794    }
795    /// Returns the [`Scope`] of the spending key required to make spend authorizing signatures for
796    /// the note.
797    pub fn spending_key_scope(&self) -> Scope {
798        self.spending_key_scope
799    }
800    /// Returns the position of the note in the note commitment tree.
801    pub fn note_commitment_tree_position(&self) -> Position {
802        self.note_commitment_tree_position
803    }
804    /// Returns the block height at which the transaction that produced the note was mined.
805    pub fn mined_height(&self) -> Option<BlockHeight> {
806        self.mined_height
807    }
808    /// Returns the maximum block height among those at which transparent inputs to the transaction
809    /// that produced the note were created, considering only transparent inputs that belong to the
810    /// same wallet account as the note. This height is used in determining the effective number of
811    /// confirmations for externally-received value. See [`ZIP 315`] for additional information.
812    ///
813    /// [`ZIP 315`]: https://zips.z.cash/zip-0315
814    pub fn max_shielding_input_height(&self) -> Option<BlockHeight> {
815        self.max_shielding_input_height
816    }
817
818    /// Map over the `note` field of this data structure.
819    ///
820    /// Consume this value, applying the provided function to the value of its `note` field and
821    /// returning a new `ReceivedNote` with the result as its `note` field value.
822    pub fn map_note<N, F: Fn(NoteT) -> N>(self, f: F) -> ReceivedNote<NoteRef, N> {
823        ReceivedNote {
824            note_id: self.note_id,
825            txid: self.txid,
826            output_index: self.output_index,
827            note: f(self.note),
828            spending_key_scope: self.spending_key_scope,
829            note_commitment_tree_position: self.note_commitment_tree_position,
830            mined_height: self.mined_height,
831            max_shielding_input_height: self.max_shielding_input_height,
832        }
833    }
834}
835
836impl<NoteRef: Debug> Debug for ReceivedNote<NoteRef, sapling::Note> {
837    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
838        f.debug_struct("ReceivedNote")
839            .field("note_id", &self.note_id)
840            .field("txid", &self.txid)
841            .field("output_index", &self.output_index)
842            .field("note_value", &self.note_value())
843            .field("spending_key_scope", &self.spending_key_scope)
844            .field(
845                "note_commitment_tree_position",
846                &self.note_commitment_tree_position,
847            )
848            .field("mined_height", &self.mined_height)
849            .finish()
850    }
851}
852
853#[cfg(feature = "orchard")]
854impl<NoteRef: Debug> Debug for ReceivedNote<NoteRef, orchard::note::Note> {
855    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
856        f.debug_struct("ReceivedNote")
857            .field("note_id", &self.note_id)
858            .field("txid", &self.txid)
859            .field("output_index", &self.output_index)
860            .field("note_value", &self.note_value())
861            .field("spending_key_scope", &self.spending_key_scope)
862            .field(
863                "note_commitment_tree_position",
864                &self.note_commitment_tree_position,
865            )
866            .field("mined_height", &self.mined_height)
867            .finish()
868    }
869}
870
871impl<NoteRef> ReceivedNote<NoteRef, sapling::Note> {
872    pub fn note_value(&self) -> Result<Zatoshis, BalanceError> {
873        self.note.value().inner().try_into()
874    }
875}
876
877#[cfg(feature = "orchard")]
878impl<NoteRef> ReceivedNote<NoteRef, orchard::note::Note> {
879    pub fn note_value(&self) -> Result<Zatoshis, BalanceError> {
880        self.note.value().inner().try_into()
881    }
882}
883
884impl<NoteRef> sapling_fees::InputView<NoteRef> for (NoteRef, sapling::value::NoteValue) {
885    fn note_id(&self) -> &NoteRef {
886        &self.0
887    }
888
889    fn value(&self) -> Zatoshis {
890        self.1
891            .inner()
892            .try_into()
893            .expect("Sapling note values are indirectly checked by consensus.")
894    }
895}
896
897impl<NoteRef> sapling_fees::InputView<NoteRef> for ReceivedNote<NoteRef, sapling::Note> {
898    fn note_id(&self) -> &NoteRef {
899        &self.note_id
900    }
901
902    fn value(&self) -> Zatoshis {
903        self.note
904            .value()
905            .inner()
906            .try_into()
907            .expect("Sapling note values are indirectly checked by consensus.")
908    }
909}
910
911#[cfg(feature = "orchard")]
912impl<NoteRef> orchard_fees::InputView<NoteRef> for (NoteRef, orchard::value::NoteValue) {
913    fn note_id(&self) -> &NoteRef {
914        &self.0
915    }
916
917    fn value(&self) -> Zatoshis {
918        self.1
919            .inner()
920            .try_into()
921            .expect("Orchard note values are indirectly checked by consensus.")
922    }
923}
924
925#[cfg(feature = "orchard")]
926impl<NoteRef> orchard_fees::InputView<NoteRef> for ReceivedNote<NoteRef, orchard::Note> {
927    fn note_id(&self) -> &NoteRef {
928        &self.note_id
929    }
930
931    fn value(&self) -> Zatoshis {
932        self.note
933            .value()
934            .inner()
935            .try_into()
936            .expect("Orchard note values are indirectly checked by consensus.")
937    }
938}
939
940/// Describes a policy for which outgoing viewing key should be able to decrypt
941/// transaction outputs.
942///
943/// For details on what transaction information is visible to the holder of an outgoing
944/// viewing key, refer to [ZIP 310].
945///
946/// [ZIP 310]: https://zips.z.cash/zip-0310
947#[derive(Debug, Clone)]
948pub enum OvkPolicy {
949    /// Use an outgoing viewing key produced from the sender's [`UnifiedFullViewingKey`],
950    /// selected via the policy documented in [`UnifiedFullViewingKey::select_ovk`].
951    ///
952    /// External transaction outputs will be decryptable by the sender, in addition to the
953    /// recipients. Wallet-internal transaction outputs will be decryptable only with the wallet's
954    /// internal-scoped incoming viewing key.
955    ///
956    /// [`UnifiedFullViewingKey`]: zcash_keys::keys::UnifiedFullViewingKey
957    /// [`UnifiedFullViewingKey::select_ovk`]: zcash_keys::keys::UnifiedFullViewingKey::select_ovk
958    Sender,
959
960    /// Use custom outgoing viewing keys. These might for instance be derived from a
961    /// different seed than the wallet's spending keys.
962    ///
963    /// Transaction outputs will be decryptable by the recipients, and whoever controls
964    /// the provided outgoing viewing keys.
965    Custom {
966        external_ovk: OutgoingViewingKey,
967        internal_ovk: Option<OutgoingViewingKey>,
968    },
969    /// Use no outgoing viewing keys. Transaction outputs will be decryptable by their
970    /// recipients, but not by the sender.
971    Discard,
972}
973
974impl OvkPolicy {
975    /// Constructs an [`OvkPolicy::Custom`] value from a single arbitrary 32-byte key with both the
976    /// external_ovk and internal_ovk components set to the same key.
977    ///
978    /// Outputs of transactions created with this OVK policy will be recoverable using this key
979    /// irrespective of whether they are external outputs or wallet-internal change outputs.
980    pub fn custom_from_common_bytes(key: &[u8; 32]) -> Self {
981        let k = OutgoingViewingKey::from(*key);
982        OvkPolicy::Custom {
983            external_ovk: k,
984            internal_ovk: Some(k),
985        }
986    }
987}
988
989/// Metadata describing the gap limit position of a transparent address.
990#[derive(Debug, Clone, Copy, PartialEq, Eq)]
991#[cfg(feature = "transparent-inputs")]
992pub enum GapMetadata {
993    /// The address, or an address at a greater child index, has received transparent funds and
994    /// will be discovered by wallet recovery by exploration over the space of
995    /// [`NonHardenedChildIndex`]es using the provided gap limit.
996    GapRecoverable { gap_limit: u32 },
997    /// The address exists within an address gap of the given limit size, and will be discovered by
998    /// wallet recovery by exploration using the provided gap limit. In the view of the wallet, no
999    /// addresses at the given position or greater (up to the gap limit) have received funds. The
1000    /// number of addresses remaining within the gap limit before no additional addresses can be
1001    /// allocated is given by `gap_limit - (gap_position + 1)`.
1002    InGap {
1003        /// A zero-based index over the child indices in the gap.
1004        gap_position: u32,
1005        /// The maximum number of sequential child indices that can be allocated to addresses
1006        /// without any of those addresses having received funds.
1007        gap_limit: u32,
1008    },
1009    /// The wallet does not contain derivation information for the associated address, and so its
1010    /// relationship to other addresses in the wallet cannot be determined.
1011    DerivationUnknown,
1012}
1013
1014/// Metadata describing whether and when a transparent address was exposed by the wallet.
1015#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1016#[cfg(feature = "transparent-inputs")]
1017pub enum Exposure {
1018    /// The address has been exposed by the wallet.
1019    Exposed {
1020        /// The address was first exposed to the wider ecosystem at this height, to the best
1021        /// of our knowledge.
1022        ///
1023        /// - For user-generated addresses, this is the chain tip height at the time that the
1024        ///   address was generated by an explicit request by the user or reserved for use in
1025        ///   a ZIP 320 transaction. These heights are not recoverable from chain.
1026        /// - In the case of an address with its first use discovered in a transaction
1027        ///   obtained by scanning the chain, this will be set to the mined height of that
1028        ///   transaction. In recover from seed cases, this is what user-generated addresses
1029        ///   will be assigned.
1030        at_height: BlockHeight,
1031        /// Transparent address gap metadata, as of the time the query that produced this exposure
1032        /// metadata was executed.
1033        gap_metadata: GapMetadata,
1034    },
1035    /// The address is not known to have been exposed to an external caller by the wallet.
1036    ///
1037    /// The wallet makes its determination based on observed chain data and inference from
1038    /// standard wallet address generation patterns. In particular, this is the state that
1039    /// an address is in when it has been generated by the advancement of the transparent
1040    /// address gap. This judgement may be incorrect for restored wallets.
1041    Unknown,
1042    /// It is not possible for the wallet to determine whether the address has been exposed,
1043    /// given the information the wallet has access to.
1044    CannotKnow,
1045}
1046
1047/// Information about a transparent address controlled by the wallet.
1048#[derive(Debug, Clone, PartialEq, Eq)]
1049#[cfg(feature = "transparent-inputs")]
1050pub struct TransparentAddressMetadata {
1051    source: TransparentAddressSource,
1052    exposure: Exposure,
1053    next_check_time: Option<SystemTime>,
1054}
1055
1056#[cfg(feature = "transparent-inputs")]
1057impl TransparentAddressMetadata {
1058    /// Constructs a new [`TransparentAddressMetadata`] value from its constituent parts.
1059    pub fn new(
1060        source: TransparentAddressSource,
1061        exposure: Exposure,
1062        next_check_time: Option<SystemTime>,
1063    ) -> Self {
1064        Self {
1065            source,
1066            exposure,
1067            next_check_time,
1068        }
1069    }
1070
1071    /// Returns a [`TransparentAddressMetadata`] with [`TransparentAddressSource::Derived`] source
1072    /// information and the specified exposure height.
1073    pub fn derived(
1074        scope: TransparentKeyScope,
1075        address_index: NonHardenedChildIndex,
1076        exposure: Exposure,
1077        next_check_time: Option<SystemTime>,
1078    ) -> Self {
1079        Self {
1080            source: TransparentAddressSource::Derived {
1081                scope,
1082                address_index,
1083            },
1084            exposure,
1085            next_check_time,
1086        }
1087    }
1088
1089    /// Returns a [`TransparentAddressMetadata`] with [`TransparentAddressSource::StandalonePubkey`]
1090    /// source information for a P2PKH address and the specified exposure height.
1091    #[cfg(feature = "transparent-key-import")]
1092    pub fn standalone_p2pkh(
1093        pubkey: secp256k1::PublicKey,
1094        exposure: Exposure,
1095        next_check_time: Option<SystemTime>,
1096    ) -> Self {
1097        Self {
1098            source: TransparentAddressSource::StandalonePubkey(pubkey),
1099            exposure,
1100            next_check_time,
1101        }
1102    }
1103
1104    /// Returns a [`TransparentAddressMetadata`] with [`TransparentAddressSource::StandaloneScript`]
1105    /// source information for a P2SH address and the specified exposure height.
1106    #[cfg(feature = "transparent-key-import")]
1107    pub fn standalone_script(
1108        redeem_script: script::Redeem,
1109        exposure: Exposure,
1110        next_check_time: Option<SystemTime>,
1111    ) -> Self {
1112        Self {
1113            source: TransparentAddressSource::StandaloneScript(redeem_script),
1114            exposure,
1115            next_check_time,
1116        }
1117    }
1118
1119    /// Returns a [`TransparentAddressMetadata`] with [`TransparentAddressSource::StandaloneAddress`]
1120    /// source information for an imported bare transparent address and the specified exposure
1121    /// height.
1122    #[cfg(feature = "transparent-key-import")]
1123    pub fn standalone_address(exposure: Exposure, next_check_time: Option<SystemTime>) -> Self {
1124        Self {
1125            source: TransparentAddressSource::StandaloneAddress,
1126            exposure,
1127            next_check_time,
1128        }
1129    }
1130
1131    /// Returns the source metadata for the address.
1132    pub fn source(&self) -> &TransparentAddressSource {
1133        &self.source
1134    }
1135
1136    /// Returns the exposure metadata for this transparent address.
1137    pub fn exposure(&self) -> Exposure {
1138        self.exposure
1139    }
1140
1141    /// Returns a copy of this metadata, with its exposure metadata updated
1142    pub fn with_exposure_at(
1143        &self,
1144        exposure_height: BlockHeight,
1145        gap_metadata: GapMetadata,
1146    ) -> Self {
1147        Self {
1148            source: self.source.clone(),
1149            exposure: Exposure::Exposed {
1150                at_height: exposure_height,
1151                gap_metadata,
1152            },
1153            next_check_time: self.next_check_time,
1154        }
1155    }
1156
1157    /// Returns the timestamp of the earliest time that the light wallet server may be queried for
1158    /// UTXOs associated with this address, or `None` if the wallet backend is not placing any
1159    /// restrictions on when this address can be queried. Unless the wallet application is
1160    /// requesting address information from a light wallet server that is trusted for privacy,
1161    /// only one such query should be performed at a time, to avoid linking multiple transparent
1162    /// addresses as belonging to the same wallet in the view of the light wallet server.
1163    pub fn next_check_time(&self) -> Option<SystemTime> {
1164        self.next_check_time
1165    }
1166
1167    /// Returns the [`TransparentKeyScope`] of the private key from which the address was derived,
1168    /// if known. Returns `None` for standalone addresses in the wallet.
1169    pub fn scope(&self) -> Option<TransparentKeyScope> {
1170        self.source.scope()
1171    }
1172
1173    /// Returns the BIP 44 [`NonHardenedChildIndex`] at which the address was derived, if known.
1174    /// Returns `None` for standalone addresses in the wallet.
1175    pub fn address_index(&self) -> Option<NonHardenedChildIndex> {
1176        self.source.address_index()
1177    }
1178
1179    /// Returns the redeem script for the address, if this is a P2SH address.
1180    /// Returns `None` for non-P2SH addresses.
1181    #[cfg(feature = "transparent-key-import")]
1182    pub fn redeem_script(&self) -> Option<&script::Redeem> {
1183        self.source.redeem_script()
1184    }
1185}
1186
1187/// Source information for a transparent address.
1188#[derive(Debug, Clone, PartialEq, Eq)]
1189#[cfg(feature = "transparent-inputs")]
1190pub enum TransparentAddressSource {
1191    /// BIP 44 path derivation information for the address below account pubkey level, i.e. the
1192    /// `change` and `index` elements of the path.
1193    Derived {
1194        scope: TransparentKeyScope,
1195        address_index: NonHardenedChildIndex,
1196    },
1197
1198    /// The address was derived from a secp256k1 public key for which derivation information is
1199    /// unknown or for which the associated spending key was produced from system randomness.
1200    /// This variant provides the public key directly.
1201    #[cfg(feature = "transparent-key-import")]
1202    StandalonePubkey(secp256k1::PublicKey),
1203
1204    /// The address was derived from a P2SH redeem_script for which derivation information is
1205    /// unknown.
1206    /// This variant provides the redeem script directly.
1207    #[cfg(feature = "transparent-key-import")]
1208    StandaloneScript(script::Redeem),
1209
1210    /// The address was imported as a bare transparent address, without any associated key
1211    /// material. The wallet watches for outputs received by the address, but holds neither
1212    /// the public key (P2PKH) nor the redeem script (P2SH) from which it was derived, so
1213    /// funds received by it cannot be spent unless that material is subsequently imported.
1214    #[cfg(feature = "transparent-key-import")]
1215    StandaloneAddress,
1216}
1217
1218#[cfg(feature = "transparent-inputs")]
1219impl TransparentAddressSource {
1220    /// Returns the [`TransparentKeyScope`] of the private key from which the address was derived,
1221    /// if known. Returns `None` for standalone addresses in the wallet.
1222    pub fn scope(&self) -> Option<TransparentKeyScope> {
1223        match self {
1224            TransparentAddressSource::Derived { scope, .. } => Some(*scope),
1225            #[cfg(feature = "transparent-key-import")]
1226            TransparentAddressSource::StandalonePubkey(_) => None,
1227            #[cfg(feature = "transparent-key-import")]
1228            TransparentAddressSource::StandaloneScript(_) => None,
1229            #[cfg(feature = "transparent-key-import")]
1230            TransparentAddressSource::StandaloneAddress => None,
1231        }
1232    }
1233
1234    /// Returns the BIP 44 [`NonHardenedChildIndex`] at which the address was derived, if known.
1235    /// Returns `None` for standalone addresses in the wallet.
1236    pub fn address_index(&self) -> Option<NonHardenedChildIndex> {
1237        match self {
1238            TransparentAddressSource::Derived { address_index, .. } => Some(*address_index),
1239            #[cfg(feature = "transparent-key-import")]
1240            TransparentAddressSource::StandalonePubkey(_) => None,
1241            #[cfg(feature = "transparent-key-import")]
1242            TransparentAddressSource::StandaloneScript(_) => None,
1243            #[cfg(feature = "transparent-key-import")]
1244            TransparentAddressSource::StandaloneAddress => None,
1245        }
1246    }
1247
1248    /// Returns the redeem script for the address, if this is a P2SH address.
1249    /// Returns `None` for non-P2SH addresses.
1250    #[cfg(feature = "transparent-key-import")]
1251    pub fn redeem_script(&self) -> Option<&script::Redeem> {
1252        match self {
1253            TransparentAddressSource::Derived { .. } => None,
1254            #[cfg(feature = "transparent-key-import")]
1255            TransparentAddressSource::StandalonePubkey(_) => None,
1256            #[cfg(feature = "transparent-key-import")]
1257            TransparentAddressSource::StandaloneScript(redeem_script) => Some(redeem_script),
1258            #[cfg(feature = "transparent-key-import")]
1259            TransparentAddressSource::StandaloneAddress => None,
1260        }
1261    }
1262}
1263
1264/// Property tests for [`OutputRef`], whose identity (txid, pool, output index) is the key the
1265/// note-locking tables and the proposal double-spend check operate on.
1266#[cfg(test)]
1267mod output_ref_tests {
1268    use proptest::prelude::*;
1269    use zcash_protocol::{PoolType, ShieldedPool, TxId};
1270
1271    use super::{NoteId, OutputRef};
1272
1273    fn arb_shielded_pool() -> impl Strategy<Value = ShieldedPool> {
1274        prop_oneof![
1275            Just(ShieldedPool::Sapling),
1276            Just(ShieldedPool::Orchard),
1277            Just(ShieldedPool::Ironwood),
1278        ]
1279    }
1280
1281    fn arb_pool_type() -> impl Strategy<Value = PoolType> {
1282        prop_oneof![
1283            Just(PoolType::Transparent),
1284            arb_shielded_pool().prop_map(PoolType::Shielded),
1285        ]
1286    }
1287
1288    fn arb_output_ref() -> impl Strategy<Value = OutputRef> {
1289        (any::<[u8; 32]>(), arb_pool_type(), any::<u32>())
1290            .prop_map(|(txid, pool, idx)| OutputRef::new(TxId::from_bytes(txid), pool, idx))
1291    }
1292
1293    proptest! {
1294        /// Converting a `NoteId` preserves every component: the note's pool maps into the
1295        /// shielded arm of `PoolType`, and the `u16` output index widens losslessly.
1296        #[test]
1297        fn from_note_id_preserves_fields(
1298            txid in any::<[u8; 32]>(),
1299            pool in arb_shielded_pool(),
1300            idx in any::<u16>(),
1301        ) {
1302            let txid = TxId::from_bytes(txid);
1303            let output_ref = OutputRef::from(NoteId::new(txid, pool, idx));
1304            prop_assert_eq!(output_ref.txid(), &txid);
1305            prop_assert_eq!(output_ref.pool(), PoolType::Shielded(pool));
1306            prop_assert_eq!(output_ref.output_index(), u32::from(idx));
1307        }
1308
1309        /// Identity is exactly the (txid, pool, output index) triple: a reference equals
1310        /// itself, differs from any single-field mutation of itself, and `Ord` agrees with
1311        /// `Eq` (the `BTreeSet` double-spend check in proposal construction and the lock
1312        /// tables both rely on this).
1313        #[test]
1314        fn identity_is_the_full_triple(a in arb_output_ref()) {
1315            prop_assert_eq!(a, a);
1316            prop_assert_eq!(a.cmp(&a), std::cmp::Ordering::Equal);
1317
1318            // A different output index is a different output.
1319            let other_index = OutputRef::new(
1320                *a.txid(),
1321                a.pool(),
1322                a.output_index().wrapping_add(1),
1323            );
1324            prop_assert_ne!(a, other_index);
1325            prop_assert_ne!(a.cmp(&other_index), std::cmp::Ordering::Equal);
1326
1327            // A different pool is a different output, even at the same (txid, index): the
1328            // same transaction may have outputs at the same index in several pools.
1329            let other_pool = OutputRef::new(
1330                *a.txid(),
1331                match a.pool() {
1332                    PoolType::Transparent => PoolType::SAPLING,
1333                    PoolType::Shielded(_) => PoolType::Transparent,
1334                },
1335                a.output_index(),
1336            );
1337            prop_assert_ne!(a, other_pool);
1338            prop_assert_ne!(a.cmp(&other_pool), std::cmp::Ordering::Equal);
1339
1340            // A different transaction is a different output.
1341            let mut txid = <[u8; 32]>::from(*a.txid());
1342            txid[0] = txid[0].wrapping_add(1);
1343            let other_txid = OutputRef::new(TxId::from_bytes(txid), a.pool(), a.output_index());
1344            prop_assert_ne!(a, other_txid);
1345            prop_assert_ne!(a.cmp(&other_txid), std::cmp::Ordering::Equal);
1346        }
1347
1348        /// Two independently drawn references are equal exactly when all three components
1349        /// match.
1350        #[test]
1351        fn equality_is_component_wise(a in arb_output_ref(), b in arb_output_ref()) {
1352            let components_equal = a.txid() == b.txid()
1353                && a.pool() == b.pool()
1354                && a.output_index() == b.output_index();
1355            prop_assert_eq!(a == b, components_equal);
1356        }
1357    }
1358}