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::RngCore;
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::RngCore> 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(self.conn.borrow(), ScanPriority::Historic)
1390    }
1391
1392    fn get_target_and_anchor_heights(
1393        &self,
1394        min_confirmations: NonZeroU32,
1395    ) -> Result<Option<(TargetHeight, BlockHeight)>, Self::Error> {
1396        wallet::get_target_and_anchor_heights(self.conn.borrow(), min_confirmations)
1397    }
1398
1399    fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error> {
1400        wallet::get_tx_height(self.conn.borrow(), txid)
1401    }
1402
1403    fn get_unified_full_viewing_keys(
1404        &self,
1405    ) -> Result<HashMap<Self::AccountId, UnifiedFullViewingKey>, Self::Error> {
1406        wallet::get_unified_full_viewing_keys(self.conn.borrow(), &self.params)
1407    }
1408
1409    fn get_memo(&self, note_id: NoteId) -> Result<Option<Memo>, Self::Error> {
1410        let sent_memo = wallet::get_sent_memo(self.conn.borrow(), note_id)?;
1411        if sent_memo.is_some() {
1412            Ok(sent_memo)
1413        } else {
1414            wallet::get_received_memo(self.conn.borrow(), note_id)
1415        }
1416    }
1417
1418    fn get_transaction(&self, txid: TxId) -> Result<Option<Transaction>, Self::Error> {
1419        wallet::get_transaction(self.conn.borrow(), &self.params, txid)
1420            .map(|res| res.map(|(_, tx)| tx))
1421    }
1422
1423    fn get_sapling_nullifiers(
1424        &self,
1425        query: NullifierQuery,
1426    ) -> Result<Vec<(Self::AccountId, sapling::Nullifier)>, Self::Error> {
1427        wallet::sapling::get_sapling_nullifiers(self.conn.borrow(), query)
1428    }
1429
1430    #[cfg(feature = "orchard")]
1431    fn get_orchard_nullifiers(
1432        &self,
1433        query: NullifierQuery,
1434    ) -> Result<Vec<(Self::AccountId, orchard::note::Nullifier)>, Self::Error> {
1435        wallet::orchard::get_orchard_nullifiers(self.conn.borrow(), query)
1436    }
1437
1438    #[cfg(feature = "orchard")]
1439    fn get_ironwood_nullifiers(
1440        &self,
1441        query: NullifierQuery,
1442    ) -> Result<Vec<(Self::AccountId, orchard::note::Nullifier)>, Self::Error> {
1443        wallet::orchard::get_ironwood_nullifiers(self.conn.borrow(), query)
1444    }
1445
1446    #[cfg(feature = "transparent-inputs")]
1447    fn get_transparent_receivers(
1448        &self,
1449        account: Self::AccountId,
1450        include_change: bool,
1451        include_standalone: bool,
1452    ) -> Result<HashMap<TransparentAddress, TransparentAddressMetadata>, Self::Error> {
1453        let key_scopes = Some(KeyScope::EXTERNAL)
1454            .into_iter()
1455            .chain(include_change.then_some(KeyScope::INTERNAL))
1456            .chain(
1457                (include_standalone && cfg!(feature = "transparent-key-import"))
1458                    .then_some(KeyScope::Foreign),
1459            )
1460            .collect::<Vec<_>>();
1461
1462        wallet::transparent::get_transparent_receivers(
1463            self.conn.borrow(),
1464            &self.params,
1465            &self.gap_limits,
1466            account,
1467            &key_scopes[..],
1468            None,
1469            false,
1470        )
1471    }
1472
1473    #[cfg(feature = "transparent-inputs")]
1474    fn get_ephemeral_transparent_receivers(
1475        &self,
1476        account: Self::AccountId,
1477        exposure_depth: u32,
1478        exclude_used: bool,
1479    ) -> Result<HashMap<TransparentAddress, TransparentAddressMetadata>, Self::Error> {
1480        wallet::transparent::get_transparent_receivers(
1481            self.conn.borrow(),
1482            &self.params,
1483            &self.gap_limits,
1484            account,
1485            &[KeyScope::Ephemeral],
1486            Some(exposure_depth),
1487            exclude_used,
1488        )
1489    }
1490
1491    #[cfg(feature = "transparent-inputs")]
1492    fn get_transparent_balances(
1493        &self,
1494        account: Self::AccountId,
1495        target_height: TargetHeight,
1496        confirmations_policy: ConfirmationsPolicy,
1497    ) -> Result<TransparentBalances, Self::Error> {
1498        wallet::transparent::get_transparent_balances(
1499            self.conn.borrow(),
1500            &self.params,
1501            account,
1502            target_height,
1503            confirmations_policy,
1504        )
1505    }
1506
1507    #[cfg(feature = "transparent-inputs")]
1508    fn get_transparent_address_metadata(
1509        &self,
1510        account: Self::AccountId,
1511        address: &TransparentAddress,
1512    ) -> Result<Option<TransparentAddressMetadata>, Self::Error> {
1513        wallet::transparent::get_transparent_address_metadata(
1514            self.conn.borrow(),
1515            &self.params,
1516            &self.gap_limits,
1517            account,
1518            address,
1519        )
1520    }
1521
1522    #[cfg(feature = "transparent-inputs")]
1523    fn utxo_query_height(&self, account: Self::AccountId) -> Result<BlockHeight, Self::Error> {
1524        let account_ref = wallet::get_account_ref(self.conn.borrow(), account)?;
1525        wallet::transparent::utxo_query_height(self.conn.borrow(), account_ref, &self.gap_limits)
1526    }
1527
1528    fn transaction_data_requests(&self) -> Result<Vec<TransactionDataRequest>, Self::Error> {
1529        if let Some(_chain_tip_height) = wallet::chain_tip_height(self.conn.borrow())? {
1530            let iter = wallet::transaction_data_requests(self.conn.borrow())?.into_iter();
1531
1532            #[cfg(feature = "transparent-inputs")]
1533            let iter = iter.chain(wallet::transparent::transaction_data_requests(
1534                self.conn.borrow(),
1535                &self.params,
1536                _chain_tip_height,
1537            )?);
1538
1539            Ok(iter.collect())
1540        } else {
1541            // If the chain tip height is unknown, we're not in a state where it makes sense to process
1542            // transaction data requests anyway so we just return the empty vector of requests.
1543            Ok(vec![])
1544        }
1545    }
1546
1547    fn get_received_outputs(
1548        &self,
1549        txid: TxId,
1550        target_height: TargetHeight,
1551        confirmations_policy: ConfirmationsPolicy,
1552    ) -> Result<Vec<ReceivedTransactionOutput>, Self::Error> {
1553        wallet::get_received_outputs(
1554            self.conn.borrow(),
1555            txid,
1556            target_height,
1557            confirmations_policy,
1558        )
1559    }
1560}
1561
1562#[cfg(any(test, feature = "test-dependencies"))]
1563impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletTest
1564    for WalletDb<C, P, CL, R>
1565{
1566    fn get_tx_history(
1567        &self,
1568    ) -> Result<Vec<TransactionSummary<<Self as WalletRead>::AccountId>>, <Self as WalletRead>::Error>
1569    {
1570        wallet::testing::get_tx_history(self.conn.borrow())
1571    }
1572
1573    fn get_sent_note_ids(
1574        &self,
1575        txid: &TxId,
1576        protocol: ShieldedPool,
1577    ) -> Result<Vec<NoteId>, <Self as WalletRead>::Error> {
1578        let mut stmt_sent_notes = self.conn.borrow().prepare(
1579            "SELECT output_index
1580             FROM sent_notes
1581             JOIN transactions ON transactions.id_tx = sent_notes.transaction_id
1582             WHERE transactions.txid = :txid
1583             AND sent_notes.output_pool = :pool_code",
1584        )?;
1585
1586        let note_ids = stmt_sent_notes
1587            .query_map(
1588                named_params! {
1589                    ":txid": txid.as_ref(),
1590                    ":pool_code": pool_code(PoolType::Shielded(protocol)),
1591                },
1592                |row| Ok(NoteId::new(*txid, protocol, row.get(0)?)),
1593            )?
1594            .collect::<Result<_, _>>()?;
1595
1596        Ok(note_ids)
1597    }
1598
1599    fn get_sent_outputs(
1600        &self,
1601        txid: &TxId,
1602    ) -> Result<Vec<OutputOfSentTx>, <Self as WalletRead>::Error> {
1603        let mut stmt_sent = self.conn.borrow().prepare(
1604            "SELECT value, to_address,
1605                    a.cached_transparent_receiver_address, a.transparent_child_index
1606             FROM sent_notes
1607             JOIN transactions t ON t.id_tx = sent_notes.transaction_id
1608             LEFT JOIN transparent_received_outputs tro ON tro.transaction_id = t.id_tx
1609             LEFT JOIN addresses a ON a.id = tro.address_id AND a.key_scope = :key_scope
1610             WHERE t.txid = :txid
1611             ORDER BY value",
1612        )?;
1613
1614        let sends = stmt_sent
1615            .query_map(
1616                named_params![
1617                    ":txid": txid.as_ref(),
1618                    ":key_scope": KeyScope::Ephemeral.encode()
1619                ],
1620                |row| {
1621                    let v = row.get(0)?;
1622                    let to_address = row.get::<_, Option<String>>(1)?;
1623                    let ephemeral_address = row.get::<_, Option<String>>(2)?;
1624                    let address_index = row.get::<_, Option<u32>>(3)?;
1625                    Ok((v, to_address, ephemeral_address.zip(address_index)))
1626                },
1627            )?
1628            .map(|res| {
1629                let (amount, external_recipient, _ephemeral_address) = res?;
1630                Ok::<_, SqliteClientError>(OutputOfSentTx::from_parts(
1631                    Zatoshis::from_u64(amount)?,
1632                    external_recipient
1633                        .map(|s| {
1634                            Address::decode(&self.params, &s).ok_or_else(|| {
1635                                SqliteClientError::CorruptedData(format!(
1636                                    "invalid transparent address: {s}"
1637                                ))
1638                            })
1639                        })
1640                        .transpose()?,
1641                    #[cfg(feature = "transparent-inputs")]
1642                    _ephemeral_address
1643                        .map(|(addr_str, idx)| {
1644                            let addr =
1645                                Address::decode(&self.params, &addr_str).ok_or_else(|| {
1646                                    SqliteClientError::CorruptedData(format!(
1647                                        "invalid transparent address: {addr_str}"
1648                                    ))
1649                                })?;
1650                            let i = NonHardenedChildIndex::from_index(idx).ok_or_else(|| {
1651                                SqliteClientError::CorruptedData(format!(
1652                                    "invalid non-hardened child index: {idx}"
1653                                ))
1654                            })?;
1655
1656                            Ok::<_, SqliteClientError>((addr, i))
1657                        })
1658                        .transpose()?,
1659                ))
1660            })
1661            .collect::<Result<_, _>>()?;
1662
1663        Ok(sends)
1664    }
1665
1666    fn get_checkpoint_history(
1667        &self,
1668        protocol: &ShieldedPool,
1669    ) -> Result<
1670        Vec<(BlockHeight, Option<incrementalmerkletree::Position>)>,
1671        <Self as WalletRead>::Error,
1672    > {
1673        wallet::testing::get_checkpoint_history(self.conn.borrow(), *protocol)
1674    }
1675
1676    #[cfg(feature = "transparent-inputs")]
1677    fn get_transparent_output(
1678        &self,
1679        outpoint: &OutPoint,
1680        target_height: Option<TargetHeight>,
1681    ) -> Result<
1682        Option<WalletTransparentOutput<<Self as InputSource>::AccountId>>,
1683        <Self as InputSource>::Error,
1684    > {
1685        wallet::transparent::get_wallet_transparent_output(
1686            self.conn.borrow(),
1687            outpoint,
1688            target_height,
1689        )
1690    }
1691
1692    fn get_notes(
1693        &self,
1694        protocol: ShieldedPool,
1695    ) -> Result<Vec<ReceivedNote<Self::NoteRef, Note>>, <Self as InputSource>::Error> {
1696        let (target_height, _) = self
1697            .get_target_and_anchor_heights(NonZeroU32::MIN)?
1698            .ok_or(SqliteClientError::ChainHeightUnknown)?;
1699
1700        let TableConstants {
1701            table_prefix,
1702            output_index_col,
1703            ..
1704        } = wallet::common::table_constants::<<Self as InputSource>::Error>(protocol)?;
1705        let mut stmt_received_notes = self.conn.borrow().prepare(&format!(
1706            "SELECT txid, {output_index_col}
1707             FROM {table_prefix}_received_notes rn
1708             INNER JOIN transactions ON transactions.id_tx = rn.transaction_id
1709             WHERE transactions.block IS NOT NULL
1710             AND recipient_key_scope IS NOT NULL
1711             AND nf IS NOT NULL
1712             AND commitment_tree_position IS NOT NULL"
1713        ))?;
1714
1715        let result = stmt_received_notes
1716            .query_map([], |row| {
1717                let txid: [u8; 32] = row.get("txid")?;
1718                let output_index: u32 = row.get(output_index_col)?;
1719                // The test accessor inspects wallet contents irrespective of lock state.
1720                let lock_filter = LockFilter::Unfiltered;
1721                let note = self
1722                    .get_spendable_note(
1723                        &TxId::from_bytes(txid),
1724                        protocol,
1725                        output_index,
1726                        target_height,
1727                        lock_filter,
1728                    )
1729                    .unwrap()
1730                    .unwrap();
1731                Ok(note)
1732            })?
1733            .collect::<Result<Vec<_>, _>>()?;
1734
1735        Ok(result)
1736    }
1737
1738    #[cfg(feature = "transparent-inputs")]
1739    fn get_known_ephemeral_addresses(
1740        &self,
1741        account: <Self as WalletRead>::AccountId,
1742        index_range: Option<Range<NonHardenedChildIndex>>,
1743    ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
1744    {
1745        let account_id = wallet::get_account_ref(self.conn.borrow(), account)?;
1746        wallet::transparent::ephemeral::get_known_ephemeral_addresses(
1747            self.conn.borrow(),
1748            &self.params,
1749            &self.gap_limits,
1750            account_id,
1751            index_range,
1752        )
1753    }
1754
1755    #[cfg(feature = "transparent-inputs")]
1756    fn find_account_for_ephemeral_address(
1757        &self,
1758        address: &TransparentAddress,
1759    ) -> Result<Option<<Self as WalletRead>::AccountId>, <Self as WalletRead>::Error> {
1760        wallet::transparent::ephemeral::find_account_for_ephemeral_address_str(
1761            self.conn.borrow(),
1762            &address.encode(&self.params),
1763        )
1764    }
1765}
1766
1767impl<C, P, CL, R> OutputLockStore for WalletDb<C, P, CL, R>
1768where
1769    C: BorrowMut<rusqlite::Connection>,
1770    P: consensus::Parameters,
1771    CL: Clock,
1772    R: RngCore,
1773{
1774    type Error = SqliteClientError;
1775    type AccountId = AccountUuid;
1776
1777    fn lock_outputs(
1778        &mut self,
1779        outputs: &[OutputRef],
1780        owner: LockOwner,
1781        lock_expiry_height: BlockHeight,
1782    ) -> Result<usize, LockError<Self::Error>> {
1783        Ok(self.transactionally(|wdb| {
1784            wallet::locking::lock_outputs(wdb.conn.0, outputs, owner, lock_expiry_height)
1785        })?)
1786    }
1787
1788    fn unlock_output(&mut self, output: &OutputRef, owner: LockOwner) -> Result<bool, Self::Error> {
1789        self.transactionally(|wdb| wallet::locking::unlock_output(wdb.conn.0, output, owner))
1790    }
1791
1792    fn clear_locked_outputs(&mut self, account: Self::AccountId) -> Result<usize, Self::Error> {
1793        self.transactionally(|wdb| wallet::locking::clear_locked_outputs(wdb.conn.0, account))
1794    }
1795
1796    fn get_locked_outputs(&self, account: Self::AccountId) -> Result<Vec<OutputRef>, Self::Error> {
1797        wallet::locking::get_locked_outputs(self.conn.borrow(), account)
1798    }
1799}
1800
1801impl<C: BorrowMut<rusqlite::Connection>, P: consensus::Parameters, CL: Clock, R: RngCore>
1802    WalletWrite for WalletDb<C, P, CL, R>
1803{
1804    type UtxoRef = UtxoId;
1805
1806    fn create_account(
1807        &mut self,
1808        account_name: &str,
1809        seed: &SecretVec<u8>,
1810        birthday: &AccountBirthday,
1811        key_source: Option<&str>,
1812    ) -> Result<(<Self as WalletRead>::AccountId, UnifiedSpendingKey), <Self as WalletRead>::Error>
1813    {
1814        self.borrow_mut()
1815            .transactionally(|wdb| wdb.create_account(account_name, seed, birthday, key_source))
1816    }
1817
1818    fn import_account_hd(
1819        &mut self,
1820        account_name: &str,
1821        seed: &SecretVec<u8>,
1822        account_index: zip32::AccountId,
1823        birthday: &AccountBirthday,
1824        key_source: Option<&str>,
1825    ) -> Result<(Self::Account, UnifiedSpendingKey), <Self as WalletRead>::Error> {
1826        self.transactionally(|wdb| {
1827            wdb.import_account_hd(account_name, seed, account_index, birthday, key_source)
1828        })
1829    }
1830
1831    fn import_account_ufvk(
1832        &mut self,
1833        account_name: &str,
1834        ufvk: &UnifiedFullViewingKey,
1835        birthday: &AccountBirthday,
1836        purpose: AccountPurpose,
1837        key_source: Option<&str>,
1838    ) -> Result<Self::Account, <Self as WalletRead>::Error> {
1839        self.transactionally(|wdb| {
1840            wdb.import_account_ufvk(account_name, ufvk, birthday, purpose, key_source)
1841        })
1842    }
1843
1844    fn delete_account(
1845        &mut self,
1846        account_uuid: <Self as WalletRead>::AccountId,
1847    ) -> Result<(), <Self as WalletRead>::Error> {
1848        self.transactionally(|wdb| wdb.delete_account(account_uuid))
1849    }
1850
1851    #[cfg(feature = "transparent-key-import")]
1852    fn import_standalone_transparent_pubkey(
1853        &mut self,
1854        account: <Self as WalletRead>::AccountId,
1855        pubkey: secp256k1::PublicKey,
1856    ) -> Result<(), <Self as WalletRead>::Error> {
1857        self.transactionally(|wdb| wdb.import_standalone_transparent_pubkey(account, pubkey))
1858    }
1859
1860    #[cfg(feature = "transparent-key-import")]
1861    fn import_standalone_transparent_pubkeys(
1862        &mut self,
1863        account: <Self as WalletRead>::AccountId,
1864        pubkeys: &[secp256k1::PublicKey],
1865    ) -> Result<(), <Self as WalletRead>::Error> {
1866        self.transactionally(|wdb| wdb.import_standalone_transparent_pubkeys(account, pubkeys))
1867    }
1868
1869    #[cfg(feature = "transparent-key-import")]
1870    fn import_standalone_transparent_script(
1871        &mut self,
1872        account: <Self as WalletRead>::AccountId,
1873        script: zcash_script::script::Redeem,
1874    ) -> Result<(), <Self as WalletRead>::Error> {
1875        self.transactionally(|wdb| wdb.import_standalone_transparent_script(account, script))
1876    }
1877
1878    fn get_next_available_address(
1879        &mut self,
1880        account_uuid: <Self as WalletRead>::AccountId,
1881        request: UnifiedAddressRequest,
1882    ) -> Result<Option<(UnifiedAddress, DiversifierIndex)>, <Self as WalletRead>::Error> {
1883        self.transactionally(|wdb| wdb.get_next_available_address(account_uuid, request))
1884    }
1885
1886    fn get_address_for_index(
1887        &mut self,
1888        account: <Self as WalletRead>::AccountId,
1889        diversifier_index: DiversifierIndex,
1890        request: UnifiedAddressRequest,
1891    ) -> Result<Option<UnifiedAddress>, <Self as WalletRead>::Error> {
1892        self.transactionally(|wdb| wdb.get_address_for_index(account, diversifier_index, request))
1893    }
1894
1895    fn update_chain_tip(
1896        &mut self,
1897        tip_height: BlockHeight,
1898    ) -> Result<(), <Self as WalletRead>::Error> {
1899        self.transactionally(|wdb| wdb.update_chain_tip(tip_height))
1900    }
1901
1902    fn prune_scan_queue_below(
1903        &mut self,
1904        height: BlockHeight,
1905        retain_with_priority: Option<ScanPriority>,
1906    ) -> Result<u64, <Self as WalletRead>::Error> {
1907        self.transactionally(|wdb| wdb.prune_scan_queue_below(height, retain_with_priority))
1908    }
1909
1910    #[tracing::instrument(skip_all, fields(height = blocks.first().map(|b| u32::from(b.height())), count = blocks.len()))]
1911    #[allow(clippy::type_complexity)]
1912    fn put_blocks(
1913        &mut self,
1914        from_state: &ChainState,
1915        blocks: Vec<ScannedBlock<<Self as WalletRead>::AccountId>>,
1916    ) -> Result<(), <Self as WalletRead>::Error> {
1917        self.transactionally(|wdb| wdb.put_blocks(from_state, blocks))
1918    }
1919
1920    fn put_received_transparent_utxo(
1921        &mut self,
1922        _output: &WalletTransparentOutput<<Self as WalletRead>::AccountId>,
1923    ) -> Result<Self::UtxoRef, <Self as WalletRead>::Error> {
1924        #[cfg(feature = "transparent-inputs")]
1925        return self.transactionally(|wdb| wdb.put_received_transparent_utxo(_output));
1926
1927        #[cfg(not(feature = "transparent-inputs"))]
1928        panic!(
1929            "The wallet must be compiled with the transparent-inputs feature to use this method."
1930        );
1931    }
1932
1933    fn store_decrypted_tx(
1934        &mut self,
1935        d_tx: DecryptedTransaction<Transaction, <Self as WalletRead>::AccountId>,
1936    ) -> Result<(), <Self as WalletRead>::Error> {
1937        self.transactionally(|wdb| wdb.store_decrypted_tx(d_tx))
1938    }
1939
1940    fn set_tx_trust(
1941        &mut self,
1942        txid: TxId,
1943        trusted: bool,
1944    ) -> Result<(), <Self as WalletRead>::Error> {
1945        self.transactionally(|wdb| wdb.set_tx_trust(txid, trusted))
1946    }
1947
1948    fn store_transactions_to_be_sent(
1949        &mut self,
1950        transactions: &[SentTransaction<<Self as WalletRead>::AccountId>],
1951    ) -> Result<(), <Self as WalletRead>::Error> {
1952        self.transactionally(|wdb| wdb.store_transactions_to_be_sent(transactions))
1953    }
1954
1955    fn truncate_to_height(
1956        &mut self,
1957        max_height: BlockHeight,
1958    ) -> Result<BlockHeight, <Self as WalletRead>::Error> {
1959        self.transactionally(|wdb| wdb.truncate_to_height(max_height))
1960    }
1961
1962    fn truncate_to_chain_state(
1963        &mut self,
1964        chain_state: ChainState,
1965    ) -> Result<(), <Self as WalletRead>::Error> {
1966        self.transactionally(|wdb| wdb.truncate_to_chain_state(chain_state))
1967    }
1968
1969    fn rewind_to_chain_state(
1970        &mut self,
1971        chain_state: ChainState,
1972        reset_account_birthdays: HashSet<<Self as WalletRead>::AccountId>,
1973    ) -> Result<(), RewindError<<Self as WalletRead>::AccountId, <Self as WalletRead>::Error>> {
1974        let tx = self
1975            .conn
1976            .borrow_mut()
1977            .transaction()
1978            .map_err(|e| RewindError::DataSource(SqliteClientError::from(e)))?;
1979        let result = wallet::rewind_to_chain_state(
1980            &tx,
1981            &self.params,
1982            #[cfg(feature = "transparent-inputs")]
1983            &self.gap_limits,
1984            &chain_state,
1985            reset_account_birthdays,
1986        );
1987        if result.is_ok() {
1988            tx.commit()
1989                .map_err(|e| RewindError::DataSource(SqliteClientError::from(e)))?;
1990        }
1991        result
1992    }
1993
1994    #[cfg(feature = "transparent-inputs")]
1995    fn reserve_next_n_ephemeral_addresses(
1996        &mut self,
1997        account_id: <Self as WalletRead>::AccountId,
1998        n: usize,
1999    ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
2000    {
2001        self.transactionally(|wdb| wdb.reserve_next_n_ephemeral_addresses(account_id, n))
2002    }
2003
2004    #[cfg(feature = "transparent-inputs")]
2005    fn reserve_next_n_internal_addresses(
2006        &mut self,
2007        account_id: <Self as WalletRead>::AccountId,
2008        n: usize,
2009    ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
2010    {
2011        self.transactionally(|wdb| wdb.reserve_next_n_internal_addresses(account_id, n))
2012    }
2013
2014    fn set_transaction_status(
2015        &mut self,
2016        txid: TxId,
2017        status: data_api::TransactionStatus,
2018    ) -> Result<(), <Self as WalletRead>::Error> {
2019        self.transactionally(|wdb| WalletWrite::set_transaction_status(wdb, txid, status))
2020    }
2021
2022    #[cfg(feature = "transparent-inputs")]
2023    fn schedule_next_check(
2024        &mut self,
2025        address: &TransparentAddress,
2026        offset_seconds: u32,
2027    ) -> Result<Option<SystemTime>, <Self as WalletRead>::Error> {
2028        self.transactionally(|wdb| wdb.schedule_next_check(address, offset_seconds))
2029    }
2030
2031    #[cfg(feature = "transparent-inputs")]
2032    fn mark_transparent_addresses_exposed(
2033        &mut self,
2034        exposures: &[(TransparentAddress, BlockHeight)],
2035    ) -> Result<(), <Self as WalletRead>::Error> {
2036        self.transactionally(|wdb| wdb.mark_transparent_addresses_exposed(exposures))
2037    }
2038
2039    #[cfg(feature = "transparent-inputs")]
2040    fn notify_address_checked(
2041        &mut self,
2042        request: TransactionsInvolvingAddress,
2043        as_of_height: BlockHeight,
2044    ) -> Result<(), <Self as WalletRead>::Error> {
2045        self.transactionally(|wdb| wdb.notify_address_checked(request, as_of_height))
2046    }
2047
2048    #[cfg(feature = "spend-index")]
2049    fn notify_output_verified_unspent(
2050        &mut self,
2051        outpoint: OutPoint,
2052        as_of_height: BlockHeight,
2053    ) -> Result<(), <Self as WalletRead>::Error> {
2054        self.transactionally(|wdb| wdb.notify_output_verified_unspent(outpoint, as_of_height))
2055    }
2056}
2057
2058/// This impl block is only usable when you already have an [`SqlTransaction`], meaning
2059/// you are inside a [`WalletDb::transactionally`] block with a lock on the database.
2060impl<P, CL, R> OutputLockStore for WalletDb<SqlTransaction<'_>, P, CL, R>
2061where
2062    P: consensus::Parameters,
2063    CL: Clock,
2064    R: RngCore,
2065{
2066    type Error = SqliteClientError;
2067    type AccountId = AccountUuid;
2068
2069    fn lock_outputs(
2070        &mut self,
2071        outputs: &[OutputRef],
2072        owner: LockOwner,
2073        lock_expiry_height: BlockHeight,
2074    ) -> Result<usize, LockError<Self::Error>> {
2075        // This impl operates within an enclosing database transaction, so the
2076        // all-or-nothing contract of `OutputLockStore::lock_outputs` holds only if a
2077        // returned error causes the enclosing transaction to be rolled back: on a
2078        // mid-batch `LockFailure`, locks taken for earlier outputs in the batch
2079        // remain pending in the transaction. `WalletDb::transactionally` (used by
2080        // the non-transactional impl above) provides that rollback.
2081        Ok(wallet::locking::lock_outputs(
2082            self.conn.0,
2083            outputs,
2084            owner,
2085            lock_expiry_height,
2086        )?)
2087    }
2088
2089    fn unlock_output(&mut self, output: &OutputRef, owner: LockOwner) -> Result<bool, Self::Error> {
2090        wallet::locking::unlock_output(self.conn.0, output, owner)
2091    }
2092
2093    fn clear_locked_outputs(&mut self, account: Self::AccountId) -> Result<usize, Self::Error> {
2094        wallet::locking::clear_locked_outputs(self.conn.0, account)
2095    }
2096
2097    fn get_locked_outputs(&self, account: Self::AccountId) -> Result<Vec<OutputRef>, Self::Error> {
2098        wallet::locking::get_locked_outputs(self.conn.0, account)
2099    }
2100}
2101
2102impl<P: consensus::Parameters, CL: Clock, R: RngCore> WalletWrite
2103    for WalletDb<SqlTransaction<'_>, P, CL, R>
2104{
2105    type UtxoRef = UtxoId;
2106
2107    fn create_account(
2108        &mut self,
2109        account_name: &str,
2110        seed: &SecretVec<u8>,
2111        birthday: &AccountBirthday,
2112        key_source: Option<&str>,
2113    ) -> Result<(<Self as WalletRead>::AccountId, UnifiedSpendingKey), <Self as WalletRead>::Error>
2114    {
2115        let seed_fingerprint =
2116            SeedFingerprint::from_seed(seed.expose_secret()).ok_or_else(|| {
2117                SqliteClientError::BadAccountData(
2118                    "Seed must be between 32 and 252 bytes in length.".to_owned(),
2119                )
2120            })?;
2121        let zip32_account_index = wallet::max_zip32_account_index(self.conn.0, &seed_fingerprint)?
2122            .map(|a| {
2123                a.next()
2124                    .ok_or(SqliteClientError::Zip32AccountIndexOutOfRange)
2125            })
2126            .transpose()?
2127            .unwrap_or(zip32::AccountId::ZERO);
2128
2129        let usk =
2130            UnifiedSpendingKey::from_seed(&self.params, seed.expose_secret(), zip32_account_index)
2131                .map_err(|_| SqliteClientError::KeyDerivationError(zip32_account_index))?;
2132        let ufvk = usk.to_unified_full_viewing_key();
2133
2134        let account = wallet::add_account(
2135            self.conn.0,
2136            &self.params,
2137            account_name,
2138            &AccountSource::Derived {
2139                derivation: Zip32Derivation::new(
2140                    seed_fingerprint,
2141                    zip32_account_index,
2142                    #[cfg(feature = "zcashd-compat")]
2143                    None,
2144                ),
2145                key_source: key_source.map(|s| s.to_owned()),
2146            },
2147            wallet::ViewingKey::Full(Box::new(ufvk)),
2148            birthday,
2149            #[cfg(feature = "transparent-inputs")]
2150            &self.gap_limits,
2151        )?;
2152
2153        Ok((account.id(), usk))
2154    }
2155
2156    fn import_account_hd(
2157        &mut self,
2158        account_name: &str,
2159        seed: &SecretVec<u8>,
2160        account_index: zip32::AccountId,
2161        birthday: &AccountBirthday,
2162        key_source: Option<&str>,
2163    ) -> Result<(Self::Account, UnifiedSpendingKey), <Self as WalletRead>::Error> {
2164        let seed_fingerprint =
2165            SeedFingerprint::from_seed(seed.expose_secret()).ok_or_else(|| {
2166                SqliteClientError::BadAccountData(
2167                    "Seed must be between 32 and 252 bytes in length.".to_owned(),
2168                )
2169            })?;
2170
2171        let usk = UnifiedSpendingKey::from_seed(&self.params, seed.expose_secret(), account_index)
2172            .map_err(|_| SqliteClientError::KeyDerivationError(account_index))?;
2173        let ufvk = usk.to_unified_full_viewing_key();
2174
2175        let account = wallet::add_account(
2176            self.conn.0,
2177            &self.params,
2178            account_name,
2179            &AccountSource::Derived {
2180                derivation: Zip32Derivation::new(
2181                    seed_fingerprint,
2182                    account_index,
2183                    #[cfg(feature = "zcashd-compat")]
2184                    None,
2185                ),
2186                key_source: key_source.map(|s| s.to_owned()),
2187            },
2188            wallet::ViewingKey::Full(Box::new(ufvk)),
2189            birthday,
2190            #[cfg(feature = "transparent-inputs")]
2191            &self.gap_limits,
2192        )?;
2193
2194        Ok((account, usk))
2195    }
2196
2197    fn import_account_ufvk(
2198        &mut self,
2199        account_name: &str,
2200        ufvk: &UnifiedFullViewingKey,
2201        birthday: &AccountBirthday,
2202        purpose: AccountPurpose,
2203        key_source: Option<&str>,
2204    ) -> Result<Self::Account, <Self as WalletRead>::Error> {
2205        wallet::add_account(
2206            self.conn.0,
2207            &self.params,
2208            account_name,
2209            &AccountSource::Imported {
2210                purpose,
2211                key_source: key_source.map(|s| s.to_owned()),
2212            },
2213            wallet::ViewingKey::Full(Box::new(ufvk.to_owned())),
2214            birthday,
2215            #[cfg(feature = "transparent-inputs")]
2216            &self.gap_limits,
2217        )
2218    }
2219
2220    fn delete_account(
2221        &mut self,
2222        account_uuid: <Self as WalletRead>::AccountId,
2223    ) -> Result<(), <Self as WalletRead>::Error> {
2224        wallet::delete_account(self.conn.0, account_uuid)
2225    }
2226
2227    #[cfg(feature = "transparent-key-import")]
2228    fn import_standalone_transparent_pubkey(
2229        &mut self,
2230        account: <Self as WalletRead>::AccountId,
2231        pubkey: secp256k1::PublicKey,
2232    ) -> Result<(), <Self as WalletRead>::Error> {
2233        wallet::import_standalone_transparent_pubkey(self.conn.0, &self.params, account, pubkey)
2234            .map(|_inserted| ())
2235    }
2236
2237    #[cfg(feature = "transparent-key-import")]
2238    fn import_standalone_transparent_pubkeys(
2239        &mut self,
2240        account: <Self as WalletRead>::AccountId,
2241        pubkeys: &[secp256k1::PublicKey],
2242    ) -> Result<(), <Self as WalletRead>::Error> {
2243        wallet::import_standalone_transparent_pubkeys(self.conn.0, &self.params, account, pubkeys)
2244            .map(|_inserted| ())
2245    }
2246
2247    #[cfg(feature = "transparent-key-import")]
2248    fn import_standalone_transparent_script(
2249        &mut self,
2250        account: <Self as WalletRead>::AccountId,
2251        script: zcash_script::script::Redeem,
2252    ) -> Result<(), <Self as WalletRead>::Error> {
2253        wallet::import_standalone_transparent_script(self.conn.0, &self.params, account, script)
2254    }
2255
2256    fn get_next_available_address(
2257        &mut self,
2258        account_uuid: <Self as WalletRead>::AccountId,
2259        request: UnifiedAddressRequest,
2260    ) -> Result<Option<(UnifiedAddress, DiversifierIndex)>, <Self as WalletRead>::Error> {
2261        wallet::get_next_available_address(
2262            self.conn.0,
2263            &self.params,
2264            &self.clock,
2265            account_uuid,
2266            request,
2267            #[cfg(feature = "transparent-inputs")]
2268            &self.gap_limits,
2269        )
2270    }
2271
2272    fn get_address_for_index(
2273        &mut self,
2274        account: <Self as WalletRead>::AccountId,
2275        diversifier_index: DiversifierIndex,
2276        request: UnifiedAddressRequest,
2277    ) -> Result<Option<UnifiedAddress>, <Self as WalletRead>::Error> {
2278        if let Some(account) = self.get_account(account)? {
2279            match account.uivk().address(diversifier_index, request) {
2280                Ok(address) => {
2281                    let chain_tip_height = wallet::chain_tip_height(self.conn.borrow())?;
2282                    upsert_address(
2283                        self.conn.borrow(),
2284                        &self.params,
2285                        account.internal_id(),
2286                        diversifier_index,
2287                        &address,
2288                        Some(chain_tip_height.unwrap_or(account.birthday())),
2289                        true,
2290                    )?;
2291
2292                    Ok(Some(address))
2293                }
2294                #[cfg(feature = "transparent-inputs")]
2295                Err(InvalidTransparentChildIndex(_)) => Ok(None),
2296                Err(InvalidSaplingDiversifierIndex(_)) => Ok(None),
2297                Err(e) => Err(SqliteClientError::AddressGeneration(e)),
2298            }
2299        } else {
2300            Err(SqliteClientError::AccountUnknown)
2301        }
2302    }
2303
2304    fn update_chain_tip(
2305        &mut self,
2306        tip_height: BlockHeight,
2307    ) -> Result<(), <Self as WalletRead>::Error> {
2308        wallet::scanning::update_chain_tip(self.conn.0, &self.params, tip_height)?;
2309        Ok(())
2310    }
2311
2312    fn prune_scan_queue_below(
2313        &mut self,
2314        height: BlockHeight,
2315        retain_with_priority: Option<ScanPriority>,
2316    ) -> Result<u64, <Self as WalletRead>::Error> {
2317        wallet::scanning::prune_scan_queue_below(self.conn.0, height, retain_with_priority)
2318    }
2319
2320    #[allow(clippy::type_complexity)]
2321    fn put_blocks(
2322        &mut self,
2323        from_state: &ChainState,
2324        blocks: Vec<ScannedBlock<<Self as WalletRead>::AccountId>>,
2325    ) -> Result<(), <Self as WalletRead>::Error> {
2326        // Once the NU6.3 (Ironwood) activation height is reached, checkpoints on the anchor
2327        // retention grids are retained as durable anchors. The activation height is `None` (and so
2328        // anchor retention is inactive) on networks that do not yet have an assigned NU6.3
2329        // activation height.
2330        //
2331        // Upstream also unions in the grid every in-flight pool migration was committed under,
2332        // read from the database. This fork does not carry the pool-migration engine, so no such
2333        // row can exist and the union is exactly this wallet's configured interval. Restoring that
2334        // behaviour means restoring the engine, not just this call.
2335        let anchor_retention = self
2336            .params
2337            .activation_height(consensus::NetworkUpgrade::Nu6_3)
2338            .map(|from_height| {
2339                Ok::<_, SqliteClientError>(AnchorRetention::union(
2340                    from_height,
2341                    core::iter::once(self.anchor_retention_interval),
2342                ))
2343            })
2344            .transpose()?
2345            .flatten();
2346
2347        ll::wallet::put_blocks::<_, SqliteClientError, commitment_tree::Error>(
2348            self,
2349            #[cfg(feature = "transparent-inputs")]
2350            self.gap_limits,
2351            from_state,
2352            blocks,
2353            anchor_retention.as_ref(),
2354        )
2355        .map_err(SqliteClientError::from)
2356    }
2357
2358    fn put_received_transparent_utxo(
2359        &mut self,
2360        _output: &WalletTransparentOutput<<Self as WalletRead>::AccountId>,
2361    ) -> Result<Self::UtxoRef, <Self as WalletRead>::Error> {
2362        #[cfg(feature = "transparent-inputs")]
2363        return {
2364            let (account_id, _, key_scope, utxo_id) =
2365                wallet::transparent::put_received_transparent_utxo(
2366                    self.conn.0,
2367                    &self.params,
2368                    &self.gap_limits,
2369                    _output,
2370                )?;
2371
2372            if let Some(t_key_scope) = <Option<TransparentKeyScope>>::from(key_scope) {
2373                wallet::transparent::generate_gap_addresses(
2374                    self.conn.0,
2375                    &self.params,
2376                    &self.gap_limits,
2377                    account_id,
2378                    t_key_scope,
2379                    UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require),
2380                    true,
2381                )?;
2382            }
2383
2384            Ok(utxo_id)
2385        };
2386
2387        #[cfg(not(feature = "transparent-inputs"))]
2388        panic!(
2389            "The wallet must be compiled with the transparent-inputs feature to use this method."
2390        );
2391    }
2392
2393    fn store_decrypted_tx(
2394        &mut self,
2395        d_tx: DecryptedTransaction<Transaction, <Self as WalletRead>::AccountId>,
2396    ) -> Result<(), <Self as WalletRead>::Error> {
2397        let chain_tip = wallet::chain_tip_height(self.conn.borrow())?
2398            .ok_or(SqliteClientError::ChainHeightUnknown)?;
2399        store_decrypted_tx(
2400            self,
2401            &self.params.clone(),
2402            #[cfg(feature = "transparent-inputs")]
2403            self.gap_limits,
2404            chain_tip,
2405            d_tx,
2406        )
2407    }
2408
2409    fn set_tx_trust(
2410        &mut self,
2411        txid: TxId,
2412        trusted: bool,
2413    ) -> Result<(), <Self as WalletRead>::Error> {
2414        wallet::set_tx_trust(self.conn.0, txid, trusted)
2415    }
2416
2417    fn store_transactions_to_be_sent(
2418        &mut self,
2419        transactions: &[SentTransaction<<Self as WalletRead>::AccountId>],
2420    ) -> Result<(), <Self as WalletRead>::Error> {
2421        for sent_tx in transactions {
2422            wallet::store_transaction_to_be_sent(
2423                self.conn.0,
2424                &self.params,
2425                #[cfg(feature = "transparent-inputs")]
2426                &self.gap_limits,
2427                sent_tx,
2428            )?;
2429        }
2430        Ok(())
2431    }
2432
2433    fn truncate_to_height(
2434        &mut self,
2435        max_height: BlockHeight,
2436    ) -> Result<BlockHeight, <Self as WalletRead>::Error> {
2437        wallet::truncate_to_height(
2438            self.conn.0,
2439            &self.params,
2440            #[cfg(feature = "transparent-inputs")]
2441            &self.gap_limits,
2442            max_height,
2443        )
2444    }
2445
2446    fn truncate_to_chain_state(
2447        &mut self,
2448        chain_state: ChainState,
2449    ) -> Result<(), <Self as WalletRead>::Error> {
2450        wallet::truncate_to_chain_state(self, chain_state)
2451    }
2452
2453    fn rewind_to_chain_state(
2454        &mut self,
2455        chain_state: ChainState,
2456        reset_account_birthdays: HashSet<<Self as WalletRead>::AccountId>,
2457    ) -> Result<(), RewindError<<Self as WalletRead>::AccountId, <Self as WalletRead>::Error>> {
2458        wallet::rewind_to_chain_state(
2459            self.conn.0,
2460            &self.params,
2461            #[cfg(feature = "transparent-inputs")]
2462            &self.gap_limits,
2463            &chain_state,
2464            reset_account_birthdays,
2465        )
2466    }
2467
2468    #[cfg(feature = "transparent-inputs")]
2469    fn reserve_next_n_ephemeral_addresses(
2470        &mut self,
2471        account_id: <Self as WalletRead>::AccountId,
2472        n: usize,
2473    ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
2474    {
2475        let account_id = wallet::get_account_ref(self.conn.0, account_id)?;
2476        let reserved = wallet::transparent::reserve_next_n_addresses(
2477            self.conn.0,
2478            &self.params,
2479            account_id,
2480            TransparentKeyScope::EPHEMERAL,
2481            self.gap_limits.ephemeral(),
2482            n,
2483        )?;
2484
2485        Ok(reserved.into_iter().map(|(_, a, m)| (a, m)).collect())
2486    }
2487
2488    #[cfg(feature = "transparent-inputs")]
2489    fn reserve_next_n_internal_addresses(
2490        &mut self,
2491        account_id: <Self as WalletRead>::AccountId,
2492        n: usize,
2493    ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
2494    {
2495        let account_id = wallet::get_account_ref(self.conn.0, account_id)?;
2496        let reserved = wallet::transparent::reserve_next_n_addresses(
2497            self.conn.0,
2498            &self.params,
2499            account_id,
2500            TransparentKeyScope::INTERNAL,
2501            self.gap_limits.internal(),
2502            n,
2503        )?;
2504
2505        Ok(reserved.into_iter().map(|(_, a, m)| (a, m)).collect())
2506    }
2507
2508    fn set_transaction_status(
2509        &mut self,
2510        txid: TxId,
2511        status: data_api::TransactionStatus,
2512    ) -> Result<(), <Self as WalletRead>::Error> {
2513        wallet::set_transaction_status(
2514            self.conn.0,
2515            &self.params,
2516            #[cfg(feature = "transparent-inputs")]
2517            &self.gap_limits,
2518            txid,
2519            status,
2520        )
2521    }
2522
2523    #[cfg(feature = "transparent-inputs")]
2524    fn schedule_next_check(
2525        &mut self,
2526        address: &TransparentAddress,
2527        offset_seconds: u32,
2528    ) -> Result<Option<SystemTime>, <Self as WalletRead>::Error> {
2529        wallet::transparent::schedule_next_check(
2530            self.conn.0,
2531            &self.params,
2532            &self.clock,
2533            &mut self.rng,
2534            address,
2535            offset_seconds,
2536        )
2537    }
2538
2539    #[cfg(feature = "transparent-inputs")]
2540    fn mark_transparent_addresses_exposed(
2541        &mut self,
2542        exposures: &[(TransparentAddress, BlockHeight)],
2543    ) -> Result<(), <Self as WalletRead>::Error> {
2544        wallet::transparent::mark_transparent_addresses_exposed(
2545            self.conn.0,
2546            &self.params,
2547            exposures,
2548        )
2549    }
2550
2551    #[cfg(feature = "transparent-inputs")]
2552    fn notify_address_checked(
2553        &mut self,
2554        request: TransactionsInvolvingAddress,
2555        as_of_height: BlockHeight,
2556    ) -> Result<(), <Self as WalletRead>::Error> {
2557        if let Some(requested_end) = request.block_range_end() {
2558            // block_end_height is end-exclusive
2559            if as_of_height != requested_end - 1 {
2560                return Err(SqliteClientError::NotificationMismatch {
2561                    expected: requested_end - 1,
2562                    actual: as_of_height,
2563                });
2564            }
2565        }
2566
2567        wallet::transparent::update_observed_unspent_heights(
2568            self.conn.0,
2569            &self.params,
2570            request.address(),
2571            as_of_height,
2572        )
2573    }
2574
2575    #[cfg(feature = "spend-index")]
2576    fn notify_output_verified_unspent(
2577        &mut self,
2578        outpoint: OutPoint,
2579        as_of_height: BlockHeight,
2580    ) -> Result<(), <Self as WalletRead>::Error> {
2581        wallet::transparent::update_observed_unspent_height_for_outpoint(
2582            self.conn.0,
2583            &outpoint,
2584            as_of_height,
2585        )
2586    }
2587}
2588
2589impl<'a, C: Borrow<rusqlite::Transaction<'a>>, P: consensus::Parameters, CL: Clock, R: RngCore>
2590    LowLevelWalletRead for WalletDb<C, P, CL, R>
2591{
2592    type AccountId = AccountUuid;
2593    type AccountRef = AccountRef;
2594    type Account = wallet::Account;
2595    type Error = SqliteClientError;
2596    type TxRef = TxRef;
2597
2598    fn block_fully_scanned_height(
2599        &self,
2600    ) -> Result<Option<zcash_protocol::consensus::BlockHeight>, Self::Error> {
2601        Ok(
2602            wallet::block_fully_scanned(self.conn.borrow(), &self.params)?
2603                .map(|meta| meta.block_height()),
2604        )
2605    }
2606
2607    fn select_receiving_address(
2608        &self,
2609        account: Self::AccountId,
2610        receiver: &zcash_keys::address::Receiver,
2611    ) -> Result<Option<zcash_address::ZcashAddress>, Self::Error> {
2612        wallet::select_receiving_address(self.conn.borrow(), &self.params, account, receiver)
2613    }
2614
2615    #[cfg(feature = "transparent-inputs")]
2616    fn find_involved_accounts(
2617        &self,
2618        tx_refs: impl IntoIterator<Item = Self::TxRef>,
2619    ) -> Result<HashSet<(Self::AccountId, Option<TransparentKeyScope>)>, Self::Error> {
2620        Ok(wallet::involved_accounts(self.conn.borrow(), tx_refs)?
2621            .into_iter()
2622            .map(|(_, uuid, scope)| (uuid, scope))
2623            .collect())
2624    }
2625
2626    #[cfg(feature = "transparent-inputs")]
2627    fn find_account_for_transparent_address(
2628        &self,
2629        address: &TransparentAddress,
2630    ) -> Result<Option<(Self::AccountId, Option<TransparentKeyScope>)>, Self::Error> {
2631        wallet::transparent::find_account_uuid_for_transparent_address(
2632            self.conn.borrow(),
2633            &self.params,
2634            address,
2635        )
2636        .map(|opt| opt.map(|(a, s)| (a, s.as_transparent())))
2637    }
2638
2639    #[cfg(feature = "transparent-inputs")]
2640    fn detect_accounts_transparent<'t>(
2641        &self,
2642        spends: impl Iterator<Item = &'t transparent::bundle::OutPoint>,
2643    ) -> Result<std::collections::HashSet<Self::AccountId>, Self::Error> {
2644        wallet::transparent::detect_spending_accounts(self.conn.borrow(), spends)
2645            .map_err(SqliteClientError::from)
2646    }
2647
2648    fn detect_accounts_sapling<'t>(
2649        &self,
2650        spends: impl Iterator<Item = &'t sapling::Nullifier>,
2651    ) -> Result<std::collections::HashSet<Self::AccountId>, Self::Error> {
2652        wallet::sapling::detect_spending_accounts(self.conn.borrow(), spends)
2653            .map_err(SqliteClientError::from)
2654    }
2655
2656    #[cfg(feature = "orchard")]
2657    fn detect_accounts_orchard<'t>(
2658        &self,
2659        spends: impl Iterator<Item = &'t orchard::note::Nullifier>,
2660    ) -> Result<std::collections::HashSet<Self::AccountId>, Self::Error> {
2661        wallet::orchard::detect_spending_accounts(self.conn.borrow(), ORCHARD_TABLES_PREFIX, spends)
2662            .map_err(SqliteClientError::from)
2663    }
2664
2665    #[cfg(feature = "orchard")]
2666    fn detect_accounts_ironwood<'t>(
2667        &self,
2668        spends: impl Iterator<Item = &'t orchard::note::Nullifier>,
2669    ) -> Result<std::collections::HashSet<Self::AccountId>, Self::Error> {
2670        wallet::orchard::detect_spending_accounts(
2671            self.conn.borrow(),
2672            IRONWOOD_TABLES_PREFIX,
2673            spends,
2674        )
2675        .map_err(SqliteClientError::from)
2676    }
2677
2678    #[cfg(feature = "transparent-inputs")]
2679    fn get_wallet_transparent_output(
2680        &self,
2681        outpoint: &OutPoint,
2682        target_height: Option<TargetHeight>,
2683    ) -> Result<Option<WalletTransparentOutput<Self::AccountId>>, Self::Error> {
2684        wallet::transparent::get_wallet_transparent_output(
2685            self.conn.borrow(),
2686            outpoint,
2687            target_height,
2688        )
2689    }
2690
2691    fn get_txs_spending_transparent_outputs_of(
2692        &self,
2693        tx_ref: Self::TxRef,
2694    ) -> Result<Vec<(Self::TxRef, Transaction)>, Self::Error> {
2695        wallet::get_txs_spending_transparent_outputs_of(self.conn.borrow(), &self.params, tx_ref)
2696    }
2697
2698    fn detect_sapling_spend(
2699        &self,
2700        nf: &::sapling::Nullifier,
2701    ) -> Result<Option<Self::TxRef>, Self::Error> {
2702        wallet::query_nullifier_map(self.conn.borrow(), ShieldedPool::Sapling, nf)
2703    }
2704
2705    #[cfg(feature = "orchard")]
2706    fn detect_orchard_spend(
2707        &self,
2708        nf: &::orchard::note::Nullifier,
2709    ) -> Result<Option<Self::TxRef>, Self::Error> {
2710        wallet::query_nullifier_map(self.conn.borrow(), ShieldedPool::Orchard, &nf.to_bytes())
2711    }
2712
2713    #[cfg(feature = "orchard")]
2714    fn detect_ironwood_spend(
2715        &self,
2716        nf: &::orchard::note::Nullifier,
2717    ) -> Result<Option<Self::TxRef>, Self::Error> {
2718        wallet::query_nullifier_map(self.conn.borrow(), ShieldedPool::Ironwood, &nf.to_bytes())
2719    }
2720
2721    #[cfg(feature = "transparent-inputs")]
2722    fn get_account_ref(
2723        &self,
2724        account_uuid: Self::AccountId,
2725    ) -> Result<Self::AccountRef, Self::Error> {
2726        wallet::get_account_ref(self.conn.borrow(), account_uuid)
2727    }
2728
2729    #[cfg(feature = "transparent-inputs")]
2730    fn get_account_internal(
2731        &self,
2732        account_id: Self::AccountRef,
2733    ) -> Result<Option<wallet::Account>, SqliteClientError> {
2734        wallet::get_account_internal(self.conn.borrow(), &self.params, account_id)
2735    }
2736}
2737
2738impl<'a, C: Borrow<rusqlite::Transaction<'a>>, P: consensus::Parameters, CL: Clock, R: RngCore>
2739    LowLevelWalletWrite for WalletDb<C, P, CL, R>
2740{
2741    fn put_block_meta(
2742        &mut self,
2743        block_height: BlockHeight,
2744        block_hash: BlockHash,
2745        block_time: u32,
2746        sapling_commitment_tree_size: u32,
2747        sapling_output_count: u32,
2748        #[cfg(feature = "orchard")] orchard_commitment_tree_size: u32,
2749        #[cfg(feature = "orchard")] orchard_action_count: u32,
2750        #[cfg(feature = "orchard")] ironwood_commitment_tree_size: u32,
2751        #[cfg(feature = "orchard")] ironwood_action_count: u32,
2752    ) -> Result<(), Self::Error> {
2753        wallet::put_block(
2754            self.conn.borrow(),
2755            block_height,
2756            block_hash,
2757            block_time,
2758            sapling_commitment_tree_size,
2759            sapling_output_count,
2760            #[cfg(feature = "orchard")]
2761            orchard_commitment_tree_size,
2762            #[cfg(feature = "orchard")]
2763            orchard_action_count,
2764            #[cfg(feature = "orchard")]
2765            ironwood_commitment_tree_size,
2766            #[cfg(feature = "orchard")]
2767            ironwood_action_count,
2768        )
2769    }
2770
2771    fn put_tx_meta(
2772        &mut self,
2773        tx: &WalletTx<Self::AccountId>,
2774        height: BlockHeight,
2775    ) -> Result<Self::TxRef, Self::Error> {
2776        wallet::put_tx_meta(self.conn.borrow(), tx, height)
2777    }
2778
2779    fn put_tx_data(
2780        &mut self,
2781        tx: &Transaction,
2782        fee: Option<zcash_protocol::value::Zatoshis>,
2783        created_at: Option<time::OffsetDateTime>,
2784        target_height: Option<TargetHeight>,
2785        observed_height: BlockHeight,
2786    ) -> Result<Self::TxRef, Self::Error> {
2787        wallet::put_tx_data(
2788            self.conn.borrow(),
2789            tx,
2790            fee,
2791            created_at,
2792            target_height,
2793            observed_height,
2794        )
2795    }
2796
2797    fn set_transaction_status(
2798        &mut self,
2799        txid: TxId,
2800        status: data_api::TransactionStatus,
2801    ) -> Result<(), Self::Error> {
2802        wallet::set_transaction_status(
2803            self.conn.borrow(),
2804            &self.params,
2805            #[cfg(feature = "transparent-inputs")]
2806            &self.gap_limits,
2807            txid,
2808            status,
2809        )
2810    }
2811
2812    fn put_zip318_classification(
2813        &mut self,
2814        tx_ref: Self::TxRef,
2815        classification: zcash_protocol::zip318::Zip318Classification,
2816    ) -> Result<(), Self::Error> {
2817        wallet::put_zip318_classification(self.conn.borrow(), tx_ref, classification)
2818    }
2819
2820    fn put_received_sapling_note<T: ReceivedSaplingOutput<AccountId = Self::AccountId>>(
2821        &mut self,
2822        output: &T,
2823        tx_ref: Self::TxRef,
2824        target_or_mined_height: Option<BlockHeight>,
2825        spent_in: Option<Self::TxRef>,
2826    ) -> Result<(), Self::Error> {
2827        wallet::sapling::put_received_note(
2828            self.conn.borrow(),
2829            &self.params,
2830            output,
2831            tx_ref,
2832            target_or_mined_height,
2833            spent_in,
2834        )?;
2835
2836        Ok(())
2837    }
2838
2839    fn mark_sapling_note_spent(
2840        &mut self,
2841        nf: &::sapling::Nullifier,
2842        tx_ref: Self::TxRef,
2843    ) -> Result<bool, Self::Error> {
2844        wallet::sapling::mark_sapling_note_spent(self.conn.borrow(), tx_ref, nf)
2845    }
2846
2847    fn track_block_sapling_nullifiers(
2848        &mut self,
2849        block_height: BlockHeight,
2850        nfs: &[(TxIndex, TxId, Vec<::sapling::Nullifier>)],
2851    ) -> Result<(), Self::Error> {
2852        wallet::insert_nullifier_map(self.conn.borrow(), block_height, ShieldedPool::Sapling, nfs)
2853    }
2854
2855    #[cfg(feature = "orchard")]
2856    fn put_received_orchard_note<T: ReceivedOrchardOutput<AccountId = Self::AccountId>>(
2857        &mut self,
2858        output: &T,
2859        tx_ref: Self::TxRef,
2860        target_or_mined_height: Option<BlockHeight>,
2861        spent_in: Option<Self::TxRef>,
2862    ) -> Result<(), Self::Error> {
2863        wallet::orchard::put_received_note(
2864            self.conn.borrow(),
2865            &self.params,
2866            ShieldedPool::Orchard,
2867            output,
2868            tx_ref,
2869            target_or_mined_height,
2870            spent_in,
2871        )?;
2872
2873        Ok(())
2874    }
2875
2876    #[cfg(feature = "orchard")]
2877    fn put_received_ironwood_note<T: ReceivedOrchardOutput<AccountId = Self::AccountId>>(
2878        &mut self,
2879        output: &T,
2880        tx_ref: Self::TxRef,
2881        target_or_mined_height: Option<BlockHeight>,
2882        spent_in: Option<Self::TxRef>,
2883    ) -> Result<(), Self::Error> {
2884        wallet::orchard::put_received_note(
2885            self.conn.borrow(),
2886            &self.params,
2887            ShieldedPool::Ironwood,
2888            output,
2889            tx_ref,
2890            target_or_mined_height,
2891            spent_in,
2892        )?;
2893
2894        Ok(())
2895    }
2896
2897    #[cfg(feature = "orchard")]
2898    fn mark_orchard_note_spent(
2899        &mut self,
2900        nf: &::orchard::note::Nullifier,
2901        tx_ref: Self::TxRef,
2902    ) -> Result<bool, Self::Error> {
2903        wallet::orchard::mark_orchard_note_spent(self.conn.borrow(), tx_ref, nf)
2904    }
2905
2906    #[cfg(feature = "orchard")]
2907    fn mark_ironwood_note_spent(
2908        &mut self,
2909        nf: &::orchard::note::Nullifier,
2910        tx_ref: Self::TxRef,
2911    ) -> Result<bool, Self::Error> {
2912        wallet::orchard::mark_ironwood_note_spent(self.conn.borrow(), tx_ref, nf)
2913    }
2914
2915    #[cfg(feature = "orchard")]
2916    fn track_block_orchard_nullifiers(
2917        &mut self,
2918        block_height: BlockHeight,
2919        nfs: &[(TxIndex, TxId, Vec<::orchard::note::Nullifier>)],
2920    ) -> Result<(), Self::Error> {
2921        wallet::insert_nullifier_map(
2922            self.conn.borrow(),
2923            block_height,
2924            ShieldedPool::Orchard,
2925            &nfs.iter()
2926                .map(|(idx, txid, nfs)| (*idx, *txid, nfs.iter().map(|n| n.to_bytes()).collect()))
2927                .collect::<Vec<_>>(),
2928        )
2929    }
2930
2931    #[cfg(feature = "orchard")]
2932    fn track_block_ironwood_nullifiers(
2933        &mut self,
2934        block_height: BlockHeight,
2935        nfs: &[(TxIndex, TxId, Vec<::orchard::note::Nullifier>)],
2936    ) -> Result<(), Self::Error> {
2937        wallet::insert_nullifier_map(
2938            self.conn.borrow(),
2939            block_height,
2940            ShieldedPool::Ironwood,
2941            &nfs.iter()
2942                .map(|(idx, txid, nfs)| (*idx, *txid, nfs.iter().map(|n| n.to_bytes()).collect()))
2943                .collect::<Vec<_>>(),
2944        )
2945    }
2946
2947    fn prune_tracked_nullifiers(&mut self, pruning_depth: u32) -> Result<(), Self::Error> {
2948        if let Some(meta) = wallet::block_fully_scanned(self.conn.borrow(), &self.params)? {
2949            wallet::prune_nullifier_map(
2950                self.conn.borrow(),
2951                meta.block_height().saturating_sub(pruning_depth),
2952            )?;
2953        }
2954
2955        Ok(())
2956    }
2957
2958    fn put_sent_output(
2959        &mut self,
2960        from_account_uuid: Self::AccountId,
2961        tx_ref: Self::TxRef,
2962        output_index: usize,
2963        recipient: &zcash_client_backend::wallet::Recipient<Self::AccountId>,
2964        value: zcash_protocol::value::Zatoshis,
2965        memo: Option<&zcash_protocol::memo::MemoBytes>,
2966    ) -> Result<(), Self::Error> {
2967        wallet::put_sent_output(
2968            self.conn.borrow(),
2969            &self.params,
2970            from_account_uuid,
2971            tx_ref,
2972            output_index,
2973            recipient,
2974            value,
2975            memo,
2976        )
2977    }
2978
2979    fn update_tx_fee(
2980        &mut self,
2981        tx_ref: Self::TxRef,
2982        fee: zcash_protocol::value::Zatoshis,
2983    ) -> Result<(), Self::Error> {
2984        wallet::update_tx_fee(self.conn.borrow(), tx_ref, fee)
2985    }
2986
2987    #[cfg(feature = "transparent-inputs")]
2988    fn put_transparent_output(
2989        &mut self,
2990        output: &WalletTransparentOutput<Self::AccountId>,
2991        observation_height: BlockHeight,
2992        known_unspent: bool,
2993    ) -> Result<(Self::AccountId, Option<TransparentKeyScope>), Self::Error> {
2994        let (_, account_uuid, key_scope, _) = wallet::transparent::put_transparent_output(
2995            self.conn.borrow(),
2996            &self.params,
2997            &self.gap_limits,
2998            output,
2999            observation_height,
3000            known_unspent,
3001        )?;
3002
3003        Ok((account_uuid, key_scope.as_transparent()))
3004    }
3005
3006    #[cfg(feature = "transparent-inputs")]
3007    fn mark_transparent_utxo_spent(
3008        &mut self,
3009        outpoint: &OutPoint,
3010        spent_in_tx: Self::TxRef,
3011    ) -> Result<bool, Self::Error> {
3012        wallet::transparent::mark_transparent_utxo_spent(self.conn.borrow(), spent_in_tx, outpoint)
3013    }
3014
3015    #[cfg(feature = "transparent-inputs")]
3016    fn generate_transparent_gap_addresses(
3017        &mut self,
3018        account_id: Self::AccountId,
3019        key_scope: TransparentKeyScope,
3020        request: UnifiedAddressRequest,
3021    ) -> Result<(), Self::Error> {
3022        generate_transparent_gap_addresses(self, self.gap_limits, account_id, key_scope, request)?;
3023        Ok(())
3024    }
3025
3026    #[cfg(feature = "transparent-inputs")]
3027    fn queue_transparent_spend_detection(
3028        &mut self,
3029        receiving_address: TransparentAddress,
3030        tx_ref: Self::TxRef,
3031        output_index: u32,
3032    ) -> Result<(), Self::Error> {
3033        wallet::transparent::queue_transparent_spend_detection(
3034            self.conn.borrow(),
3035            &self.params,
3036            receiving_address,
3037            tx_ref,
3038            output_index,
3039        )
3040    }
3041
3042    #[cfg(feature = "transparent-inputs")]
3043    fn queue_transparent_input_retrieval(
3044        &mut self,
3045        tx_ref: Self::TxRef,
3046        d_tx: &DecryptedTransaction<Transaction, Self::AccountId>,
3047    ) -> Result<(), Self::Error> {
3048        wallet::queue_transparent_input_retrieval(self.conn.borrow(), tx_ref, d_tx)
3049    }
3050
3051    fn queue_tx_retrieval(
3052        &mut self,
3053        txids: impl Iterator<Item = TxId>,
3054        dependent_tx_ref: Option<Self::TxRef>,
3055    ) -> Result<(), Self::Error> {
3056        wallet::queue_tx_retrieval(self.conn.borrow(), txids, dependent_tx_ref)
3057    }
3058
3059    fn queue_tx_status(&mut self, txid: TxId) -> Result<(), Self::Error> {
3060        wallet::queue_tx_status(self.conn.borrow(), txid)
3061    }
3062
3063    fn delete_retrieval_queue_entries(&mut self, txid: TxId) -> Result<(), Self::Error> {
3064        wallet::delete_retrieval_queue_entries(self.conn.borrow(), txid)
3065    }
3066
3067    fn notify_scan_complete(
3068        &mut self,
3069        range: Range<BlockHeight>,
3070        wallet_note_positions: &[(ShieldedPool, Position)],
3071    ) -> Result<(), Self::Error> {
3072        wallet::scanning::scan_complete(
3073            self.conn.borrow(),
3074            &self.params,
3075            range,
3076            wallet_note_positions,
3077        )
3078    }
3079
3080    #[cfg(feature = "transparent-inputs")]
3081    fn update_gap_limits(
3082        &mut self,
3083        gap_limits: &GapLimits,
3084        txid: TxId,
3085        observation_height: BlockHeight,
3086    ) -> Result<(), Self::Error> {
3087        wallet::transparent::update_gap_limits(
3088            self.conn.borrow(),
3089            &self.params,
3090            gap_limits,
3091            txid,
3092            observation_height,
3093        )
3094    }
3095}
3096
3097pub(crate) type SaplingShardStore<C> = SqliteShardStore<C, sapling::Node, SAPLING_SHARD_HEIGHT>;
3098pub(crate) type SaplingCommitmentTree<C> =
3099    ShardTree<SaplingShardStore<C>, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>;
3100
3101pub(crate) fn sapling_tree<C>(
3102    conn: C,
3103) -> Result<SaplingCommitmentTree<C>, ShardTreeError<commitment_tree::Error>>
3104where
3105    SaplingShardStore<C>: ShardStore<H = sapling::Node, CheckpointId = BlockHeight>,
3106{
3107    Ok(ShardTree::new(
3108        SqliteShardStore::from_connection(conn, SAPLING_TABLES_PREFIX)
3109            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?,
3110        PRUNING_DEPTH.try_into().unwrap(),
3111    ))
3112}
3113
3114#[cfg(feature = "orchard")]
3115pub(crate) type OrchardShardStore<C> =
3116    SqliteShardStore<C, orchard::tree::MerkleHashOrchard, ORCHARD_SHARD_HEIGHT>;
3117
3118#[cfg(feature = "orchard")]
3119pub(crate) type OrchardCommitmentTree<C> = ShardTree<
3120    OrchardShardStore<C>,
3121    { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
3122    ORCHARD_SHARD_HEIGHT,
3123>;
3124
3125#[cfg(feature = "orchard")]
3126pub(crate) fn orchard_tree<C>(
3127    conn: C,
3128) -> Result<OrchardCommitmentTree<C>, ShardTreeError<commitment_tree::Error>>
3129where
3130    OrchardShardStore<C>:
3131        ShardStore<H = orchard::tree::MerkleHashOrchard, CheckpointId = BlockHeight>,
3132{
3133    Ok(ShardTree::new(
3134        SqliteShardStore::from_connection(conn, ORCHARD_TABLES_PREFIX)
3135            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?,
3136        PRUNING_DEPTH.try_into().unwrap(),
3137    ))
3138}
3139
3140/// The shard store backing the Ironwood note commitment tree.
3141///
3142/// Ironwood note commitments are Orchard-shaped, so this reuses the Orchard hash type and shard
3143/// height; only the backing table prefix differs (see [`IRONWOOD_TABLES_PREFIX`]). It is defined
3144/// as a distinct alias to make Ironwood usage self-documenting at call sites.
3145#[cfg(feature = "orchard")]
3146pub(crate) type IronwoodShardStore<C> =
3147    SqliteShardStore<C, orchard::tree::MerkleHashOrchard, IRONWOOD_SHARD_HEIGHT>;
3148
3149#[cfg(feature = "orchard")]
3150pub(crate) type IronwoodCommitmentTree<C> = ShardTree<
3151    IronwoodShardStore<C>,
3152    { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
3153    IRONWOOD_SHARD_HEIGHT,
3154>;
3155
3156/// Returns a handle to the Ironwood note commitment tree.
3157#[cfg(feature = "orchard")]
3158pub(crate) fn ironwood_tree<C>(
3159    conn: C,
3160) -> Result<IronwoodCommitmentTree<C>, ShardTreeError<commitment_tree::Error>>
3161where
3162    IronwoodShardStore<C>:
3163        ShardStore<H = orchard::tree::MerkleHashOrchard, CheckpointId = BlockHeight>,
3164{
3165    Ok(ShardTree::new(
3166        SqliteShardStore::from_connection(conn, IRONWOOD_TABLES_PREFIX)
3167            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?,
3168        PRUNING_DEPTH.try_into().unwrap(),
3169    ))
3170}
3171
3172impl<C: BorrowMut<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletCommitmentTrees
3173    for WalletDb<C, P, CL, R>
3174{
3175    type Error = commitment_tree::Error;
3176    type SaplingShardStore<'a> = SaplingShardStore<&'a rusqlite::Transaction<'a>>;
3177
3178    fn with_sapling_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
3179    where
3180        for<'a> F:
3181            FnMut(&'a mut SaplingCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
3182        E: From<ShardTreeError<Self::Error>>,
3183    {
3184        let tx = self
3185            .conn
3186            .borrow_mut()
3187            .transaction()
3188            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3189        let result = {
3190            let mut shardtree = sapling_tree(&tx)?;
3191            callback(&mut shardtree)?
3192        };
3193
3194        tx.commit()
3195            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3196        Ok(result)
3197    }
3198
3199    fn put_sapling_subtree_roots(
3200        &mut self,
3201        start_index: u64,
3202        roots: &[CommitmentTreeRoot<sapling::Node>],
3203    ) -> Result<(), ShardTreeError<Self::Error>> {
3204        let tx = self
3205            .conn
3206            .borrow_mut()
3207            .transaction()
3208            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3209        put_shard_roots::<_, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>(
3210            &tx,
3211            SAPLING_TABLES_PREFIX,
3212            start_index,
3213            roots,
3214        )?;
3215        tx.commit()
3216            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3217        Ok(())
3218    }
3219
3220    fn get_sapling_subtree_root(
3221        &mut self,
3222        index: u64,
3223    ) -> Result<Option<sapling::Node>, ShardTreeError<Self::Error>> {
3224        wallet::commitment_tree::get_subtree_root(self.conn.borrow(), SAPLING_TABLES_PREFIX, index)
3225            .map_err(ShardTreeError::Storage)
3226    }
3227
3228    #[cfg(feature = "orchard")]
3229    type OrchardShardStore<'a> = SqliteShardStore<
3230        &'a rusqlite::Transaction<'a>,
3231        orchard::tree::MerkleHashOrchard,
3232        ORCHARD_SHARD_HEIGHT,
3233    >;
3234
3235    #[cfg(feature = "orchard")]
3236    fn with_orchard_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
3237    where
3238        for<'a> F:
3239            FnMut(&'a mut OrchardCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
3240        E: From<ShardTreeError<Self::Error>>,
3241    {
3242        let tx = self
3243            .conn
3244            .borrow_mut()
3245            .transaction()
3246            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3247        let result = {
3248            let mut shardtree = orchard_tree(&tx)?;
3249            callback(&mut shardtree)?
3250        };
3251
3252        tx.commit()
3253            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3254        Ok(result)
3255    }
3256
3257    #[cfg(feature = "orchard")]
3258    fn put_orchard_subtree_roots(
3259        &mut self,
3260        start_index: u64,
3261        roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
3262    ) -> Result<(), ShardTreeError<Self::Error>> {
3263        let tx = self
3264            .conn
3265            .borrow_mut()
3266            .transaction()
3267            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3268        put_shard_roots::<_, { ORCHARD_SHARD_HEIGHT * 2 }, ORCHARD_SHARD_HEIGHT>(
3269            &tx,
3270            ORCHARD_TABLES_PREFIX,
3271            start_index,
3272            roots,
3273        )?;
3274        tx.commit()
3275            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3276        Ok(())
3277    }
3278
3279    #[cfg(feature = "orchard")]
3280    fn get_orchard_subtree_root(
3281        &mut self,
3282        index: u64,
3283    ) -> Result<Option<orchard::tree::MerkleHashOrchard>, ShardTreeError<Self::Error>> {
3284        wallet::commitment_tree::get_subtree_root(self.conn.borrow(), ORCHARD_TABLES_PREFIX, index)
3285            .map_err(ShardTreeError::Storage)
3286    }
3287
3288    #[cfg(feature = "orchard")]
3289    fn put_ironwood_subtree_roots(
3290        &mut self,
3291        start_index: u64,
3292        roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
3293    ) -> Result<(), ShardTreeError<Self::Error>> {
3294        let tx = self
3295            .conn
3296            .borrow_mut()
3297            .transaction()
3298            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3299        put_shard_roots::<_, { ORCHARD_SHARD_HEIGHT * 2 }, ORCHARD_SHARD_HEIGHT>(
3300            &tx,
3301            IRONWOOD_TABLES_PREFIX,
3302            start_index,
3303            roots,
3304        )?;
3305        tx.commit()
3306            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3307        Ok(())
3308    }
3309
3310    #[cfg(feature = "orchard")]
3311    fn get_ironwood_subtree_root(
3312        &mut self,
3313        index: u64,
3314    ) -> Result<Option<orchard::tree::MerkleHashOrchard>, ShardTreeError<Self::Error>> {
3315        wallet::commitment_tree::get_subtree_root(self.conn.borrow(), IRONWOOD_TABLES_PREFIX, index)
3316            .map_err(ShardTreeError::Storage)
3317    }
3318
3319    #[cfg(feature = "orchard")]
3320    fn with_ironwood_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<Option<A>, E>
3321    where
3322        for<'a> F:
3323            FnMut(&'a mut IronwoodCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
3324        E: From<ShardTreeError<Self::Error>>,
3325    {
3326        let tx = self
3327            .conn
3328            .borrow_mut()
3329            .transaction()
3330            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3331        let result = {
3332            let mut shardtree = ironwood_tree(&tx)?;
3333            callback(&mut shardtree)?
3334        };
3335
3336        tx.commit()
3337            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
3338        Ok(Some(result))
3339    }
3340}
3341
3342impl<P: consensus::Parameters, CL, R> WalletCommitmentTrees
3343    for WalletDb<SqlTransaction<'_>, P, CL, R>
3344{
3345    type Error = commitment_tree::Error;
3346    type SaplingShardStore<'a> = crate::SaplingShardStore<&'a rusqlite::Transaction<'a>>;
3347
3348    fn with_sapling_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
3349    where
3350        for<'a> F:
3351            FnMut(&'a mut SaplingCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
3352        E: From<ShardTreeError<commitment_tree::Error>>,
3353    {
3354        let mut shardtree = sapling_tree(self.conn.0)?;
3355        let result = callback(&mut shardtree)?;
3356
3357        Ok(result)
3358    }
3359
3360    fn put_sapling_subtree_roots(
3361        &mut self,
3362        start_index: u64,
3363        roots: &[CommitmentTreeRoot<sapling::Node>],
3364    ) -> Result<(), ShardTreeError<Self::Error>> {
3365        put_shard_roots::<_, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>(
3366            self.conn.0,
3367            SAPLING_TABLES_PREFIX,
3368            start_index,
3369            roots,
3370        )
3371    }
3372
3373    fn get_sapling_subtree_root(
3374        &mut self,
3375        index: u64,
3376    ) -> Result<Option<sapling::Node>, ShardTreeError<Self::Error>> {
3377        wallet::commitment_tree::get_subtree_root(self.conn.0, SAPLING_TABLES_PREFIX, index)
3378            .map_err(ShardTreeError::Storage)
3379    }
3380
3381    #[cfg(feature = "orchard")]
3382    type OrchardShardStore<'a> = crate::OrchardShardStore<&'a rusqlite::Transaction<'a>>;
3383
3384    #[cfg(feature = "orchard")]
3385    fn with_orchard_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
3386    where
3387        for<'a> F:
3388            FnMut(&'a mut OrchardCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
3389        E: From<ShardTreeError<Self::Error>>,
3390    {
3391        let mut shardtree = orchard_tree(self.conn.0)?;
3392        let result = callback(&mut shardtree)?;
3393
3394        Ok(result)
3395    }
3396
3397    #[cfg(feature = "orchard")]
3398    fn put_orchard_subtree_roots(
3399        &mut self,
3400        start_index: u64,
3401        roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
3402    ) -> Result<(), ShardTreeError<Self::Error>> {
3403        put_shard_roots::<_, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, ORCHARD_SHARD_HEIGHT>(
3404            self.conn.0,
3405            ORCHARD_TABLES_PREFIX,
3406            start_index,
3407            roots,
3408        )
3409    }
3410
3411    #[cfg(feature = "orchard")]
3412    fn get_orchard_subtree_root(
3413        &mut self,
3414        index: u64,
3415    ) -> Result<Option<orchard::tree::MerkleHashOrchard>, ShardTreeError<Self::Error>> {
3416        wallet::commitment_tree::get_subtree_root(self.conn.0, ORCHARD_TABLES_PREFIX, index)
3417            .map_err(ShardTreeError::Storage)
3418    }
3419
3420    #[cfg(feature = "orchard")]
3421    fn put_ironwood_subtree_roots(
3422        &mut self,
3423        start_index: u64,
3424        roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
3425    ) -> Result<(), ShardTreeError<Self::Error>> {
3426        put_shard_roots::<_, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, ORCHARD_SHARD_HEIGHT>(
3427            self.conn.0,
3428            IRONWOOD_TABLES_PREFIX,
3429            start_index,
3430            roots,
3431        )
3432    }
3433
3434    #[cfg(feature = "orchard")]
3435    fn get_ironwood_subtree_root(
3436        &mut self,
3437        index: u64,
3438    ) -> Result<Option<orchard::tree::MerkleHashOrchard>, ShardTreeError<Self::Error>> {
3439        wallet::commitment_tree::get_subtree_root(self.conn.0, IRONWOOD_TABLES_PREFIX, index)
3440            .map_err(ShardTreeError::Storage)
3441    }
3442
3443    #[cfg(feature = "orchard")]
3444    fn with_ironwood_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<Option<A>, E>
3445    where
3446        for<'a> F:
3447            FnMut(&'a mut IronwoodCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
3448        E: From<ShardTreeError<Self::Error>>,
3449    {
3450        let mut shardtree = ironwood_tree(self.conn.0)?;
3451        let result = callback(&mut shardtree)?;
3452
3453        Ok(Some(result))
3454    }
3455}
3456
3457#[cfg(feature = "transparent-inputs")]
3458impl<'a, C: Borrow<rusqlite::Transaction<'a>>, P: consensus::Parameters, CL: Clock, R: RngCore>
3459    AddressStore for WalletDb<C, P, CL, R>
3460{
3461    type Error = SqliteClientError;
3462    type AccountRef = AccountRef;
3463
3464    fn find_gap_start(
3465        &self,
3466        account_ref: Self::AccountRef,
3467        key_scope: TransparentKeyScope,
3468        gap_limit: u32,
3469    ) -> Result<Option<NonHardenedChildIndex>, Self::Error> {
3470        wallet::transparent::find_gap_start(self.conn.borrow(), account_ref, key_scope, gap_limit)
3471    }
3472
3473    fn store_address_range(
3474        &mut self,
3475        account_id: Self::AccountRef,
3476        key_scope: TransparentKeyScope,
3477        list: Vec<(Address, TransparentAddress, NonHardenedChildIndex)>,
3478    ) -> Result<(), Self::Error> {
3479        wallet::transparent::store_address_range(
3480            self.conn.borrow(),
3481            &self.params,
3482            account_id,
3483            key_scope,
3484            list,
3485        )
3486    }
3487}
3488
3489#[cfg(feature = "orchard")]
3490impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletDb<C, P, CL, R> {
3491    /// Return all Orchard notes received at or before `height`
3492    /// and unspent as of that height, for the given account.
3493    ///
3494    /// Unlike [`InputSource::select_unspent_notes`] (which applies confirmation,
3495    /// dust, and expiry filters for transaction construction), this returns every
3496    /// note that existed and was unspent at the given height.
3497    ///
3498    /// This function does not verify that a Merkle witness can be constructed
3499    /// for each returned note at `height`. Witness construction is a separate
3500    /// concern intended to be handled by the callers. As an example, a companion
3501    /// `WalletDb::generate_orchard_witnesses_at_historical_height` returns an
3502    /// actionable error for any position the wallet cannot witness at `height`
3503    /// (for example, because the wallet has not synced through `height`, the checkpoint was pruned,
3504    /// or the position does not belong to the wallet).
3505    pub fn get_unspent_orchard_notes_at_historical_height(
3506        &self,
3507        account: AccountUuid,
3508        height: BlockHeight,
3509    ) -> Result<Vec<ReceivedNote<ReceivedNoteId, orchard::note::Note>>, SqliteClientError> {
3510        wallet::orchard::get_unspent_orchard_notes_at_historical_height(
3511            self.conn.borrow(),
3512            &self.params,
3513            account,
3514            height,
3515        )
3516    }
3517
3518    /// Returns every Ironwood note received by `height` that was unspent at that height.
3519    ///
3520    /// This does not apply transaction construction filters or check witness availability.
3521    /// Use [`Self::generate_ironwood_witnesses_at_historical_height`] to check the latter.
3522    ///
3523    /// # Errors
3524    ///
3525    /// Returns an error if the query fails or a note cannot be reconstructed.
3526    pub fn get_unspent_ironwood_notes_at_historical_height(
3527        &self,
3528        account: AccountUuid,
3529        height: BlockHeight,
3530    ) -> Result<Vec<ReceivedNote<ReceivedNoteId, orchard::note::Note>>, SqliteClientError> {
3531        wallet::orchard::get_unspent_ironwood_notes_at_historical_height(
3532            self.conn.borrow(),
3533            &self.params,
3534            account,
3535            height,
3536        )
3537    }
3538
3539    /// Generates Orchard Merkle witnesses at a historical height.
3540    ///
3541    /// Loads the wallet's Orchard shard data into an ephemeral in-memory
3542    /// `ShardStore`, inserts the provided frontier at `height` as a checkpoint,
3543    /// and generates a witness for each of the given note positions.
3544    ///
3545    /// The caller must provide the valid frontier at the given height. The wallet DB
3546    /// is strictly read-only; shard data is read but not modified.
3547    ///
3548    /// # Errors
3549    ///
3550    /// Returns:
3551    /// - [`SqliteClientError::CommitmentTree`] if reading the wallet's shard
3552    ///   or cap data fails, or if the shard data reconstructed from the
3553    ///   wallet is internally inconsistent at a node the computation
3554    ///   requires.
3555    /// - [`SqliteClientError::HistoricalFrontierInvalid`] if
3556    ///   `frontier_at_height` is inconsistent with the shard data
3557    ///   reconstructed from the wallet at `height`.
3558    /// - [`SqliteClientError::HistoricalWitnessUnavailable`] if a witness
3559    ///   cannot be generated for one of `note_positions` at `height` (most
3560    ///   commonly because the wallet has not yet synced through that
3561    ///   height).
3562    pub fn generate_orchard_witnesses_at_historical_height(
3563        &self,
3564        note_positions: &[Position],
3565        frontier_at_height: incrementalmerkletree::frontier::NonEmptyFrontier<
3566            orchard::tree::MerkleHashOrchard,
3567        >,
3568        height: BlockHeight,
3569    ) -> Result<
3570        Vec<
3571            incrementalmerkletree::MerklePath<
3572                orchard::tree::MerkleHashOrchard,
3573                { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
3574            >,
3575        >,
3576        SqliteClientError,
3577    > {
3578        wallet::commitment_tree::generate_orchard_witnesses_at_historical_height(
3579            self.conn.borrow(),
3580            note_positions,
3581            frontier_at_height,
3582            height,
3583        )
3584    }
3585
3586    /// Generates Ironwood Merkle witnesses at a historical height.
3587    ///
3588    /// Loads the wallet's Ironwood shard data into an ephemeral in-memory
3589    /// `ShardStore`, inserts the provided frontier at `height` as a checkpoint,
3590    /// and generates a witness for each of the given note positions.
3591    ///
3592    /// The caller must provide the valid frontier at the given height. The wallet DB
3593    /// is strictly read-only; shard data is read but not modified.
3594    ///
3595    /// # Errors
3596    ///
3597    /// Returns:
3598    /// - [`SqliteClientError::CommitmentTree`] if reading the wallet's shard
3599    ///   or cap data fails, or if the shard data reconstructed from the
3600    ///   wallet is internally inconsistent at a node the computation
3601    ///   requires.
3602    /// - [`SqliteClientError::HistoricalFrontierInvalid`] if
3603    ///   `frontier_at_height` is inconsistent with the shard data
3604    ///   reconstructed from the wallet at `height`.
3605    /// - [`SqliteClientError::HistoricalWitnessUnavailable`] if a witness
3606    ///   cannot be generated for one of `note_positions` at `height` (most
3607    ///   commonly because the wallet has not yet synced through that
3608    ///   height).
3609    pub fn generate_ironwood_witnesses_at_historical_height(
3610        &self,
3611        note_positions: &[Position],
3612        frontier_at_height: incrementalmerkletree::frontier::NonEmptyFrontier<
3613            orchard::tree::MerkleHashOrchard,
3614        >,
3615        height: BlockHeight,
3616    ) -> Result<
3617        Vec<
3618            incrementalmerkletree::MerklePath<
3619                orchard::tree::MerkleHashOrchard,
3620                { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
3621            >,
3622        >,
3623        SqliteClientError,
3624    > {
3625        wallet::commitment_tree::generate_ironwood_witnesses_at_historical_height(
3626            self.conn.borrow(),
3627            note_positions,
3628            frontier_at_height,
3629            height,
3630        )
3631    }
3632}
3633
3634/// A handle for the SQLite block source.
3635pub struct BlockDb(rusqlite::Connection);
3636
3637impl BlockDb {
3638    /// Opens a connection to the wallet database stored at the specified path.
3639    pub fn for_path<P: AsRef<Path>>(path: P) -> Result<Self, rusqlite::Error> {
3640        rusqlite::Connection::open(path).map(BlockDb)
3641    }
3642
3643    #[cfg(any(test, feature = "test-dependencies"))]
3644    pub(crate) fn from_connection(conn: rusqlite::Connection) -> Self {
3645        Self(conn)
3646    }
3647}
3648
3649impl BlockSource for BlockDb {
3650    type Error = SqliteClientError;
3651
3652    fn with_blocks<F, DbErrT>(
3653        &self,
3654        from_height: Option<BlockHeight>,
3655        limit: Option<usize>,
3656        with_row: F,
3657    ) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>
3658    where
3659        F: FnMut(CompactBlock) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>,
3660    {
3661        chain::blockdb_with_blocks(self, from_height, limit, with_row)
3662    }
3663}
3664
3665/// A block source that reads block data from disk and block metadata from a SQLite database.
3666///
3667/// This block source expects each compact block to be stored on disk in the `blocks` subdirectory
3668/// of the `blockstore_root` path provided at the time of construction. Each block should be
3669/// written, as the serialized bytes of its protobuf representation, where the path for each block
3670/// has the pattern:
3671///
3672/// `<blockstore_root>/blocks/<block_height>-<block_hash>-compactblock`
3673///
3674/// where `<block_height>` is the decimal value of the height at which the block was mined, and
3675/// `<block_hash>` is the hexadecimal representation of the block hash, as produced by the
3676/// [`fmt::Display`] implementation for [`zcash_primitives::block::BlockHash`].
3677///
3678/// This block source is intended to be used with the following data flow:
3679/// * When the cache is being filled:
3680///   * The caller requests the current maximum height at which cached data is available
3681///     using [`FsBlockDb::get_max_cached_height`]. If no cached data is available, the caller
3682///     can use the wallet's synced-to height for the following operations instead.
3683///   * (recommended for privacy) the caller should round the returned height down to some 100- /
3684///     1000-block boundary.
3685///   * The caller uses the lightwalletd's `getblock` gRPC method to obtain a stream of blocks.
3686///     For each block returned, the caller writes the compact block to `blocks_dir` using the
3687///     path format specified above. It is fine to overwrite an existing block, since block hashes
3688///     are immutable and collision-resistant.
3689///   * Once a caller-determined number of blocks have been successfully written to disk, the
3690///     caller should invoke [`FsBlockDb::write_block_metadata`] with the metadata for each block
3691///     written to disk.
3692/// * The cache can then be scanned using the [`BlockSource`] implementation, providing the
3693///   wallet's synced-to-height as a starting point.
3694/// * When part of the cache is no longer needed:
3695///   * The caller determines some height `H` that is the earliest block data it needs to preserve.
3696///     This might be determined based on where the wallet is fully-synced to, or other heuristics.
3697///   * The caller searches the defined filesystem folder for all files beginning in `HEIGHT-*` where
3698///     `HEIGHT < H`, and deletes those files.
3699///
3700/// Note: This API is unstable, and may change in the future. In particular, the [`BlockSource`]
3701/// API and the above description currently assume that scanning is performed in linear block
3702/// order; this assumption is likely to be weakened and/or removed in a future update.
3703#[cfg(feature = "unstable")]
3704pub struct FsBlockDb {
3705    conn: rusqlite::Connection,
3706    blocks_dir: PathBuf,
3707}
3708
3709/// Errors that can be generated by the filesystem/sqlite-backed
3710/// block source.
3711#[derive(Debug)]
3712#[cfg(feature = "unstable")]
3713#[non_exhaustive]
3714pub enum FsBlockDbError {
3715    /// Filesystem I/O error.
3716    Fs(io::Error),
3717    /// SQLite database error.
3718    Db(rusqlite::Error),
3719    /// Protobuf decoding error.
3720    Protobuf(prost::DecodeError),
3721    /// The expected block file was not found at the given path.
3722    MissingBlockPath(PathBuf),
3723    /// The block store root directory is invalid.
3724    InvalidBlockstoreRoot(PathBuf),
3725    /// A block file path within the store is invalid.
3726    InvalidBlockPath(PathBuf),
3727    /// Data in the block store is corrupted.
3728    CorruptedData(String),
3729    /// The requested block was not found in the cache.
3730    CacheMiss(BlockHeight),
3731}
3732
3733#[cfg(feature = "unstable")]
3734impl From<io::Error> for FsBlockDbError {
3735    fn from(err: io::Error) -> Self {
3736        FsBlockDbError::Fs(err)
3737    }
3738}
3739
3740#[cfg(feature = "unstable")]
3741impl From<rusqlite::Error> for FsBlockDbError {
3742    fn from(err: rusqlite::Error) -> Self {
3743        FsBlockDbError::Db(err)
3744    }
3745}
3746
3747#[cfg(feature = "unstable")]
3748impl From<prost::DecodeError> for FsBlockDbError {
3749    fn from(e: prost::DecodeError) -> Self {
3750        FsBlockDbError::Protobuf(e)
3751    }
3752}
3753
3754#[cfg(feature = "unstable")]
3755impl FsBlockDb {
3756    /// Creates a filesystem-backed block store at the given path.
3757    ///
3758    /// This will construct or open a SQLite database at the path
3759    /// `<fsblockdb_root>/blockmeta.sqlite` and will ensure that a directory exists at
3760    /// `<fsblockdb_root>/blocks` where this block store will expect to find serialized block
3761    /// files as described for [`FsBlockDb`].
3762    ///
3763    /// An application using this constructor should ensure that they call
3764    /// [`crate::chain::init::init_blockmeta_db`] at application startup to ensure
3765    /// that the resulting metadata database is properly initialized and has had all required
3766    /// migrations applied before use.
3767    pub fn for_path<P: AsRef<Path>>(fsblockdb_root: P) -> Result<Self, FsBlockDbError> {
3768        let meta = fs::metadata(&fsblockdb_root).map_err(FsBlockDbError::Fs)?;
3769        if meta.is_dir() {
3770            let db_path = fsblockdb_root.as_ref().join("blockmeta.sqlite");
3771            let blocks_dir = fsblockdb_root.as_ref().join("blocks");
3772            fs::create_dir_all(&blocks_dir)?;
3773            Ok(FsBlockDb {
3774                conn: rusqlite::Connection::open(db_path).map_err(FsBlockDbError::Db)?,
3775                blocks_dir,
3776            })
3777        } else {
3778            Err(FsBlockDbError::InvalidBlockstoreRoot(
3779                fsblockdb_root.as_ref().to_path_buf(),
3780            ))
3781        }
3782    }
3783
3784    /// Returns the maximum height of blocks known to the block metadata database.
3785    pub fn get_max_cached_height(&self) -> Result<Option<BlockHeight>, FsBlockDbError> {
3786        Ok(chain::blockmetadb_get_max_cached_height(&self.conn)?)
3787    }
3788
3789    /// Adds a set of block metadata entries to the metadata database, overwriting any
3790    /// existing entries at the given block heights.
3791    ///
3792    /// This will return an error if any block file corresponding to one of these metadata records
3793    /// is absent from the blocks directory.
3794    pub fn write_block_metadata(&self, block_meta: &[BlockMeta]) -> Result<(), FsBlockDbError> {
3795        for m in block_meta {
3796            let block_path = m.block_file_path(&self.blocks_dir);
3797            match fs::metadata(&block_path) {
3798                Err(e) => {
3799                    return Err(match e.kind() {
3800                        io::ErrorKind::NotFound => FsBlockDbError::MissingBlockPath(block_path),
3801                        _ => FsBlockDbError::Fs(e),
3802                    });
3803                }
3804                Ok(meta) => {
3805                    if !meta.is_file() {
3806                        return Err(FsBlockDbError::InvalidBlockPath(block_path));
3807                    }
3808                }
3809            }
3810        }
3811
3812        Ok(chain::blockmetadb_insert(&self.conn, block_meta)?)
3813    }
3814
3815    /// Returns the metadata for the block with the given height, if it exists in the
3816    /// database.
3817    pub fn find_block(&self, height: BlockHeight) -> Result<Option<BlockMeta>, FsBlockDbError> {
3818        Ok(chain::blockmetadb_find_block(&self.conn, height)?)
3819    }
3820
3821    /// Rewinds the BlockMeta Db to the `block_height` provided.
3822    ///
3823    /// This doesn't delete any files referenced by the records
3824    /// stored in BlockMeta.
3825    ///
3826    /// If the requested height is greater than or equal to the height
3827    /// of the last scanned block, or if the DB is empty, this function
3828    /// does nothing.
3829    pub fn truncate_to_height(&self, block_height: BlockHeight) -> Result<(), FsBlockDbError> {
3830        Ok(chain::blockmetadb_truncate_to_height(
3831            &self.conn,
3832            block_height,
3833        )?)
3834    }
3835}
3836
3837#[cfg(feature = "unstable")]
3838impl BlockSource for FsBlockDb {
3839    type Error = FsBlockDbError;
3840
3841    fn with_blocks<F, DbErrT>(
3842        &self,
3843        from_height: Option<BlockHeight>,
3844        limit: Option<usize>,
3845        with_row: F,
3846    ) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>
3847    where
3848        F: FnMut(CompactBlock) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>,
3849    {
3850        fsblockdb_with_blocks(self, from_height, limit, with_row)
3851    }
3852}
3853
3854#[cfg(feature = "unstable")]
3855impl std::fmt::Display for FsBlockDbError {
3856    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3857        match self {
3858            FsBlockDbError::Fs(io_error) => {
3859                write!(f, "Failed to access the file system: {io_error}")
3860            }
3861            FsBlockDbError::Db(e) => {
3862                write!(f, "There was a problem with the sqlite db: {e}")
3863            }
3864            FsBlockDbError::Protobuf(e) => {
3865                write!(f, "Failed to parse protobuf-encoded record: {e}")
3866            }
3867            FsBlockDbError::MissingBlockPath(block_path) => {
3868                write!(
3869                    f,
3870                    "CompactBlock file expected but not found at {}",
3871                    block_path.display(),
3872                )
3873            }
3874            FsBlockDbError::InvalidBlockstoreRoot(fsblockdb_root) => {
3875                write!(
3876                    f,
3877                    "The block storage root {} is not a directory",
3878                    fsblockdb_root.display(),
3879                )
3880            }
3881            FsBlockDbError::InvalidBlockPath(block_path) => {
3882                write!(
3883                    f,
3884                    "CompactBlock path {} is not a file",
3885                    block_path.display(),
3886                )
3887            }
3888            FsBlockDbError::CorruptedData(e) => {
3889                write!(
3890                    f,
3891                    "The block cache has corrupted data and this caused an error: {e}",
3892                )
3893            }
3894            FsBlockDbError::CacheMiss(height) => {
3895                write!(
3896                    f,
3897                    "Requested height {height} does not exist in the block cache"
3898                )
3899            }
3900        }
3901    }
3902}
3903
3904#[cfg(test)]
3905#[macro_use]
3906extern crate assert_matches;
3907
3908#[cfg(test)]
3909mod tests {
3910    use std::time::{Duration, SystemTime};
3911
3912    use secrecy::{ExposeSecret, Secret, SecretVec};
3913    use uuid::Uuid;
3914    #[cfg(feature = "orchard")]
3915    use zcash_client_backend::data_api::error::FindAccountForAddressError;
3916    use zcash_client_backend::data_api::{
3917        Account, AccountBirthday, AccountPurpose, AccountSource, SAPLING_SHARD_HEIGHT,
3918        WalletCommitmentTrees, WalletRead, WalletTest, WalletWrite,
3919        chain::{ChainState, CommitmentTreeRoot},
3920        testing::{TestBuilder, TestState},
3921    };
3922    use zcash_keys::{
3923        address::{Address, UnifiedAddress},
3924        keys::{
3925            ReceiverRequirement::*, UnifiedAddressRequest, UnifiedFullViewingKey,
3926            UnifiedIncomingViewingKey, UnifiedSpendingKey,
3927        },
3928    };
3929    use zcash_primitives::block::BlockHash;
3930    use zcash_protocol::{consensus, local_consensus::LocalNetwork};
3931    use zip32::DiversifierIndex;
3932
3933    use crate::{
3934        AccountUuid,
3935        error::SqliteClientError,
3936        testing::db::{TestDb, TestDbFactory},
3937        util::Clock as _,
3938        wallet::MIN_SHIELDED_DIVERSIFIER_OFFSET,
3939    };
3940
3941    use incrementalmerkletree::Hashable as _;
3942    #[cfg(feature = "unstable")]
3943    use {
3944        crate::testing::FsBlockCache,
3945        zcash_client_backend::data_api::testing::AddressType,
3946        zcash_keys::keys::sapling,
3947        zcash_protocol::{consensus::NetworkConstants, value::Zatoshis},
3948    };
3949    #[cfg(all(feature = "orchard", feature = "transparent-inputs"))]
3950    use {
3951        crate::{AccountRef, wallet::transparent},
3952        ::transparent::keys::{NonHardenedChildIndex, TransparentKeyScope},
3953        rusqlite::named_params,
3954    };
3955    #[cfg(feature = "transparent-inputs")]
3956    use {
3957        crate::{GapLimits, testing::BlockCache, wallet::transparent::transaction_data_requests},
3958        std::collections::BTreeSet,
3959        zcash_client_backend::data_api::TransactionDataRequest,
3960    };
3961
3962    #[test]
3963    fn get_wallet_recover_until_is_max_across_accounts() {
3964        let mut st = TestBuilder::new()
3965            .with_data_store_factory(TestDbFactory::default())
3966            .with_account_from_sapling_activation(BlockHash([0; 32]))
3967            .build();
3968        // The fixture account has no recovery horizon.
3969        assert_eq!(st.wallet().get_wallet_recover_until().unwrap(), None);
3970        // The result reflects the maximum recover_until height across accounts.
3971        st.wallet_mut()
3972            .conn_mut()
3973            .execute("UPDATE accounts SET recover_until_height = 123456", [])
3974            .unwrap();
3975        assert_eq!(
3976            st.wallet().get_wallet_recover_until().unwrap(),
3977            Some(zcash_protocol::consensus::BlockHeight::from_u32(123456))
3978        );
3979    }
3980
3981    #[test]
3982    fn get_subtree_root_round_trips_put_subtree_roots() {
3983        let mut st = TestBuilder::new()
3984            .with_data_store_factory(TestDbFactory::default())
3985            .build();
3986        let root = ::sapling::Node::empty_root(SAPLING_SHARD_HEIGHT.into());
3987        st.wallet_mut()
3988            .db_mut()
3989            .put_sapling_subtree_roots(
3990                0,
3991                &[CommitmentTreeRoot::from_parts(
3992                    zcash_protocol::consensus::BlockHeight::from_u32(500_000),
3993                    root,
3994                )],
3995            )
3996            .unwrap();
3997        assert_eq!(
3998            st.wallet_mut()
3999                .db_mut()
4000                .get_sapling_subtree_root(0)
4001                .unwrap(),
4002            Some(root)
4003        );
4004        assert_eq!(
4005            st.wallet_mut()
4006                .db_mut()
4007                .get_sapling_subtree_root(1)
4008                .unwrap(),
4009            None
4010        );
4011    }
4012
4013    /// Builds a test wallet with a single account and an application-owned extension
4014    /// table (`ext_test_notes`), simulating an external migration having created it.
4015    fn ext_test_state() -> TestState<(), TestDb, LocalNetwork> {
4016        let mut st = TestBuilder::new()
4017            .with_data_store_factory(TestDbFactory::default())
4018            .with_account_from_sapling_activation(BlockHash([0; 32]))
4019            .build();
4020        st.wallet_mut()
4021            .conn_mut()
4022            .execute_batch(
4023                "CREATE TABLE ext_test_notes (account_uuid BLOB NOT NULL, note TEXT NOT NULL);",
4024            )
4025            .unwrap();
4026        st
4027    }
4028
4029    /// Returns an owned seed and birthday suitable for creating a second account, released
4030    /// from any borrow of `st` so the wallet may be borrowed mutably afterwards.
4031    fn account_creation_inputs(
4032        st: &TestState<(), TestDb, LocalNetwork>,
4033    ) -> (SecretVec<u8>, AccountBirthday) {
4034        let birthday = st.test_account().unwrap().birthday().clone();
4035        let seed = SecretVec::new(st.test_seed().unwrap().expose_secret().to_vec());
4036        (seed, birthday)
4037    }
4038
4039    #[test]
4040    fn transactionally_with_extension_commits_both() {
4041        let mut st = ext_test_state();
4042        let (seed, birthday) = account_creation_inputs(&st);
4043
4044        let new_account = st
4045            .wallet_mut()
4046            .db_mut()
4047            .transactionally_with_extension::<_, _, SqliteClientError>(|wdb, ext| {
4048                let (account_id, _usk) = wdb.create_account("second", &seed, &birthday, None)?;
4049                ext.execute(
4050                    "INSERT INTO ext_test_notes (account_uuid, note) VALUES (?1, ?2)",
4051                    (account_id.expose_uuid(), "hello"),
4052                )?;
4053                Ok(account_id)
4054            })
4055            .unwrap();
4056
4057        // The wallet write persisted.
4058        let account_exists: bool = st
4059            .wallet()
4060            .conn()
4061            .query_row(
4062                "SELECT EXISTS(SELECT 1 FROM accounts WHERE uuid = ?1)",
4063                [new_account.expose_uuid()],
4064                |row| row.get(0),
4065            )
4066            .unwrap();
4067        assert!(account_exists);
4068
4069        // The extension write persisted, in the same transaction.
4070        let note: String = st
4071            .wallet()
4072            .conn()
4073            .query_row(
4074                "SELECT note FROM ext_test_notes WHERE account_uuid = ?1",
4075                [new_account.expose_uuid()],
4076                |row| row.get(0),
4077            )
4078            .unwrap();
4079        assert_eq!(note, "hello");
4080    }
4081
4082    #[test]
4083    fn transactionally_with_extension_rolls_back_on_error() {
4084        let mut st = ext_test_state();
4085        let (seed, birthday) = account_creation_inputs(&st);
4086
4087        let accounts_before: i64 = st
4088            .wallet()
4089            .conn()
4090            .query_row("SELECT COUNT(*) FROM accounts", [], |row| row.get(0))
4091            .unwrap();
4092
4093        let result: Result<(), SqliteClientError> = st
4094            .wallet_mut()
4095            .db_mut()
4096            .transactionally_with_extension(|wdb, ext| {
4097                let (account_id, _usk) = wdb.create_account("second", &seed, &birthday, None)?;
4098                ext.execute(
4099                    "INSERT INTO ext_test_notes (account_uuid, note) VALUES (?1, ?2)",
4100                    (account_id.expose_uuid(), "hello"),
4101                )?;
4102                // Fail after both writes; everything in this transaction must roll back.
4103                Err(SqliteClientError::AccountUnknown)
4104            });
4105        assert_matches!(result, Err(SqliteClientError::AccountUnknown));
4106
4107        // Neither the wallet write nor the extension write persisted.
4108        let accounts_after: i64 = st
4109            .wallet()
4110            .conn()
4111            .query_row("SELECT COUNT(*) FROM accounts", [], |row| row.get(0))
4112            .unwrap();
4113        assert_eq!(accounts_before, accounts_after);
4114        let ext_rows: i64 = st
4115            .wallet()
4116            .conn()
4117            .query_row("SELECT COUNT(*) FROM ext_test_notes", [], |row| row.get(0))
4118            .unwrap();
4119        assert_eq!(ext_rows, 0);
4120    }
4121
4122    #[test]
4123    fn transactionally_with_extension_denies_wallet_table_write() {
4124        let mut st = ext_test_state();
4125
4126        let accounts_before: i64 = st
4127            .wallet()
4128            .conn()
4129            .query_row("SELECT COUNT(*) FROM accounts", [], |row| row.get(0))
4130            .unwrap();
4131
4132        let result: Result<(), SqliteClientError> = st
4133            .wallet_mut()
4134            .db_mut()
4135            .transactionally_with_extension(|_wdb, ext| {
4136                // Deleting from a wallet-owned table is denied by the authorizer.
4137                ext.execute("DELETE FROM accounts", [])?;
4138                Ok(())
4139            });
4140        assert!(result.is_err());
4141
4142        // The wallet handle remains usable, and the denied statement had no effect: the
4143        // account is still present and a fresh wallet read succeeds.
4144        let accounts_after: i64 = st
4145            .wallet()
4146            .conn()
4147            .query_row("SELECT COUNT(*) FROM accounts", [], |row| row.get(0))
4148            .unwrap();
4149        assert_eq!(accounts_before, accounts_after);
4150        assert!(!st.wallet().get_account_ids().unwrap().is_empty());
4151    }
4152
4153    #[test]
4154    fn transactionally_with_extension_denies_transaction_control() {
4155        let mut st = ext_test_state();
4156
4157        let result: Result<(), SqliteClientError> = st
4158            .wallet_mut()
4159            .db_mut()
4160            .transactionally_with_extension(|_wdb, ext| {
4161                // Transaction-control statements are denied so extension SQL cannot break
4162                // out of the enclosing transaction.
4163                ext.execute("COMMIT", [])?;
4164                Ok(())
4165            });
4166        assert!(result.is_err());
4167
4168        // The wallet handle remains usable afterwards.
4169        assert!(!st.wallet().get_account_ids().unwrap().is_empty());
4170    }
4171
4172    #[test]
4173    fn validate_seed() {
4174        let st = TestBuilder::new()
4175            .with_data_store_factory(TestDbFactory::default())
4176            .with_account_from_sapling_activation(BlockHash([0; 32]))
4177            .build();
4178        let account = st.test_account().unwrap();
4179
4180        assert!({
4181            st.wallet()
4182                .validate_seed(account.id(), st.test_seed().unwrap())
4183                .unwrap()
4184        });
4185
4186        // check that passing an invalid account results in a failure
4187        assert!({
4188            let wrong_account_uuid = AccountUuid(Uuid::nil());
4189            !st.wallet()
4190                .validate_seed(wrong_account_uuid, st.test_seed().unwrap())
4191                .unwrap()
4192        });
4193
4194        // check that passing an invalid seed results in a failure
4195        assert!({
4196            !st.wallet()
4197                .validate_seed(account.id(), &SecretVec::new(vec![1u8; 32]))
4198                .unwrap()
4199        });
4200    }
4201
4202    #[test]
4203    pub(crate) fn get_next_available_address() {
4204        let mut st = TestBuilder::new()
4205            .with_data_store_factory(TestDbFactory::default())
4206            .with_account_from_sapling_activation(BlockHash([0; 32]))
4207            .build();
4208        let account = st.test_account().cloned().unwrap();
4209
4210        // We have to have the chain tip height in order to allocate new addresses, to record the
4211        // exposed-at height.
4212        st.wallet_mut()
4213            .update_chain_tip(account.birthday().height())
4214            .unwrap();
4215
4216        let current_addr = st
4217            .wallet()
4218            .get_last_generated_address_matching(
4219                account.id(),
4220                UnifiedAddressRequest::AllAvailableKeys,
4221            )
4222            .unwrap();
4223        assert!(current_addr.is_some());
4224
4225        let addr2 = st
4226            .wallet_mut()
4227            .get_next_available_address(account.id(), UnifiedAddressRequest::AllAvailableKeys)
4228            .unwrap()
4229            .map(|(a, _)| a);
4230        assert!(addr2.is_some());
4231        assert_ne!(current_addr, addr2);
4232
4233        let addr2_cur = st
4234            .wallet()
4235            .get_last_generated_address_matching(
4236                account.id(),
4237                UnifiedAddressRequest::AllAvailableKeys,
4238            )
4239            .unwrap();
4240        assert_eq!(addr2, addr2_cur);
4241
4242        // Perform similar tests for shielded-only addresses. These should be timestamp-based; we
4243        // will tick the clock between each generation.
4244        #[cfg(feature = "orchard")]
4245        let shielded_only_request = UnifiedAddressRequest::unsafe_custom(Require, Require, Omit);
4246        #[cfg(not(feature = "orchard"))]
4247        let shielded_only_request = UnifiedAddressRequest::unsafe_custom(Omit, Require, Omit);
4248
4249        let cur_shielded_only = st
4250            .wallet()
4251            .get_last_generated_address_matching(account.id(), shielded_only_request)
4252            .unwrap();
4253        // If transparent support is disabled, then the previous "transparent-including"
4254        // addresses were actually shielded-only, so we do have a current address.
4255        #[cfg(not(feature = "transparent-inputs"))]
4256        assert_eq!(cur_shielded_only, addr2);
4257        // If transparent support is enabled, this works as expected.
4258        #[cfg(feature = "transparent-inputs")]
4259        assert!(cur_shielded_only.is_none());
4260
4261        let di_lower = st
4262            .wallet()
4263            .db()
4264            .clock
4265            .now()
4266            .duration_since(SystemTime::UNIX_EPOCH)
4267            .expect("current time is valid")
4268            .as_secs()
4269            .saturating_add(MIN_SHIELDED_DIVERSIFIER_OFFSET);
4270
4271        let (shielded_only, di) = st
4272            .wallet_mut()
4273            .get_next_available_address(account.id(), shielded_only_request)
4274            .unwrap()
4275            .expect("generated a shielded-only address");
4276
4277        // since not every Sapling diversifier index is valid, the resulting index will be bounded
4278        // by the current time, but may not be equal to it
4279        assert!(u128::from(di) >= u128::from(di_lower));
4280
4281        let cur_shielded_only = st
4282            .wallet()
4283            .get_last_generated_address_matching(account.id(), shielded_only_request)
4284            .unwrap()
4285            .expect("retrieved the last-generated shielded-only address");
4286        assert_eq!(cur_shielded_only, shielded_only);
4287
4288        // This gives around a 2^{-32} probability of `di` and `di_2` colliding, which is
4289        // low enough for unit tests.
4290        let collision_offset = 32;
4291
4292        st.wallet_mut()
4293            .db_mut()
4294            .clock
4295            .tick(Duration::from_secs(collision_offset));
4296
4297        let (shielded_only_2, di_2) = st
4298            .wallet_mut()
4299            .get_next_available_address(account.id(), shielded_only_request)
4300            .unwrap()
4301            .expect("generated a shielded-only address");
4302        assert_ne!(shielded_only_2, shielded_only);
4303        assert!(u128::from(di_2) >= u128::from(di_lower) + u128::from(collision_offset));
4304    }
4305
4306    #[test]
4307    pub(crate) fn import_account_hd_0() {
4308        let st = TestBuilder::new()
4309            .with_data_store_factory(TestDbFactory::default())
4310            .with_account_from_sapling_activation(BlockHash([0; 32]))
4311            .set_account_index(zip32::AccountId::ZERO)
4312            .build();
4313        assert_matches!(
4314            st.test_account().unwrap().account().source(),
4315            AccountSource::Derived { derivation, .. } if derivation.account_index() == zip32::AccountId::ZERO);
4316    }
4317
4318    #[test]
4319    pub(crate) fn import_account_hd_1_then_2() {
4320        let mut st = TestBuilder::new()
4321            .with_data_store_factory(TestDbFactory::default())
4322            .build();
4323
4324        let birthday = AccountBirthday::from_parts(
4325            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
4326            None,
4327        );
4328
4329        let seed = Secret::new(vec![0u8; 32]);
4330        let zip32_index_1 = zip32::AccountId::ZERO.next().unwrap();
4331
4332        let first = st
4333            .wallet_mut()
4334            .import_account_hd("", &seed, zip32_index_1, &birthday, None)
4335            .unwrap();
4336        assert_matches!(
4337            first.0.source(),
4338            AccountSource::Derived { derivation, .. } if derivation.account_index() == zip32_index_1);
4339
4340        let zip32_index_2 = zip32_index_1.next().unwrap();
4341        let second = st
4342            .wallet_mut()
4343            .import_account_hd("", &seed, zip32_index_2, &birthday, None)
4344            .unwrap();
4345        assert_matches!(
4346            second.0.source(),
4347            AccountSource::Derived { derivation, .. } if derivation.account_index() == zip32_index_2);
4348    }
4349
4350    fn check_collisions<C, DbT: WalletTest + WalletWrite, P: consensus::Parameters>(
4351        st: &mut TestState<C, DbT, P>,
4352        ufvk: &UnifiedFullViewingKey,
4353        birthday: &AccountBirthday,
4354        is_account_collision: impl Fn(&<DbT as WalletRead>::Error) -> bool,
4355    ) where
4356        DbT::Account: core::fmt::Debug,
4357    {
4358        // Re-importing the same UFVK is a duplicate (no new capability added), so
4359        // it should produce an AccountCollision error.
4360        assert_matches!(
4361            st.wallet_mut()
4362                .import_account_ufvk("", ufvk, birthday, AccountPurpose::Spending { derivation: None }, None),
4363            Err(e) if is_account_collision(&e)
4364        );
4365
4366        // Importing a UFVK with fewer components than the existing account should fail:
4367        // the existing IVK items are not a subset of the new (smaller) FVK's items.
4368        #[cfg(feature = "transparent-inputs")]
4369        {
4370            assert!(ufvk.transparent().is_some());
4371            let subset_ufvk = UnifiedFullViewingKey::new(
4372                None,
4373                ufvk.sapling().cloned(),
4374                #[cfg(feature = "orchard")]
4375                ufvk.orchard().cloned(),
4376            )
4377            .unwrap();
4378            assert_matches!(
4379                st.wallet_mut().import_account_ufvk(
4380                    "",
4381                    &subset_ufvk,
4382                    birthday,
4383                    AccountPurpose::Spending { derivation: None },
4384                    None,
4385                ),
4386                Err(e) if is_account_collision(&e)
4387            );
4388        }
4389
4390        // Remove the Orchard component: still a collision since existing has Orchard.
4391        #[cfg(feature = "orchard")]
4392        {
4393            assert!(ufvk.orchard().is_some());
4394            let subset_ufvk = UnifiedFullViewingKey::new(
4395                #[cfg(feature = "transparent-inputs")]
4396                ufvk.transparent().cloned(),
4397                ufvk.sapling().cloned(),
4398                None,
4399            )
4400            .unwrap();
4401            assert_matches!(
4402                st.wallet_mut().import_account_ufvk(
4403                    "",
4404                    &subset_ufvk,
4405                    birthday,
4406                    AccountPurpose::Spending { derivation: None },
4407                    None,
4408                ),
4409                Err(e) if is_account_collision(&e)
4410            );
4411        }
4412    }
4413
4414    #[test]
4415    pub(crate) fn import_account_hd_1_then_conflicts() {
4416        let mut st = TestBuilder::new()
4417            .with_data_store_factory(TestDbFactory::default())
4418            .build();
4419
4420        let birthday = AccountBirthday::from_parts(
4421            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
4422            None,
4423        );
4424
4425        let seed = Secret::new(vec![0u8; 32]);
4426        let zip32_index_1 = zip32::AccountId::ZERO.next().unwrap();
4427
4428        let (first_account, _) = st
4429            .wallet_mut()
4430            .import_account_hd("", &seed, zip32_index_1, &birthday, None)
4431            .unwrap();
4432        let ufvk = first_account.ufvk().unwrap();
4433
4434        assert_matches!(
4435            st.wallet_mut().import_account_hd("", &seed, zip32_index_1, &birthday, None),
4436            Err(SqliteClientError::AccountCollision(id)) if id == first_account.id());
4437
4438        check_collisions(
4439            &mut st,
4440            ufvk,
4441            &birthday,
4442            |e| matches!(e, SqliteClientError::AccountCollision(id) if *id == first_account.id()),
4443        );
4444    }
4445
4446    #[test]
4447    pub(crate) fn import_account_ufvk_then_conflicts() {
4448        let mut st = TestBuilder::new()
4449            .with_data_store_factory(TestDbFactory::default())
4450            .build();
4451
4452        let birthday = AccountBirthday::from_parts(
4453            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
4454            None,
4455        );
4456
4457        let seed = Secret::new(vec![0u8; 32]);
4458        let zip32_index_0 = zip32::AccountId::ZERO;
4459        let usk = UnifiedSpendingKey::from_seed(st.network(), seed.expose_secret(), zip32_index_0)
4460            .unwrap();
4461        let ufvk = usk.to_unified_full_viewing_key();
4462
4463        let account = st
4464            .wallet_mut()
4465            .import_account_ufvk(
4466                "",
4467                &ufvk,
4468                &birthday,
4469                AccountPurpose::Spending { derivation: None },
4470                None,
4471            )
4472            .unwrap();
4473        assert_eq!(
4474            ufvk.encode(st.network()),
4475            account.ufvk().unwrap().encode(st.network())
4476        );
4477
4478        assert_matches!(
4479            account.source(),
4480            AccountSource::Imported {
4481                purpose: AccountPurpose::Spending { .. },
4482                ..
4483            }
4484        );
4485
4486        assert_matches!(
4487            st.wallet_mut().import_account_hd("", &seed, zip32_index_0, &birthday, None),
4488            Err(SqliteClientError::AccountCollision(id)) if id == account.id());
4489
4490        check_collisions(
4491            &mut st,
4492            &ufvk,
4493            &birthday,
4494            |e| matches!(e, SqliteClientError::AccountCollision(id) if *id == account.id()),
4495        );
4496    }
4497
4498    #[test]
4499    pub(crate) fn create_account_then_conflicts() {
4500        let mut st = TestBuilder::new()
4501            .with_data_store_factory(TestDbFactory::default())
4502            .build();
4503
4504        let birthday = AccountBirthday::from_parts(
4505            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
4506            None,
4507        );
4508
4509        let seed = Secret::new(vec![0u8; 32]);
4510        let zip32_index_0 = zip32::AccountId::ZERO;
4511        let seed_based = st
4512            .wallet_mut()
4513            .create_account("", &seed, &birthday, None)
4514            .unwrap();
4515        let seed_based_account = st.wallet().get_account(seed_based.0).unwrap().unwrap();
4516        let ufvk = seed_based_account.ufvk().unwrap();
4517
4518        assert_matches!(
4519            st.wallet_mut().import_account_hd("", &seed, zip32_index_0, &birthday, None),
4520            Err(SqliteClientError::AccountCollision(id)) if id == seed_based.0);
4521
4522        check_collisions(
4523            &mut st,
4524            ufvk,
4525            &birthday,
4526            |e| matches!(e, SqliteClientError::AccountCollision(id) if *id == seed_based.0),
4527        );
4528    }
4529
4530    #[test]
4531    pub(crate) fn ivk_only_account_upgrade_paths() {
4532        let mut st = TestBuilder::new()
4533            .with_data_store_factory(TestDbFactory::default())
4534            .build();
4535
4536        let birthday = AccountBirthday::from_parts(
4537            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
4538            None,
4539        );
4540
4541        let seed = vec![0u8; 32];
4542        let usk =
4543            UnifiedSpendingKey::from_seed(st.network(), &seed, zip32::AccountId::ZERO).unwrap();
4544        let ufvk = usk.to_unified_full_viewing_key();
4545        let full_uivk = ufvk.to_unified_incoming_viewing_key();
4546
4547        // Create a UIVK with only the Sapling component (a subset of the full UIVK).
4548        let sapling_only_uivk = UnifiedIncomingViewingKey::new(
4549            #[cfg(feature = "transparent-inputs")]
4550            None,
4551            full_uivk.sapling().clone(),
4552            #[cfg(feature = "orchard")]
4553            None,
4554        );
4555
4556        // Import the sapling-only IVK as an IVK-only account.
4557        let network = *st.network();
4558        let ivk_account = st
4559            .wallet_mut()
4560            .db_mut()
4561            .transactionally(|wdb| {
4562                crate::wallet::add_account(
4563                    wdb.conn.0,
4564                    &wdb.params,
4565                    "ivk-only",
4566                    &AccountSource::Imported {
4567                        purpose: AccountPurpose::ViewOnly,
4568                        key_source: None,
4569                    },
4570                    crate::wallet::ViewingKey::Incoming(Box::new(sapling_only_uivk.clone())),
4571                    &birthday,
4572                    #[cfg(feature = "transparent-inputs")]
4573                    &crate::GapLimits::default(),
4574                )
4575            })
4576            .unwrap();
4577
4578        // (a) Same IVK import should fail (duplicate, no new capability).
4579        assert_matches!(
4580            st.wallet_mut().db_mut().transactionally(|wdb| {
4581                crate::wallet::add_account(
4582                    wdb.conn.0,
4583                    &wdb.params,
4584                    "duplicate",
4585                    &AccountSource::Imported {
4586                        purpose: AccountPurpose::ViewOnly,
4587                        key_source: None,
4588                    },
4589                    crate::wallet::ViewingKey::Incoming(Box::new(sapling_only_uivk.clone())),
4590                    &birthday,
4591                    #[cfg(feature = "transparent-inputs")]
4592                    &crate::GapLimits::default(),
4593                )
4594            }),
4595            Err(SqliteClientError::AccountCollision(id)) if id == ivk_account.id()
4596        );
4597
4598        // (b) UFVK that subsumes the existing IVK should succeed as an upgrade.
4599        let ufvk_upgraded = st
4600            .wallet_mut()
4601            .import_account_ufvk(
4602                "",
4603                &ufvk,
4604                &birthday,
4605                AccountPurpose::Spending { derivation: None },
4606                None,
4607            )
4608            .unwrap();
4609        // Should return the same account, now with the UFVK.
4610        assert_eq!(ufvk_upgraded.id(), ivk_account.id());
4611        assert!(ufvk_upgraded.ufvk().is_some());
4612        assert_eq!(
4613            ufvk_upgraded.ufvk().unwrap().encode(&network),
4614            ufvk.encode(&network),
4615        );
4616
4617        // (c) IVK import over an account that now has a UFVK should fail.
4618        assert_matches!(
4619            st.wallet_mut().db_mut().transactionally(|wdb| {
4620                crate::wallet::add_account(
4621                    wdb.conn.0,
4622                    &wdb.params,
4623                    "downgrade",
4624                    &AccountSource::Imported {
4625                        purpose: AccountPurpose::ViewOnly,
4626                        key_source: None,
4627                    },
4628                    crate::wallet::ViewingKey::Incoming(Box::new(full_uivk)),
4629                    &birthday,
4630                    #[cfg(feature = "transparent-inputs")]
4631                    &crate::GapLimits::default(),
4632                )
4633            }),
4634            Err(SqliteClientError::AccountCollision(id)) if id == ivk_account.id()
4635        );
4636    }
4637
4638    /// Tests that importing a UIVK with additional items over an existing IVK-only
4639    /// account succeeds as an additive upgrade. Only meaningful when features provide
4640    /// more than one shielded pool (e.g. `orchard`).
4641    #[cfg(feature = "orchard")]
4642    #[test]
4643    pub(crate) fn ivk_over_ivk_additive_upgrade() {
4644        let mut st = TestBuilder::new()
4645            .with_data_store_factory(TestDbFactory::default())
4646            .build();
4647
4648        let birthday = AccountBirthday::from_parts(
4649            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
4650            None,
4651        );
4652
4653        let seed = vec![0u8; 32];
4654        let usk =
4655            UnifiedSpendingKey::from_seed(st.network(), &seed, zip32::AccountId::ZERO).unwrap();
4656        let ufvk = usk.to_unified_full_viewing_key();
4657        let full_uivk = ufvk.to_unified_incoming_viewing_key();
4658        let network = *st.network();
4659
4660        // Create a UIVK with only Sapling (a strict subset of the full UIVK).
4661        let sapling_only_uivk = UnifiedIncomingViewingKey::new(
4662            #[cfg(feature = "transparent-inputs")]
4663            None,
4664            full_uivk.sapling().clone(),
4665            None, // no Orchard
4666        );
4667
4668        // Import the sapling-only IVK.
4669        let ivk_account = st
4670            .wallet_mut()
4671            .db_mut()
4672            .transactionally(|wdb| {
4673                crate::wallet::add_account(
4674                    wdb.conn.0,
4675                    &wdb.params,
4676                    "sapling-only",
4677                    &AccountSource::Imported {
4678                        purpose: AccountPurpose::ViewOnly,
4679                        key_source: None,
4680                    },
4681                    crate::wallet::ViewingKey::Incoming(Box::new(sapling_only_uivk)),
4682                    &birthday,
4683                    #[cfg(feature = "transparent-inputs")]
4684                    &crate::GapLimits::default(),
4685                )
4686            })
4687            .unwrap();
4688
4689        // Import the full UIVK (sapling + orchard) — should upgrade.
4690        let upgraded = st
4691            .wallet_mut()
4692            .db_mut()
4693            .transactionally(|wdb| {
4694                crate::wallet::add_account(
4695                    wdb.conn.0,
4696                    &wdb.params,
4697                    "upgraded",
4698                    &AccountSource::Imported {
4699                        purpose: AccountPurpose::ViewOnly,
4700                        key_source: None,
4701                    },
4702                    crate::wallet::ViewingKey::Incoming(Box::new(full_uivk)),
4703                    &birthday,
4704                    #[cfg(feature = "transparent-inputs")]
4705                    &crate::GapLimits::default(),
4706                )
4707            })
4708            .unwrap();
4709
4710        assert_eq!(upgraded.id(), ivk_account.id());
4711        assert!(upgraded.ufvk().is_none());
4712        assert!(upgraded.uivk().encode(&network) != ivk_account.uivk().encode(&network));
4713    }
4714
4715    #[cfg(feature = "transparent-inputs")]
4716    #[test]
4717    fn transparent_receivers() {
4718        let mut st = TestBuilder::new()
4719            .with_data_store_factory(TestDbFactory::default())
4720            .with_block_cache(BlockCache::new())
4721            .with_account_from_sapling_activation(BlockHash([0; 32]))
4722            .build();
4723        let account = st.test_account().unwrap();
4724        let ufvk = account.usk().to_unified_full_viewing_key();
4725        let (taddr, _) = account.usk().default_transparent_address();
4726        let birthday = account.birthday().height();
4727        let account_id = account.id();
4728
4729        let receivers = st
4730            .wallet()
4731            .get_transparent_receivers(account.id(), false, true)
4732            .unwrap();
4733
4734        // The receiver for the default UA should be in the set.
4735        assert!(
4736            receivers.contains_key(
4737                ufvk.default_address(UnifiedAddressRequest::AllAvailableKeys)
4738                    .expect("A valid default address exists for the UFVK")
4739                    .0
4740                    .transparent()
4741                    .unwrap()
4742            )
4743        );
4744
4745        // The default t-addr should be in the set.
4746        assert!(receivers.contains_key(&taddr));
4747
4748        // The chain tip height must be known in order to query for data requests.
4749        st.wallet_mut().update_chain_tip(birthday).unwrap();
4750
4751        // Transaction data requests should include a request for each ephemeral address
4752        let ephemeral_addrs = st
4753            .wallet()
4754            .get_known_ephemeral_addresses(account_id, None)
4755            .unwrap();
4756
4757        assert_eq!(
4758            ephemeral_addrs.len(),
4759            GapLimits::default().ephemeral() as usize
4760        );
4761
4762        st.wallet_mut()
4763            .db_mut()
4764            .schedule_ephemeral_address_checks()
4765            .unwrap();
4766        let data_requests =
4767            transaction_data_requests(st.wallet().conn(), &st.wallet().db().params, birthday)
4768                .unwrap();
4769
4770        let base_time = st.wallet().db().clock.now();
4771        let day = Duration::from_secs(60 * 60 * 24);
4772        let mut check_times = BTreeSet::new();
4773        for (addr, _) in ephemeral_addrs {
4774            let has_valid_request = data_requests.iter().any(|req| match req {
4775                TransactionDataRequest::TransactionsInvolvingAddress(req) => {
4776                    if let Some(t) = req.request_at() {
4777                        req.address() == addr && t > base_time && {
4778                            let t_delta = t.duration_since(base_time).unwrap();
4779                            // This is an imprecise check; the objective of the randomized time
4780                            // selection is that all ephemeral address checks be performed within a
4781                            // day, and that their check times be distinct. We are bounding the
4782                            // overall delta to two days to limit the probability of test failure,
4783                            // since due to randomization of the check intervals it's possible that
4784                            // scheduled checks might exceed one day; if we find an example where
4785                            // checks exceed two days then we're in some long tail of the
4786                            // distribution.
4787                            let result = t_delta < 2 * day && !check_times.contains(&t);
4788                            check_times.insert(t);
4789                            result
4790                        }
4791                    } else {
4792                        false
4793                    }
4794                }
4795                _ => false,
4796            });
4797
4798            assert!(has_valid_request);
4799        }
4800    }
4801
4802    #[cfg(feature = "unstable")]
4803    #[test]
4804    pub(crate) fn fsblockdb_api() {
4805        let mut st = TestBuilder::new()
4806            .with_data_store_factory(TestDbFactory::default())
4807            .with_block_cache(FsBlockCache::new())
4808            .build();
4809
4810        // The BlockMeta DB starts off empty.
4811        assert_eq!(st.cache().get_max_cached_height().unwrap(), None);
4812
4813        // Generate some fake CompactBlocks.
4814        let seed = [0u8; 32];
4815        let hd_account_index = zip32::AccountId::ZERO;
4816        let extsk = sapling::spending_key(&seed, st.network().coin_type(), hd_account_index);
4817        let dfvk = extsk.to_diversifiable_full_viewing_key();
4818        let (h1, meta1, _) = st.generate_next_block(
4819            &dfvk,
4820            AddressType::DefaultExternal,
4821            Zatoshis::const_from_u64(5),
4822        );
4823        let (h2, meta2, _) = st.generate_next_block(
4824            &dfvk,
4825            AddressType::DefaultExternal,
4826            Zatoshis::const_from_u64(10),
4827        );
4828
4829        // The BlockMeta DB is not updated until we do so explicitly.
4830        assert_eq!(st.cache().get_max_cached_height().unwrap(), None);
4831
4832        // Inform the BlockMeta DB about the newly-persisted CompactBlocks.
4833        st.cache()
4834            .write_block_metadata(&[meta1.block_meta, meta2.block_meta])
4835            .unwrap();
4836
4837        // The BlockMeta DB now sees blocks up to height 2.
4838        assert_eq!(st.cache().get_max_cached_height().unwrap(), Some(h2),);
4839        assert_eq!(st.cache().find_block(h1).unwrap(), Some(meta1.block_meta));
4840        assert_eq!(st.cache().find_block(h2).unwrap(), Some(meta2.block_meta));
4841        assert_eq!(st.cache().find_block(h2 + 1).unwrap(), None);
4842
4843        // Rewinding to height 1 should cause the metadata for height 2 to be deleted.
4844        st.cache().truncate_to_height(h1).unwrap();
4845        assert_eq!(st.cache().get_max_cached_height().unwrap(), Some(h1));
4846        assert_eq!(st.cache().find_block(h1).unwrap(), Some(meta1.block_meta));
4847        assert_eq!(st.cache().find_block(h2).unwrap(), None);
4848        assert_eq!(st.cache().find_block(h2 + 1).unwrap(), None);
4849    }
4850
4851    #[test]
4852    fn find_account_for_address_returns_matching_account_for_own_ua() {
4853        // Create a test wallet with one account and expose one of its own UAs
4854        let mut state = create_test_wallet_with_one_account();
4855        let account = state.test_account().cloned().unwrap();
4856
4857        state
4858            .wallet_mut()
4859            .update_chain_tip(account.birthday().height())
4860            .unwrap();
4861
4862        let (ua, _) = generate_unified_address_with_all_available_keys(&mut state, account.id());
4863
4864        // Asserts that looking up the exact same UA returns the owning account
4865        let result = state
4866            .wallet()
4867            .find_account_for_address(state.network(), &Address::Unified(ua));
4868
4869        assert_eq!(result.unwrap(), Some(account.id()));
4870    }
4871
4872    #[test]
4873    fn find_account_for_address_returns_none_for_unknown_address() {
4874        // Create a test wallet with one account
4875        let st = create_test_wallet_with_one_account();
4876
4877        // Build a transparent address that is not present in the wallet DB
4878        let unknown_address = Address::Transparent(
4879            ::transparent::address::TransparentAddress::PublicKeyHash([0u8; 20]),
4880        );
4881
4882        // Asserts that an unrelated address does not resolve to any account
4883        assert_eq!(
4884            st.wallet()
4885                .find_account_for_address(st.network(), &unknown_address)
4886                .unwrap(),
4887            None
4888        );
4889    }
4890
4891    #[test]
4892    fn find_account_for_address_returns_matching_account_for_receivers_of_own_ua() {
4893        // Create a test wallet with one account and expose one of its own UAs
4894        let mut state = create_test_wallet_with_one_account();
4895        let account = state.test_account().cloned().unwrap();
4896        state
4897            .wallet_mut()
4898            .update_chain_tip(account.birthday().height())
4899            .unwrap();
4900        let (ua, _) = generate_unified_address_with_all_available_keys(&mut state, account.id());
4901        // Asserts that looking up a receiver address extracted from the stored UA
4902        // returns the owning account via the non-UA query path.
4903        if let Some(taddr) = ua.transparent() {
4904            let result = state
4905                .wallet()
4906                .find_account_for_address(state.network(), &Address::Transparent(*taddr));
4907            assert_eq!(result.unwrap(), Some(account.id()));
4908        }
4909        if let Some(pa) = ua.sapling() {
4910            let result = state
4911                .wallet()
4912                .find_account_for_address(state.network(), &Address::Sapling(*pa));
4913            assert_eq!(result.unwrap(), Some(account.id()));
4914        }
4915    }
4916
4917    #[cfg(all(feature = "orchard", feature = "transparent-inputs"))]
4918    #[test]
4919    fn find_account_for_ua_finds_via_transparent_receiver_cache() {
4920        // Create a test wallet with one account
4921        let mut state = create_test_wallet_with_one_account();
4922        let account = state.test_account().cloned().unwrap();
4923        let acc1_id = account.id();
4924
4925        let account_rowid = delete_account_addresses(&mut state, acc1_id);
4926
4927        // Inserts in the DB one row representing a transparent address of that account
4928        let transparent_address =
4929            UnifiedSpendingKey::from_seed(&state.network(), &[7u8; 32], zip32::AccountId::ZERO)
4930                .expect("valid seed")
4931                .to_unified_full_viewing_key()
4932                .default_address(UnifiedAddressRequest::unsafe_custom(Omit, Require, Require))
4933                .unwrap()
4934                .0
4935                .transparent()
4936                .cloned()
4937                .expect("UA must have transparent receiver");
4938
4939        state
4940            .wallet_mut()
4941            .update_chain_tip(account.birthday().height())
4942            .unwrap();
4943
4944        state
4945            .wallet_mut()
4946            .db_mut()
4947            .transactionally(|wdb| {
4948                transparent::store_address_range(
4949                    wdb.conn.0,
4950                    wdb.params(),
4951                    AccountRef(account_rowid),
4952                    TransparentKeyScope::EXTERNAL,
4953                    vec![(
4954                        Address::Transparent(transparent_address),
4955                        transparent_address,
4956                        NonHardenedChildIndex::ZERO,
4957                    )],
4958                )?;
4959                transparent::reserve_next_n_addresses(
4960                    wdb.conn.0,
4961                    wdb.params(),
4962                    AccountRef(account_rowid),
4963                    TransparentKeyScope::EXTERNAL,
4964                    20,
4965                    1,
4966                )?;
4967                Ok::<_, SqliteClientError>(())
4968            })
4969            .unwrap();
4970
4971        // Builds a new UA that shares the transparent receiver, but also has an
4972        // Orchard receiver coming from a different seed
4973        let usk_external =
4974            UnifiedSpendingKey::from_seed(&state.network(), &[99u8; 32], zip32::AccountId::ZERO)
4975                .expect("valid seed");
4976
4977        let o_external = usk_external
4978            .to_unified_full_viewing_key()
4979            .default_address(UnifiedAddressRequest::AllAvailableKeys)
4980            .expect("default address must be derivable")
4981            .0
4982            .orchard()
4983            .cloned()
4984            .expect("orchard receiver must be present");
4985        let address = Address::Unified(
4986            UnifiedAddress::from_receivers(Some(o_external), None, Some(transparent_address))
4987                .expect("orchard+transparent UA must be valid"),
4988        );
4989
4990        // Asserts that the unique possible account is found anyways, based on the transparent address,
4991        // since there are no UA conflicts.
4992        let result = state
4993            .wallet()
4994            .find_account_for_address(state.network(), &address);
4995        assert_eq!(result.unwrap(), Some(acc1_id));
4996    }
4997
4998    #[test]
4999    fn find_account_for_ua_finds_via_sapling() {
5000        // Create a test wallet with one account
5001        let mut state = create_test_wallet_with_one_account();
5002
5003        let birthday = AccountBirthday::from_parts(
5004            ChainState::empty(state.network().sapling.unwrap() - 1, BlockHash([0; 32])),
5005            None,
5006        );
5007        let sapling_activation = state.network().sapling.unwrap();
5008
5009        let (acc1_id, _) = state
5010            .wallet_mut()
5011            .create_account("", &Secret::new(vec![0u8; 32]), &birthday, None)
5012            .unwrap();
5013
5014        state
5015            .wallet_mut()
5016            .update_chain_tip(sapling_activation)
5017            .unwrap();
5018
5019        // Expose a normal UA for that account and keep only its Sapling receiver
5020        let (ua1, _) = generate_unified_address_with_all_available_keys(&mut state, acc1_id);
5021
5022        let sapling_receiver = ua1
5023            .sapling()
5024            .cloned()
5025            .expect("UA must have sapling receiver");
5026
5027        let address = Address::Unified(
5028            {
5029                #[cfg(feature = "orchard")]
5030                {
5031                    UnifiedAddress::from_receivers(None, Some(sapling_receiver), None)
5032                }
5033
5034                #[cfg(not(feature = "orchard"))]
5035                {
5036                    UnifiedAddress::from_receivers(Some(sapling_receiver), None)
5037                }
5038            }
5039            .expect("sapling-only UA must be valid"),
5040        );
5041
5042        // Asserts that the account is still found via the shielded receiver flags path
5043        let result = state
5044            .wallet()
5045            .find_account_for_address(state.network(), &address);
5046
5047        assert_eq!(result.unwrap(), Some(acc1_id));
5048    }
5049
5050    #[cfg(feature = "orchard")]
5051    #[test]
5052    fn find_account_for_ua_finds_via_orchard() {
5053        // Create a test wallet with one account
5054        let mut state = create_test_wallet_with_one_account();
5055
5056        let birthday = AccountBirthday::from_parts(
5057            ChainState::empty(state.network().sapling.unwrap() - 1, BlockHash([0; 32])),
5058            None,
5059        );
5060        let sapling_activation = state.network().sapling.unwrap();
5061
5062        let (acc1_id, _) = state
5063            .wallet_mut()
5064            .create_account("", &Secret::new(vec![0u8; 32]), &birthday, None)
5065            .unwrap();
5066
5067        state
5068            .wallet_mut()
5069            .update_chain_tip(sapling_activation)
5070            .unwrap();
5071
5072        // Expose a normal UA for that account and keep only its Orchard receiver
5073        let (ua1, _) = generate_unified_address_with_all_available_keys(&mut state, acc1_id);
5074
5075        let orchard_receiver = ua1
5076            .orchard()
5077            .cloned()
5078            .expect("UA must have orchard receiver");
5079
5080        let address = Address::Unified(
5081            UnifiedAddress::from_receivers(Some(orchard_receiver), None, None)
5082                .expect("orchard-only UA must be valid"),
5083        );
5084
5085        // Asserts that the account is still found via the shielded receiver flags path
5086        let result = state
5087            .wallet()
5088            .find_account_for_address(state.network(), &address);
5089
5090        assert_eq!(result.unwrap(), Some(acc1_id));
5091    }
5092
5093    /// A UA whose Sapling receiver belongs to account 1 and whose Orchard receiver
5094    /// belongs to account 2 must produce a `UnifiedAddressConflict` error.
5095    #[cfg(feature = "orchard")]
5096    #[test]
5097    fn find_account_for_ua_errors_when_receivers_map_to_different_accounts() {
5098        // Create a test wallet with two different accounts
5099        let mut state = create_test_wallet_with_one_account();
5100
5101        let birthday = AccountBirthday::from_parts(
5102            ChainState::empty(state.network().sapling.unwrap() - 1, BlockHash([0; 32])),
5103            None,
5104        );
5105        let sapling_activation = state.network().sapling.unwrap();
5106
5107        let seed1 = Secret::new(vec![0u8; 32]);
5108        let seed2 = Secret::new(vec![1u8; 32]);
5109
5110        let (acc1_id, _) = state
5111            .wallet_mut()
5112            .create_account("", &seed1, &birthday, None)
5113            .unwrap();
5114        let (acc2_id, _) = state
5115            .wallet_mut()
5116            .create_account("", &seed2, &birthday, None)
5117            .unwrap();
5118
5119        state
5120            .wallet_mut()
5121            .update_chain_tip(sapling_activation)
5122            .unwrap();
5123
5124        let (ua1, _) = generate_unified_address_with_all_available_keys(&mut state, acc1_id);
5125        let (ua2, _) = generate_unified_address_with_all_available_keys(&mut state, acc2_id);
5126
5127        // Build a synthetic UA that mixes receivers from two different accounts
5128        let sapling_receiver_1 = ua1.sapling().cloned().unwrap();
5129        let orchard_receiver_2 = ua2.orchard().cloned().unwrap();
5130
5131        let invalid_address = Address::Unified(
5132            UnifiedAddress::from_receivers(
5133                Some(orchard_receiver_2),
5134                Some(sapling_receiver_1),
5135                None,
5136            )
5137            .expect("sapling+orchard UA must be valid"),
5138        );
5139
5140        // Asserts that the lookup reports a conflict instead of arbitrarily choosing one account
5141        let result = state
5142            .wallet()
5143            .find_account_for_address(state.network(), &invalid_address);
5144        assert!(matches!(
5145            result,
5146            Err(FindAccountForAddressError::UnifiedAddressConflict)
5147        ));
5148    }
5149
5150    fn create_test_wallet_with_one_account() -> TestState<(), TestDb, LocalNetwork> {
5151        TestBuilder::new()
5152            .with_data_store_factory(TestDbFactory::default())
5153            .with_account_from_sapling_activation(BlockHash([0; 32]))
5154            .build()
5155    }
5156
5157    fn generate_unified_address_with_all_available_keys(
5158        state: &mut TestState<(), TestDb, LocalNetwork>,
5159        account_id: AccountUuid,
5160    ) -> (UnifiedAddress, DiversifierIndex) {
5161        state
5162            .wallet_mut()
5163            .get_next_available_address(account_id, UnifiedAddressRequest::AllAvailableKeys)
5164            .unwrap()
5165            .expect("address generation for account 1 must succeed")
5166    }
5167
5168    #[cfg(all(feature = "orchard", feature = "transparent-inputs"))]
5169    fn delete_account_addresses(
5170        state: &mut TestState<(), TestDb, LocalNetwork>,
5171        account_id: AccountUuid,
5172    ) -> i64 {
5173        // Remove from the DB all the addresses associated to the account
5174        let account_rowid: i64 = state
5175            .wallet()
5176            .conn()
5177            .query_row(
5178                "SELECT id FROM accounts WHERE uuid = :uuid",
5179                named_params![":uuid": account_id.expose_uuid()],
5180                |row| row.get(0),
5181            )
5182            .unwrap();
5183
5184        state
5185            .wallet()
5186            .conn()
5187            .execute(
5188                "DELETE FROM addresses WHERE account_id = :account_id",
5189                named_params![":account_id": account_rowid],
5190            )
5191            .unwrap();
5192        account_rowid
5193    }
5194}