1use std::{borrow::BorrowMut, fmt, rc::Rc};
4
5use rand_core::Rng;
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<
351 C: BorrowMut<rusqlite::Connection>,
352 P: consensus::Parameters + 'static,
353 CL: Clock + Clone + 'static,
354 R: Rng + Clone + 'static,
355>(
356 wdb: &mut WalletDb<C, P, CL, R>,
357 seed: Option<SecretVec<u8>>,
358) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
359 if let Some(seed) = seed {
360 WalletMigrator::new().with_seed(seed)
361 } else {
362 WalletMigrator::new()
363 }
364 .init_or_migrate(wdb)
365}
366
367pub struct WalletMigrator {
448 seed: Option<SecretVec<u8>>,
449 verify_seed_relevance: bool,
450 external_migrations: Option<Vec<Box<dyn RusqliteMigration<Error = WalletMigrationError>>>>,
451}
452
453impl Default for WalletMigrator {
454 fn default() -> Self {
455 Self::new()
456 }
457}
458
459impl WalletMigrator {
460 pub fn new() -> Self {
462 Self {
463 seed: None,
464 verify_seed_relevance: true,
465 external_migrations: None,
466 }
467 }
468
469 pub fn with_seed(mut self, seed: SecretVec<u8>) -> Self {
471 self.seed = Some(seed);
472 self
473 }
474
475 #[cfg(test)]
477 pub(crate) fn ignore_seed_relevance(mut self) -> Self {
478 self.verify_seed_relevance = false;
479 self
480 }
481
482 pub fn with_external_migrations(
541 mut self,
542 migrations: Vec<Box<dyn RusqliteMigration<Error = WalletMigrationError>>>,
543 ) -> Self {
544 self.external_migrations = Some(migrations);
545 self
546 }
547
548 pub fn init_or_migrate<
551 C: BorrowMut<rusqlite::Connection>,
552 P: consensus::Parameters + 'static,
553 CL: Clock + Clone + 'static,
554 R: Rng + Clone + 'static,
555 >(
556 self,
557 wdb: &mut WalletDb<C, P, CL, R>,
558 ) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
559 self.init_or_migrate_to(wdb, &[])
560 }
561
562 pub(crate) fn init_or_migrate_to<
565 C: BorrowMut<rusqlite::Connection>,
566 P: consensus::Parameters + 'static,
567 CL: Clock + Clone + 'static,
568 R: Rng + Clone + 'static,
569 >(
570 self,
571 wdb: &mut WalletDb<C, P, CL, R>,
572 target_migrations: &[Uuid],
573 ) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
574 init_wallet_db_internal(
575 wdb,
576 self.seed,
577 self.external_migrations,
578 target_migrations,
579 self.verify_seed_relevance,
580 )
581 }
582}
583
584fn init_wallet_db_internal<
585 C: BorrowMut<rusqlite::Connection>,
586 P: consensus::Parameters + 'static,
587 CL: Clock + Clone + 'static,
588 R: Rng + Clone + 'static,
589>(
590 wdb: &mut WalletDb<C, P, CL, R>,
591 seed: Option<SecretVec<u8>>,
592 external_migrations: Option<Vec<Box<dyn RusqliteMigration<Error = WalletMigrationError>>>>,
593 target_migrations: &[Uuid],
594 verify_seed_relevance: bool,
595) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
596 let seed = seed.map(Rc::new);
597
598 verify_sqlite_version_compatibility(wdb.conn.borrow()).map_err(MigratorError::Adapter)?;
599
600 wdb.conn
606 .borrow()
607 .execute_batch("PRAGMA foreign_keys = OFF;")
608 .map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?;
609
610 {
616 let adapter = RusqliteAdapter::<'_, WalletMigrationError>::new(
617 wdb.conn.borrow_mut(),
618 Some(MIGRATIONS_TABLE.to_string()),
619 );
620 adapter.init().expect("Migrations table setup succeeds.");
621 }
622
623 verify_network_compatibility(wdb.conn.borrow(), &wdb.params).map_err(MigratorError::Adapter)?;
627
628 let adapter = RusqliteAdapter::new(wdb.conn.borrow_mut(), Some(MIGRATIONS_TABLE.to_string()));
631 let mut migrator = Migrator::new(adapter);
632 migrator
633 .register_multiple(
634 migrations::all_migrations(
635 &wdb.params,
636 wdb.clock.clone(),
637 wdb.rng.clone(),
638 seed.clone(),
639 )
640 .into_iter(),
641 )
642 .expect("Wallet migration registration should have been successful.");
643 if let Some(migrations) = external_migrations {
644 migrator.register_multiple(migrations.into_iter())?;
645 }
646 if target_migrations.is_empty() {
647 migrator.up(None)?;
648 } else {
649 for target_migration in target_migrations {
650 migrator.up(Some(*target_migration))?;
651 }
652 }
653 wdb.conn
654 .borrow()
655 .execute("PRAGMA foreign_keys = ON", [])
656 .map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?;
657
658 if verify_seed_relevance && let Some(seed) = seed {
664 match wdb
665 .seed_relevance_to_derived_accounts(&seed)
666 .map_err(sqlite_client_error_to_wallet_migration_error)?
667 {
668 SeedRelevance::Relevant { .. } => (),
669 SeedRelevance::NoAccounts => (),
672 SeedRelevance::NotRelevant | SeedRelevance::NoDerivedAccounts => {
674 return Err(WalletMigrationError::SeedNotRelevant.into());
675 }
676 }
677 }
678
679 Ok(())
680}
681
682fn verify_sqlite_version_compatibility(
686 conn: &rusqlite::Connection,
687) -> Result<(), WalletMigrationError> {
688 let sqlite_version =
689 conn.query_row("SELECT sqlite_version()", [], |row| row.get::<_, String>(0))?;
690
691 let version_re = Regex::new(r"^(?<major>[0-9]+)\.(?<minor>[0-9]+).*$").unwrap();
692 let captures =
693 version_re
694 .captures(&sqlite_version)
695 .ok_or(WalletMigrationError::DatabaseNotSupported(
696 "Unknown".to_owned(),
697 ))?;
698 let parse_version_part = |part: &str| {
699 captures[part].parse::<u32>().map_err(|_| {
700 WalletMigrationError::CorruptedData(format!(
701 "Cannot decode SQLite {} version component {}",
702 part, &captures[part]
703 ))
704 })
705 };
706 let major = parse_version_part("major")?;
707 let minor = parse_version_part("minor")?;
708
709 if major != SQLITE_MAJOR_VERSION || minor < MIN_SQLITE_MINOR_VERSION {
710 Err(WalletMigrationError::DatabaseNotSupported(sqlite_version))
711 } else {
712 Ok(())
713 }
714}
715
716#[cfg(test)]
717pub(crate) mod testing {
718 use rand::Rng;
719 use schemerz::MigratorError;
720 use secrecy::SecretVec;
721 use uuid::Uuid;
722 use zcash_protocol::consensus;
723
724 use crate::{WalletDb, util::Clock};
725
726 use super::WalletMigrationError;
727
728 pub(crate) fn init_wallet_db<
729 P: consensus::Parameters + 'static,
730 CL: Clock + Clone + 'static,
731 R: Rng + Clone + 'static,
732 >(
733 wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
734 seed: Option<SecretVec<u8>>,
735 ) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
736 super::init_wallet_db_internal(wdb, seed, None, &[], true)
737 }
738}
739
740#[cfg(test)]
741mod tests {
742 use rand::Rng;
743 use rusqlite::{self, Connection, ToSql, named_params};
744 use secrecy::Secret;
745
746 use tempfile::NamedTempFile;
747
748 use ::sapling::zip32::ExtendedFullViewingKey;
749 use zcash_client_backend::data_api::testing::TestBuilder;
750 use zcash_keys::{
751 address::Address,
752 encoding::{encode_extended_full_viewing_key, encode_payment_address},
753 keys::{
754 ReceiverRequirement::*, UnifiedAddressRequest, UnifiedFullViewingKey,
755 UnifiedSpendingKey, sapling,
756 },
757 };
758 use zcash_primitives::transaction::{TransactionData, TxVersion};
759 use zcash_protocol::consensus::{self, BlockHeight, BranchId, Network, NetworkConstants};
760 use zip32::AccountId;
761
762 use super::testing::init_wallet_db;
763 use crate::{
764 UA_TRANSPARENT, WalletDb,
765 testing::db::{TestDbFactory, test_clock, test_rng},
766 util::Clock,
767 wallet::db,
768 };
769
770 #[cfg(feature = "transparent-inputs")]
771 use {
772 super::WalletMigrationError,
773 crate::wallet::{self, PoolType, pool_code},
774 zcash_address::test_vectors,
775 zcash_client_backend::data_api::{AccountBirthday, AccountSource, WalletRead, WalletWrite},
776 zcash_primitives::block::BlockHash,
777 zip32::DiversifierIndex,
778 };
779
780 use regex::Regex;
781 #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
782 use zcash_protocol::value::Zatoshis;
783
784 pub(crate) fn describe_tables(conn: &Connection) -> Result<Vec<String>, rusqlite::Error> {
785 let result = conn
786 .prepare("SELECT sql FROM sqlite_schema WHERE type = 'table' ORDER BY tbl_name")?
787 .query_and_then([], |row| row.get::<_, String>(0))?
788 .collect::<Result<Vec<_>, _>>()?;
789
790 Ok(result)
791 }
792
793 fn normalize_sql(s: &str) -> String {
802 let re = Regex::new(r"\s+").unwrap();
803 let re_punct = Regex::new(r"([(),])").unwrap();
804 re.replace_all(&re_punct.replace_all(s, " $1 "), " ")
805 .trim()
806 .to_string()
807 }
808
809 #[test]
810 fn verify_schema() {
811 let st = TestBuilder::new()
812 .with_data_store_factory(TestDbFactory::default())
813 .build();
814
815 let normalize = normalize_sql;
816
817 let expected_tables = vec![
818 db::TABLE_ACCOUNTS,
819 db::TABLE_ADDRESSES,
820 db::TABLE_BLOCKS,
821 db::TABLE_IRONWOOD_RECEIVED_NOTE_SPENDS,
822 db::TABLE_IRONWOOD_RECEIVED_NOTES,
823 db::TABLE_IRONWOOD_TREE_CAP,
824 db::TABLE_IRONWOOD_TREE_CHECKPOINT_MARKS_REMOVED,
825 db::TABLE_IRONWOOD_TREE_CHECKPOINTS,
826 db::TABLE_IRONWOOD_TREE_RETAINED_CHECKPOINTS,
827 db::TABLE_IRONWOOD_TREE_SHARDS,
828 db::TABLE_NULLIFIER_MAP,
829 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_CROSSING_VALUES,
830 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_PREP_DIRECT_FUNDING,
831 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_PREP_INPUTS,
832 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_PREP_OUTPUTS,
833 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_SPEND_NULLIFIERS,
834 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_TRANSACTION_DEPS,
835 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_TRANSACTIONS,
836 db::TABLE_ORCHARD_IRONWOOD_MIGRATIONS,
837 db::TABLE_ORCHARD_RECEIVED_NOTE_SPENDS,
838 db::TABLE_ORCHARD_RECEIVED_NOTES,
839 db::TABLE_ORCHARD_TREE_CAP,
840 db::TABLE_ORCHARD_TREE_CHECKPOINT_MARKS_REMOVED,
841 db::TABLE_ORCHARD_TREE_CHECKPOINTS,
842 db::TABLE_ORCHARD_TREE_RETAINED_CHECKPOINTS,
843 db::TABLE_ORCHARD_TREE_SHARDS,
844 db::TABLE_SAPLING_RECEIVED_NOTE_SPENDS,
845 db::TABLE_SAPLING_RECEIVED_NOTES,
846 db::TABLE_SAPLING_TREE_CAP,
847 db::TABLE_SAPLING_TREE_CHECKPOINT_MARKS_REMOVED,
848 db::TABLE_SAPLING_TREE_CHECKPOINTS,
849 db::TABLE_SAPLING_TREE_RETAINED_CHECKPOINTS,
850 db::TABLE_SAPLING_TREE_SHARDS,
851 db::TABLE_SCAN_QUEUE,
852 db::TABLE_SCHEMERZ_MIGRATIONS,
853 db::TABLE_SENT_NOTES,
854 db::TABLE_SQLITE_SEQUENCE,
855 db::TABLE_TRANSACTIONS,
856 db::TABLE_TRANSPARENT_RECEIVED_OUTPUT_SPENDS,
857 db::TABLE_TRANSPARENT_RECEIVED_OUTPUTS,
858 db::TABLE_TRANSPARENT_SPEND_MAP,
859 db::TABLE_TRANSPARENT_SPEND_SEARCH_QUEUE,
860 db::TABLE_TX_LOCATOR_MAP,
861 db::TABLE_TX_RETRIEVAL_QUEUE,
862 ];
863
864 let rows = describe_tables(&st.wallet().db().conn).unwrap();
865 assert_eq!(rows.len(), expected_tables.len());
866 for (actual, expected) in rows.iter().zip(expected_tables.iter()) {
867 assert_eq!(normalize(actual), normalize(expected));
868 }
869
870 let expected_indices = vec![
871 db::INDEX_ACCOUNTS_ORCHARD_IVK,
872 db::INDEX_ACCOUNTS_P2PKH_IVK,
873 db::INDEX_ACCOUNTS_P2SH_IVK,
874 db::INDEX_ACCOUNTS_SAPLING_IVK,
875 db::INDEX_ACCOUNTS_UFVK,
876 db::INDEX_ACCOUNTS_UIVK,
877 db::INDEX_ACCOUNTS_UUID,
878 db::INDEX_HD_ACCOUNT,
879 db::INDEX_ADDRESSES_ACCOUNTS,
880 db::INDEX_ADDRESSES_CACHED_TRANSPARENT_RECEIVER_ADDRESS,
881 db::INDEX_ADDRESSES_INDICES,
882 db::INDEX_ADDRESSES_PUBKEYS,
883 db::INDEX_ADDRESSES_T_INDICES,
884 db::INDEX_IRONWOOD_RNS_NOTE,
885 db::INDEX_IRONWOOD_RNS_TX,
886 db::INDEX_IRONWOOD_RECEIVED_NOTES_ACCOUNT,
887 db::INDEX_IRONWOOD_RECEIVED_NOTES_ADDRESS,
888 db::INDEX_IRONWOOD_RECEIVED_NOTES_TX,
889 db::INDEX_IRONWOOD_RECEIVED_NOTES_WITNESS_STABILIZED,
890 db::INDEX_NF_MAP_LOCATOR_IDX,
891 db::INDEX_ORCHARD_IRONWOOD_MIGRATION_TX_DUE,
892 db::INDEX_ORCHARD_IRONWOOD_MIGRATIONS_ACCOUNT,
893 db::INDEX_ORCHARD_RNS_NOTE,
894 db::INDEX_ORCHARD_RNS_TX,
895 db::INDEX_ORCHARD_RECEIVED_NOTES_ACCOUNT,
896 db::INDEX_ORCHARD_RECEIVED_NOTES_ADDRESS,
897 db::INDEX_ORCHARD_RECEIVED_NOTES_TX,
898 db::INDEX_ORCHARD_RECEIVED_NOTES_WITNESS_STABILIZED,
899 db::INDEX_SAPLING_RNS_NOTE,
900 db::INDEX_SAPLING_RNS_TX,
901 db::INDEX_SAPLING_RECEIVED_NOTES_ACCOUNT,
902 db::INDEX_SAPLING_RECEIVED_NOTES_ADDRESS,
903 db::INDEX_SAPLING_RECEIVED_NOTES_TX,
904 db::INDEX_SAPLING_RECEIVED_NOTES_WITNESS_STABILIZED,
905 db::INDEX_SENT_NOTES_FROM_ACCOUNT,
906 db::INDEX_SENT_NOTES_TO_ACCOUNT,
907 db::INDEX_SENT_NOTES_TX,
908 db::INDEX_TRANSPARENT_ROS_OUTPUT,
909 db::INDEX_TRANSPARENT_ROS_TX,
910 db::INDEX_TRANSPARENT_RECEIVED_OUTPUTS_ACCOUNT,
911 db::INDEX_TRANSPARENT_RECEIVED_OUTPUTS_ADDRESS,
912 db::INDEX_TRANSPARENT_RECEIVED_OUTPUTS_TX,
913 db::INDEX_TRANSPARENT_RECEIVED_OUTPUTS_VALUE_ZAT,
914 db::INDEX_TRANSPARENT_SPEND_MAP_TX,
915 db::INDEX_TRANSPARENT_SPEND_SEARCH_TX,
916 db::INDEX_TX_RETIREVAL_QUEUE_DEPENDENT_TX,
917 ];
918 let mut indices_query = st
919 .wallet()
920 .db()
921 .conn
922 .prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND sql != '' ORDER BY tbl_name, name")
923 .unwrap();
924 let mut rows = indices_query.query([]).unwrap();
925 let mut expected_idx = 0;
926 while let Some(row) = rows.next().unwrap() {
927 let actual: String = row.get(0).unwrap();
928 assert_eq!(
929 normalize(&actual),
930 normalize(expected_indices[expected_idx])
931 );
932 expected_idx += 1;
933 }
934
935 let expected_views = vec![
936 db::VIEW_ADDRESS_FIRST_USE.to_owned(),
937 db::VIEW_ADDRESS_USES.to_owned(),
938 db::view_ironwood_shard_scan_ranges(st.network()),
939 db::view_ironwood_shard_unscanned_ranges(),
940 db::VIEW_IRONWOOD_SHARDS_SCAN_STATE.to_owned(),
941 db::view_orchard_shard_scan_ranges(st.network()),
942 db::view_orchard_shard_unscanned_ranges(),
943 db::VIEW_ORCHARD_SHARDS_SCAN_STATE.to_owned(),
944 db::VIEW_RECEIVED_OUTPUT_SPENDS.to_owned(),
945 db::VIEW_RECEIVED_OUTPUTS.to_owned(),
946 db::view_sapling_shard_scan_ranges(st.network()),
947 db::view_sapling_shard_unscanned_ranges(),
948 db::VIEW_SAPLING_SHARDS_SCAN_STATE.to_owned(),
949 db::VIEW_TRANSACTIONS.to_owned(),
950 db::VIEW_TX_OUTPUTS.to_owned(),
951 ];
952
953 let mut views_query = st
954 .wallet()
955 .db()
956 .conn
957 .prepare("SELECT sql FROM sqlite_schema WHERE type = 'view' ORDER BY tbl_name")
958 .unwrap();
959 let mut rows = views_query.query([]).unwrap();
960 let mut expected_idx = 0;
961 while let Some(row) = rows.next().unwrap() {
962 let actual: String = row.get(0).unwrap();
963 assert_eq!(normalize(&actual), normalize(&expected_views[expected_idx]));
964 expected_idx += 1;
965 }
966 }
967
968 #[test]
980 fn canonical_pool_migration_ddl_matches_the_migration_path() {
981 let conn = Connection::open_in_memory().unwrap();
982 crate::wallet::db::init_orchard_ironwood_migration_tables(&conn).unwrap();
983
984 let expected = [
985 (
986 "orchard_ironwood_migrations",
987 db::TABLE_ORCHARD_IRONWOOD_MIGRATIONS,
988 ),
989 (
990 "orchard_ironwood_migration_crossing_values",
991 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_CROSSING_VALUES,
992 ),
993 (
994 "orchard_ironwood_migration_prep_inputs",
995 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_PREP_INPUTS,
996 ),
997 (
998 "orchard_ironwood_migration_prep_outputs",
999 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_PREP_OUTPUTS,
1000 ),
1001 (
1002 "orchard_ironwood_migration_prep_direct_funding",
1003 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_PREP_DIRECT_FUNDING,
1004 ),
1005 (
1006 "orchard_ironwood_migration_transactions",
1007 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_TRANSACTIONS,
1008 ),
1009 (
1010 "orchard_ironwood_migration_transaction_deps",
1011 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_TRANSACTION_DEPS,
1012 ),
1013 (
1014 "orchard_ironwood_migration_spend_nullifiers",
1015 db::TABLE_ORCHARD_IRONWOOD_MIGRATION_SPEND_NULLIFIERS,
1016 ),
1017 (
1018 "idx_orchard_ironwood_migration_tx_due",
1019 db::INDEX_ORCHARD_IRONWOOD_MIGRATION_TX_DUE,
1020 ),
1021 (
1022 "idx_orchard_ironwood_migrations_account",
1023 db::INDEX_ORCHARD_IRONWOOD_MIGRATIONS_ACCOUNT,
1024 ),
1025 ];
1026
1027 let mut stmt = conn
1028 .prepare("SELECT sql FROM sqlite_master WHERE name = ? AND sql IS NOT NULL")
1029 .unwrap();
1030 for (name, expected) in expected {
1031 let actual: String = stmt
1032 .query_row([name], |row| row.get(0))
1033 .unwrap_or_else(|e| panic!("the canonical DDL creates {name}: {e}"));
1034 assert_eq!(normalize_sql(&actual), normalize_sql(expected));
1035 }
1036 }
1037
1038 #[test]
1039 fn external_schema_prefix_unused() {
1040 let st = TestBuilder::new()
1041 .with_data_store_factory(TestDbFactory::default())
1042 .build();
1043
1044 let mut names_query = st
1045 .wallet()
1046 .db()
1047 .conn
1048 .prepare("SELECT tbl_name FROM sqlite_schema")
1049 .unwrap();
1050 let mut rows = names_query.query([]).unwrap();
1051 while let Some(row) = rows.next().unwrap() {
1052 let name: String = row.get(0).unwrap();
1053 assert!(!name.starts_with("ext_"));
1054 }
1055 }
1056
1057 #[test]
1058 fn init_migrate_from_0_3_0() {
1059 fn init_0_3_0<P: consensus::Parameters, CL: Clock + Clone, R: Rng + Clone>(
1060 wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
1061 extfvk: &ExtendedFullViewingKey,
1062 account: AccountId,
1063 ) -> Result<(), rusqlite::Error> {
1064 wdb.conn.execute(
1065 "CREATE TABLE accounts (
1066 account INTEGER PRIMARY KEY,
1067 extfvk TEXT NOT NULL,
1068 address TEXT NOT NULL
1069 )",
1070 [],
1071 )?;
1072 wdb.conn.execute(
1073 "CREATE TABLE blocks (
1074 height INTEGER PRIMARY KEY,
1075 hash BLOB NOT NULL,
1076 time INTEGER NOT NULL,
1077 sapling_tree BLOB NOT NULL
1078 )",
1079 [],
1080 )?;
1081 wdb.conn.execute(
1082 "CREATE TABLE transactions (
1083 id_tx INTEGER PRIMARY KEY,
1084 txid BLOB NOT NULL UNIQUE,
1085 created TEXT,
1086 block INTEGER,
1087 tx_index INTEGER,
1088 expiry_height INTEGER,
1089 raw BLOB,
1090 FOREIGN KEY (block) REFERENCES blocks(height)
1091 )",
1092 [],
1093 )?;
1094 wdb.conn.execute(
1095 "CREATE TABLE received_notes (
1096 id_note INTEGER PRIMARY KEY,
1097 tx INTEGER NOT NULL,
1098 output_index INTEGER NOT NULL,
1099 account INTEGER NOT NULL,
1100 diversifier BLOB NOT NULL,
1101 value INTEGER NOT NULL,
1102 rcm BLOB NOT NULL,
1103 nf BLOB NOT NULL UNIQUE,
1104 is_change INTEGER NOT NULL,
1105 memo BLOB,
1106 spent INTEGER,
1107 FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1108 FOREIGN KEY (account) REFERENCES accounts(account),
1109 FOREIGN KEY (spent) REFERENCES transactions(id_tx),
1110 CONSTRAINT tx_output UNIQUE (tx, output_index)
1111 )",
1112 [],
1113 )?;
1114 wdb.conn.execute(
1115 "CREATE TABLE sapling_witnesses (
1116 id_witness INTEGER PRIMARY KEY,
1117 note INTEGER NOT NULL,
1118 block INTEGER NOT NULL,
1119 witness BLOB NOT NULL,
1120 FOREIGN KEY (note) REFERENCES received_notes(id_note),
1121 FOREIGN KEY (block) REFERENCES blocks(height),
1122 CONSTRAINT witness_height UNIQUE (note, block)
1123 )",
1124 [],
1125 )?;
1126 wdb.conn.execute(
1127 "CREATE TABLE sent_notes (
1128 id_note INTEGER PRIMARY KEY,
1129 tx INTEGER NOT NULL,
1130 output_index INTEGER NOT NULL,
1131 from_account INTEGER NOT NULL,
1132 address TEXT NOT NULL,
1133 value INTEGER NOT NULL,
1134 memo BLOB,
1135 FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1136 FOREIGN KEY (from_account) REFERENCES accounts(account),
1137 CONSTRAINT tx_output UNIQUE (tx, output_index)
1138 )",
1139 [],
1140 )?;
1141
1142 let address = encode_payment_address(
1143 wdb.params.hrp_sapling_payment_address(),
1144 &extfvk.default_address().1,
1145 );
1146 let extfvk = encode_extended_full_viewing_key(
1147 wdb.params.hrp_sapling_extended_full_viewing_key(),
1148 extfvk,
1149 );
1150 wdb.conn.execute(
1151 "INSERT INTO accounts (account, extfvk, address)
1152 VALUES (?, ?, ?)",
1153 [
1154 u32::from(account).to_sql()?,
1155 extfvk.to_sql()?,
1156 address.to_sql()?,
1157 ],
1158 )?;
1159
1160 Ok(())
1161 }
1162
1163 let data_file = NamedTempFile::new().unwrap();
1164 let mut db_data = WalletDb::for_path(
1165 data_file.path(),
1166 Network::TestNetwork,
1167 test_clock(),
1168 test_rng(),
1169 )
1170 .unwrap();
1171
1172 let seed = [0xab; 32];
1173 let account = AccountId::ZERO;
1174 let secret_key = sapling::spending_key(&seed, db_data.params.coin_type(), account);
1175 #[allow(deprecated)]
1176 let extfvk = secret_key.to_extended_full_viewing_key();
1177
1178 init_0_3_0(&mut db_data, &extfvk, account).unwrap();
1179 assert_matches!(
1180 init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1181 Ok(_)
1182 );
1183 }
1184
1185 #[test]
1186 fn init_migrate_from_autoshielding_poc() {
1187 fn init_autoshielding<P: consensus::Parameters, CL, R>(
1188 wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
1189 extfvk: &ExtendedFullViewingKey,
1190 account: AccountId,
1191 ) -> Result<(), rusqlite::Error> {
1192 wdb.conn.execute(
1193 "CREATE TABLE accounts (
1194 account INTEGER PRIMARY KEY,
1195 extfvk TEXT NOT NULL,
1196 address TEXT NOT NULL,
1197 transparent_address TEXT NOT NULL
1198 )",
1199 [],
1200 )?;
1201 wdb.conn.execute(
1202 "CREATE TABLE blocks (
1203 height INTEGER PRIMARY KEY,
1204 hash BLOB NOT NULL,
1205 time INTEGER NOT NULL,
1206 sapling_tree BLOB NOT NULL
1207 )",
1208 [],
1209 )?;
1210 wdb.conn.execute(
1211 "CREATE TABLE transactions (
1212 id_tx INTEGER PRIMARY KEY,
1213 txid BLOB NOT NULL UNIQUE,
1214 created TEXT,
1215 block INTEGER,
1216 tx_index INTEGER,
1217 expiry_height INTEGER,
1218 raw BLOB,
1219 FOREIGN KEY (block) REFERENCES blocks(height)
1220 )",
1221 [],
1222 )?;
1223 wdb.conn.execute(
1224 "CREATE TABLE received_notes (
1225 id_note INTEGER PRIMARY KEY,
1226 tx INTEGER NOT NULL,
1227 output_index INTEGER NOT NULL,
1228 account INTEGER NOT NULL,
1229 diversifier BLOB NOT NULL,
1230 value INTEGER NOT NULL,
1231 rcm BLOB NOT NULL,
1232 nf BLOB NOT NULL UNIQUE,
1233 is_change INTEGER NOT NULL,
1234 memo BLOB,
1235 spent INTEGER,
1236 FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1237 FOREIGN KEY (account) REFERENCES accounts(account),
1238 FOREIGN KEY (spent) REFERENCES transactions(id_tx),
1239 CONSTRAINT tx_output UNIQUE (tx, output_index)
1240 )",
1241 [],
1242 )?;
1243 wdb.conn.execute(
1244 "CREATE TABLE sapling_witnesses (
1245 id_witness INTEGER PRIMARY KEY,
1246 note INTEGER NOT NULL,
1247 block INTEGER NOT NULL,
1248 witness BLOB NOT NULL,
1249 FOREIGN KEY (note) REFERENCES received_notes(id_note),
1250 FOREIGN KEY (block) REFERENCES blocks(height),
1251 CONSTRAINT witness_height UNIQUE (note, block)
1252 )",
1253 [],
1254 )?;
1255 wdb.conn.execute(
1256 "CREATE TABLE sent_notes (
1257 id_note INTEGER PRIMARY KEY,
1258 tx INTEGER NOT NULL,
1259 output_index INTEGER NOT NULL,
1260 from_account INTEGER NOT NULL,
1261 address TEXT NOT NULL,
1262 value INTEGER NOT NULL,
1263 memo BLOB,
1264 FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1265 FOREIGN KEY (from_account) REFERENCES accounts(account),
1266 CONSTRAINT tx_output UNIQUE (tx, output_index)
1267 )",
1268 [],
1269 )?;
1270 wdb.conn.execute(
1271 "CREATE TABLE utxos (
1272 id_utxo INTEGER PRIMARY KEY,
1273 address TEXT NOT NULL,
1274 prevout_txid BLOB NOT NULL,
1275 prevout_idx INTEGER NOT NULL,
1276 script BLOB NOT NULL,
1277 value_zat INTEGER NOT NULL,
1278 height INTEGER NOT NULL,
1279 spent_in_tx INTEGER,
1280 FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx),
1281 CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx)
1282 )",
1283 [],
1284 )?;
1285
1286 let address = encode_payment_address(
1287 wdb.params.hrp_sapling_payment_address(),
1288 &extfvk.default_address().1,
1289 );
1290 let extfvk = encode_extended_full_viewing_key(
1291 wdb.params.hrp_sapling_extended_full_viewing_key(),
1292 extfvk,
1293 );
1294 wdb.conn.execute(
1295 "INSERT INTO accounts (account, extfvk, address, transparent_address)
1296 VALUES (?, ?, ?, '')",
1297 [
1298 u32::from(account).to_sql()?,
1299 extfvk.to_sql()?,
1300 address.to_sql()?,
1301 ],
1302 )?;
1303
1304 wdb.conn.execute(
1306 "INSERT INTO blocks (height, hash, time, sapling_tree) \
1307 VALUES (0, x'0000000000000000000000000000000000000000000000000000000000000000', 0, x'000000')",
1308 [],
1309 )?;
1310
1311 let tx = TransactionData::from_parts(
1312 TxVersion::V4,
1313 BranchId::Canopy,
1314 0,
1315 BlockHeight::from(0),
1316 #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
1317 Zatoshis::ZERO,
1318 None,
1319 None,
1320 None,
1321 None,
1322 )
1323 .freeze()
1324 .unwrap();
1325
1326 let mut tx_bytes = vec![];
1327 tx.write(&mut tx_bytes).unwrap();
1328 wdb.conn.execute(
1329 "INSERT INTO transactions (block, id_tx, txid, raw) VALUES (0, 0, :txid, :tx_bytes)",
1330 named_params![
1331 ":txid": tx.txid().as_ref(),
1332 ":tx_bytes": &tx_bytes[..]
1333 ],
1334 )?;
1335 wdb.conn.execute(
1336 "INSERT INTO sent_notes (tx, output_index, from_account, address, value)
1337 VALUES (0, 0, ?, ?, 0)",
1338 [u32::from(account).to_sql()?, address.to_sql()?],
1339 )?;
1340
1341 Ok(())
1342 }
1343
1344 let data_file = NamedTempFile::new().unwrap();
1345 let mut db_data = WalletDb::for_path(
1346 data_file.path(),
1347 Network::TestNetwork,
1348 test_clock(),
1349 test_rng(),
1350 )
1351 .unwrap();
1352
1353 let seed = [0xab; 32];
1354 let account = AccountId::ZERO;
1355 let secret_key = sapling::spending_key(&seed, db_data.params.coin_type(), account);
1356 #[allow(deprecated)]
1357 let extfvk = secret_key.to_extended_full_viewing_key();
1358
1359 init_autoshielding(&mut db_data, &extfvk, account).unwrap();
1360 assert_matches!(
1361 init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1362 Ok(_)
1363 );
1364 }
1365
1366 #[test]
1367 fn init_migrate_from_main_pre_migrations() {
1368 fn init_main<P: consensus::Parameters, CL, R>(
1369 wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
1370 ufvk: &UnifiedFullViewingKey,
1371 account: AccountId,
1372 ) -> Result<(), rusqlite::Error> {
1373 wdb.conn.execute(
1374 "CREATE TABLE accounts (
1375 account INTEGER PRIMARY KEY,
1376 ufvk TEXT,
1377 address TEXT,
1378 transparent_address TEXT
1379 )",
1380 [],
1381 )?;
1382 wdb.conn.execute(
1383 "CREATE TABLE blocks (
1384 height INTEGER PRIMARY KEY,
1385 hash BLOB NOT NULL,
1386 time INTEGER NOT NULL,
1387 sapling_tree BLOB NOT NULL
1388 )",
1389 [],
1390 )?;
1391 wdb.conn.execute(
1392 "CREATE TABLE transactions (
1393 id_tx INTEGER PRIMARY KEY,
1394 txid BLOB NOT NULL UNIQUE,
1395 created TEXT,
1396 block INTEGER,
1397 tx_index INTEGER,
1398 expiry_height INTEGER,
1399 raw BLOB,
1400 FOREIGN KEY (block) REFERENCES blocks(height)
1401 )",
1402 [],
1403 )?;
1404 wdb.conn.execute(
1405 "CREATE TABLE received_notes (
1406 id_note INTEGER PRIMARY KEY,
1407 tx INTEGER NOT NULL,
1408 output_index INTEGER NOT NULL,
1409 account INTEGER NOT NULL,
1410 diversifier BLOB NOT NULL,
1411 value INTEGER NOT NULL,
1412 rcm BLOB NOT NULL,
1413 nf BLOB NOT NULL UNIQUE,
1414 is_change INTEGER NOT NULL,
1415 memo BLOB,
1416 spent INTEGER,
1417 FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1418 FOREIGN KEY (account) REFERENCES accounts(account),
1419 FOREIGN KEY (spent) REFERENCES transactions(id_tx),
1420 CONSTRAINT tx_output UNIQUE (tx, output_index)
1421 )",
1422 [],
1423 )?;
1424 wdb.conn.execute(
1425 "CREATE TABLE sapling_witnesses (
1426 id_witness INTEGER PRIMARY KEY,
1427 note INTEGER NOT NULL,
1428 block INTEGER NOT NULL,
1429 witness BLOB NOT NULL,
1430 FOREIGN KEY (note) REFERENCES received_notes(id_note),
1431 FOREIGN KEY (block) REFERENCES blocks(height),
1432 CONSTRAINT witness_height UNIQUE (note, block)
1433 )",
1434 [],
1435 )?;
1436 wdb.conn.execute(
1437 "CREATE TABLE sent_notes (
1438 id_note INTEGER PRIMARY KEY,
1439 tx INTEGER NOT NULL,
1440 output_pool INTEGER NOT NULL,
1441 output_index INTEGER NOT NULL,
1442 from_account INTEGER NOT NULL,
1443 address TEXT NOT NULL,
1444 value INTEGER NOT NULL,
1445 memo BLOB,
1446 FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1447 FOREIGN KEY (from_account) REFERENCES accounts(account),
1448 CONSTRAINT tx_output UNIQUE (tx, output_pool, output_index)
1449 )",
1450 [],
1451 )?;
1452 wdb.conn.execute(
1453 "CREATE TABLE utxos (
1454 id_utxo INTEGER PRIMARY KEY,
1455 address TEXT NOT NULL,
1456 prevout_txid BLOB NOT NULL,
1457 prevout_idx INTEGER NOT NULL,
1458 script BLOB NOT NULL,
1459 value_zat INTEGER NOT NULL,
1460 height INTEGER NOT NULL,
1461 spent_in_tx INTEGER,
1462 FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx),
1463 CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx)
1464 )",
1465 [],
1466 )?;
1467
1468 let ufvk_str = ufvk.encode(&wdb.params);
1469
1470 let ua_request = UnifiedAddressRequest::unsafe_custom(Omit, Require, UA_TRANSPARENT);
1473 let address_str = Address::Unified(
1474 ufvk.default_address(ua_request)
1475 .expect("A valid default address exists for the UFVK")
1476 .0,
1477 )
1478 .encode(&wdb.params);
1479 wdb.conn.execute(
1480 "INSERT INTO accounts (account, ufvk, address, transparent_address)
1481 VALUES (?, ?, ?, '')",
1482 [
1483 u32::from(account).to_sql()?,
1484 ufvk_str.to_sql()?,
1485 address_str.to_sql()?,
1486 ],
1487 )?;
1488
1489 #[cfg(feature = "transparent-inputs")]
1491 {
1492 let taddr = Address::Transparent(
1493 *ufvk
1494 .default_address(ua_request)
1495 .expect("A valid default address exists for the UFVK")
1496 .0
1497 .transparent()
1498 .unwrap(),
1499 )
1500 .encode(&wdb.params);
1501 wdb.conn.execute(
1502 "INSERT INTO blocks (height, hash, time, sapling_tree) \
1503 VALUES (0, x'0000000000000000000000000000000000000000000000000000000000000000', 0, x'000000')",
1504 [],
1505 )?;
1506 wdb.conn.execute(
1507 "INSERT INTO transactions (block, id_tx, txid) VALUES (0, 0, '')",
1508 [],
1509 )?;
1510 wdb.conn.execute(
1511 "INSERT INTO sent_notes (tx, output_pool, output_index, from_account, address, value)
1512 VALUES (0, ?, 0, ?, ?, 0)",
1513 [pool_code(PoolType::TRANSPARENT).to_sql()?, u32::from(account).to_sql()?, taddr.to_sql()?])?;
1514 }
1515
1516 Ok(())
1517 }
1518
1519 let data_file = NamedTempFile::new().unwrap();
1520 let mut db_data = WalletDb::for_path(
1521 data_file.path(),
1522 Network::TestNetwork,
1523 test_clock(),
1524 test_rng(),
1525 )
1526 .unwrap();
1527
1528 let seed = [0xab; 32];
1529 let account = AccountId::ZERO;
1530 let secret_key = UnifiedSpendingKey::from_seed(&db_data.params, &seed, account).unwrap();
1531
1532 init_main(
1533 &mut db_data,
1534 &secret_key.to_unified_full_viewing_key(),
1535 account,
1536 )
1537 .unwrap();
1538 assert_matches!(
1539 init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1540 Ok(_)
1541 );
1542 }
1543
1544 #[test]
1545 #[cfg(feature = "transparent-inputs")]
1546 fn account_produces_expected_ua_sequence() {
1547 let network = Network::MainNetwork;
1548 let data_file = NamedTempFile::new().unwrap();
1549 let mut db_data =
1550 WalletDb::for_path(data_file.path(), network, test_clock(), test_rng()).unwrap();
1551 assert_matches!(init_wallet_db(&mut db_data, None), Ok(_));
1552
1553 let seed = test_vectors::UNIFIED[0].root_seed;
1555 let other_seed = [7; 32];
1556 assert_matches!(
1557 init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1558 Ok(())
1559 );
1560 assert_matches!(
1561 init_wallet_db(&mut db_data, Some(Secret::new(other_seed.to_vec()))),
1562 Ok(())
1563 );
1564
1565 let birthday = AccountBirthday::from_sapling_activation(&network, BlockHash([0; 32]));
1566 let (account_id, _usk) = db_data
1567 .create_account("", &Secret::new(seed.to_vec()), &birthday, None)
1568 .unwrap();
1569
1570 db_data.update_chain_tip(birthday.height()).unwrap();
1573
1574 assert_matches!(
1575 db_data.get_account(account_id),
1576 Ok(Some(account)) if matches!(
1577 &account.kind,
1578 AccountSource::Derived{derivation, ..} if derivation.account_index() == zip32::AccountId::ZERO,
1579 )
1580 );
1581
1582 assert_matches!(
1584 init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1585 Ok(())
1586 );
1587 assert_matches!(
1588 init_wallet_db(&mut db_data, Some(Secret::new(other_seed.to_vec()))),
1589 Err(schemerz::MigratorError::Adapter(
1590 WalletMigrationError::SeedNotRelevant
1591 ))
1592 );
1593
1594 for tv in &test_vectors::UNIFIED[..3] {
1595 if let Some(Address::Unified(tvua)) =
1596 Address::decode(&Network::MainNetwork, tv.unified_addr)
1597 {
1598 let ua_request = UnifiedAddressRequest::unsafe_custom(Omit, Require, Require);
1600
1601 let (ua, di) = wallet::get_last_generated_address_matching(
1602 &db_data.conn,
1603 &db_data.params,
1604 account_id,
1605 if tv.diversifier_index == 0 {
1606 UnifiedAddressRequest::AllAvailableKeys
1607 } else {
1608 ua_request
1609 },
1610 )
1611 .unwrap()
1612 .expect("create_account generated the first address");
1613 assert_eq!(DiversifierIndex::from(tv.diversifier_index), di);
1614 assert_eq!(tvua.transparent(), ua.transparent());
1615 assert_eq!(tvua.sapling(), ua.sapling());
1616 #[cfg(not(feature = "orchard"))]
1617 assert_eq!(tv.unified_addr, ua.encode(&Network::MainNetwork));
1618
1619 db_data
1620 .get_next_available_address(account_id, ua_request)
1621 .unwrap()
1622 .expect("get_next_available_address generated an address");
1623 } else {
1624 panic!(
1625 "{} did not decode to a valid unified address",
1626 tv.unified_addr
1627 );
1628 }
1629 }
1630 }
1631}