/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2023 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use std::{any::Any, path::PathBuf, sync::Arc}; use log::{debug, info}; use rusqlite::Connection; use smol::lock::Mutex; use crate::Result; pub type WalletPtr = Arc; /// Types we want to allow to query from the SQL wallet pub enum QueryType { /// Integer gets decoded into `u64` Integer = 0x00, /// Blob gets decoded into `Vec` Blob = 0x01, /// OptionInteger gets decoded into `Option` OptionInteger = 0x02, /// OptionBlob gets decoded into `Option>` OptionBlob = 0x03, /// Text gets decoded into `String` Text = 0x04, /// Last type, increment this when you add new types. Last = 0x05, } impl From for QueryType { fn from(x: u8) -> Self { match x { 0x00 => Self::Integer, 0x01 => Self::Blob, 0x02 => Self::OptionInteger, 0x03 => Self::OptionBlob, 0x04 => Self::Text, _ => unimplemented!(), } } } #[derive(Debug)] pub enum SqlType { Integer(i64), Text(String), Blob(Vec), Null, } impl SqlType { pub fn inner(&self) -> Option<&T> { match self { SqlType::Integer(v) => (v as &dyn Any).downcast_ref::(), SqlType::Text(v) => (v as &dyn Any).downcast_ref::(), SqlType::Blob(v) => (v as &dyn Any).downcast_ref::(), SqlType::Null => None, } } } /// Structure representing base wallet operations. /// Additional operations can be implemented by trait extensions. pub struct WalletDb { pub conn: Mutex, } impl WalletDb { /// Create a new wallet. If `path` is `None`, create it in memory. pub fn new(path: Option, password: Option<&str>) -> Result { let conn = match path.clone() { Some(p) => Connection::open(p)?, None => Connection::open_in_memory()?, }; if let Some(password) = password { conn.pragma_update(None, "key", password)?; } conn.pragma_update(None, "foreign_keys", "ON")?; info!(target: "wallet::walletdb", "[WalletDb] Opened Sqlite connection at \"{:?}\"", path); Ok(Arc::new(Self { conn: Mutex::new(conn) })) } /// This function executes a given SQL query, but isn't able to return anything. /// Therefore it's best to use it for initializing a table or similar things. pub async fn exec_sql(&self, query: &str) -> Result<()> { info!(target: "wallet::walletdb", "[WalletDb] Executing SQL query"); debug!(target: "wallet::walletdb", "[WalletDb] Query:\n{}", query); let _ = self.conn.lock().await.execute(query, ())?; Ok(()) } pub async fn query_single( &self, table: &str, col_names: Vec<&str>, where_queries: Option>, ) -> Result> { let mut query = format!("SELECT {} FROM {}", col_names.join(", "), table); if let Some(wq) = where_queries.as_ref() { let where_str: Vec = wq.iter().map(|(k, _)| format!("{} = ?", k)).collect(); query.push_str(&format!(" WHERE {}", where_str.join(" AND "))); } let params: Vec = where_queries.map_or(Vec::new(), |wq| { wq.into_iter() .map(|(_, v)| match v { SqlType::Integer(i) => rusqlite::types::ToSqlOutput::from(i), SqlType::Text(t) => rusqlite::types::ToSqlOutput::from(t), SqlType::Blob(b) => rusqlite::types::ToSqlOutput::from(b), SqlType::Null => rusqlite::types::ToSqlOutput::from(rusqlite::types::Null), }) .collect::>() }); let wallet_conn = self.conn.lock().await; let mut stmt = wallet_conn.prepare(&query)?; let params_as_slice: Vec<&dyn rusqlite::ToSql> = params.iter().map(|x| x as &dyn rusqlite::ToSql).collect(); let mut rows = stmt.query(params_as_slice.as_slice())?; let row = match rows.next()? { Some(row_result) => row_result, None => return Ok(vec![]), }; let mut result = vec![]; for (idx, _) in col_names.iter().enumerate() { let value: SqlType = match row.get_ref(idx)?.data_type() { rusqlite::types::Type::Integer => SqlType::Integer(row.get(idx)?), rusqlite::types::Type::Text => SqlType::Text(row.get(idx)?), rusqlite::types::Type::Blob => SqlType::Blob(row.get(idx)?), rusqlite::types::Type::Null => SqlType::Null, _ => unimplemented!(), }; result.push(value); } Ok(result) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_mem_wallet() { smol::block_on(async { let wallet = WalletDb::new(None, Some("foobar")).unwrap(); wallet.exec_sql("CREATE TABLE mista ( numba INTEGER );").await.unwrap(); wallet.exec_sql("INSERT INTO mista ( numba ) VALUES ( 42 );").await.unwrap(); let conn = wallet.conn.lock().await; let mut stmt = conn.prepare("SELECT numba FROM mista").unwrap(); let numba: u64 = stmt.query_row((), |row| Ok(row.get("numba").unwrap())).unwrap(); stmt.finalize().unwrap(); assert!(numba == 42); }); } #[test] fn test_query_single() { smol::block_on(async { let wallet = WalletDb::new(None, None).unwrap(); wallet .exec_sql("CREATE TABLE mista ( why INTEGER, are TEXT, you INTEGER, gae BLOB );") .await .unwrap(); let why = 42; let are = "are".to_string(); let you = 69; let gae = vec![42u8; 32]; let query_str = "INSERT INTO mista ( why, are, you, gae ) VALUES (?1, ?2, ?3, ?4);"; let wallet_conn = wallet.conn.lock().await; let mut stmt = wallet_conn.prepare(query_str).unwrap(); stmt.execute(rusqlite::params![why, are, you, gae]).unwrap(); stmt.finalize().unwrap(); drop(wallet_conn); let ret = wallet.query_single("mista", vec!["why", "are", "you", "gae"], None).await.unwrap(); assert!(ret.len() == 4); assert!(ret[0].inner::().unwrap() == &why); assert!(ret[1].inner::().unwrap() == &are); assert!(ret[2].inner::().unwrap() == &you); assert!(ret[3].inner::>().unwrap() == &gae); }); } }