drk/
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
19use rusqlite::types::Value;
20
21use darkfi::{tx::Transaction, Error, Result};
22use darkfi_serial::{deserialize_async, serialize};
23
24use 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.
32const WALLET_TXS_HISTORY_TABLE: &str = "transactions_history";
33const WALLET_TXS_HISTORY_COL_TX_HASH: &str = "transaction_hash";
34const WALLET_TXS_HISTORY_COL_STATUS: &str = "status";
35const WALLET_TXS_HISTORY_BLOCK_HEIGHT: &str = "block_height";
36const WALLET_TXS_HISTORY_COL_TX: &str = "tx";
37
38impl 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}