use crate::chain::Nf; use crate::contact::Contact; use crate::note_selection::{Source, UTXO}; use crate::orchard::{derive_orchard_keys, OrchardKeyBytes, OrchardViewKey}; use crate::prices::Quote; use crate::sapling::SaplingViewKey; use crate::sync::tree::{CTree, TreeCheckpoint}; use crate::taddr::{derive_tkeys, TransparentTxInfo}; use crate::transaction::{GetTransactionDetailRequest, TransactionDetails}; use crate::unified::UnifiedAddressType; use crate::{sync, BlockId, CoinConfig, CompactTxStreamerClient, Hash}; use anyhow::anyhow; use orchard::keys::FullViewingKey; use rusqlite::Error::QueryReturnedNoRows; use rusqlite::{params, Connection, OpenFlags, OptionalExtension, Transaction}; use serde::Serialize; use std::collections::HashMap; use std::convert::TryInto; use std::path::Path; use tonic::transport::Channel; use tonic::Request; use zcash_client_backend::encoding::decode_extended_full_viewing_key; use zcash_params::coin::{get_coin_chain, CoinType}; use zcash_primitives::consensus::{Network, NetworkUpgrade, Parameters}; use zcash_primitives::merkle_tree::IncrementalWitness; use zcash_primitives::sapling::{Diversifier, Node, Note, SaplingIvk}; use zcash_primitives::zip32::ExtendedFullViewingKey; mod backup; pub mod cipher; pub mod data_generated; mod migration; pub mod read; use crate::db::cipher::set_db_passwd; use crate::db::data_generated::fb::SendTemplate; pub use backup::FullEncryptedBackup; #[allow(dead_code)] pub const DEFAULT_DB_PATH: &str = "zec.db"; #[derive(Clone)] pub struct DbAdapterBuilder { pub coin_type: CoinType, pub db_path: String, pub passwd: String, } pub struct DbAdapter { pub coin_type: CoinType, pub connection: Connection, pub db_path: String, } #[derive(Debug)] pub struct ReceivedNote { pub account: u32, pub height: u32, pub output_index: u32, pub diversifier: Vec, pub value: u64, pub rcm: Vec, pub nf: Vec, pub rho: Option>, pub spent: Option, } #[derive(Clone)] pub struct ReceivedNoteShort { pub id: u32, pub account: u32, pub nf: Nf, pub value: u64, } #[derive(Clone)] pub struct SpendableNote { pub id: u32, pub note: Note, pub diversifier: Diversifier, pub witness: IncrementalWitness, } pub struct AccountViewKey { pub fvk: ExtendedFullViewingKey, pub ivk: SaplingIvk, pub viewonly: bool, } pub fn wrap_query_no_rows(name: &'static str) -> impl Fn(rusqlite::Error) -> anyhow::Error { move |err: rusqlite::Error| match err { QueryReturnedNoRows => anyhow::anyhow!("Query {} returned no rows", name), other => anyhow::anyhow!(other.to_string()), } } impl DbAdapterBuilder { pub fn build(&self) -> anyhow::Result { DbAdapter::new(self.coin_type, &self.db_path, &self.passwd) } } impl DbAdapter { pub fn new(coin_type: CoinType, db_path: &str, passwd: &str) -> anyhow::Result { let connection = Connection::open(db_path)?; set_db_passwd(&connection, passwd)?; Ok(DbAdapter { coin_type, connection, db_path: db_path.to_owned(), }) } pub fn migrate_db( network: &Network, db_path: &str, passwd: &str, has_ua: bool, ) -> anyhow::Result<()> { let dir = Path::new(db_path) .parent() .ok_or(anyhow!("Invalid db path"))?; std::fs::create_dir_all(dir)?; let connection = Connection::open_with_flags( db_path, OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE, )?; set_db_passwd(&connection, passwd)?; connection.query_row("PRAGMA journal_mode=wal", [], |_| Ok(()))?; migration::init_db(&connection, network, has_ua)?; Ok(()) } pub async fn migrate_data(&self, coin: u8) -> anyhow::Result<()> { let cc = CoinConfig::get(coin); if cc.chain.has_unified() { let mut client: Option> = None; let mut stmt = self.connection.prepare("select s.height from sapling_tree s LEFT JOIN orchard_tree o ON s.height = o.height WHERE o.height IS NULL")?; let rows = stmt.query_map([], |row| { let height: u32 = row.get(0)?; Ok(height) })?; let mut trees = HashMap::new(); for r in rows { trees.insert(r?, vec![]); } for (height, tree) in trees.iter_mut() { if client.is_none() { client = Some(cc.connect_lwd().await?); } let client = client.as_mut().unwrap(); let tree_state = client .get_tree_state(Request::new(BlockId { height: *height as u64, hash: vec![], })) .await? .into_inner(); let orchard_tree = hex::decode(&tree_state.orchard_tree).unwrap(); tree.extend(orchard_tree); } for (height, tree) in trees.iter() { self.connection.execute( "INSERT INTO orchard_tree(height, tree) VALUES (?1, ?2) ON CONFLICT DO NOTHING", params![height, tree], )?; } } Ok(()) } pub fn begin_transaction(&mut self) -> anyhow::Result { let tx = self.connection.transaction()?; Ok(tx) } pub fn init_db(&mut self) -> anyhow::Result<()> { self.delete_incomplete_scan()?; self.delete_orphan_transactions()?; Ok(()) } pub fn reset_db(&self) -> anyhow::Result<()> { migration::reset_db(&self.connection)?; Ok(()) } pub fn get_account_id(&self, ivk: &str) -> anyhow::Result> { let r = self .connection .query_row( "SELECT id_account FROM accounts WHERE ivk = ?1", params![ivk], |r| { let id: u32 = r.get(0)?; Ok(id) }, ) .optional()?; Ok(r) } pub fn store_account( &self, name: &str, seed: Option<&str>, index: u32, sk: Option<&str>, ivk: &str, address: &str, ) -> anyhow::Result { self.connection.execute( "INSERT INTO accounts(name, seed, aindex, sk, ivk, address) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", params![name, seed, index, sk, ivk, address], )?; let id_account: u32 = self .connection .query_row( "SELECT id_account FROM accounts WHERE ivk = ?1", params![ivk], |row| row.get(0), ) .map_err(wrap_query_no_rows("store_account/id_account"))?; Ok(id_account) } pub fn next_account_id(&self, seed: &str) -> anyhow::Result { let index = self.connection.query_row( "SELECT MAX(aindex) FROM accounts WHERE seed = ?1", [seed], |row| { let aindex: Option = row.get(0)?; Ok(aindex.unwrap_or(-1)) }, )? + 1; Ok(index as u32) } pub fn store_transparent_key( &self, id_account: u32, sk: &str, addr: &str, ) -> anyhow::Result<()> { self.connection.execute( "UPDATE taddrs SET sk = ?1, address = ?2 WHERE account = ?3", params![sk, addr, id_account], )?; Ok(()) } pub fn convert_to_watchonly(&self, id_account: u32) -> anyhow::Result<()> { self.connection.execute( "UPDATE accounts SET seed = NULL, sk = NULL WHERE id_account = ?1", params![id_account], )?; self.connection.execute( "UPDATE orchard_addrs SET sk = NULL WHERE account = ?1", params![id_account], )?; Ok(()) } pub fn get_sapling_fvks(&self) -> anyhow::Result> { let mut statement = self .connection .prepare("SELECT id_account, ivk FROM accounts")?; let rows = statement.query_map([], |row| { let account: u32 = row.get(0)?; let ivk: String = row.get(1)?; let fvk = decode_extended_full_viewing_key( self.network().hrp_sapling_extended_full_viewing_key(), &ivk, ) .unwrap(); let ivk = fvk.fvk.vk.ivk(); Ok(SaplingViewKey { account, fvk, ivk }) })?; let mut fvks = vec![]; for r in rows { let row = r?; fvks.push(row); } Ok(fvks) } pub fn get_orchard_fvks(&self) -> anyhow::Result> { let mut statement = self .connection .prepare("SELECT account, fvk FROM orchard_addrs")?; let rows = statement.query_map([], |row| { let account: u32 = row.get(0)?; let fvk: Vec = row.get(1)?; let fvk: [u8; 96] = fvk.try_into().unwrap(); let fvk = FullViewingKey::from_bytes(&fvk).unwrap(); let vk = OrchardViewKey { account, fvk }; Ok(vk) })?; let mut fvks = vec![]; for r in rows { let row = r?; fvks.push(row); } Ok(fvks) } pub fn drop_last_checkpoint(&mut self) -> anyhow::Result { let height = self.get_last_sync_height()?; if let Some(height) = height { let height = self.trim_to_height(height - 1)?; return Ok(height); } Ok(self.sapling_activation_height()) } pub fn trim_to_height(&mut self, height: u32) -> anyhow::Result { // snap height to an existing checkpoint let height = self.connection.query_row( "SELECT MAX(height) from blocks WHERE height <= ?1", params![height], |row| { let height: Option = row.get(0)?; Ok(height) }, )?; let height = height.unwrap_or(0); log::info!("Rewind to height: {}", height); let tx = self.connection.transaction()?; tx.execute("DELETE FROM blocks WHERE height > ?1", params![height])?; tx.execute( "DELETE FROM sapling_tree WHERE height > ?1", params![height], )?; tx.execute( "DELETE FROM orchard_tree WHERE height > ?1", params![height], )?; tx.execute( "DELETE FROM sapling_witnesses WHERE height > ?1", params![height], )?; tx.execute( "DELETE FROM orchard_witnesses WHERE height > ?1", params![height], )?; tx.execute( "DELETE FROM received_notes WHERE height > ?1", params![height], )?; tx.execute( "UPDATE received_notes SET spent = NULL WHERE spent > ?1", params![height], )?; tx.execute( "DELETE FROM transactions WHERE height > ?1", params![height], )?; tx.execute("DELETE FROM messages WHERE height > ?1", params![height])?; tx.commit()?; Ok(height) } pub fn store_block( connection: &Connection, height: u32, hash: &[u8], timestamp: u32, sapling_tree: &CTree, orchard_tree: &CTree, ) -> anyhow::Result<()> { log::info!("+store_block"); let mut sapling_bb: Vec = vec![]; sapling_tree.write(&mut sapling_bb)?; connection.execute( "INSERT INTO blocks(height, hash, timestamp) VALUES (?1, ?2, ?3) ON CONFLICT DO NOTHING", params![height, hash, timestamp], )?; connection.execute( "INSERT INTO sapling_tree(height, tree) VALUES (?1, ?2) ON CONFLICT DO NOTHING", params![height, &sapling_bb], )?; let mut orchard_bb: Vec = vec![]; orchard_tree.write(&mut orchard_bb)?; connection.execute( "INSERT INTO orchard_tree(height, tree) VALUES (?1, ?2) ON CONFLICT DO NOTHING", params![height, &orchard_bb], )?; log::debug!("-block"); Ok(()) } pub fn store_transaction( txid: &[u8], account: u32, height: u32, timestamp: u32, tx_index: u32, db_tx: &Transaction, ) -> anyhow::Result { log::debug!("+transaction"); db_tx.execute( "INSERT INTO transactions(account, txid, height, timestamp, tx_index, value) VALUES (?1, ?2, ?3, ?4, ?5, 0) ON CONFLICT DO NOTHING", // ignore conflict when same tx has sapling + orchard outputs params![account, txid, height, timestamp, tx_index], )?; let id_tx: u32 = db_tx .query_row( "SELECT id_tx FROM transactions WHERE account = ?1 AND txid = ?2", params![account, txid], |row| row.get(0), ) .map_err(wrap_query_no_rows("store_transaction/id_tx"))?; log::debug!("-transaction {}", id_tx); Ok(id_tx) } pub fn store_received_note( note: &ReceivedNote, id_tx: u32, position: usize, db_tx: &Transaction, ) -> anyhow::Result { log::info!("+received_note {} {:?}", id_tx, note); let orchard = note.rho.is_some(); db_tx.execute("INSERT INTO received_notes(account, tx, height, position, output_index, diversifier, value, rcm, rho, nf, orchard, spent) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", params![note.account, id_tx, note.height, position as u32, note.output_index, note.diversifier, note.value as i64, note.rcm, note.rho, note.nf, orchard, note.spent])?; let id_note: u32 = db_tx .query_row( "SELECT id_note FROM received_notes WHERE tx = ?1 AND output_index = ?2 AND orchard = ?3", params![id_tx, note.output_index, orchard], |row| row.get(0), ) .map_err(wrap_query_no_rows("store_received_note/id_note"))?; log::debug!("-received_note"); Ok(id_note) } pub fn store_witness( witness: &sync::Witness, height: u32, id_note: u32, connection: &Connection, shielded_pool: &str, ) -> anyhow::Result<()> { log::debug!("+store_witness"); let mut bb: Vec = vec![]; witness.write(&mut bb)?; connection.execute( &format!( "INSERT INTO {}_witnesses(note, height, witness) VALUES (?1, ?2, ?3)", shielded_pool ), params![id_note, height, bb], )?; log::debug!("-store_witness"); Ok(()) } pub fn store_block_timestamp( &self, height: u32, hash: &[u8], timestamp: u32, ) -> anyhow::Result<()> { self.connection.execute( "INSERT INTO blocks(height, hash, timestamp) VALUES (?1,?2,?3)", params![height, hash, timestamp], )?; Ok(()) } pub fn store_tree( height: u32, tree: &CTree, db_tx: &Connection, shielded_pool: &str, ) -> anyhow::Result<()> { let mut bb: Vec = vec![]; tree.write(&mut bb)?; db_tx.execute( &format!( "INSERT INTO {}_tree(height, tree) VALUES (?1,?2)", shielded_pool ), params![height, &bb], )?; Ok(()) } pub fn update_transaction_with_memo(&self, details: &TransactionDetails) -> anyhow::Result<()> { self.connection.execute( "UPDATE transactions SET address = ?1, memo = ?2 WHERE id_tx = ?3", params![details.address, details.memo, details.id_tx], )?; Ok(()) } pub fn store_transparent_tx( _account: u32, _tx: &TransparentTxInfo, _db_tx: &Transaction, ) -> anyhow::Result<()> { todo!() } pub fn add_value(id_tx: u32, value: i64, db_tx: &Transaction) -> anyhow::Result<()> { db_tx.execute( "UPDATE transactions SET value = value + ?2 WHERE id_tx = ?1", params![id_tx, value], )?; Ok(()) } #[allow(dead_code)] pub fn get_balance(&self, account: u32) -> anyhow::Result { let balance: Option = self.connection.query_row( "SELECT SUM(value) FROM received_notes WHERE (spent IS NULL OR spent = 0) AND account = ?1", params![account], |row| row.get(0), )?; Ok(balance.unwrap_or(0) as u64) } pub fn get_last_sync_height(&self) -> anyhow::Result> { let height: Option = self.connection .query_row("SELECT MAX(height) FROM blocks", [], |row| row.get(0))?; Ok(height) } pub fn get_checkpoint_height(&self, max_height: u32) -> anyhow::Result> { let height: Option = self.connection.query_row( "SELECT MAX(height) FROM blocks WHERE height <= ?1", [max_height], |row| row.get(0), )?; Ok(height) } pub fn get_db_height(&self) -> anyhow::Result { let height: u32 = self .get_last_sync_height()? .unwrap_or_else(|| self.sapling_activation_height()); Ok(height) } pub fn get_db_hash(&self, height: u32) -> anyhow::Result> { let hash: Option> = self .connection .query_row( "SELECT hash FROM blocks WHERE height = ?1", params![height], |row| row.get(0), ) .optional()?; Ok(hash.map(|h| { let mut hash = [0u8; 32]; hash.copy_from_slice(&h); hash })) } pub fn get_tree_by_name( &self, height: u32, shielded_pool: &str, ) -> anyhow::Result { let tree = self .connection .query_row( &format!("SELECT tree FROM {}_tree WHERE height = ?1", shielded_pool), [height], |row| { let tree: Vec = row.get(0)?; Ok(tree) }, ) .optional()?; match tree { Some(tree) => { let tree = CTree::read(&*tree)?; let mut statement = self.connection.prepare( &format!("SELECT id_note, witness FROM {}_witnesses w, received_notes n WHERE w.height = ?1 AND w.note = n.id_note AND (n.spent IS NULL OR n.spent = 0)", shielded_pool))?; let ws = statement.query_map(params![height], |row| { let id_note: u32 = row.get(0)?; let witness: Vec = row.get(1)?; Ok(sync::Witness::read(id_note, &*witness).unwrap()) })?; let mut witnesses = vec![]; for w in ws { witnesses.push(w?); } Ok(TreeCheckpoint { tree, witnesses }) } None => Ok(TreeCheckpoint { tree: CTree::new(), witnesses: vec![], }), } } pub fn get_nullifier_amounts( &self, account: u32, unspent_only: bool, ) -> anyhow::Result> { let mut sql = "SELECT value, nf FROM received_notes WHERE account = ?1".to_string(); if unspent_only { sql += " AND (spent IS NULL OR spent = 0)"; } let mut statement = self.connection.prepare(&sql)?; let nfs_res = statement.query_map(params![account], |row| { let amount: i64 = row.get(0)?; let nf: Vec = row.get(1)?; Ok((amount, nf.try_into().unwrap())) })?; let mut nfs: HashMap = HashMap::new(); for n in nfs_res { let n = n?; nfs.insert(n.1, n.0 as u64); } Ok(nfs) } pub fn get_unspent_nullifiers(&self) -> anyhow::Result> { let sql = "SELECT id_note, account, nf, value FROM received_notes WHERE spent IS NULL OR spent = 0"; let mut statement = self.connection.prepare(sql)?; let nfs_res = statement.query_map(params![], |row| { let id: u32 = row.get(0)?; let account: u32 = row.get(1)?; let nf: Vec = row.get(2)?; let value: i64 = row.get(3)?; let nf: [u8; 32] = nf.try_into().unwrap(); let nf = Nf(nf); Ok(ReceivedNoteShort { id, account, nf, value: value as u64, }) })?; let mut nfs = vec![]; for n in nfs_res { let n = n?; nfs.push(n); } Ok(nfs) } pub fn get_unspent_received_notes( &self, account: u32, checkpoint_height: u32, orchard: bool, ) -> anyhow::Result> { let mut notes = vec![]; if !orchard { let mut statement = self.connection.prepare( "SELECT id_note, diversifier, value, rcm, witness FROM received_notes r, sapling_witnesses w WHERE spent IS NULL AND account = ?2 AND rho IS NULL AND (r.excluded IS NULL OR NOT r.excluded) AND w.height = ?1 AND r.id_note = w.note")?; let rows = statement.query_map(params![checkpoint_height, account], |row| { let id_note: u32 = row.get(0)?; let diversifier: Vec = row.get(1)?; let amount: i64 = row.get(2)?; let rcm: Vec = row.get(3)?; let witness: Vec = row.get(4)?; let source = Source::Sapling { id_note, diversifier: diversifier.try_into().unwrap(), rseed: rcm.try_into().unwrap(), witness, }; Ok(UTXO { id: id_note, source, amount: amount as u64, }) })?; for r in rows { let note = r?; notes.push(note); } } else { let mut statement = self.connection.prepare( "SELECT id_note, diversifier, value, rcm, rho, witness FROM received_notes r, orchard_witnesses w WHERE spent IS NULL AND account = ?2 AND rho IS NOT NULL AND (r.excluded IS NULL OR NOT r.excluded) AND w.height = ?1 AND r.id_note = w.note")?; let rows = statement.query_map(params![checkpoint_height, account], |row| { let id_note: u32 = row.get(0)?; let diversifier: Vec = row.get(1)?; let amount: i64 = row.get(2)?; let rcm: Vec = row.get(3)?; let rho: Vec = row.get(4).unwrap(); let witness: Vec = row.get(5)?; let source = Source::Orchard { id_note, diversifier: diversifier.try_into().unwrap(), rseed: rcm.try_into().unwrap(), rho: rho.try_into().unwrap(), witness, }; Ok(UTXO { id: id_note, source, amount: amount as u64, }) })?; for r in rows { let note = r?; notes.push(note); } }; Ok(notes) } pub fn tx_mark_spend(&mut self, selected_notes: &[u32]) -> anyhow::Result<()> { let db_tx = self.begin_transaction()?; for id_note in selected_notes.iter() { DbAdapter::mark_spent(*id_note, 0, &db_tx)?; } db_tx.commit()?; Ok(()) } pub fn mark_spent(id: u32, height: u32, tx: &Transaction) -> anyhow::Result<()> { log::debug!("+mark_spent"); tx.execute( "UPDATE received_notes SET spent = ?1 WHERE id_note = ?2", params![height, id], )?; log::debug!("-mark_spent"); Ok(()) } pub fn purge_old_witnesses(&mut self, height: u32) -> anyhow::Result<()> { log::debug!("+purge_old_witnesses"); const BLOCKS_PER_HOUR: u32 = 60 * 60 / 75; const BLOCKS_PER_DAY: u32 = 24 * BLOCKS_PER_HOUR; const BLOCKS_PER_MONTH: u32 = 30 * BLOCKS_PER_DAY; // Keep the last hour for i in 2..=24 { // 1 checkpoint per hour self.prune_interval( height - i * BLOCKS_PER_HOUR, height - (i - 1) * BLOCKS_PER_HOUR, )?; } for i in 2..=30 { // 1 checkpoint per day self.prune_interval( height - i * BLOCKS_PER_DAY, height - (i - 1) * BLOCKS_PER_DAY, )?; } for i in 2..=12 { // 1 checkpoint per 30 days self.prune_interval( height - i * BLOCKS_PER_MONTH, height - (i - 1) * BLOCKS_PER_MONTH, )?; } log::debug!("-purge_old_witnesses"); Ok(()) } // Only keep the oldest checkpoint in [low, high) fn prune_interval(&mut self, low: u32, high: u32) -> anyhow::Result<()> { log::debug!("prune_interval {} {}", low, high); let keep_height: Option = self.connection.query_row( "SELECT MIN(height) FROM blocks WHERE height >= ?1 AND height < ?2", params![low, high], |row| row.get(0), )?; if let Some(keep_height) = keep_height { log::info!("keep checkpoint {}", keep_height); let transaction = self.connection.transaction()?; transaction.execute( "DELETE FROM sapling_witnesses WHERE height >= ?1 AND height < ?2 AND height != ?3", params![low, high, keep_height], )?; transaction.execute( "DELETE FROM orchard_witnesses WHERE height >= ?1 AND height < ?2 AND height != ?3", params![low, high, keep_height], )?; transaction.execute( "DELETE FROM blocks WHERE height >= ?1 AND height < ?2 AND height != ?3", params![low, high, keep_height], )?; transaction.execute( "DELETE FROM sapling_tree WHERE height >= ?1 AND height < ?2 AND height != ?3", params![low, high, keep_height], )?; transaction.execute( "DELETE FROM orchard_tree WHERE height >= ?1 AND height < ?2 AND height != ?3", params![low, high, keep_height], )?; transaction.commit()?; } Ok(()) } pub fn store_contact(&self, contact: &Contact, dirty: bool) -> anyhow::Result<()> { if contact.id == 0 { self.connection.execute( "INSERT INTO contacts(name, address, dirty) VALUES (?1, ?2, ?3)", params![&contact.name, &contact.address, dirty], )?; } else { self.connection.execute( "INSERT INTO contacts(id, name, address, dirty) VALUES (?1, ?2, ?3, ?4) ON CONFLICT (id) DO UPDATE SET name = excluded.name, address = excluded.address, dirty = excluded.dirty", params![contact.id, &contact.name, &contact.address, dirty], )?; } Ok(()) } pub fn get_unsaved_contacts(&self) -> anyhow::Result> { let mut statement = self .connection .prepare("SELECT id, name, address FROM contacts WHERE dirty = TRUE")?; let rows = statement.query_map([], |row| { let id: u32 = row.get(0)?; let name: String = row.get(1)?; let address: String = row.get(2)?; let contact = Contact { id, name, address }; Ok(contact) })?; let mut contacts: Vec = vec![]; for r in rows { contacts.push(r?); } Ok(contacts) } pub fn get_account_info(&self, account: u32) -> anyhow::Result { assert_ne!(account, 0); log::info!("get_account_info {} {}", self.db_path, account); let account_data = self .connection .query_row( "SELECT name, seed, sk, ivk, address, aindex FROM accounts WHERE id_account = ?1", params![account], |row| { let name: String = row.get(0)?; let seed: Option = row.get(1)?; let sk: Option = row.get(2)?; let fvk: String = row.get(3)?; let address: String = row.get(4)?; let aindex: u32 = row.get(5)?; Ok(AccountData { name, seed, sk, fvk, address, aindex, }) }, ) .map_err(wrap_query_no_rows("get_account_info"))?; Ok(account_data) } pub fn get_taddr(&self, account: u32) -> anyhow::Result> { let address = self .connection .query_row( "SELECT address FROM taddrs WHERE account = ?1", params![account], |row| { let address: String = row.get(0)?; Ok(address) }, ) .optional()?; Ok(address) } pub fn get_tsk(&self, account: u32) -> anyhow::Result> { let sk = self .connection .query_row( "SELECT sk FROM taddrs WHERE account = ?1", params![account], |row| { let sk: String = row.get(0)?; Ok(sk) }, ) .optional()?; Ok(sk) } pub fn create_taddr(&self, account: u32) -> anyhow::Result<()> { let AccountData { seed, aindex, .. } = self.get_account_info(account)?; if let Some(seed) = seed { let bip44_path = format!("m/44'/{}'/0'/0/{}", self.network().coin_type(), aindex); let (sk, address) = derive_tkeys(self.network(), &seed, &bip44_path)?; self.connection.execute( "INSERT INTO taddrs(account, sk, address) VALUES (?1, ?2, ?3)", params![account, &sk, &address], )?; } Ok(()) } pub fn create_orchard(&self, account: u32) -> anyhow::Result<()> { let AccountData { seed, aindex, .. } = self.get_account_info(account)?; if let Some(seed) = seed { let keys = derive_orchard_keys(self.network().coin_type(), &seed, aindex); self.connection.execute( "INSERT INTO orchard_addrs(account, sk, fvk) VALUES (?1, ?2, ?3)", params![account, &keys.sk, &keys.fvk], )?; } Ok(()) } pub fn store_orchard_fvk(&self, account: u32, fvk: &[u8; 96]) -> anyhow::Result<()> { self.connection.execute( "INSERT INTO orchard_addrs(account, sk, fvk) VALUES (?1, NULL, ?2) ON CONFLICT DO NOTHING", params![account, fvk], )?; Ok(()) } pub fn find_account_by_fvk(&self, fvk: &str) -> anyhow::Result> { let account = self .connection .query_row( "SELECT id_account FROM accounts WHERE fvk = ?1", params![fvk], |row| { let account: u32 = row.get(0)?; Ok(account) }, ) .optional()?; Ok(account) } pub fn get_orchard(&self, account: u32) -> anyhow::Result> { let key = self .connection .query_row( "SELECT sk, fvk FROM orchard_addrs WHERE account = ?1", params![account], |row| { let sk: Option> = row.get(0)?; let fvk: Vec = row.get(1)?; Ok(OrchardKeyBytes { sk: sk.map(|sk| sk.try_into().unwrap()), fvk: fvk.try_into().unwrap(), }) }, ) .optional()?; Ok(key) } pub fn store_ua_settings( &self, account: u32, transparent: bool, sapling: bool, orchard: bool, ) -> anyhow::Result<()> { self.connection.execute( "INSERT INTO ua_settings(account, transparent, sapling, orchard) VALUES (?1, ?2, ?3, ?4)", params![account, transparent, sapling, orchard], )?; Ok(()) } pub fn get_ua_settings(&self, account: u32) -> anyhow::Result { let tpe = self.connection.query_row( "SELECT transparent, sapling, orchard FROM ua_settings WHERE account = ?1", params![account], |row| { let transparent: bool = row.get(0)?; let sapling: bool = row.get(1)?; let orchard: bool = row.get(2)?; Ok(UnifiedAddressType { transparent, sapling, orchard, }) }, )?; Ok(tpe) } pub fn store_historical_prices( &mut self, prices: &[Quote], currency: &str, ) -> anyhow::Result<()> { let db_transaction = self.connection.transaction()?; { let mut statement = db_transaction.prepare( "INSERT INTO historical_prices(timestamp, price, currency) VALUES (?1, ?2, ?3)", )?; for q in prices { statement.execute(params![q.timestamp, q.price, currency])?; } } db_transaction.commit()?; Ok(()) } pub fn get_latest_quote(&self, currency: &str) -> anyhow::Result> { let quote = self.connection.query_row( "SELECT timestamp, price FROM historical_prices WHERE currency = ?1 ORDER BY timestamp DESC", params![currency], |row| { let timestamp: i64 = row.get(0)?; let price: f64 = row.get(1)?; Ok(Quote { timestamp, price }) }).optional()?; Ok(quote) } pub fn truncate_data(&self) -> anyhow::Result<()> { self.truncate_sync_data()?; self.connection.execute("DELETE FROM diversifiers", [])?; Ok(()) } pub fn truncate_sync_data(&self) -> anyhow::Result<()> { self.connection.execute("DELETE FROM blocks", [])?; self.connection.execute("DELETE FROM sapling_tree", [])?; self.connection.execute("DELETE FROM orchard_tree", [])?; self.connection.execute("DELETE FROM contacts", [])?; self.connection.execute("DELETE FROM diversifiers", [])?; self.connection .execute("DELETE FROM historical_prices", [])?; self.connection.execute("DELETE FROM received_notes", [])?; self.connection .execute("DELETE FROM sapling_witnesses", [])?; self.connection .execute("DELETE FROM orchard_witnesses", [])?; self.connection.execute("DELETE FROM transactions", [])?; self.connection.execute("DELETE FROM messages", [])?; Ok(()) } pub fn delete_incomplete_scan(&mut self) -> anyhow::Result<()> { let synced_height = self.get_last_sync_height()?; if let Some(synced_height) = synced_height { self.trim_to_height(synced_height)?; } Ok(()) } pub fn delete_account(&self, account: u32) -> anyhow::Result<()> { self.connection.execute( "DELETE FROM received_notes WHERE account = ?1", params![account], )?; self.connection.execute( "DELETE FROM transactions WHERE account = ?1", params![account], )?; self.connection.execute( "DELETE FROM diversifiers WHERE account = ?1", params![account], )?; self.connection.execute( "DELETE FROM accounts WHERE id_account = ?1", params![account], )?; self.connection .execute("DELETE FROM taddrs WHERE account = ?1", params![account])?; self.connection.execute( "DELETE FROM orchard_addrs WHERE account = ?1", params![account], )?; self.connection.execute( "DELETE FROM ua_settings WHERE account = ?1", params![account], )?; self.connection .execute("DELETE FROM messages WHERE account = ?1", params![account])?; self.connection .execute("DELETE FROM hw_wallets WHERE account = ?1", params![account])?; Ok(()) } pub fn delete_orphan_transactions(&self) -> anyhow::Result<()> { self.connection.execute("DELETE FROM transactions WHERE id_tx IN (SELECT tx.id_tx FROM transactions tx LEFT JOIN accounts a ON tx.account = a.id_account WHERE a.id_account IS NULL)", [])?; Ok(()) } pub fn store_message(&self, account: u32, message: &ZMessage) -> anyhow::Result<()> { self.connection.execute("INSERT INTO messages(account, id_tx, sender, recipient, subject, body, timestamp, height, incoming, read) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10)", params![account, message.id_tx, message.sender, message.recipient, message.subject, message.body, message.timestamp, message.height, message.incoming, false])?; Ok(()) } pub fn mark_message_read(&self, message_id: u32, read: bool) -> anyhow::Result<()> { self.connection.execute( "UPDATE messages SET read = ?1 WHERE id = ?2", params![read, message_id], )?; Ok(()) } pub fn mark_all_messages_read(&self, account: u32, read: bool) -> anyhow::Result<()> { self.connection.execute( "UPDATE messages SET read = ?1 WHERE account = ?2", params![read, account], )?; Ok(()) } pub fn clear_tx_details(connection: &Connection, account: u32) -> anyhow::Result<()> { connection.execute( "UPDATE transactions SET address = NULL, memo = NULL WHERE account = ?1", [account], )?; connection.execute("DELETE FROM messages WHERE account = ?1", [account])?; Ok(()) } pub fn get_accounts(&self) -> anyhow::Result> { let mut s = self .connection .prepare("SELECT id_account, name, address FROM accounts")?; let accounts = s.query_map([], |row| { let id_account: u32 = row.get(0)?; let name: String = row.get(1)?; let address: String = row.get(2)?; Ok(AccountRec { id_account, name, address, }) })?; let mut account_recs = vec![]; for row in accounts { account_recs.push(row?); } Ok(account_recs) } pub fn get_txs(&self, account: u32) -> anyhow::Result> { let mut s = self.connection.prepare("SELECT txid, height, timestamp, value, address, memo FROM transactions WHERE account = ?1")?; let tx_rec = s.query_map(params![account], |row| { let mut txid: Vec = row.get(0)?; txid.reverse(); let txid = hex::encode(txid); let height: u32 = row.get(1)?; let timestamp: u32 = row.get(2)?; let value: i64 = row.get(3)?; let address: String = row.get(4)?; let memo: String = row.get(5)?; Ok(TxRec { txid, height, timestamp, value, address, memo, }) })?; let mut txs = vec![]; for row in tx_rec { txs.push(row?); } Ok(txs) } pub fn get_txid_without_memo(&self) -> anyhow::Result> { let mut stmt = self.connection.prepare( "SELECT account, id_tx, height, timestamp, txid, value FROM transactions WHERE memo IS NULL", )?; let rows = stmt.query_map([], |row| { let account: u32 = row.get(0)?; let id_tx: u32 = row.get(1)?; let height: u32 = row.get(2)?; let timestamp: u32 = row.get(3)?; let txid: Vec = row.get(4)?; let value: i64 = row.get(5)?; Ok(GetTransactionDetailRequest { account, id_tx, height, timestamp, txid: txid.try_into().unwrap(), value, }) })?; let mut reqs = vec![]; for r in rows { reqs.push(r?); } Ok(reqs) } pub fn store_template(&self, t: &SendTemplate) -> anyhow::Result { let id = if t.id() == 0 { self.connection.execute("INSERT INTO \ send_templates(title, address, amount, fiat_amount, fee_included, fiat, include_reply_to, subject, body) \ VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)", params![t.title().unwrap(), t.address().unwrap(), t.amount(), t.fiat_amount(), t.fee_included(), t.fiat(), t.include_reply_to(), t.subject().unwrap(), t.body().unwrap()])?; self.connection.last_insert_rowid() as u32 } else { self.connection.execute("UPDATE send_templates SET \ title=?1, address=?2, amount=?3, fiat_amount=?4, fee_included=?5, fiat=?6, include_reply_to=?7, subject=?8, body=?9 \ WHERE id_send_template=?10", params![t.title().unwrap(), t.address().unwrap(), t.amount(), t.fiat_amount(), t.fee_included(), t.fiat(), t.include_reply_to(), t.subject().unwrap(), t.body().unwrap(), t.id()])?; t.id() }; Ok(id) } pub fn delete_template(&self, id: u32) -> anyhow::Result<()> { self.connection.execute( "DELETE FROM send_templates WHERE id_send_template=?1", params![id], )?; Ok(()) } fn network(&self) -> &'static Network { let chain = get_coin_chain(self.coin_type); chain.network() } pub fn sapling_activation_height(&self) -> u32 { self.network() .activation_height(NetworkUpgrade::Sapling) .unwrap() .into() } } pub struct ZMessage { pub id_tx: u32, pub sender: Option, pub recipient: String, pub subject: String, pub body: String, pub timestamp: u32, pub height: u32, pub incoming: bool, } impl ZMessage { pub fn is_empty(&self) -> bool { self.sender.is_none() && self.subject.is_empty() && self.body.is_empty() } } #[derive(Serialize)] pub struct TxRec { txid: String, height: u32, timestamp: u32, value: i64, address: String, memo: String, } #[derive(Serialize)] pub struct AccountRec { id_account: u32, name: String, address: String, } pub struct AccountData { pub name: String, pub seed: Option, pub sk: Option, pub fvk: String, pub address: String, pub aindex: u32, } #[cfg(test)] mod tests { use crate::db::{DbAdapter, DEFAULT_DB_PATH}; use zcash_params::coin::CoinType; #[test] fn test_balance() { let db = DbAdapter::new(CoinType::Zcash, DEFAULT_DB_PATH, "").unwrap(); let balance = db.get_balance(1).unwrap(); println!("{}", balance); } }