walletdb.rs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2025 Dyne.org foundation
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License as
  7. * published by the Free Software Foundation, either version 3 of the
  8. * License, or (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU Affero General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. use std::{
  19. path::PathBuf,
  20. sync::{Arc, Mutex},
  21. };
  22. use rusqlite::{
  23. types::{ToSql, Value},
  24. Connection,
  25. };
  26. use tracing::{debug, error};
  27. use crate::error::{WalletDbError, WalletDbResult};
  28. pub type WalletPtr = Arc<WalletDb>;
  29. /// Structure representing base wallet database operations.
  30. pub struct WalletDb {
  31. /// Connection to the SQLite database.
  32. pub conn: Mutex<Connection>,
  33. }
  34. impl WalletDb {
  35. /// Create a new wallet database handler. If `path` is `None`, create it in memory.
  36. pub fn new(path: Option<PathBuf>, password: Option<&str>) -> WalletDbResult<WalletPtr> {
  37. let Ok(conn) = (match path.clone() {
  38. Some(p) => Connection::open(p),
  39. None => Connection::open_in_memory(),
  40. }) else {
  41. return Err(WalletDbError::ConnectionFailed);
  42. };
  43. if let Some(password) = password {
  44. if let Err(e) = conn.pragma_update(None, "key", password) {
  45. error!(target: "walletdb::new", "[WalletDb] Pragma update failed: {e}");
  46. return Err(WalletDbError::PragmaUpdateError);
  47. };
  48. }
  49. if let Err(e) = conn.pragma_update(None, "foreign_keys", "ON") {
  50. error!(target: "walletdb::new", "[WalletDb] Pragma update failed: {e}");
  51. return Err(WalletDbError::PragmaUpdateError);
  52. };
  53. debug!(target: "walletdb::new", "[WalletDb] Opened Sqlite connection at \"{path:?}\"");
  54. Ok(Arc::new(Self { conn: Mutex::new(conn) }))
  55. }
  56. /// This function executes a given SQL query that contains multiple SQL statements,
  57. /// that don't contain any parameters.
  58. pub fn exec_batch_sql(&self, query: &str) -> WalletDbResult<()> {
  59. debug!(target: "walletdb::exec_batch_sql", "[WalletDb] Executing batch SQL query:\n{query}");
  60. let Ok(conn) = self.conn.lock() else { return Err(WalletDbError::FailedToAquireLock) };
  61. if let Err(e) = conn.execute_batch(query) {
  62. error!(target: "walletdb::exec_batch_sql", "[WalletDb] Query failed: {e}");
  63. return Err(WalletDbError::QueryExecutionFailed)
  64. };
  65. Ok(())
  66. }
  67. /// This function executes a given SQL query, but isn't able to return anything.
  68. /// Therefore it's best to use it for initializing a table or similar things.
  69. pub fn exec_sql(&self, query: &str, params: &[&dyn ToSql]) -> WalletDbResult<()> {
  70. debug!(target: "walletdb::exec_sql", "[WalletDb] Executing SQL query:\n{query}");
  71. let Ok(conn) = self.conn.lock() else { return Err(WalletDbError::FailedToAquireLock) };
  72. // If no params are provided, execute directly
  73. if params.is_empty() {
  74. if let Err(e) = conn.execute(query, ()) {
  75. error!(target: "walletdb::exec_sql", "[WalletDb] Query failed: {e}");
  76. return Err(WalletDbError::QueryExecutionFailed)
  77. };
  78. return Ok(())
  79. }
  80. // First we prepare the query
  81. let Ok(mut stmt) = conn.prepare(query) else {
  82. return Err(WalletDbError::QueryPreparationFailed)
  83. };
  84. // Execute the query using provided params
  85. if let Err(e) = stmt.execute(params) {
  86. error!(target: "walletdb::exec_sql", "[WalletDb] Query failed: {e}");
  87. return Err(WalletDbError::QueryExecutionFailed)
  88. };
  89. // Finalize query and drop connection lock
  90. if let Err(e) = stmt.finalize() {
  91. error!(target: "walletdb::exec_sql", "[WalletDb] Query finalization failed: {e}");
  92. return Err(WalletDbError::QueryFinalizationFailed)
  93. };
  94. drop(conn);
  95. Ok(())
  96. }
  97. /// Generate a new statement for provided query and bind the provided params,
  98. /// returning the raw SQL query as a string.
  99. pub fn create_prepared_statement(
  100. &self,
  101. query: &str,
  102. params: &[&dyn ToSql],
  103. ) -> WalletDbResult<String> {
  104. debug!(target: "walletdb::create_prepared_statement", "[WalletDb] Preparing statement for SQL query:\n{query}");
  105. let Ok(conn) = self.conn.lock() else { return Err(WalletDbError::FailedToAquireLock) };
  106. // First we prepare the query
  107. let Ok(mut stmt) = conn.prepare(query) else {
  108. return Err(WalletDbError::QueryPreparationFailed)
  109. };
  110. // Bind all provided params
  111. for (index, param) in params.iter().enumerate() {
  112. if stmt.raw_bind_parameter(index + 1, param).is_err() {
  113. return Err(WalletDbError::QueryPreparationFailed)
  114. };
  115. }
  116. // Grab the raw SQL
  117. let query = stmt.expanded_sql().unwrap();
  118. // Drop statement and the connection lock
  119. drop(stmt);
  120. drop(conn);
  121. Ok(query)
  122. }
  123. /// Generate a `SELECT` query for provided table from selected column names and
  124. /// provided `WHERE` clauses. Named parameters are supported in the `WHERE` clauses,
  125. /// assuming they follow the normal formatting ":{column_name}".
  126. fn generate_select_query(
  127. &self,
  128. table: &str,
  129. col_names: &[&str],
  130. params: &[(&str, &dyn ToSql)],
  131. ) -> String {
  132. let mut query = if col_names.is_empty() {
  133. format!("SELECT * FROM {table}")
  134. } else {
  135. format!("SELECT {} FROM {table}", col_names.join(", "))
  136. };
  137. if params.is_empty() {
  138. return query
  139. }
  140. let mut where_str = Vec::with_capacity(params.len());
  141. for (k, _) in params {
  142. let col = &k[1..];
  143. where_str.push(format!("{col} = {k}"));
  144. }
  145. query.push_str(&format!(" WHERE {}", where_str.join(" AND ")));
  146. query
  147. }
  148. /// Query provided table from selected column names and provided `WHERE` clauses,
  149. /// for a single row.
  150. pub fn query_single(
  151. &self,
  152. table: &str,
  153. col_names: &[&str],
  154. params: &[(&str, &dyn ToSql)],
  155. ) -> WalletDbResult<Vec<Value>> {
  156. // Generate `SELECT` query
  157. let query = self.generate_select_query(table, col_names, params);
  158. debug!(target: "walletdb::query_single", "[WalletDb] Executing SQL query:\n{query}");
  159. // First we prepare the query
  160. let Ok(conn) = self.conn.lock() else { return Err(WalletDbError::FailedToAquireLock) };
  161. let Ok(mut stmt) = conn.prepare(&query) else {
  162. return Err(WalletDbError::QueryPreparationFailed)
  163. };
  164. // Execute the query using provided params
  165. let Ok(mut rows) = stmt.query(params) else {
  166. return Err(WalletDbError::QueryExecutionFailed)
  167. };
  168. // Check if row exists
  169. let Ok(next) = rows.next() else { return Err(WalletDbError::QueryExecutionFailed) };
  170. let row = match next {
  171. Some(row_result) => row_result,
  172. None => return Err(WalletDbError::RowNotFound),
  173. };
  174. // Grab returned values
  175. let mut result = vec![];
  176. if col_names.is_empty() {
  177. let mut idx = 0;
  178. loop {
  179. let Ok(value) = row.get(idx) else { break };
  180. result.push(value);
  181. idx += 1;
  182. }
  183. } else {
  184. for col in col_names {
  185. let Ok(value) = row.get(*col) else {
  186. return Err(WalletDbError::ParseColumnValueError)
  187. };
  188. result.push(value);
  189. }
  190. }
  191. Ok(result)
  192. }
  193. /// Query provided table from selected column names and provided `WHERE` clauses,
  194. /// for multiple rows.
  195. pub fn query_multiple(
  196. &self,
  197. table: &str,
  198. col_names: &[&str],
  199. params: &[(&str, &dyn ToSql)],
  200. ) -> WalletDbResult<Vec<Vec<Value>>> {
  201. // Generate `SELECT` query
  202. let query = self.generate_select_query(table, col_names, params);
  203. debug!(target: "walletdb::query_multiple", "[WalletDb] Executing SQL query:\n{query}");
  204. // First we prepare the query
  205. let Ok(conn) = self.conn.lock() else { return Err(WalletDbError::FailedToAquireLock) };
  206. let Ok(mut stmt) = conn.prepare(&query) else {
  207. return Err(WalletDbError::QueryPreparationFailed)
  208. };
  209. // Execute the query using provided converted params
  210. let Ok(mut rows) = stmt.query(params) else {
  211. return Err(WalletDbError::QueryExecutionFailed)
  212. };
  213. // Loop over returned rows and parse them
  214. let mut result = vec![];
  215. loop {
  216. // Check if an error occured
  217. let row = match rows.next() {
  218. Ok(r) => r,
  219. Err(_) => return Err(WalletDbError::QueryExecutionFailed),
  220. };
  221. // Check if no row was returned
  222. let row = match row {
  223. Some(r) => r,
  224. None => break,
  225. };
  226. // Grab row returned values
  227. let mut row_values = vec![];
  228. if col_names.is_empty() {
  229. let mut idx = 0;
  230. loop {
  231. let Ok(value) = row.get(idx) else { break };
  232. row_values.push(value);
  233. idx += 1;
  234. }
  235. } else {
  236. for col in col_names {
  237. let Ok(value) = row.get(*col) else {
  238. return Err(WalletDbError::ParseColumnValueError)
  239. };
  240. row_values.push(value);
  241. }
  242. }
  243. result.push(row_values);
  244. }
  245. Ok(result)
  246. }
  247. /// Query provided table using provided query for multiple rows.
  248. pub fn query_custom(
  249. &self,
  250. query: &str,
  251. params: &[&dyn ToSql],
  252. ) -> WalletDbResult<Vec<Vec<Value>>> {
  253. debug!(target: "walletdb::query_custom", "[WalletDb] Executing SQL query:\n{query}");
  254. // First we prepare the query
  255. let Ok(conn) = self.conn.lock() else { return Err(WalletDbError::FailedToAquireLock) };
  256. let Ok(mut stmt) = conn.prepare(query) else {
  257. return Err(WalletDbError::QueryPreparationFailed)
  258. };
  259. // Execute the query using provided converted params
  260. let Ok(mut rows) = stmt.query(params) else {
  261. return Err(WalletDbError::QueryExecutionFailed)
  262. };
  263. // Loop over returned rows and parse them
  264. let mut result = vec![];
  265. loop {
  266. // Check if an error occured
  267. let row = match rows.next() {
  268. Ok(r) => r,
  269. Err(_) => return Err(WalletDbError::QueryExecutionFailed),
  270. };
  271. // Check if no row was returned
  272. let row = match row {
  273. Some(r) => r,
  274. None => break,
  275. };
  276. // Grab row returned values
  277. let mut row_values = vec![];
  278. let mut idx = 0;
  279. loop {
  280. let Ok(value) = row.get(idx) else { break };
  281. row_values.push(value);
  282. idx += 1;
  283. }
  284. result.push(row_values);
  285. }
  286. Ok(result)
  287. }
  288. }
  289. /// Custom implementation of rusqlite::named_params! to use `expr` instead of `literal` as `$param_name`,
  290. /// and append the ":" named parameters prefix.
  291. #[macro_export]
  292. macro_rules! convert_named_params {
  293. () => {
  294. &[] as &[(&str, &dyn rusqlite::types::ToSql)]
  295. };
  296. ($(($param_name:expr, $param_val:expr)),+ $(,)?) => {
  297. &[$((format!(":{}", $param_name).as_str(), &$param_val as &dyn rusqlite::types::ToSql)),+] as &[(&str, &dyn rusqlite::types::ToSql)]
  298. };
  299. }
  300. #[cfg(test)]
  301. mod tests {
  302. use rusqlite::types::Value;
  303. use crate::walletdb::WalletDb;
  304. #[test]
  305. fn test_mem_wallet() {
  306. let wallet = WalletDb::new(None, Some("foobar")).unwrap();
  307. wallet
  308. .exec_batch_sql(
  309. "CREATE TABLE mista ( numba INTEGER ); INSERT INTO mista ( numba ) VALUES ( 42 );",
  310. )
  311. .unwrap();
  312. let ret = wallet.query_single("mista", &["numba"], &[]).unwrap();
  313. assert_eq!(ret.len(), 1);
  314. let numba: i64 = if let Value::Integer(numba) = ret[0] { numba } else { -1 };
  315. assert_eq!(numba, 42);
  316. let ret = wallet.query_custom("SELECT numba FROM mista;", &[]).unwrap();
  317. assert_eq!(ret.len(), 1);
  318. assert_eq!(ret[0].len(), 1);
  319. let numba: i64 = if let Value::Integer(numba) = ret[0][0] { numba } else { -1 };
  320. assert_eq!(numba, 42);
  321. }
  322. #[test]
  323. fn test_query_single() {
  324. let wallet = WalletDb::new(None, None).unwrap();
  325. wallet
  326. .exec_batch_sql("CREATE TABLE mista ( why INTEGER, are TEXT, you INTEGER, gae BLOB );")
  327. .unwrap();
  328. let why = 42;
  329. let are = "are".to_string();
  330. let you = 69;
  331. let gae = vec![42u8; 32];
  332. wallet
  333. .exec_sql(
  334. "INSERT INTO mista ( why, are, you, gae ) VALUES (?1, ?2, ?3, ?4);",
  335. rusqlite::params![why, are, you, gae],
  336. )
  337. .unwrap();
  338. let ret = wallet.query_single("mista", &["why", "are", "you", "gae"], &[]).unwrap();
  339. assert_eq!(ret.len(), 4);
  340. assert_eq!(ret[0], Value::Integer(why));
  341. assert_eq!(ret[1], Value::Text(are.clone()));
  342. assert_eq!(ret[2], Value::Integer(you));
  343. assert_eq!(ret[3], Value::Blob(gae.clone()));
  344. let ret = wallet.query_custom("SELECT why, are, you, gae FROM mista;", &[]).unwrap();
  345. assert_eq!(ret.len(), 1);
  346. assert_eq!(ret[0].len(), 4);
  347. assert_eq!(ret[0][0], Value::Integer(why));
  348. assert_eq!(ret[0][1], Value::Text(are.clone()));
  349. assert_eq!(ret[0][2], Value::Integer(you));
  350. assert_eq!(ret[0][3], Value::Blob(gae.clone()));
  351. let ret = wallet
  352. .query_single(
  353. "mista",
  354. &["gae"],
  355. rusqlite::named_params! {":why": why, ":are": are, ":you": you},
  356. )
  357. .unwrap();
  358. assert_eq!(ret.len(), 1);
  359. assert_eq!(ret[0], Value::Blob(gae.clone()));
  360. let ret = wallet
  361. .query_custom(
  362. "SELECT gae FROM mista WHERE why = ?1 AND are = ?2 AND you = ?3;",
  363. rusqlite::params![why, are, you],
  364. )
  365. .unwrap();
  366. assert_eq!(ret.len(), 1);
  367. assert_eq!(ret[0].len(), 1);
  368. assert_eq!(ret[0][0], Value::Blob(gae));
  369. }
  370. #[test]
  371. fn test_query_multi() {
  372. let wallet = WalletDb::new(None, None).unwrap();
  373. wallet
  374. .exec_batch_sql("CREATE TABLE mista ( why INTEGER, are TEXT, you INTEGER, gae BLOB );")
  375. .unwrap();
  376. let why = 42;
  377. let are = "are".to_string();
  378. let you = 69;
  379. let gae = vec![42u8; 32];
  380. wallet
  381. .exec_sql(
  382. "INSERT INTO mista ( why, are, you, gae ) VALUES (?1, ?2, ?3, ?4);",
  383. rusqlite::params![why, are, you, gae],
  384. )
  385. .unwrap();
  386. wallet
  387. .exec_sql(
  388. "INSERT INTO mista ( why, are, you, gae ) VALUES (?1, ?2, ?3, ?4);",
  389. rusqlite::params![why, are, you, gae],
  390. )
  391. .unwrap();
  392. let ret = wallet.query_multiple("mista", &[], &[]).unwrap();
  393. assert_eq!(ret.len(), 2);
  394. for row in ret {
  395. assert_eq!(row.len(), 4);
  396. assert_eq!(row[0], Value::Integer(why));
  397. assert_eq!(row[1], Value::Text(are.clone()));
  398. assert_eq!(row[2], Value::Integer(you));
  399. assert_eq!(row[3], Value::Blob(gae.clone()));
  400. }
  401. let ret = wallet.query_custom("SELECT * FROM mista;", &[]).unwrap();
  402. assert_eq!(ret.len(), 2);
  403. for row in ret {
  404. assert_eq!(row.len(), 4);
  405. assert_eq!(row[0], Value::Integer(why));
  406. assert_eq!(row[1], Value::Text(are.clone()));
  407. assert_eq!(row[2], Value::Integer(you));
  408. assert_eq!(row[3], Value::Blob(gae.clone()));
  409. }
  410. let ret = wallet
  411. .query_multiple(
  412. "mista",
  413. &["gae"],
  414. convert_named_params! {("why", why), ("are", are), ("you", you)},
  415. )
  416. .unwrap();
  417. assert_eq!(ret.len(), 2);
  418. for row in ret {
  419. assert_eq!(row.len(), 1);
  420. assert_eq!(row[0], Value::Blob(gae.clone()));
  421. }
  422. let ret = wallet
  423. .query_custom(
  424. "SELECT gae FROM mista WHERE why = ?1 AND are = ?2 AND you = ?3;",
  425. rusqlite::params![why, are, you],
  426. )
  427. .unwrap();
  428. assert_eq!(ret.len(), 2);
  429. for row in ret {
  430. assert_eq!(row.len(), 1);
  431. assert_eq!(row[0], Value::Blob(gae.clone()));
  432. }
  433. }
  434. }