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