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