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#[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#[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 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 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 #[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 #[tokio::test]
290 #[serial]
291 async fn test_file_system_database_get_state_error() {
292 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}