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}