zcash_client_sqlite/testing/
db.rs1use ambassador::Delegate;
5use rusqlite::Connection;
6use std::num::NonZeroU32;
7use std::time::Duration;
8use std::{
9 collections::{HashMap, HashSet},
10 time::SystemTime,
11};
12use uuid::Uuid;
13
14use tempfile::NamedTempFile;
15
16use rusqlite::{self};
17use secrecy::SecretVec;
18use shardtree::{ShardTree, error::ShardTreeError};
19
20use zcash_client_backend::{
21 data_api::{
22 TargetValue,
23 anchor_retention::AnchorRetentionInterval,
24 chain::{ChainState, CommitmentTreeRoot},
25 error::{LockError, RewindError},
26 scanning::{ScanPriority, ScanRange},
27 testing::{DataStoreFactory, Reset, TestRng, TestState},
28 wallet::{ConfirmationsPolicy, TargetHeight, input_selection::LockFilter},
29 *,
30 },
31 wallet::{LockOwner, Note, NoteId, OutputRef, ReceivedNote, WalletTransparentOutput},
32};
33use zcash_keys::{
34 address::UnifiedAddress,
35 keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
36};
37use zcash_primitives::{
38 block::BlockHash,
39 transaction::{Transaction, TxId},
40};
41use zcash_protocol::{
42 ShieldedPool, consensus, consensus::BlockHeight, local_consensus::LocalNetwork, memo::Memo,
43 value::Zatoshis,
44};
45use zip32::DiversifierIndex;
46
47use crate::{
48 AccountUuid, WalletDb, error::SqliteClientError, util::testing::FixedClock,
49 wallet::init::WalletMigrator,
50};
51
52#[cfg(feature = "transparent-inputs")]
53use {
54 crate::TransparentAddressMetadata,
55 ::transparent::{address::TransparentAddress, bundle::OutPoint, keys::NonHardenedChildIndex},
56 core::ops::Range,
57 zcash_client_backend::fees::StandardFeeRule,
58 zcash_keys::keys::transparent::gap_limits::GapLimits,
59};
60
61const TEST_EPOCH_SECONDS_OFFSET: Duration = Duration::from_secs(1740441600);
63
64pub(crate) fn test_clock() -> FixedClock {
65 FixedClock::new(SystemTime::UNIX_EPOCH + TEST_EPOCH_SECONDS_OFFSET)
66}
67
68pub(crate) fn test_rng() -> TestRng {
69 TestRng::seed_from_u64(0)
70}
71
72#[allow(clippy::duplicated_attributes, reason = "False positive")]
75#[derive(Delegate)]
76#[delegate(InputSource, target = "wallet_db")]
77#[delegate(WalletRead, target = "wallet_db")]
78#[delegate(WalletTest, target = "wallet_db")]
79#[delegate(OutputLockStore, target = "wallet_db")]
80#[delegate(WalletWrite, target = "wallet_db")]
81#[delegate(WalletCommitmentTrees, target = "wallet_db")]
82pub struct TestDb {
83 wallet_db: WalletDb<Connection, LocalNetwork, FixedClock, TestRng>,
84 data_file: Option<NamedTempFile>,
85}
86
87impl TestDb {
88 fn from_parts(
89 wallet_db: WalletDb<Connection, LocalNetwork, FixedClock, TestRng>,
90 data_file: Option<NamedTempFile>,
91 ) -> Self {
92 Self {
93 wallet_db,
94 data_file,
95 }
96 }
97
98 pub fn db(&self) -> &WalletDb<Connection, LocalNetwork, FixedClock, TestRng> {
100 &self.wallet_db
101 }
102
103 pub fn db_mut(&mut self) -> &mut WalletDb<Connection, LocalNetwork, FixedClock, TestRng> {
105 &mut self.wallet_db
106 }
107
108 pub fn conn(&self) -> &Connection {
111 &self.wallet_db.conn
112 }
113
114 pub fn conn_mut(&mut self) -> &mut Connection {
116 &mut self.wallet_db.conn
117 }
118
119 pub(crate) fn take_data_file(self) -> Option<NamedTempFile> {
120 self.data_file
121 }
122
123 #[allow(dead_code)]
124 pub(crate) fn data_file_path(&self) -> &std::path::Path {
125 self.data_file
126 .as_ref()
127 .expect("this test requires a file-backed TestDbFactory")
128 .path()
129 }
130
131 #[allow(dead_code)]
140 #[cfg(feature = "unstable")]
141 pub(crate) fn dump_table(&self, name: &'static str) {
142 assert!(name.chars().all(|c| c.is_ascii_alphabetic() || c == '_'));
143 unsafe {
144 run_sqlite3(self.data_file_path(), &format!(r#".dump "{name}""#));
145 }
146 }
147
148 #[allow(dead_code)]
156 #[cfg(feature = "unstable")]
157 pub(crate) unsafe fn run_sqlite3(&self, command: &str) {
158 unsafe { run_sqlite3(self.data_file_path(), command) }
159 }
160}
161
162#[cfg(feature = "unstable")]
163use std::{ffi::OsStr, process::Command};
164
165#[allow(dead_code)]
170#[cfg(feature = "unstable")]
171unsafe fn run_sqlite3<S: AsRef<OsStr>>(db_path: S, command: &str) {
172 let output = Command::new("sqlite3")
173 .arg(db_path)
174 .arg("-safe")
175 .arg("-readonly")
176 .arg(command)
177 .output()
178 .expect("failed to execute sqlite3 process");
179
180 eprintln!(
181 "{}\n------\n{}",
182 command,
183 String::from_utf8_lossy(&output.stdout)
184 );
185 if !output.stderr.is_empty() {
186 eprintln!(
187 "------ stderr:\n{}",
188 String::from_utf8_lossy(&output.stderr)
189 );
190 }
191 eprintln!("------");
192}
193
194#[derive(Default)]
200pub struct TestDbFactory {
201 target_migrations: Option<Vec<Uuid>>,
202 file_backed: bool,
203}
204
205impl TestDbFactory {
206 pub fn file_backed() -> Self {
208 Self {
209 target_migrations: None,
210 file_backed: true,
211 }
212 }
213}
214
215impl DataStoreFactory for TestDbFactory {
216 type Error = ();
217 type AccountId = AccountUuid;
218 type Account = crate::wallet::Account;
219 type DsError = SqliteClientError;
220 type DataStore = TestDb;
221
222 fn new_data_store(
223 &self,
224 network: LocalNetwork,
225 anchor_retention_interval: Option<AnchorRetentionInterval>,
226 #[cfg(feature = "transparent-inputs")] gap_limits: Option<GapLimits>,
227 ) -> Result<Self::DataStore, Self::Error> {
228 let (mut db_data, data_file) = if self.file_backed {
229 let data_file = NamedTempFile::new().unwrap();
230 let db_data =
231 WalletDb::for_path(data_file.path(), network, test_clock(), test_rng()).unwrap();
232 (db_data, Some(data_file))
233 } else {
234 let conn = Connection::open_in_memory().unwrap();
235 rusqlite::vtab::array::load_module(&conn).unwrap();
236 (
237 WalletDb::from_connection(conn, network, test_clock(), test_rng()),
238 None,
239 )
240 };
241 if let Some(interval) = anchor_retention_interval {
242 db_data = db_data.with_anchor_retention_interval(interval);
243 }
244 #[cfg(feature = "transparent-inputs")]
245 if let Some(gap_limits) = gap_limits {
246 db_data = db_data.with_gap_limits(gap_limits);
247 }
248
249 let migrator = WalletMigrator::new();
250 if let Some(migrations) = &self.target_migrations {
251 migrator
252 .init_or_migrate_to(&mut db_data, migrations)
253 .expect("wallet migration succeeds for test setup with target migrations");
254 } else {
255 migrator
256 .init_or_migrate(&mut db_data)
257 .expect("wallet migration succeeds for test setup with default migrations");
258 }
259 Ok(TestDb::from_parts(db_data, data_file))
260 }
261}
262
263impl Reset for TestDb {
264 type Handle = Option<NamedTempFile>;
265
266 fn reset<C>(st: &mut TestState<C, Self, LocalNetwork>) -> Self::Handle {
267 let network = *st.network();
268 let anchor_retention_interval = st.wallet().db().anchor_retention_interval;
269 #[cfg(feature = "transparent-inputs")]
270 let gap_limits = st.wallet().db().gap_limits;
271 let old_db = std::mem::replace(
272 st.wallet_mut(),
273 TestDbFactory::default()
274 .new_data_store(
275 network,
276 Some(anchor_retention_interval),
277 #[cfg(feature = "transparent-inputs")]
278 Some(gap_limits),
279 )
280 .unwrap(),
281 );
282 old_db.take_data_file()
283 }
284}