1use std::{borrow::BorrowMut, fmt, rc::Rc};
4
5use rand_core::RngCore;
6use regex::Regex;
7use schemerz::{Migrator, MigratorError};
8use schemerz_rusqlite::{RusqliteAdapter, RusqliteMigration};
9use secrecy::SecretVec;
10use shardtree::error::ShardTreeError;
11use uuid::Uuid;
12
13use zcash_client_backend::data_api::{SeedRelevance, WalletRead};
14use zcash_keys::keys::AddressGenerationError;
15use zcash_protocol::{consensus, value::BalanceError};
16
17use self::migrations::verify_network_compatibility;
18
19use super::commitment_tree;
20use crate::{WalletDb, error::SqliteClientError, util::Clock};
21
22pub mod migrations;
23
24const SQLITE_MAJOR_VERSION: u32 = 3;
25const MIN_SQLITE_MINOR_VERSION: u32 = 35;
26
27const MIGRATIONS_TABLE: &str = "schemer_migrations";
28
29#[derive(Debug)]
31#[non_exhaustive]
32pub enum WalletMigrationError {
33 DatabaseNotSupported(String),
36
37 SeedRequired,
39
40 SeedNotRelevant,
50
51 CorruptedData(String),
53
54 AddressGeneration(AddressGenerationError),
56
57 DbError(rusqlite::Error),
59
60 BalanceError(BalanceError),
62
63 CommitmentTree(Box<ShardTreeError<commitment_tree::Error>>),
65
66 CannotRevert(Uuid),
68
69 Other(Box<SqliteClientError>),
71}
72
73impl From<rusqlite::Error> for WalletMigrationError {
74 fn from(e: rusqlite::Error) -> Self {
75 WalletMigrationError::DbError(e)
76 }
77}
78
79impl From<BalanceError> for WalletMigrationError {
80 fn from(e: BalanceError) -> Self {
81 WalletMigrationError::BalanceError(e)
82 }
83}
84
85impl From<ShardTreeError<commitment_tree::Error>> for WalletMigrationError {
86 fn from(e: ShardTreeError<commitment_tree::Error>) -> Self {
87 WalletMigrationError::CommitmentTree(Box::new(e))
88 }
89}
90
91impl From<AddressGenerationError> for WalletMigrationError {
92 fn from(e: AddressGenerationError) -> Self {
93 WalletMigrationError::AddressGeneration(e)
94 }
95}
96
97impl From<SqliteClientError> for WalletMigrationError {
98 fn from(value: SqliteClientError) -> Self {
99 match value {
100 SqliteClientError::CorruptedData(err) => WalletMigrationError::CorruptedData(err),
101 SqliteClientError::DbError(err) => WalletMigrationError::DbError(err),
102 SqliteClientError::CommitmentTree(err) => {
103 WalletMigrationError::CommitmentTree(Box::new(err))
104 }
105 SqliteClientError::BalanceError(err) => WalletMigrationError::BalanceError(err),
106 SqliteClientError::AddressGeneration(err) => {
107 WalletMigrationError::AddressGeneration(err)
108 }
109 other => WalletMigrationError::Other(Box::new(other)),
110 }
111 }
112}
113
114impl fmt::Display for WalletMigrationError {
115 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
116 match &self {
117 WalletMigrationError::DatabaseNotSupported(version) => {
118 write!(
119 f,
120 "The installed SQLite version {version} does not support operations required by the wallet."
121 )
122 }
123 WalletMigrationError::SeedRequired => {
124 write!(
125 f,
126 "The wallet seed is required in order to update the database."
127 )
128 }
129 WalletMigrationError::SeedNotRelevant => {
130 write!(
131 f,
132 "The provided seed is not relevant to any derived accounts in the database."
133 )
134 }
135 WalletMigrationError::CorruptedData(reason) => {
136 write!(f, "Wallet database is corrupted: {reason}")
137 }
138 WalletMigrationError::DbError(e) => write!(f, "{e}"),
139 WalletMigrationError::BalanceError(e) => write!(f, "Balance error: {e:?}"),
140 WalletMigrationError::CommitmentTree(e) => write!(f, "Commitment tree error: {e:?}"),
141 WalletMigrationError::AddressGeneration(e) => {
142 write!(f, "Address generation error: {e:?}")
143 }
144 WalletMigrationError::CannotRevert(uuid) => {
145 write!(f, "Reverting migration {uuid} is not supported")
146 }
147 WalletMigrationError::Other(err) => {
148 write!(f, "Unexpected violation of database business rules: {err}")
149 }
150 }
151 }
152}
153
154impl std::error::Error for WalletMigrationError {
155 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
156 match &self {
157 WalletMigrationError::DbError(e) => Some(e),
158 WalletMigrationError::BalanceError(e) => Some(e),
159 WalletMigrationError::CommitmentTree(e) => Some(e),
160 WalletMigrationError::AddressGeneration(e) => Some(e),
161 WalletMigrationError::Other(e) => Some(e),
162 _ => None,
163 }
164 }
165}
166
167fn sqlite_client_error_to_wallet_migration_error(e: SqliteClientError) -> WalletMigrationError {
173 match e {
174 SqliteClientError::CorruptedData(e) => WalletMigrationError::CorruptedData(e),
175 SqliteClientError::Protobuf(e) => WalletMigrationError::CorruptedData(e.to_string()),
176 SqliteClientError::InvalidNote => {
177 WalletMigrationError::CorruptedData("invalid note".into())
178 }
179 SqliteClientError::DecodingError(e) => WalletMigrationError::CorruptedData(e.to_string()),
180 #[cfg(feature = "transparent-inputs")]
181 SqliteClientError::TransparentDerivation(e) => {
182 WalletMigrationError::CorruptedData(e.to_string())
183 }
184 #[cfg(feature = "transparent-inputs")]
185 SqliteClientError::TransparentAddress(e) => {
186 WalletMigrationError::CorruptedData(e.to_string())
187 }
188 SqliteClientError::DbError(e) => WalletMigrationError::DbError(e),
189 SqliteClientError::Io(e) => WalletMigrationError::CorruptedData(e.to_string()),
190 SqliteClientError::InvalidMemo(e) => WalletMigrationError::CorruptedData(e.to_string()),
191 SqliteClientError::AddressGeneration(e) => WalletMigrationError::AddressGeneration(e),
192 SqliteClientError::BadAccountData(e) => WalletMigrationError::CorruptedData(e),
193 SqliteClientError::CommitmentTree(e) => WalletMigrationError::CommitmentTree(Box::new(e)),
194 SqliteClientError::UnsupportedPoolType(pool) => WalletMigrationError::CorruptedData(
195 format!("Wallet DB contains unsupported pool type {pool}"),
196 ),
197 SqliteClientError::BalanceError(e) => WalletMigrationError::BalanceError(e),
198 SqliteClientError::TableNotEmpty => unreachable!("wallet already initialized"),
199 SqliteClientError::BlockConflict(_)
200 | SqliteClientError::NonSequentialBlocks
201 | SqliteClientError::PutBlocksCommitmentTree { .. }
202 | SqliteClientError::TruncateCommitmentTree { .. }
203 | SqliteClientError::RequestedRewindInvalid { .. }
204 | SqliteClientError::KeyDerivationError(_)
205 | SqliteClientError::Zip32AccountIndexOutOfRange
206 | SqliteClientError::AccountCollision(_)
207 | SqliteClientError::CacheMiss(_)
208 | SqliteClientError::BackendError(_) => {
209 unreachable!("we only call WalletRead methods; mutations can't occur")
210 }
211 #[cfg(feature = "transparent-inputs")]
212 SqliteClientError::AddressNotRecognized(_) => {
213 unreachable!("we only call WalletRead methods; mutations can't occur")
214 }
215 SqliteClientError::AccountUnknown => {
216 unreachable!("all accounts are known in migration context")
217 }
218 SqliteClientError::UnknownZip32Derivation => {
219 unreachable!("we don't call methods that require operating on imported accounts")
220 }
221 SqliteClientError::ChainHeightUnknown => {
222 unreachable!("we don't call methods that require a known chain height")
223 }
224 #[cfg(feature = "transparent-inputs")]
225 SqliteClientError::ReachedGapLimit(..) => {
226 unreachable!("we don't do ephemeral address tracking")
227 }
228 SqliteClientError::DiversifierIndexReuse(i, _) => {
229 WalletMigrationError::CorruptedData(format!(
230 "invalid attempt to overwrite address at diversifier index {}",
231 u128::from(i)
232 ))
233 }
234 SqliteClientError::IneligibleNotes => {
235 unreachable!("there are no ineligible notes in migrations")
236 }
237 SqliteClientError::AddressReuse(_, _) => {
238 unreachable!("we don't create transactions in migrations")
239 }
240 SqliteClientError::NoteFilterInvalid(_) => {
241 unreachable!("we don't do note selection in migrations")
242 }
243 #[cfg(feature = "transparent-inputs")]
244 SqliteClientError::Scheduling(e) => {
245 WalletMigrationError::Other(Box::new(SqliteClientError::Scheduling(e)))
246 }
247 #[cfg(feature = "transparent-inputs")]
248 SqliteClientError::NotificationMismatch { .. } => {
249 unreachable!("we don't service transaction data requests in migrations")
250 }
251 #[cfg(feature = "transparent-key-import")]
252 SqliteClientError::StandaloneImportConflict(_) => {
253 unreachable!("we do not import standalone transparent addresses in migrations")
254 }
255 #[cfg(feature = "orchard")]
256 SqliteClientError::HistoricalFrontierInvalid(_)
257 | SqliteClientError::HistoricalWitnessUnavailable { .. } => {
258 unreachable!("we do not generate historical witnesses in migrations")
259 }
260 #[cfg(feature = "transparent-inputs")]
261 SqliteClientError::FeeRuleError(_) => {
262 unreachable!("we don't use fee rules in migrations")
263 }
264 }
265}
266
267pub fn init_wallet_db<
346 C: BorrowMut<rusqlite::Connection>,
347 P: consensus::Parameters + 'static,
348 CL: Clock + Clone + 'static,
349 R: RngCore + Clone + 'static,
350>(
351 wdb: &mut WalletDb<C, P, CL, R>,
352 seed: Option<SecretVec<u8>>,
353) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
354 if let Some(seed) = seed {
355 WalletMigrator::new().with_seed(seed)
356 } else {
357 WalletMigrator::new()
358 }
359 .init_or_migrate(wdb)
360}
361
362pub struct WalletMigrator {
438 seed: Option<SecretVec<u8>>,
439 verify_seed_relevance: bool,
440 external_migrations: Option<Vec<Box<dyn RusqliteMigration<Error = WalletMigrationError>>>>,
441}
442
443impl Default for WalletMigrator {
444 fn default() -> Self {
445 Self::new()
446 }
447}
448
449impl WalletMigrator {
450 pub fn new() -> Self {
452 Self {
453 seed: None,
454 verify_seed_relevance: true,
455 external_migrations: None,
456 }
457 }
458
459 pub fn with_seed(mut self, seed: SecretVec<u8>) -> Self {
461 self.seed = Some(seed);
462 self
463 }
464
465 #[cfg(test)]
467 pub(crate) fn ignore_seed_relevance(mut self) -> Self {
468 self.verify_seed_relevance = false;
469 self
470 }
471
472 pub fn with_external_migrations(
531 mut self,
532 migrations: Vec<Box<dyn RusqliteMigration<Error = WalletMigrationError>>>,
533 ) -> Self {
534 self.external_migrations = Some(migrations);
535 self
536 }
537
538 pub fn init_or_migrate<
541 C: BorrowMut<rusqlite::Connection>,
542 P: consensus::Parameters + 'static,
543 CL: Clock + Clone + 'static,
544 R: RngCore + Clone + 'static,
545 >(
546 self,
547 wdb: &mut WalletDb<C, P, CL, R>,
548 ) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
549 self.init_or_migrate_to(wdb, &[])
550 }
551
552 pub(crate) fn init_or_migrate_to<
555 C: BorrowMut<rusqlite::Connection>,
556 P: consensus::Parameters + 'static,
557 CL: Clock + Clone + 'static,
558 R: RngCore + Clone + 'static,
559 >(
560 self,
561 wdb: &mut WalletDb<C, P, CL, R>,
562 target_migrations: &[Uuid],
563 ) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
564 init_wallet_db_internal(
565 wdb,
566 self.seed,
567 self.external_migrations,
568 target_migrations,
569 self.verify_seed_relevance,
570 )
571 }
572}
573
574fn init_wallet_db_internal<
575 C: BorrowMut<rusqlite::Connection>,
576 P: consensus::Parameters + 'static,
577 CL: Clock + Clone + 'static,
578 R: RngCore + Clone + 'static,
579>(
580 wdb: &mut WalletDb<C, P, CL, R>,
581 seed: Option<SecretVec<u8>>,
582 external_migrations: Option<Vec<Box<dyn RusqliteMigration<Error = WalletMigrationError>>>>,
583 target_migrations: &[Uuid],
584 verify_seed_relevance: bool,
585) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
586 let seed = seed.map(Rc::new);
587
588 verify_sqlite_version_compatibility(wdb.conn.borrow()).map_err(MigratorError::Adapter)?;
589
590 wdb.conn
596 .borrow()
597 .execute_batch("PRAGMA foreign_keys = OFF;")
598 .map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?;
599
600 {
606 let adapter = RusqliteAdapter::<'_, WalletMigrationError>::new(
607 wdb.conn.borrow_mut(),
608 Some(MIGRATIONS_TABLE.to_string()),
609 );
610 adapter.init().expect("Migrations table setup succeeds.");
611 }
612
613 verify_network_compatibility(wdb.conn.borrow(), &wdb.params).map_err(MigratorError::Adapter)?;
617
618 let adapter = RusqliteAdapter::new(wdb.conn.borrow_mut(), Some(MIGRATIONS_TABLE.to_string()));
621 let mut migrator = Migrator::new(adapter);
622 migrator
623 .register_multiple(
624 migrations::all_migrations(
625 &wdb.params,
626 wdb.clock.clone(),
627 wdb.rng.clone(),
628 seed.clone(),
629 )
630 .into_iter(),
631 )
632 .expect("Wallet migration registration should have been successful.");
633 if let Some(migrations) = external_migrations {
634 migrator.register_multiple(migrations.into_iter())?;
635 }
636 if target_migrations.is_empty() {
637 migrator.up(None)?;
638 } else {
639 for target_migration in target_migrations {
640 migrator.up(Some(*target_migration))?;
641 }
642 }
643 wdb.conn
644 .borrow()
645 .execute("PRAGMA foreign_keys = ON", [])
646 .map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?;
647
648 if verify_seed_relevance && let Some(seed) = seed {
654 match wdb
655 .seed_relevance_to_derived_accounts(&seed)
656 .map_err(sqlite_client_error_to_wallet_migration_error)?
657 {
658 SeedRelevance::Relevant { .. } => (),
659 SeedRelevance::NoAccounts => (),
662 SeedRelevance::NotRelevant | SeedRelevance::NoDerivedAccounts => {
664 return Err(WalletMigrationError::SeedNotRelevant.into());
665 }
666 }
667 }
668
669 Ok(())
670}
671
672fn verify_sqlite_version_compatibility(
676 conn: &rusqlite::Connection,
677) -> Result<(), WalletMigrationError> {
678 let sqlite_version =
679 conn.query_row("SELECT sqlite_version()", [], |row| row.get::<_, String>(0))?;
680
681 let version_re = Regex::new(r"^(?<major>[0-9]+)\.(?<minor>[0-9]+).*$").unwrap();
682 let captures =
683 version_re
684 .captures(&sqlite_version)
685 .ok_or(WalletMigrationError::DatabaseNotSupported(
686 "Unknown".to_owned(),
687 ))?;
688 let parse_version_part = |part: &str| {
689 captures[part].parse::<u32>().map_err(|_| {
690 WalletMigrationError::CorruptedData(format!(
691 "Cannot decode SQLite {} version component {}",
692 part, &captures[part]
693 ))
694 })
695 };
696 let major = parse_version_part("major")?;
697 let minor = parse_version_part("minor")?;
698
699 if major != SQLITE_MAJOR_VERSION || minor < MIN_SQLITE_MINOR_VERSION {
700 Err(WalletMigrationError::DatabaseNotSupported(sqlite_version))
701 } else {
702 Ok(())
703 }
704}
705
706#[cfg(test)]
707pub(crate) mod testing {
708 use rand::RngCore;
709 use schemerz::MigratorError;
710 use secrecy::SecretVec;
711 use uuid::Uuid;
712 use zcash_protocol::consensus;
713
714 use crate::{WalletDb, util::Clock};
715
716 use super::WalletMigrationError;
717
718 pub(crate) fn init_wallet_db<
719 P: consensus::Parameters + 'static,
720 CL: Clock + Clone + 'static,
721 R: RngCore + Clone + 'static,
722 >(
723 wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
724 seed: Option<SecretVec<u8>>,
725 ) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
726 super::init_wallet_db_internal(wdb, seed, None, &[], true)
727 }
728}
729
730#[cfg(test)]
731mod tests {
732 use rand::RngCore;
733 use rusqlite::{self, Connection, ToSql, named_params};
734 use secrecy::Secret;
735
736 use tempfile::NamedTempFile;
737
738 use ::sapling::zip32::ExtendedFullViewingKey;
739 use zcash_client_backend::data_api::testing::TestBuilder;
740 use zcash_keys::{
741 address::Address,
742 encoding::{encode_extended_full_viewing_key, encode_payment_address},
743 keys::{
744 ReceiverRequirement::*, UnifiedAddressRequest, UnifiedFullViewingKey,
745 UnifiedSpendingKey, sapling,
746 },
747 };
748 use zcash_primitives::transaction::{TransactionData, TxVersion};
749 use zcash_protocol::consensus::{self, BlockHeight, BranchId, Network, NetworkConstants};
750 use zip32::AccountId;
751
752 use super::testing::init_wallet_db;
753 use crate::{
754 UA_TRANSPARENT, WalletDb,
755 testing::db::{TestDbFactory, test_clock, test_rng},
756 util::Clock,
757 wallet::db,
758 };
759
760 #[cfg(feature = "transparent-inputs")]
761 use {
762 super::WalletMigrationError,
763 crate::wallet::{self, PoolType, pool_code},
764 zcash_address::test_vectors,
765 zcash_client_backend::data_api::{AccountBirthday, AccountSource, WalletRead, WalletWrite},
766 zcash_primitives::block::BlockHash,
767 zip32::DiversifierIndex,
768 };
769
770 use regex::Regex;
771 #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
772 use zcash_protocol::value::Zatoshis;
773
774 pub(crate) fn describe_tables(conn: &Connection) -> Result<Vec<String>, rusqlite::Error> {
775 let result = conn
776 .prepare("SELECT sql FROM sqlite_schema WHERE type = 'table' ORDER BY tbl_name")?
777 .query_and_then([], |row| row.get::<_, String>(0))?
778 .collect::<Result<Vec<_>, _>>()?;
779
780 Ok(result)
781 }
782
783 fn normalize_sql(s: &str) -> String {
792 let re = Regex::new(r"\s+").unwrap();
793 let re_punct = Regex::new(r"([(),])").unwrap();
794 re.replace_all(&re_punct.replace_all(s, " $1 "), " ")
795 .trim()
796 .to_string()
797 }
798
799 #[test]
800 fn verify_schema() {
801 let st = TestBuilder::new()
802 .with_data_store_factory(TestDbFactory::default())
803 .build();
804
805 let normalize = normalize_sql;
806
807 let expected_tables = vec![
808 db::TABLE_ACCOUNTS,
809 db::TABLE_ADDRESSES,
810 db::TABLE_BLOCKS,
811 db::TABLE_IRONWOOD_RECEIVED_NOTE_SPENDS,
812 db::TABLE_IRONWOOD_RECEIVED_NOTES,
813 db::TABLE_IRONWOOD_TREE_CAP,
814 db::TABLE_IRONWOOD_TREE_CHECKPOINT_MARKS_REMOVED,
815 db::TABLE_IRONWOOD_TREE_CHECKPOINTS,
816 db::TABLE_IRONWOOD_TREE_RETAINED_CHECKPOINTS,
817 db::TABLE_IRONWOOD_TREE_SHARDS,
818 db::TABLE_NULLIFIER_MAP,
819 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_CROSSING_VALUES,
820 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_PREP_DIRECT_FUNDING,
821 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_PREP_INPUTS,
822 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_PREP_OUTPUTS,
823 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_SPEND_NULLIFIERS,
824 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_TRANSACTION_DEPS,
825 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_TRANSACTIONS,
826 db::TABLE_ORCHARD_IRONWOOD_MIGRATIONS,
827 db::TABLE_ORCHARD_RECEIVED_NOTE_SPENDS,
828 db::TABLE_ORCHARD_RECEIVED_NOTES,
829 db::TABLE_ORCHARD_TREE_CAP,
830 db::TABLE_ORCHARD_TREE_CHECKPOINT_MARKS_REMOVED,
831 db::TABLE_ORCHARD_TREE_CHECKPOINTS,
832 db::TABLE_ORCHARD_TREE_RETAINED_CHECKPOINTS,
833 db::TABLE_ORCHARD_TREE_SHARDS,
834 db::TABLE_SAPLING_RECEIVED_NOTE_SPENDS,
835 db::TABLE_SAPLING_RECEIVED_NOTES,
836 db::TABLE_SAPLING_TREE_CAP,
837 db::TABLE_SAPLING_TREE_CHECKPOINT_MARKS_REMOVED,
838 db::TABLE_SAPLING_TREE_CHECKPOINTS,
839 db::TABLE_SAPLING_TREE_RETAINED_CHECKPOINTS,
840 db::TABLE_SAPLING_TREE_SHARDS,
841 db::TABLE_SCAN_QUEUE,
842 db::TABLE_SCHEMERZ_MIGRATIONS,
843 db::TABLE_SENT_NOTES,
844 db::TABLE_SQLITE_SEQUENCE,
845 db::TABLE_TRANSACTIONS,
846 db::TABLE_TRANSPARENT_RECEIVED_OUTPUT_SPENDS,
847 db::TABLE_TRANSPARENT_RECEIVED_OUTPUTS,
848 db::TABLE_TRANSPARENT_SPEND_MAP,
849 db::TABLE_TRANSPARENT_SPEND_SEARCH_QUEUE,
850 db::TABLE_TX_LOCATOR_MAP,
851 db::TABLE_TX_RETRIEVAL_QUEUE,
852 ];
853
854 let rows = describe_tables(&st.wallet().db().conn).unwrap();
855 assert_eq!(rows.len(), expected_tables.len());
856 for (actual, expected) in rows.iter().zip(expected_tables.iter()) {
857 assert_eq!(normalize(actual), normalize(expected));
858 }
859
860 let expected_indices = vec![
861 db::INDEX_ACCOUNTS_ORCHARD_IVK,
862 db::INDEX_ACCOUNTS_P2PKH_IVK,
863 db::INDEX_ACCOUNTS_P2SH_IVK,
864 db::INDEX_ACCOUNTS_SAPLING_IVK,
865 db::INDEX_ACCOUNTS_UFVK,
866 db::INDEX_ACCOUNTS_UIVK,
867 db::INDEX_ACCOUNTS_UUID,
868 db::INDEX_HD_ACCOUNT,
869 db::INDEX_ADDRESSES_ACCOUNTS,
870 db::INDEX_ADDRESSES_CACHED_TRANSPARENT_RECEIVER_ADDRESS,
871 db::INDEX_ADDRESSES_INDICES,
872 db::INDEX_ADDRESSES_PUBKEYS,
873 db::INDEX_ADDRESSES_T_INDICES,
874 db::INDEX_IRONWOOD_RNS_NOTE,
875 db::INDEX_IRONWOOD_RNS_TX,
876 db::INDEX_IRONWOOD_RECEIVED_NOTES_ACCOUNT,
877 db::INDEX_IRONWOOD_RECEIVED_NOTES_ADDRESS,
878 db::INDEX_IRONWOOD_RECEIVED_NOTES_TX,
879 db::INDEX_IRONWOOD_RECEIVED_NOTES_WITNESS_STABILIZED,
880 db::INDEX_NF_MAP_LOCATOR_IDX,
881 db::INDEX_ORCHARD_IRONWOOD_MIGRATION_TX_DUE,
882 db::INDEX_ORCHARD_IRONWOOD_MIGRATIONS_ACCOUNT,
883 db::INDEX_ORCHARD_RNS_NOTE,
884 db::INDEX_ORCHARD_RNS_TX,
885 db::INDEX_ORCHARD_RECEIVED_NOTES_ACCOUNT,
886 db::INDEX_ORCHARD_RECEIVED_NOTES_ADDRESS,
887 db::INDEX_ORCHARD_RECEIVED_NOTES_TX,
888 db::INDEX_ORCHARD_RECEIVED_NOTES_WITNESS_STABILIZED,
889 db::INDEX_SAPLING_RNS_NOTE,
890 db::INDEX_SAPLING_RNS_TX,
891 db::INDEX_SAPLING_RECEIVED_NOTES_ACCOUNT,
892 db::INDEX_SAPLING_RECEIVED_NOTES_ADDRESS,
893 db::INDEX_SAPLING_RECEIVED_NOTES_TX,
894 db::INDEX_SAPLING_RECEIVED_NOTES_WITNESS_STABILIZED,
895 db::INDEX_SENT_NOTES_FROM_ACCOUNT,
896 db::INDEX_SENT_NOTES_TO_ACCOUNT,
897 db::INDEX_SENT_NOTES_TX,
898 db::INDEX_TRANSPARENT_ROS_OUTPUT,
899 db::INDEX_TRANSPARENT_ROS_TX,
900 db::INDEX_TRANSPARENT_RECEIVED_OUTPUTS_ACCOUNT,
901 db::INDEX_TRANSPARENT_RECEIVED_OUTPUTS_ADDRESS,
902 db::INDEX_TRANSPARENT_RECEIVED_OUTPUTS_TX,
903 db::INDEX_TRANSPARENT_RECEIVED_OUTPUTS_VALUE_ZAT,
904 db::INDEX_TRANSPARENT_SPEND_MAP_TX,
905 db::INDEX_TRANSPARENT_SPEND_SEARCH_TX,
906 db::INDEX_TX_RETIREVAL_QUEUE_DEPENDENT_TX,
907 ];
908 let mut indices_query = st
909 .wallet()
910 .db()
911 .conn
912 .prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND sql != '' ORDER BY tbl_name, name")
913 .unwrap();
914 let mut rows = indices_query.query([]).unwrap();
915 let mut expected_idx = 0;
916 while let Some(row) = rows.next().unwrap() {
917 let actual: String = row.get(0).unwrap();
918 assert_eq!(
919 normalize(&actual),
920 normalize(expected_indices[expected_idx])
921 );
922 expected_idx += 1;
923 }
924
925 let expected_views = vec![
926 db::VIEW_ADDRESS_FIRST_USE.to_owned(),
927 db::VIEW_ADDRESS_USES.to_owned(),
928 db::view_ironwood_shard_scan_ranges(st.network()),
929 db::view_ironwood_shard_unscanned_ranges(),
930 db::VIEW_IRONWOOD_SHARDS_SCAN_STATE.to_owned(),
931 db::view_orchard_shard_scan_ranges(st.network()),
932 db::view_orchard_shard_unscanned_ranges(),
933 db::VIEW_ORCHARD_SHARDS_SCAN_STATE.to_owned(),
934 db::VIEW_RECEIVED_OUTPUT_SPENDS.to_owned(),
935 db::VIEW_RECEIVED_OUTPUTS.to_owned(),
936 db::view_sapling_shard_scan_ranges(st.network()),
937 db::view_sapling_shard_unscanned_ranges(),
938 db::VIEW_SAPLING_SHARDS_SCAN_STATE.to_owned(),
939 db::VIEW_TRANSACTIONS.to_owned(),
940 db::VIEW_TX_OUTPUTS.to_owned(),
941 ];
942
943 let mut views_query = st
944 .wallet()
945 .db()
946 .conn
947 .prepare("SELECT sql FROM sqlite_schema WHERE type = 'view' ORDER BY tbl_name")
948 .unwrap();
949 let mut rows = views_query.query([]).unwrap();
950 let mut expected_idx = 0;
951 while let Some(row) = rows.next().unwrap() {
952 let actual: String = row.get(0).unwrap();
953 assert_eq!(normalize(&actual), normalize(&expected_views[expected_idx]));
954 expected_idx += 1;
955 }
956 }
957
958 #[test]
970 fn canonical_pool_migration_ddl_matches_the_migration_path() {
971 let conn = Connection::open_in_memory().unwrap();
972 crate::wallet::db::init_orchard_ironwood_migration_tables(&conn).unwrap();
973
974 let expected = [
975 (
976 "orchard_ironwood_migrations",
977 db::TABLE_ORCHARD_IRONWOOD_MIGRATIONS,
978 ),
979 (
980 "orchard_ironwood_migration_crossing_values",
981 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_CROSSING_VALUES,
982 ),
983 (
984 "orchard_ironwood_migration_prep_inputs",
985 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_PREP_INPUTS,
986 ),
987 (
988 "orchard_ironwood_migration_prep_outputs",
989 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_PREP_OUTPUTS,
990 ),
991 (
992 "orchard_ironwood_migration_prep_direct_funding",
993 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_PREP_DIRECT_FUNDING,
994 ),
995 (
996 "orchard_ironwood_migration_transactions",
997 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_TRANSACTIONS,
998 ),
999 (
1000 "orchard_ironwood_migration_transaction_deps",
1001 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_TRANSACTION_DEPS,
1002 ),
1003 (
1004 "orchard_ironwood_migration_spend_nullifiers",
1005 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_SPEND_NULLIFIERS,
1006 ),
1007 (
1008 "idx_orchard_ironwood_migration_tx_due",
1009 db::INDEX_ORCHARD_IRONWOOD_MIGRATION_TX_DUE,
1010 ),
1011 (
1012 "idx_orchard_ironwood_migrations_account",
1013 db::INDEX_ORCHARD_IRONWOOD_MIGRATIONS_ACCOUNT,
1014 ),
1015 ];
1016
1017 let mut stmt = conn
1018 .prepare("SELECT sql FROM sqlite_master WHERE name = ? AND sql IS NOT NULL")
1019 .unwrap();
1020 for (name, expected) in expected {
1021 let actual: String = stmt
1022 .query_row([name], |row| row.get(0))
1023 .unwrap_or_else(|e| panic!("the canonical DDL creates {name}: {e}"));
1024 assert_eq!(normalize_sql(&actual), normalize_sql(expected));
1025 }
1026 }
1027
1028 #[test]
1029 fn external_schema_prefix_unused() {
1030 let st = TestBuilder::new()
1031 .with_data_store_factory(TestDbFactory::default())
1032 .build();
1033
1034 let mut names_query = st
1035 .wallet()
1036 .db()
1037 .conn
1038 .prepare("SELECT tbl_name FROM sqlite_schema")
1039 .unwrap();
1040 let mut rows = names_query.query([]).unwrap();
1041 while let Some(row) = rows.next().unwrap() {
1042 let name: String = row.get(0).unwrap();
1043 assert!(!name.starts_with("ext_"));
1044 }
1045 }
1046
1047 #[test]
1048 fn init_migrate_from_0_3_0() {
1049 fn init_0_3_0<P: consensus::Parameters, CL: Clock + Clone, R: RngCore + Clone>(
1050 wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
1051 extfvk: &ExtendedFullViewingKey,
1052 account: AccountId,
1053 ) -> Result<(), rusqlite::Error> {
1054 wdb.conn.execute(
1055 "CREATE TABLE accounts (
1056 account INTEGER PRIMARY KEY,
1057 extfvk TEXT NOT NULL,
1058 address TEXT NOT NULL
1059 )",
1060 [],
1061 )?;
1062 wdb.conn.execute(
1063 "CREATE TABLE blocks (
1064 height INTEGER PRIMARY KEY,
1065 hash BLOB NOT NULL,
1066 time INTEGER NOT NULL,
1067 sapling_tree BLOB NOT NULL
1068 )",
1069 [],
1070 )?;
1071 wdb.conn.execute(
1072 "CREATE TABLE transactions (
1073 id_tx INTEGER PRIMARY KEY,
1074 txid BLOB NOT NULL UNIQUE,
1075 created TEXT,
1076 block INTEGER,
1077 tx_index INTEGER,
1078 expiry_height INTEGER,
1079 raw BLOB,
1080 FOREIGN KEY (block) REFERENCES blocks(height)
1081 )",
1082 [],
1083 )?;
1084 wdb.conn.execute(
1085 "CREATE TABLE received_notes (
1086 id_note INTEGER PRIMARY KEY,
1087 tx INTEGER NOT NULL,
1088 output_index INTEGER NOT NULL,
1089 account INTEGER NOT NULL,
1090 diversifier BLOB NOT NULL,
1091 value INTEGER NOT NULL,
1092 rcm BLOB NOT NULL,
1093 nf BLOB NOT NULL UNIQUE,
1094 is_change INTEGER NOT NULL,
1095 memo BLOB,
1096 spent INTEGER,
1097 FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1098 FOREIGN KEY (account) REFERENCES accounts(account),
1099 FOREIGN KEY (spent) REFERENCES transactions(id_tx),
1100 CONSTRAINT tx_output UNIQUE (tx, output_index)
1101 )",
1102 [],
1103 )?;
1104 wdb.conn.execute(
1105 "CREATE TABLE sapling_witnesses (
1106 id_witness INTEGER PRIMARY KEY,
1107 note INTEGER NOT NULL,
1108 block INTEGER NOT NULL,
1109 witness BLOB NOT NULL,
1110 FOREIGN KEY (note) REFERENCES received_notes(id_note),
1111 FOREIGN KEY (block) REFERENCES blocks(height),
1112 CONSTRAINT witness_height UNIQUE (note, block)
1113 )",
1114 [],
1115 )?;
1116 wdb.conn.execute(
1117 "CREATE TABLE sent_notes (
1118 id_note INTEGER PRIMARY KEY,
1119 tx INTEGER NOT NULL,
1120 output_index INTEGER NOT NULL,
1121 from_account INTEGER NOT NULL,
1122 address TEXT NOT NULL,
1123 value INTEGER NOT NULL,
1124 memo BLOB,
1125 FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1126 FOREIGN KEY (from_account) REFERENCES accounts(account),
1127 CONSTRAINT tx_output UNIQUE (tx, output_index)
1128 )",
1129 [],
1130 )?;
1131
1132 let address = encode_payment_address(
1133 wdb.params.hrp_sapling_payment_address(),
1134 &extfvk.default_address().1,
1135 );
1136 let extfvk = encode_extended_full_viewing_key(
1137 wdb.params.hrp_sapling_extended_full_viewing_key(),
1138 extfvk,
1139 );
1140 wdb.conn.execute(
1141 "INSERT INTO accounts (account, extfvk, address)
1142 VALUES (?, ?, ?)",
1143 [
1144 u32::from(account).to_sql()?,
1145 extfvk.to_sql()?,
1146 address.to_sql()?,
1147 ],
1148 )?;
1149
1150 Ok(())
1151 }
1152
1153 let data_file = NamedTempFile::new().unwrap();
1154 let mut db_data = WalletDb::for_path(
1155 data_file.path(),
1156 Network::TestNetwork,
1157 test_clock(),
1158 test_rng(),
1159 )
1160 .unwrap();
1161
1162 let seed = [0xab; 32];
1163 let account = AccountId::ZERO;
1164 let secret_key = sapling::spending_key(&seed, db_data.params.coin_type(), account);
1165 #[allow(deprecated)]
1166 let extfvk = secret_key.to_extended_full_viewing_key();
1167
1168 init_0_3_0(&mut db_data, &extfvk, account).unwrap();
1169 assert_matches!(
1170 init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1171 Ok(_)
1172 );
1173 }
1174
1175 #[test]
1176 fn init_migrate_from_autoshielding_poc() {
1177 fn init_autoshielding<P: consensus::Parameters, CL, R>(
1178 wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
1179 extfvk: &ExtendedFullViewingKey,
1180 account: AccountId,
1181 ) -> Result<(), rusqlite::Error> {
1182 wdb.conn.execute(
1183 "CREATE TABLE accounts (
1184 account INTEGER PRIMARY KEY,
1185 extfvk TEXT NOT NULL,
1186 address TEXT NOT NULL,
1187 transparent_address TEXT NOT NULL
1188 )",
1189 [],
1190 )?;
1191 wdb.conn.execute(
1192 "CREATE TABLE blocks (
1193 height INTEGER PRIMARY KEY,
1194 hash BLOB NOT NULL,
1195 time INTEGER NOT NULL,
1196 sapling_tree BLOB NOT NULL
1197 )",
1198 [],
1199 )?;
1200 wdb.conn.execute(
1201 "CREATE TABLE transactions (
1202 id_tx INTEGER PRIMARY KEY,
1203 txid BLOB NOT NULL UNIQUE,
1204 created TEXT,
1205 block INTEGER,
1206 tx_index INTEGER,
1207 expiry_height INTEGER,
1208 raw BLOB,
1209 FOREIGN KEY (block) REFERENCES blocks(height)
1210 )",
1211 [],
1212 )?;
1213 wdb.conn.execute(
1214 "CREATE TABLE received_notes (
1215 id_note INTEGER PRIMARY KEY,
1216 tx INTEGER NOT NULL,
1217 output_index INTEGER NOT NULL,
1218 account INTEGER NOT NULL,
1219 diversifier BLOB NOT NULL,
1220 value INTEGER NOT NULL,
1221 rcm BLOB NOT NULL,
1222 nf BLOB NOT NULL UNIQUE,
1223 is_change INTEGER NOT NULL,
1224 memo BLOB,
1225 spent INTEGER,
1226 FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1227 FOREIGN KEY (account) REFERENCES accounts(account),
1228 FOREIGN KEY (spent) REFERENCES transactions(id_tx),
1229 CONSTRAINT tx_output UNIQUE (tx, output_index)
1230 )",
1231 [],
1232 )?;
1233 wdb.conn.execute(
1234 "CREATE TABLE sapling_witnesses (
1235 id_witness INTEGER PRIMARY KEY,
1236 note INTEGER NOT NULL,
1237 block INTEGER NOT NULL,
1238 witness BLOB NOT NULL,
1239 FOREIGN KEY (note) REFERENCES received_notes(id_note),
1240 FOREIGN KEY (block) REFERENCES blocks(height),
1241 CONSTRAINT witness_height UNIQUE (note, block)
1242 )",
1243 [],
1244 )?;
1245 wdb.conn.execute(
1246 "CREATE TABLE sent_notes (
1247 id_note INTEGER PRIMARY KEY,
1248 tx INTEGER NOT NULL,
1249 output_index INTEGER NOT NULL,
1250 from_account INTEGER NOT NULL,
1251 address TEXT NOT NULL,
1252 value INTEGER NOT NULL,
1253 memo BLOB,
1254 FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1255 FOREIGN KEY (from_account) REFERENCES accounts(account),
1256 CONSTRAINT tx_output UNIQUE (tx, output_index)
1257 )",
1258 [],
1259 )?;
1260 wdb.conn.execute(
1261 "CREATE TABLE utxos (
1262 id_utxo INTEGER PRIMARY KEY,
1263 address TEXT NOT NULL,
1264 prevout_txid BLOB NOT NULL,
1265 prevout_idx INTEGER NOT NULL,
1266 script BLOB NOT NULL,
1267 value_zat INTEGER NOT NULL,
1268 height INTEGER NOT NULL,
1269 spent_in_tx INTEGER,
1270 FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx),
1271 CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx)
1272 )",
1273 [],
1274 )?;
1275
1276 let address = encode_payment_address(
1277 wdb.params.hrp_sapling_payment_address(),
1278 &extfvk.default_address().1,
1279 );
1280 let extfvk = encode_extended_full_viewing_key(
1281 wdb.params.hrp_sapling_extended_full_viewing_key(),
1282 extfvk,
1283 );
1284 wdb.conn.execute(
1285 "INSERT INTO accounts (account, extfvk, address, transparent_address)
1286 VALUES (?, ?, ?, '')",
1287 [
1288 u32::from(account).to_sql()?,
1289 extfvk.to_sql()?,
1290 address.to_sql()?,
1291 ],
1292 )?;
1293
1294 wdb.conn.execute(
1296 "INSERT INTO blocks (height, hash, time, sapling_tree) \
1297 VALUES (0, x'0000000000000000000000000000000000000000000000000000000000000000', 0, x'000000')",
1298 [],
1299 )?;
1300
1301 let tx = TransactionData::from_parts(
1302 TxVersion::V4,
1303 BranchId::Canopy,
1304 0,
1305 BlockHeight::from(0),
1306 #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
1307 Zatoshis::ZERO,
1308 None,
1309 None,
1310 None,
1311 None,
1312 )
1313 .freeze()
1314 .unwrap();
1315
1316 let mut tx_bytes = vec![];
1317 tx.write(&mut tx_bytes).unwrap();
1318 wdb.conn.execute(
1319 "INSERT INTO transactions (block, id_tx, txid, raw) VALUES (0, 0, :txid, :tx_bytes)",
1320 named_params![
1321 ":txid": tx.txid().as_ref(),
1322 ":tx_bytes": &tx_bytes[..]
1323 ],
1324 )?;
1325 wdb.conn.execute(
1326 "INSERT INTO sent_notes (tx, output_index, from_account, address, value)
1327 VALUES (0, 0, ?, ?, 0)",
1328 [u32::from(account).to_sql()?, address.to_sql()?],
1329 )?;
1330
1331 Ok(())
1332 }
1333
1334 let data_file = NamedTempFile::new().unwrap();
1335 let mut db_data = WalletDb::for_path(
1336 data_file.path(),
1337 Network::TestNetwork,
1338 test_clock(),
1339 test_rng(),
1340 )
1341 .unwrap();
1342
1343 let seed = [0xab; 32];
1344 let account = AccountId::ZERO;
1345 let secret_key = sapling::spending_key(&seed, db_data.params.coin_type(), account);
1346 #[allow(deprecated)]
1347 let extfvk = secret_key.to_extended_full_viewing_key();
1348
1349 init_autoshielding(&mut db_data, &extfvk, account).unwrap();
1350 assert_matches!(
1351 init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1352 Ok(_)
1353 );
1354 }
1355
1356 #[test]
1357 fn init_migrate_from_main_pre_migrations() {
1358 fn init_main<P: consensus::Parameters, CL, R>(
1359 wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
1360 ufvk: &UnifiedFullViewingKey,
1361 account: AccountId,
1362 ) -> Result<(), rusqlite::Error> {
1363 wdb.conn.execute(
1364 "CREATE TABLE accounts (
1365 account INTEGER PRIMARY KEY,
1366 ufvk TEXT,
1367 address TEXT,
1368 transparent_address TEXT
1369 )",
1370 [],
1371 )?;
1372 wdb.conn.execute(
1373 "CREATE TABLE blocks (
1374 height INTEGER PRIMARY KEY,
1375 hash BLOB NOT NULL,
1376 time INTEGER NOT NULL,
1377 sapling_tree BLOB NOT NULL
1378 )",
1379 [],
1380 )?;
1381 wdb.conn.execute(
1382 "CREATE TABLE transactions (
1383 id_tx INTEGER PRIMARY KEY,
1384 txid BLOB NOT NULL UNIQUE,
1385 created TEXT,
1386 block INTEGER,
1387 tx_index INTEGER,
1388 expiry_height INTEGER,
1389 raw BLOB,
1390 FOREIGN KEY (block) REFERENCES blocks(height)
1391 )",
1392 [],
1393 )?;
1394 wdb.conn.execute(
1395 "CREATE TABLE received_notes (
1396 id_note INTEGER PRIMARY KEY,
1397 tx INTEGER NOT NULL,
1398 output_index INTEGER NOT NULL,
1399 account INTEGER NOT NULL,
1400 diversifier BLOB NOT NULL,
1401 value INTEGER NOT NULL,
1402 rcm BLOB NOT NULL,
1403 nf BLOB NOT NULL UNIQUE,
1404 is_change INTEGER NOT NULL,
1405 memo BLOB,
1406 spent INTEGER,
1407 FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1408 FOREIGN KEY (account) REFERENCES accounts(account),
1409 FOREIGN KEY (spent) REFERENCES transactions(id_tx),
1410 CONSTRAINT tx_output UNIQUE (tx, output_index)
1411 )",
1412 [],
1413 )?;
1414 wdb.conn.execute(
1415 "CREATE TABLE sapling_witnesses (
1416 id_witness INTEGER PRIMARY KEY,
1417 note INTEGER NOT NULL,
1418 block INTEGER NOT NULL,
1419 witness BLOB NOT NULL,
1420 FOREIGN KEY (note) REFERENCES received_notes(id_note),
1421 FOREIGN KEY (block) REFERENCES blocks(height),
1422 CONSTRAINT witness_height UNIQUE (note, block)
1423 )",
1424 [],
1425 )?;
1426 wdb.conn.execute(
1427 "CREATE TABLE sent_notes (
1428 id_note INTEGER PRIMARY KEY,
1429 tx INTEGER NOT NULL,
1430 output_pool INTEGER NOT NULL,
1431 output_index INTEGER NOT NULL,
1432 from_account INTEGER NOT NULL,
1433 address TEXT NOT NULL,
1434 value INTEGER NOT NULL,
1435 memo BLOB,
1436 FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1437 FOREIGN KEY (from_account) REFERENCES accounts(account),
1438 CONSTRAINT tx_output UNIQUE (tx, output_pool, output_index)
1439 )",
1440 [],
1441 )?;
1442 wdb.conn.execute(
1443 "CREATE TABLE utxos (
1444 id_utxo INTEGER PRIMARY KEY,
1445 address TEXT NOT NULL,
1446 prevout_txid BLOB NOT NULL,
1447 prevout_idx INTEGER NOT NULL,
1448 script BLOB NOT NULL,
1449 value_zat INTEGER NOT NULL,
1450 height INTEGER NOT NULL,
1451 spent_in_tx INTEGER,
1452 FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx),
1453 CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx)
1454 )",
1455 [],
1456 )?;
1457
1458 let ufvk_str = ufvk.encode(&wdb.params);
1459
1460 let ua_request = UnifiedAddressRequest::unsafe_custom(Omit, Require, UA_TRANSPARENT);
1463 let address_str = Address::Unified(
1464 ufvk.default_address(ua_request)
1465 .expect("A valid default address exists for the UFVK")
1466 .0,
1467 )
1468 .encode(&wdb.params);
1469 wdb.conn.execute(
1470 "INSERT INTO accounts (account, ufvk, address, transparent_address)
1471 VALUES (?, ?, ?, '')",
1472 [
1473 u32::from(account).to_sql()?,
1474 ufvk_str.to_sql()?,
1475 address_str.to_sql()?,
1476 ],
1477 )?;
1478
1479 #[cfg(feature = "transparent-inputs")]
1481 {
1482 let taddr = Address::Transparent(
1483 *ufvk
1484 .default_address(ua_request)
1485 .expect("A valid default address exists for the UFVK")
1486 .0
1487 .transparent()
1488 .unwrap(),
1489 )
1490 .encode(&wdb.params);
1491 wdb.conn.execute(
1492 "INSERT INTO blocks (height, hash, time, sapling_tree) \
1493 VALUES (0, x'0000000000000000000000000000000000000000000000000000000000000000', 0, x'000000')",
1494 [],
1495 )?;
1496 wdb.conn.execute(
1497 "INSERT INTO transactions (block, id_tx, txid) VALUES (0, 0, '')",
1498 [],
1499 )?;
1500 wdb.conn.execute(
1501 "INSERT INTO sent_notes (tx, output_pool, output_index, from_account, address, value)
1502 VALUES (0, ?, 0, ?, ?, 0)",
1503 [pool_code(PoolType::TRANSPARENT).to_sql()?, u32::from(account).to_sql()?, taddr.to_sql()?])?;
1504 }
1505
1506 Ok(())
1507 }
1508
1509 let data_file = NamedTempFile::new().unwrap();
1510 let mut db_data = WalletDb::for_path(
1511 data_file.path(),
1512 Network::TestNetwork,
1513 test_clock(),
1514 test_rng(),
1515 )
1516 .unwrap();
1517
1518 let seed = [0xab; 32];
1519 let account = AccountId::ZERO;
1520 let secret_key = UnifiedSpendingKey::from_seed(&db_data.params, &seed, account).unwrap();
1521
1522 init_main(
1523 &mut db_data,
1524 &secret_key.to_unified_full_viewing_key(),
1525 account,
1526 )
1527 .unwrap();
1528 assert_matches!(
1529 init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1530 Ok(_)
1531 );
1532 }
1533
1534 #[test]
1535 #[cfg(feature = "transparent-inputs")]
1536 fn account_produces_expected_ua_sequence() {
1537 let network = Network::MainNetwork;
1538 let data_file = NamedTempFile::new().unwrap();
1539 let mut db_data =
1540 WalletDb::for_path(data_file.path(), network, test_clock(), test_rng()).unwrap();
1541 assert_matches!(init_wallet_db(&mut db_data, None), Ok(_));
1542
1543 let seed = test_vectors::UNIFIED[0].root_seed;
1545 let other_seed = [7; 32];
1546 assert_matches!(
1547 init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1548 Ok(())
1549 );
1550 assert_matches!(
1551 init_wallet_db(&mut db_data, Some(Secret::new(other_seed.to_vec()))),
1552 Ok(())
1553 );
1554
1555 let birthday = AccountBirthday::from_sapling_activation(&network, BlockHash([0; 32]));
1556 let (account_id, _usk) = db_data
1557 .create_account("", &Secret::new(seed.to_vec()), &birthday, None)
1558 .unwrap();
1559
1560 db_data.update_chain_tip(birthday.height()).unwrap();
1563
1564 assert_matches!(
1565 db_data.get_account(account_id),
1566 Ok(Some(account)) if matches!(
1567 &account.kind,
1568 AccountSource::Derived{derivation, ..} if derivation.account_index() == zip32::AccountId::ZERO,
1569 )
1570 );
1571
1572 assert_matches!(
1574 init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1575 Ok(())
1576 );
1577 assert_matches!(
1578 init_wallet_db(&mut db_data, Some(Secret::new(other_seed.to_vec()))),
1579 Err(schemerz::MigratorError::Adapter(
1580 WalletMigrationError::SeedNotRelevant
1581 ))
1582 );
1583
1584 for tv in &test_vectors::UNIFIED[..3] {
1585 if let Some(Address::Unified(tvua)) =
1586 Address::decode(&Network::MainNetwork, tv.unified_addr)
1587 {
1588 let ua_request = UnifiedAddressRequest::unsafe_custom(Omit, Require, Require);
1590
1591 let (ua, di) = wallet::get_last_generated_address_matching(
1592 &db_data.conn,
1593 &db_data.params,
1594 account_id,
1595 if tv.diversifier_index == 0 {
1596 UnifiedAddressRequest::AllAvailableKeys
1597 } else {
1598 ua_request
1599 },
1600 )
1601 .unwrap()
1602 .expect("create_account generated the first address");
1603 assert_eq!(DiversifierIndex::from(tv.diversifier_index), di);
1604 assert_eq!(tvua.transparent(), ua.transparent());
1605 assert_eq!(tvua.sapling(), ua.sapling());
1606 #[cfg(not(feature = "orchard"))]
1607 assert_eq!(tv.unified_addr, ua.encode(&Network::MainNetwork));
1608
1609 db_data
1610 .get_next_available_address(account_id, ua_request)
1611 .unwrap()
1612 .expect("get_next_available_address generated an address");
1613 } else {
1614 panic!(
1615 "{} did not decode to a valid unified address",
1616 tv.unified_addr
1617 );
1618 }
1619 }
1620 }
1621}