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    /// An error occurred while working with PCZTs.
127    #[cfg(feature = "pczt")]
128    Pczt(PcztError),
129}
130
131/// Errors that may occur when rewinding the wallet to a previous chain state.
132#[non_exhaustive]
133pub enum RewindError<AccountId: Hash + Eq, E> {
134    /// An error occurred retrieving data from the underlying data source.
135    DataSource(E),
136    /// Every account in the wallet has a birthday height greater than the height to which any
137    /// reset birthday would be lowered (the chain state's block height plus one), and the
138    /// `reset_account_birthdays` argument supplied by the caller was empty. So long as at
139    /// least one account already has a birthday at or below the new birthday floor, the
140    /// rewind proceeds without lowering any birthdays even when `reset_account_birthdays` is
141    /// empty. The caller should re-try the rewind, providing a non-empty set of accounts
142    /// whose birthday metadata should be lowered to the new birthday floor.
143    ///
144    /// The reported map contains every account in the wallet along with its existing birthday
145    /// height. The caller may include any subset of these in the next call's
146    /// `reset_account_birthdays`; accounts not included will retain their existing birthday
147    /// metadata. (Rescanning of any blocks above the rewind target is performed against all
148    /// wallet viewing keys regardless of which accounts' birthday metadata is reset.)
149    RewindBeyondBirthdays(HashMap<AccountId, BlockHeight>),
150}
151
152impl<AccountId: Hash + Eq + Debug, E: Debug> Debug for RewindError<AccountId, E> {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        match self {
155            RewindError::DataSource(e) => f.debug_tuple("DataSource").field(e).finish(),
156            RewindError::RewindBeyondBirthdays(birthdays) => f
157                .debug_tuple("RewindBeyondBirthdays")
158                .field(birthdays)
159                .finish(),
160        }
161    }
162}
163
164impl<AccountId: Hash + Eq + Debug, E: Display> Display for RewindError<AccountId, E> {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        match self {
167            RewindError::DataSource(e) => write!(f, "Wallet data source error: {e}"),
168            RewindError::RewindBeyondBirthdays(birthdays) => write!(
169                f,
170                "Rewind would precede the birthday height of one or more accounts: {birthdays:?}"
171            ),
172        }
173    }
174}
175
176impl<AccountId: Hash + Eq + Debug, E: error::Error + 'static> error::Error
177    for RewindError<AccountId, E>
178{
179    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
180        match self {
181            RewindError::DataSource(e) => Some(e),
182            RewindError::RewindBeyondBirthdays(_) => None,
183        }
184    }
185}
186
187/// Errors that can occur while working with PCZTs.
188#[cfg(feature = "pczt")]
189#[derive(Debug)]
190#[non_exhaustive]
191pub enum PcztError {
192    /// An error occurred while building a PCZT.
193    Build,
194
195    /// An error occurred while finalizing the IO of a PCZT.
196    IoFinalization(pczt::roles::io_finalizer::Error),
197
198    /// An error occurred while updating the Orchard bundle of a PCZT.
199    UpdateOrchard(pczt::roles::updater::OrchardError),
200
201    /// An error occurred while updating the Sapling bundle of a PCZT.
202    UpdateSapling(pczt::roles::updater::SaplingError),
203
204    /// An error occurred while updating the transparent bundle of a PCZT.
205    UpdateTransparent(pczt::roles::updater::TransparentError),
206
207    /// An error occurred while finalizing the spends of a PCZT.
208    SpendFinalization(pczt::roles::spend_finalizer::Error),
209
210    /// An error occurred while extracting a transaction from a PCZT.
211    Extraction(pczt::roles::tx_extractor::Error),
212
213    /// PCZT parsing resulted in an invalid condition.
214    Invalid(String),
215}
216
217impl<DE, TE, SE, FE, CE, N> fmt::Display for Error<DE, TE, SE, FE, CE, N>
218where
219    DE: fmt::Display,
220    TE: fmt::Display,
221    SE: fmt::Display,
222    FE: fmt::Display,
223    CE: fmt::Display,
224    N: fmt::Display,
225{
226    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
227        match self {
228            Error::DataSource(e) => {
229                write!(
230                    f,
231                    "The underlying datasource produced the following error: {e}"
232                )
233            }
234            Error::CommitmentTree(e) => {
235                write!(
236                    f,
237                    "An error occurred in querying or updating a note commitment tree: {e}"
238                )
239            }
240            Error::NoteSelection(e) => {
241                write!(f, "Note selection encountered the following error: {e}")
242            }
243            Error::Change(e) => {
244                write!(f, "Change output generation failed: {e}")
245            }
246            Error::Proposal(e) => {
247                write!(
248                    f,
249                    "Input selection attempted to construct an invalid proposal: {e}"
250                )
251            }
252            Error::ProposalNotSupported => write!(
253                f,
254                "The proposal was valid but tried to do something that is not supported \
255                 (spend shielded outputs of prior transaction steps or use a feature that \
256                 is not enabled).",
257            ),
258            Error::KeyNotRecognized => {
259                write!(
260                    f,
261                    "Wallet does not contain an account corresponding to the provided spending key"
262                )
263            }
264            Error::AccountCannotSpend => {
265                write!(
266                    f,
267                    "The given account cannot be used for spending, because it is unable to maintain an accurate balance.",
268                )
269            }
270            Error::AccountIdNotRecognized => {
271                write!(
272                    f,
273                    "Wallet does not contain an account corresponding to the provided ID"
274                )
275            }
276            Error::BalanceError(e) => write!(
277                f,
278                "The value lies outside the valid range of Zcash amounts: {e:?}."
279            ),
280            Error::InsufficientFunds {
281                available,
282                required,
283            } => write!(
284                f,
285                "Insufficient balance (have {}, need {} including fee)",
286                u64::from(*available),
287                u64::from(*required)
288            ),
289            Error::ScanRequired => write!(f, "Must scan blocks first"),
290            Error::Builder(e) => write!(f, "An error occurred building the transaction: {e}"),
291            Error::Payment(e) => write!(f, "An error occurred constructing a payment: {e}"),
292            Error::UnsupportedChangeType(t) => write!(
293                f,
294                "Attempted to send change to an unsupported pool type: {t}"
295            ),
296            Error::NoSupportedReceivers(ua) => write!(
297                f,
298                "A recipient's unified address does not contain any receivers to which the wallet can send funds; required one of {}",
299                ua.receiver_types()
300                    .iter()
301                    .enumerate()
302                    .fold(String::new(), |mut acc, (i, tc)| {
303                        let _ = write!(acc, "{}{:?}", if i > 0 { ", " } else { "" }, tc);
304                        acc
305                    })
306            ),
307            Error::KeyNotAvailable(pool) => write!(
308                f,
309                "A key required for transaction construction was not available for pool type {pool}"
310            ),
311            Error::NoteMismatch(n) => write!(
312                f,
313                "A note being spent ({n:?}) does not correspond to either the internal or external full viewing key for the provided spending key."
314            ),
315
316            Error::Address(e) => {
317                write!(
318                    f,
319                    "An error occurred decoding the address from a payment request: {e}."
320                )
321            }
322            #[cfg(feature = "transparent-inputs")]
323            Error::AddressNotRecognized(_) => {
324                write!(
325                    f,
326                    "The specified transparent address was not recognized as belonging to the wallet."
327                )
328            }
329            Error::ExpiryHeightConflictsWithCanonicalCrossing { requested } => write!(
330                f,
331                "An expiry height of {requested} was requested for a canonical ZIP 318 crossing, \
332                 which takes the ZIP 318 rolling expiry; pass `None` to accept it."
333            ),
334            Error::ExpiryHeightBelowTargetHeight {
335                expiry_height,
336                min_target_height,
337            } => write!(
338                f,
339                "The requested expiry height {expiry_height} is below the proposal's \
340                 minimum target height {min_target_height}; the transaction would already be \
341                 expired at the earliest height at which it could be mined."
342            ),
343            #[cfg(feature = "pczt")]
344            Error::Pczt(e) => write!(f, "PCZT error: {e}"),
345        }
346    }
347}
348
349#[cfg(feature = "pczt")]
350impl fmt::Display for PcztError {
351    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
352        match self {
353            PcztError::Build => {
354                write!(
355                    f,
356                    "Failed to generate the PCZT prior to proving or signing."
357                )
358            }
359            PcztError::IoFinalization(e) => {
360                write!(f, "Failed to finalize IO: {e:?}.")
361            }
362            PcztError::UpdateOrchard(e) => {
363                write!(f, "Failed to updating Orchard PCZT data: {e:?}.")
364            }
365            PcztError::UpdateSapling(e) => {
366                write!(f, "Failed to updating Sapling PCZT data: {e:?}.")
367            }
368            PcztError::UpdateTransparent(e) => {
369                write!(f, "Failed to updating transparent PCZT data: {e:?}.")
370            }
371            PcztError::SpendFinalization(e) => {
372                write!(f, "Failed to finalize the PCZT spends: {e:?}.")
373            }
374            PcztError::Extraction(e) => {
375                write!(f, "Failed to extract the final transaction: {e:?}.")
376            }
377            PcztError::Invalid(e) => {
378                write!(f, "PCZT parsing resulted in an invalid condition: {e}.")
379            }
380        }
381    }
382}
383
384impl<DE, TE, SE, FE, CE, N> error::Error for Error<DE, TE, SE, FE, CE, N>
385where
386    DE: Debug + Display + error::Error + 'static,
387    TE: Debug + Display + error::Error + 'static,
388    SE: Debug + Display + error::Error + 'static,
389    FE: Debug + Display + 'static,
390    CE: Debug + Display + error::Error + 'static,
391    N: Debug + Display + 'static,
392{
393    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
394        match &self {
395            Error::DataSource(e) => Some(e),
396            Error::CommitmentTree(e) => Some(e),
397            Error::NoteSelection(e) => Some(e),
398            Error::Proposal(e) => Some(e),
399            Error::Builder(e) => Some(e),
400            #[cfg(feature = "pczt")]
401            Error::Pczt(e) => Some(e),
402            _ => None,
403        }
404    }
405}
406
407#[cfg(feature = "pczt")]
408impl error::Error for PcztError {}
409
410impl<DE, TE, SE, FE, CE, N> From<builder::Error<FE>> for Error<DE, TE, SE, FE, CE, N> {
411    fn from(e: builder::Error<FE>) -> Self {
412        Error::Builder(e)
413    }
414}
415
416impl<DE, TE, SE, FE, CE, N> From<ProposalError> for Error<DE, TE, SE, FE, CE, N> {
417    fn from(e: ProposalError) -> Self {
418        Error::Proposal(e)
419    }
420}
421
422impl<DE, TE, SE, FE, CE, N> From<BalanceError> for Error<DE, TE, SE, FE, CE, N> {
423    fn from(e: BalanceError) -> Self {
424        Error::BalanceError(e)
425    }
426}
427
428impl<DE, TE, SE, FE, CE, N> From<ConversionError<&'static str>> for Error<DE, TE, SE, FE, CE, N> {
429    fn from(value: ConversionError<&'static str>) -> Self {
430        Error::Address(value)
431    }
432}
433
434impl<DE, TE, SE, FE, CE, N> From<InputSelectorError<DE, SE, CE, N>>
435    for Error<DE, TE, SE, FE, CE, N>
436{
437    fn from(e: InputSelectorError<DE, SE, CE, N>) -> Self {
438        match e {
439            InputSelectorError::DataSource(e) => Error::DataSource(e),
440            InputSelectorError::Selection(e) => Error::NoteSelection(e),
441            InputSelectorError::Change(e) => Error::Change(e),
442            InputSelectorError::Proposal(e) => Error::Proposal(e),
443            InputSelectorError::InsufficientFunds {
444                available,
445                required,
446            } => Error::InsufficientFunds {
447                available,
448                required,
449            },
450            InputSelectorError::SyncRequired => Error::ScanRequired,
451            InputSelectorError::Address(e) => Error::Address(e),
452        }
453    }
454}
455
456impl<DE, TE, SE, FE, CE, N> From<sapling::builder::Error> for Error<DE, TE, SE, FE, CE, N> {
457    fn from(e: sapling::builder::Error) -> Self {
458        Error::Builder(builder::Error::SaplingBuild(e))
459    }
460}
461
462impl<DE, TE, SE, FE, CE, N> From<transparent::builder::Error> for Error<DE, TE, SE, FE, CE, N> {
463    fn from(e: ::transparent::builder::Error) -> Self {
464        Error::Builder(builder::Error::TransparentBuild(e))
465    }
466}
467
468impl<DE, TE, SE, FE, CE, N> From<ShardTreeError<TE>> for Error<DE, TE, SE, FE, CE, N> {
469    fn from(e: ShardTreeError<TE>) -> Self {
470        Error::CommitmentTree(e)
471    }
472}
473
474#[cfg(feature = "pczt")]
475impl<DE, TE, SE, FE, CE, N> From<PcztError> for Error<DE, TE, SE, FE, CE, N> {
476    fn from(e: PcztError) -> Self {
477        Error::Pczt(e)
478    }
479}
480
481#[cfg(feature = "pczt")]
482impl<DE, TE, SE, FE, CE, N> From<pczt::roles::io_finalizer::Error>
483    for Error<DE, TE, SE, FE, CE, N>
484{
485    fn from(e: pczt::roles::io_finalizer::Error) -> Self {
486        Error::Pczt(PcztError::IoFinalization(e))
487    }
488}
489
490#[cfg(feature = "pczt")]
491impl<DE, TE, SE, FE, CE, N> From<pczt::roles::updater::OrchardError>
492    for Error<DE, TE, SE, FE, CE, N>
493{
494    fn from(e: pczt::roles::updater::OrchardError) -> Self {
495        Error::Pczt(PcztError::UpdateOrchard(e))
496    }
497}
498
499#[cfg(feature = "pczt")]
500impl<DE, TE, SE, FE, CE, N> From<pczt::roles::updater::SaplingError>
501    for Error<DE, TE, SE, FE, CE, N>
502{
503    fn from(e: pczt::roles::updater::SaplingError) -> Self {
504        Error::Pczt(PcztError::UpdateSapling(e))
505    }
506}
507
508#[cfg(feature = "pczt")]
509impl<DE, TE, SE, FE, CE, N> From<pczt::roles::updater::TransparentError>
510    for Error<DE, TE, SE, FE, CE, N>
511{
512    fn from(e: pczt::roles::updater::TransparentError) -> Self {
513        Error::Pczt(PcztError::UpdateTransparent(e))
514    }
515}
516
517#[cfg(feature = "pczt")]
518impl<DE, TE, SE, FE, CE, N> From<pczt::roles::spend_finalizer::Error>
519    for Error<DE, TE, SE, FE, CE, N>
520{
521    fn from(e: pczt::roles::spend_finalizer::Error) -> Self {
522        Error::Pczt(PcztError::SpendFinalization(e))
523    }
524}
525
526#[cfg(feature = "pczt")]
527impl<DE, TE, SE, FE, CE, N> From<pczt::roles::tx_extractor::Error>
528    for Error<DE, TE, SE, FE, CE, N>
529{
530    fn from(e: pczt::roles::tx_extractor::Error) -> Self {
531        Error::Pczt(PcztError::Extraction(e))
532    }
533}
534
535/// Errors that may occur when resolving the account controlling an address.
536#[derive(Debug)]
537#[non_exhaustive]
538pub enum FindAccountForAddressError<E> {
539    /// Error returned by the underlying wallet backend.
540    Backend(E),
541
542    /// A Unified Address whose receivers map to different accounts.
543    UnifiedAddressConflict,
544}
545
546impl<E> From<E> for FindAccountForAddressError<E> {
547    fn from(err: E) -> Self {
548        Self::Backend(err)
549    }
550}
551
552impl<E: Display> Display for FindAccountForAddressError<E> {
553    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
554        match self {
555            FindAccountForAddressError::Backend(e) => {
556                write!(f, "Wallet backend error: {e}")
557            }
558            FindAccountForAddressError::UnifiedAddressConflict => write!(
559                f,
560                "Receivers of the provided Unified Address map to different wallet accounts."
561            ),
562        }
563    }
564}
565
566impl<E: error::Error + 'static> error::Error for FindAccountForAddressError<E> {
567    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
568        match self {
569            FindAccountForAddressError::Backend(e) => Some(e),
570            FindAccountForAddressError::UnifiedAddressConflict => None,
571        }
572    }
573}
574
575pub use super::locking::LockError;