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