zakura-client-backend 0.1.0-rc2

APIs for creating shielded Zcash light clients
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
//! Types for wallet error handling.

use std::{
    collections::HashMap,
    error,
    fmt::{self, Debug, Display, Write},
    hash::Hash,
};

use shardtree::error::ShardTreeError;
use zcash_address::ConversionError;
use zcash_keys::address::UnifiedAddress;
use zcash_primitives::transaction::builder;
use zcash_protocol::{
    PoolType,
    consensus::BlockHeight,
    value::{BalanceError, Zatoshis},
};

use crate::{
    data_api::wallet::input_selection::InputSelectorError, fees::ChangeError,
    proposal::ProposalError, wallet::NoteId,
};

#[cfg(feature = "transparent-inputs")]
use ::transparent::address::TransparentAddress;

/// Errors that can occur as a consequence of wallet operations.
#[derive(Debug)]
#[non_exhaustive]
pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError, ChangeErrT, NoteRefT>
{
    /// An error occurred retrieving data from the underlying data source
    DataSource(DataSourceError),

    /// An error in computations involving the note commitment trees.
    CommitmentTree(ShardTreeError<CommitmentTreeError>),

    /// An error in note selection
    NoteSelection(SelectionError),

    /// An error in change selection during transaction proposal construction
    Change(ChangeError<ChangeErrT, NoteRefT>),

    /// An error in transaction proposal construction
    Proposal(ProposalError),

    /// The proposal was structurally valid, but tried to do one of these unsupported things:
    /// * spend a prior shielded output;
    /// * pay to an output pool for which the corresponding feature is not enabled;
    /// * pay to a TEX address if the "transparent-inputs" feature is not enabled.
    /// * a proposal step has no inputs
    ProposalNotSupported,

    /// No account could be found corresponding to a provided ID.
    AccountIdNotRecognized,

    /// No account could be found corresponding to a provided spending key.
    KeyNotRecognized,

    /// The given account cannot be used for spending, because it is unable to maintain an
    /// accurate balance.
    AccountCannotSpend,

    /// Zcash amount computation encountered an overflow or underflow.
    BalanceError(BalanceError),

    /// Unable to create a new spend because the wallet balance is not sufficient.
    InsufficientFunds {
        available: Zatoshis,
        required: Zatoshis,
    },

    /// The wallet must first perform a scan of the blockchain before other
    /// operations can be performed.
    ScanRequired,

    /// An error occurred building a new transaction.
    Builder(builder::Error<FeeError>),

    /// An error occurred constructing a payment for the transaction.
    Payment(zip321::PaymentError),

    /// Attempted to send change to an unsupported pool.
    ///
    /// This is indicative of a programming error; execution of a transaction proposal that
    /// presumes support for the specified pool was performed using an application that does not
    /// provide such support.
    UnsupportedChangeType(PoolType),

    /// Attempted to create a spend to an unsupported Unified Address receiver
    NoSupportedReceivers(Box<UnifiedAddress>),

    /// A proposed transaction cannot be built because it requires spending an input of
    /// a type for which a key required to construct the transaction is not available.
    KeyNotAvailable(PoolType),

    /// A note being spent does not correspond to either the internal or external
    /// full viewing key for an account.
    NoteMismatch(NoteId),

    /// An error occurred parsing the address from a payment request.
    Address(ConversionError<&'static str>),

    /// The address associated with a record being inserted was not recognized as
    /// belonging to the wallet.
    #[cfg(feature = "transparent-inputs")]
    AddressNotRecognized(TransparentAddress),

    /// The caller requested a nonzero target expiry height below the proposal's
    /// minimum target height. Zero remains valid because it disables expiry.
    ExpiryHeightBelowTargetHeight {
        expiry_height: BlockHeight,
        min_target_height: BlockHeight,
    },

    /// The caller requested an expiry height for a step that is a canonical ZIP 318 crossing.
    ///
    /// Such a step takes its expiry from the ZIP 318 rolling window, which every crossing in the
    /// same period shares; a caller-chosen expiry would single it out and undo the shape the
    /// unpadded bundle and bucketed anchor were chosen to produce. Those are already fixed by the
    /// time the transaction is built, so the conflict is reported rather than silently resolved.
    /// Pass `None` to accept the canonical expiry.
    ExpiryHeightConflictsWithCanonicalCrossing { requested: BlockHeight },

    /// An error occurred while working with PCZTs.
    #[cfg(feature = "pczt")]
    Pczt(PcztError),
}

/// Errors that may occur when rewinding the wallet to a previous chain state.
#[non_exhaustive]
pub enum RewindError<AccountId: Hash + Eq, E> {
    /// An error occurred retrieving data from the underlying data source.
    DataSource(E),
    /// Every account in the wallet has a birthday height greater than the height to which any
    /// reset birthday would be lowered (the chain state's block height plus one), and the
    /// `reset_account_birthdays` argument supplied by the caller was empty. So long as at
    /// least one account already has a birthday at or below the new birthday floor, the
    /// rewind proceeds without lowering any birthdays even when `reset_account_birthdays` is
    /// empty. The caller should re-try the rewind, providing a non-empty set of accounts
    /// whose birthday metadata should be lowered to the new birthday floor.
    ///
    /// The reported map contains every account in the wallet along with its existing birthday
    /// height. The caller may include any subset of these in the next call's
    /// `reset_account_birthdays`; accounts not included will retain their existing birthday
    /// metadata. (Rescanning of any blocks above the rewind target is performed against all
    /// wallet viewing keys regardless of which accounts' birthday metadata is reset.)
    RewindBeyondBirthdays(HashMap<AccountId, BlockHeight>),
}

impl<AccountId: Hash + Eq + Debug, E: Debug> Debug for RewindError<AccountId, E> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            RewindError::DataSource(e) => f.debug_tuple("DataSource").field(e).finish(),
            RewindError::RewindBeyondBirthdays(birthdays) => f
                .debug_tuple("RewindBeyondBirthdays")
                .field(birthdays)
                .finish(),
        }
    }
}

impl<AccountId: Hash + Eq + Debug, E: Display> Display for RewindError<AccountId, E> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            RewindError::DataSource(e) => write!(f, "Wallet data source error: {e}"),
            RewindError::RewindBeyondBirthdays(birthdays) => write!(
                f,
                "Rewind would precede the birthday height of one or more accounts: {birthdays:?}"
            ),
        }
    }
}

impl<AccountId: Hash + Eq + Debug, E: error::Error + 'static> error::Error
    for RewindError<AccountId, E>
{
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        match self {
            RewindError::DataSource(e) => Some(e),
            RewindError::RewindBeyondBirthdays(_) => None,
        }
    }
}

/// Errors that can occur while working with PCZTs.
#[cfg(feature = "pczt")]
#[derive(Debug)]
#[non_exhaustive]
pub enum PcztError {
    /// An error occurred while building a PCZT.
    Build,

    /// An error occurred while finalizing the IO of a PCZT.
    IoFinalization(pczt::roles::io_finalizer::Error),

    /// An error occurred while updating the Orchard bundle of a PCZT.
    UpdateOrchard(pczt::roles::updater::OrchardError),

    /// An error occurred while updating the Sapling bundle of a PCZT.
    UpdateSapling(pczt::roles::updater::SaplingError),

    /// An error occurred while updating the transparent bundle of a PCZT.
    UpdateTransparent(pczt::roles::updater::TransparentError),

    /// An error occurred while finalizing the spends of a PCZT.
    SpendFinalization(pczt::roles::spend_finalizer::Error),

    /// An error occurred while extracting a transaction from a PCZT.
    Extraction(pczt::roles::tx_extractor::Error),

    /// PCZT parsing resulted in an invalid condition.
    Invalid(String),
}

impl<DE, TE, SE, FE, CE, N> fmt::Display for Error<DE, TE, SE, FE, CE, N>
where
    DE: fmt::Display,
    TE: fmt::Display,
    SE: fmt::Display,
    FE: fmt::Display,
    CE: fmt::Display,
    N: fmt::Display,
{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Error::DataSource(e) => {
                write!(
                    f,
                    "The underlying datasource produced the following error: {e}"
                )
            }
            Error::CommitmentTree(e) => {
                write!(
                    f,
                    "An error occurred in querying or updating a note commitment tree: {e}"
                )
            }
            Error::NoteSelection(e) => {
                write!(f, "Note selection encountered the following error: {e}")
            }
            Error::Change(e) => {
                write!(f, "Change output generation failed: {e}")
            }
            Error::Proposal(e) => {
                write!(
                    f,
                    "Input selection attempted to construct an invalid proposal: {e}"
                )
            }
            Error::ProposalNotSupported => write!(
                f,
                "The proposal was valid but tried to do something that is not supported \
                 (spend shielded outputs of prior transaction steps or use a feature that \
                 is not enabled).",
            ),
            Error::KeyNotRecognized => {
                write!(
                    f,
                    "Wallet does not contain an account corresponding to the provided spending key"
                )
            }
            Error::AccountCannotSpend => {
                write!(
                    f,
                    "The given account cannot be used for spending, because it is unable to maintain an accurate balance.",
                )
            }
            Error::AccountIdNotRecognized => {
                write!(
                    f,
                    "Wallet does not contain an account corresponding to the provided ID"
                )
            }
            Error::BalanceError(e) => write!(
                f,
                "The value lies outside the valid range of Zcash amounts: {e:?}."
            ),
            Error::InsufficientFunds {
                available,
                required,
            } => write!(
                f,
                "Insufficient balance (have {}, need {} including fee)",
                u64::from(*available),
                u64::from(*required)
            ),
            Error::ScanRequired => write!(f, "Must scan blocks first"),
            Error::Builder(e) => write!(f, "An error occurred building the transaction: {e}"),
            Error::Payment(e) => write!(f, "An error occurred constructing a payment: {e}"),
            Error::UnsupportedChangeType(t) => write!(
                f,
                "Attempted to send change to an unsupported pool type: {t}"
            ),
            Error::NoSupportedReceivers(ua) => write!(
                f,
                "A recipient's unified address does not contain any receivers to which the wallet can send funds; required one of {}",
                ua.receiver_types()
                    .iter()
                    .enumerate()
                    .fold(String::new(), |mut acc, (i, tc)| {
                        let _ = write!(acc, "{}{:?}", if i > 0 { ", " } else { "" }, tc);
                        acc
                    })
            ),
            Error::KeyNotAvailable(pool) => write!(
                f,
                "A key required for transaction construction was not available for pool type {pool}"
            ),
            Error::NoteMismatch(n) => write!(
                f,
                "A note being spent ({n:?}) does not correspond to either the internal or external full viewing key for the provided spending key."
            ),

            Error::Address(e) => {
                write!(
                    f,
                    "An error occurred decoding the address from a payment request: {e}."
                )
            }
            #[cfg(feature = "transparent-inputs")]
            Error::AddressNotRecognized(_) => {
                write!(
                    f,
                    "The specified transparent address was not recognized as belonging to the wallet."
                )
            }
            Error::ExpiryHeightConflictsWithCanonicalCrossing { requested } => write!(
                f,
                "An expiry height of {requested} was requested for a canonical ZIP 318 crossing, \
                 which takes the ZIP 318 rolling expiry; pass `None` to accept it."
            ),
            Error::ExpiryHeightBelowTargetHeight {
                expiry_height,
                min_target_height,
            } => write!(
                f,
                "The requested expiry height {expiry_height} is below the proposal's \
                 minimum target height {min_target_height}; the transaction would already be \
                 expired at the earliest height at which it could be mined."
            ),
            #[cfg(feature = "pczt")]
            Error::Pczt(e) => write!(f, "PCZT error: {e}"),
        }
    }
}

#[cfg(feature = "pczt")]
impl fmt::Display for PcztError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            PcztError::Build => {
                write!(
                    f,
                    "Failed to generate the PCZT prior to proving or signing."
                )
            }
            PcztError::IoFinalization(e) => {
                write!(f, "Failed to finalize IO: {e:?}.")
            }
            PcztError::UpdateOrchard(e) => {
                write!(f, "Failed to updating Orchard PCZT data: {e:?}.")
            }
            PcztError::UpdateSapling(e) => {
                write!(f, "Failed to updating Sapling PCZT data: {e:?}.")
            }
            PcztError::UpdateTransparent(e) => {
                write!(f, "Failed to updating transparent PCZT data: {e:?}.")
            }
            PcztError::SpendFinalization(e) => {
                write!(f, "Failed to finalize the PCZT spends: {e:?}.")
            }
            PcztError::Extraction(e) => {
                write!(f, "Failed to extract the final transaction: {e:?}.")
            }
            PcztError::Invalid(e) => {
                write!(f, "PCZT parsing resulted in an invalid condition: {e}.")
            }
        }
    }
}

impl<DE, TE, SE, FE, CE, N> error::Error for Error<DE, TE, SE, FE, CE, N>
where
    DE: Debug + Display + error::Error + 'static,
    TE: Debug + Display + error::Error + 'static,
    SE: Debug + Display + error::Error + 'static,
    FE: Debug + Display + 'static,
    CE: Debug + Display + error::Error + 'static,
    N: Debug + Display + 'static,
{
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        match &self {
            Error::DataSource(e) => Some(e),
            Error::CommitmentTree(e) => Some(e),
            Error::NoteSelection(e) => Some(e),
            Error::Proposal(e) => Some(e),
            Error::Builder(e) => Some(e),
            #[cfg(feature = "pczt")]
            Error::Pczt(e) => Some(e),
            _ => None,
        }
    }
}

#[cfg(feature = "pczt")]
impl error::Error for PcztError {}

impl<DE, TE, SE, FE, CE, N> From<builder::Error<FE>> for Error<DE, TE, SE, FE, CE, N> {
    fn from(e: builder::Error<FE>) -> Self {
        Error::Builder(e)
    }
}

impl<DE, TE, SE, FE, CE, N> From<ProposalError> for Error<DE, TE, SE, FE, CE, N> {
    fn from(e: ProposalError) -> Self {
        Error::Proposal(e)
    }
}

impl<DE, TE, SE, FE, CE, N> From<BalanceError> for Error<DE, TE, SE, FE, CE, N> {
    fn from(e: BalanceError) -> Self {
        Error::BalanceError(e)
    }
}

impl<DE, TE, SE, FE, CE, N> From<ConversionError<&'static str>> for Error<DE, TE, SE, FE, CE, N> {
    fn from(value: ConversionError<&'static str>) -> Self {
        Error::Address(value)
    }
}

impl<DE, TE, SE, FE, CE, N> From<InputSelectorError<DE, SE, CE, N>>
    for Error<DE, TE, SE, FE, CE, N>
{
    fn from(e: InputSelectorError<DE, SE, CE, N>) -> Self {
        match e {
            InputSelectorError::DataSource(e) => Error::DataSource(e),
            InputSelectorError::Selection(e) => Error::NoteSelection(e),
            InputSelectorError::Change(e) => Error::Change(e),
            InputSelectorError::Proposal(e) => Error::Proposal(e),
            InputSelectorError::InsufficientFunds {
                available,
                required,
            } => Error::InsufficientFunds {
                available,
                required,
            },
            InputSelectorError::SyncRequired => Error::ScanRequired,
            InputSelectorError::Address(e) => Error::Address(e),
        }
    }
}

impl<DE, TE, SE, FE, CE, N> From<sapling::builder::Error> for Error<DE, TE, SE, FE, CE, N> {
    fn from(e: sapling::builder::Error) -> Self {
        Error::Builder(builder::Error::SaplingBuild(e))
    }
}

impl<DE, TE, SE, FE, CE, N> From<transparent::builder::Error> for Error<DE, TE, SE, FE, CE, N> {
    fn from(e: ::transparent::builder::Error) -> Self {
        Error::Builder(builder::Error::TransparentBuild(e))
    }
}

impl<DE, TE, SE, FE, CE, N> From<ShardTreeError<TE>> for Error<DE, TE, SE, FE, CE, N> {
    fn from(e: ShardTreeError<TE>) -> Self {
        Error::CommitmentTree(e)
    }
}

#[cfg(feature = "pczt")]
impl<DE, TE, SE, FE, CE, N> From<PcztError> for Error<DE, TE, SE, FE, CE, N> {
    fn from(e: PcztError) -> Self {
        Error::Pczt(e)
    }
}

#[cfg(feature = "pczt")]
impl<DE, TE, SE, FE, CE, N> From<pczt::roles::io_finalizer::Error>
    for Error<DE, TE, SE, FE, CE, N>
{
    fn from(e: pczt::roles::io_finalizer::Error) -> Self {
        Error::Pczt(PcztError::IoFinalization(e))
    }
}

#[cfg(feature = "pczt")]
impl<DE, TE, SE, FE, CE, N> From<pczt::roles::updater::OrchardError>
    for Error<DE, TE, SE, FE, CE, N>
{
    fn from(e: pczt::roles::updater::OrchardError) -> Self {
        Error::Pczt(PcztError::UpdateOrchard(e))
    }
}

#[cfg(feature = "pczt")]
impl<DE, TE, SE, FE, CE, N> From<pczt::roles::updater::SaplingError>
    for Error<DE, TE, SE, FE, CE, N>
{
    fn from(e: pczt::roles::updater::SaplingError) -> Self {
        Error::Pczt(PcztError::UpdateSapling(e))
    }
}

#[cfg(feature = "pczt")]
impl<DE, TE, SE, FE, CE, N> From<pczt::roles::updater::TransparentError>
    for Error<DE, TE, SE, FE, CE, N>
{
    fn from(e: pczt::roles::updater::TransparentError) -> Self {
        Error::Pczt(PcztError::UpdateTransparent(e))
    }
}

#[cfg(feature = "pczt")]
impl<DE, TE, SE, FE, CE, N> From<pczt::roles::spend_finalizer::Error>
    for Error<DE, TE, SE, FE, CE, N>
{
    fn from(e: pczt::roles::spend_finalizer::Error) -> Self {
        Error::Pczt(PcztError::SpendFinalization(e))
    }
}

#[cfg(feature = "pczt")]
impl<DE, TE, SE, FE, CE, N> From<pczt::roles::tx_extractor::Error>
    for Error<DE, TE, SE, FE, CE, N>
{
    fn from(e: pczt::roles::tx_extractor::Error) -> Self {
        Error::Pczt(PcztError::Extraction(e))
    }
}

/// Errors that may occur when resolving the account controlling an address.
#[derive(Debug)]
#[non_exhaustive]
pub enum FindAccountForAddressError<E> {
    /// Error returned by the underlying wallet backend.
    Backend(E),

    /// A Unified Address whose receivers map to different accounts.
    UnifiedAddressConflict,
}

impl<E> From<E> for FindAccountForAddressError<E> {
    fn from(err: E) -> Self {
        Self::Backend(err)
    }
}

impl<E: Display> Display for FindAccountForAddressError<E> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FindAccountForAddressError::Backend(e) => {
                write!(f, "Wallet backend error: {e}")
            }
            FindAccountForAddressError::UnifiedAddressConflict => write!(
                f,
                "Receivers of the provided Unified Address map to different wallet accounts."
            ),
        }
    }
}

impl<E: error::Error + 'static> error::Error for FindAccountForAddressError<E> {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        match self {
            FindAccountForAddressError::Backend(e) => Some(e),
            FindAccountForAddressError::UnifiedAddressConflict => None,
        }
    }
}

pub use super::locking::LockError;