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