1#![doc = document_features::document_features!()]
23#![cfg_attr(docsrs, feature(doc_cfg))]
31#![cfg_attr(docsrs, doc(auto_cfg))]
32#![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#[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
172pub(crate) const PRUNING_DEPTH: u32 = 100;
176
177pub(crate) const VERIFY_LOOKAHEAD: u32 = 10;
179
180pub(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#[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 pub fn from_uuid(value: Uuid) -> Self {
232 AccountUuid(value)
233 }
234
235 pub fn expose_uuid(&self) -> Uuid {
237 self.0
238 }
239}
240
241#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Hash)]
246pub struct AccountRef(i64);
247
248#[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#[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#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
272pub struct UtxoId(pub(crate) i64);
273
274#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
276pub struct TxRef(pub(crate) i64);
277
278#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
280struct AddressRef(pub(crate) i64);
281
282pub 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
295pub 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
310const EXTENSION_SCHEMA_PREFIX: &str = "ext_";
317
318pub struct ExtensionTransaction<'conn> {
351 conn: &'conn rusqlite::Connection,
352}
353
354struct 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
368fn 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 AuthAction::Select
383 | AuthAction::Read { .. }
384 | AuthAction::Function { .. }
385 | AuthAction::Recursive => Authorization::Allow,
386 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 _ => Authorization::Deny,
393 }
394}
395
396impl<'conn> ExtensionTransaction<'conn> {
397 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 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 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 pub fn params(&self) -> &P {
444 &self.params
445 }
446}
447
448impl<P, CL, R> WalletDb<rusqlite::Connection, P, CL, R> {
449 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 pub fn with_anchor_retention_interval(mut self, interval: AnchorRetentionInterval) -> Self {
497 self.set_anchor_retention_interval(interval);
498 self
499 }
500
501 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 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 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 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 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 let ext = ExtensionTransaction { conn: &tx };
630 let result = f(&mut wdb, &ext)?;
631 tx.commit()?;
632 Ok(result)
633 }
634
635 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 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 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 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 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 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 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 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 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 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 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 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
1966impl<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 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 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 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#[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#[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 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 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 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 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
3542pub struct BlockDb(rusqlite::Connection);
3544
3545impl BlockDb {
3546 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#[cfg(feature = "unstable")]
3612pub struct FsBlockDb {
3613 conn: rusqlite::Connection,
3614 blocks_dir: PathBuf,
3615}
3616
3617#[derive(Debug)]
3620#[cfg(feature = "unstable")]
3621#[non_exhaustive]
3622pub enum FsBlockDbError {
3623 Fs(io::Error),
3625 Db(rusqlite::Error),
3627 Protobuf(prost::DecodeError),
3629 MissingBlockPath(PathBuf),
3631 InvalidBlockstoreRoot(PathBuf),
3633 InvalidBlockPath(PathBuf),
3635 CorruptedData(String),
3637 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 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 pub fn get_max_cached_height(&self) -> Result<Option<BlockHeight>, FsBlockDbError> {
3694 Ok(chain::blockmetadb_get_max_cached_height(&self.conn)?)
3695 }
3696
3697 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 pub fn find_block(&self, height: BlockHeight) -> Result<Option<BlockMeta>, FsBlockDbError> {
3726 Ok(chain::blockmetadb_find_block(&self.conn, height)?)
3727 }
3728
3729 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 assert_eq!(st.wallet().get_wallet_recover_until().unwrap(), None);
3878 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 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 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 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 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 Err(SqliteClientError::AccountUnknown)
4012 });
4013 assert_matches!(result, Err(SqliteClientError::AccountUnknown));
4014
4015 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 ext.execute("DELETE FROM accounts", [])?;
4046 Ok(())
4047 });
4048 assert!(result.is_err());
4049
4050 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 ext.execute("COMMIT", [])?;
4072 Ok(())
4073 });
4074 assert!(result.is_err());
4075
4076 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 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 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 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 #[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 #[cfg(not(feature = "transparent-inputs"))]
4164 assert_eq!(cur_shielded_only, addr2);
4165 #[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 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 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 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 #[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 #[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 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 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 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 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 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 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 #[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 let sapling_only_uivk = UnifiedIncomingViewingKey::new(
4570 #[cfg(feature = "transparent-inputs")]
4571 None,
4572 full_uivk.sapling().clone(),
4573 None, );
4575
4576 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 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 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 assert!(receivers.contains_key(&taddr));
4655
4656 st.wallet_mut().update_chain_tip(birthday).unwrap();
4658
4659 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 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 assert_eq!(st.cache().get_max_cached_height().unwrap(), None);
4720
4721 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 assert_eq!(st.cache().get_max_cached_height().unwrap(), None);
4739
4740 st.cache()
4742 .write_block_metadata(&[meta1.block_meta, meta2.block_meta])
4743 .unwrap();
4744
4745 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 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 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 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 let st = create_test_wallet_with_one_account();
4784
4785 let unknown_address = Address::Transparent(
4787 ::transparent::address::TransparentAddress::PublicKeyHash([0u8; 20]),
4788 );
4789
4790 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 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 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 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 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 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 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 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 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 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 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 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 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 #[cfg(feature = "orchard")]
5004 #[test]
5005 fn find_account_for_ua_errors_when_receivers_map_to_different_accounts() {
5006 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 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 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 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}