Jelajahi Sumber

drk: tx history handling cleanup

skoupidi 2 tahun lalu
induk
melakukan
4101f8b608
7 mengubah file dengan 199 tambahan dan 138 penghapusan
  1. 2 8
      bin/drk/money.sql
  2. 35 6
      bin/drk/src/cli_util.rs
  3. 44 37
      bin/drk/src/main.rs
  4. 66 18
      bin/drk/src/money.rs
  5. 16 7
      bin/drk/src/rpc.rs
  6. 35 61
      bin/drk/src/txs_history.rs
  7. 1 1
      bin/drk/wallet.sql

+ 2 - 8
bin/drk/money.sql

@@ -33,7 +33,8 @@ CREATE TABLE IF NOT EXISTS BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o_money_co
 	secret BLOB NOT NULL,
 	nullifier BLOB NOT NULL,
 	leaf_position BLOB NOT NULL,
-	memo BLOB
+	memo BLOB,
+	spent_tx_hash TEXT DEFAULT '-'
 );
 
 -- Arbitrary tokens
@@ -49,10 +50,3 @@ CREATE TABLE IF NOT EXISTS BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o_money_al
 	alias BLOB PRIMARY KEY NOT NULL,
 	token_id BLOB NOT NULL
 );
-
-CREATE TABLE IF NOT EXISTS BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o_transactions_history (
-	id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
-	transaction_hash TEXT UNIQUE NOT NULL,
-	status TEXT NOT NULL,
-	tx TEXT UNIQUE NOT NULL
-);

+ 35 - 6
bin/drk/src/cli_util.rs

@@ -15,22 +15,46 @@
  * You should have received a copy of the GNU Affero General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
-use std::{io::Cursor, process::exit, str::FromStr};
+use std::{
+    io::{stdin, Cursor, Read},
+    process::exit,
+    str::FromStr,
+};
 
 use rodio::{source::Source, Decoder, OutputStream};
 use structopt_toml::clap::{App, Arg, Shell, SubCommand};
 
-use darkfi::{cli_desc, system::sleep, util::parse::decode_base10, Error, Result};
+use darkfi::{
+    cli_desc,
+    system::sleep,
+    tx::Transaction,
+    util::{encoding::base64, parse::decode_base10},
+    Error, Result,
+};
 use darkfi_money_contract::model::TokenId;
+use darkfi_serial::deserialize_async;
 
 use crate::{money::BALANCE_BASE10_DECIMALS, Drk};
 
+/// Auxiliary function to parse a base64 encoded transaction from stdin.
+pub async fn parse_tx_from_stdin() -> Result<Transaction> {
+    println!("Reading transaction from stdin...");
+    let mut buf = String::new();
+    stdin().read_to_string(&mut buf)?;
+    let Some(bytes) = base64::decode(buf.trim()) else {
+        eprintln!("Failed to decode transaction");
+        exit(2);
+    };
+
+    Ok(deserialize_async(&bytes).await?)
+}
+
 /// Auxiliary function to parse provided string into a values pair.
 pub fn parse_value_pair(s: &str) -> Result<(u64, u64)> {
     let v: Vec<&str> = s.split(':').collect();
     if v.len() != 2 {
         eprintln!("Invalid value pair. Use a pair such as 13.37:11.0");
-        exit(1);
+        exit(2);
     }
 
     let val0 = decode_base10(v[0], BALANCE_BASE10_DECIMALS, true);
@@ -38,7 +62,7 @@ pub fn parse_value_pair(s: &str) -> Result<(u64, u64)> {
 
     if val0.is_err() || val1.is_err() {
         eprintln!("Invalid value pair. Use a pair such as 13.37:11.0");
-        exit(1);
+        exit(2);
     }
 
     Ok((val0.unwrap(), val1.unwrap()))
@@ -52,7 +76,7 @@ pub async fn parse_token_pair(drk: &Drk, s: &str) -> Result<(TokenId, TokenId)>
         eprintln!("WCKD:MLDY");
         eprintln!("or");
         eprintln!("A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2");
-        exit(1);
+        exit(2);
     }
 
     let tok0 = drk.get_token(v[0].to_string()).await;
@@ -63,7 +87,7 @@ pub async fn parse_token_pair(drk: &Drk, s: &str) -> Result<(TokenId, TokenId)>
         eprintln!("WCKD:MLDY");
         eprintln!("or");
         eprintln!("A7f1RKsCUUHrSXA7a9ogmwg8p3bs6F47ggsW826HD4yd:FCuoMii64H5Ee4eVWBjP18WTFS8iLUJmGi16Qti1xFQ2");
-        exit(1);
+        exit(2);
     }
 
     Ok((tok0.unwrap(), tok1.unwrap()))
@@ -149,6 +173,10 @@ pub fn generate_completions(shell: &str) -> Result<()> {
         coins,
     ]);
 
+    // Spend
+    let spend = SubCommand::with_name("spend")
+        .about("Read a transaction from stdin and mark its input coins as spent");
+
     // Unspend
     let coin = Arg::with_name("coin").help("base58-encoded coin to mark as unspent");
 
@@ -435,6 +463,7 @@ pub fn generate_completions(shell: &str) -> Result<()> {
         ping,
         completions,
         wallet,
+        spend,
         unspend,
         transfer,
         otc,

+ 44 - 37
bin/drk/src/main.rs

@@ -69,7 +69,9 @@ mod token;
 
 /// CLI utility functions
 mod cli_util;
-use cli_util::{generate_completions, kaching, parse_token_pair, parse_value_pair};
+use cli_util::{
+    generate_completions, kaching, parse_token_pair, parse_tx_from_stdin, parse_value_pair,
+};
 
 /// Wallet functionality related to Money
 mod money;
@@ -186,6 +188,9 @@ enum Subcmd {
         coins: bool,
     },
 
+    /// Read a transaction from stdin and mark its input coins as spent
+    Spend,
+
     /// Unspend a coin
     Unspend {
         /// base58-encoded coin to mark as unspent
@@ -798,7 +803,8 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
                     "Aliases",
                     "Value",
                     "Spend Hook",
-                    "User Data"
+                    "User Data",
+                    "Spent TX"
                 ]);
                 for coin in coins {
                     let aliases = match aliases_map.get(&coin.0.note.token_id.to_string()) {
@@ -835,7 +841,8 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
                             encode_base10(coin.0.note.value, BALANCE_BASE10_DECIMALS)
                         ),
                         spend_hook,
-                        user_data
+                        user_data,
+                        coin.2
                     ]);
                 }
 
@@ -847,6 +854,19 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
             unreachable!()
         }
 
+        Subcmd::Spend => {
+            let tx = parse_tx_from_stdin().await?;
+
+            let drk = Drk::new(args.wallet_path, args.wallet_pass, Some(args.endpoint), ex).await?;
+
+            if let Err(e) = drk.mark_tx_spend(&tx).await {
+                eprintln!("Failed to mark transaction coins as spent: {e:?}");
+                exit(2);
+            };
+
+            Ok(())
+        }
+
         Subcmd::Unspend { coin } => {
             let bytes: [u8; 32] = match bs58::decode(&coin).into_vec()?.try_into() {
                 Ok(b) => b,
@@ -976,7 +996,7 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
                 stdin().read_to_string(&mut buf)?;
                 let Some(bytes) = base64::decode(buf.trim()) else {
                     eprintln!("Failed to decode swap transaction");
-                    exit(1);
+                    exit(2);
                 };
 
                 let mut tx: Transaction = deserialize_async(&bytes).await?;
@@ -1271,31 +1291,26 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
         },
 
         Subcmd::Inspect => {
-            let mut buf = String::new();
-            stdin().read_to_string(&mut buf)?;
-            let Some(bytes) = base64::decode(buf.trim()) else {
-                eprintln!("Failed to decode transaction");
-                exit(1);
-            };
-
-            let tx: Transaction = deserialize_async(&bytes).await?;
+            let tx = parse_tx_from_stdin().await?;
             println!("{tx:#?}");
             Ok(())
         }
 
         Subcmd::Broadcast => {
-            println!("Reading transaction from stdin...");
-            let mut buf = String::new();
-            stdin().read_to_string(&mut buf)?;
-            let Some(bytes) = base64::decode(buf.trim()) else {
-                eprintln!("Failed to decode transaction");
-                exit(1);
-            };
-
-            let tx = deserialize_async(&bytes).await?;
+            let tx = parse_tx_from_stdin().await?;
 
             let drk = Drk::new(args.wallet_path, args.wallet_pass, Some(args.endpoint), ex).await?;
 
+            if let Err(e) = drk.simulate_tx(&tx).await {
+                eprintln!("Failed to simulate tx: {e:?}");
+                exit(2);
+            };
+
+            if let Err(e) = drk.mark_tx_spend(&tx).await {
+                eprintln!("Failed to mark transaction coins as spent: {e:?}");
+                exit(2);
+            };
+
             let txid = match drk.broadcast_tx(&tx).await {
                 Ok(t) => t,
                 Err(e) => {
@@ -1398,15 +1413,7 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
             }
 
             ExplorerSubcmd::SimulateTx => {
-                println!("Reading transaction from stdin...");
-                let mut buf = String::new();
-                stdin().read_to_string(&mut buf)?;
-                let Some(bytes) = base64::decode(buf.trim()) else {
-                    eprintln!("Failed to decode transaction");
-                    exit(1);
-                };
-
-                let tx = deserialize_async(&bytes).await?;
+                let tx = parse_tx_from_stdin().await?;
 
                 let drk =
                     Drk::new(args.wallet_path, args.wallet_pass, Some(args.endpoint), ex).await?;
@@ -1539,17 +1546,17 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
         Subcmd::Token { command } => match command {
             TokenSubcmd::Import { secret_key, token_blind } => {
                 let mint_authority = match SecretKey::from_str(&secret_key) {
-                    Ok(r) => r,
+                    Ok(ma) => ma,
                     Err(e) => {
-                        eprintln!("Invalid secret key: {e:?}");
+                        eprintln!("Invalid mint authority: {e:?}");
                         exit(2);
                     }
                 };
 
                 let token_blind = match BaseBlind::from_str(&token_blind) {
-                    Ok(r) => r,
+                    Ok(tb) => tb,
                     Err(e) => {
-                        eprintln!("Invalid recipient: {e:?}");
+                        eprintln!("Invalid token blind: {e:?}");
                         exit(2);
                     }
                 };
@@ -1682,7 +1689,7 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
 
                 if let Err(e) = drk.deploy_auth_keygen().await {
                     eprintln!("Error creating deploy auth keypair: {:?}", e);
-                    exit(1);
+                    exit(2);
                 }
 
                 Ok(())
@@ -1721,7 +1728,7 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
                     Ok(v) => v,
                     Err(e) => {
                         eprintln!("Error creating contract deployment tx: {}", e);
-                        exit(1);
+                        exit(2);
                     }
                 };
 
@@ -1737,7 +1744,7 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
                     Ok(v) => v,
                     Err(e) => {
                         eprintln!("Error creating contract lock tx: {}", e);
-                        exit(1);
+                        exit(2);
                     }
                 };
 

+ 66 - 18
bin/drk/src/money.rs

@@ -101,6 +101,7 @@ pub const MONEY_COINS_COL_SECRET: &str = "secret";
 pub const MONEY_COINS_COL_NULLIFIER: &str = "nullifier";
 pub const MONEY_COINS_COL_LEAF_POSITION: &str = "leaf_position";
 pub const MONEY_COINS_COL_MEMO: &str = "memo";
+pub const MONEY_COINS_COL_SPENT_TX_HASH: &str = "spent_tx_hash";
 
 // MONEY_TOKENS_TABLE
 pub const MONEY_TOKENS_COL_TOKEN_ID: &str = "token_id";
@@ -390,7 +391,7 @@ impl Drk {
     /// 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)>> {
+    pub async fn get_coins(&self, fetch_spent: bool) -> Result<Vec<(OwnCoin, bool, String)>> {
         let query = if fetch_spent {
             self.wallet.query_multiple(&MONEY_COINS_TABLE, &[], &[]).await
         } else {
@@ -448,7 +449,7 @@ impl Drk {
 
     /// Auxiliary function to parse a `MONEY_COINS_TABLE` record.
     /// The boolean in the returned tuple notes if the coin was marked as spent.
-    async fn parse_coin_record(&self, row: &[Value]) -> Result<(OwnCoin, bool)> {
+    async fn parse_coin_record(&self, row: &[Value]) -> Result<(OwnCoin, bool, String)> {
         let Value::Blob(ref coin_bytes) = row[0] else {
             return Err(Error::ParseFailed("[parse_coin_record] Coin bytes parsing failed"))
         };
@@ -517,6 +518,12 @@ impl Drk {
             return Err(Error::ParseFailed("[parse_coin_record] Memo parsing failed"))
         };
 
+        let Value::Text(ref spent_tx_hash) = row[13] else {
+            return Err(Error::ParseFailed(
+                "[parse_coin_record] Spent transaction hash parsing failed",
+            ))
+        };
+
         let note = MoneyNote {
             value,
             token_id,
@@ -528,7 +535,7 @@ impl Drk {
             memo: memo.clone(),
         };
 
-        Ok((OwnCoin { coin, note, secret, leaf_position }, is_spent))
+        Ok((OwnCoin { coin, note, secret, leaf_position }, is_spent, spent_tx_hash.clone()))
     }
 
     /// Create an alias record for provided Token ID.
@@ -618,11 +625,17 @@ impl Drk {
     pub async fn unspend_coin(&self, coin: &Coin) -> WalletDbResult<()> {
         let is_spend = 0;
         let query = format!(
-            "UPDATE {} SET {} = ?1 WHERE {} = ?2",
-            *MONEY_COINS_TABLE, MONEY_COINS_COL_IS_SPENT, MONEY_COINS_COL_COIN,
+            "UPDATE {} SET {} = ?1, {} = ?2 WHERE {} = ?3;",
+            *MONEY_COINS_TABLE,
+            MONEY_COINS_COL_IS_SPENT,
+            MONEY_COINS_COL_SPENT_TX_HASH,
+            MONEY_COINS_COL_COIN
         );
         self.wallet
-            .exec_sql(&query, rusqlite::params![is_spend, serialize_async(&coin.inner()).await])
+            .exec_sql(
+                &query,
+                rusqlite::params![is_spend, "-", serialize_async(&coin.inner()).await],
+            )
             .await
     }
 
@@ -673,8 +686,12 @@ impl Drk {
         Ok(height)
     }
 
-    /// Append data related to Money contract transactions into the wallet database.
-    pub async fn apply_tx_money_data(&self, data: &[u8]) -> Result<()> {
+    /// Auxiliary function to  grab all the nullifiers, coins, notes and freezes from
+    /// transaction data.
+    async fn parse_call_data(
+        &self,
+        data: &[u8],
+    ) -> Result<(Vec<Nullifier>, Vec<Coin>, Vec<AeadEncryptedNote>, Vec<TokenId>)> {
         let mut nullifiers: Vec<Nullifier> = vec![];
         let mut coins: Vec<Coin> = vec![];
         let mut notes: Vec<AeadEncryptedNote> = vec![];
@@ -745,6 +762,12 @@ impl Drk {
             }
         }
 
+        Ok((nullifiers, coins, notes, freezes))
+    }
+
+    /// Append data related to Money contract transactions into the wallet database.
+    pub async fn apply_tx_money_data(&self, data: &[u8], tx_hash: &String) -> Result<()> {
+        let (nullifiers, coins, notes, freezes) = self.parse_call_data(data).await?;
         let secrets = self.get_money_secrets().await?;
         let dao_secrets = self.get_dao_secrets().await?;
         let mut tree = self.get_money_tree().await?;
@@ -775,9 +798,7 @@ impl Drk {
                 "[apply_tx_money_data] Put Money tree failed: {e:?}"
             )))
         }
-        if !nullifiers.is_empty() {
-            self.mark_spent_coins(&nullifiers).await?;
-        }
+        self.mark_spent_coins(&nullifiers, tx_hash).await?;
 
         // This is the SQL query we'll be executing to insert new coins
         // into the wallet
@@ -849,27 +870,54 @@ impl Drk {
         Ok(())
     }
 
+    /// Mark provided transaction input coins as spent.
+    pub async fn mark_tx_spend(&self, tx: &Transaction) -> Result<()> {
+        let tx_hash = tx.hash().to_string();
+        println!("[mark_tx_spend] Processing transaction: {tx_hash}");
+        for (i, call) in tx.calls.iter().enumerate() {
+            if call.data.contract_id != *MONEY_CONTRACT_ID {
+                continue
+            }
+
+            println!("[mark_tx_spend] Found Money contract in call {i}");
+            let (nullifiers, _, _, _) = self.parse_call_data(&call.data.data).await?;
+            self.mark_spent_coins(&nullifiers, &tx_hash).await?;
+        }
+
+        Ok(())
+    }
+
     /// Mark a coin in the wallet as spent
-    pub async fn mark_spent_coin(&self, coin: &Coin) -> WalletDbResult<()> {
+    pub async fn mark_spent_coin(&self, coin: &Coin, spent_tx_hash: &String) -> WalletDbResult<()> {
         let query = format!(
-            "UPDATE {} SET {} = ?1 WHERE {} = ?2;",
-            *MONEY_COINS_TABLE, MONEY_COINS_COL_IS_SPENT, MONEY_COINS_COL_COIN
+            "UPDATE {} SET {} = ?1, {} = ?2 WHERE {} = ?3;",
+            *MONEY_COINS_TABLE,
+            MONEY_COINS_COL_IS_SPENT,
+            MONEY_COINS_COL_SPENT_TX_HASH,
+            MONEY_COINS_COL_COIN
         );
         let is_spent = 1;
         self.wallet
-            .exec_sql(&query, rusqlite::params![is_spent, serialize_async(&coin.inner()).await])
+            .exec_sql(
+                &query,
+                rusqlite::params![is_spent, spent_tx_hash, serialize_async(&coin.inner()).await],
+            )
             .await
     }
 
     /// Marks all coins in the wallet as spent, if their nullifier is in the given set
-    pub async fn mark_spent_coins(&self, nullifiers: &[Nullifier]) -> Result<()> {
+    pub async fn mark_spent_coins(
+        &self,
+        nullifiers: &[Nullifier],
+        spent_tx_hash: &String,
+    ) -> Result<()> {
         if nullifiers.is_empty() {
             return Ok(())
         }
 
-        for (coin, _) in self.get_coins(false).await? {
+        for (coin, _, _) in self.get_coins(false).await? {
             if nullifiers.contains(&coin.nullifier()) {
-                if let Err(e) = self.mark_spent_coin(&coin.coin).await {
+                if let Err(e) = self.mark_spent_coin(&coin.coin, spent_tx_hash).await {
                     return Err(Error::RusqliteError(format!(
                         "[mark_spent_coins] Marking spent coin failed: {e:?}"
                     )))

+ 16 - 7
bin/drk/src/rpc.rs

@@ -140,9 +140,16 @@ impl Drk {
                                 "[subscribe_blocks] Scanning block failed: {e:?}"
                             )))
                         }
-                        if let Err(e) = self
-                            .update_tx_history_records_status(&block_data.txs, "Finalized")
-                            .await
+                        let txs_hashes = match self.insert_tx_history_records(&block_data.txs).await {
+                            Ok(hashes) => hashes,
+                            Err(e) => {
+                                return Err(Error::RusqliteError(format!(
+                                    "[subscribe_blocks] Inserting transaction history records failed: {e:?}"
+                                )))
+                            },
+                        };
+                        if let Err(e) =
+                            self.update_tx_history_records_status(&txs_hashes, "Finalized").await
                         {
                             return Err(Error::RusqliteError(format!(
                                 "[subscribe_blocks] Update transaction history record status failed: {e:?}"
@@ -174,11 +181,12 @@ impl Drk {
     async fn scan_block(&self, block: &BlockInfo) -> Result<()> {
         println!("[scan_block] Iterating over {} transactions", block.txs.len());
         for tx in block.txs.iter() {
-            println!("[scan_block] Processing transaction: {}", tx.hash());
+            let tx_hash = tx.hash().to_string();
+            println!("[scan_block] Processing transaction: {tx_hash}");
             for (i, call) in tx.calls.iter().enumerate() {
                 if call.data.contract_id == *MONEY_CONTRACT_ID {
                     println!("[scan_block] Found Money contract in call {i}");
-                    self.apply_tx_money_data(&call.data.data).await?;
+                    self.apply_tx_money_data(&call.data.data, &tx_hash).await?;
                     continue
                 }
 
@@ -260,7 +268,7 @@ impl Drk {
             }
 
             while height <= last {
-                eprint!("Requesting block {}... ", height);
+                println!("Requesting block {}... ", height);
                 let block = match self.get_block_by_height(height).await {
                     Ok(r) => r,
                     Err(e) => {
@@ -272,7 +280,8 @@ impl Drk {
                     eprintln!("[scan_blocks] Scan block failed: {e:?}");
                     return Err(WalletDbError::GenericError)
                 };
-                self.update_tx_history_records_status(&block.txs, "Finalized").await?;
+                let txs_hashes = self.insert_tx_history_records(&block.txs).await?;
+                self.update_tx_history_records_status(&txs_hashes, "Finalized").await?;
                 height += 1;
             }
         }

+ 35 - 61
bin/drk/src/txs_history.rs

@@ -16,11 +16,9 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use lazy_static::lazy_static;
 use rusqlite::types::Value;
 
-use darkfi::{tx::Transaction, util::encoding::base64, Error, Result};
-use darkfi_sdk::crypto::MONEY_CONTRACT_ID;
+use darkfi::{tx::Transaction, Error, Result};
 use darkfi_serial::{deserialize_async, serialize_async};
 
 use crate::{
@@ -30,36 +28,43 @@ use crate::{
 };
 
 // Wallet SQL table constant names. These have to represent the `wallet.sql`
-// SQL schema. Table names are prefixed with the contract ID to avoid collisions.
-lazy_static! {
-    pub static ref WALLET_TXS_HISTORY_TABLE: String =
-        format!("{}_transactions_history", MONEY_CONTRACT_ID.to_string());
-}
+// SQL schema.
+const WALLET_TXS_HISTORY_TABLE: &str = "transactions_history";
 const WALLET_TXS_HISTORY_COL_TX_HASH: &str = "transaction_hash";
 const WALLET_TXS_HISTORY_COL_STATUS: &str = "status";
 const WALLET_TXS_HISTORY_COL_TX: &str = "tx";
 
 impl Drk {
-    /// Insert a [`Transaction`] history record into the wallet.
-    pub async fn insert_tx_history_record(&self, tx: &Transaction) -> WalletDbResult<()> {
+    /// Insert a `Transaction` history record into the wallet.
+    pub async fn insert_tx_history_record(&self, tx: &Transaction) -> WalletDbResult<String> {
         let query = format!(
-            "INSERT INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3);",
-            *WALLET_TXS_HISTORY_TABLE,
+            "INSERT OR IGNORE INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3);",
+            WALLET_TXS_HISTORY_TABLE,
             WALLET_TXS_HISTORY_COL_TX_HASH,
             WALLET_TXS_HISTORY_COL_STATUS,
             WALLET_TXS_HISTORY_COL_TX,
         );
-        let tx_hash = tx.hash();
+        let tx_hash = tx.hash().to_string();
         self.wallet
             .exec_sql(
                 &query,
-                rusqlite::params![
-                    tx_hash.to_string(),
-                    "Broadcasted",
-                    base64::encode(&serialize_async(tx).await),
-                ],
+                rusqlite::params![tx_hash, "Broadcasted", &serialize_async(tx).await,],
             )
-            .await
+            .await?;
+
+        Ok(tx_hash)
+    }
+
+    /// Insert a slice of [`Transaction`] history records into the wallet.
+    pub async fn insert_tx_history_records(
+        &self,
+        txs: &[Transaction],
+    ) -> WalletDbResult<Vec<String>> {
+        let mut ret = Vec::with_capacity(txs.len());
+        for tx in txs {
+            ret.push(self.insert_tx_history_record(tx).await?);
+        }
+        Ok(ret)
     }
 
     /// Get a transaction history record.
@@ -70,7 +75,7 @@ impl Drk {
         let row = match self
             .wallet
             .query_single(
-                &WALLET_TXS_HISTORY_TABLE,
+                WALLET_TXS_HISTORY_TABLE,
                 &[],
                 convert_named_params! {(WALLET_TXS_HISTORY_COL_TX_HASH, tx_hash)},
             )
@@ -89,28 +94,19 @@ impl Drk {
                 "[get_tx_history_record] Transaction hash parsing failed",
             ))
         };
-        let tx_hash = tx_hash.clone();
 
         let Value::Text(ref status) = row[1] else {
             return Err(Error::ParseFailed("[get_tx_history_record] Status parsing failed"))
         };
-        let status = status.clone();
-
-        let Value::Text(ref tx_encoded) = row[2] else {
-            return Err(Error::ParseFailed(
-                "[get_tx_history_record] Encoded transaction parsing failed",
-            ))
-        };
 
-        let Some(tx_bytes) = base64::decode(tx_encoded) else {
+        let Value::Blob(ref bytes) = row[2] else {
             return Err(Error::ParseFailed(
-                "[get_tx_history_record] Encoded transaction parsing failed",
+                "[get_tx_history_record] Transaction bytes parsing failed",
             ))
         };
+        let tx: Transaction = deserialize_async(bytes).await?;
 
-        let tx: Transaction = deserialize_async(&tx_bytes).await?;
-
-        Ok((tx_hash, status, tx))
+        Ok((tx_hash.clone(), status.clone(), tx))
     }
 
     /// Fetch all transactions history records, excluding bytes column.
@@ -118,7 +114,7 @@ impl Drk {
         let rows = self
             .wallet
             .query_multiple(
-                &WALLET_TXS_HISTORY_TABLE,
+                WALLET_TXS_HISTORY_TABLE,
                 &[WALLET_TXS_HISTORY_COL_TX_HASH, WALLET_TXS_HISTORY_COL_STATUS],
                 &[],
             )
@@ -129,53 +125,31 @@ impl Drk {
             let Value::Text(ref tx_hash) = row[0] else {
                 return Err(WalletDbError::ParseColumnValueError)
             };
-            let tx_hash = tx_hash.clone();
 
             let Value::Text(ref status) = row[1] else {
                 return Err(WalletDbError::ParseColumnValueError)
             };
-            let status = status.clone();
 
-            ret.push((tx_hash, status));
+            ret.push((tx_hash.clone(), status.clone()));
         }
 
         Ok(ret)
     }
 
-    /// Update a transactions history record status to the given one.
-    pub async fn update_tx_history_record_status(
-        &self,
-        tx_hash: &str,
-        status: &str,
-    ) -> WalletDbResult<()> {
-        let query = format!(
-            "UPDATE {} SET {} = ?1 WHERE {} = ?2;",
-            *WALLET_TXS_HISTORY_TABLE,
-            WALLET_TXS_HISTORY_COL_STATUS,
-            WALLET_TXS_HISTORY_COL_TX_HASH,
-        );
-        self.wallet.exec_sql(&query, rusqlite::params![status, tx_hash]).await
-    }
-
     /// Update given transactions history record statuses to the given one.
     pub async fn update_tx_history_records_status(
         &self,
-        txs: &Vec<Transaction>,
+        txs_hashes: &[String],
         status: &str,
     ) -> WalletDbResult<()> {
-        if txs.is_empty() {
+        if txs_hashes.is_empty() {
             return Ok(())
         }
 
-        let mut txs_hashes = Vec::with_capacity(txs.len());
-        for tx in txs {
-            let tx_hash = tx.hash();
-            txs_hashes.push(format!("{tx_hash}"));
-        }
         let txs_hashes_string = format!("{:?}", txs_hashes).replace('[', "(").replace(']', ")");
         let query = format!(
             "UPDATE {} SET {} = ?1 WHERE {} IN {};",
-            *WALLET_TXS_HISTORY_TABLE,
+            WALLET_TXS_HISTORY_TABLE,
             WALLET_TXS_HISTORY_COL_STATUS,
             WALLET_TXS_HISTORY_COL_TX_HASH,
             txs_hashes_string
@@ -188,7 +162,7 @@ impl Drk {
     pub async fn update_all_tx_history_records_status(&self, status: &str) -> WalletDbResult<()> {
         let query = format!(
             "UPDATE {} SET {} = ?1",
-            *WALLET_TXS_HISTORY_TABLE, WALLET_TXS_HISTORY_COL_STATUS,
+            WALLET_TXS_HISTORY_TABLE, WALLET_TXS_HISTORY_COL_STATUS,
         );
         self.wallet.exec_sql(&query, rusqlite::params![status]).await
     }

+ 1 - 1
bin/drk/wallet.sql

@@ -1,7 +1,7 @@
 -- Wallet definitions for drk.
 -- We store data that is needed for wallet operations.
 
--- Broadcasted transactions history
+-- Transactions history
 CREATE TABLE IF NOT EXISTS transactions_history (
     transaction_hash TEXT PRIMARY KEY NOT NULL,
     status TEXT NOT NULL,