eth_tx_manager/
database.rs

1use async_trait::async_trait;
2use std::fmt::Debug;
3use std::io::ErrorKind;
4use tokio::fs;
5use tokio::io::{AsyncReadExt, AsyncWriteExt};
6
7use crate::transaction::PersistentState;
8
9#[async_trait]
10pub trait Database: Debug {
11    type Error: std::error::Error;
12
13    async fn set_state(&mut self, state: &PersistentState) -> Result<(), Self::Error>;
14
15    async fn get_state(&self) -> Result<Option<PersistentState>, Self::Error>;
16
17    async fn clear_state(&mut self) -> Result<(), Self::Error>;
18}
19
20// Implementation using the file system.
21
22#[derive(Debug, thiserror::Error)]
23pub enum FileSystemDatabaseError {
24    #[error("could not create file: {0}")]
25    CreateFile(std::io::Error),
26
27    #[error("could not convert string to JSON: {0}")]
28    ToJSON(serde_json::Error),
29
30    #[error("could not write to file: {0}")]
31    WriteToFile(std::io::Error),
32
33    #[error("could not read file to string: {0}")]
34    ReadFile(std::io::Error),
35
36    #[error("could not parse JSON to string: {0}")]
37    ParseJSON(serde_json::Error),
38
39    #[error("could not delete file: {0}")]
40    DeleteFile(std::io::Error),
41}
42
43#[derive(Debug)]
44pub struct FileSystemDatabase {
45    path: String,
46}
47
48impl FileSystemDatabase {
49    pub fn new(path: String) -> FileSystemDatabase {
50        FileSystemDatabase { path }
51    }
52}
53
54#[async_trait]
55impl Database for FileSystemDatabase {
56    type Error = FileSystemDatabaseError;
57
58    async fn set_state(&mut self, state: &PersistentState) -> Result<(), Self::Error> {
59        let mut file = fs::File::create(self.path.clone())
60            .await
61            .map_err(Self::Error::CreateFile)?;
62
63        let s = serde_json::to_string_pretty(state).map_err(Self::Error::ToJSON)?;
64
65        file.write_all(s.as_bytes())
66            .await
67            .map_err(Self::Error::WriteToFile)?;
68
69        file.sync_all().await.map_err(Self::Error::WriteToFile)?;
70
71        return Ok(());
72    }
73
74    async fn get_state(&self) -> Result<Option<PersistentState>, Self::Error> {
75        let file = fs::File::open(self.path.clone()).await;
76
77        return match file {
78            Err(err) if err.kind() == ErrorKind::NotFound => Ok(None),
79
80            Err(err) => Err(Self::Error::ReadFile(err)),
81
82            Ok(mut file) => {
83                let mut s = String::new();
84
85                file.read_to_string(&mut s)
86                    .await
87                    .map_err(Self::Error::ReadFile)?;
88
89                let state = serde_json::de::from_str(&s).map_err(Self::Error::ParseJSON)?;
90
91                return Ok(Some(state));
92            }
93        };
94    }
95
96    async fn clear_state(&mut self) -> Result<(), Self::Error> {
97        Ok(fs::remove_file(self.path.clone())
98            .await
99            .map_err(Self::Error::DeleteFile)?)
100    }
101}
102
103// Unit tests for the file system database.
104
105#[cfg(test)]
106mod test {
107    use ethers::types::{H160, H256};
108    use serde_json::error::Category;
109    use serial_test::serial;
110    use std::fs::{remove_file, File};
111    use std::io::Write;
112    use std::path::PathBuf;
113
114    use crate::database::{Database, FileSystemDatabase, FileSystemDatabaseError};
115    use crate::transaction::{PersistentState, StaticTxData, SubmittedTxs};
116    use crate::transaction::{Priority, Transaction, Value};
117
118    /// Auxiliary.
119    fn setup(str: String) -> (PathBuf, FileSystemDatabase) {
120        let path = PathBuf::from(&str);
121        let database = FileSystemDatabase::new(str);
122        let _ = remove_file(path.as_path());
123        (path, database)
124    }
125
126    #[tokio::test]
127    #[serial]
128    async fn test_file_system_database_set_state_ok_empty_state() {
129        let state = PersistentState {
130            tx_data: StaticTxData {
131                nonce: 1u64.into(),
132                transaction: Transaction {
133                    from: H160::from_low_u64_ne(1u64),
134                    to: H160::from_low_u64_ne(2u64),
135                    value: Value::Number(5000u64.into()),
136                    call_data: None,
137                },
138                priority: Priority::Normal,
139                confirmations: 0,
140            },
141            submitted_txs: SubmittedTxs::new(),
142        };
143
144        let (path, mut database) = setup("./set_database.json".to_string());
145        let path = path.as_path();
146
147        assert!(!path.is_file());
148        let result = database.set_state(&state).await;
149        assert!(result.is_ok());
150        assert!(path.is_file());
151        remove_file(path).unwrap();
152        assert!(!path.is_file());
153    }
154
155    #[tokio::test]
156    #[serial]
157    async fn test_file_system_database_set_state_ok_existing_state() {
158        let state = PersistentState {
159            tx_data: StaticTxData {
160                nonce: 2u64.into(),
161                transaction: Transaction {
162                    from: H160::from_low_u64_ne(5u64),
163                    to: H160::from_low_u64_ne(6u64),
164                    value: Value::Number(3000u64.into()),
165                    call_data: None,
166                },
167                priority: Priority::High,
168                confirmations: 5,
169            },
170            submitted_txs: SubmittedTxs {
171                txs_hashes: vec![
172                    H256::from_low_u64_ne(1400u64),
173                    H256::from_low_u64_ne(1500u64),
174                ],
175            },
176        };
177
178        let (path, mut database) = setup("./set_database.json".to_string());
179        let path = path.as_path();
180
181        assert!(!path.is_file());
182        let result = database.set_state(&state).await;
183        assert!(result.is_ok());
184        assert!(path.is_file());
185        let result = database.set_state(&state).await;
186        assert!(result.is_ok());
187        assert!(path.is_file());
188        remove_file(path).unwrap();
189        assert!(!path.is_file());
190    }
191
192    #[tokio::test]
193    #[serial]
194    async fn test_file_system_database_set_state_error() {
195        // error => could not create the file (invalid path)
196
197        let state = PersistentState {
198            tx_data: StaticTxData {
199                nonce: 1u64.into(),
200                transaction: Transaction {
201                    from: H160::from_low_u64_ne(1u64),
202                    to: H160::from_low_u64_ne(2u64),
203                    value: Value::Number(5000u64.into()),
204                    call_data: None,
205                },
206                priority: Priority::Normal,
207                confirmations: 0,
208            },
209            submitted_txs: SubmittedTxs::new(),
210        };
211
212        let path_str = "/bin/set_database.json".to_string();
213        let path = PathBuf::from(&path_str);
214        let path = path.as_path();
215        let mut database = FileSystemDatabase::new(path_str.clone());
216
217        assert!(!path.is_file());
218        let result = database.set_state(&state).await;
219        assert!(result.is_err());
220        let err = result.as_ref().err().unwrap();
221        assert!(
222            matches!(err,
223                FileSystemDatabaseError::CreateFile(err)
224                    if err.kind() == std::io::ErrorKind::PermissionDenied
225            ),
226            "expected CreateFile::PermissionDenied error, got {}",
227            err
228        );
229        assert!(!path.is_file());
230    }
231
232    // Currently not testing the ToJSON and WriteToFile errors.
233
234    #[tokio::test]
235    #[serial]
236    async fn test_file_system_database_get_state_ok_empty_state() {
237        let (path, database) = setup("./get_database.json".to_string());
238        assert!(!path.is_file());
239        let result = database.get_state().await;
240        assert!(result.is_ok());
241        assert!(result.unwrap().is_none());
242        assert!(!path.is_file());
243    }
244
245    #[tokio::test]
246    #[serial]
247    async fn test_file_system_database_get_state_ok_existing_state() {
248        let original_state = PersistentState {
249            tx_data: StaticTxData {
250                nonce: 2u64.into(),
251                transaction: Transaction {
252                    from: H160::from_low_u64_ne(5u64),
253                    to: H160::from_low_u64_ne(6u64),
254                    value: Value::Number(3000u64.into()),
255                    call_data: None,
256                },
257                priority: Priority::High,
258                confirmations: 5,
259            },
260            submitted_txs: SubmittedTxs {
261                txs_hashes: vec![
262                    H256::from_low_u64_ne(1400u64),
263                    H256::from_low_u64_ne(1500u64),
264                ],
265            },
266        };
267
268        let (path, mut database) = setup("./get_database.json".to_string());
269        let path = path.as_path();
270
271        assert!(!path.is_file());
272        let result = database.set_state(&original_state).await;
273        assert!(result.is_ok());
274        assert!(path.is_file());
275        let result = database.get_state().await;
276        assert!(result.is_ok());
277        let some_state = result.unwrap();
278        assert!(some_state.is_some());
279        let retrieved_state = some_state.unwrap();
280        assert_eq!(original_state, retrieved_state);
281
282        assert!(path.is_file());
283        remove_file(path).unwrap();
284        assert!(!path.is_file());
285    }
286
287    // Currently not testing the ReadFile error.
288
289    #[tokio::test]
290    #[serial]
291    async fn test_file_system_database_get_state_error() {
292        // error => could not parse the read file to JSON
293
294        let path_str = "./parse_json_test.json".to_string();
295        let path = PathBuf::from(path_str.clone());
296        let path = path.as_path();
297        let _ = remove_file(path);
298        assert!(!path.is_file());
299        let mut file = File::create(path).unwrap();
300        file.write_all("this is not a JSON!".as_bytes()).unwrap();
301
302        let database = FileSystemDatabase::new(path_str.clone());
303        let result = database.get_state().await;
304        assert!(result.is_err());
305        let err = result.as_ref().err().unwrap();
306        assert!(
307            matches!(err,
308                FileSystemDatabaseError::ParseJSON(err)
309                    if err.classify() == Category::Syntax
310            ),
311            "expected ParseJSON::Syntax error, got {}",
312            err
313        );
314
315        assert!(path.is_file());
316        remove_file(path).unwrap();
317        assert!(!path.is_file());
318    }
319
320    #[tokio::test]
321    #[serial]
322    async fn test_file_system_database_clear_state_ok() {
323        let path_str = "./clear_database.json".to_string();
324        let (path, mut database) = setup(path_str.clone());
325        assert!(File::create(path_str.clone()).is_ok());
326
327        let result = database.clear_state().await;
328        assert!(result.is_ok());
329        assert!(!path.is_file());
330    }
331
332    #[tokio::test]
333    #[serial]
334    async fn test_file_system_database_clear_state_error_empty_state() {
335        let path_str = "./clear_database.json".to_string();
336        let (path, mut database) = setup(path_str.clone());
337
338        let result = database.clear_state().await;
339        assert!(result.is_err());
340        let err = result.as_ref().err().unwrap();
341        assert!(
342            matches!(err,
343                FileSystemDatabaseError::DeleteFile(err)
344                    if err.kind() == std::io::ErrorKind::NotFound
345            ),
346            "expected DeleteFile::NotFound error. got {}",
347            err
348        );
349
350        assert!(!path.is_file());
351    }
352}