Skip to main content

zcash_client_sqlite/
lib.rs

1//! *An SQLite-based Zcash light client.*
2//!
3//! `zcash_client_sqlite` contains complete SQLite-based implementations of the [`WalletRead`],
4//! [`WalletWrite`], and [`BlockSource`] traits from the [`zcash_client_backend`] crate. In
5//! combination with [`zcash_client_backend`], it provides a full implementation of a SQLite-backed
6//! client for the Zcash network.
7//!
8//! # Design
9//!
10//! The light client is built around two SQLite databases:
11//!
12//! - A cache database, used to inform the light client about new [`CompactBlock`]s. It is
13//!   read-only within all light client APIs *except* for [`init_cache_database`] which
14//!   can be used to initialize the database.
15//!
16//! - A data database, where the light client's state is stored. It is read-write within
17//!   the light client APIs, and **assumed to be read-only outside these APIs**. Callers
18//!   **MUST NOT** write to the database without using these APIs. Callers **MAY** read
19//!   the database directly in order to extract information for display to users.
20//!
21//! ## Feature flags
22#![doc = document_features::document_features!()]
23//!
24//! [`WalletRead`]: zcash_client_backend::data_api::WalletRead
25//! [`WalletWrite`]: zcash_client_backend::data_api::WalletWrite
26//! [`BlockSource`]: zcash_client_backend::data_api::chain::BlockSource
27//! [`CompactBlock`]: zcash_client_backend::proto::compact_formats::CompactBlock
28//! [`init_cache_database`]: crate::chain::init::init_cache_database
29
30#![cfg_attr(docsrs, feature(doc_cfg))]
31#![cfg_attr(docsrs, doc(auto_cfg))]
32// Catch documentation errors caused by code changes.
33#![deny(rustdoc::broken_intra_doc_links)]
34#![deny(missing_docs)]
35
36use incrementalmerkletree::Position;
37use nonempty::NonEmpty;
38use rand_core::Rng;
39use secrecy::{ExposeSecret, SecretVec};
40use shardtree::{ShardTree, error::ShardTreeError, store::ShardStore};
41use std::{
42    borrow::{Borrow, BorrowMut},
43    cmp::{max, min},
44    collections::{HashMap, HashSet},
45    convert::AsRef,
46    fmt,
47    num::NonZeroU32,
48    ops::Range,
49    path::Path,
50};
51use subtle::ConditionallySelectable;
52use tracing::warn;
53use util::Clock;
54use uuid::Uuid;
55
56use zcash_client_backend::{
57    TransferType,
58    data_api::{
59        self, Account, AccountBirthday, AccountMeta, AccountPurpose, AccountSource, AddressInfo,
60        BlockMetadata, ConsolidationNotes, DecryptedTransaction, InputSource, NoteFilter,
61        NullifierQuery, OutputLockStore, ReceivedNotes, ReceivedTransactionOutput,
62        SAPLING_SHARD_HEIGHT, ScannedBlock, SeedRelevance, SentTransaction, TargetValue,
63        TransactionDataRequest, WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite,
64        Zip32Derivation,
65        anchor_retention::{AnchorRetention, AnchorRetentionInterval},
66        chain::{BlockSource, ChainState, CommitmentTreeRoot},
67        error::{FindAccountForAddressError, LockError, RewindError},
68        ll::{
69            self, LowLevelWalletRead, LowLevelWalletWrite, ReceivedSaplingOutput,
70            wallet::store_decrypted_tx,
71        },
72        scanning::{ScanPriority, ScanRange},
73        wallet::{ConfirmationsPolicy, TargetHeight, input_selection::LockFilter},
74    },
75    proto::compact_formats::CompactBlock,
76    wallet::{LockOwner, Note, NoteId, OutputRef, ReceivedNote, WalletTransparentOutput, WalletTx},
77};
78use zcash_keys::{
79    address::UnifiedAddress,
80    keys::{
81        AddressGenerationError::*, ReceiverRequirement, UnifiedAddressRequest,
82        UnifiedFullViewingKey, UnifiedSpendingKey,
83    },
84};
85use zcash_primitives::{
86    block::BlockHash,
87    transaction::{Transaction, TxId},
88};
89use zcash_protocol::{
90    ShieldedPool,
91    consensus::{self, BlockHeight, TxIndex},
92    memo::Memo,
93    value::Zatoshis,
94};
95use zip32::{DiversifierIndex, fingerprint::SeedFingerprint};
96
97use crate::{
98    error::SqliteClientError,
99    wallet::{chain_tip_height, commitment_tree::SqliteShardStore},
100};
101use wallet::{
102    SubtreeProgressEstimator,
103    commitment_tree::{self, put_shard_roots},
104    common::{TableConstants, unspent_notes_meta},
105    scanning::replace_queue_entries,
106    upsert_address,
107};
108
109#[cfg(feature = "orchard")]
110use zcash_client_backend::data_api::{
111    IRONWOOD_SHARD_HEIGHT, ORCHARD_SHARD_HEIGHT, ll::ReceivedOrchardOutput,
112};
113
114#[cfg(feature = "transparent-inputs")]
115use {
116    crate::wallet::transparent::ephemeral::schedule_ephemeral_address_checks,
117    ::transparent::{
118        address::TransparentAddress,
119        bundle::OutPoint,
120        keys::{NonHardenedChildIndex, TransparentKeyScope},
121    },
122    ReceiverRequirement::*,
123    std::time::SystemTime,
124    zcash_client_backend::{
125        data_api::{
126            CoinbaseFilter, TransactionsInvolvingAddress, TransparentBalances,
127            ll::wallet::generate_transparent_gap_addresses,
128        },
129        fees::StandardFeeRule,
130        wallet::TransparentAddressMetadata,
131    },
132    zcash_keys::keys::transparent::gap_limits::{AddressStore, GapLimits},
133};
134
135// `AddressCodec` is used only by `find_account_for_ephemeral_address`, which is
136// part of the `WalletTest` surface.
137#[cfg(all(
138    any(test, feature = "test-dependencies"),
139    feature = "transparent-inputs"
140))]
141use zcash_keys::encoding::AddressCodec;
142
143#[cfg(any(test, feature = "test-dependencies"))]
144use {
145    crate::wallet::encoding::pool_code,
146    rusqlite::named_params,
147    zcash_client_backend::data_api::{OutputOfSentTx, WalletTest, testing::TransactionSummary},
148};
149
150#[cfg(any(test, feature = "test-dependencies", feature = "transparent-inputs"))]
151use {crate::wallet::encoding::KeyScope, zcash_keys::address::Address};
152
153#[cfg(any(test, feature = "test-dependencies", not(feature = "orchard")))]
154use zcash_protocol::PoolType;
155
156use rusqlite::hooks::{AuthAction, Authorization};
157#[cfg(feature = "unstable")]
158use {
159    crate::chain::{BlockMeta, fsblockdb_with_blocks},
160    std::{fs, io, path::PathBuf},
161};
162
163pub mod chain;
164pub mod error;
165pub mod util;
166pub mod wallet;
167#[cfg(feature = "zewif")]
168pub mod zewif;
169
170#[cfg(any(test, feature = "test-dependencies"))]
171pub mod testing;
172
173/// The maximum number of blocks the wallet is allowed to rewind. This is
174/// consistent with the bound in zcashd, and allows block data deeper than
175/// this delta from the chain tip to be pruned.
176pub(crate) const PRUNING_DEPTH: u32 = 100;
177
178/// The number of blocks to verify ahead when the chain tip is updated.
179pub(crate) const VERIFY_LOOKAHEAD: u32 = 10;
180
181// The Orchard and Ironwood tables exist in the schema (and so may be named in queries)
182// regardless of whether the `orchard` feature is enabled; they are only written to when it
183// is.
184pub(crate) const SAPLING_TABLES_PREFIX: &str = "sapling";
185pub(crate) const ORCHARD_TABLES_PREFIX: &str = "orchard";
186pub(crate) const IRONWOOD_TABLES_PREFIX: &str = "ironwood";
187
188#[cfg(not(feature = "orchard"))]
189pub(crate) const UA_ORCHARD: ReceiverRequirement = ReceiverRequirement::Omit;
190#[cfg(feature = "orchard")]
191pub(crate) const UA_ORCHARD: ReceiverRequirement = ReceiverRequirement::Require;
192
193#[cfg(not(feature = "transparent-inputs"))]
194pub(crate) const UA_TRANSPARENT: ReceiverRequirement = ReceiverRequirement::Omit;
195#[cfg(feature = "transparent-inputs")]
196pub(crate) const UA_TRANSPARENT: ReceiverRequirement = ReceiverRequirement::Require;
197
198/// Unique identifier for a specific account tracked by a [`WalletDb`].
199///
200/// Account identifiers are "one-way stable": a given identifier always points to a
201/// specific viewing key within a specific [`WalletDb`] instance, but the same viewing key
202/// may have multiple account identifiers over time. In particular, this crate upholds the
203/// following properties:
204///
205/// - When an account starts being tracked within a [`WalletDb`] instance (via APIs like
206///   [`WalletWrite::create_account`], [`WalletWrite::import_account_hd`], or
207///   [`WalletWrite::import_account_ufvk`]), a new `AccountUuid` is generated.
208/// - If an `AccountUuid` is present within a [`WalletDb`], it always points to the same
209///   account.
210///
211/// What this means is that account identifiers are not stable across "wallet recreation
212/// events". Examples of these include:
213/// - Restoring a wallet from a backed-up seed.
214/// - Importing the same viewing key into two different wallet instances.
215#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Hash)]
216#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
217pub struct AccountUuid(#[cfg_attr(feature = "serde", serde(with = "uuid::serde::compact"))] Uuid);
218
219impl ConditionallySelectable for AccountUuid {
220    fn conditional_select(a: &Self, b: &Self, choice: subtle::Choice) -> Self {
221        AccountUuid(Uuid::from_u128(
222            ConditionallySelectable::conditional_select(&a.0.as_u128(), &b.0.as_u128(), choice),
223        ))
224    }
225}
226
227impl AccountUuid {
228    /// Constructs an `AccountUuid` from a bare [`Uuid`] value.
229    ///
230    /// The resulting identifier is not guaranteed to correspond to any account stored in
231    /// a [`WalletDb`].
232    pub fn from_uuid(value: Uuid) -> Self {
233        AccountUuid(value)
234    }
235
236    /// Exposes the opaque account identifier from its typesafe wrapper.
237    pub fn expose_uuid(&self) -> Uuid {
238        self.0
239    }
240}
241
242/// A typesafe wrapper for the primary key identifier for a row in the `accounts` table.
243///
244/// This is an ephemeral value for efficiently and generically working with accounts in a
245/// [`WalletDb`]. To reference accounts in external contexts, use [`AccountUuid`].
246#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Hash)]
247pub struct AccountRef(i64);
248
249/// This implementation is retained under `#[cfg(test)]` for pre-AccountUuid testing.
250#[cfg(test)]
251impl ConditionallySelectable for AccountRef {
252    fn conditional_select(a: &Self, b: &Self, choice: subtle::Choice) -> Self {
253        AccountRef(ConditionallySelectable::conditional_select(
254            &a.0, &b.0, choice,
255        ))
256    }
257}
258
259/// An opaque type for received note identifiers.
260#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
261pub struct ReceivedNoteId(pub(crate) ShieldedPool, pub(crate) i64);
262
263impl fmt::Display for ReceivedNoteId {
264    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
265        match self {
266            ReceivedNoteId(protocol, id) => write!(f, "Received {protocol:?} Note: {id}"),
267        }
268    }
269}
270
271/// A newtype wrapper for sqlite primary key values for the utxos table.
272#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
273pub struct UtxoId(pub(crate) i64);
274
275/// A newtype wrapper for sqlite primary key values for the transactions table.
276#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
277pub struct TxRef(pub(crate) i64);
278
279/// A newtype wrapper for sqlite primary key values for the addresses table.
280#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
281struct AddressRef(pub(crate) i64);
282
283/// A wrapper for the SQLite connection to the wallet database, along with a capability to read the
284/// system from the clock. A `WalletDb` encapsulates the full set of capabilities that are required
285/// in order to implement the [`WalletRead`], [`WalletWrite`] and [`WalletCommitmentTrees`] traits.
286pub struct WalletDb<C, P, CL, R> {
287    conn: C,
288    params: P,
289    clock: CL,
290    rng: R,
291    anchor_retention_interval: AnchorRetentionInterval,
292    #[cfg(feature = "transparent-inputs")]
293    gap_limits: GapLimits,
294}
295
296/// A wrapper for a SQLite transaction affecting the wallet database.
297pub struct SqlTransaction<'conn>(&'conn rusqlite::Transaction<'conn>);
298
299impl Borrow<rusqlite::Connection> for SqlTransaction<'_> {
300    fn borrow(&self) -> &rusqlite::Connection {
301        self.0
302    }
303}
304
305impl<'a> Borrow<rusqlite::Transaction<'a>> for SqlTransaction<'a> {
306    fn borrow(&self) -> &rusqlite::Transaction<'a> {
307        self.0
308    }
309}
310
311/// The prefix reserved for schema (tables, indices, views, ...) created by external
312/// migrations.
313///
314/// The `zcash_client_sqlite` schema promises never to use this prefix for any of its own
315/// names, so any object whose name begins with it is owned by an application rather than
316/// by the wallet.
317const EXTENSION_SCHEMA_PREFIX: &str = "ext_";
318
319/// A restricted statement executor for writing to application-owned extension tables
320/// within a wallet database transaction.
321///
322/// A handle of this type is provided by [`WalletDb::transactionally_with_extension`]
323/// alongside the wallet handle, and shares the same database transaction: statements run
324/// through it either commit atomically with the wallet operations performed in the same
325/// closure, or are rolled back together with them.
326///
327/// # Authorization policy
328///
329/// Every statement executed through this type runs under a SQLite authorizer that is
330/// installed only for the duration of that single statement. The authorizer:
331///
332/// - **allows** reads (`SELECT`, and reads of individual rows and columns) against any
333///   table, so that extension statements may reference wallet data (for example, to
334///   satisfy a foreign key into an account row);
335/// - **allows** `INSERT`, `UPDATE`, and `DELETE` only against tables whose names begin
336///   with the `ext_` prefix reserved for external migrations (see
337///   [`WalletMigrator::with_external_migrations`]); and
338/// - **denies** everything else, including all schema changes (DDL), `PRAGMA`,
339///   `ATTACH`/`DETACH`, and transaction-control actions (`BEGIN`, `COMMIT`, `ROLLBACK`,
340///   `SAVEPOINT`, `RELEASE`), so that extension statements cannot alter the wallet schema
341///   or interfere with the enclosing transaction.
342///
343/// Because writes are restricted to the `ext_` prefix, a statement that inserts into an
344/// `AUTOINCREMENT` extension table is denied: SQLite services `AUTOINCREMENT` by writing
345/// to the internal `sqlite_sequence` table, which does not carry the prefix. Extension
346/// tables that must be written through this API should therefore avoid `AUTOINCREMENT`
347/// (an ordinary `INTEGER PRIMARY KEY` rowid, or an explicitly supplied key, works
348/// without it).
349///
350/// [`WalletMigrator::with_external_migrations`]: crate::wallet::init::WalletMigrator::with_external_migrations
351pub struct ExtensionTransaction<'conn> {
352    conn: &'conn rusqlite::Connection,
353}
354
355/// Removes the extension authorizer from a connection when dropped, ensuring the wallet's
356/// own statements are never subject to it (including when an extension statement fails).
357struct AuthorizerGuard<'conn> {
358    conn: &'conn rusqlite::Connection,
359}
360
361impl Drop for AuthorizerGuard<'_> {
362    fn drop(&mut self) {
363        self.conn.authorizer(
364            None::<fn(rusqlite::hooks::AuthContext<'_>) -> rusqlite::hooks::Authorization>,
365        );
366    }
367}
368
369/// The authorizer callback enforcing the [`ExtensionTransaction`] policy.
370fn extension_authorizer(ctx: rusqlite::hooks::AuthContext<'_>) -> rusqlite::hooks::Authorization {
371    let allow_if_extension = |table: &str| {
372        if table.starts_with(EXTENSION_SCHEMA_PREFIX) {
373            Authorization::Allow
374        } else {
375            Authorization::Deny
376        }
377    };
378
379    match ctx.action {
380        // Reads are permitted everywhere so that extension statements may reference wallet
381        // data (e.g. account foreign keys). `Function` and `Recursive` accompany read-only
382        // expression and CTE evaluation.
383        AuthAction::Select
384        | AuthAction::Read { .. }
385        | AuthAction::Function { .. }
386        | AuthAction::Recursive => Authorization::Allow,
387        // Writes are restricted to application-owned extension tables.
388        AuthAction::Insert { table_name } | AuthAction::Delete { table_name } => {
389            allow_if_extension(table_name)
390        }
391        AuthAction::Update { table_name, .. } => allow_if_extension(table_name),
392        // Everything else (DDL, PRAGMA, ATTACH/DETACH, transaction control, ...) is denied.
393        _ => Authorization::Deny,
394    }
395}
396
397impl<'conn> ExtensionTransaction<'conn> {
398    /// Runs `f` with the extension authorizer installed on the connection, removing it
399    /// again (even on error or panic) before returning.
400    fn with_authorizer<T>(
401        &self,
402        f: impl FnOnce() -> Result<T, rusqlite::Error>,
403    ) -> Result<T, rusqlite::Error> {
404        self.conn.authorizer(Some(extension_authorizer));
405        let _guard = AuthorizerGuard { conn: self.conn };
406        f()
407    }
408
409    /// Executes a single non-query SQL statement against an extension table, returning the
410    /// number of rows that were changed.
411    ///
412    /// The statement is subject to the authorization policy documented on
413    /// [`ExtensionTransaction`]; a statement that touches wallet-owned schema, or that
414    /// attempts a denied action, fails with an authorization error and makes no changes.
415    pub fn execute(
416        &self,
417        sql: &str,
418        params: impl rusqlite::Params,
419    ) -> Result<usize, rusqlite::Error> {
420        self.with_authorizer(|| self.conn.execute(sql, params))
421    }
422
423    /// Executes a SQL query that is expected to return a single row, and applies `f` to
424    /// that row to produce a result.
425    ///
426    /// The statement is subject to the authorization policy documented on
427    /// [`ExtensionTransaction`]. As with [`rusqlite::Connection::query_row`], this returns
428    /// [`rusqlite::Error::QueryReturnedNoRows`] if the query selects no rows.
429    pub fn query_row<T, F>(
430        &self,
431        sql: &str,
432        params: impl rusqlite::Params,
433        f: F,
434    ) -> Result<T, rusqlite::Error>
435    where
436        F: FnOnce(&rusqlite::Row<'_>) -> Result<T, rusqlite::Error>,
437    {
438        self.with_authorizer(|| self.conn.query_row(sql, params, f))
439    }
440}
441
442impl<C, P, CL, R> WalletDb<C, P, CL, R> {
443    /// Returns the network parameters that this walletdb instance is bound to.
444    pub fn params(&self) -> &P {
445        &self.params
446    }
447}
448
449impl<P, CL, R> WalletDb<rusqlite::Connection, P, CL, R> {
450    /// Construct a [`WalletDb`] instance that connects to the wallet database stored at the
451    /// specified path.
452    ///
453    /// ## Parameters
454    /// - `path`: The path to the SQLite database used to store wallet data.
455    /// - `params`: Parameters associated with the Zcash network that the wallet will connect to.
456    /// - `clock`: The clock to use in the case that the backend needs access to the system time.
457    /// - `rng`: The random number generation capability to be exposed by the created `WalletDb`
458    ///   instance.
459    pub fn for_path<F: AsRef<Path>>(
460        path: F,
461        params: P,
462        clock: CL,
463        rng: R,
464    ) -> Result<Self, rusqlite::Error> {
465        rusqlite::Connection::open(path).and_then(move |conn| {
466            rusqlite::vtab::array::load_module(&conn)?;
467            Ok(WalletDb {
468                conn,
469                params,
470                clock,
471                rng,
472                anchor_retention_interval: AnchorRetentionInterval::default(),
473                #[cfg(feature = "transparent-inputs")]
474                gap_limits: GapLimits::default(),
475            })
476        })
477    }
478}
479
480impl<C, P, CL, R> WalletDb<C, P, CL, R> {
481    /// Sets the interval on which this wallet retains note commitment tree checkpoints as durable
482    /// anchors, exempt from ordinary checkpoint pruning.
483    ///
484    /// A ZIP 318 pool migration planned over this wallet reads the interval back through
485    /// [`WalletRead::anchor_retention_interval`] and draws its transfers' anchors from the same
486    /// grid, so the two cannot disagree.
487    ///
488    /// This setting is not persisted, but it does not need to be: once a migration is committed,
489    /// the grid it was committed under is recorded with it, and this wallet keeps retaining that
490    /// grid's boundaries for as long as the migration is in flight, whatever it is currently
491    /// configured with. Reopening the wallet without reapplying a non-default interval therefore
492    /// cannot strand an in-flight migration; it only affects what grid the NEXT migration is
493    /// planned against.
494    ///
495    /// The default is [`AnchorRetentionInterval::ZIP_318`], which every wallet on the production
496    /// network must use.
497    pub fn with_anchor_retention_interval(mut self, interval: AnchorRetentionInterval) -> Self {
498        self.set_anchor_retention_interval(interval);
499        self
500    }
501
502    /// Sets the anchor retention interval on an existing handle; see
503    /// [`Self::with_anchor_retention_interval`], of which this is the by-reference form.
504    pub fn set_anchor_retention_interval(&mut self, interval: AnchorRetentionInterval) {
505        self.anchor_retention_interval = interval;
506    }
507}
508
509#[cfg(feature = "transparent-inputs")]
510impl<C, P, CL, R> WalletDb<C, P, CL, R> {
511    /// Sets the gap limits to be used by the wallet in transparent address generation.
512    pub fn with_gap_limits(mut self, gap_limits: GapLimits) -> Self {
513        self.gap_limits = gap_limits;
514        self
515    }
516}
517
518impl<C: Borrow<rusqlite::Connection>, P, CL, R> WalletDb<C, P, CL, R> {
519    /// Constructs a new wrapper around the given connection.
520    ///
521    /// This is provided for use cases such as connection pooling, where `conn` may be an
522    /// `&mut rusqlite::Connection`.
523    ///
524    /// The caller must ensure that [`rusqlite::vtab::array::load_module`] has been called
525    /// on the connection.
526    ///
527    /// ## Parameters
528    /// - `conn`: A connection to the wallet database.
529    /// - `params`: Parameters associated with the Zcash network that the wallet will connect to.
530    /// - `clock`: The clock to use in the case that the backend needs access to the system time.
531    /// - `rng`: The random number generation capability to be exposed by the created `WalletDb`
532    ///   instance.
533    pub fn from_connection(conn: C, params: P, clock: CL, rng: R) -> Self {
534        WalletDb {
535            conn,
536            params,
537            clock,
538            rng,
539            anchor_retention_interval: AnchorRetentionInterval::default(),
540            #[cfg(feature = "transparent-inputs")]
541            gap_limits: GapLimits::default(),
542        }
543    }
544}
545
546impl<C: BorrowMut<rusqlite::Connection>, P, CL, R> WalletDb<C, P, CL, R> {
547    /// Performs several wallet database operations atomically.
548    ///
549    /// This has two main uses:
550    /// - Ensuring that several [`WalletRead`] and/or [`WalletWrite`] operations either
551    ///   all succeed, or nothing happens. If an error occurs inside the given function,
552    ///   any operations completed by it are rolled back.
553    /// - Amortizing the cost of database transactionality. If several identical
554    ///   operations are planned in sequence (e.g. [`WalletWrite::store_decrypted_tx`]),
555    ///   this function can be used to avoid the overhead of a separate database
556    ///   transaction per insert.
557    pub fn transactionally<F, A, E: From<rusqlite::Error>>(&mut self, f: F) -> Result<A, E>
558    where
559        F: FnOnce(&mut WalletDb<SqlTransaction<'_>, &P, &CL, &mut R>) -> Result<A, E>,
560    {
561        let tx = self.conn.borrow_mut().transaction()?;
562        let mut wdb = WalletDb {
563            conn: SqlTransaction(&tx),
564            params: &self.params,
565            clock: &self.clock,
566            rng: &mut self.rng,
567            anchor_retention_interval: self.anchor_retention_interval,
568            #[cfg(feature = "transparent-inputs")]
569            gap_limits: self.gap_limits,
570        };
571        let result = f(&mut wdb)?;
572        tx.commit()?;
573        Ok(result)
574    }
575
576    /// Performs wallet database operations and writes to application-owned extension tables
577    /// atomically within a single database transaction.
578    ///
579    /// This behaves like [`WalletDb::transactionally`], but additionally provides an
580    /// [`ExtensionTransaction`] handle sharing the same transaction. This allows an
581    /// application to pair a wallet operation (such as importing an account) with writes to
582    /// its own tables created via [`WalletMigrator::with_external_migrations`], so that
583    /// either both take effect or neither does.
584    ///
585    /// The extension handle restricts the statements it will execute; see
586    /// [`ExtensionTransaction`] for the exact authorization policy. In particular, writes
587    /// are permitted only against tables whose names begin with the `ext_` prefix.
588    ///
589    /// # Examples
590    ///
591    /// ```ignore
592    /// wallet_db.transactionally_with_extension(|wdb, ext| {
593    ///     let account = wdb.import_account_ufvk(
594    ///         "external account",
595    ///         &ufvk,
596    ///         &birthday,
597    ///         AccountPurpose::ViewOnly,
598    ///         None,
599    ///     )?;
600    ///     ext.execute(
601    ///         "INSERT INTO ext_myapp_accounts (account_uuid, label) VALUES (?1, ?2)",
602    ///         (account.id().expose_uuid(), "external account"),
603    ///     )?;
604    ///     Ok::<_, SqliteClientError>(account)
605    /// })?;
606    /// ```
607    ///
608    /// [`WalletMigrator::with_external_migrations`]: crate::wallet::init::WalletMigrator::with_external_migrations
609    pub fn transactionally_with_extension<F, A, E: From<rusqlite::Error>>(
610        &mut self,
611        f: F,
612    ) -> Result<A, E>
613    where
614        F: FnOnce(
615            &mut WalletDb<SqlTransaction<'_>, &P, &CL, &mut R>,
616            &ExtensionTransaction<'_>,
617        ) -> Result<A, E>,
618    {
619        let tx = self.conn.borrow_mut().transaction()?;
620        let mut wdb = WalletDb {
621            conn: SqlTransaction(&tx),
622            params: &self.params,
623            clock: &self.clock,
624            rng: &mut self.rng,
625            anchor_retention_interval: self.anchor_retention_interval,
626            #[cfg(feature = "transparent-inputs")]
627            gap_limits: self.gap_limits,
628        };
629        // Both handles hold shared references to the same transaction, so aliasing is fine.
630        let ext = ExtensionTransaction { conn: &tx };
631        let result = f(&mut wdb, &ext)?;
632        tx.commit()?;
633        Ok(result)
634    }
635
636    /// Attempts to construct a witness for each note belonging to the wallet that is believed by
637    /// the wallet to currently be spendable, and returns a vector of the ranges that must be
638    /// rescanned in order to correct missing witness data.
639    ///
640    /// This method is intended for repairing wallets that broke due to bugs in `shardtree`.
641    pub fn check_witnesses(&mut self) -> Result<Vec<Range<BlockHeight>>, SqliteClientError> {
642        self.transactionally(|wdb| {
643            if let Some(anchor_height) = chain_tip_height(wdb.conn.0)? {
644                wallet::commitment_tree::check_witnesses(wdb.conn.0, anchor_height)
645            } else {
646                Ok(vec![])
647            }
648        })
649    }
650
651    /// Updates the scan queue by inserting scan ranges for the given range of block heights, with
652    /// the specified scanning priority.
653    pub fn queue_rescans(
654        &mut self,
655        rescan_ranges: NonEmpty<Range<BlockHeight>>,
656        priority: ScanPriority,
657    ) -> Result<(), SqliteClientError> {
658        let query_range = rescan_ranges
659            .iter()
660            .fold(None, |acc: Option<Range<BlockHeight>>, scan_range| {
661                if let Some(range) = acc {
662                    Some(min(range.start, scan_range.start)..max(range.end, scan_range.end))
663                } else {
664                    Some(scan_range.clone())
665                }
666            })
667            .expect("rescan_ranges is nonempty");
668
669        self.transactionally::<_, _, SqliteClientError>(|wdb| {
670            replace_queue_entries(
671                wdb.conn.0,
672                &query_range,
673                rescan_ranges
674                    .into_iter()
675                    .map(|r| ScanRange::from_parts(r, priority)),
676                true,
677            )
678        })?;
679
680        Ok(())
681    }
682}
683
684#[cfg(feature = "transparent-inputs")]
685impl<C: BorrowMut<rusqlite::Connection>, P, CL: Clock, R: rand::Rng> WalletDb<C, P, CL, R> {
686    /// For each ephemeral address in the wallet, ensure that the transaction data request queue
687    /// contains a request for the wallet to check for UTXOs belonging to that address at some time
688    /// during the next 24-hour period.
689    ///
690    /// We use randomized scheduling of ephemeral address checks to ensure that a
691    /// lightwalletd-compromising adversary cannot use temporal clustering to determine what
692    /// ephemeral addresses belong to a given wallet.
693    pub fn schedule_ephemeral_address_checks(&mut self) -> Result<(), SqliteClientError> {
694        self.borrow_mut().transactionally(|wdb| {
695            schedule_ephemeral_address_checks(wdb.conn.0, wdb.clock, &mut wdb.rng)
696        })
697    }
698}
699
700impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL, R> InputSource
701    for WalletDb<C, P, CL, R>
702{
703    type Error = SqliteClientError;
704    type NoteRef = ReceivedNoteId;
705    type AccountId = AccountUuid;
706
707    fn get_spendable_note(
708        &self,
709        txid: &TxId,
710        protocol: ShieldedPool,
711        index: u32,
712        target_height: TargetHeight,
713        lock_filter: LockFilter<'_>,
714    ) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
715        match protocol {
716            ShieldedPool::Sapling => wallet::sapling::get_spendable_sapling_note(
717                self.conn.borrow(),
718                &self.params,
719                txid,
720                index,
721                target_height,
722                lock_filter,
723            )
724            .map(|opt| opt.map(|n| n.map_note(Note::Sapling))),
725            ShieldedPool::Orchard => {
726                #[cfg(feature = "orchard")]
727                return wallet::orchard::get_spendable_orchard_note(
728                    self.conn.borrow(),
729                    &self.params,
730                    txid,
731                    index,
732                    target_height,
733                    lock_filter,
734                )
735                .map(|opt| {
736                    opt.map(|n| {
737                        n.map_note(|note| Note::Orchard {
738                            note,
739                            pool: ::orchard::ValuePool::Orchard,
740                        })
741                    })
742                });
743
744                #[cfg(not(feature = "orchard"))]
745                return Err(SqliteClientError::UnsupportedPoolType(PoolType::ORCHARD));
746            }
747            ShieldedPool::Ironwood => {
748                #[cfg(feature = "orchard")]
749                return wallet::orchard::get_spendable_ironwood_note(
750                    self.conn.borrow(),
751                    &self.params,
752                    txid,
753                    index,
754                    target_height,
755                    lock_filter,
756                )
757                .map(|opt| {
758                    opt.map(|n| {
759                        n.map_note(|note| Note::Orchard {
760                            note,
761                            pool: ::orchard::ValuePool::Ironwood,
762                        })
763                    })
764                });
765
766                #[cfg(not(feature = "orchard"))]
767                return Err(SqliteClientError::UnsupportedPoolType(PoolType::IRONWOOD));
768            }
769        }
770    }
771
772    fn anchor_computable(
773        &self,
774        protocol: ShieldedPool,
775        height: BlockHeight,
776    ) -> Result<bool, Self::Error> {
777        wallet::anchor_computable(self.conn.borrow(), protocol, height)
778    }
779
780    fn select_spendable_notes(
781        &self,
782        account: Self::AccountId,
783        target_value: TargetValue,
784        sources: &[ShieldedPool],
785        target_height: TargetHeight,
786        confirmations_policy: ConfirmationsPolicy,
787        exclude: &[Self::NoteRef],
788        lock_filter: LockFilter<'_>,
789    ) -> Result<ReceivedNotes<Self::NoteRef>, Self::Error> {
790        Ok(ReceivedNotes::new(
791            if sources.contains(&ShieldedPool::Sapling) {
792                wallet::sapling::select_spendable_sapling_notes(
793                    self.conn.borrow(),
794                    &self.params,
795                    account,
796                    target_value,
797                    target_height,
798                    confirmations_policy,
799                    exclude,
800                    lock_filter,
801                )?
802            } else {
803                vec![]
804            },
805            #[cfg(feature = "orchard")]
806            if sources.contains(&ShieldedPool::Orchard) {
807                wallet::orchard::select_spendable_orchard_notes(
808                    self.conn.borrow(),
809                    &self.params,
810                    account,
811                    target_value,
812                    target_height,
813                    confirmations_policy,
814                    exclude,
815                    lock_filter,
816                )?
817            } else {
818                vec![]
819            },
820            #[cfg(feature = "orchard")]
821            if sources.contains(&ShieldedPool::Ironwood) {
822                wallet::orchard::select_spendable_ironwood_notes(
823                    self.conn.borrow(),
824                    &self.params,
825                    account,
826                    target_value,
827                    target_height,
828                    confirmations_policy,
829                    exclude,
830                    lock_filter,
831                )?
832            } else {
833                vec![]
834            },
835        ))
836    }
837
838    fn select_spendable_notes_for_consolidation(
839        &self,
840        account: Self::AccountId,
841        value: Zatoshis,
842        source: ShieldedPool,
843        target_height: TargetHeight,
844        confirmations_policy: ConfirmationsPolicy,
845        exclude: &[Self::NoteRef],
846        lock_filter: LockFilter<'_>,
847        max_additional_notes: usize,
848    ) -> Result<ConsolidationNotes<Self::NoteRef>, Self::Error> {
849        let (funding, additional) = match source {
850            ShieldedPool::Sapling => {
851                let (funding, additional) =
852                    wallet::sapling::select_spendable_sapling_notes_for_consolidation(
853                        self.conn.borrow(),
854                        &self.params,
855                        account,
856                        value,
857                        target_height,
858                        confirmations_policy,
859                        exclude,
860                        lock_filter,
861                        max_additional_notes,
862                    )?;
863                (
864                    ReceivedNotes::new(
865                        funding,
866                        #[cfg(feature = "orchard")]
867                        vec![],
868                        #[cfg(feature = "orchard")]
869                        vec![],
870                    ),
871                    ReceivedNotes::new(
872                        additional,
873                        #[cfg(feature = "orchard")]
874                        vec![],
875                        #[cfg(feature = "orchard")]
876                        vec![],
877                    ),
878                )
879            }
880            #[cfg(feature = "orchard")]
881            ShieldedPool::Orchard => {
882                let (funding, additional) =
883                    wallet::orchard::select_spendable_orchard_notes_for_consolidation(
884                        self.conn.borrow(),
885                        &self.params,
886                        account,
887                        value,
888                        target_height,
889                        confirmations_policy,
890                        exclude,
891                        lock_filter,
892                        max_additional_notes,
893                    )?;
894                (
895                    ReceivedNotes::new(vec![], funding, vec![]),
896                    ReceivedNotes::new(vec![], additional, vec![]),
897                )
898            }
899            #[cfg(feature = "orchard")]
900            ShieldedPool::Ironwood => {
901                let (funding, additional) =
902                    wallet::orchard::select_spendable_ironwood_notes_for_consolidation(
903                        self.conn.borrow(),
904                        &self.params,
905                        account,
906                        value,
907                        target_height,
908                        confirmations_policy,
909                        exclude,
910                        lock_filter,
911                        max_additional_notes,
912                    )?;
913                (
914                    ReceivedNotes::new(vec![], vec![], funding),
915                    ReceivedNotes::new(vec![], vec![], additional),
916                )
917            }
918            #[cfg(not(feature = "orchard"))]
919            ShieldedPool::Orchard | ShieldedPool::Ironwood => {
920                return Err(SqliteClientError::UnsupportedPoolType(PoolType::Shielded(
921                    source,
922                )));
923            }
924        };
925
926        Ok(ConsolidationNotes::from_parts(funding, additional))
927    }
928
929    fn select_single_spendable_note(
930        &self,
931        account: Self::AccountId,
932        value: Zatoshis,
933        sources: &[ShieldedPool],
934        target_height: TargetHeight,
935        confirmations_policy: ConfirmationsPolicy,
936        exclude: &[Self::NoteRef],
937        lock_filter: LockFilter<'_>,
938    ) -> Result<ReceivedNotes<Self::NoteRef>, Self::Error> {
939        // Pools are tried in the caller's preference order; the first pool holding a covering
940        // note supplies it.
941        for pool in sources {
942            match pool {
943                ShieldedPool::Sapling => {
944                    if let Some(note) = wallet::sapling::select_single_spendable_sapling_note(
945                        self.conn.borrow(),
946                        &self.params,
947                        account,
948                        value,
949                        target_height,
950                        confirmations_policy,
951                        exclude,
952                        lock_filter,
953                    )? {
954                        return Ok(ReceivedNotes::new(
955                            vec![note],
956                            #[cfg(feature = "orchard")]
957                            vec![],
958                            #[cfg(feature = "orchard")]
959                            vec![],
960                        ));
961                    }
962                }
963                #[cfg(feature = "orchard")]
964                ShieldedPool::Orchard => {
965                    if let Some(note) = wallet::orchard::select_single_spendable_orchard_note(
966                        self.conn.borrow(),
967                        &self.params,
968                        account,
969                        value,
970                        target_height,
971                        confirmations_policy,
972                        exclude,
973                        lock_filter,
974                    )? {
975                        return Ok(ReceivedNotes::new(vec![], vec![note], vec![]));
976                    }
977                }
978                #[cfg(feature = "orchard")]
979                ShieldedPool::Ironwood => {
980                    if let Some(note) = wallet::orchard::select_single_spendable_ironwood_note(
981                        self.conn.borrow(),
982                        &self.params,
983                        account,
984                        value,
985                        target_height,
986                        confirmations_policy,
987                        exclude,
988                        lock_filter,
989                    )? {
990                        return Ok(ReceivedNotes::new(vec![], vec![], vec![note]));
991                    }
992                }
993                #[cfg(not(feature = "orchard"))]
994                ShieldedPool::Orchard | ShieldedPool::Ironwood => {}
995            }
996        }
997        Ok(ReceivedNotes::empty())
998    }
999
1000    fn select_unspent_notes(
1001        &self,
1002        account: Self::AccountId,
1003        sources: &[ShieldedPool],
1004        target_height: TargetHeight,
1005        exclude: &[Self::NoteRef],
1006        lock_filter: LockFilter<'_>,
1007    ) -> Result<ReceivedNotes<Self::NoteRef>, Self::Error> {
1008        Ok(ReceivedNotes::new(
1009            if sources.contains(&ShieldedPool::Sapling) {
1010                wallet::common::select_unspent_notes(
1011                    self.conn.borrow(),
1012                    &self.params,
1013                    account,
1014                    target_height,
1015                    ConfirmationsPolicy::MIN,
1016                    exclude,
1017                    ShieldedPool::Sapling,
1018                    wallet::sapling::to_received_note,
1019                    wallet::common::NoteRequest::Unspent,
1020                    lock_filter,
1021                )?
1022            } else {
1023                vec![]
1024            },
1025            #[cfg(feature = "orchard")]
1026            if sources.contains(&ShieldedPool::Orchard) {
1027                wallet::common::select_unspent_notes(
1028                    self.conn.borrow(),
1029                    &self.params,
1030                    account,
1031                    target_height,
1032                    ConfirmationsPolicy::MIN,
1033                    exclude,
1034                    ShieldedPool::Orchard,
1035                    wallet::orchard::to_received_note,
1036                    wallet::common::NoteRequest::Unspent,
1037                    lock_filter,
1038                )?
1039            } else {
1040                vec![]
1041            },
1042            #[cfg(feature = "orchard")]
1043            if sources.contains(&ShieldedPool::Ironwood) {
1044                wallet::common::select_unspent_notes(
1045                    self.conn.borrow(),
1046                    &self.params,
1047                    account,
1048                    target_height,
1049                    ConfirmationsPolicy::MIN,
1050                    exclude,
1051                    ShieldedPool::Ironwood,
1052                    wallet::orchard::to_received_note,
1053                    wallet::common::NoteRequest::Unspent,
1054                    lock_filter,
1055                )?
1056            } else {
1057                vec![]
1058            },
1059        ))
1060    }
1061
1062    #[cfg(feature = "transparent-inputs")]
1063    fn get_unspent_transparent_output(
1064        &self,
1065        outpoint: &OutPoint,
1066        target_height: TargetHeight,
1067    ) -> Result<Option<WalletTransparentOutput<Self::AccountId>>, Self::Error> {
1068        wallet::transparent::get_wallet_transparent_output(
1069            self.conn.borrow(),
1070            outpoint,
1071            Some(target_height),
1072        )
1073    }
1074
1075    #[cfg(feature = "transparent-inputs")]
1076    fn get_spendable_transparent_outputs(
1077        &self,
1078        address: &TransparentAddress,
1079        target_height: TargetHeight,
1080        confirmations_policy: ConfirmationsPolicy,
1081        output_filter: CoinbaseFilter,
1082        lock_filter: LockFilter<'_>,
1083    ) -> Result<Vec<WalletTransparentOutput<Self::AccountId>>, Self::Error> {
1084        wallet::transparent::get_spendable_transparent_outputs(
1085            self.conn.borrow(),
1086            &self.params,
1087            address,
1088            target_height,
1089            confirmations_policy,
1090            output_filter,
1091            lock_filter,
1092        )
1093    }
1094
1095    #[cfg(feature = "transparent-inputs")]
1096    fn get_spendable_transparent_outputs_for_addresses(
1097        &self,
1098        addresses: &[TransparentAddress],
1099        target_height: TargetHeight,
1100        confirmations_policy: ConfirmationsPolicy,
1101        output_filter: CoinbaseFilter,
1102        lock_filter: LockFilter<'_>,
1103    ) -> Result<Vec<WalletTransparentOutput<Self::AccountId>>, Self::Error> {
1104        wallet::transparent::get_spendable_transparent_outputs_for_addresses(
1105            self.conn.borrow(),
1106            &self.params,
1107            addresses,
1108            target_height,
1109            confirmations_policy,
1110            output_filter,
1111            lock_filter,
1112        )
1113    }
1114
1115    #[cfg(feature = "transparent-inputs")]
1116    fn select_spendable_transparent_outputs(
1117        &self,
1118        account: Self::AccountId,
1119        target_height: TargetHeight,
1120        confirmations_policy: ConfirmationsPolicy,
1121        output_filter: CoinbaseFilter,
1122        address_allow_list: Option<&[TransparentAddress]>,
1123        target_value: TargetValue,
1124        max_inputs: usize,
1125        fee_rule: &StandardFeeRule,
1126        lock_filter: LockFilter<'_>,
1127    ) -> Result<Vec<WalletTransparentOutput<Self::AccountId>>, Self::Error> {
1128        wallet::transparent::select_spendable_transparent_outputs(
1129            self.conn.borrow(),
1130            &self.params,
1131            account,
1132            target_height,
1133            confirmations_policy,
1134            output_filter,
1135            address_allow_list,
1136            target_value,
1137            max_inputs,
1138            fee_rule,
1139            lock_filter,
1140        )
1141    }
1142
1143    /// Returns metadata for the spendable notes in the wallet.
1144    fn get_account_metadata(
1145        &self,
1146        account_id: Self::AccountId,
1147        selector: &NoteFilter,
1148        target_height: TargetHeight,
1149        exclude: &[Self::NoteRef],
1150        lock_filter: LockFilter<'_>,
1151    ) -> Result<AccountMeta, Self::Error> {
1152        let sapling_pool_meta = unspent_notes_meta(
1153            self.conn.borrow(),
1154            ShieldedPool::Sapling,
1155            target_height,
1156            account_id,
1157            selector,
1158            exclude,
1159            lock_filter,
1160        )?;
1161
1162        #[cfg(feature = "orchard")]
1163        let orchard_pool_meta = unspent_notes_meta(
1164            self.conn.borrow(),
1165            ShieldedPool::Orchard,
1166            target_height,
1167            account_id,
1168            selector,
1169            exclude,
1170            lock_filter,
1171        )?;
1172        #[cfg(not(feature = "orchard"))]
1173        let orchard_pool_meta = None;
1174
1175        #[cfg(feature = "orchard")]
1176        let ironwood_pool_meta = unspent_notes_meta(
1177            self.conn.borrow(),
1178            ShieldedPool::Ironwood,
1179            target_height,
1180            account_id,
1181            selector,
1182            exclude,
1183            lock_filter,
1184        )?;
1185        #[cfg(not(feature = "orchard"))]
1186        let ironwood_pool_meta = None;
1187
1188        Ok(AccountMeta::new(
1189            sapling_pool_meta,
1190            orchard_pool_meta,
1191            ironwood_pool_meta,
1192        ))
1193    }
1194}
1195
1196impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletRead
1197    for WalletDb<C, P, CL, R>
1198{
1199    type Error = SqliteClientError;
1200    type AccountId = AccountUuid;
1201    type Account = wallet::Account;
1202
1203    fn get_account_ids(&self) -> Result<Vec<Self::AccountId>, Self::Error> {
1204        Ok(wallet::get_account_ids(self.conn.borrow())?)
1205    }
1206
1207    fn get_account(
1208        &self,
1209        account_id: Self::AccountId,
1210    ) -> Result<Option<Self::Account>, Self::Error> {
1211        wallet::get_account(self.conn.borrow(), &self.params, account_id)
1212    }
1213
1214    fn get_derived_account(
1215        &self,
1216        derivation: &Zip32Derivation,
1217    ) -> Result<Option<Self::Account>, Self::Error> {
1218        wallet::get_derived_account(
1219            self.conn.borrow(),
1220            &self.params,
1221            derivation.seed_fingerprint(),
1222            derivation.account_index(),
1223            #[cfg(feature = "zcashd-compat")]
1224            derivation.legacy_address_index(),
1225        )
1226    }
1227
1228    fn validate_seed(
1229        &self,
1230        account_id: Self::AccountId,
1231        seed: &SecretVec<u8>,
1232    ) -> Result<bool, Self::Error> {
1233        if let Some(account) = self.get_account(account_id)? {
1234            if let AccountSource::Derived { derivation, .. } = account.source() {
1235                wallet::seed_matches_derived_account(
1236                    &self.params,
1237                    seed,
1238                    derivation.seed_fingerprint(),
1239                    derivation.account_index(),
1240                    &account.uivk(),
1241                )
1242            } else {
1243                Err(SqliteClientError::UnknownZip32Derivation)
1244            }
1245        } else {
1246            // Missing account is documented to return false.
1247            Ok(false)
1248        }
1249    }
1250
1251    fn seed_relevance_to_derived_accounts(
1252        &self,
1253        seed: &SecretVec<u8>,
1254    ) -> Result<SeedRelevance<Self::AccountId>, Self::Error> {
1255        let mut has_accounts = false;
1256        let mut has_derived = false;
1257        let mut relevant_account_ids = vec![];
1258
1259        for account_id in self.get_account_ids()? {
1260            has_accounts = true;
1261            let account = self.get_account(account_id)?.expect("account ID exists");
1262
1263            // If the account is imported, the seed _might_ be relevant, but the only
1264            // way we could determine that is by brute-forcing the ZIP 32 account
1265            // index space, which we're not going to do. The method name indicates to
1266            // the caller that we only check derived accounts.
1267            if let AccountSource::Derived { derivation, .. } = account.source() {
1268                has_derived = true;
1269
1270                if wallet::seed_matches_derived_account(
1271                    &self.params,
1272                    seed,
1273                    derivation.seed_fingerprint(),
1274                    derivation.account_index(),
1275                    &account.uivk(),
1276                )? {
1277                    // The seed is relevant to this account.
1278                    relevant_account_ids.push(account_id);
1279                }
1280            }
1281        }
1282
1283        Ok(
1284            if let Some(account_ids) = NonEmpty::from_vec(relevant_account_ids) {
1285                SeedRelevance::Relevant { account_ids }
1286            } else if has_derived {
1287                SeedRelevance::NotRelevant
1288            } else if has_accounts {
1289                SeedRelevance::NoDerivedAccounts
1290            } else {
1291                SeedRelevance::NoAccounts
1292            },
1293        )
1294    }
1295
1296    fn get_account_for_ufvk(
1297        &self,
1298        ufvk: &UnifiedFullViewingKey,
1299    ) -> Result<Option<Self::Account>, Self::Error> {
1300        wallet::get_account_for_ufvk(self.conn.borrow(), &self.params, ufvk)
1301    }
1302
1303    fn list_addresses(&self, account: Self::AccountId) -> Result<Vec<AddressInfo>, Self::Error> {
1304        wallet::list_addresses(self.conn.borrow(), &self.params, account)
1305    }
1306
1307    /// Implements this method with a single SQL query, avoiding the O(accounts × addresses)
1308    /// scan that delegating to
1309    /// [`zcash_client_backend::data_api::defaults::find_account_for_address`] would require.
1310    /// See [`zcash_client_backend::data_api::WalletRead::find_account_for_address`] for the
1311    /// semantics.
1312    fn find_account_for_address<Q: consensus::Parameters>(
1313        &self,
1314        params: &Q,
1315        address: &zcash_keys::address::Address,
1316    ) -> Result<Option<Self::AccountId>, FindAccountForAddressError<Self::Error>> {
1317        wallet::find_account_for_address(self.conn.borrow(), params, address)
1318    }
1319
1320    fn get_last_generated_address_matching(
1321        &self,
1322        account: Self::AccountId,
1323        request: UnifiedAddressRequest,
1324    ) -> Result<Option<UnifiedAddress>, Self::Error> {
1325        wallet::get_last_generated_address_matching(
1326            self.conn.borrow(),
1327            &self.params,
1328            account,
1329            request,
1330        )
1331        .map(|res| res.map(|(addr, _)| addr))
1332    }
1333
1334    fn get_account_birthday(&self, account: Self::AccountId) -> Result<BlockHeight, Self::Error> {
1335        wallet::account_birthday(self.conn.borrow(), account)
1336    }
1337
1338    fn get_wallet_birthday(&self) -> Result<Option<BlockHeight>, Self::Error> {
1339        wallet::wallet_birthday(self.conn.borrow()).map_err(SqliteClientError::from)
1340    }
1341
1342    fn get_wallet_recover_until(&self) -> Result<Option<BlockHeight>, Self::Error> {
1343        wallet::wallet_recover_until(self.conn.borrow()).map_err(SqliteClientError::from)
1344    }
1345
1346    fn get_wallet_summary(
1347        &self,
1348        confirmations_policy: ConfirmationsPolicy,
1349    ) -> Result<Option<WalletSummary<Self::AccountId>>, Self::Error> {
1350        // This will return a runtime error if we call `get_wallet_summary` from two
1351        // threads at the same time, as transactions cannot nest.
1352        wallet::get_wallet_summary(
1353            &self.conn.borrow().unchecked_transaction()?,
1354            &self.params,
1355            confirmations_policy,
1356            &SubtreeProgressEstimator,
1357        )
1358    }
1359
1360    fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
1361        wallet::chain_tip_height(self.conn.borrow()).map_err(SqliteClientError::from)
1362    }
1363
1364    fn anchor_retention_interval(&self) -> AnchorRetentionInterval {
1365        self.anchor_retention_interval
1366    }
1367
1368    fn get_block_hash(&self, block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error> {
1369        wallet::get_block_hash(self.conn.borrow(), block_height).map_err(SqliteClientError::from)
1370    }
1371
1372    fn block_metadata(&self, height: BlockHeight) -> Result<Option<BlockMetadata>, Self::Error> {
1373        wallet::block_metadata(self.conn.borrow(), &self.params, height)
1374    }
1375
1376    fn block_fully_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
1377        wallet::block_fully_scanned(self.conn.borrow(), &self.params)
1378    }
1379
1380    fn get_max_height_hash(&self) -> Result<Option<(BlockHeight, BlockHash)>, Self::Error> {
1381        wallet::get_max_height_hash(self.conn.borrow()).map_err(SqliteClientError::from)
1382    }
1383
1384    fn block_max_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
1385        wallet::block_max_scanned(self.conn.borrow(), &self.params)
1386    }
1387
1388    fn suggest_scan_ranges(&self) -> Result<Vec<ScanRange>, Self::Error> {
1389        wallet::scanning::suggest_scan_ranges(
1390            self.conn.borrow(),
1391            ScanPriority::Historic,
1392            wallet::scanning::latest_pool_activation(&self.params),
1393        )
1394    }
1395
1396    fn get_target_and_anchor_heights(
1397        &self,
1398        min_confirmations: NonZeroU32,
1399    ) -> Result<Option<(TargetHeight, BlockHeight)>, Self::Error> {
1400        wallet::get_target_and_anchor_heights(self.conn.borrow(), min_confirmations)
1401    }
1402
1403    fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error> {
1404        wallet::get_tx_height(self.conn.borrow(), txid)
1405    }
1406
1407    fn get_unified_full_viewing_keys(
1408        &self,
1409    ) -> Result<HashMap<Self::AccountId, UnifiedFullViewingKey>, Self::Error> {
1410        wallet::get_unified_full_viewing_keys(self.conn.borrow(), &self.params)
1411    }
1412
1413    fn get_memo(&self, note_id: NoteId) -> Result<Option<Memo>, Self::Error> {
1414        let sent_memo = wallet::get_sent_memo(self.conn.borrow(), note_id)?;
1415        if sent_memo.is_some() {
1416            Ok(sent_memo)
1417        } else {
1418            wallet::get_received_memo(self.conn.borrow(), note_id)
1419        }
1420    }
1421
1422    fn get_transaction(&self, txid: TxId) -> Result<Option<Transaction>, Self::Error> {
1423        wallet::get_transaction(self.conn.borrow(), &self.params, txid)
1424            .map(|res| res.map(|(_, tx)| tx))
1425    }
1426
1427    fn get_sapling_nullifiers(
1428        &self,
1429        query: NullifierQuery,
1430    ) -> Result<Vec<(Self::AccountId, sapling::Nullifier)>, Self::Error> {
1431        wallet::sapling::get_sapling_nullifiers(self.conn.borrow(), query)
1432    }
1433
1434    #[cfg(feature = "orchard")]
1435    fn get_orchard_nullifiers(
1436        &self,
1437        query: NullifierQuery,
1438    ) -> Result<Vec<(Self::AccountId, orchard::note::Nullifier)>, Self::Error> {
1439        wallet::orchard::get_orchard_nullifiers(self.conn.borrow(), query)
1440    }
1441
1442    #[cfg(feature = "orchard")]
1443    fn get_ironwood_nullifiers(
1444        &self,
1445        query: NullifierQuery,
1446    ) -> Result<Vec<(Self::AccountId, orchard::note::Nullifier)>, Self::Error> {
1447        wallet::orchard::get_ironwood_nullifiers(self.conn.borrow(), query)
1448    }
1449
1450    #[cfg(feature = "transparent-inputs")]
1451    fn get_transparent_receivers(
1452        &self,
1453        account: Self::AccountId,
1454        include_change: bool,
1455        include_standalone: bool,
1456    ) -> Result<HashMap<TransparentAddress, TransparentAddressMetadata>, Self::Error> {
1457        let key_scopes = Some(KeyScope::EXTERNAL)
1458            .into_iter()
1459            .chain(include_change.then_some(KeyScope::INTERNAL))
1460            .chain(
1461                (include_standalone && cfg!(feature = "transparent-key-import"))
1462                    .then_some(KeyScope::Foreign),
1463            )
1464            .collect::<Vec<_>>();
1465
1466        wallet::transparent::get_transparent_receivers(
1467            self.conn.borrow(),
1468            &self.params,
1469            &self.gap_limits,
1470            account,
1471            &key_scopes[..],
1472            None,
1473            false,
1474        )
1475    }
1476
1477    #[cfg(feature = "transparent-inputs")]
1478    fn get_ephemeral_transparent_receivers(
1479        &self,
1480        account: Self::AccountId,
1481        exposure_depth: u32,
1482        exclude_used: bool,
1483    ) -> Result<HashMap<TransparentAddress, TransparentAddressMetadata>, Self::Error> {
1484        wallet::transparent::get_transparent_receivers(
1485            self.conn.borrow(),
1486            &self.params,
1487            &self.gap_limits,
1488            account,
1489            &[KeyScope::Ephemeral],
1490            Some(exposure_depth),
1491            exclude_used,
1492        )
1493    }
1494
1495    #[cfg(feature = "transparent-inputs")]
1496    fn get_transparent_balances(
1497        &self,
1498        account: Self::AccountId,
1499        target_height: TargetHeight,
1500        confirmations_policy: ConfirmationsPolicy,
1501    ) -> Result<TransparentBalances, Self::Error> {
1502        wallet::transparent::get_transparent_balances(
1503            self.conn.borrow(),
1504            &self.params,
1505            account,
1506            target_height,
1507            confirmations_policy,
1508        )
1509    }
1510
1511    #[cfg(feature = "transparent-inputs")]
1512    fn get_transparent_address_metadata(
1513        &self,
1514        account: Self::AccountId,
1515        address: &TransparentAddress,
1516    ) -> Result<Option<TransparentAddressMetadata>, Self::Error> {
1517        wallet::transparent::get_transparent_address_metadata(
1518            self.conn.borrow(),
1519            &self.params,
1520            &self.gap_limits,
1521            account,
1522            address,
1523        )
1524    }
1525
1526    #[cfg(feature = "transparent-inputs")]
1527    fn utxo_query_height(&self, account: Self::AccountId) -> Result<BlockHeight, Self::Error> {
1528        let account_ref = wallet::get_account_ref(self.conn.borrow(), account)?;
1529        wallet::transparent::utxo_query_height(self.conn.borrow(), account_ref, &self.gap_limits)
1530    }
1531
1532    fn transaction_data_requests(&self) -> Result<Vec<TransactionDataRequest>, Self::Error> {
1533        if let Some(_chain_tip_height) = wallet::chain_tip_height(self.conn.borrow())? {
1534            let iter = wallet::transaction_data_requests(self.conn.borrow())?.into_iter();
1535
1536            #[cfg(feature = "transparent-inputs")]
1537            let iter = iter.chain(wallet::transparent::transaction_data_requests(
1538                self.conn.borrow(),
1539                &self.params,
1540                _chain_tip_height,
1541            )?);
1542
1543            Ok(iter.collect())
1544        } else {
1545            // If the chain tip height is unknown, we're not in a state where it makes sense to process
1546            // transaction data requests anyway so we just return the empty vector of requests.
1547            Ok(vec![])
1548        }
1549    }
1550
1551    fn get_received_outputs(
1552        &self,
1553        txid: TxId,
1554        target_height: TargetHeight,
1555        confirmations_policy: ConfirmationsPolicy,
1556    ) -> Result<Vec<ReceivedTransactionOutput>, Self::Error> {
1557        wallet::get_received_outputs(
1558            self.conn.borrow(),
1559            txid,
1560            target_height,
1561            confirmations_policy,
1562        )
1563    }
1564}
1565
1566#[cfg(any(test, feature = "test-dependencies"))]
1567impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletTest
1568    for WalletDb<C, P, CL, R>
1569{
1570    fn get_tx_history(
1571        &self,
1572    ) -> Result<Vec<TransactionSummary<<Self as WalletRead>::AccountId>>, <Self as WalletRead>::Error>
1573    {
1574        wallet::testing::get_tx_history(self.conn.borrow())
1575    }
1576
1577    fn get_sent_note_ids(
1578        &self,
1579        txid: &TxId,
1580        protocol: ShieldedPool,
1581    ) -> Result<Vec<NoteId>, <Self as WalletRead>::Error> {
1582        let mut stmt_sent_notes = self.conn.borrow().prepare(
1583            "SELECT output_index
1584             FROM sent_notes
1585             JOIN transactions ON transactions.id_tx = sent_notes.transaction_id
1586             WHERE transactions.txid = :txid
1587             AND sent_notes.output_pool = :pool_code",
1588        )?;
1589
1590        let note_ids = stmt_sent_notes
1591            .query_map(
1592                named_params! {
1593                    ":txid": txid.as_ref(),
1594                    ":pool_code": pool_code(PoolType::Shielded(protocol)),
1595                },
1596                |row| Ok(NoteId::new(*txid, protocol, row.get(0)?)),
1597            )?
1598            .collect::<Result<_, _>>()?;
1599
1600        Ok(note_ids)
1601    }
1602
1603    fn get_sent_outputs(
1604        &self,
1605        txid: &TxId,
1606    ) -> Result<Vec<OutputOfSentTx>, <Self as WalletRead>::Error> {
1607        let mut stmt_sent = self.conn.borrow().prepare(
1608            "SELECT value, to_address,
1609                    a.cached_transparent_receiver_address, a.transparent_child_index
1610             FROM sent_notes
1611             JOIN transactions t ON t.id_tx = sent_notes.transaction_id
1612             LEFT JOIN transparent_received_outputs tro ON tro.transaction_id = t.id_tx
1613             LEFT JOIN addresses a ON a.id = tro.address_id AND a.key_scope = :key_scope
1614             WHERE t.txid = :txid
1615             ORDER BY value",
1616        )?;
1617
1618        let sends = stmt_sent
1619            .query_map(
1620                named_params![
1621                    ":txid": txid.as_ref(),
1622                    ":key_scope": KeyScope::Ephemeral.encode()
1623                ],
1624                |row| {
1625                    let v = row.get(0)?;
1626                    let to_address = row.get::<_, Option<String>>(1)?;
1627                    let ephemeral_address = row.get::<_, Option<String>>(2)?;
1628                    let address_index = row.get::<_, Option<u32>>(3)?;
1629                    Ok((v, to_address, ephemeral_address.zip(address_index)))
1630                },
1631            )?
1632            .map(|res| {
1633                let (amount, external_recipient, _ephemeral_address) = res?;
1634                Ok::<_, SqliteClientError>(OutputOfSentTx::from_parts(
1635                    Zatoshis::from_u64(amount)?,
1636                    external_recipient
1637                        .map(|s| {
1638                            Address::decode(&self.params, &s).ok_or_else(|| {
1639                                SqliteClientError::CorruptedData(format!(
1640                                    "invalid transparent address: {s}"
1641                                ))
1642                            })
1643                        })
1644                        .transpose()?,
1645                    #[cfg(feature = "transparent-inputs")]
1646                    _ephemeral_address
1647                        .map(|(addr_str, idx)| {
1648                            let addr =
1649                                Address::decode(&self.params, &addr_str).ok_or_else(|| {
1650                                    SqliteClientError::CorruptedData(format!(
1651                                        "invalid transparent address: {addr_str}"
1652                                    ))
1653                                })?;
1654                            let i = NonHardenedChildIndex::from_index(idx).ok_or_else(|| {
1655                                SqliteClientError::CorruptedData(format!(
1656                                    "invalid non-hardened child index: {idx}"
1657                                ))
1658                            })?;
1659
1660                            Ok::<_, SqliteClientError>((addr, i))
1661                        })
1662                        .transpose()?,
1663                ))
1664            })
1665            .collect::<Result<_, _>>()?;
1666
1667        Ok(sends)
1668    }
1669
1670    fn get_checkpoint_history(
1671        &self,
1672        protocol: &ShieldedPool,
1673    ) -> Result<
1674        Vec<(BlockHeight, Option<incrementalmerkletree::Position>)>,
1675        <Self as WalletRead>::Error,
1676    > {
1677        wallet::testing::get_checkpoint_history(self.conn.borrow(), *protocol)
1678    }
1679
1680    #[cfg(feature = "transparent-inputs")]
1681    fn get_transparent_output(
1682        &self,
1683        outpoint: &OutPoint,
1684        target_height: Option<TargetHeight>,
1685    ) -> Result<
1686        Option<WalletTransparentOutput<<Self as InputSource>::AccountId>>,
1687        <Self as InputSource>::Error,
1688    > {
1689        wallet::transparent::get_wallet_transparent_output(
1690            self.conn.borrow(),
1691            outpoint,
1692            target_height,
1693        )
1694    }
1695
1696    fn get_notes(
1697        &self,
1698        protocol: ShieldedPool,
1699    ) -> Result<Vec<ReceivedNote<Self::NoteRef, Note>>, <Self as InputSource>::Error> {
1700        let (target_height, _) = self
1701            .get_target_and_anchor_heights(NonZeroU32::MIN)?
1702            .ok_or(SqliteClientError::ChainHeightUnknown)?;
1703
1704        let TableConstants {
1705            table_prefix,
1706            output_index_col,
1707            ..
1708        } = wallet::common::table_constants::<<Self as InputSource>::Error>(protocol)?;
1709        let mut stmt_received_notes = self.conn.borrow().prepare(&format!(
1710            "SELECT txid, {output_index_col}
1711             FROM {table_prefix}_received_notes rn
1712             INNER JOIN transactions ON transactions.id_tx = rn.transaction_id
1713             WHERE transactions.block IS NOT NULL
1714             AND recipient_key_scope IS NOT NULL
1715             AND nf IS NOT NULL
1716             AND commitment_tree_position IS NOT NULL"
1717        ))?;
1718
1719        let result = stmt_received_notes
1720            .query_map([], |row| {
1721                let txid: [u8; 32] = row.get("txid")?;
1722                let output_index: u32 = row.get(output_index_col)?;
1723                // The test accessor inspects wallet contents irrespective of lock state.
1724                let lock_filter = LockFilter::Unfiltered;
1725                let note = self
1726                    .get_spendable_note(
1727                        &TxId::from_bytes(txid),
1728                        protocol,
1729                        output_index,
1730                        target_height,
1731                        lock_filter,
1732                    )
1733                    .unwrap()
1734                    .unwrap();
1735                Ok(note)
1736            })?
1737            .collect::<Result<Vec<_>, _>>()?;
1738
1739        Ok(result)
1740    }
1741
1742    #[cfg(feature = "transparent-inputs")]
1743    fn get_known_ephemeral_addresses(
1744        &self,
1745        account: <Self as WalletRead>::AccountId,
1746        index_range: Option<Range<NonHardenedChildIndex>>,
1747    ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
1748    {
1749        let account_id = wallet::get_account_ref(self.conn.borrow(), account)?;
1750        wallet::transparent::ephemeral::get_known_ephemeral_addresses(
1751            self.conn.borrow(),
1752            &self.params,
1753            &self.gap_limits,
1754            account_id,
1755            index_range,
1756        )
1757    }
1758
1759    #[cfg(feature = "transparent-inputs")]
1760    fn find_account_for_ephemeral_address(
1761        &self,
1762        address: &TransparentAddress,
1763    ) -> Result<Option<<Self as WalletRead>::AccountId>, <Self as WalletRead>::Error> {
1764        wallet::transparent::ephemeral::find_account_for_ephemeral_address_str(
1765            self.conn.borrow(),
1766            &address.encode(&self.params),
1767        )
1768    }
1769}
1770
1771impl<C, P, CL, R> OutputLockStore for WalletDb<C, P, CL, R>
1772where
1773    C: BorrowMut<rusqlite::Connection>,
1774    P: consensus::Parameters,
1775    CL: Clock,
1776    R: Rng,
1777{
1778    type Error = SqliteClientError;
1779    type AccountId = AccountUuid;
1780
1781    fn lock_outputs(
1782        &mut self,
1783        outputs: &[OutputRef],
1784        owner: LockOwner,
1785        lock_expiry_height: BlockHeight,
1786    ) -> Result<usize, LockError<Self::Error>> {
1787        Ok(self.transactionally(|wdb| {
1788            wallet::locking::lock_outputs(wdb.conn.0, outputs, owner, lock_expiry_height)
1789        })?)
1790    }
1791
1792    fn unlock_output(&mut self, output: &OutputRef, owner: LockOwner) -> Result<bool, Self::Error> {
1793        self.transactionally(|wdb| wallet::locking::unlock_output(wdb.conn.0, output, owner))
1794    }
1795
1796    fn clear_locked_outputs(&mut self, account: Self::AccountId) -> Result<usize, Self::Error> {
1797        self.transactionally(|wdb| wallet::locking::clear_locked_outputs(wdb.conn.0, account))
1798    }
1799
1800    fn get_locked_outputs(&self, account: Self::AccountId) -> Result<Vec<OutputRef>, Self::Error> {
1801        wallet::locking::get_locked_outputs(self.conn.borrow(), account)
1802    }
1803}
1804
1805impl<C: BorrowMut<rusqlite::Connection>, P: consensus::Parameters, CL: Clock, R: Rng> WalletWrite
1806    for WalletDb<C, P, CL, R>
1807{
1808    type UtxoRef = UtxoId;
1809
1810    fn create_account(
1811        &mut self,
1812        account_name: &str,
1813        seed: &SecretVec<u8>,
1814        birthday: &AccountBirthday,
1815        key_source: Option<&str>,
1816    ) -> Result<(<Self as WalletRead>::AccountId, UnifiedSpendingKey), <Self as WalletRead>::Error>
1817    {
1818        self.borrow_mut()
1819            .transactionally(|wdb| wdb.create_account(account_name, seed, birthday, key_source))
1820    }
1821
1822    fn import_account_hd(
1823        &mut self,
1824        account_name: &str,
1825        seed: &SecretVec<u8>,
1826        account_index: zip32::AccountId,
1827        birthday: &AccountBirthday,
1828        key_source: Option<&str>,
1829    ) -> Result<(Self::Account, UnifiedSpendingKey), <Self as WalletRead>::Error> {
1830        self.transactionally(|wdb| {
1831            wdb.import_account_hd(account_name, seed, account_index, birthday, key_source)
1832        })
1833    }
1834
1835    fn import_account_ufvk(
1836        &mut self,
1837        account_name: &str,
1838        ufvk: &UnifiedFullViewingKey,
1839        birthday: &AccountBirthday,
1840        purpose: AccountPurpose,
1841        key_source: Option<&str>,
1842    ) -> Result<Self::Account, <Self as WalletRead>::Error> {
1843        self.transactionally(|wdb| {
1844            wdb.import_account_ufvk(account_name, ufvk, birthday, purpose, key_source)
1845        })
1846    }
1847
1848    fn delete_account(
1849        &mut self,
1850        account_uuid: <Self as WalletRead>::AccountId,
1851    ) -> Result<(), <Self as WalletRead>::Error> {
1852        self.transactionally(|wdb| wdb.delete_account(account_uuid))
1853    }
1854
1855    #[cfg(feature = "transparent-key-import")]
1856    fn import_standalone_transparent_pubkey(
1857        &mut self,
1858        account: <Self as WalletRead>::AccountId,
1859        pubkey: secp256k1::PublicKey,
1860    ) -> Result<(), <Self as WalletRead>::Error> {
1861        self.transactionally(|wdb| wdb.import_standalone_transparent_pubkey(account, pubkey))
1862    }
1863
1864    #[cfg(feature = "transparent-key-import")]
1865    fn import_standalone_transparent_pubkeys(
1866        &mut self,
1867        account: <Self as WalletRead>::AccountId,
1868        pubkeys: &[secp256k1::PublicKey],
1869    ) -> Result<(), <Self as WalletRead>::Error> {
1870        self.transactionally(|wdb| wdb.import_standalone_transparent_pubkeys(account, pubkeys))
1871    }
1872
1873    #[cfg(feature = "transparent-key-import")]
1874    fn import_standalone_transparent_script(
1875        &mut self,
1876        account: <Self as WalletRead>::AccountId,
1877        script: zcash_script::script::Redeem,
1878    ) -> Result<(), <Self as WalletRead>::Error> {
1879        self.transactionally(|wdb| wdb.import_standalone_transparent_script(account, script))
1880    }
1881
1882    fn get_next_available_address(
1883        &mut self,
1884        account_uuid: <Self as WalletRead>::AccountId,
1885        request: UnifiedAddressRequest,
1886    ) -> Result<Option<(UnifiedAddress, DiversifierIndex)>, <Self as WalletRead>::Error> {
1887        self.transactionally(|wdb| wdb.get_next_available_address(account_uuid, request))
1888    }
1889
1890    fn get_address_for_index(
1891        &mut self,
1892        account: <Self as WalletRead>::AccountId,
1893        diversifier_index: DiversifierIndex,
1894        request: UnifiedAddressRequest,
1895    ) -> Result<Option<UnifiedAddress>, <Self as WalletRead>::Error> {
1896        self.transactionally(|wdb| wdb.get_address_for_index(account, diversifier_index, request))
1897    }
1898
1899    fn update_chain_tip(
1900        &mut self,
1901        tip_height: BlockHeight,
1902    ) -> Result<(), <Self as WalletRead>::Error> {
1903        self.transactionally(|wdb| wdb.update_chain_tip(tip_height))
1904    }
1905
1906    fn prune_scan_queue_below(
1907        &mut self,
1908        height: BlockHeight,
1909        retain_with_priority: Option<ScanPriority>,
1910    ) -> Result<u64, <Self as WalletRead>::Error> {
1911        self.transactionally(|wdb| wdb.prune_scan_queue_below(height, retain_with_priority))
1912    }
1913
1914    #[tracing::instrument(skip_all, fields(height = blocks.first().map(|b| u32::from(b.height())), count = blocks.len()))]
1915    #[allow(clippy::type_complexity)]
1916    fn put_blocks(
1917        &mut self,
1918        from_state: &ChainState,
1919        blocks: Vec<ScannedBlock<<Self as WalletRead>::AccountId>>,
1920    ) -> Result<(), <Self as WalletRead>::Error> {
1921        self.transactionally(|wdb| wdb.put_blocks(from_state, blocks))
1922    }
1923
1924    fn put_received_transparent_utxo(
1925        &mut self,
1926        _output: &WalletTransparentOutput<<Self as WalletRead>::AccountId>,
1927    ) -> Result<Self::UtxoRef, <Self as WalletRead>::Error> {
1928        #[cfg(feature = "transparent-inputs")]
1929        return self.transactionally(|wdb| wdb.put_received_transparent_utxo(_output));
1930
1931        #[cfg(not(feature = "transparent-inputs"))]
1932        panic!(
1933            "The wallet must be compiled with the transparent-inputs feature to use this method."
1934        );
1935    }
1936
1937    fn store_decrypted_tx(
1938        &mut self,
1939        d_tx: DecryptedTransaction<Transaction, <Self as WalletRead>::AccountId>,
1940    ) -> Result<(), <Self as WalletRead>::Error> {
1941        self.transactionally(|wdb| wdb.store_decrypted_tx(d_tx))
1942    }
1943
1944    fn set_tx_trust(
1945        &mut self,
1946        txid: TxId,
1947        trusted: bool,
1948    ) -> Result<(), <Self as WalletRead>::Error> {
1949        self.transactionally(|wdb| wdb.set_tx_trust(txid, trusted))
1950    }
1951
1952    fn store_transactions_to_be_sent(
1953        &mut self,
1954        transactions: &[SentTransaction<<Self as WalletRead>::AccountId>],
1955    ) -> Result<(), <Self as WalletRead>::Error> {
1956        self.transactionally(|wdb| wdb.store_transactions_to_be_sent(transactions))
1957    }
1958
1959    fn truncate_to_height(
1960        &mut self,
1961        max_height: BlockHeight,
1962    ) -> Result<BlockHeight, <Self as WalletRead>::Error> {
1963        self.transactionally(|wdb| wdb.truncate_to_height(max_height))
1964    }
1965
1966    fn truncate_to_chain_state(
1967        &mut self,
1968        chain_state: ChainState,
1969    ) -> Result<(), <Self as WalletRead>::Error> {
1970        self.transactionally(|wdb| wdb.truncate_to_chain_state(chain_state))
1971    }
1972
1973    fn rewind_to_chain_state(
1974        &mut self,
1975        chain_state: ChainState,
1976        reset_account_birthdays: HashSet<<Self as WalletRead>::AccountId>,
1977    ) -> Result<(), RewindError<<Self as WalletRead>::AccountId, <Self as WalletRead>::Error>> {
1978        let tx = self
1979            .conn
1980            .borrow_mut()
1981            .transaction()
1982            .map_err(|e| RewindError::DataSource(SqliteClientError::from(e)))?;
1983        let result = wallet::rewind_to_chain_state(
1984            &tx,
1985            &self.params,
1986            #[cfg(feature = "transparent-inputs")]
1987            &self.gap_limits,
1988            &chain_state,
1989            reset_account_birthdays,
1990        );
1991        if result.is_ok() {
1992            tx.commit()
1993                .map_err(|e| RewindError::DataSource(SqliteClientError::from(e)))?;
1994        }
1995        result
1996    }
1997
1998    #[cfg(feature = "transparent-inputs")]
1999    fn reserve_next_n_ephemeral_addresses(
2000        &mut self,
2001        account_id: <Self as WalletRead>::AccountId,
2002        n: usize,
2003    ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
2004    {
2005        self.transactionally(|wdb| wdb.reserve_next_n_ephemeral_addresses(account_id, n))
2006    }
2007
2008    #[cfg(feature = "transparent-inputs")]
2009    fn reserve_next_n_internal_addresses(
2010        &mut self,
2011        account_id: <Self as WalletRead>::AccountId,
2012        n: usize,
2013    ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
2014    {
2015        self.transactionally(|wdb| wdb.reserve_next_n_internal_addresses(account_id, n))
2016    }
2017
2018    fn set_transaction_status(
2019        &mut self,
2020        txid: TxId,
2021        status: data_api::TransactionStatus,
2022    ) -> Result<(), <Self as WalletRead>::Error> {
2023        self.transactionally(|wdb| WalletWrite::set_transaction_status(wdb, txid, status))
2024    }
2025
2026    #[cfg(feature = "transparent-inputs")]
2027    fn schedule_next_check(
2028        &mut self,
2029        address: &TransparentAddress,
2030        offset_seconds: u32,
2031    ) -> Result<Option<SystemTime>, <Self as WalletRead>::Error> {
2032        self.transactionally(|wdb| wdb.schedule_next_check(address, offset_seconds))
2033    }
2034
2035    #[cfg(feature = "transparent-inputs")]
2036    fn mark_transparent_addresses_exposed(
2037        &mut self,
2038        exposures: &[(TransparentAddress, BlockHeight)],
2039    ) -> Result<(), <Self as WalletRead>::Error> {
2040        self.transactionally(|wdb| wdb.mark_transparent_addresses_exposed(exposures))
2041    }
2042
2043    #[cfg(feature = "transparent-inputs")]
2044    fn notify_address_checked(
2045        &mut self,
2046        request: TransactionsInvolvingAddress,
2047        as_of_height: BlockHeight,
2048    ) -> Result<(), <Self as WalletRead>::Error> {
2049        self.transactionally(|wdb| wdb.notify_address_checked(request, as_of_height))
2050    }
2051
2052    #[cfg(feature = "spend-index")]
2053    fn notify_output_verified_unspent(
2054        &mut self,
2055        outpoint: OutPoint,
2056        as_of_height: BlockHeight,
2057    ) -> Result<(), <Self as WalletRead>::Error> {
2058        self.transactionally(|wdb| wdb.notify_output_verified_unspent(outpoint, as_of_height))
2059    }
2060}
2061
2062/// This impl block is only usable when you already have an [`SqlTransaction`], meaning
2063/// you are inside a [`WalletDb::transactionally`] block with a lock on the database.
2064impl<P, CL, R> OutputLockStore for WalletDb<SqlTransaction<'_>, P, CL, R>
2065where
2066    P: consensus::Parameters,
2067    CL: Clock,
2068    R: Rng,
2069{
2070    type Error = SqliteClientError;
2071    type AccountId = AccountUuid;
2072
2073    fn lock_outputs(
2074        &mut self,
2075        outputs: &[OutputRef],
2076        owner: LockOwner,
2077        lock_expiry_height: BlockHeight,
2078    ) -> Result<usize, LockError<Self::Error>> {
2079        // This impl operates within an enclosing database transaction, so the
2080        // all-or-nothing contract of `OutputLockStore::lock_outputs` holds only if a
2081        // returned error causes the enclosing transaction to be rolled back: on a
2082        // mid-batch `LockFailure`, locks taken for earlier outputs in the batch
2083        // remain pending in the transaction. `WalletDb::transactionally` (used by
2084        // the non-transactional impl above) provides that rollback.
2085        Ok(wallet::locking::lock_outputs(
2086            self.conn.0,
2087            outputs,
2088            owner,
2089            lock_expiry_height,
2090        )?)
2091    }
2092
2093    fn unlock_output(&mut self, output: &OutputRef, owner: LockOwner) -> Result<bool, Self::Error> {
2094        wallet::locking::unlock_output(self.conn.0, output, owner)
2095    }
2096
2097    fn clear_locked_outputs(&mut self, account: Self::AccountId) -> Result<usize, Self::Error> {
2098        wallet::locking::clear_locked_outputs(self.conn.0, account)
2099    }
2100
2101    fn get_locked_outputs(&self, account: Self::AccountId) -> Result<Vec<OutputRef>, Self::Error> {
2102        wallet::locking::get_locked_outputs(self.conn.0, account)
2103    }
2104}
2105
2106impl<P: consensus::Parameters, CL: Clock, R: Rng> WalletWrite
2107    for WalletDb<SqlTransaction<'_>, P, CL, R>
2108{
2109    type UtxoRef = UtxoId;
2110
2111    fn create_account(
2112        &mut self,
2113        account_name: &str,
2114        seed: &SecretVec<u8>,
2115        birthday: &AccountBirthday,
2116        key_source: Option<&str>,
2117    ) -> Result<(<Self as WalletRead>::AccountId, UnifiedSpendingKey), <Self as WalletRead>::Error>
2118    {
2119        let seed_fingerprint =
2120            SeedFingerprint::from_seed(seed.expose_secret()).ok_or_else(|| {
2121                SqliteClientError::BadAccountData(
2122                    "Seed must be between 32 and 252 bytes in length.".to_owned(),
2123                )
2124            })?;
2125        let zip32_account_index = wallet::max_zip32_account_index(self.conn.0, &seed_fingerprint)?
2126            .map(|a| {
2127                a.next()
2128                    .ok_or(SqliteClientError::Zip32AccountIndexOutOfRange)
2129            })
2130            .transpose()?
2131            .unwrap_or(zip32::AccountId::ZERO);
2132
2133        let usk =
2134            UnifiedSpendingKey::from_seed(&self.params, seed.expose_secret(), zip32_account_index)
2135                .map_err(|_| SqliteClientError::KeyDerivationError(zip32_account_index))?;
2136        let ufvk = usk.to_unified_full_viewing_key();
2137
2138        let account = wallet::add_account(
2139            self.conn.0,
2140            &self.params,
2141            account_name,
2142            &AccountSource::Derived {
2143                derivation: Zip32Derivation::new(
2144                    seed_fingerprint,
2145                    zip32_account_index,
2146                    #[cfg(feature = "zcashd-compat")]
2147                    None,
2148                ),
2149                key_source: key_source.map(|s| s.to_owned()),
2150            },
2151            wallet::ViewingKey::Full(Box::new(ufvk)),
2152            birthday,
2153            #[cfg(feature = "transparent-inputs")]
2154            &self.gap_limits,
2155        )?;
2156
2157        Ok((account.id(), usk))
2158    }
2159
2160    fn import_account_hd(
2161        &mut self,
2162        account_name: &str,
2163        seed: &SecretVec<u8>,
2164        account_index: zip32::AccountId,
2165        birthday: &AccountBirthday,
2166        key_source: Option<&str>,
2167    ) -> Result<(Self::Account, UnifiedSpendingKey), <Self as WalletRead>::Error> {
2168        let seed_fingerprint =
2169            SeedFingerprint::from_seed(seed.expose_secret()).ok_or_else(|| {
2170                SqliteClientError::BadAccountData(
2171                    "Seed must be between 32 and 252 bytes in length.".to_owned(),
2172                )
2173            })?;
2174
2175        let usk = UnifiedSpendingKey::from_seed(&self.params, seed.expose_secret(), account_index)
2176            .map_err(|_| SqliteClientError::KeyDerivationError(account_index))?;
2177        let ufvk = usk.to_unified_full_viewing_key();
2178
2179        let account = wallet::add_account(
2180            self.conn.0,
2181            &self.params,
2182            account_name,
2183            &AccountSource::Derived {
2184                derivation: Zip32Derivation::new(
2185                    seed_fingerprint,
2186                    account_index,
2187                    #[cfg(feature = "zcashd-compat")]
2188                    None,
2189                ),
2190                key_source: key_source.map(|s| s.to_owned()),
2191            },
2192            wallet::ViewingKey::Full(Box::new(ufvk)),
2193            birthday,
2194            #[cfg(feature = "transparent-inputs")]
2195            &self.gap_limits,
2196        )?;
2197
2198        Ok((account, usk))
2199    }
2200
2201    fn import_account_ufvk(
2202        &mut self,
2203        account_name: &str,
2204        ufvk: &UnifiedFullViewingKey,
2205        birthday: &AccountBirthday,
2206        purpose: AccountPurpose,
2207        key_source: Option<&str>,
2208    ) -> Result<Self::Account, <Self as WalletRead>::Error> {
2209        wallet::add_account(
2210            self.conn.0,
2211            &self.params,
2212            account_name,
2213            &AccountSource::Imported {
2214                purpose,
2215                key_source: key_source.map(|s| s.to_owned()),
2216            },
2217            wallet::ViewingKey::Full(Box::new(ufvk.to_owned())),
2218            birthday,
2219            #[cfg(feature = "transparent-inputs")]
2220            &self.gap_limits,
2221        )
2222    }
2223
2224    fn delete_account(
2225        &mut self,
2226        account_uuid: <Self as WalletRead>::AccountId,
2227    ) -> Result<(), <Self as WalletRead>::Error> {
2228        wallet::delete_account(self.conn.0, account_uuid)
2229    }
2230
2231    #[cfg(feature = "transparent-key-import")]
2232    fn import_standalone_transparent_pubkey(
2233        &mut self,
2234        account: <Self as WalletRead>::AccountId,
2235        pubkey: secp256k1::PublicKey,
2236    ) -> Result<(), <Self as WalletRead>::Error> {
2237        wallet::import_standalone_transparent_pubkey(self.conn.0, &self.params, account, pubkey)
2238            .map(|_inserted| ())
2239    }
2240
2241    #[cfg(feature = "transparent-key-import")]
2242    fn import_standalone_transparent_pubkeys(
2243        &mut self,
2244        account: <Self as WalletRead>::AccountId,
2245        pubkeys: &[secp256k1::PublicKey],
2246    ) -> Result<(), <Self as WalletRead>::Error> {
2247        wallet::import_standalone_transparent_pubkeys(self.conn.0, &self.params, account, pubkeys)
2248            .map(|_inserted| ())
2249    }
2250
2251    #[cfg(feature = "transparent-key-import")]
2252    fn import_standalone_transparent_script(
2253        &mut self,
2254        account: <Self as WalletRead>::AccountId,
2255        script: zcash_script::script::Redeem,
2256    ) -> Result<(), <Self as WalletRead>::Error> {
2257        wallet::import_standalone_transparent_script(self.conn.0, &self.params, account, script)
2258    }
2259
2260    fn get_next_available_address(
2261        &mut self,
2262        account_uuid: <Self as WalletRead>::AccountId,
2263        request: UnifiedAddressRequest,
2264    ) -> Result<Option<(UnifiedAddress, DiversifierIndex)>, <Self as WalletRead>::Error> {
2265        wallet::get_next_available_address(
2266            self.conn.0,
2267            &self.params,
2268            &self.clock,
2269            account_uuid,
2270            request,
2271            #[cfg(feature = "transparent-inputs")]
2272            &self.gap_limits,
2273        )
2274    }
2275
2276    fn get_address_for_index(
2277        &mut self,
2278        account: <Self as WalletRead>::AccountId,
2279        diversifier_index: DiversifierIndex,
2280        request: UnifiedAddressRequest,
2281    ) -> Result<Option<UnifiedAddress>, <Self as WalletRead>::Error> {
2282        if let Some(account) = self.get_account(account)? {
2283            match account.uivk().address(diversifier_index, request) {
2284                Ok(address) => {
2285                    let chain_tip_height = wallet::chain_tip_height(self.conn.borrow())?;
2286                    upsert_address(
2287                        self.conn.borrow(),
2288                        &self.params,
2289                        account.internal_id(),
2290                        diversifier_index,
2291                        &address,
2292                        Some(chain_tip_height.unwrap_or(account.birthday())),
2293                        true,
2294                    )?;
2295
2296                    Ok(Some(address))
2297                }
2298                #[cfg(feature = "transparent-inputs")]
2299                Err(InvalidTransparentChildIndex(_)) => Ok(None),
2300                Err(InvalidSaplingDiversifierIndex(_)) => Ok(None),
2301                Err(e) => Err(SqliteClientError::AddressGeneration(e)),
2302            }
2303        } else {
2304            Err(SqliteClientError::AccountUnknown)
2305        }
2306    }
2307
2308    fn update_chain_tip(
2309        &mut self,
2310        tip_height: BlockHeight,
2311    ) -> Result<(), <Self as WalletRead>::Error> {
2312        wallet::scanning::update_chain_tip(self.conn.0, &self.params, tip_height)?;
2313        Ok(())
2314    }
2315
2316    fn prune_scan_queue_below(
2317        &mut self,
2318        height: BlockHeight,
2319        retain_with_priority: Option<ScanPriority>,
2320    ) -> Result<u64, <Self as WalletRead>::Error> {
2321        wallet::scanning::prune_scan_queue_below(self.conn.0, height, retain_with_priority)
2322    }
2323
2324    #[allow(clippy::type_complexity)]
2325    fn put_blocks(
2326        &mut self,
2327        from_state: &ChainState,
2328        blocks: Vec<ScannedBlock<<Self as WalletRead>::AccountId>>,
2329    ) -> Result<(), <Self as WalletRead>::Error> {
2330        // Once the NU6.3 (Ironwood) activation height is reached, checkpoints on the anchor
2331        // retention grids are retained as durable anchors. The activation height is `None` (and so
2332        // anchor retention is inactive) on networks that do not yet have an assigned NU6.3
2333        // activation height.
2334        //
2335        // Upstream also unions in the grid every in-flight pool migration was committed under,
2336        // read from the database. This fork does not carry the pool-migration engine, so no such
2337        // row can exist and the union is exactly this wallet's configured interval. Restoring that
2338        // behaviour means restoring the engine, not just this call.
2339        let anchor_retention = self
2340            .params
2341            .activation_height(consensus::NetworkUpgrade::Nu6_3)
2342            .map(|from_height| {
2343                Ok::<_, SqliteClientError>(AnchorRetention::union(
2344                    from_height,
2345                    core::iter::once(self.anchor_retention_interval),
2346                ))
2347            })
2348            .transpose()?
2349            .flatten();
2350
2351        ll::wallet::put_blocks::<_, SqliteClientError, commitment_tree::Error>(
2352            self,
2353            #[cfg(feature = "transparent-inputs")]
2354            self.gap_limits,
2355            from_state,
2356            blocks,
2357            anchor_retention.as_ref(),
2358        )
2359        .map_err(SqliteClientError::from)
2360    }
2361
2362    fn put_received_transparent_utxo(
2363        &mut self,
2364        _output: &WalletTransparentOutput<<Self as WalletRead>::AccountId>,
2365    ) -> Result<Self::UtxoRef, <Self as WalletRead>::Error> {
2366        #[cfg(feature = "transparent-inputs")]
2367        return {
2368            let (account_id, _, key_scope, utxo_id) =
2369                wallet::transparent::put_received_transparent_utxo(
2370                    self.conn.0,
2371                    &self.params,
2372                    &self.gap_limits,
2373                    _output,
2374                )?;
2375
2376            if let Some(t_key_scope) = <Option<TransparentKeyScope>>::from(key_scope) {
2377                wallet::transparent::generate_gap_addresses(
2378                    self.conn.0,
2379                    &self.params,
2380                    &self.gap_limits,
2381                    account_id,
2382                    t_key_scope,
2383                    UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require),
2384                    true,
2385                )?;
2386            }
2387
2388            Ok(utxo_id)
2389        };
2390
2391        #[cfg(not(feature = "transparent-inputs"))]
2392        panic!(
2393            "The wallet must be compiled with the transparent-inputs feature to use this method."
2394        );
2395    }
2396
2397    fn store_decrypted_tx(
2398        &mut self,
2399        d_tx: DecryptedTransaction<Transaction, <Self as WalletRead>::AccountId>,
2400    ) -> Result<(), <Self as WalletRead>::Error> {
2401        let chain_tip = wallet::chain_tip_height(self.conn.borrow())?
2402            .ok_or(SqliteClientError::ChainHeightUnknown)?;
2403        store_decrypted_tx(
2404            self,
2405            &self.params.clone(),
2406            #[cfg(feature = "transparent-inputs")]
2407            self.gap_limits,
2408            chain_tip,
2409            d_tx,
2410        )
2411    }
2412
2413    fn set_tx_trust(
2414        &mut self,
2415        txid: TxId,
2416        trusted: bool,
2417    ) -> Result<(), <Self as WalletRead>::Error> {
2418        wallet::set_tx_trust(self.conn.0, txid, trusted)
2419    }
2420
2421    fn store_transactions_to_be_sent(
2422        &mut self,
2423        transactions: &[SentTransaction<<Self as WalletRead>::AccountId>],
2424    ) -> Result<(), <Self as WalletRead>::Error> {
2425        for sent_tx in transactions {
2426            wallet::store_transaction_to_be_sent(
2427                self.conn.0,
2428                &self.params,
2429                #[cfg(feature = "transparent-inputs")]
2430                &self.gap_limits,
2431                sent_tx,
2432            )?;
2433        }
2434        Ok(())
2435    }
2436
2437    fn truncate_to_height(
2438        &mut self,
2439        max_height: BlockHeight,
2440    ) -> Result<BlockHeight, <Self as WalletRead>::Error> {
2441        wallet::truncate_to_height(
2442            self.conn.0,
2443            &self.params,
2444            #[cfg(feature = "transparent-inputs")]
2445            &self.gap_limits,
2446            max_height,
2447        )
2448    }
2449
2450    fn truncate_to_chain_state(
2451        &mut self,
2452        chain_state: ChainState,
2453    ) -> Result<(), <Self as WalletRead>::Error> {
2454        wallet::truncate_to_chain_state(self, chain_state)
2455    }
2456
2457    fn rewind_to_chain_state(
2458        &mut self,
2459        chain_state: ChainState,
2460        reset_account_birthdays: HashSet<<Self as WalletRead>::AccountId>,
2461    ) -> Result<(), RewindError<<Self as WalletRead>::AccountId, <Self as WalletRead>::Error>> {
2462        wallet::rewind_to_chain_state(
2463            self.conn.0,
2464            &self.params,
2465            #[cfg(feature = "transparent-inputs")]
2466            &self.gap_limits,
2467            &chain_state,
2468            reset_account_birthdays,
2469        )
2470    }
2471
2472    #[cfg(feature = "transparent-inputs")]
2473    fn reserve_next_n_ephemeral_addresses(
2474        &mut self,
2475        account_id: <Self as WalletRead>::AccountId,
2476        n: usize,
2477    ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
2478    {
2479        let account_id = wallet::get_account_ref(self.conn.0, account_id)?;
2480        let reserved = wallet::transparent::reserve_next_n_addresses(
2481            self.conn.0,
2482            &self.params,
2483            account_id,
2484            TransparentKeyScope::EPHEMERAL,
2485            self.gap_limits.ephemeral(),
2486            n,
2487        )?;
2488
2489        Ok(reserved.into_iter().map(|(_, a, m)| (a, m)).collect())
2490    }
2491
2492    #[cfg(feature = "transparent-inputs")]
2493    fn reserve_next_n_internal_addresses(
2494        &mut self,
2495        account_id: <Self as WalletRead>::AccountId,
2496        n: usize,
2497    ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
2498    {
2499        let account_id = wallet::get_account_ref(self.conn.0, account_id)?;
2500        let reserved = wallet::transparent::reserve_next_n_addresses(
2501            self.conn.0,
2502            &self.params,
2503            account_id,
2504            TransparentKeyScope::INTERNAL,
2505            self.gap_limits.internal(),
2506            n,
2507        )?;
2508
2509        Ok(reserved.into_iter().map(|(_, a, m)| (a, m)).collect())
2510    }
2511
2512    fn set_transaction_status(
2513        &mut self,
2514        txid: TxId,
2515        status: data_api::TransactionStatus,
2516    ) -> Result<(), <Self as WalletRead>::Error> {
2517        wallet::set_transaction_status(
2518            self.conn.0,
2519            &self.params,
2520            #[cfg(feature = "transparent-inputs")]
2521            &self.gap_limits,
2522            txid,
2523            status,
2524        )
2525    }
2526
2527    #[cfg(feature = "transparent-inputs")]
2528    fn schedule_next_check(
2529        &mut self,
2530        address: &TransparentAddress,
2531        offset_seconds: u32,
2532    ) -> Result<Option<SystemTime>, <Self as WalletRead>::Error> {
2533        wallet::transparent::schedule_next_check(
2534            self.conn.0,
2535            &self.params,
2536            &self.clock,
2537            &mut self.rng,
2538            address,
2539            offset_seconds,
2540        )
2541    }
2542
2543    #[cfg(feature = "transparent-inputs")]
2544    fn mark_transparent_addresses_exposed(
2545        &mut self,
2546        exposures: &[(TransparentAddress, BlockHeight)],
2547    ) -> Result<(), <Self as WalletRead>::Error> {
2548        wallet::transparent::mark_transparent_addresses_exposed(
2549            self.conn.0,
2550            &self.params,
2551            exposures,
2552        )
2553    }
2554
2555    #[cfg(feature = "transparent-inputs")]
2556    fn notify_address_checked(
2557        &mut self,
2558        request: TransactionsInvolvingAddress,
2559        as_of_height: BlockHeight,
2560    ) -> Result<(), <Self as WalletRead>::Error> {
2561        if let Some(requested_end) = request.block_range_end() {
2562            // block_end_height is end-exclusive
2563            if as_of_height != requested_end - 1 {
2564                return Err(SqliteClientError::NotificationMismatch {
2565                    expected: requested_end - 1,
2566                    actual: as_of_height,
2567                });
2568            }
2569        }
2570
2571        wallet::transparent::update_observed_unspent_heights(
2572            self.conn.0,
2573            &self.params,
2574            request.address(),
2575            as_of_height,
2576        )
2577    }
2578
2579    #[cfg(feature = "spend-index")]
2580    fn notify_output_verified_unspent(
2581        &mut self,
2582        outpoint: OutPoint,
2583        as_of_height: BlockHeight,
2584    ) -> Result<(), <Self as WalletRead>::Error> {
2585        wallet::transparent::update_observed_unspent_height_for_outpoint(
2586            self.conn.0,
2587            &outpoint,
2588            as_of_height,
2589        )
2590    }
2591}
2592
2593impl<'a, C: Borrow<rusqlite::Transaction<'a>>, P: consensus::Parameters, CL: Clock, R: Rng>
2594    LowLevelWalletRead for WalletDb<C, P, CL, R>
2595{
2596    type AccountId = AccountUuid;
2597    type AccountRef = AccountRef;
2598    type Account = wallet::Account;
2599    type Error = SqliteClientError;
2600    type TxRef = TxRef;
2601
2602    fn block_fully_scanned_height(
2603        &self,
2604    ) -> Result<Option<zcash_protocol::consensus::BlockHeight>, Self::Error> {
2605        Ok(
2606            wallet::block_fully_scanned(self.conn.borrow(), &self.params)?
2607                .map(|meta| meta.block_height()),
2608        )
2609    }
2610
2611    fn select_receiving_address(
2612        &self,
2613        account: Self::AccountId,
2614        receiver: &zcash_keys::address::Receiver,
2615    ) -> Result<Option<zcash_address::ZcashAddress>, Self::Error> {
2616        wallet::select_receiving_address(self.conn.borrow(), &self.params, account, receiver)
2617    }
2618
2619    #[cfg(feature = "transparent-inputs")]
2620    fn find_involved_accounts(
2621        &self,
2622        tx_refs: impl IntoIterator<Item = Self::TxRef>,
2623    ) -> Result<HashSet<(Self::AccountId, Option<TransparentKeyScope>)>, Self::Error> {
2624        Ok(wallet::involved_accounts(self.conn.borrow(), tx_refs)?
2625            .into_iter()
2626            .map(|(_, uuid, scope)| (uuid, scope))
2627            .collect())
2628    }
2629
2630    #[cfg(feature = "transparent-inputs")]
2631    fn find_account_for_transparent_address(
2632        &self,
2633        address: &TransparentAddress,
2634    ) -> Result<Option<(Self::AccountId, Option<TransparentKeyScope>)>, Self::Error> {
2635        wallet::transparent::find_account_uuid_for_transparent_address(
2636            self.conn.borrow(),
2637            &self.params,
2638            address,
2639        )
2640        .map(|opt| opt.map(|(a, s)| (a, s.as_transparent())))
2641    }
2642
2643    #[cfg(feature = "transparent-inputs")]
2644    fn detect_accounts_transparent<'t>(
2645        &self,
2646        spends: impl Iterator<Item = &'t transparent::bundle::OutPoint>,
2647    ) -> Result<std::collections::HashSet<Self::AccountId>, Self::Error> {
2648        wallet::transparent::detect_spending_accounts(self.conn.borrow(), spends)
2649            .map_err(SqliteClientError::from)
2650    }
2651
2652    fn detect_accounts_sapling<'t>(
2653        &self,
2654        spends: impl Iterator<Item = &'t sapling::Nullifier>,
2655    ) -> Result<std::collections::HashSet<Self::AccountId>, Self::Error> {
2656        wallet::sapling::detect_spending_accounts(self.conn.borrow(), spends)
2657            .map_err(SqliteClientError::from)
2658    }
2659
2660    #[cfg(feature = "orchard")]
2661    fn detect_accounts_orchard<'t>(
2662        &self,
2663        spends: impl Iterator<Item = &'t orchard::note::Nullifier>,
2664    ) -> Result<std::collections::HashSet<Self::AccountId>, Self::Error> {
2665        wallet::orchard::detect_spending_accounts(self.conn.borrow(), ORCHARD_TABLES_PREFIX, spends)
2666            .map_err(SqliteClientError::from)
2667    }
2668
2669    #[cfg(feature = "orchard")]
2670    fn detect_accounts_ironwood<'t>(
2671        &self,
2672        spends: impl Iterator<Item = &'t orchard::note::Nullifier>,
2673    ) -> Result<std::collections::HashSet<Self::AccountId>, Self::Error> {
2674        wallet::orchard::detect_spending_accounts(
2675            self.conn.borrow(),
2676            IRONWOOD_TABLES_PREFIX,
2677            spends,
2678        )
2679        .map_err(SqliteClientError::from)
2680    }
2681
2682    #[cfg(feature = "transparent-inputs")]
2683    fn get_wallet_transparent_output(
2684        &self,
2685        outpoint: &OutPoint,
2686        target_height: Option<TargetHeight>,
2687    ) -> Result<Option<WalletTransparentOutput<Self::AccountId>>, Self::Error> {
2688        wallet::transparent::get_wallet_transparent_output(
2689            self.conn.borrow(),
2690            outpoint,
2691            target_height,
2692        )
2693    }
2694
2695    fn get_txs_spending_transparent_outputs_of(
2696        &self,
2697        tx_ref: Self::TxRef,
2698    ) -> Result<Vec<(Self::TxRef, Transaction)>, Self::Error> {
2699        wallet::get_txs_spending_transparent_outputs_of(self.conn.borrow(), &self.params, tx_ref)
2700    }
2701
2702    fn detect_sapling_spend(
2703        &self,
2704        nf: &::sapling::Nullifier,
2705    ) -> Result<Option<Self::TxRef>, Self::Error> {
2706        wallet::query_nullifier_map(self.conn.borrow(), ShieldedPool::Sapling, nf)
2707    }
2708
2709    #[cfg(feature = "orchard")]
2710    fn detect_orchard_spend(
2711        &self,
2712        nf: &::orchard::note::Nullifier,
2713    ) -> Result<Option<Self::TxRef>, Self::Error> {
2714        wallet::query_nullifier_map(self.conn.borrow(), ShieldedPool::Orchard, &nf.to_bytes())
2715    }
2716
2717    #[cfg(feature = "orchard")]
2718    fn detect_ironwood_spend(
2719        &self,
2720        nf: &::orchard::note::Nullifier,
2721    ) -> Result<Option<Self::TxRef>, Self::Error> {
2722        wallet::query_nullifier_map(self.conn.borrow(), ShieldedPool::Ironwood, &nf.to_bytes())
2723    }
2724
2725    #[cfg(feature = "transparent-inputs")]
2726    fn get_account_ref(
2727        &self,
2728        account_uuid: Self::AccountId,
2729    ) -> Result<Self::AccountRef, Self::Error> {
2730        wallet::get_account_ref(self.conn.borrow(), account_uuid)
2731    }
2732
2733    #[cfg(feature = "transparent-inputs")]
2734    fn get_account_internal(
2735        &self,
2736        account_id: Self::AccountRef,
2737    ) -> Result<Option<wallet::Account>, SqliteClientError> {
2738        wallet::get_account_internal(self.conn.borrow(), &self.params, account_id)
2739    }
2740}
2741
2742impl<'a, C: Borrow<rusqlite::Transaction<'a>>, P: consensus::Parameters, CL: Clock, R: Rng>
2743    LowLevelWalletWrite for WalletDb<C, P, CL, R>
2744{
2745    fn put_block_meta(
2746        &mut self,
2747        block_height: BlockHeight,
2748        block_hash: BlockHash,
2749        block_time: u32,
2750        sapling_commitment_tree_size: u32,
2751        sapling_output_count: u32,
2752        #[cfg(feature = "orchard")] orchard_commitment_tree_size: u32,
2753        #[cfg(feature = "orchard")] orchard_action_count: u32,
2754        #[cfg(feature = "orchard")] ironwood_commitment_tree_size: u32,
2755        #[cfg(feature = "orchard")] ironwood_action_count: u32,
2756    ) -> Result<(), Self::Error> {
2757        wallet::put_block(
2758            self.conn.borrow(),
2759            block_height,
2760            block_hash,
2761            block_time,
2762            sapling_commitment_tree_size,
2763            sapling_output_count,
2764            #[cfg(feature = "orchard")]
2765            orchard_commitment_tree_size,
2766            #[cfg(feature = "orchard")]
2767            orchard_action_count,
2768            #[cfg(feature = "orchard")]
2769            ironwood_commitment_tree_size,
2770            #[cfg(feature = "orchard")]
2771            ironwood_action_count,
2772        )
2773    }
2774
2775    fn put_tx_meta(
2776        &mut self,
2777        tx: &WalletTx<Self::AccountId>,
2778        height: BlockHeight,
2779    ) -> Result<Self::TxRef, Self::Error> {
2780        wallet::put_tx_meta(self.conn.borrow(), tx, height)
2781    }
2782
2783    fn put_tx_data(
2784        &mut self,
2785        tx: &Transaction,
2786        fee: Option<zcash_protocol::value::Zatoshis>,
2787        created_at: Option<time::OffsetDateTime>,
2788        target_height: Option<TargetHeight>,
2789        observed_height: BlockHeight,
2790    ) -> Result<Self::TxRef, Self::Error> {
2791        wallet::put_tx_data(
2792            self.conn.borrow(),
2793            tx,
2794            fee,
2795            created_at,
2796            target_height,
2797            observed_height,
2798        )
2799    }
2800
2801    fn set_transaction_status(
2802        &mut self,
2803        txid: TxId,
2804        status: data_api::TransactionStatus,
2805    ) -> Result<(), Self::Error> {
2806        wallet::set_transaction_status(
2807            self.conn.borrow(),
2808            &self.params,
2809            #[cfg(feature = "transparent-inputs")]
2810            &self.gap_limits,
2811            txid,
2812            status,
2813        )
2814    }
2815
2816    fn put_zip318_classification(
2817        &mut self,
2818        tx_ref: Self::TxRef,
2819        classification: zcash_protocol::zip318::Zip318Classification,
2820    ) -> Result<(), Self::Error> {
2821        wallet::put_zip318_classification(self.conn.borrow(), tx_ref, classification)
2822    }
2823
2824    fn put_received_sapling_note<T: ReceivedSaplingOutput<AccountId = Self::AccountId>>(
2825        &mut self,
2826        output: &T,
2827        tx_ref: Self::TxRef,
2828        target_or_mined_height: Option<BlockHeight>,
2829        spent_in: Option<Self::TxRef>,
2830    ) -> Result<(), Self::Error> {
2831        wallet::sapling::put_received_note(
2832            self.conn.borrow(),
2833            &self.params,
2834            output,
2835            tx_ref,
2836            target_or_mined_height,
2837            spent_in,
2838        )?;
2839
2840        Ok(())
2841    }
2842
2843    fn mark_sapling_note_spent(
2844        &mut self,
2845        nf: &::sapling::Nullifier,
2846        tx_ref: Self::TxRef,
2847    ) -> Result<bool, Self::Error> {
2848        wallet::sapling::mark_sapling_note_spent(self.conn.borrow(), tx_ref, nf)
2849    }
2850
2851    fn track_block_sapling_nullifiers(
2852        &mut self,
2853        block_height: BlockHeight,
2854        nfs: &[(TxIndex, TxId, Vec<::sapling::Nullifier>)],
2855    ) -> Result<(), Self::Error> {
2856        wallet::insert_nullifier_map(self.conn.borrow(), block_height, ShieldedPool::Sapling, nfs)
2857    }
2858
2859    #[cfg(feature = "orchard")]
2860    fn put_received_orchard_note<T: ReceivedOrchardOutput<AccountId = Self::AccountId>>(
2861        &mut self,
2862        output: &T,
2863        tx_ref: Self::TxRef,
2864        target_or_mined_height: Option<BlockHeight>,
2865        spent_in: Option<Self::TxRef>,
2866    ) -> Result<(), Self::Error> {
2867        wallet::orchard::put_received_note(
2868            self.conn.borrow(),
2869            &self.params,
2870            ShieldedPool::Orchard,
2871            output,
2872            tx_ref,
2873            target_or_mined_height,
2874            spent_in,
2875        )?;
2876
2877        Ok(())
2878    }
2879
2880    #[cfg(feature = "orchard")]
2881    fn put_received_ironwood_note<T: ReceivedOrchardOutput<AccountId = Self::AccountId>>(
2882        &mut self,
2883        output: &T,
2884        tx_ref: Self::TxRef,
2885        target_or_mined_height: Option<BlockHeight>,
2886        spent_in: Option<Self::TxRef>,
2887    ) -> Result<(), Self::Error> {
2888        wallet::orchard::put_received_note(
2889            self.conn.borrow(),
2890            &self.params,
2891            ShieldedPool::Ironwood,
2892            output,
2893            tx_ref,
2894            target_or_mined_height,
2895            spent_in,
2896        )?;
2897
2898        Ok(())
2899    }
2900
2901    #[cfg(feature = "orchard")]
2902    fn mark_orchard_note_spent(
2903        &mut self,
2904        nf: &::orchard::note::Nullifier,
2905        tx_ref: Self::TxRef,
2906    ) -> Result<bool, Self::Error> {
2907        wallet::orchard::mark_orchard_note_spent(self.conn.borrow(), tx_ref, nf)
2908    }
2909
2910    #[cfg(feature = "orchard")]
2911    fn mark_ironwood_note_spent(
2912        &mut self,
2913        nf: &::orchard::note::Nullifier,
2914        tx_ref: Self::TxRef,
2915    ) -> Result<bool, Self::Error> {
2916        wallet::orchard::mark_ironwood_note_spent(self.conn.borrow(), tx_ref, nf)
2917    }
2918
2919    #[cfg(feature = "orchard")]
2920    fn track_block_orchard_nullifiers(
2921        &mut self,
2922        block_height: BlockHeight,
2923        nfs: &[(TxIndex, TxId, Vec<::orchard::note::Nullifier>)],
2924    ) -> Result<(), Self::Error> {
2925        wallet::insert_nullifier_map(
2926            self.conn.borrow(),
2927            block_height,
2928            ShieldedPool::Orchard,
2929            &nfs.iter()
2930                .map(|(idx, txid, nfs)| (*idx, *txid, nfs.iter().map(|n| n.to_bytes()).collect()))
2931                .collect::<Vec<_>>(),
2932        )
2933    }
2934
2935    #[cfg(feature = "orchard")]
2936    fn track_block_ironwood_nullifiers(
2937        &mut self,
2938        block_height: BlockHeight,
2939        nfs: &[(TxIndex, TxId, Vec<::orchard::note::Nullifier>)],
2940    ) -> Result<(), Self::Error> {
2941        wallet::insert_nullifier_map(
2942            self.conn.borrow(),
2943            block_height,
2944            ShieldedPool::Ironwood,
2945            &nfs.iter()
2946                .map(|(idx, txid, nfs)| (*idx, *txid, nfs.iter().map(|n| n.to_bytes()).collect()))
2947                .collect::<Vec<_>>(),
2948        )
2949    }
2950
2951    fn prune_tracked_nullifiers(&mut self, pruning_depth: u32) -> Result<(), Self::Error> {
2952        if let Some(meta) = wallet::block_fully_scanned(self.conn.borrow(), &self.params)? {
2953            wallet::prune_nullifier_map(
2954                self.conn.borrow(),
2955                meta.block_height().saturating_sub(pruning_depth),
2956            )?;
2957        }
2958
2959        Ok(())
2960    }
2961
2962    fn put_sent_output(
2963        &mut self,
2964        from_account_uuid: Self::AccountId,
2965        tx_ref: Self::TxRef,
2966        output_index: usize,
2967        recipient: &zcash_client_backend::wallet::Recipient<Self::AccountId>,
2968        value: zcash_protocol::value::Zatoshis,
2969        memo: Option<&zcash_protocol::memo::MemoBytes>,
2970    ) -> Result<(), Self::Error> {
2971        wallet::put_sent_output(
2972            self.conn.borrow(),
2973            &self.params,
2974            from_account_uuid,
2975            tx_ref,
2976            output_index,
2977            recipient,
2978            value,
2979            memo,
2980        )
2981    }
2982
2983    fn update_tx_fee(
2984        &mut self,
2985        tx_ref: Self::TxRef,
2986        fee: zcash_protocol::value::Zatoshis,
2987    ) -> Result<(), Self::Error> {
2988        wallet::update_tx_fee(self.conn.borrow(), tx_ref, fee)
2989    }
2990
2991    #[cfg(feature = "transparent-inputs")]
2992    fn put_transparent_output(
2993        &mut self,
2994        output: &WalletTransparentOutput<Self::AccountId>,
2995        observation_height: BlockHeight,
2996        known_unspent: bool,
2997    ) -> Result<(Self::AccountId, Option<TransparentKeyScope>), Self::Error> {
2998        let (_, account_uuid, key_scope, _) = wallet::transparent::put_transparent_output(
2999            self.conn.borrow(),
3000            &self.params,
3001            &self.gap_limits,
3002            output,
3003            observation_height,
3004            known_unspent,
3005        )?;
3006
3007        Ok((account_uuid, key_scope.as_transparent()))
3008    }
3009
3010    #[cfg(feature = "transparent-inputs")]
3011    fn mark_transparent_utxo_spent(
3012        &mut self,
3013        outpoint: &OutPoint,
3014        spent_in_tx: Self::TxRef,
3015    ) -> Result<bool, Self::Error> {
3016        wallet::transparent::mark_transparent_utxo_spent(self.conn.borrow(), spent_in_tx, outpoint)
3017    }
3018
3019    #[cfg(feature = "transparent-inputs")]
3020    fn generate_transparent_gap_addresses(
3021        &mut self,
3022        account_id: Self::AccountId,
3023        key_scope: TransparentKeyScope,
3024        request: UnifiedAddressRequest,
3025    ) -> Result<(), Self::Error> {
3026        generate_transparent_gap_addresses(self, self.gap_limits, account_id, key_scope, request)?;
3027        Ok(())
3028    }
3029
3030    #[cfg(feature = "transparent-inputs")]
3031    fn queue_transparent_spend_detection(
3032        &mut self,
3033        receiving_address: TransparentAddress,
3034        tx_ref: Self::TxRef,
3035        output_index: u32,
3036    ) -> Result<(), Self::Error> {
3037        wallet::transparent::queue_transparent_spend_detection(
3038            self.conn.borrow(),
3039            &self.params,
3040            receiving_address,
3041            tx_ref,
3042            output_index,
3043        )
3044    }
3045
3046    #[cfg(feature = "transparent-inputs")]
3047    fn queue_transparent_input_retrieval(
3048        &mut self,
3049        tx_ref: Self::TxRef,
3050        d_tx: &DecryptedTransaction<Transaction, Self::AccountId>,
3051    ) -> Result<(), Self::Error> {
3052        wallet::queue_transparent_input_retrieval(self.conn.borrow(), tx_ref, d_tx)
3053    }
3054
3055    fn queue_tx_retrieval(
3056        &mut self,
3057        txids: impl Iterator<Item = TxId>,
3058        dependent_tx_ref: Option<Self::TxRef>,
3059    ) -> Result<(), Self::Error> {
3060        wallet::queue_tx_retrieval(self.conn.borrow(), txids, dependent_tx_ref)
3061    }
3062
3063    fn queue_tx_status(&mut self, txid: TxId) -> Result<(), Self::Error> {
3064        wallet::queue_tx_status(self.conn.borrow(), txid)
3065    }
3066
3067    fn delete_retrieval_queue_entries(&mut self, txid: TxId) -> Result<(), Self::Error> {
3068        wallet::delete_retrieval_queue_entries(self.conn.borrow(), txid)
3069    }
3070
3071    fn notify_scan_complete(
3072        &mut self,
3073        range: Range<BlockHeight>,
3074        wallet_note_positions: &[(ShieldedPool, Position)],
3075    ) -> Result<(), Self::Error> {
3076        wallet::scanning::scan_complete(
3077            self.conn.borrow(),
3078            &self.params,
3079            range,
3080            wallet_note_positions,
3081        )
3082    }
3083
3084    #[cfg(feature = "transparent-inputs")]
3085    fn update_gap_limits(
3086        &mut self,
3087        gap_limits: &GapLimits,
3088        txid: TxId,
3089        observation_height: BlockHeight,
3090    ) -> Result<(), Self::Error> {
3091        wallet::transparent::update_gap_limits(
3092            self.conn.borrow(),
3093            &self.params,
3094            gap_limits,
3095            txid,
3096            observation_height,
3097        )
3098    }
3099}
3100
3101pub(crate) type SaplingShardStore<C> = SqliteShardStore<C, sapling::Node, SAPLING_SHARD_HEIGHT>;
3102pub(crate) type SaplingCommitmentTree<C> =
3103    ShardTree<SaplingShardStore<C>, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>;
3104
3105pub(crate) fn sapling_tree<C>(
3106    conn: C,
3107) -> Result<SaplingCommitmentTree<C>, ShardTreeError<commitment_tree::Error>>
3108where
3109    SaplingShardStore<C>: ShardStore<H = sapling::Node, CheckpointId = BlockHeight>,
3110{
3111    Ok(ShardTree::new(
3112        SqliteShardStore::from_connection(conn, SAPLING_TABLES_PREFIX)
3113            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?,
3114        PRUNING_DEPTH.try_into().unwrap(),
3115    ))
3116}
3117
3118#[cfg(feature = "orchard")]
3119pub(crate) type OrchardShardStore<C> =
3120    SqliteShardStore<C, orchard::tree::MerkleHashOrchard, ORCHARD_SHARD_HEIGHT>;
3121
3122#[cfg(feature = "orchard")]
3123pub(crate) type OrchardCommitmentTree<C> = ShardTree<
3124    OrchardShardStore<C>,
3125    { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
3126    ORCHARD_SHARD_HEIGHT,
3127>;
3128
3129#[cfg(feature = "orchard")]
3130pub(crate) fn orchard_tree<C>(
3131    conn: C,
3132) -> Result<OrchardCommitmentTree<C>, ShardTreeError<commitment_tree::Error>>
3133where
3134    OrchardShardStore<C>:
3135        ShardStore<H = orchard::tree::MerkleHashOrchard, CheckpointId = BlockHeight>,
3136{
3137    Ok(ShardTree::new(
3138        SqliteShardStore::from_connection(conn, ORCHARD_TABLES_PREFIX)
3139            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?,
3140        PRUNING_DEPTH.try_into().unwrap(),
3141    ))
3142}
3143
3144/// The shard store backing the Ironwood note commitment tree.
3145///
3146/// Ironwood note commitments are Orchard-shaped, so this reuses the Orchard hash type and shard
3147/// height; only the backing table prefix differs (see [`IRONWOOD_TABLES_PREFIX`]). It is defined
3148/// as a distinct alias to make Ironwood usage self-documenting at call sites.
3149#[cfg(feature = "orchard")]
3150pub(crate) type IronwoodShardStore<C> =
3151    SqliteShardStore<C, orchard::tree::MerkleHashOrchard, IRONWOOD_SHARD_HEIGHT>;
3152
3153#[cfg(feature = "orchard")]
3154pub(crate) type IronwoodCommitmentTree<C> = ShardTree<
3155    IronwoodShardStore<C>,
3156    { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
3157    IRONWOOD_SHARD_HEIGHT,
3158>;
3159
3160/// Returns a handle to the Ironwood note commitment tree.
3161#[cfg(feature = "orchard")]
3162pub(crate) fn ironwood_tree<C>(
3163    conn: C,
3164) -> Result<IronwoodCommitmentTree<C>, ShardTreeError<commitment_tree::Error>>
3165where
3166    IronwoodShardStore<C>:
3167        ShardStore<H = orchard::tree::MerkleHashOrchard, CheckpointId = BlockHeight>,
3168{
3169    Ok(ShardTree::new(
3170        SqliteShardStore::from_connection(conn, IRONWOOD_TABLES_PREFIX)
3171            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?,
3172        PRUNING_DEPTH.try_into().unwrap(),
3173    ))
3174}
3175
3176impl<C: BorrowMut<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletCommitmentTrees
3177    for WalletDb<C, P, CL, R>
3178{
3179    type Error = commitment_tree::Error;
3180    type SaplingShardStore<'a> = SaplingShardStore<&'a rusqlite::Transaction<'a>>;
3181
3182    fn with_sapling_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
3183    where
3184        for<'a> F:
3185            FnMut(&'a mut SaplingCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
3186        E: From<ShardTreeError<Self::Error>>,
3187    {
3188        let tx = self
3189            .conn
3190            .borrow_mut()
3191            .transaction()
3192            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3193        let result = {
3194            let mut shardtree = sapling_tree(&tx)?;
3195            callback(&mut shardtree)?
3196        };
3197
3198        tx.commit()
3199            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3200        Ok(result)
3201    }
3202
3203    fn put_sapling_subtree_roots(
3204        &mut self,
3205        start_index: u64,
3206        roots: &[CommitmentTreeRoot<sapling::Node>],
3207    ) -> Result<(), ShardTreeError<Self::Error>> {
3208        let tx = self
3209            .conn
3210            .borrow_mut()
3211            .transaction()
3212            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3213        put_shard_roots::<_, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>(
3214            &tx,
3215            SAPLING_TABLES_PREFIX,
3216            start_index,
3217            roots,
3218        )?;
3219        tx.commit()
3220            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3221        Ok(())
3222    }
3223
3224    fn get_sapling_subtree_root(
3225        &mut self,
3226        index: u64,
3227    ) -> Result<Option<sapling::Node>, ShardTreeError<Self::Error>> {
3228        wallet::commitment_tree::get_subtree_root(self.conn.borrow(), SAPLING_TABLES_PREFIX, index)
3229            .map_err(ShardTreeError::Storage)
3230    }
3231
3232    #[cfg(feature = "orchard")]
3233    type OrchardShardStore<'a> = SqliteShardStore<
3234        &'a rusqlite::Transaction<'a>,
3235        orchard::tree::MerkleHashOrchard,
3236        ORCHARD_SHARD_HEIGHT,
3237    >;
3238
3239    #[cfg(feature = "orchard")]
3240    fn with_orchard_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
3241    where
3242        for<'a> F:
3243            FnMut(&'a mut OrchardCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
3244        E: From<ShardTreeError<Self::Error>>,
3245    {
3246        let tx = self
3247            .conn
3248            .borrow_mut()
3249            .transaction()
3250            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3251        let result = {
3252            let mut shardtree = orchard_tree(&tx)?;
3253            callback(&mut shardtree)?
3254        };
3255
3256        tx.commit()
3257            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3258        Ok(result)
3259    }
3260
3261    #[cfg(feature = "orchard")]
3262    fn put_orchard_subtree_roots(
3263        &mut self,
3264        start_index: u64,
3265        roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
3266    ) -> Result<(), ShardTreeError<Self::Error>> {
3267        let tx = self
3268            .conn
3269            .borrow_mut()
3270            .transaction()
3271            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3272        put_shard_roots::<_, { ORCHARD_SHARD_HEIGHT * 2 }, ORCHARD_SHARD_HEIGHT>(
3273            &tx,
3274            ORCHARD_TABLES_PREFIX,
3275            start_index,
3276            roots,
3277        )?;
3278        tx.commit()
3279            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3280        Ok(())
3281    }
3282
3283    #[cfg(feature = "orchard")]
3284    fn get_orchard_subtree_root(
3285        &mut self,
3286        index: u64,
3287    ) -> Result<Option<orchard::tree::MerkleHashOrchard>, ShardTreeError<Self::Error>> {
3288        wallet::commitment_tree::get_subtree_root(self.conn.borrow(), ORCHARD_TABLES_PREFIX, index)
3289            .map_err(ShardTreeError::Storage)
3290    }
3291
3292    #[cfg(feature = "orchard")]
3293    fn put_ironwood_subtree_roots(
3294        &mut self,
3295        start_index: u64,
3296        roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
3297    ) -> Result<(), ShardTreeError<Self::Error>> {
3298        let tx = self
3299            .conn
3300            .borrow_mut()
3301            .transaction()
3302            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3303        put_shard_roots::<_, { ORCHARD_SHARD_HEIGHT * 2 }, ORCHARD_SHARD_HEIGHT>(
3304            &tx,
3305            IRONWOOD_TABLES_PREFIX,
3306            start_index,
3307            roots,
3308        )?;
3309        tx.commit()
3310            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3311        Ok(())
3312    }
3313
3314    #[cfg(feature = "orchard")]
3315    fn get_ironwood_subtree_root(
3316        &mut self,
3317        index: u64,
3318    ) -> Result<Option<orchard::tree::MerkleHashOrchard>, ShardTreeError<Self::Error>> {
3319        wallet::commitment_tree::get_subtree_root(self.conn.borrow(), IRONWOOD_TABLES_PREFIX, index)
3320            .map_err(ShardTreeError::Storage)
3321    }
3322
3323    #[cfg(feature = "orchard")]
3324    fn with_ironwood_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<Option<A>, E>
3325    where
3326        for<'a> F:
3327            FnMut(&'a mut IronwoodCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
3328        E: From<ShardTreeError<Self::Error>>,
3329    {
3330        let tx = self
3331            .conn
3332            .borrow_mut()
3333            .transaction()
3334            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3335        let result = {
3336            let mut shardtree = ironwood_tree(&tx)?;
3337            callback(&mut shardtree)?
3338        };
3339
3340        tx.commit()
3341            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3342        Ok(Some(result))
3343    }
3344}
3345
3346impl<P: consensus::Parameters, CL, R> WalletCommitmentTrees
3347    for WalletDb<SqlTransaction<'_>, P, CL, R>
3348{
3349    type Error = commitment_tree::Error;
3350    type SaplingShardStore<'a> = crate::SaplingShardStore<&'a rusqlite::Transaction<'a>>;
3351
3352    fn with_sapling_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
3353    where
3354        for<'a> F:
3355            FnMut(&'a mut SaplingCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
3356        E: From<ShardTreeError<commitment_tree::Error>>,
3357    {
3358        let mut shardtree = sapling_tree(self.conn.0)?;
3359        let result = callback(&mut shardtree)?;
3360
3361        Ok(result)
3362    }
3363
3364    fn put_sapling_subtree_roots(
3365        &mut self,
3366        start_index: u64,
3367        roots: &[CommitmentTreeRoot<sapling::Node>],
3368    ) -> Result<(), ShardTreeError<Self::Error>> {
3369        put_shard_roots::<_, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>(
3370            self.conn.0,
3371            SAPLING_TABLES_PREFIX,
3372            start_index,
3373            roots,
3374        )
3375    }
3376
3377    fn get_sapling_subtree_root(
3378        &mut self,
3379        index: u64,
3380    ) -> Result<Option<sapling::Node>, ShardTreeError<Self::Error>> {
3381        wallet::commitment_tree::get_subtree_root(self.conn.0, SAPLING_TABLES_PREFIX, index)
3382            .map_err(ShardTreeError::Storage)
3383    }
3384
3385    #[cfg(feature = "orchard")]
3386    type OrchardShardStore<'a> = crate::OrchardShardStore<&'a rusqlite::Transaction<'a>>;
3387
3388    #[cfg(feature = "orchard")]
3389    fn with_orchard_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
3390    where
3391        for<'a> F:
3392            FnMut(&'a mut OrchardCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
3393        E: From<ShardTreeError<Self::Error>>,
3394    {
3395        let mut shardtree = orchard_tree(self.conn.0)?;
3396        let result = callback(&mut shardtree)?;
3397
3398        Ok(result)
3399    }
3400
3401    #[cfg(feature = "orchard")]
3402    fn put_orchard_subtree_roots(
3403        &mut self,
3404        start_index: u64,
3405        roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
3406    ) -> Result<(), ShardTreeError<Self::Error>> {
3407        put_shard_roots::<_, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, ORCHARD_SHARD_HEIGHT>(
3408            self.conn.0,
3409            ORCHARD_TABLES_PREFIX,
3410            start_index,
3411            roots,
3412        )
3413    }
3414
3415    #[cfg(feature = "orchard")]
3416    fn get_orchard_subtree_root(
3417        &mut self,
3418        index: u64,
3419    ) -> Result<Option<orchard::tree::MerkleHashOrchard>, ShardTreeError<Self::Error>> {
3420        wallet::commitment_tree::get_subtree_root(self.conn.0, ORCHARD_TABLES_PREFIX, index)
3421            .map_err(ShardTreeError::Storage)
3422    }
3423
3424    #[cfg(feature = "orchard")]
3425    fn put_ironwood_subtree_roots(
3426        &mut self,
3427        start_index: u64,
3428        roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
3429    ) -> Result<(), ShardTreeError<Self::Error>> {
3430        put_shard_roots::<_, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, ORCHARD_SHARD_HEIGHT>(
3431            self.conn.0,
3432            IRONWOOD_TABLES_PREFIX,
3433            start_index,
3434            roots,
3435        )
3436    }
3437
3438    #[cfg(feature = "orchard")]
3439    fn get_ironwood_subtree_root(
3440        &mut self,
3441        index: u64,
3442    ) -> Result<Option<orchard::tree::MerkleHashOrchard>, ShardTreeError<Self::Error>> {
3443        wallet::commitment_tree::get_subtree_root(self.conn.0, IRONWOOD_TABLES_PREFIX, index)
3444            .map_err(ShardTreeError::Storage)
3445    }
3446
3447    #[cfg(feature = "orchard")]
3448    fn with_ironwood_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<Option<A>, E>
3449    where
3450        for<'a> F:
3451            FnMut(&'a mut IronwoodCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
3452        E: From<ShardTreeError<Self::Error>>,
3453    {
3454        let mut shardtree = ironwood_tree(self.conn.0)?;
3455        let result = callback(&mut shardtree)?;
3456
3457        Ok(Some(result))
3458    }
3459}
3460
3461#[cfg(feature = "transparent-inputs")]
3462impl<'a, C: Borrow<rusqlite::Transaction<'a>>, P: consensus::Parameters, CL: Clock, R: Rng>
3463    AddressStore for WalletDb<C, P, CL, R>
3464{
3465    type Error = SqliteClientError;
3466    type AccountRef = AccountRef;
3467
3468    fn find_gap_start(
3469        &self,
3470        account_ref: Self::AccountRef,
3471        key_scope: TransparentKeyScope,
3472        gap_limit: u32,
3473    ) -> Result<Option<NonHardenedChildIndex>, Self::Error> {
3474        wallet::transparent::find_gap_start(self.conn.borrow(), account_ref, key_scope, gap_limit)
3475    }
3476
3477    fn store_address_range(
3478        &mut self,
3479        account_id: Self::AccountRef,
3480        key_scope: TransparentKeyScope,
3481        list: Vec<(Address, TransparentAddress, NonHardenedChildIndex)>,
3482    ) -> Result<(), Self::Error> {
3483        wallet::transparent::store_address_range(
3484            self.conn.borrow(),
3485            &self.params,
3486            account_id,
3487            key_scope,
3488            list,
3489        )
3490    }
3491}
3492
3493#[cfg(feature = "orchard")]
3494impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletDb<C, P, CL, R> {
3495    /// Return all Orchard notes received at or before `height`
3496    /// and unspent as of that height, for the given account.
3497    ///
3498    /// Unlike [`InputSource::select_unspent_notes`] (which applies confirmation,
3499    /// dust, and expiry filters for transaction construction), this returns every
3500    /// note that existed and was unspent at the given height.
3501    ///
3502    /// This function does not verify that a Merkle witness can be constructed
3503    /// for each returned note at `height`. Witness construction is a separate
3504    /// concern intended to be handled by the callers. As an example, a companion
3505    /// `WalletDb::generate_orchard_witnesses_at_historical_height` returns an
3506    /// actionable error for any position the wallet cannot witness at `height`
3507    /// (for example, because the wallet has not synced through `height`, the checkpoint was pruned,
3508    /// or the position does not belong to the wallet).
3509    pub fn get_unspent_orchard_notes_at_historical_height(
3510        &self,
3511        account: AccountUuid,
3512        height: BlockHeight,
3513    ) -> Result<Vec<ReceivedNote<ReceivedNoteId, orchard::note::Note>>, SqliteClientError> {
3514        wallet::orchard::get_unspent_orchard_notes_at_historical_height(
3515            self.conn.borrow(),
3516            &self.params,
3517            account,
3518            height,
3519        )
3520    }
3521
3522    /// Returns every Ironwood note received by `height` that was unspent at that height.
3523    ///
3524    /// This does not apply transaction construction filters or check witness availability.
3525    /// Use [`Self::generate_ironwood_witnesses_at_historical_height`] to check the latter.
3526    ///
3527    /// # Errors
3528    ///
3529    /// Returns an error if the query fails or a note cannot be reconstructed.
3530    pub fn get_unspent_ironwood_notes_at_historical_height(
3531        &self,
3532        account: AccountUuid,
3533        height: BlockHeight,
3534    ) -> Result<Vec<ReceivedNote<ReceivedNoteId, orchard::note::Note>>, SqliteClientError> {
3535        wallet::orchard::get_unspent_ironwood_notes_at_historical_height(
3536            self.conn.borrow(),
3537            &self.params,
3538            account,
3539            height,
3540        )
3541    }
3542
3543    /// Generates Orchard Merkle witnesses at a historical height.
3544    ///
3545    /// Loads the wallet's Orchard shard data into an ephemeral in-memory
3546    /// `ShardStore`, inserts the provided frontier at `height` as a checkpoint,
3547    /// and generates a witness for each of the given note positions.
3548    ///
3549    /// The caller must provide the valid frontier at the given height. The wallet DB
3550    /// is strictly read-only; shard data is read but not modified.
3551    ///
3552    /// # Errors
3553    ///
3554    /// Returns:
3555    /// - [`SqliteClientError::CommitmentTree`] if reading the wallet's shard
3556    ///   or cap data fails, or if the shard data reconstructed from the
3557    ///   wallet is internally inconsistent at a node the computation
3558    ///   requires.
3559    /// - [`SqliteClientError::HistoricalFrontierInvalid`] if
3560    ///   `frontier_at_height` is inconsistent with the shard data
3561    ///   reconstructed from the wallet at `height`.
3562    /// - [`SqliteClientError::HistoricalWitnessUnavailable`] if a witness
3563    ///   cannot be generated for one of `note_positions` at `height` (most
3564    ///   commonly because the wallet has not yet synced through that
3565    ///   height).
3566    pub fn generate_orchard_witnesses_at_historical_height(
3567        &self,
3568        note_positions: &[Position],
3569        frontier_at_height: incrementalmerkletree::frontier::NonEmptyFrontier<
3570            orchard::tree::MerkleHashOrchard,
3571        >,
3572        height: BlockHeight,
3573    ) -> Result<
3574        Vec<
3575            incrementalmerkletree::MerklePath<
3576                orchard::tree::MerkleHashOrchard,
3577                { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
3578            >,
3579        >,
3580        SqliteClientError,
3581    > {
3582        wallet::commitment_tree::generate_orchard_witnesses_at_historical_height(
3583            self.conn.borrow(),
3584            note_positions,
3585            frontier_at_height,
3586            height,
3587        )
3588    }
3589
3590    /// Generates Ironwood Merkle witnesses at a historical height.
3591    ///
3592    /// Loads the wallet's Ironwood shard data into an ephemeral in-memory
3593    /// `ShardStore`, inserts the provided frontier at `height` as a checkpoint,
3594    /// and generates a witness for each of the given note positions.
3595    ///
3596    /// The caller must provide the valid frontier at the given height. The wallet DB
3597    /// is strictly read-only; shard data is read but not modified.
3598    ///
3599    /// # Errors
3600    ///
3601    /// Returns:
3602    /// - [`SqliteClientError::CommitmentTree`] if reading the wallet's shard
3603    ///   or cap data fails, or if the shard data reconstructed from the
3604    ///   wallet is internally inconsistent at a node the computation
3605    ///   requires.
3606    /// - [`SqliteClientError::HistoricalFrontierInvalid`] if
3607    ///   `frontier_at_height` is inconsistent with the shard data
3608    ///   reconstructed from the wallet at `height`.
3609    /// - [`SqliteClientError::HistoricalWitnessUnavailable`] if a witness
3610    ///   cannot be generated for one of `note_positions` at `height` (most
3611    ///   commonly because the wallet has not yet synced through that
3612    ///   height).
3613    pub fn generate_ironwood_witnesses_at_historical_height(
3614        &self,
3615        note_positions: &[Position],
3616        frontier_at_height: incrementalmerkletree::frontier::NonEmptyFrontier<
3617            orchard::tree::MerkleHashOrchard,
3618        >,
3619        height: BlockHeight,
3620    ) -> Result<
3621        Vec<
3622            incrementalmerkletree::MerklePath<
3623                orchard::tree::MerkleHashOrchard,
3624                { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
3625            >,
3626        >,
3627        SqliteClientError,
3628    > {
3629        wallet::commitment_tree::generate_ironwood_witnesses_at_historical_height(
3630            self.conn.borrow(),
3631            note_positions,
3632            frontier_at_height,
3633            height,
3634        )
3635    }
3636}
3637
3638/// A handle for the SQLite block source.
3639pub struct BlockDb(rusqlite::Connection);
3640
3641impl BlockDb {
3642    /// Opens a connection to the wallet database stored at the specified path.
3643    pub fn for_path<P: AsRef<Path>>(path: P) -> Result<Self, rusqlite::Error> {
3644        rusqlite::Connection::open(path).map(BlockDb)
3645    }
3646
3647    #[cfg(any(test, feature = "test-dependencies"))]
3648    pub(crate) fn from_connection(conn: rusqlite::Connection) -> Self {
3649        Self(conn)
3650    }
3651}
3652
3653impl BlockSource for BlockDb {
3654    type Error = SqliteClientError;
3655
3656    fn with_blocks<F, DbErrT>(
3657        &self,
3658        from_height: Option<BlockHeight>,
3659        limit: Option<usize>,
3660        with_row: F,
3661    ) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>
3662    where
3663        F: FnMut(CompactBlock) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>,
3664    {
3665        chain::blockdb_with_blocks(self, from_height, limit, with_row)
3666    }
3667}
3668
3669/// A block source that reads block data from disk and block metadata from a SQLite database.
3670///
3671/// This block source expects each compact block to be stored on disk in the `blocks` subdirectory
3672/// of the `blockstore_root` path provided at the time of construction. Each block should be
3673/// written, as the serialized bytes of its protobuf representation, where the path for each block
3674/// has the pattern:
3675///
3676/// `<blockstore_root>/blocks/<block_height>-<block_hash>-compactblock`
3677///
3678/// where `<block_height>` is the decimal value of the height at which the block was mined, and
3679/// `<block_hash>` is the hexadecimal representation of the block hash, as produced by the
3680/// [`fmt::Display`] implementation for [`zcash_primitives::block::BlockHash`].
3681///
3682/// This block source is intended to be used with the following data flow:
3683/// * When the cache is being filled:
3684///   * The caller requests the current maximum height at which cached data is available
3685///     using [`FsBlockDb::get_max_cached_height`]. If no cached data is available, the caller
3686///     can use the wallet's synced-to height for the following operations instead.
3687///   * (recommended for privacy) the caller should round the returned height down to some 100- /
3688///     1000-block boundary.
3689///   * The caller uses the lightwalletd's `getblock` gRPC method to obtain a stream of blocks.
3690///     For each block returned, the caller writes the compact block to `blocks_dir` using the
3691///     path format specified above. It is fine to overwrite an existing block, since block hashes
3692///     are immutable and collision-resistant.
3693///   * Once a caller-determined number of blocks have been successfully written to disk, the
3694///     caller should invoke [`FsBlockDb::write_block_metadata`] with the metadata for each block
3695///     written to disk.
3696/// * The cache can then be scanned using the [`BlockSource`] implementation, providing the
3697///   wallet's synced-to-height as a starting point.
3698/// * When part of the cache is no longer needed:
3699///   * The caller determines some height `H` that is the earliest block data it needs to preserve.
3700///     This might be determined based on where the wallet is fully-synced to, or other heuristics.
3701///   * The caller searches the defined filesystem folder for all files beginning in `HEIGHT-*` where
3702///     `HEIGHT < H`, and deletes those files.
3703///
3704/// Note: This API is unstable, and may change in the future. In particular, the [`BlockSource`]
3705/// API and the above description currently assume that scanning is performed in linear block
3706/// order; this assumption is likely to be weakened and/or removed in a future update.
3707#[cfg(feature = "unstable")]
3708pub struct FsBlockDb {
3709    conn: rusqlite::Connection,
3710    blocks_dir: PathBuf,
3711}
3712
3713/// Errors that can be generated by the filesystem/sqlite-backed
3714/// block source.
3715#[derive(Debug)]
3716#[cfg(feature = "unstable")]
3717#[non_exhaustive]
3718pub enum FsBlockDbError {
3719    /// Filesystem I/O error.
3720    Fs(io::Error),
3721    /// SQLite database error.
3722    Db(rusqlite::Error),
3723    /// Protobuf decoding error.
3724    Protobuf(prost::DecodeError),
3725    /// The expected block file was not found at the given path.
3726    MissingBlockPath(PathBuf),
3727    /// The block store root directory is invalid.
3728    InvalidBlockstoreRoot(PathBuf),
3729    /// A block file path within the store is invalid.
3730    InvalidBlockPath(PathBuf),
3731    /// Data in the block store is corrupted.
3732    CorruptedData(String),
3733    /// The requested block was not found in the cache.
3734    CacheMiss(BlockHeight),
3735}
3736
3737#[cfg(feature = "unstable")]
3738impl From<io::Error> for FsBlockDbError {
3739    fn from(err: io::Error) -> Self {
3740        FsBlockDbError::Fs(err)
3741    }
3742}
3743
3744#[cfg(feature = "unstable")]
3745impl From<rusqlite::Error> for FsBlockDbError {
3746    fn from(err: rusqlite::Error) -> Self {
3747        FsBlockDbError::Db(err)
3748    }
3749}
3750
3751#[cfg(feature = "unstable")]
3752impl From<prost::DecodeError> for FsBlockDbError {
3753    fn from(e: prost::DecodeError) -> Self {
3754        FsBlockDbError::Protobuf(e)
3755    }
3756}
3757
3758#[cfg(feature = "unstable")]
3759impl FsBlockDb {
3760    /// Creates a filesystem-backed block store at the given path.
3761    ///
3762    /// This will construct or open a SQLite database at the path
3763    /// `<fsblockdb_root>/blockmeta.sqlite` and will ensure that a directory exists at
3764    /// `<fsblockdb_root>/blocks` where this block store will expect to find serialized block
3765    /// files as described for [`FsBlockDb`].
3766    ///
3767    /// An application using this constructor should ensure that they call
3768    /// [`crate::chain::init::init_blockmeta_db`] at application startup to ensure
3769    /// that the resulting metadata database is properly initialized and has had all required
3770    /// migrations applied before use.
3771    pub fn for_path<P: AsRef<Path>>(fsblockdb_root: P) -> Result<Self, FsBlockDbError> {
3772        let meta = fs::metadata(&fsblockdb_root).map_err(FsBlockDbError::Fs)?;
3773        if meta.is_dir() {
3774            let db_path = fsblockdb_root.as_ref().join("blockmeta.sqlite");
3775            let blocks_dir = fsblockdb_root.as_ref().join("blocks");
3776            fs::create_dir_all(&blocks_dir)?;
3777            Ok(FsBlockDb {
3778                conn: rusqlite::Connection::open(db_path).map_err(FsBlockDbError::Db)?,
3779                blocks_dir,
3780            })
3781        } else {
3782            Err(FsBlockDbError::InvalidBlockstoreRoot(
3783                fsblockdb_root.as_ref().to_path_buf(),
3784            ))
3785        }
3786    }
3787
3788    /// Returns the maximum height of blocks known to the block metadata database.
3789    pub fn get_max_cached_height(&self) -> Result<Option<BlockHeight>, FsBlockDbError> {
3790        Ok(chain::blockmetadb_get_max_cached_height(&self.conn)?)
3791    }
3792
3793    /// Adds a set of block metadata entries to the metadata database, overwriting any
3794    /// existing entries at the given block heights.
3795    ///
3796    /// This will return an error if any block file corresponding to one of these metadata records
3797    /// is absent from the blocks directory.
3798    pub fn write_block_metadata(&self, block_meta: &[BlockMeta]) -> Result<(), FsBlockDbError> {
3799        for m in block_meta {
3800            let block_path = m.block_file_path(&self.blocks_dir);
3801            match fs::metadata(&block_path) {
3802                Err(e) => {
3803                    return Err(match e.kind() {
3804                        io::ErrorKind::NotFound => FsBlockDbError::MissingBlockPath(block_path),
3805                        _ => FsBlockDbError::Fs(e),
3806                    });
3807                }
3808                Ok(meta) => {
3809                    if !meta.is_file() {
3810                        return Err(FsBlockDbError::InvalidBlockPath(block_path));
3811                    }
3812                }
3813            }
3814        }
3815
3816        Ok(chain::blockmetadb_insert(&self.conn, block_meta)?)
3817    }
3818
3819    /// Returns the metadata for the block with the given height, if it exists in the
3820    /// database.
3821    pub fn find_block(&self, height: BlockHeight) -> Result<Option<BlockMeta>, FsBlockDbError> {
3822        Ok(chain::blockmetadb_find_block(&self.conn, height)?)
3823    }
3824
3825    /// Rewinds the BlockMeta Db to the `block_height` provided.
3826    ///
3827    /// This doesn't delete any files referenced by the records
3828    /// stored in BlockMeta.
3829    ///
3830    /// If the requested height is greater than or equal to the height
3831    /// of the last scanned block, or if the DB is empty, this function
3832    /// does nothing.
3833    pub fn truncate_to_height(&self, block_height: BlockHeight) -> Result<(), FsBlockDbError> {
3834        Ok(chain::blockmetadb_truncate_to_height(
3835            &self.conn,
3836            block_height,
3837        )?)
3838    }
3839}
3840
3841#[cfg(feature = "unstable")]
3842impl BlockSource for FsBlockDb {
3843    type Error = FsBlockDbError;
3844
3845    fn with_blocks<F, DbErrT>(
3846        &self,
3847        from_height: Option<BlockHeight>,
3848        limit: Option<usize>,
3849        with_row: F,
3850    ) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>
3851    where
3852        F: FnMut(CompactBlock) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>,
3853    {
3854        fsblockdb_with_blocks(self, from_height, limit, with_row)
3855    }
3856}
3857
3858#[cfg(feature = "unstable")]
3859impl std::fmt::Display for FsBlockDbError {
3860    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3861        match self {
3862            FsBlockDbError::Fs(io_error) => {
3863                write!(f, "Failed to access the file system: {io_error}")
3864            }
3865            FsBlockDbError::Db(e) => {
3866                write!(f, "There was a problem with the sqlite db: {e}")
3867            }
3868            FsBlockDbError::Protobuf(e) => {
3869                write!(f, "Failed to parse protobuf-encoded record: {e}")
3870            }
3871            FsBlockDbError::MissingBlockPath(block_path) => {
3872                write!(
3873                    f,
3874                    "CompactBlock file expected but not found at {}",
3875                    block_path.display(),
3876                )
3877            }
3878            FsBlockDbError::InvalidBlockstoreRoot(fsblockdb_root) => {
3879                write!(
3880                    f,
3881                    "The block storage root {} is not a directory",
3882                    fsblockdb_root.display(),
3883                )
3884            }
3885            FsBlockDbError::InvalidBlockPath(block_path) => {
3886                write!(
3887                    f,
3888                    "CompactBlock path {} is not a file",
3889                    block_path.display(),
3890                )
3891            }
3892            FsBlockDbError::CorruptedData(e) => {
3893                write!(
3894                    f,
3895                    "The block cache has corrupted data and this caused an error: {e}",
3896                )
3897            }
3898            FsBlockDbError::CacheMiss(height) => {
3899                write!(
3900                    f,
3901                    "Requested height {height} does not exist in the block cache"
3902                )
3903            }
3904        }
3905    }
3906}
3907
3908#[cfg(test)]
3909#[macro_use]
3910extern crate assert_matches;
3911
3912#[cfg(test)]
3913mod tests {
3914    use std::time::{Duration, SystemTime};
3915
3916    use secrecy::{ExposeSecret, Secret, SecretVec};
3917    use uuid::Uuid;
3918    #[cfg(feature = "orchard")]
3919    use zcash_client_backend::data_api::error::FindAccountForAddressError;
3920    use zcash_client_backend::data_api::{
3921        Account, AccountBirthday, AccountPurpose, AccountSource, SAPLING_SHARD_HEIGHT,
3922        WalletCommitmentTrees, WalletRead, WalletTest, WalletWrite,
3923        chain::{ChainState, CommitmentTreeRoot},
3924        testing::{TestBuilder, TestState},
3925    };
3926    use zcash_keys::{
3927        address::{Address, UnifiedAddress},
3928        keys::{
3929            ReceiverRequirement::*, UnifiedAddressRequest, UnifiedFullViewingKey,
3930            UnifiedIncomingViewingKey, UnifiedSpendingKey,
3931        },
3932    };
3933    use zcash_primitives::block::BlockHash;
3934    use zcash_protocol::{consensus, local_consensus::LocalNetwork};
3935    use zip32::DiversifierIndex;
3936
3937    use crate::{
3938        AccountUuid,
3939        error::SqliteClientError,
3940        testing::db::{TestDb, TestDbFactory},
3941        util::Clock as _,
3942        wallet::MIN_SHIELDED_DIVERSIFIER_OFFSET,
3943    };
3944
3945    use incrementalmerkletree::Hashable as _;
3946    #[cfg(feature = "unstable")]
3947    use {
3948        crate::testing::FsBlockCache,
3949        zcash_client_backend::data_api::testing::AddressType,
3950        zcash_keys::keys::sapling,
3951        zcash_protocol::{consensus::NetworkConstants, value::Zatoshis},
3952    };
3953    #[cfg(all(feature = "orchard", feature = "transparent-inputs"))]
3954    use {
3955        crate::{AccountRef, wallet::transparent},
3956        ::transparent::keys::{NonHardenedChildIndex, TransparentKeyScope},
3957        rusqlite::named_params,
3958    };
3959    #[cfg(feature = "transparent-inputs")]
3960    use {
3961        crate::{GapLimits, testing::BlockCache, wallet::transparent::transaction_data_requests},
3962        std::collections::BTreeSet,
3963        zcash_client_backend::data_api::TransactionDataRequest,
3964    };
3965
3966    #[test]
3967    fn get_wallet_recover_until_is_max_across_accounts() {
3968        let mut st = TestBuilder::new()
3969            .with_data_store_factory(TestDbFactory::default())
3970            .with_account_from_sapling_activation(BlockHash([0; 32]))
3971            .build();
3972        // The fixture account has no recovery horizon.
3973        assert_eq!(st.wallet().get_wallet_recover_until().unwrap(), None);
3974        // The result reflects the maximum recover_until height across accounts.
3975        st.wallet_mut()
3976            .conn_mut()
3977            .execute("UPDATE accounts SET recover_until_height = 123456", [])
3978            .unwrap();
3979        assert_eq!(
3980            st.wallet().get_wallet_recover_until().unwrap(),
3981            Some(zcash_protocol::consensus::BlockHeight::from_u32(123456))
3982        );
3983    }
3984
3985    #[test]
3986    fn get_subtree_root_round_trips_put_subtree_roots() {
3987        let mut st = TestBuilder::new()
3988            .with_data_store_factory(TestDbFactory::default())
3989            .build();
3990        let root = ::sapling::Node::empty_root(SAPLING_SHARD_HEIGHT.into());
3991        st.wallet_mut()
3992            .db_mut()
3993            .put_sapling_subtree_roots(
3994                0,
3995                &[CommitmentTreeRoot::from_parts(
3996                    zcash_protocol::consensus::BlockHeight::from_u32(500_000),
3997                    root,
3998                )],
3999            )
4000            .unwrap();
4001        assert_eq!(
4002            st.wallet_mut()
4003                .db_mut()
4004                .get_sapling_subtree_root(0)
4005                .unwrap(),
4006            Some(root)
4007        );
4008        assert_eq!(
4009            st.wallet_mut()
4010                .db_mut()
4011                .get_sapling_subtree_root(1)
4012                .unwrap(),
4013            None
4014        );
4015    }
4016
4017    /// Builds a test wallet with a single account and an application-owned extension
4018    /// table (`ext_test_notes`), simulating an external migration having created it.
4019    fn ext_test_state() -> TestState<(), TestDb, LocalNetwork> {
4020        let mut st = TestBuilder::new()
4021            .with_data_store_factory(TestDbFactory::default())
4022            .with_account_from_sapling_activation(BlockHash([0; 32]))
4023            .build();
4024        st.wallet_mut()
4025            .conn_mut()
4026            .execute_batch(
4027                "CREATE TABLE ext_test_notes (account_uuid BLOB NOT NULL, note TEXT NOT NULL);",
4028            )
4029            .unwrap();
4030        st
4031    }
4032
4033    /// Returns an owned seed and birthday suitable for creating a second account, released
4034    /// from any borrow of `st` so the wallet may be borrowed mutably afterwards.
4035    fn account_creation_inputs(
4036        st: &TestState<(), TestDb, LocalNetwork>,
4037    ) -> (SecretVec<u8>, AccountBirthday) {
4038        let birthday = st.test_account().unwrap().birthday().clone();
4039        let seed = SecretVec::new(st.test_seed().unwrap().expose_secret().to_vec());
4040        (seed, birthday)
4041    }
4042
4043    #[test]
4044    fn transactionally_with_extension_commits_both() {
4045        let mut st = ext_test_state();
4046        let (seed, birthday) = account_creation_inputs(&st);
4047
4048        let new_account = st
4049            .wallet_mut()
4050            .db_mut()
4051            .transactionally_with_extension::<_, _, SqliteClientError>(|wdb, ext| {
4052                let (account_id, _usk) = wdb.create_account("second", &seed, &birthday, None)?;
4053                ext.execute(
4054                    "INSERT INTO ext_test_notes (account_uuid, note) VALUES (?1, ?2)",
4055                    (account_id.expose_uuid(), "hello"),
4056                )?;
4057                Ok(account_id)
4058            })
4059            .unwrap();
4060
4061        // The wallet write persisted.
4062        let account_exists: bool = st
4063            .wallet()
4064            .conn()
4065            .query_row(
4066                "SELECT EXISTS(SELECT 1 FROM accounts WHERE uuid = ?1)",
4067                [new_account.expose_uuid()],
4068                |row| row.get(0),
4069            )
4070            .unwrap();
4071        assert!(account_exists);
4072
4073        // The extension write persisted, in the same transaction.
4074        let note: String = st
4075            .wallet()
4076            .conn()
4077            .query_row(
4078                "SELECT note FROM ext_test_notes WHERE account_uuid = ?1",
4079                [new_account.expose_uuid()],
4080                |row| row.get(0),
4081            )
4082            .unwrap();
4083        assert_eq!(note, "hello");
4084    }
4085
4086    #[test]
4087    fn transactionally_with_extension_rolls_back_on_error() {
4088        let mut st = ext_test_state();
4089        let (seed, birthday) = account_creation_inputs(&st);
4090
4091        let accounts_before: i64 = st
4092            .wallet()
4093            .conn()
4094            .query_row("SELECT COUNT(*) FROM accounts", [], |row| row.get(0))
4095            .unwrap();
4096
4097        let result: Result<(), SqliteClientError> = st
4098            .wallet_mut()
4099            .db_mut()
4100            .transactionally_with_extension(|wdb, ext| {
4101                let (account_id, _usk) = wdb.create_account("second", &seed, &birthday, None)?;
4102                ext.execute(
4103                    "INSERT INTO ext_test_notes (account_uuid, note) VALUES (?1, ?2)",
4104                    (account_id.expose_uuid(), "hello"),
4105                )?;
4106                // Fail after both writes; everything in this transaction must roll back.
4107                Err(SqliteClientError::AccountUnknown)
4108            });
4109        assert_matches!(result, Err(SqliteClientError::AccountUnknown));
4110
4111        // Neither the wallet write nor the extension write persisted.
4112        let accounts_after: i64 = st
4113            .wallet()
4114            .conn()
4115            .query_row("SELECT COUNT(*) FROM accounts", [], |row| row.get(0))
4116            .unwrap();
4117        assert_eq!(accounts_before, accounts_after);
4118        let ext_rows: i64 = st
4119            .wallet()
4120            .conn()
4121            .query_row("SELECT COUNT(*) FROM ext_test_notes", [], |row| row.get(0))
4122            .unwrap();
4123        assert_eq!(ext_rows, 0);
4124    }
4125
4126    #[test]
4127    fn transactionally_with_extension_denies_wallet_table_write() {
4128        let mut st = ext_test_state();
4129
4130        let accounts_before: i64 = st
4131            .wallet()
4132            .conn()
4133            .query_row("SELECT COUNT(*) FROM accounts", [], |row| row.get(0))
4134            .unwrap();
4135
4136        let result: Result<(), SqliteClientError> = st
4137            .wallet_mut()
4138            .db_mut()
4139            .transactionally_with_extension(|_wdb, ext| {
4140                // Deleting from a wallet-owned table is denied by the authorizer.
4141                ext.execute("DELETE FROM accounts", [])?;
4142                Ok(())
4143            });
4144        assert!(result.is_err());
4145
4146        // The wallet handle remains usable, and the denied statement had no effect: the
4147        // account is still present and a fresh wallet read succeeds.
4148        let accounts_after: i64 = st
4149            .wallet()
4150            .conn()
4151            .query_row("SELECT COUNT(*) FROM accounts", [], |row| row.get(0))
4152            .unwrap();
4153        assert_eq!(accounts_before, accounts_after);
4154        assert!(!st.wallet().get_account_ids().unwrap().is_empty());
4155    }
4156
4157    #[test]
4158    fn transactionally_with_extension_denies_transaction_control() {
4159        let mut st = ext_test_state();
4160
4161        let result: Result<(), SqliteClientError> = st
4162            .wallet_mut()
4163            .db_mut()
4164            .transactionally_with_extension(|_wdb, ext| {
4165                // Transaction-control statements are denied so extension SQL cannot break
4166                // out of the enclosing transaction.
4167                ext.execute("COMMIT", [])?;
4168                Ok(())
4169            });
4170        assert!(result.is_err());
4171
4172        // The wallet handle remains usable afterwards.
4173        assert!(!st.wallet().get_account_ids().unwrap().is_empty());
4174    }
4175
4176    #[test]
4177    fn validate_seed() {
4178        let st = TestBuilder::new()
4179            .with_data_store_factory(TestDbFactory::default())
4180            .with_account_from_sapling_activation(BlockHash([0; 32]))
4181            .build();
4182        let account = st.test_account().unwrap();
4183
4184        assert!({
4185            st.wallet()
4186                .validate_seed(account.id(), st.test_seed().unwrap())
4187                .unwrap()
4188        });
4189
4190        // check that passing an invalid account results in a failure
4191        assert!({
4192            let wrong_account_uuid = AccountUuid(Uuid::nil());
4193            !st.wallet()
4194                .validate_seed(wrong_account_uuid, st.test_seed().unwrap())
4195                .unwrap()
4196        });
4197
4198        // check that passing an invalid seed results in a failure
4199        assert!({
4200            !st.wallet()
4201                .validate_seed(account.id(), &SecretVec::new(vec![1u8; 32]))
4202                .unwrap()
4203        });
4204    }
4205
4206    #[test]
4207    pub(crate) fn get_next_available_address() {
4208        let mut st = TestBuilder::new()
4209            .with_data_store_factory(TestDbFactory::default())
4210            .with_account_from_sapling_activation(BlockHash([0; 32]))
4211            .build();
4212        let account = st.test_account().cloned().unwrap();
4213
4214        // We have to have the chain tip height in order to allocate new addresses, to record the
4215        // exposed-at height.
4216        st.wallet_mut()
4217            .update_chain_tip(account.birthday().height())
4218            .unwrap();
4219
4220        let current_addr = st
4221            .wallet()
4222            .get_last_generated_address_matching(
4223                account.id(),
4224                UnifiedAddressRequest::AllAvailableKeys,
4225            )
4226            .unwrap();
4227        assert!(current_addr.is_some());
4228
4229        let addr2 = st
4230            .wallet_mut()
4231            .get_next_available_address(account.id(), UnifiedAddressRequest::AllAvailableKeys)
4232            .unwrap()
4233            .map(|(a, _)| a);
4234        assert!(addr2.is_some());
4235        assert_ne!(current_addr, addr2);
4236
4237        let addr2_cur = st
4238            .wallet()
4239            .get_last_generated_address_matching(
4240                account.id(),
4241                UnifiedAddressRequest::AllAvailableKeys,
4242            )
4243            .unwrap();
4244        assert_eq!(addr2, addr2_cur);
4245
4246        // Perform similar tests for shielded-only addresses. These should be timestamp-based; we
4247        // will tick the clock between each generation.
4248        #[cfg(feature = "orchard")]
4249        let shielded_only_request = UnifiedAddressRequest::unsafe_custom(Require, Require, Omit);
4250        #[cfg(not(feature = "orchard"))]
4251        let shielded_only_request = UnifiedAddressRequest::unsafe_custom(Omit, Require, Omit);
4252
4253        let cur_shielded_only = st
4254            .wallet()
4255            .get_last_generated_address_matching(account.id(), shielded_only_request)
4256            .unwrap();
4257        // If transparent support is disabled, then the previous "transparent-including"
4258        // addresses were actually shielded-only, so we do have a current address.
4259        #[cfg(not(feature = "transparent-inputs"))]
4260        assert_eq!(cur_shielded_only, addr2);
4261        // If transparent support is enabled, this works as expected.
4262        #[cfg(feature = "transparent-inputs")]
4263        assert!(cur_shielded_only.is_none());
4264
4265        let di_lower = st
4266            .wallet()
4267            .db()
4268            .clock
4269            .now()
4270            .duration_since(SystemTime::UNIX_EPOCH)
4271            .expect("current time is valid")
4272            .as_secs()
4273            .saturating_add(MIN_SHIELDED_DIVERSIFIER_OFFSET);
4274
4275        let (shielded_only, di) = st
4276            .wallet_mut()
4277            .get_next_available_address(account.id(), shielded_only_request)
4278            .unwrap()
4279            .expect("generated a shielded-only address");
4280
4281        // since not every Sapling diversifier index is valid, the resulting index will be bounded
4282        // by the current time, but may not be equal to it
4283        assert!(u128::from(di) >= u128::from(di_lower));
4284
4285        let cur_shielded_only = st
4286            .wallet()
4287            .get_last_generated_address_matching(account.id(), shielded_only_request)
4288            .unwrap()
4289            .expect("retrieved the last-generated shielded-only address");
4290        assert_eq!(cur_shielded_only, shielded_only);
4291
4292        // This gives around a 2^{-32} probability of `di` and `di_2` colliding, which is
4293        // low enough for unit tests.
4294        let collision_offset = 32;
4295
4296        st.wallet_mut()
4297            .db_mut()
4298            .clock
4299            .tick(Duration::from_secs(collision_offset));
4300
4301        let (shielded_only_2, di_2) = st
4302            .wallet_mut()
4303            .get_next_available_address(account.id(), shielded_only_request)
4304            .unwrap()
4305            .expect("generated a shielded-only address");
4306        assert_ne!(shielded_only_2, shielded_only);
4307        assert!(u128::from(di_2) >= u128::from(di_lower) + u128::from(collision_offset));
4308    }
4309
4310    #[test]
4311    pub(crate) fn import_account_hd_0() {
4312        let st = TestBuilder::new()
4313            .with_data_store_factory(TestDbFactory::default())
4314            .with_account_from_sapling_activation(BlockHash([0; 32]))
4315            .set_account_index(zip32::AccountId::ZERO)
4316            .build();
4317        assert_matches!(
4318            st.test_account().unwrap().account().source(),
4319            AccountSource::Derived { derivation, .. } if derivation.account_index() == zip32::AccountId::ZERO);
4320    }
4321
4322    #[test]
4323    pub(crate) fn import_account_hd_1_then_2() {
4324        let mut st = TestBuilder::new()
4325            .with_data_store_factory(TestDbFactory::default())
4326            .build();
4327
4328        let birthday = AccountBirthday::from_parts(
4329            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
4330            None,
4331        );
4332
4333        let seed = Secret::new(vec![0u8; 32]);
4334        let zip32_index_1 = zip32::AccountId::ZERO.next().unwrap();
4335
4336        let first = st
4337            .wallet_mut()
4338            .import_account_hd("", &seed, zip32_index_1, &birthday, None)
4339            .unwrap();
4340        assert_matches!(
4341            first.0.source(),
4342            AccountSource::Derived { derivation, .. } if derivation.account_index() == zip32_index_1);
4343
4344        let zip32_index_2 = zip32_index_1.next().unwrap();
4345        let second = st
4346            .wallet_mut()
4347            .import_account_hd("", &seed, zip32_index_2, &birthday, None)
4348            .unwrap();
4349        assert_matches!(
4350            second.0.source(),
4351            AccountSource::Derived { derivation, .. } if derivation.account_index() == zip32_index_2);
4352    }
4353
4354    fn check_collisions<C, DbT: WalletTest + WalletWrite, P: consensus::Parameters>(
4355        st: &mut TestState<C, DbT, P>,
4356        ufvk: &UnifiedFullViewingKey,
4357        birthday: &AccountBirthday,
4358        is_account_collision: impl Fn(&<DbT as WalletRead>::Error) -> bool,
4359    ) where
4360        DbT::Account: core::fmt::Debug,
4361    {
4362        // Re-importing the same UFVK is a duplicate (no new capability added), so
4363        // it should produce an AccountCollision error.
4364        assert_matches!(
4365            st.wallet_mut()
4366                .import_account_ufvk("", ufvk, birthday, AccountPurpose::Spending { derivation: None }, None),
4367            Err(e) if is_account_collision(&e)
4368        );
4369
4370        // Importing a UFVK with fewer components than the existing account should fail:
4371        // the existing IVK items are not a subset of the new (smaller) FVK's items.
4372        #[cfg(feature = "transparent-inputs")]
4373        {
4374            assert!(ufvk.transparent().is_some());
4375            let subset_ufvk = UnifiedFullViewingKey::new(
4376                None,
4377                ufvk.sapling().cloned(),
4378                #[cfg(feature = "orchard")]
4379                ufvk.orchard().cloned(),
4380            )
4381            .unwrap();
4382            assert_matches!(
4383                st.wallet_mut().import_account_ufvk(
4384                    "",
4385                    &subset_ufvk,
4386                    birthday,
4387                    AccountPurpose::Spending { derivation: None },
4388                    None,
4389                ),
4390                Err(e) if is_account_collision(&e)
4391            );
4392        }
4393
4394        // Remove the Orchard component: still a collision since existing has Orchard.
4395        #[cfg(feature = "orchard")]
4396        {
4397            assert!(ufvk.orchard().is_some());
4398            let subset_ufvk = UnifiedFullViewingKey::new(
4399                #[cfg(feature = "transparent-inputs")]
4400                ufvk.transparent().cloned(),
4401                ufvk.sapling().cloned(),
4402                None,
4403            )
4404            .unwrap();
4405            assert_matches!(
4406                st.wallet_mut().import_account_ufvk(
4407                    "",
4408                    &subset_ufvk,
4409                    birthday,
4410                    AccountPurpose::Spending { derivation: None },
4411                    None,
4412                ),
4413                Err(e) if is_account_collision(&e)
4414            );
4415        }
4416    }
4417
4418    #[test]
4419    pub(crate) fn import_account_hd_1_then_conflicts() {
4420        let mut st = TestBuilder::new()
4421            .with_data_store_factory(TestDbFactory::default())
4422            .build();
4423
4424        let birthday = AccountBirthday::from_parts(
4425            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
4426            None,
4427        );
4428
4429        let seed = Secret::new(vec![0u8; 32]);
4430        let zip32_index_1 = zip32::AccountId::ZERO.next().unwrap();
4431
4432        let (first_account, _) = st
4433            .wallet_mut()
4434            .import_account_hd("", &seed, zip32_index_1, &birthday, None)
4435            .unwrap();
4436        let ufvk = first_account.ufvk().unwrap();
4437
4438        assert_matches!(
4439            st.wallet_mut().import_account_hd("", &seed, zip32_index_1, &birthday, None),
4440            Err(SqliteClientError::AccountCollision(id)) if id == first_account.id());
4441
4442        check_collisions(
4443            &mut st,
4444            ufvk,
4445            &birthday,
4446            |e| matches!(e, SqliteClientError::AccountCollision(id) if *id == first_account.id()),
4447        );
4448    }
4449
4450    #[test]
4451    pub(crate) fn import_account_ufvk_then_conflicts() {
4452        let mut st = TestBuilder::new()
4453            .with_data_store_factory(TestDbFactory::default())
4454            .build();
4455
4456        let birthday = AccountBirthday::from_parts(
4457            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
4458            None,
4459        );
4460
4461        let seed = Secret::new(vec![0u8; 32]);
4462        let zip32_index_0 = zip32::AccountId::ZERO;
4463        let usk = UnifiedSpendingKey::from_seed(st.network(), seed.expose_secret(), zip32_index_0)
4464            .unwrap();
4465        let ufvk = usk.to_unified_full_viewing_key();
4466
4467        let account = st
4468            .wallet_mut()
4469            .import_account_ufvk(
4470                "",
4471                &ufvk,
4472                &birthday,
4473                AccountPurpose::Spending { derivation: None },
4474                None,
4475            )
4476            .unwrap();
4477        assert_eq!(
4478            ufvk.encode(st.network()),
4479            account.ufvk().unwrap().encode(st.network())
4480        );
4481
4482        assert_matches!(
4483            account.source(),
4484            AccountSource::Imported {
4485                purpose: AccountPurpose::Spending { .. },
4486                ..
4487            }
4488        );
4489
4490        assert_matches!(
4491            st.wallet_mut().import_account_hd("", &seed, zip32_index_0, &birthday, None),
4492            Err(SqliteClientError::AccountCollision(id)) if id == account.id());
4493
4494        check_collisions(
4495            &mut st,
4496            &ufvk,
4497            &birthday,
4498            |e| matches!(e, SqliteClientError::AccountCollision(id) if *id == account.id()),
4499        );
4500    }
4501
4502    #[test]
4503    pub(crate) fn create_account_then_conflicts() {
4504        let mut st = TestBuilder::new()
4505            .with_data_store_factory(TestDbFactory::default())
4506            .build();
4507
4508        let birthday = AccountBirthday::from_parts(
4509            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
4510            None,
4511        );
4512
4513        let seed = Secret::new(vec![0u8; 32]);
4514        let zip32_index_0 = zip32::AccountId::ZERO;
4515        let seed_based = st
4516            .wallet_mut()
4517            .create_account("", &seed, &birthday, None)
4518            .unwrap();
4519        let seed_based_account = st.wallet().get_account(seed_based.0).unwrap().unwrap();
4520        let ufvk = seed_based_account.ufvk().unwrap();
4521
4522        assert_matches!(
4523            st.wallet_mut().import_account_hd("", &seed, zip32_index_0, &birthday, None),
4524            Err(SqliteClientError::AccountCollision(id)) if id == seed_based.0);
4525
4526        check_collisions(
4527            &mut st,
4528            ufvk,
4529            &birthday,
4530            |e| matches!(e, SqliteClientError::AccountCollision(id) if *id == seed_based.0),
4531        );
4532    }
4533
4534    #[test]
4535    pub(crate) fn ivk_only_account_upgrade_paths() {
4536        let mut st = TestBuilder::new()
4537            .with_data_store_factory(TestDbFactory::default())
4538            .build();
4539
4540        let birthday = AccountBirthday::from_parts(
4541            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
4542            None,
4543        );
4544
4545        let seed = vec![0u8; 32];
4546        let usk =
4547            UnifiedSpendingKey::from_seed(st.network(), &seed, zip32::AccountId::ZERO).unwrap();
4548        let ufvk = usk.to_unified_full_viewing_key();
4549        let full_uivk = ufvk.to_unified_incoming_viewing_key();
4550
4551        // Create a UIVK with only the Sapling component (a subset of the full UIVK).
4552        let sapling_only_uivk = UnifiedIncomingViewingKey::new(
4553            #[cfg(feature = "transparent-inputs")]
4554            None,
4555            full_uivk.sapling().clone(),
4556            #[cfg(feature = "orchard")]
4557            None,
4558        );
4559
4560        // Import the sapling-only IVK as an IVK-only account.
4561        let network = *st.network();
4562        let ivk_account = st
4563            .wallet_mut()
4564            .db_mut()
4565            .transactionally(|wdb| {
4566                crate::wallet::add_account(
4567                    wdb.conn.0,
4568                    &wdb.params,
4569                    "ivk-only",
4570                    &AccountSource::Imported {
4571                        purpose: AccountPurpose::ViewOnly,
4572                        key_source: None,
4573                    },
4574                    crate::wallet::ViewingKey::Incoming(Box::new(sapling_only_uivk.clone())),
4575                    &birthday,
4576                    #[cfg(feature = "transparent-inputs")]
4577                    &crate::GapLimits::default(),
4578                )
4579            })
4580            .unwrap();
4581
4582        // (a) Same IVK import should fail (duplicate, no new capability).
4583        assert_matches!(
4584            st.wallet_mut().db_mut().transactionally(|wdb| {
4585                crate::wallet::add_account(
4586                    wdb.conn.0,
4587                    &wdb.params,
4588                    "duplicate",
4589                    &AccountSource::Imported {
4590                        purpose: AccountPurpose::ViewOnly,
4591                        key_source: None,
4592                    },
4593                    crate::wallet::ViewingKey::Incoming(Box::new(sapling_only_uivk.clone())),
4594                    &birthday,
4595                    #[cfg(feature = "transparent-inputs")]
4596                    &crate::GapLimits::default(),
4597                )
4598            }),
4599            Err(SqliteClientError::AccountCollision(id)) if id == ivk_account.id()
4600        );
4601
4602        // (b) UFVK that subsumes the existing IVK should succeed as an upgrade.
4603        let ufvk_upgraded = st
4604            .wallet_mut()
4605            .import_account_ufvk(
4606                "",
4607                &ufvk,
4608                &birthday,
4609                AccountPurpose::Spending { derivation: None },
4610                None,
4611            )
4612            .unwrap();
4613        // Should return the same account, now with the UFVK.
4614        assert_eq!(ufvk_upgraded.id(), ivk_account.id());
4615        assert!(ufvk_upgraded.ufvk().is_some());
4616        assert_eq!(
4617            ufvk_upgraded.ufvk().unwrap().encode(&network),
4618            ufvk.encode(&network),
4619        );
4620
4621        // (c) IVK import over an account that now has a UFVK should fail.
4622        assert_matches!(
4623            st.wallet_mut().db_mut().transactionally(|wdb| {
4624                crate::wallet::add_account(
4625                    wdb.conn.0,
4626                    &wdb.params,
4627                    "downgrade",
4628                    &AccountSource::Imported {
4629                        purpose: AccountPurpose::ViewOnly,
4630                        key_source: None,
4631                    },
4632                    crate::wallet::ViewingKey::Incoming(Box::new(full_uivk)),
4633                    &birthday,
4634                    #[cfg(feature = "transparent-inputs")]
4635                    &crate::GapLimits::default(),
4636                )
4637            }),
4638            Err(SqliteClientError::AccountCollision(id)) if id == ivk_account.id()
4639        );
4640    }
4641
4642    /// Tests that importing a UIVK with additional items over an existing IVK-only
4643    /// account succeeds as an additive upgrade. Only meaningful when features provide
4644    /// more than one shielded pool (e.g. `orchard`).
4645    #[cfg(feature = "orchard")]
4646    #[test]
4647    pub(crate) fn ivk_over_ivk_additive_upgrade() {
4648        let mut st = TestBuilder::new()
4649            .with_data_store_factory(TestDbFactory::default())
4650            .build();
4651
4652        let birthday = AccountBirthday::from_parts(
4653            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
4654            None,
4655        );
4656
4657        let seed = vec![0u8; 32];
4658        let usk =
4659            UnifiedSpendingKey::from_seed(st.network(), &seed, zip32::AccountId::ZERO).unwrap();
4660        let ufvk = usk.to_unified_full_viewing_key();
4661        let full_uivk = ufvk.to_unified_incoming_viewing_key();
4662        let network = *st.network();
4663
4664        // Create a UIVK with only Sapling (a strict subset of the full UIVK).
4665        let sapling_only_uivk = UnifiedIncomingViewingKey::new(
4666            #[cfg(feature = "transparent-inputs")]
4667            None,
4668            full_uivk.sapling().clone(),
4669            None, // no Orchard
4670        );
4671
4672        // Import the sapling-only IVK.
4673        let ivk_account = st
4674            .wallet_mut()
4675            .db_mut()
4676            .transactionally(|wdb| {
4677                crate::wallet::add_account(
4678                    wdb.conn.0,
4679                    &wdb.params,
4680                    "sapling-only",
4681                    &AccountSource::Imported {
4682                        purpose: AccountPurpose::ViewOnly,
4683                        key_source: None,
4684                    },
4685                    crate::wallet::ViewingKey::Incoming(Box::new(sapling_only_uivk)),
4686                    &birthday,
4687                    #[cfg(feature = "transparent-inputs")]
4688                    &crate::GapLimits::default(),
4689                )
4690            })
4691            .unwrap();
4692
4693        // Import the full UIVK (sapling + orchard) — should upgrade.
4694        let upgraded = st
4695            .wallet_mut()
4696            .db_mut()
4697            .transactionally(|wdb| {
4698                crate::wallet::add_account(
4699                    wdb.conn.0,
4700                    &wdb.params,
4701                    "upgraded",
4702                    &AccountSource::Imported {
4703                        purpose: AccountPurpose::ViewOnly,
4704                        key_source: None,
4705                    },
4706                    crate::wallet::ViewingKey::Incoming(Box::new(full_uivk)),
4707                    &birthday,
4708                    #[cfg(feature = "transparent-inputs")]
4709                    &crate::GapLimits::default(),
4710                )
4711            })
4712            .unwrap();
4713
4714        assert_eq!(upgraded.id(), ivk_account.id());
4715        assert!(upgraded.ufvk().is_none());
4716        assert!(upgraded.uivk().encode(&network) != ivk_account.uivk().encode(&network));
4717    }
4718
4719    #[cfg(feature = "transparent-inputs")]
4720    #[test]
4721    fn transparent_receivers() {
4722        let mut st = TestBuilder::new()
4723            .with_data_store_factory(TestDbFactory::default())
4724            .with_block_cache(BlockCache::new())
4725            .with_account_from_sapling_activation(BlockHash([0; 32]))
4726            .build();
4727        let account = st.test_account().unwrap();
4728        let ufvk = account.usk().to_unified_full_viewing_key();
4729        let (taddr, _) = account.usk().default_transparent_address();
4730        let birthday = account.birthday().height();
4731        let account_id = account.id();
4732
4733        let receivers = st
4734            .wallet()
4735            .get_transparent_receivers(account.id(), false, true)
4736            .unwrap();
4737
4738        // The receiver for the default UA should be in the set.
4739        assert!(
4740            receivers.contains_key(
4741                ufvk.default_address(UnifiedAddressRequest::AllAvailableKeys)
4742                    .expect("A valid default address exists for the UFVK")
4743                    .0
4744                    .transparent()
4745                    .unwrap()
4746            )
4747        );
4748
4749        // The default t-addr should be in the set.
4750        assert!(receivers.contains_key(&taddr));
4751
4752        // The chain tip height must be known in order to query for data requests.
4753        st.wallet_mut().update_chain_tip(birthday).unwrap();
4754
4755        // Transaction data requests should include a request for each ephemeral address
4756        let ephemeral_addrs = st
4757            .wallet()
4758            .get_known_ephemeral_addresses(account_id, None)
4759            .unwrap();
4760
4761        assert_eq!(
4762            ephemeral_addrs.len(),
4763            GapLimits::default().ephemeral() as usize
4764        );
4765
4766        st.wallet_mut()
4767            .db_mut()
4768            .schedule_ephemeral_address_checks()
4769            .unwrap();
4770        let data_requests =
4771            transaction_data_requests(st.wallet().conn(), &st.wallet().db().params, birthday)
4772                .unwrap();
4773
4774        let base_time = st.wallet().db().clock.now();
4775        let day = Duration::from_secs(60 * 60 * 24);
4776        let mut check_times = BTreeSet::new();
4777        for (addr, _) in ephemeral_addrs {
4778            let has_valid_request = data_requests.iter().any(|req| match req {
4779                TransactionDataRequest::TransactionsInvolvingAddress(req) => {
4780                    if let Some(t) = req.request_at() {
4781                        req.address() == addr && t > base_time && {
4782                            let t_delta = t.duration_since(base_time).unwrap();
4783                            // This is an imprecise check; the objective of the randomized time
4784                            // selection is that all ephemeral address checks be performed within a
4785                            // day, and that their check times be distinct. We are bounding the
4786                            // overall delta to two days to limit the probability of test failure,
4787                            // since due to randomization of the check intervals it's possible that
4788                            // scheduled checks might exceed one day; if we find an example where
4789                            // checks exceed two days then we're in some long tail of the
4790                            // distribution.
4791                            let result = t_delta < 2 * day && !check_times.contains(&t);
4792                            check_times.insert(t);
4793                            result
4794                        }
4795                    } else {
4796                        false
4797                    }
4798                }
4799                _ => false,
4800            });
4801
4802            assert!(has_valid_request);
4803        }
4804    }
4805
4806    #[cfg(feature = "unstable")]
4807    #[test]
4808    pub(crate) fn fsblockdb_api() {
4809        let mut st = TestBuilder::new()
4810            .with_data_store_factory(TestDbFactory::default())
4811            .with_block_cache(FsBlockCache::new())
4812            .build();
4813
4814        // The BlockMeta DB starts off empty.
4815        assert_eq!(st.cache().get_max_cached_height().unwrap(), None);
4816
4817        // Generate some fake CompactBlocks.
4818        let seed = [0u8; 32];
4819        let hd_account_index = zip32::AccountId::ZERO;
4820        let extsk = sapling::spending_key(&seed, st.network().coin_type(), hd_account_index);
4821        let dfvk = extsk.to_diversifiable_full_viewing_key();
4822        let (h1, meta1, _) = st.generate_next_block(
4823            &dfvk,
4824            AddressType::DefaultExternal,
4825            Zatoshis::const_from_u64(5),
4826        );
4827        let (h2, meta2, _) = st.generate_next_block(
4828            &dfvk,
4829            AddressType::DefaultExternal,
4830            Zatoshis::const_from_u64(10),
4831        );
4832
4833        // The BlockMeta DB is not updated until we do so explicitly.
4834        assert_eq!(st.cache().get_max_cached_height().unwrap(), None);
4835
4836        // Inform the BlockMeta DB about the newly-persisted CompactBlocks.
4837        st.cache()
4838            .write_block_metadata(&[meta1.block_meta, meta2.block_meta])
4839            .unwrap();
4840
4841        // The BlockMeta DB now sees blocks up to height 2.
4842        assert_eq!(st.cache().get_max_cached_height().unwrap(), Some(h2),);
4843        assert_eq!(st.cache().find_block(h1).unwrap(), Some(meta1.block_meta));
4844        assert_eq!(st.cache().find_block(h2).unwrap(), Some(meta2.block_meta));
4845        assert_eq!(st.cache().find_block(h2 + 1).unwrap(), None);
4846
4847        // Rewinding to height 1 should cause the metadata for height 2 to be deleted.
4848        st.cache().truncate_to_height(h1).unwrap();
4849        assert_eq!(st.cache().get_max_cached_height().unwrap(), Some(h1));
4850        assert_eq!(st.cache().find_block(h1).unwrap(), Some(meta1.block_meta));
4851        assert_eq!(st.cache().find_block(h2).unwrap(), None);
4852        assert_eq!(st.cache().find_block(h2 + 1).unwrap(), None);
4853    }
4854
4855    #[test]
4856    fn find_account_for_address_returns_matching_account_for_own_ua() {
4857        // Create a test wallet with one account and expose one of its own UAs
4858        let mut state = create_test_wallet_with_one_account();
4859        let account = state.test_account().cloned().unwrap();
4860
4861        state
4862            .wallet_mut()
4863            .update_chain_tip(account.birthday().height())
4864            .unwrap();
4865
4866        let (ua, _) = generate_unified_address_with_all_available_keys(&mut state, account.id());
4867
4868        // Asserts that looking up the exact same UA returns the owning account
4869        let result = state
4870            .wallet()
4871            .find_account_for_address(state.network(), &Address::Unified(ua));
4872
4873        assert_eq!(result.unwrap(), Some(account.id()));
4874    }
4875
4876    #[test]
4877    fn find_account_for_address_returns_none_for_unknown_address() {
4878        // Create a test wallet with one account
4879        let st = create_test_wallet_with_one_account();
4880
4881        // Build a transparent address that is not present in the wallet DB
4882        let unknown_address = Address::Transparent(
4883            ::transparent::address::TransparentAddress::PublicKeyHash([0u8; 20]),
4884        );
4885
4886        // Asserts that an unrelated address does not resolve to any account
4887        assert_eq!(
4888            st.wallet()
4889                .find_account_for_address(st.network(), &unknown_address)
4890                .unwrap(),
4891            None
4892        );
4893    }
4894
4895    #[test]
4896    fn find_account_for_address_returns_matching_account_for_receivers_of_own_ua() {
4897        // Create a test wallet with one account and expose one of its own UAs
4898        let mut state = create_test_wallet_with_one_account();
4899        let account = state.test_account().cloned().unwrap();
4900        state
4901            .wallet_mut()
4902            .update_chain_tip(account.birthday().height())
4903            .unwrap();
4904        let (ua, _) = generate_unified_address_with_all_available_keys(&mut state, account.id());
4905        // Asserts that looking up a receiver address extracted from the stored UA
4906        // returns the owning account via the non-UA query path.
4907        if let Some(taddr) = ua.transparent() {
4908            let result = state
4909                .wallet()
4910                .find_account_for_address(state.network(), &Address::Transparent(*taddr));
4911            assert_eq!(result.unwrap(), Some(account.id()));
4912        }
4913        if let Some(pa) = ua.sapling() {
4914            let result = state
4915                .wallet()
4916                .find_account_for_address(state.network(), &Address::Sapling(*pa));
4917            assert_eq!(result.unwrap(), Some(account.id()));
4918        }
4919    }
4920
4921    #[cfg(all(feature = "orchard", feature = "transparent-inputs"))]
4922    #[test]
4923    fn find_account_for_ua_finds_via_transparent_receiver_cache() {
4924        // Create a test wallet with one account
4925        let mut state = create_test_wallet_with_one_account();
4926        let account = state.test_account().cloned().unwrap();
4927        let acc1_id = account.id();
4928
4929        let account_rowid = delete_account_addresses(&mut state, acc1_id);
4930
4931        // Inserts in the DB one row representing a transparent address of that account
4932        let transparent_address =
4933            UnifiedSpendingKey::from_seed(&state.network(), &[7u8; 32], zip32::AccountId::ZERO)
4934                .expect("valid seed")
4935                .to_unified_full_viewing_key()
4936                .default_address(UnifiedAddressRequest::unsafe_custom(Omit, Require, Require))
4937                .unwrap()
4938                .0
4939                .transparent()
4940                .cloned()
4941                .expect("UA must have transparent receiver");
4942
4943        state
4944            .wallet_mut()
4945            .update_chain_tip(account.birthday().height())
4946            .unwrap();
4947
4948        state
4949            .wallet_mut()
4950            .db_mut()
4951            .transactionally(|wdb| {
4952                transparent::store_address_range(
4953                    wdb.conn.0,
4954                    wdb.params(),
4955                    AccountRef(account_rowid),
4956                    TransparentKeyScope::EXTERNAL,
4957                    vec![(
4958                        Address::Transparent(transparent_address),
4959                        transparent_address,
4960                        NonHardenedChildIndex::ZERO,
4961                    )],
4962                )?;
4963                transparent::reserve_next_n_addresses(
4964                    wdb.conn.0,
4965                    wdb.params(),
4966                    AccountRef(account_rowid),
4967                    TransparentKeyScope::EXTERNAL,
4968                    20,
4969                    1,
4970                )?;
4971                Ok::<_, SqliteClientError>(())
4972            })
4973            .unwrap();
4974
4975        // Builds a new UA that shares the transparent receiver, but also has an
4976        // Orchard receiver coming from a different seed
4977        let usk_external =
4978            UnifiedSpendingKey::from_seed(&state.network(), &[99u8; 32], zip32::AccountId::ZERO)
4979                .expect("valid seed");
4980
4981        let o_external = usk_external
4982            .to_unified_full_viewing_key()
4983            .default_address(UnifiedAddressRequest::AllAvailableKeys)
4984            .expect("default address must be derivable")
4985            .0
4986            .orchard()
4987            .cloned()
4988            .expect("orchard receiver must be present");
4989        let address = Address::Unified(
4990            UnifiedAddress::from_receivers(Some(o_external), None, Some(transparent_address))
4991                .expect("orchard+transparent UA must be valid"),
4992        );
4993
4994        // Asserts that the unique possible account is found anyways, based on the transparent address,
4995        // since there are no UA conflicts.
4996        let result = state
4997            .wallet()
4998            .find_account_for_address(state.network(), &address);
4999        assert_eq!(result.unwrap(), Some(acc1_id));
5000    }
5001
5002    #[test]
5003    fn find_account_for_ua_finds_via_sapling() {
5004        // Create a test wallet with one account
5005        let mut state = create_test_wallet_with_one_account();
5006
5007        let birthday = AccountBirthday::from_parts(
5008            ChainState::empty(state.network().sapling.unwrap() - 1, BlockHash([0; 32])),
5009            None,
5010        );
5011        let sapling_activation = state.network().sapling.unwrap();
5012
5013        let (acc1_id, _) = state
5014            .wallet_mut()
5015            .create_account("", &Secret::new(vec![0u8; 32]), &birthday, None)
5016            .unwrap();
5017
5018        state
5019            .wallet_mut()
5020            .update_chain_tip(sapling_activation)
5021            .unwrap();
5022
5023        // Expose a normal UA for that account and keep only its Sapling receiver
5024        let (ua1, _) = generate_unified_address_with_all_available_keys(&mut state, acc1_id);
5025
5026        let sapling_receiver = ua1
5027            .sapling()
5028            .cloned()
5029            .expect("UA must have sapling receiver");
5030
5031        let address = Address::Unified(
5032            {
5033                #[cfg(feature = "orchard")]
5034                {
5035                    UnifiedAddress::from_receivers(None, Some(sapling_receiver), None)
5036                }
5037
5038                #[cfg(not(feature = "orchard"))]
5039                {
5040                    UnifiedAddress::from_receivers(Some(sapling_receiver), None)
5041                }
5042            }
5043            .expect("sapling-only UA must be valid"),
5044        );
5045
5046        // Asserts that the account is still found via the shielded receiver flags path
5047        let result = state
5048            .wallet()
5049            .find_account_for_address(state.network(), &address);
5050
5051        assert_eq!(result.unwrap(), Some(acc1_id));
5052    }
5053
5054    #[cfg(feature = "orchard")]
5055    #[test]
5056    fn find_account_for_ua_finds_via_orchard() {
5057        // Create a test wallet with one account
5058        let mut state = create_test_wallet_with_one_account();
5059
5060        let birthday = AccountBirthday::from_parts(
5061            ChainState::empty(state.network().sapling.unwrap() - 1, BlockHash([0; 32])),
5062            None,
5063        );
5064        let sapling_activation = state.network().sapling.unwrap();
5065
5066        let (acc1_id, _) = state
5067            .wallet_mut()
5068            .create_account("", &Secret::new(vec![0u8; 32]), &birthday, None)
5069            .unwrap();
5070
5071        state
5072            .wallet_mut()
5073            .update_chain_tip(sapling_activation)
5074            .unwrap();
5075
5076        // Expose a normal UA for that account and keep only its Orchard receiver
5077        let (ua1, _) = generate_unified_address_with_all_available_keys(&mut state, acc1_id);
5078
5079        let orchard_receiver = ua1
5080            .orchard()
5081            .cloned()
5082            .expect("UA must have orchard receiver");
5083
5084        let address = Address::Unified(
5085            UnifiedAddress::from_receivers(Some(orchard_receiver), None, None)
5086                .expect("orchard-only UA must be valid"),
5087        );
5088
5089        // Asserts that the account is still found via the shielded receiver flags path
5090        let result = state
5091            .wallet()
5092            .find_account_for_address(state.network(), &address);
5093
5094        assert_eq!(result.unwrap(), Some(acc1_id));
5095    }
5096
5097    /// A UA whose Sapling receiver belongs to account 1 and whose Orchard receiver
5098    /// belongs to account 2 must produce a `UnifiedAddressConflict` error.
5099    #[cfg(feature = "orchard")]
5100    #[test]
5101    fn find_account_for_ua_errors_when_receivers_map_to_different_accounts() {
5102        // Create a test wallet with two different accounts
5103        let mut state = create_test_wallet_with_one_account();
5104
5105        let birthday = AccountBirthday::from_parts(
5106            ChainState::empty(state.network().sapling.unwrap() - 1, BlockHash([0; 32])),
5107            None,
5108        );
5109        let sapling_activation = state.network().sapling.unwrap();
5110
5111        let seed1 = Secret::new(vec![0u8; 32]);
5112        let seed2 = Secret::new(vec![1u8; 32]);
5113
5114        let (acc1_id, _) = state
5115            .wallet_mut()
5116            .create_account("", &seed1, &birthday, None)
5117            .unwrap();
5118        let (acc2_id, _) = state
5119            .wallet_mut()
5120            .create_account("", &seed2, &birthday, None)
5121            .unwrap();
5122
5123        state
5124            .wallet_mut()
5125            .update_chain_tip(sapling_activation)
5126            .unwrap();
5127
5128        let (ua1, _) = generate_unified_address_with_all_available_keys(&mut state, acc1_id);
5129        let (ua2, _) = generate_unified_address_with_all_available_keys(&mut state, acc2_id);
5130
5131        // Build a synthetic UA that mixes receivers from two different accounts
5132        let sapling_receiver_1 = ua1.sapling().cloned().unwrap();
5133        let orchard_receiver_2 = ua2.orchard().cloned().unwrap();
5134
5135        let invalid_address = Address::Unified(
5136            UnifiedAddress::from_receivers(
5137                Some(orchard_receiver_2),
5138                Some(sapling_receiver_1),
5139                None,
5140            )
5141            .expect("sapling+orchard UA must be valid"),
5142        );
5143
5144        // Asserts that the lookup reports a conflict instead of arbitrarily choosing one account
5145        let result = state
5146            .wallet()
5147            .find_account_for_address(state.network(), &invalid_address);
5148        assert!(matches!(
5149            result,
5150            Err(FindAccountForAddressError::UnifiedAddressConflict)
5151        ));
5152    }
5153
5154    fn create_test_wallet_with_one_account() -> TestState<(), TestDb, LocalNetwork> {
5155        TestBuilder::new()
5156            .with_data_store_factory(TestDbFactory::default())
5157            .with_account_from_sapling_activation(BlockHash([0; 32]))
5158            .build()
5159    }
5160
5161    fn generate_unified_address_with_all_available_keys(
5162        state: &mut TestState<(), TestDb, LocalNetwork>,
5163        account_id: AccountUuid,
5164    ) -> (UnifiedAddress, DiversifierIndex) {
5165        state
5166            .wallet_mut()
5167            .get_next_available_address(account_id, UnifiedAddressRequest::AllAvailableKeys)
5168            .unwrap()
5169            .expect("address generation for account 1 must succeed")
5170    }
5171
5172    #[cfg(all(feature = "orchard", feature = "transparent-inputs"))]
5173    fn delete_account_addresses(
5174        state: &mut TestState<(), TestDb, LocalNetwork>,
5175        account_id: AccountUuid,
5176    ) -> i64 {
5177        // Remove from the DB all the addresses associated to the account
5178        let account_rowid: i64 = state
5179            .wallet()
5180            .conn()
5181            .query_row(
5182                "SELECT id FROM accounts WHERE uuid = :uuid",
5183                named_params![":uuid": account_id.expose_uuid()],
5184                |row| row.get(0),
5185            )
5186            .unwrap();
5187
5188        state
5189            .wallet()
5190            .conn()
5191            .execute(
5192                "DELETE FROM addresses WHERE account_id = :account_id",
5193                named_params![":account_id": account_rowid],
5194            )
5195            .unwrap();
5196        account_rowid
5197    }
5198}