1use std::fmt::{Display, Formatter, Write};
2use std::io::{BufRead, BufReader};
3
4use log::{debug, info};
5
6use crate::core::consensus::slip::{Slip, SlipType};
7use crate::core::defs::{
8 BlockId, PrintForLog, SaitoHash, SaitoPublicKey, Timestamp, UTXO_KEY_LENGTH,
9};
10
11pub type BalanceFileRowType = (String, String, String, String);
12
13pub struct BalanceSnapshot {
14 pub latest_block_id: BlockId,
15 pub latest_block_hash: SaitoHash,
16 pub timestamp: Timestamp,
17 pub slips: Vec<Slip>,
18}
19
20impl BalanceSnapshot {
21 pub fn get_data(&self) -> (String, Vec<String>) {
41 let file_name: String = self.get_file_name();
42 let entries: Vec<String> = self.get_rows();
43
44 (file_name, entries)
45 }
46
47 pub fn get_file_name(&self) -> String {
48 self.timestamp.to_string()
49 + "-"
50 + self.latest_block_id.to_string().as_str()
51 + "-"
52 + self.latest_block_hash.to_hex().as_str()
53 + ".snap"
54 }
55 pub fn get_rows(&self) -> Vec<String> {
56 self.slips
57 .iter()
58 .map(|slip| {
59 let key = slip.public_key.to_base58();
60 let entry = format!(
61 "{} {:?} {:?} {:?} {:?}",
62 key, slip.block_id, slip.tx_ordinal, slip.slip_index, slip.amount
63 );
64 entry
65 })
66 .collect()
67 }
68
69 pub fn new(file_name: String, rows: Vec<String>) -> Result<BalanceSnapshot, String> {
70 debug!(
71 "creating new balance snapshot from file : {:?} with {:?} rows",
72 file_name,
73 rows.len()
74 );
75 let mut tokens: Vec<&str> = file_name.split('.').collect();
76 let file_name = tokens.remove(0);
77 let tokens: Vec<&str> = file_name.split('-').collect();
78 if tokens.len() != 3 {
79 return Err(format!(
80 "file name : {:?} is invalid for balance snapshot file",
81 file_name
82 ));
83 }
84 let timestamp = tokens.get(0).unwrap();
85 let block_id = tokens.get(1).unwrap();
86 let block_hash = tokens.get(2).unwrap();
87
88 let timestamp: Timestamp = timestamp
89 .parse()
90 .map_err(|err| format!("failed parsing timestamp : {:?}. {:?}", timestamp, err))?;
91 let block_id: u64 = block_id
92 .parse()
93 .map_err(|err| format!("failed parsing block id : {:?}. {:?}", block_id, err))?;
94 let block_hash = SaitoHash::from_hex(block_hash)
95 .map_err(|err| format!("failed parsing block hash : {:?}. {:?}", block_hash, err))?;
96
97 let mut snapshot = BalanceSnapshot {
98 latest_block_id: block_id,
99 latest_block_hash: block_hash,
100 timestamp,
101 slips: vec![],
102 };
103
104 rows.iter().try_for_each(|row| {
105 let cols: Vec<&str> = row.split(' ').collect();
106 if cols.len() != 5 {
107 return Err(format!(
108 "row is invalid. number of columns is {:?}. but should be 5",
109 cols.len()
110 ));
111 }
112 let key = cols.get(0).ok_or("cannot find key in row".to_string())?;
113 let block_id = cols
114 .get(1)
115 .ok_or("cannot find block id in row".to_string())?;
116 let tx_id = cols.get(2).ok_or("cannot find tx id in row".to_string())?;
117 let slip_id = cols
118 .get(3)
119 .ok_or("cannot find slip id in row".to_string())?;
120 let amount = cols.get(4).ok_or("cannot find amount in row".to_string())?;
121
122 let key: SaitoPublicKey = SaitoPublicKey::from_base58(key)
123 .or(Err(format!("failed parsing key : {:?}", key)))?;
124 let block_id = block_id
125 .parse()
126 .or(Err(format!("failed parsing block id : {:?}", block_id)))?;
127 let tx_id = tx_id
128 .parse()
129 .or(Err(format!("failed parsing tx id : {:?}", tx_id)))?;
130 let slip_id = slip_id
131 .parse()
132 .or(Err(format!("failed parsing slip id : {:?}", slip_id)))?;
133 let amount = amount
134 .parse()
135 .or(Err(format!("failed parsing amount : {:?}", amount)))?;
136
137 let mut slip = Slip {
138 public_key: key,
139 amount,
140 slip_index: slip_id,
141 block_id,
142 tx_ordinal: tx_id,
143 slip_type: SlipType::Normal,
144 utxoset_key: [0; UTXO_KEY_LENGTH],
145 is_utxoset_key_set: false,
146 };
147 slip.generate_utxoset_key();
148 snapshot.slips.push(slip);
149
150 Ok(())
151 })?;
152
153 Ok(snapshot)
154 }
155}
156
157impl Display for BalanceSnapshot {
158 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
159 let (file_name, rows) = self.get_data();
160 writeln!(f, "{}", file_name).unwrap();
161 rows.iter().for_each(|row| {
162 writeln!(f, "{}", row).unwrap();
163 });
164
165 std::fmt::Result::Ok(())
166 }
167}
168
169impl TryFrom<String> for BalanceSnapshot {
170 type Error = String;
171
172 fn try_from(value: String) -> Result<Self, Self::Error> {
173 let mut reader = BufReader::new(value.as_bytes());
174
175 let mut file_name = "".to_string();
176 reader
177 .read_line(&mut file_name)
178 .map_err(|_err| "failed reading file name from balance snapshot")?;
179 file_name.pop();
180
181 let mut rows = vec![];
182 loop {
183 let mut row = "".to_string();
184 if reader.read_line(&mut row).is_ok() {
185 info!("row = {}", row);
186 if row.is_empty() {
187 break;
188 }
189 row.pop();
191 rows.push(row);
192 } else {
193 break;
194 }
195 }
196
197 BalanceSnapshot::new(file_name, rows)
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use log::info;
204
205 use crate::core::consensus::slip::{Slip, SlipType};
206 use crate::core::defs::{PrintForLog, UTXO_KEY_LENGTH};
207 use crate::core::util::balance_snapshot::BalanceSnapshot;
208
209 #[test]
210 fn load_save_test() {
211 let mut snapshot = BalanceSnapshot {
212 latest_block_id: 200,
213 latest_block_hash: [1; 32],
214 timestamp: 10000,
215 slips: vec![],
216 };
217 snapshot.slips.push(Slip {
218 public_key: [1; 33],
219 amount: 10,
220 slip_index: 1,
221 block_id: 1,
222 tx_ordinal: 1,
223 slip_type: SlipType::Normal,
224 utxoset_key: [0; UTXO_KEY_LENGTH],
225 is_utxoset_key_set: false,
226 });
227 snapshot.slips.push(Slip {
228 public_key: [2; 33],
229 amount: 20,
230 slip_index: 2,
231 block_id: 2,
232 tx_ordinal: 2,
233 slip_type: SlipType::Normal,
234 utxoset_key: [0; UTXO_KEY_LENGTH],
235 is_utxoset_key_set: false,
236 });
237 snapshot.slips.push(Slip {
238 public_key: [3; 33],
239 amount: 30,
240 slip_index: 3,
241 block_id: 3,
242 tx_ordinal: 3,
243 slip_type: SlipType::Normal,
244 utxoset_key: [0; UTXO_KEY_LENGTH],
245 is_utxoset_key_set: false,
246 });
247 let (file_name, rows) = snapshot.get_data();
248 assert!(!file_name.is_empty());
249 let expected_file_name = snapshot.timestamp.to_string()
250 + "-"
251 + snapshot.latest_block_id.to_string().as_str()
252 + "-"
253 + snapshot.latest_block_hash.to_hex().as_str()
254 + ".snap";
255 assert_eq!(file_name, expected_file_name);
256
257 assert_eq!(rows.len(), 3);
258 for (index, row) in rows.iter().enumerate() {
259 let slip = snapshot.slips.get(index);
260 assert!(slip.is_some());
261 let slip = slip.unwrap();
262 let key = slip.public_key.to_base58();
263 info!("key = {:?}", key);
264 let expected_str = format!(
265 "{} {:?} {:?} {:?} {:?}",
266 key, slip.block_id, slip.tx_ordinal, slip.slip_index, slip.amount
267 );
268 info!("{:?} row = {:?}", index, expected_str);
269
270 assert_eq!(row.as_str(), expected_str.as_str());
271 }
272 let file_name = file_name
273 .split('.')
274 .collect::<Vec<&str>>()
275 .first()
276 .unwrap()
277 .to_string();
278 assert!(!file_name.is_empty());
279 let snapshot2 = BalanceSnapshot::new(file_name, rows);
280 assert!(snapshot2.is_ok(), "error : {:?}", snapshot2.err().unwrap());
281 let snapshot2 = snapshot2.unwrap();
282 assert_eq!(snapshot.timestamp, snapshot2.timestamp);
283 assert_eq!(snapshot.latest_block_id, snapshot2.latest_block_id);
284 assert_eq!(snapshot.latest_block_hash, snapshot2.latest_block_hash);
285
286 for (index, slip) in snapshot.slips.iter().enumerate() {
287 let slip2 = snapshot2.slips.get(index).unwrap();
288 assert_eq!(slip.public_key, slip2.public_key);
289 assert_eq!(slip.block_id, slip2.block_id);
290 assert_eq!(slip.tx_ordinal, slip2.tx_ordinal);
291 assert_eq!(slip.amount, slip2.amount);
292 }
293 }
294
295 #[test]
296 fn to_string_test() {
297 let mut snapshot = BalanceSnapshot {
299 latest_block_id: 200,
300 latest_block_hash: [1; 32],
301 timestamp: 10000,
302 slips: vec![],
303 };
304 snapshot.slips.push(Slip {
305 public_key: [1; 33],
306 amount: 10,
307 slip_index: 1,
308 block_id: 1,
309 tx_ordinal: 1,
310 slip_type: SlipType::Normal,
311 utxoset_key: [0; UTXO_KEY_LENGTH],
312 is_utxoset_key_set: false,
313 });
314 snapshot.slips.push(Slip {
315 public_key: [2; 33],
316 amount: 20,
317 slip_index: 2,
318 block_id: 2,
319 tx_ordinal: 2,
320 slip_type: SlipType::Normal,
321 utxoset_key: [0; UTXO_KEY_LENGTH],
322 is_utxoset_key_set: false,
323 });
324 snapshot.slips.push(Slip {
325 public_key: [3; 33],
326 amount: 30,
327 slip_index: 3,
328 block_id: 3,
329 tx_ordinal: 3,
330 slip_type: SlipType::Normal,
331 utxoset_key: [0; UTXO_KEY_LENGTH],
332 is_utxoset_key_set: false,
333 });
334
335 let str = snapshot.to_string();
336 assert_ne!(str.len(), 0);
337
338 let result: Result<BalanceSnapshot, String> = str.try_into();
339 assert!(result.is_ok(), "{:?}", result.err().unwrap());
340
341 let snapshot2 = result.unwrap();
342 assert_eq!(snapshot.timestamp, snapshot2.timestamp);
343 assert_eq!(snapshot.latest_block_id, snapshot2.latest_block_id);
344 assert_eq!(snapshot.latest_block_hash, snapshot2.latest_block_hash);
345 assert_eq!(snapshot2.slips.len(), 3);
346
347 for (index, slip) in snapshot.slips.iter().enumerate() {
348 let slip2 = snapshot2.slips.get(index).unwrap();
349 assert_eq!(slip.public_key, slip2.public_key);
350 assert_eq!(slip.block_id, slip2.block_id);
351 assert_eq!(slip.tx_ordinal, slip2.tx_ordinal);
352 assert_eq!(slip.amount, slip2.amount);
353 }
354 }
355}