Skip to main content

zcash_client_backend/data_api/
error.rs

1//! Types for wallet error handling.
2
3use std::{
4    collections::HashMap,
5    error,
6    fmt::{self, Debug, Display, Write},
7    hash::Hash,
8};
9
10use shardtree::error::ShardTreeError;
11use zcash_address::ConversionError;
12use zcash_keys::address::UnifiedAddress;
13use zcash_primitives::transaction::builder;
14use zcash_protocol::{
15    PoolType,
16    consensus::BlockHeight,
17    value::{BalanceError, Zatoshis},
18};
19
20use crate::{
21    data_api::wallet::input_selection::InputSelectorError, fees::ChangeError,
22    proposal::ProposalError, wallet::NoteId,
23};
24
25#[cfg(feature = "transparent-inputs")]
26use ::transparent::address::TransparentAddress;
27
28/// Errors that can occur as a consequence of wallet operations.
29#[derive(Debug)]
30#[non_exhaustive]
31pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError, ChangeErrT, NoteRefT>
32{
33    /// An error occurred retrieving data from the underlying data source
34    DataSource(DataSourceError),
35
36    /// An error in computations involving the note commitment trees.
37    CommitmentTree(ShardTreeError<CommitmentTreeError>),
38
39    /// An error in note selection
40    NoteSelection(SelectionError),
41
42    /// An error in change selection during transaction proposal construction
43    Change(ChangeError<ChangeErrT, NoteRefT>),
44
45    /// An error in transaction proposal construction
46    Proposal(ProposalError),
47
48    /// The proposal was structurally valid, but tried to do one of these unsupported things:
49    /// * spend a prior shielded output;
50    /// * pay to an output pool for which the corresponding feature is not enabled;
51    /// * pay to a TEX address if the "transparent-inputs" feature is not enabled.
52    /// * a proposal step has no inputs
53    ProposalNotSupported,
54
55    /// No account could be found corresponding to a provided ID.
56    AccountIdNotRecognized,
57
58    /// No account could be found corresponding to a provided spending key.
59    KeyNotRecognized,
60
61    /// The given account cannot be used for spending, because it is unable to maintain an
62    /// accurate balance.
63    AccountCannotSpend,
64
65    /// Zcash amount computation encountered an overflow or underflow.
66    BalanceError(BalanceError),
67
68    /// Unable to create a new spend because the wallet balance is not sufficient.
69    InsufficientFunds {
70        available: Zatoshis,
71        required: Zatoshis,
72    },
73
74    /// The wallet must first perform a scan of the blockchain before other
75    /// operations can be performed.
76    ScanRequired,
77
78    /// An error occurred building a new transaction.
79    Builder(builder::Error<FeeError>),
80
81    /// An error occurred constructing a payment for the transaction.
82    Payment(zip321::PaymentError),
83
84    /// Attempted to send change to an unsupported pool.
85    ///
86    /// This is indicative of a programming error; execution of a transaction proposal that
87    /// presumes support for the specified pool was performed using an application that does not
88    /// provide such support.
89    UnsupportedChangeType(PoolType),
90
91    /// Attempted to create a spend to an unsupported Unified Address receiver
92    NoSupportedReceivers(Box<UnifiedAddress>),
93
94    /// A proposed transaction cannot be built because it requires spending an input of
95    /// a type for which a key required to construct the transaction is not available.
96    KeyNotAvailable(PoolType),
97
98    /// A note being spent does not correspond to either the internal or external
99    /// full viewing key for an account.
100    NoteMismatch(NoteId),
101
102    /// An error occurred parsing the address from a payment request.
103    Address(ConversionError<&'static str>),
104
105    /// The address associated with a record being inserted was not recognized as
106    /// belonging to the wallet.
107    #[cfg(feature = "transparent-inputs")]
108    AddressNotRecognized(TransparentAddress),
109
110    /// The caller requested a nonzero target expiry height below the proposal's
111    /// minimum target height. Zero remains valid because it disables expiry.
112    ExpiryHeightBelowTargetHeight {
113        expiry_height: BlockHeight,
114        min_target_height: BlockHeight,
115    },
116
117    /// The caller requested an expiry height for a step that is a canonical ZIP 318 crossing.
118    ///
119    /// Such a step takes its expiry from the ZIP 318 rolling window, which every crossing in the
120    /// same period shares; a caller-chosen expiry would single it out and undo the shape the
121    /// unpadded bundle and bucketed anchor were chosen to produce. Those are already fixed by the
122    /// time the transaction is built, so the conflict is reported rather than silently resolved.
123    /// Pass `None` to accept the canonical expiry.
124    ExpiryHeightConflictsWithCanonicalCrossing { requested: BlockHeight },
125
126    /// A ZIP 316 Revision 2 recipient address of the payment at the given index has
127    /// expired, or has expiry metadata that conflicts with the transaction's expiry
128    /// height. A currently-valid address should be obtained for the intended recipient.
129    RecipientAddressExpiry {
130        payment_index: usize,
131        error: AddressExpiryError,
132    },
133
134    /// An error occurred while working with PCZTs.
135    #[cfg(feature = "pczt")]
136    Pczt(PcztError),
137}
138
139/// Errors that may occur when rewinding the wallet to a previous chain state.
140#[non_exhaustive]
141pub enum RewindError<AccountId: Hash + Eq, E> {
142    /// An error occurred retrieving data from the underlying data source.
143    DataSource(E),
144    /// Every account in the wallet has a birthday height greater than the height to which any
145    /// reset birthday would be lowered (the chain state's block height plus one), and the
146    /// `reset_account_birthdays` argument supplied by the caller was empty. So long as at
147    /// least one account already has a birthday at or below the new birthday floor, the
148    /// rewind proceeds without lowering any birthdays even when `reset_account_birthdays` is
149    /// empty. The caller should re-try the rewind, providing a non-empty set of accounts
150    /// whose birthday metadata should be lowered to the new birthday floor.
151    ///
152    /// The reported map contains every account in the wallet along with its existing birthday
153    /// height. The caller may include any subset of these in the next call's
154    /// `reset_account_birthdays`; accounts not included will retain their existing birthday
155    /// metadata. (Rescanning of any blocks above the rewind target is performed against all
156    /// wallet viewing keys regardless of which accounts' birthday metadata is reset.)
157    RewindBeyondBirthdays(HashMap<AccountId, BlockHeight>),
158}
159
160impl<AccountId: Hash + Eq + Debug, E: Debug> Debug for RewindError<AccountId, E> {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        match self {
163            RewindError::DataSource(e) => f.debug_tuple("DataSource").field(e).finish(),
164            RewindError::RewindBeyondBirthdays(birthdays) => f
165                .debug_tuple("RewindBeyondBirthdays")
166                .field(birthdays)
167                .finish(),
168        }
169    }
170}
171
172impl<AccountId: Hash + Eq + Debug, E: Display> Display for RewindError<AccountId, E> {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        match self {
175            RewindError::DataSource(e) => write!(f, "Wallet data source error: {e}"),
176            RewindError::RewindBeyondBirthdays(birthdays) => write!(
177                f,
178                "Rewind would precede the birthday height of one or more accounts: {birthdays:?}"
179            ),
180        }
181    }
182}
183
184impl<AccountId: Hash + Eq + Debug, E: error::Error + 'static> error::Error
185    for RewindError<AccountId, E>
186{
187    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
188        match self {
189            RewindError::DataSource(e) => Some(e),
190            RewindError::RewindBeyondBirthdays(_) => None,
191        }
192    }
193}
194
195/// Errors that can occur while working with PCZTs.
196#[cfg(feature = "pczt")]
197#[derive(Debug)]
198#[non_exhaustive]
199pub enum PcztError {
200    /// An error occurred while building a PCZT.
201    Build,
202
203    /// An error occurred while finalizing the IO of a PCZT.
204    IoFinalization(pczt::roles::io_finalizer::Error),
205
206    /// An error occurred while updating the Orchard bundle of a PCZT.
207    UpdateOrchard(pczt::roles::updater::OrchardError),
208
209    /// An error occurred while updating the Sapling bundle of a PCZT.
210    UpdateSapling(pczt::roles::updater::SaplingError),
211
212    /// An error occurred while updating the transparent bundle of a PCZT.
213    UpdateTransparent(pczt::roles::updater::TransparentError),
214
215    /// An error occurred while finalizing the spends of a PCZT.
216    SpendFinalization(pczt::roles::spend_finalizer::Error),
217
218    /// An error occurred while extracting a transaction from a PCZT.
219    Extraction(pczt::roles::tx_extractor::Error),
220
221    /// PCZT parsing resulted in an invalid condition.
222    Invalid(String),
223}
224
225/// The ways in which a ZIP 316 Revision 2 recipient address's expiry metadata can
226/// prevent a payment from being constructed.
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228#[non_exhaustive]
229pub enum AddressExpiryError {
230    /// The address's expiry time is earlier than the current time.
231    TimeExpired {
232        /// The address's expiry time, in seconds since the Unix epoch.
233        expiry_time: u64,
234    },
235    /// The chain has advanced past the address's expiry height.
236    HeightExpired {
237        /// The address's expiry height.
238        address_expiry: BlockHeight,
239        /// The height of the chain tip against which expiry was evaluated.
240        chain_height: BlockHeight,
241    },
242    /// The transaction's expiry height would exceed the address's expiry height.
243    ///
244    /// ZIP 316 forbids sending in this case rather than lowering the transaction's
245    /// expiry height to match, since an expiry height derived from the address's
246    /// metadata would leak information about the destination on chain.
247    ExpiryHeightConflict {
248        address_expiry: BlockHeight,
249        transaction_expiry: BlockHeight,
250    },
251    /// The address defines an expiry height, which requires the transaction to carry a
252    /// nonzero expiry height, but transaction expiry was disabled.
253    TransactionExpiryDisabled { address_expiry: BlockHeight },
254}
255
256impl fmt::Display for AddressExpiryError {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        match self {
259            AddressExpiryError::TimeExpired { expiry_time } => write!(
260                f,
261                "the address expired at {expiry_time} (seconds since the Unix epoch)"
262            ),
263            AddressExpiryError::HeightExpired {
264                address_expiry,
265                chain_height,
266            } => write!(
267                f,
268                "the address expired at height {address_expiry}, and the chain has \
269                 reached height {chain_height}"
270            ),
271            AddressExpiryError::ExpiryHeightConflict {
272                address_expiry,
273                transaction_expiry,
274            } => write!(
275                f,
276                "the transaction would expire at height {transaction_expiry}, after the \
277                 address's expiry height {address_expiry}"
278            ),
279            AddressExpiryError::TransactionExpiryDisabled { address_expiry } => write!(
280                f,
281                "the address expires at height {address_expiry}, which requires the \
282                 transaction to carry a nonzero expiry height, but transaction expiry \
283                 was disabled"
284            ),
285        }
286    }
287}
288
289impl error::Error for AddressExpiryError {}
290
291impl<DE, TE, SE, FE, CE, N> fmt::Display for Error<DE, TE, SE, FE, CE, N>
292where
293    DE: fmt::Display,
294    TE: fmt::Display,
295    SE: fmt::Display,
296    FE: fmt::Display,
297    CE: fmt::Display,
298    N: fmt::Display,
299{
300    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
301        match self {
302            Error::DataSource(e) => {
303                write!(
304                    f,
305                    "The underlying datasource produced the following error: {e}"
306                )
307            }
308            Error::CommitmentTree(e) => {
309                write!(
310                    f,
311                    "An error occurred in querying or updating a note commitment tree: {e}"
312                )
313            }
314            Error::NoteSelection(e) => {
315                write!(f, "Note selection encountered the following error: {e}")
316            }
317            Error::Change(e) => {
318                write!(f, "Change output generation failed: {e}")
319            }
320            Error::Proposal(e) => {
321                write!(
322                    f,
323                    "Input selection attempted to construct an invalid proposal: {e}"
324                )
325            }
326            Error::ProposalNotSupported => write!(
327                f,
328                "The proposal was valid but tried to do something that is not supported \
329                 (spend shielded outputs of prior transaction steps or use a feature that \
330                 is not enabled).",
331            ),
332            Error::KeyNotRecognized => {
333                write!(
334                    f,
335                    "Wallet does not contain an account corresponding to the provided spending key"
336                )
337            }
338            Error::AccountCannotSpend => {
339                write!(
340                    f,
341                    "The given account cannot be used for spending, because it is unable to maintain an accurate balance.",
342                )
343            }
344            Error::AccountIdNotRecognized => {
345                write!(
346                    f,
347                    "Wallet does not contain an account corresponding to the provided ID"
348                )
349            }
350            Error::BalanceError(e) => write!(
351                f,
352                "The value lies outside the valid range of Zcash amounts: {e:?}."
353            ),
354            Error::InsufficientFunds {
355                available,
356                required,
357            } => write!(
358                f,
359                "Insufficient balance (have {}, need {} including fee)",
360                u64::from(*available),
361                u64::from(*required)
362            ),
363            Error::ScanRequired => write!(f, "Must scan blocks first"),
364            Error::Builder(e) => write!(f, "An error occurred building the transaction: {e}"),
365            Error::Payment(e) => write!(f, "An error occurred constructing a payment: {e}"),
366            Error::UnsupportedChangeType(t) => write!(
367                f,
368                "Attempted to send change to an unsupported pool type: {t}"
369            ),
370            Error::NoSupportedReceivers(ua) => write!(
371                f,
372                "A recipient's unified address does not contain any receivers to which the wallet can send funds; required one of {}",
373                ua.receiver_types()
374                    .iter()
375                    .enumerate()
376                    .fold(String::new(), |mut acc, (i, tc)| {
377                        let _ = write!(acc, "{}{:?}", if i > 0 { ", " } else { "" }, tc);
378                        acc
379                    })
380            ),
381            Error::KeyNotAvailable(pool) => write!(
382                f,
383                "A key required for transaction construction was not available for pool type {pool}"
384            ),
385            Error::NoteMismatch(n) => write!(
386                f,
387                "A note being spent ({n:?}) does not correspond to either the internal or external full viewing key for the provided spending key."
388            ),
389
390            Error::Address(e) => {
391                write!(
392                    f,
393                    "An error occurred decoding the address from a payment request: {e}."
394                )
395            }
396            #[cfg(feature = "transparent-inputs")]
397            Error::AddressNotRecognized(_) => {
398                write!(
399                    f,
400                    "The specified transparent address was not recognized as belonging to the wallet."
401                )
402            }
403            Error::ExpiryHeightConflictsWithCanonicalCrossing { requested } => write!(
404                f,
405                "An expiry height of {requested} was requested for a canonical ZIP 318 crossing, \
406                 which takes the ZIP 318 rolling expiry; pass `None` to accept it."
407            ),
408            Error::RecipientAddressExpiry {
409                payment_index,
410                error,
411            } => write!(
412                f,
413                "The recipient address of the payment at index {payment_index} cannot be \
414                 paid: {error}. Try to obtain a currently-valid address for the intended \
415                 recipient."
416            ),
417            Error::ExpiryHeightBelowTargetHeight {
418                expiry_height,
419                min_target_height,
420            } => write!(
421                f,
422                "The requested expiry height {expiry_height} is below the proposal's \
423                 minimum target height {min_target_height}; the transaction would already be \
424                 expired at the earliest height at which it could be mined."
425            ),
426            #[cfg(feature = "pczt")]
427            Error::Pczt(e) => write!(f, "PCZT error: {e}"),
428        }
429    }
430}
431
432#[cfg(feature = "pczt")]
433impl fmt::Display for PcztError {
434    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
435        match self {
436            PcztError::Build => {
437                write!(
438                    f,
439                    "Failed to generate the PCZT prior to proving or signing."
440                )
441            }
442            PcztError::IoFinalization(e) => {
443                write!(f, "Failed to finalize IO: {e:?}.")
444            }
445            PcztError::UpdateOrchard(e) => {
446                write!(f, "Failed to updating Orchard PCZT data: {e:?}.")
447            }
448            PcztError::UpdateSapling(e) => {
449                write!(f, "Failed to updating Sapling PCZT data: {e:?}.")
450            }
451            PcztError::UpdateTransparent(e) => {
452                write!(f, "Failed to updating transparent PCZT data: {e:?}.")
453            }
454            PcztError::SpendFinalization(e) => {
455                write!(f, "Failed to finalize the PCZT spends: {e:?}.")
456            }
457            PcztError::Extraction(e) => {
458                write!(f, "Failed to extract the final transaction: {e:?}.")
459            }
460            PcztError::Invalid(e) => {
461                write!(f, "PCZT parsing resulted in an invalid condition: {e}.")
462            }
463        }
464    }
465}
466
467impl<DE, TE, SE, FE, CE, N> error::Error for Error<DE, TE, SE, FE, CE, N>
468where
469    DE: Debug + Display + error::Error + 'static,
470    TE: Debug + Display + error::Error + 'static,
471    SE: Debug + Display + error::Error + 'static,
472    FE: Debug + Display + 'static,
473    CE: Debug + Display + error::Error + 'static,
474    N: Debug + Display + 'static,
475{
476    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
477        match &self {
478            Error::DataSource(e) => Some(e),
479            Error::CommitmentTree(e) => Some(e),
480            Error::NoteSelection(e) => Some(e),
481            Error::Proposal(e) => Some(e),
482            Error::Builder(e) => Some(e),
483            #[cfg(feature = "pczt")]
484            Error::Pczt(e) => Some(e),
485            _ => None,
486        }
487    }
488}
489
490#[cfg(feature = "pczt")]
491impl error::Error for PcztError {}
492
493impl<DE, TE, SE, FE, CE, N> From<builder::Error<FE>> for Error<DE, TE, SE, FE, CE, N> {
494    fn from(e: builder::Error<FE>) -> Self {
495        Error::Builder(e)
496    }
497}
498
499impl<DE, TE, SE, FE, CE, N> From<ProposalError> for Error<DE, TE, SE, FE, CE, N> {
500    fn from(e: ProposalError) -> Self {
501        Error::Proposal(e)
502    }
503}
504
505impl<DE, TE, SE, FE, CE, N> From<BalanceError> for Error<DE, TE, SE, FE, CE, N> {
506    fn from(e: BalanceError) -> Self {
507        Error::BalanceError(e)
508    }
509}
510
511impl<DE, TE, SE, FE, CE, N> From<ConversionError<&'static str>> for Error<DE, TE, SE, FE, CE, N> {
512    fn from(value: ConversionError<&'static str>) -> Self {
513        Error::Address(value)
514    }
515}
516
517impl<DE, TE, SE, FE, CE, N> From<InputSelectorError<DE, SE, CE, N>>
518    for Error<DE, TE, SE, FE, CE, N>
519{
520    fn from(e: InputSelectorError<DE, SE, CE, N>) -> Self {
521        match e {
522            InputSelectorError::DataSource(e) => Error::DataSource(e),
523            InputSelectorError::Selection(e) => Error::NoteSelection(e),
524            InputSelectorError::Change(e) => Error::Change(e),
525            InputSelectorError::Proposal(e) => Error::Proposal(e),
526            InputSelectorError::InsufficientFunds {
527                available,
528                required,
529            } => Error::InsufficientFunds {
530                available,
531                required,
532            },
533            InputSelectorError::SyncRequired => Error::ScanRequired,
534            InputSelectorError::Address(e) => Error::Address(e),
535        }
536    }
537}
538
539impl<DE, TE, SE, FE, CE, N> From<sapling::builder::Error> for Error<DE, TE, SE, FE, CE, N> {
540    fn from(e: sapling::builder::Error) -> Self {
541        Error::Builder(builder::Error::SaplingBuild(e))
542    }
543}
544
545impl<DE, TE, SE, FE, CE, N> From<transparent::builder::Error> for Error<DE, TE, SE, FE, CE, N> {
546    fn from(e: ::transparent::builder::Error) -> Self {
547        Error::Builder(builder::Error::TransparentBuild(e))
548    }
549}
550
551impl<DE, TE, SE, FE, CE, N> From<ShardTreeError<TE>> for Error<DE, TE, SE, FE, CE, N> {
552    fn from(e: ShardTreeError<TE>) -> Self {
553        Error::CommitmentTree(e)
554    }
555}
556
557#[cfg(feature = "pczt")]
558impl<DE, TE, SE, FE, CE, N> From<PcztError> for Error<DE, TE, SE, FE, CE, N> {
559    fn from(e: PcztError) -> Self {
560        Error::Pczt(e)
561    }
562}
563
564#[cfg(feature = "pczt")]
565impl<DE, TE, SE, FE, CE, N> From<pczt::roles::io_finalizer::Error>
566    for Error<DE, TE, SE, FE, CE, N>
567{
568    fn from(e: pczt::roles::io_finalizer::Error) -> Self {
569        Error::Pczt(PcztError::IoFinalization(e))
570    }
571}
572
573#[cfg(feature = "pczt")]
574impl<DE, TE, SE, FE, CE, N> From<pczt::roles::updater::OrchardError>
575    for Error<DE, TE, SE, FE, CE, N>
576{
577    fn from(e: pczt::roles::updater::OrchardError) -> Self {
578        Error::Pczt(PcztError::UpdateOrchard(e))
579    }
580}
581
582#[cfg(feature = "pczt")]
583impl<DE, TE, SE, FE, CE, N> From<pczt::roles::updater::SaplingError>
584    for Error<DE, TE, SE, FE, CE, N>
585{
586    fn from(e: pczt::roles::updater::SaplingError) -> Self {
587        Error::Pczt(PcztError::UpdateSapling(e))
588    }
589}
590
591#[cfg(feature = "pczt")]
592impl<DE, TE, SE, FE, CE, N> From<pczt::roles::updater::TransparentError>
593    for Error<DE, TE, SE, FE, CE, N>
594{
595    fn from(e: pczt::roles::updater::TransparentError) -> Self {
596        Error::Pczt(PcztError::UpdateTransparent(e))
597    }
598}
599
600#[cfg(feature = "pczt")]
601impl<DE, TE, SE, FE, CE, N> From<pczt::roles::spend_finalizer::Error>
602    for Error<DE, TE, SE, FE, CE, N>
603{
604    fn from(e: pczt::roles::spend_finalizer::Error) -> Self {
605        Error::Pczt(PcztError::SpendFinalization(e))
606    }
607}
608
609#[cfg(feature = "pczt")]
610impl<DE, TE, SE, FE, CE, N> From<pczt::roles::tx_extractor::Error>
611    for Error<DE, TE, SE, FE, CE, N>
612{
613    fn from(e: pczt::roles::tx_extractor::Error) -> Self {
614        Error::Pczt(PcztError::Extraction(e))
615    }
616}
617
618/// Errors that may occur when resolving the account controlling an address.
619#[derive(Debug)]
620#[non_exhaustive]
621pub enum FindAccountForAddressError<E> {
622    /// Error returned by the underlying wallet backend.
623    Backend(E),
624
625    /// A Unified Address whose receivers map to different accounts.
626    UnifiedAddressConflict,
627}
628
629impl<E> From<E> for FindAccountForAddressError<E> {
630    fn from(err: E) -> Self {
631        Self::Backend(err)
632    }
633}
634
635impl<E: Display> Display for FindAccountForAddressError<E> {
636    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
637        match self {
638            FindAccountForAddressError::Backend(e) => {
639                write!(f, "Wallet backend error: {e}")
640            }
641            FindAccountForAddressError::UnifiedAddressConflict => write!(
642                f,
643                "Receivers of the provided Unified Address map to different wallet accounts."
644            ),
645        }
646    }
647}
648
649impl<E: error::Error + 'static> error::Error for FindAccountForAddressError<E> {
650    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
651        match self {
652            FindAccountForAddressError::Backend(e) => Some(e),
653            FindAccountForAddressError::UnifiedAddressConflict => None,
654        }
655    }
656}
657
658pub use super::locking::LockError;