/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2026 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::{convert::From, path::PathBuf, sync::Arc}; use smol::lock::Mutex as AsyncMutex; use tracing::{debug, error}; pub use turso::{Builder, Connection, EncryptionOpts, Value}; use crate::error::{WalletDbError, WalletDbResult}; pub type WalletPtr = Arc; const ENCRYPTION_ALGO: &str = "aegis256"; /// Structure representing base wallet database operations. pub struct WalletDb { /// Connection to the turso database. pub conn: AsyncMutex, } impl WalletDb { /// Create a new wallet database handler. If `path` is `None`, create it in memory. pub async fn new(path: Option, password: Option<&str>) -> WalletDbResult { // Parse database path let path = match path { Some(p) => { let Some(p) = p.to_str() else { return Err(WalletDbError::ConnectionFailed); }; String::from(p) } None => String::from(":memory:"), }; // Set encryption. We have to manually devire the key since // turso doesn't support it yet. let builder = match password { Some(password) => { let opts = EncryptionOpts { cipher: String::from(ENCRYPTION_ALGO), hexkey: blake3::hash(password.as_bytes()).to_hex().to_string(), }; Builder::new_local(&path).experimental_encryption(true).with_encryption(opts) } None => Builder::new_local(&path), }; // Initialize connection builder let Ok(builder) = builder.build().await else { return Err(WalletDbError::ConnectionFailed); }; // Connect to database let Ok(conn) = builder.connect() else { return Err(WalletDbError::ConnectionFailed); }; // Set foreign keys pragma if let Err(e) = conn.pragma_update("foreign_keys", "ON").await { error!(target: "walletdb::new", "[WalletDb] Foreign keys pragma update failed: {e}"); return Err(WalletDbError::PragmaUpdateError); }; debug!(target: "walletdb::new", "[WalletDb] Opened Sqlite connection at \"{path:?}\""); Ok(Arc::new(Self { conn: AsyncMutex::new(conn) })) } /// This function executes a given SQL query that contains multiple SQL statements, /// that don't contain any parameters. pub async fn exec_batch_sql(&self, query: &str) -> WalletDbResult<()> { debug!(target: "walletdb::exec_batch_sql", "[WalletDb] Executing batch SQL query:\n{query}"); if let Err(e) = self.conn.lock().await.execute_batch(query).await { error!(target: "walletdb::exec_batch_sql", "[WalletDb] Query failed: {e}"); return Err(WalletDbError::QueryExecutionFailed) }; Ok(()) } /// 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, params: Vec) -> WalletDbResult<()> { debug!(target: "walletdb::exec_sql", "[WalletDb] Executing SQL query:\n{query}"); let conn = self.conn.lock().await; // If no params are provided, execute directly if params.is_empty() { if let Err(e) = conn.execute(query, ()).await { error!(target: "walletdb::exec_sql", "[WalletDb] Query failed: {e}"); return Err(WalletDbError::QueryExecutionFailed) }; return Ok(()) } // First we prepare the query let Ok(mut stmt) = conn.prepare(query).await else { return Err(WalletDbError::QueryPreparationFailed) }; // Execute the query using provided params if let Err(e) = stmt.execute(params).await { error!(target: "walletdb::exec_sql", "[WalletDb] Query failed: {e}"); return Err(WalletDbError::QueryExecutionFailed) }; drop(conn); Ok(()) } /// Generate a `SELECT` query for provided table from selected column names and /// provided `WHERE` clauses. Named parameters are supported in the `WHERE` clauses, /// assuming they follow the normal formatting ":{column_name}". fn generate_select_query( &self, table: &str, col_names: &[&str], params: &[(String, Value)], ) -> String { let mut query = if col_names.is_empty() { format!("SELECT * FROM {table}") } else { format!("SELECT {} FROM {table}", col_names.join(", ")) }; if params.is_empty() { return query } let mut where_str = Vec::with_capacity(params.len()); for (k, _) in params { let col = &k[1..]; where_str.push(format!("{col} = {k}")); } query.push_str(&format!(" WHERE {}", where_str.join(" AND "))); query } /// Query provided table from selected column names and provided `WHERE` clauses, /// for a single row. pub async fn query_single( &self, table: &str, col_names: &[&str], params: Vec<(String, Value)>, ) -> WalletDbResult> { // Generate `SELECT` query let query = self.generate_select_query(table, col_names, ¶ms); debug!(target: "walletdb::query_single", "[WalletDb] Executing SQL query:\n{query}"); // First we prepare the query let conn = self.conn.lock().await; let Ok(mut stmt) = conn.prepare(&query).await else { return Err(WalletDbError::QueryPreparationFailed) }; // Execute the query using provided params let Ok(mut rows) = stmt.query(params).await else { return Err(WalletDbError::QueryExecutionFailed) }; // Check if row exists let Ok(next) = rows.next().await else { return Err(WalletDbError::QueryExecutionFailed) }; let row = match next { Some(row_result) => row_result, None => return Err(WalletDbError::RowNotFound), }; // Grab returned values let mut result = vec![]; if col_names.is_empty() { let mut idx = 0; loop { let Ok(value) = row.get_value(idx) else { break }; result.push(value); idx += 1; } } else { for col in col_names { let Ok(idx) = rows.column_index(col) else { return Err(WalletDbError::ParseColumnValueError) }; let Ok(value) = row.get_value(idx) else { return Err(WalletDbError::ParseColumnValueError) }; result.push(value); } } Ok(result) } /// Query provided table from selected column names and provided `WHERE` clauses, /// for multiple rows. pub async fn query_multiple( &self, table: &str, col_names: &[&str], params: Vec<(String, Value)>, ) -> WalletDbResult>> { // Generate `SELECT` query let query = self.generate_select_query(table, col_names, ¶ms); debug!(target: "walletdb::query_multiple", "[WalletDb] Executing SQL query:\n{query}"); // First we prepare the query let conn = self.conn.lock().await; let Ok(mut stmt) = conn.prepare(&query).await else { return Err(WalletDbError::QueryPreparationFailed) }; // Execute the query using provided converted params let Ok(mut rows) = stmt.query(params).await else { return Err(WalletDbError::QueryExecutionFailed) }; // Loop over returned rows and parse them let mut result = vec![]; loop { // Check if an error occured let row = match rows.next().await { Ok(r) => r, Err(_) => return Err(WalletDbError::QueryExecutionFailed), }; // Check if no row was returned let row = match row { Some(r) => r, None => break, }; // Grab row returned values let mut row_values = vec![]; if col_names.is_empty() { let mut idx = 0; loop { let Ok(value) = row.get_value(idx) else { break }; row_values.push(value); idx += 1; } } else { for col in col_names { let Ok(idx) = rows.column_index(col) else { return Err(WalletDbError::ParseColumnValueError) }; let Ok(value) = row.get_value(idx) else { return Err(WalletDbError::ParseColumnValueError) }; row_values.push(value); } } result.push(row_values); } Ok(result) } /// Query provided table using provided query for multiple rows. pub async fn query_custom( &self, query: &str, params: Vec, ) -> WalletDbResult>> { debug!(target: "walletdb::query_custom", "[WalletDb] Executing SQL query:\n{query}"); // First we prepare the query let conn = self.conn.lock().await; let Ok(mut stmt) = conn.prepare(query).await else { return Err(WalletDbError::QueryPreparationFailed) }; // Execute the query using provided converted params let Ok(mut rows) = stmt.query(params).await else { return Err(WalletDbError::QueryExecutionFailed) }; // Loop over returned rows and parse them let mut result = vec![]; loop { // Check if an error occured let row = match rows.next().await { Ok(r) => r, Err(_) => return Err(WalletDbError::QueryExecutionFailed), }; // Check if no row was returned let row = match row { Some(r) => r, None => break, }; // Grab row returned values let mut row_values = vec![]; let mut idx = 0; loop { let Ok(value) = row.get_value(idx) else { break }; row_values.push(value); idx += 1; } result.push(row_values); } Ok(result) } } /// Custom implementation of `turso::params!` to construct positional /// params from a heterogeneous set of params types as a vec. #[macro_export] macro_rules! params { () => { () }; ($($value:expr),* $(,)?) => { [$(turso::Value::from($value)),*].to_vec() }; } /// Custom implementation of `turso::named_params!` to construct named /// params from a heterogeneous set of params types as a vec. #[macro_export] macro_rules! named_params { () => { () }; ($($param_name:literal: $value:expr),* $(,)?) => { [$((String::from($param_name), turso::Value::from($value))),*].to_vec() }; } /// Custom implementation of `turso::named_params!` to use `expr` /// instead of `literal` as `$param_name`, and append the ":" named /// parameters prefix. #[macro_export] macro_rules! convert_named_params { () => { () }; ($(($param_name:expr, $value:expr)),* $(,)?) => { [$((format!(":{}", $param_name), turso::Value::from($value))),*].to_vec() }; } #[cfg(test)] mod tests { use crate::walletdb::{Value, WalletDb}; #[test] fn test_mem_wallet() { smol::block_on(async { let wallet = WalletDb::new(None, Some("foobar")).await.unwrap(); wallet .exec_batch_sql( "CREATE TABLE mista ( numba INTEGER ); INSERT INTO mista ( numba ) VALUES ( 42 );", ).await .unwrap(); let ret = wallet.query_single("mista", &["numba"], vec![]).await.unwrap(); assert_eq!(ret.len(), 1); let numba: i64 = if let Value::Integer(numba) = ret[0] { numba } else { -1 }; assert_eq!(numba, 42); let ret = wallet.query_custom("SELECT numba FROM mista;", vec![]).await.unwrap(); assert_eq!(ret.len(), 1); assert_eq!(ret[0].len(), 1); let numba: i64 = if let Value::Integer(numba) = ret[0][0] { numba } else { -1 }; assert_eq!(numba, 42); }) } #[test] fn test_query_single() { smol::block_on(async { let wallet = WalletDb::new(None, None).await.unwrap(); wallet .exec_batch_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]; wallet .exec_sql( "INSERT INTO mista ( why, are, you, gae ) VALUES (?1, ?2, ?3, ?4);", params![why, are.clone(), you, gae.clone()], ) .await .unwrap(); let ret = wallet.query_single("mista", &["why", "are", "you", "gae"], vec![]).await.unwrap(); assert_eq!(ret.len(), 4); assert_eq!(ret[0], Value::Integer(why)); assert_eq!(ret[1], Value::Text(are.clone())); assert_eq!(ret[2], Value::Integer(you)); assert_eq!(ret[3], Value::Blob(gae.clone())); let ret = wallet.query_custom("SELECT why, are, you, gae FROM mista;", vec![]).await.unwrap(); assert_eq!(ret.len(), 1); assert_eq!(ret[0].len(), 4); assert_eq!(ret[0][0], Value::Integer(why)); assert_eq!(ret[0][1], Value::Text(are.clone())); assert_eq!(ret[0][2], Value::Integer(you)); assert_eq!(ret[0][3], Value::Blob(gae.clone())); let ret = wallet .query_single( "mista", &["gae"], named_params! {":why": why, ":are": are.clone(), ":you": you}, ) .await .unwrap(); assert_eq!(ret.len(), 1); assert_eq!(ret[0], Value::Blob(gae.clone())); let ret = wallet .query_custom( "SELECT gae FROM mista WHERE why = ?1 AND are = ?2 AND you = ?3;", params![why, are, you], ) .await .unwrap(); assert_eq!(ret.len(), 1); assert_eq!(ret[0].len(), 1); assert_eq!(ret[0][0], Value::Blob(gae)); }) } #[test] fn test_query_multi() { smol::block_on(async { let wallet = WalletDb::new(None, None).await.unwrap(); wallet .exec_batch_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]; wallet .exec_sql( "INSERT INTO mista ( why, are, you, gae ) VALUES (?1, ?2, ?3, ?4);", params![why, are.clone(), you, gae.clone()], ) .await .unwrap(); wallet .exec_sql( "INSERT INTO mista ( why, are, you, gae ) VALUES (?1, ?2, ?3, ?4);", params![why, are.clone(), you, gae.clone()], ) .await .unwrap(); let ret = wallet.query_multiple("mista", &[], vec![]).await.unwrap(); assert_eq!(ret.len(), 2); for row in ret { assert_eq!(row.len(), 4); assert_eq!(row[0], Value::Integer(why)); assert_eq!(row[1], Value::Text(are.clone())); assert_eq!(row[2], Value::Integer(you)); assert_eq!(row[3], Value::Blob(gae.clone())); } let ret = wallet.query_custom("SELECT * FROM mista;", vec![]).await.unwrap(); assert_eq!(ret.len(), 2); for row in ret { assert_eq!(row.len(), 4); assert_eq!(row[0], Value::Integer(why)); assert_eq!(row[1], Value::Text(are.clone())); assert_eq!(row[2], Value::Integer(you)); assert_eq!(row[3], Value::Blob(gae.clone())); } let ret = wallet .query_multiple( "mista", &["gae"], convert_named_params! {("why", why), ("are", are.clone()), ("you", you)}, ) .await .unwrap(); assert_eq!(ret.len(), 2); for row in ret { assert_eq!(row.len(), 1); assert_eq!(row[0], Value::Blob(gae.clone())); } let ret = wallet .query_custom( "SELECT gae FROM mista WHERE why = ?1 AND are = ?2 AND you = ?3;", params![why, are, you], ) .await .unwrap(); assert_eq!(ret.len(), 2); for row in ret { assert_eq!(row.len(), 1); assert_eq!(row[0], Value::Blob(gae.clone())); } }) } }