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 }