use crate::database::sqlite::{schema::SCHEMA, FreshDbOptions, SqliteDbError, DB_VERSION}; use std::{convert::TryInto, fs, path, time}; use miniscript::bitcoin::secp256k1; pub const LOOK_AHEAD_LIMIT: u32 = 200; /// Perform a set of modifications to the database inside a single transaction pub fn db_exec(conn: &mut rusqlite::Connection, modifications: F) -> Result<(), rusqlite::Error> where F: FnOnce(&rusqlite::Transaction) -> rusqlite::Result<()>, { let tx = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?; modifications(&tx)?; tx.commit() } /// Internal helper for queries boilerplate pub fn db_tx_query( tx: &rusqlite::Transaction, stmt_str: &str, params: P, f: F, ) -> Result, rusqlite::Error> where P: IntoIterator + rusqlite::Params, P::Item: rusqlite::ToSql, F: FnMut(&rusqlite::Row<'_>) -> rusqlite::Result, { tx.prepare(stmt_str)? .query_map(params, f)? .collect::>>() } /// Internal helper for queries boilerplate pub fn db_query( conn: &mut rusqlite::Connection, stmt_str: &str, params: P, f: F, ) -> Result, rusqlite::Error> where P: IntoIterator + rusqlite::Params, P::Item: rusqlite::ToSql, F: FnMut(&rusqlite::Row<'_>) -> rusqlite::Result, { conn.prepare(stmt_str)? .query_map(params, f)? .collect::>>() } // Sqlite supports up to i64, thus rusqlite prevents us from inserting u64's. // We use this to panic rather than inserting a truncated integer into the database (as we'd have // done by using `n as u32`). fn timestamp_to_u32(n: u64) -> u32 { n.try_into() .expect("Is this the year 2106 yet? Misconfigured system clock.") } // Create the db file with RW permissions only for the user pub fn create_db_file(db_path: &path::Path) -> Result<(), std::io::Error> { let mut options = fs::OpenOptions::new(); let options = options.read(true).write(true).create_new(true); #[cfg(unix)] return { use std::os::unix::fs::OpenOptionsExt; options.mode(0o600).open(db_path)?; Ok(()) }; #[cfg(not(unix))] return { // TODO: permissions for Windows... options.open(db_path)?; Ok(()) }; } pub fn create_fresh_db( db_path: &path::Path, options: FreshDbOptions, secp: &secp256k1::Secp256k1, ) -> Result<(), SqliteDbError> { create_db_file(db_path)?; let timestamp = time::SystemTime::now() .duration_since(time::UNIX_EPOCH) .map(|dur| timestamp_to_u32(dur.as_secs())) .expect("System clock went backward the epoch?"); // Fill the initial addresses. On a fresh database, the deposit_derivation_index is // necessarily 0. let mut query = String::with_capacity(100 * LOOK_AHEAD_LIMIT as usize); for index in 0..LOOK_AHEAD_LIMIT { // TODO: have this as a helper in descriptors.rs let address = options .main_descriptor .derive_receive(index.into(), secp) .address(options.bitcoind_network); query += &format!( "INSERT INTO addresses (address, derivation_index) VALUES (\"{}\", {});\n", address, index ); } let mut conn = rusqlite::Connection::open(db_path)?; db_exec(&mut conn, |tx| { tx.execute_batch(SCHEMA)?; tx.execute( "INSERT INTO version (version) VALUES (?1)", rusqlite::params![DB_VERSION], )?; tx.execute( "INSERT INTO tip (network, blockheight, blockhash) VALUES (?1, NULL, NULL)", rusqlite::params![options.bitcoind_network.to_string()], )?; tx.execute( "INSERT INTO wallets (timestamp, main_descriptor, deposit_derivation_index) \ VALUES (?1, ?2, ?3)", rusqlite::params![timestamp, options.main_descriptor.to_string(), 0,], )?; tx.execute_batch(&query)?; Ok(()) })?; Ok(()) }