소스 검색

drk2: retrieval of multiple db records added

aggstam 2 년 전
부모
커밋
6ba1fb5947
5개의 변경된 파일476개의 추가작업 그리고 35개의 파일을 삭제
  1. 2 0
      bin/drk2/Cargo.toml
  2. 1 5
      bin/drk2/src/dao.rs
  3. 54 1
      bin/drk2/src/main.rs
  4. 248 9
      bin/drk2/src/money.rs
  5. 171 20
      bin/drk2/src/walletdb.rs

+ 2 - 0
bin/drk2/Cargo.toml

@@ -18,6 +18,8 @@ darkfi-serial = {path = "../../src/serial"}
 
 
 # Misc
 # Misc
 log = "0.4.20"
 log = "0.4.20"
+prettytable-rs = "0.10.0"
+rand = "0.8.5"
 rodio = {version = "0.17.3", default-features = false, features = ["minimp3"]}
 rodio = {version = "0.17.3", default-features = false, features = ["minimp3"]}
 rusqlite = {version = "0.30.0", features = ["sqlcipher"]}
 rusqlite = {version = "0.30.0", features = ["sqlcipher"]}
 url = "2.5.0"
 url = "2.5.0"

+ 1 - 5
bin/drk2/src/dao.rs

@@ -42,11 +42,7 @@ impl Drk {
         // a bit better and safer.
         // a bit better and safer.
         // For now, on success, we don't care what's returned, but in the future
         // For now, on success, we don't care what's returned, but in the future
         // we should actually check it.
         // we should actually check it.
-        if self
-            .wallet
-            .query_single(DAO_TREES_TABLE, vec![DAO_TREES_COL_DAOS_TREE], &[])
-            .await
-            .is_err()
+        if self.wallet.query_single(DAO_TREES_TABLE, &[DAO_TREES_COL_DAOS_TREE], &[]).await.is_err()
         {
         {
             eprintln!("Initializing DAO Merkle trees");
             eprintln!("Initializing DAO Merkle trees");
             let tree = MerkleTree::new(100);
             let tree = MerkleTree::new(100);

+ 54 - 1
bin/drk2/src/main.rs

@@ -18,6 +18,7 @@
 
 
 use std::{fs, process::exit, sync::Arc, time::Instant};
 use std::{fs, process::exit, sync::Arc, time::Instant};
 
 
+use prettytable::{format, row, Table};
 use smol::stream::StreamExt;
 use smol::stream::StreamExt;
 use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml};
 use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml};
 use url::Url;
 use url::Url;
@@ -25,7 +26,7 @@ use url::Url;
 use darkfi::{
 use darkfi::{
     async_daemonize, cli_desc,
     async_daemonize, cli_desc,
     rpc::{client::RpcClient, jsonrpc::JsonRequest, util::JsonValue},
     rpc::{client::RpcClient, jsonrpc::JsonRequest, util::JsonValue},
-    util::path::expand_path,
+    util::{parse::encode_base10, path::expand_path},
     Result,
     Result,
 };
 };
 
 
@@ -38,6 +39,7 @@ use cli_util::kaching;
 
 
 /// Wallet functionality related to Money
 /// Wallet functionality related to Money
 mod money;
 mod money;
+use money::BALANCE_BASE10_DECIMALS;
 
 
 /// Wallet functionality related to Dao
 /// Wallet functionality related to Dao
 mod dao;
 mod dao;
@@ -109,6 +111,14 @@ enum Subcmd {
         /// Get the default address in the wallet
         /// Get the default address in the wallet
         address: bool,
         address: bool,
 
 
+        #[structopt(long)]
+        /// Print all the addresses in the wallet
+        addresses: bool,
+
+        #[structopt(long)]
+        /// Set the default address in the wallet
+        default_address: Option<usize>,
+
         #[structopt(long)]
         #[structopt(long)]
         /// Print all the secret keys from the wallet
         /// Print all the secret keys from the wallet
         secrets: bool,
         secrets: bool,
@@ -205,6 +215,8 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
             keygen,
             keygen,
             balance,
             balance,
             address,
             address,
+            addresses,
+            default_address,
             secrets,
             secrets,
             import_secrets,
             import_secrets,
             tree,
             tree,
@@ -214,6 +226,8 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
                 !keygen &&
                 !keygen &&
                 !balance &&
                 !balance &&
                 !address &&
                 !address &&
+                !addresses &&
+                default_address.is_none() &&
                 !secrets &&
                 !secrets &&
                 !tree &&
                 !tree &&
                 !coins &&
                 !coins &&
@@ -233,6 +247,45 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
                 return Ok(())
                 return Ok(())
             }
             }
 
 
+            if keygen {
+                if let Err(e) = drk.money_keygen().await {
+                    eprintln!("Failed to generate keypair: {e:?}");
+                    exit(2);
+                }
+                return Ok(())
+            }
+
+            if balance {
+                let balmap = drk.money_balance().await?;
+
+                let aliases_map = drk.get_aliases_mapped_by_token().await?;
+
+                // Create a prettytable with the new data:
+                let mut table = Table::new();
+                table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
+                table.set_titles(row!["Token ID", "Aliases", "Balance"]);
+                for (token_id, balance) in balmap.iter() {
+                    let aliases = match aliases_map.get(token_id) {
+                        Some(a) => a,
+                        None => "-",
+                    };
+
+                    table.add_row(row![
+                        token_id,
+                        aliases,
+                        encode_base10(*balance, BALANCE_BASE10_DECIMALS)
+                    ]);
+                }
+
+                if table.is_empty() {
+                    println!("No unspent balances found");
+                } else {
+                    println!("{}", table);
+                }
+
+                return Ok(())
+            }
+
             // TODO
             // TODO
 
 
             Ok(())
             Ok(())

+ 248 - 9
bin/drk2/src/money.rs

@@ -16,25 +16,36 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
  */
 
 
-use std::process::exit;
+use std::{collections::HashMap, process::exit};
 
 
+use rand::rngs::OsRng;
 use rusqlite::types::Value;
 use rusqlite::types::Value;
 
 
-use darkfi::{zk::halo2::Field, Result};
-use darkfi_money_contract::client::{
-    MONEY_INFO_COL_LAST_SCANNED_SLOT, MONEY_INFO_TABLE, MONEY_TREE_COL_TREE, MONEY_TREE_TABLE,
+use darkfi::{zk::halo2::Field, Error, Result};
+use darkfi_money_contract::{
+    client::{
+        MoneyNote, OwnCoin, MONEY_ALIASES_TABLE, MONEY_COINS_COL_IS_SPENT, MONEY_COINS_TABLE,
+        MONEY_INFO_COL_LAST_SCANNED_SLOT, MONEY_INFO_TABLE, MONEY_KEYS_COL_IS_DEFAULT,
+        MONEY_KEYS_COL_PUBLIC, MONEY_KEYS_COL_SECRET, MONEY_KEYS_TABLE, MONEY_TREE_COL_TREE,
+        MONEY_TREE_TABLE,
+    },
+    model::Coin,
 };
 };
 use darkfi_sdk::{
 use darkfi_sdk::{
-    crypto::{MerkleNode, MerkleTree},
+    bridgetree,
+    crypto::{Keypair, MerkleNode, MerkleTree, Nullifier, SecretKey, TokenId},
     pasta::pallas,
     pasta::pallas,
 };
 };
-use darkfi_serial::serialize;
+use darkfi_serial::{deserialize, serialize};
 
 
 use crate::{
 use crate::{
+    convert_named_params,
     error::{WalletDbError, WalletDbResult},
     error::{WalletDbError, WalletDbResult},
     Drk,
     Drk,
 };
 };
 
 
+pub const BALANCE_BASE10_DECIMALS: usize = 8;
+
 impl Drk {
 impl Drk {
     /// Initialize wallet with tables for the Money contract
     /// Initialize wallet with tables for the Money contract
     pub async fn initialize_money(&self) -> Result<()> {
     pub async fn initialize_money(&self) -> Result<()> {
@@ -50,8 +61,7 @@ impl Drk {
         // new tree and push it into the table.
         // new tree and push it into the table.
         // For now, on success, we don't care what's returned, but in the future
         // For now, on success, we don't care what's returned, but in the future
         // we should actually check it.
         // we should actually check it.
-        if self.wallet.query_single(MONEY_TREE_TABLE, vec![MONEY_TREE_COL_TREE], &[]).await.is_err()
-        {
+        if self.wallet.query_single(MONEY_TREE_TABLE, &[MONEY_TREE_COL_TREE], &[]).await.is_err() {
             eprintln!("Initializing Money Merkle tree");
             eprintln!("Initializing Money Merkle tree");
             let mut tree = MerkleTree::new(100);
             let mut tree = MerkleTree::new(100);
             tree.append(MerkleNode::from(pallas::Base::ZERO));
             tree.append(MerkleNode::from(pallas::Base::ZERO));
@@ -76,6 +86,235 @@ impl Drk {
         Ok(())
         Ok(())
     }
     }
 
 
+    /// Generate a new keypair and place it into the wallet.
+    pub async fn money_keygen(&self) -> Result<()> {
+        eprintln!("Generating a new keypair");
+
+        // TODO: We might want to have hierarchical deterministic key derivation.
+        let keypair = Keypair::random(&mut OsRng);
+        let is_default = 0;
+
+        let query = format!(
+            "INSERT INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3);",
+            MONEY_KEYS_TABLE,
+            MONEY_KEYS_COL_IS_DEFAULT,
+            MONEY_KEYS_COL_PUBLIC,
+            MONEY_KEYS_COL_SECRET
+        );
+        if let Err(e) = self
+            .wallet
+            .exec_sql(
+                &query,
+                rusqlite::params![
+                    is_default,
+                    serialize(&keypair.public),
+                    serialize(&keypair.secret)
+                ],
+            )
+            .await
+        {
+            eprintln!("Error inserting new keypair: {e:?}");
+            exit(2);
+        }
+
+        eprintln!("New address:");
+        println!("{}", keypair.public);
+
+        Ok(())
+    }
+
+    /// Fetch known unspent balances from the wallet and return them as a hashmap.
+    pub async fn money_balance(&self) -> Result<HashMap<String, u64>> {
+        let mut coins = self.get_coins(false).await?;
+        coins.retain(|x| x.0.note.spend_hook == pallas::Base::zero());
+
+        // Fill this map with balances
+        let mut balmap: HashMap<String, u64> = HashMap::new();
+
+        for coin in coins {
+            let mut value = coin.0.note.value;
+
+            if let Some(prev) = balmap.get(&coin.0.note.token_id.to_string()) {
+                value += prev;
+            }
+
+            balmap.insert(coin.0.note.token_id.to_string(), value);
+        }
+
+        Ok(balmap)
+    }
+
+    /// Fetch all coins and their metadata related to the Money contract from the wallet.
+    /// Optionally also fetch spent ones.
+    /// The boolean in the returned tuple notes if the coin was marked as spent.
+    pub async fn get_coins(&self, fetch_spent: bool) -> Result<Vec<(OwnCoin, bool)>> {
+        let query = if fetch_spent {
+            self.wallet.query_multiple(MONEY_COINS_TABLE, &[], &[]).await
+        } else {
+            self.wallet
+                .query_multiple(
+                    MONEY_COINS_TABLE,
+                    &[],
+                    convert_named_params! {(MONEY_COINS_COL_IS_SPENT, false)},
+                )
+                .await
+        };
+
+        let rows = match query {
+            Ok(r) => r,
+            Err(e) => {
+                return Err(Error::RusqliteError(format!(
+                    "[get_coins] Coins retrieval failed: {e:?}"
+                )))
+            }
+        };
+
+        let mut owncoins = Vec::with_capacity(rows.len());
+
+        for row in rows {
+            let Value::Blob(ref coin_bytes) = row[0] else {
+                return Err(Error::ParseFailed("[get_coins] Coin bytes parsing failed"))
+            };
+            let coin: Coin = deserialize(coin_bytes)?;
+
+            let Value::Integer(is_spent) = row[1] else {
+                return Err(Error::ParseFailed("[get_coins] Is spent parsing failed"))
+            };
+            let Ok(is_spent) = u64::try_from(is_spent) else {
+                return Err(Error::ParseFailed("[get_coins] Is spent parsing failed"))
+            };
+            let is_spent = is_spent > 0;
+
+            let Value::Blob(ref serial_bytes) = row[2] else {
+                return Err(Error::ParseFailed("[get_coins] Serial bytes parsing failed"))
+            };
+            let serial: pallas::Base = deserialize(serial_bytes)?;
+
+            let Value::Blob(ref value_bytes) = row[3] else {
+                return Err(Error::ParseFailed("[get_coins] Value bytes parsing failed"))
+            };
+            let value: u64 = deserialize(value_bytes)?;
+
+            let Value::Blob(ref token_id_bytes) = row[4] else {
+                return Err(Error::ParseFailed("[get_coins] Token ID bytes parsing failed"))
+            };
+            let token_id: TokenId = deserialize(token_id_bytes)?;
+
+            let Value::Blob(ref spend_hook_bytes) = row[5] else {
+                return Err(Error::ParseFailed("[get_coins] Spend hook bytes parsing failed"))
+            };
+            let spend_hook: pallas::Base = deserialize(spend_hook_bytes)?;
+
+            let Value::Blob(ref user_data_bytes) = row[6] else {
+                return Err(Error::ParseFailed("[get_coins] User data bytes parsing failed"))
+            };
+            let user_data: pallas::Base = deserialize(user_data_bytes)?;
+
+            let Value::Blob(ref value_blind_bytes) = row[7] else {
+                return Err(Error::ParseFailed("[get_coins] Value blind bytes parsing failed"))
+            };
+            let value_blind: pallas::Scalar = deserialize(value_blind_bytes)?;
+
+            let Value::Blob(ref token_blind_bytes) = row[8] else {
+                return Err(Error::ParseFailed("[get_coins] Token blind bytes parsing failed"))
+            };
+            let token_blind: pallas::Base = deserialize(token_blind_bytes)?;
+
+            let Value::Blob(ref secret_bytes) = row[9] else {
+                return Err(Error::ParseFailed("[get_coins] Secret bytes parsing failed"))
+            };
+            let secret: SecretKey = deserialize(secret_bytes)?;
+
+            let Value::Blob(ref nullifier_bytes) = row[10] else {
+                return Err(Error::ParseFailed("[get_coins] Nullifier bytes parsing failed"))
+            };
+            let nullifier: Nullifier = deserialize(nullifier_bytes)?;
+
+            let Value::Blob(ref leaf_position_bytes) = row[11] else {
+                return Err(Error::ParseFailed("[get_coins] Leaf position bytes parsing failed"))
+            };
+            let leaf_position: bridgetree::Position = deserialize(leaf_position_bytes)?;
+
+            let Value::Blob(ref memo) = row[12] else {
+                return Err(Error::ParseFailed("[get_coins] Memo parsing failed"))
+            };
+
+            let note = MoneyNote {
+                serial,
+                value,
+                token_id,
+                spend_hook,
+                user_data,
+                value_blind,
+                token_blind,
+                memo: memo.clone(),
+            };
+            let owncoin = OwnCoin { coin, note, secret, nullifier, leaf_position };
+
+            owncoins.push((owncoin, is_spent))
+        }
+
+        Ok(owncoins)
+    }
+
+    /// Fetch all aliases from the wallet.
+    /// Optionally filter using alias name and/or token id.
+    pub async fn get_aliases(
+        &self,
+        alias_filter: Option<String>,
+        token_id_filter: Option<TokenId>,
+    ) -> Result<HashMap<String, TokenId>> {
+        let rows = match self.wallet.query_multiple(MONEY_ALIASES_TABLE, &[], &[]).await {
+            Ok(r) => r,
+            Err(e) => {
+                return Err(Error::RusqliteError(format!(
+                    "[get_aliases] Aliases retrieval failed: {e:?}"
+                )))
+            }
+        };
+
+        // Fill this map with aliases
+        let mut map: HashMap<String, TokenId> = HashMap::new();
+        for row in rows {
+            let Value::Blob(ref alias_bytes) = row[0] else {
+                return Err(Error::ParseFailed("[get_aliases] Alias bytes parsing failed"))
+            };
+            let alias: String = deserialize(alias_bytes)?;
+            if alias_filter.is_some() && alias_filter.as_ref().unwrap() != &alias {
+                continue
+            }
+
+            let Value::Blob(ref token_id_bytes) = row[1] else {
+                return Err(Error::ParseFailed("[get_aliases] TokenId bytes parsing failed"))
+            };
+            let token_id: TokenId = deserialize(token_id_bytes)?;
+            if token_id_filter.is_some() && token_id_filter.as_ref().unwrap() != &token_id {
+                continue
+            }
+
+            map.insert(alias, token_id);
+        }
+
+        Ok(map)
+    }
+
+    /// Fetch all aliases from the wallet, mapped by token id.
+    pub async fn get_aliases_mapped_by_token(&self) -> Result<HashMap<String, String>> {
+        let aliases = self.get_aliases(None, None).await?;
+        let mut map: HashMap<String, String> = HashMap::new();
+        for (alias, token_id) in aliases {
+            let aliases_string = if let Some(prev) = map.get(&token_id.to_string()) {
+                format!("{}, {}", prev, alias)
+            } else {
+                alias
+            };
+
+            map.insert(token_id.to_string(), aliases_string);
+        }
+
+        Ok(map)
+    }
+
     /// Replace the Money Merkle tree in the wallet.
     /// Replace the Money Merkle tree in the wallet.
     pub async fn put_money_tree(&self, tree: &MerkleTree) -> Result<()> {
     pub async fn put_money_tree(&self, tree: &MerkleTree) -> Result<()> {
         // First we remove old record
         // First we remove old record
@@ -100,7 +339,7 @@ impl Drk {
     pub async fn last_scanned_slot(&self) -> WalletDbResult<u64> {
     pub async fn last_scanned_slot(&self) -> WalletDbResult<u64> {
         let ret = self
         let ret = self
             .wallet
             .wallet
-            .query_single(MONEY_INFO_TABLE, vec![MONEY_INFO_COL_LAST_SCANNED_SLOT], &[])
+            .query_single(MONEY_INFO_TABLE, &[MONEY_INFO_COL_LAST_SCANNED_SLOT], &[])
             .await?;
             .await?;
         let Value::Integer(slot) = ret[0] else {
         let Value::Integer(slot) = ret[0] else {
             return Err(WalletDbError::ParseColumnValueError);
             return Err(WalletDbError::ParseColumnValueError);

+ 171 - 20
bin/drk2/src/walletdb.rs

@@ -64,7 +64,6 @@ impl WalletDb {
     /// that don't contain any parameters.
     /// that don't contain any parameters.
     pub async fn exec_batch_sql(&self, query: &str) -> WalletDbResult<()> {
     pub async fn exec_batch_sql(&self, query: &str) -> WalletDbResult<()> {
         debug!(target: "walletdb::exec_batch_sql", "[WalletDb] Executing batch SQL query:\n{query}");
         debug!(target: "walletdb::exec_batch_sql", "[WalletDb] Executing batch SQL query:\n{query}");
-        // If no params are provided, execute directly
         if let Err(e) = self.conn.lock().await.execute_batch(query) {
         if let Err(e) = self.conn.lock().await.execute_batch(query) {
             error!(target: "walletdb::exec_batch_sql", "[WalletDb] Query failed: {e}");
             error!(target: "walletdb::exec_batch_sql", "[WalletDb] Query failed: {e}");
             return Err(WalletDbError::QueryExecutionFailed)
             return Err(WalletDbError::QueryExecutionFailed)
@@ -89,7 +88,6 @@ impl WalletDb {
         // First we prepare the query
         // First we prepare the query
         let conn = self.conn.lock().await;
         let conn = self.conn.lock().await;
         let Ok(mut stmt) = conn.prepare(query) else {
         let Ok(mut stmt) = conn.prepare(query) else {
-            eprintln!("Error: {:?}", conn.prepare(query));
             return Err(WalletDbError::QueryPreparationFailed)
             return Err(WalletDbError::QueryPreparationFailed)
         };
         };
 
 
@@ -109,25 +107,44 @@ impl WalletDb {
         Ok(())
         Ok(())
     }
     }
 
 
-    /// Query 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}"
+    /// 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: &[(&str, &dyn ToSql)],
+    ) -> String {
+        let mut query = if col_names.is_empty() {
+            format!("SELECT * FROM {}", table)
+        } else {
+            format!("SELECT {} FROM {}", col_names.join(", "), table)
+        };
+        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(
     pub async fn query_single(
         &self,
         &self,
         table: &str,
         table: &str,
-        col_names: Vec<&str>,
+        col_names: &[&str],
         params: &[(&str, &dyn ToSql)],
         params: &[(&str, &dyn ToSql)],
     ) -> WalletDbResult<Vec<Value>> {
     ) -> WalletDbResult<Vec<Value>> {
         // Generate `SELECT` query
         // Generate `SELECT` query
-        let mut query = format!("SELECT {} FROM {}", col_names.join(", "), table);
-        if !params.is_empty() {
-            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 ")));
-        };
+        let query = self.generate_select_query(table, col_names, params);
         debug!(target: "walletdb::query_single", "[WalletDb] Executing SQL query:\n{query}");
         debug!(target: "walletdb::query_single", "[WalletDb] Executing SQL query:\n{query}");
 
 
         // First we prepare the query
         // First we prepare the query
@@ -151,12 +168,88 @@ impl WalletDb {
         // Grab returned values
         // Grab returned values
         let mut result = vec![];
         let mut result = vec![];
         for col in col_names {
         for col in col_names {
-            let Ok(value) = row.get(col) else { return Err(WalletDbError::ParseColumnValueError) };
+            let Ok(value) = row.get(*col) else { return Err(WalletDbError::ParseColumnValueError) };
             result.push(value);
             result.push(value);
         }
         }
 
 
         Ok(result)
         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: &[(&str, &dyn ToSql)],
+    ) -> WalletDbResult<Vec<Vec<Value>>> {
+        // Generate `SELECT` query
+        let query = self.generate_select_query(table, col_names, params);
+        debug!(target: "walletdb::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) else {
+            return Err(WalletDbError::QueryPreparationFailed)
+        };
+
+        // Execute the query using provided converted params
+        let Ok(mut rows) = stmt.query(params) else {
+            if let Err(e) = stmt.query(params) {
+                println!("eeer: {e:?}");
+            }
+            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() {
+                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(idx) else { break };
+                    row_values.push(value);
+                    idx += 1;
+                }
+            } else {
+                for col in col_names {
+                    let Ok(value) = row.get(*col) else {
+                        return Err(WalletDbError::ParseColumnValueError)
+                    };
+                    row_values.push(value);
+                }
+            }
+            result.push(row_values);
+        }
+
+        Ok(result)
+    }
+}
+
+/// Custom implementation of rusqlite::named_params! to use `expr` instead of `literal` as `$param_name`,
+/// and append the ":" named parameters prefix.
+#[macro_export]
+macro_rules! convert_named_params {
+    () => {
+        &[] as &[(&str, &dyn rusqlite::types::ToSql)]
+    };
+    ($(($param_name:expr, $param_val:expr)),+ $(,)?) => {
+        &[$((format!(":{}", $param_name).as_str(), &$param_val as &dyn rusqlite::types::ToSql)),+] as &[(&str, &dyn rusqlite::types::ToSql)]
+    };
 }
 }
 
 
 #[cfg(test)]
 #[cfg(test)]
@@ -172,7 +265,7 @@ mod tests {
             wallet.exec_sql("CREATE TABLE mista ( numba INTEGER );", &[]).await.unwrap();
             wallet.exec_sql("CREATE TABLE mista ( numba INTEGER );", &[]).await.unwrap();
             wallet.exec_sql("INSERT INTO mista ( numba ) VALUES ( 42 );", &[]).await.unwrap();
             wallet.exec_sql("INSERT INTO mista ( numba ) VALUES ( 42 );", &[]).await.unwrap();
 
 
-            let ret = wallet.query_single("mista", vec!["numba"], &[]).await.unwrap();
+            let ret = wallet.query_single("mista", &["numba"], &[]).await.unwrap();
             assert_eq!(ret.len(), 1);
             assert_eq!(ret.len(), 1);
             let numba: i64 = if let Value::Integer(numba) = ret[0] { numba } else { -1 };
             let numba: i64 = if let Value::Integer(numba) = ret[0] { numba } else { -1 };
             assert_eq!(numba, 42);
             assert_eq!(numba, 42);
@@ -205,7 +298,7 @@ mod tests {
                 .unwrap();
                 .unwrap();
 
 
             let ret =
             let ret =
-                wallet.query_single("mista", vec!["why", "are", "you", "gae"], &[]).await.unwrap();
+                wallet.query_single("mista", &["why", "are", "you", "gae"], &[]).await.unwrap();
             assert_eq!(ret.len(), 4);
             assert_eq!(ret.len(), 4);
             assert_eq!(ret[0], Value::Integer(why));
             assert_eq!(ret[0], Value::Integer(why));
             assert_eq!(ret[1], Value::Text(are.clone()));
             assert_eq!(ret[1], Value::Text(are.clone()));
@@ -215,8 +308,8 @@ mod tests {
             let ret = wallet
             let ret = wallet
                 .query_single(
                 .query_single(
                     "mista",
                     "mista",
-                    vec!["gae"],
-                    rusqlite::named_params! {":why" : why, ":are" : are, ":you" : you},
+                    &["gae"],
+                    rusqlite::named_params! {":why": why, ":are": are, ":you": you},
                 )
                 )
                 .await
                 .await
                 .unwrap();
                 .unwrap();
@@ -224,4 +317,62 @@ mod tests {
             assert_eq!(ret[0], Value::Blob(gae));
             assert_eq!(ret[0], Value::Blob(gae));
         });
         });
     }
     }
+
+    #[test]
+    fn test_query_multi() {
+        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];
+
+            wallet
+                .exec_sql(
+                    "INSERT INTO mista ( why, are, you, gae ) VALUES (?1, ?2, ?3, ?4);",
+                    rusqlite::params![why, are, you, gae],
+                )
+                .await
+                .unwrap();
+            wallet
+                .exec_sql(
+                    "INSERT INTO mista ( why, are, you, gae ) VALUES (?1, ?2, ?3, ?4);",
+                    rusqlite::params![why, are, you, gae],
+                )
+                .await
+                .unwrap();
+
+            let ret = wallet.query_multiple("mista", &[], &[]).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), ("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()));
+            }
+        });
+    }
 }
 }