/ bin / drk / src / txs_history.rs
txs_history.rs
  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  
 19  use rusqlite::types::Value;
 20  
 21  use darkfi::{tx::Transaction, Error, Result};
 22  use darkfi_serial::{deserialize_async, serialize};
 23  
 24  use crate::{
 25      convert_named_params,
 26      error::{WalletDbError, WalletDbResult},
 27      Drk,
 28  };
 29  
 30  // Wallet SQL table constant names. These have to represent the `wallet.sql`
 31  // SQL schema.
 32  const WALLET_TXS_HISTORY_TABLE: &str = "transactions_history";
 33  const WALLET_TXS_HISTORY_COL_TX_HASH: &str = "transaction_hash";
 34  const WALLET_TXS_HISTORY_COL_STATUS: &str = "status";
 35  const WALLET_TXS_HISTORY_BLOCK_HEIGHT: &str = "block_height";
 36  const WALLET_TXS_HISTORY_COL_TX: &str = "tx";
 37  
 38  impl Drk {
 39      /// Insert or update a `Transaction` history record into the wallet,
 40      /// with the provided status, and store its inverse query into the cache.
 41      pub async fn put_tx_history_record(
 42          &self,
 43          tx: &Transaction,
 44          status: &str,
 45          block_height: Option<u32>,
 46      ) -> WalletDbResult<String> {
 47          // Create an SQL `INSERT OR REPLACE` query
 48          let query = format!(
 49              "INSERT OR REPLACE INTO {WALLET_TXS_HISTORY_TABLE} ({WALLET_TXS_HISTORY_COL_TX_HASH}, {WALLET_TXS_HISTORY_COL_STATUS}, {WALLET_TXS_HISTORY_BLOCK_HEIGHT}, {WALLET_TXS_HISTORY_COL_TX}) VALUES (?1, ?2, ?3, ?4);"
 50          );
 51  
 52          // Execute the query
 53          let tx_hash = tx.hash().to_string();
 54          self.wallet
 55              .exec_sql(&query, rusqlite::params![tx_hash, status, block_height, &serialize(tx)])?;
 56  
 57          Ok(tx_hash)
 58      }
 59  
 60      /// Insert or update a slice of [`Transaction`] history records into the wallet,
 61      /// with the provided status.
 62      pub async fn put_tx_history_records(
 63          &self,
 64          txs: &[&Transaction],
 65          status: &str,
 66          block_height: Option<u32>,
 67      ) -> WalletDbResult<Vec<String>> {
 68          let mut ret = Vec::with_capacity(txs.len());
 69          for tx in txs {
 70              ret.push(self.put_tx_history_record(tx, status, block_height).await?);
 71          }
 72          Ok(ret)
 73      }
 74  
 75      /// Get a transaction history record.
 76      pub async fn get_tx_history_record(
 77          &self,
 78          tx_hash: &str,
 79      ) -> Result<(String, String, Option<u32>, Transaction)> {
 80          let row = match self.wallet.query_single(
 81              WALLET_TXS_HISTORY_TABLE,
 82              &[],
 83              convert_named_params! {(WALLET_TXS_HISTORY_COL_TX_HASH, tx_hash)},
 84          ) {
 85              Ok(r) => r,
 86              Err(e) => {
 87                  return Err(Error::DatabaseError(format!(
 88                      "[get_tx_history_record] Transaction history record retrieval failed: {e}"
 89                  )))
 90              }
 91          };
 92  
 93          let Value::Text(ref tx_hash) = row[0] else {
 94              return Err(Error::ParseFailed(
 95                  "[get_tx_history_record] Transaction hash parsing failed",
 96              ))
 97          };
 98  
 99          let Value::Text(ref status) = row[1] else {
100              return Err(Error::ParseFailed("[get_tx_history_record] Status parsing failed"))
101          };
102  
103          let block_height = match row[2] {
104              Value::Integer(block_height) => {
105                  let Ok(block_height) = u32::try_from(block_height) else {
106                      return Err(Error::ParseFailed(
107                          "[get_tx_history_record] Block height parsing failed",
108                      ))
109                  };
110                  Some(block_height)
111              }
112              Value::Null => None,
113              _ => {
114                  return Err(Error::ParseFailed(
115                      "[get_tx_history_record] Block height parsing failed",
116                  ))
117              }
118          };
119  
120          let Value::Blob(ref bytes) = row[3] else {
121              return Err(Error::ParseFailed(
122                  "[get_tx_history_record] Transaction bytes parsing failed",
123              ))
124          };
125          let tx: Transaction = deserialize_async(bytes).await?;
126  
127          Ok((tx_hash.clone(), status.clone(), block_height, tx))
128      }
129  
130      /// Fetch all transactions history records, excluding bytes column.
131      pub fn get_txs_history(&self) -> WalletDbResult<Vec<(String, String, Option<u32>)>> {
132          let rows = self.wallet.query_multiple(
133              WALLET_TXS_HISTORY_TABLE,
134              &[
135                  WALLET_TXS_HISTORY_COL_TX_HASH,
136                  WALLET_TXS_HISTORY_COL_STATUS,
137                  WALLET_TXS_HISTORY_BLOCK_HEIGHT,
138              ],
139              &[],
140          )?;
141  
142          let mut ret = Vec::with_capacity(rows.len());
143          for row in rows {
144              let Value::Text(ref tx_hash) = row[0] else {
145                  return Err(WalletDbError::ParseColumnValueError)
146              };
147  
148              let Value::Text(ref status) = row[1] else {
149                  return Err(WalletDbError::ParseColumnValueError)
150              };
151  
152              let block_height = match row[2] {
153                  Value::Integer(block_height) => {
154                      let Ok(block_height) = u32::try_from(block_height) else {
155                          return Err(WalletDbError::ParseColumnValueError)
156                      };
157                      Some(block_height)
158                  }
159                  Value::Null => None,
160                  _ => return Err(WalletDbError::ParseColumnValueError),
161              };
162  
163              ret.push((tx_hash.clone(), status.clone(), block_height));
164          }
165  
166          Ok(ret)
167      }
168  
169      /// Reset the transaction history records in the wallet.
170      pub fn reset_tx_history(&self, output: &mut Vec<String>) -> WalletDbResult<()> {
171          output.push(String::from("Resetting transactions history"));
172          let query = format!("DELETE FROM {WALLET_TXS_HISTORY_TABLE};");
173          self.wallet.exec_sql(&query, &[])?;
174          output.push(String::from("Successfully reset transactions history"));
175  
176          Ok(())
177      }
178  
179      /// Set reverted status to the transaction history records in the
180      /// wallet that where executed after provided height.
181      pub fn revert_transactions_after(
182          &self,
183          height: &u32,
184          output: &mut Vec<String>,
185      ) -> WalletDbResult<()> {
186          output.push(format!("Reverting transactions history after: {height}"));
187          let query = format!(
188              "UPDATE {WALLET_TXS_HISTORY_TABLE} SET {WALLET_TXS_HISTORY_COL_STATUS} = 'Reverted', {WALLET_TXS_HISTORY_BLOCK_HEIGHT} = NULL WHERE {WALLET_TXS_HISTORY_BLOCK_HEIGHT} > ?1;"
189          );
190          self.wallet.exec_sql(&query, rusqlite::params![Some(*height)])?;
191          output.push(String::from("Successfully reverted transactions history"));
192  
193          Ok(())
194      }
195  
196      /// Remove the transaction history records in the wallet
197      /// that have been reverted.
198      pub fn remove_reverted_txs(&self, output: &mut Vec<String>) -> WalletDbResult<()> {
199          output.push(String::from("Removing reverted transactions history records"));
200          let query = format!(
201              "DELETE FROM {WALLET_TXS_HISTORY_TABLE} WHERE {WALLET_TXS_HISTORY_COL_STATUS} = 'Reverted';"
202          );
203          self.wallet.exec_sql(&query, &[])?;
204          output.push(String::from("Successfully removed reverted transactions history records"));
205  
206          Ok(())
207      }
208  }