diff --git a/src/callbacks/mod.rs b/src/callbacks/mod.rs index 3ff3b44..4fca8c9 100644 --- a/src/callbacks/mod.rs +++ b/src/callbacks/mod.rs @@ -1,5 +1,6 @@ pub mod stats; pub mod csvdump; +pub mod unspentcsvdump; use clap::{ArgMatches, App}; diff --git a/src/callbacks/unspentcsvdump.rs b/src/callbacks/unspentcsvdump.rs new file mode 100644 index 0000000..8689155 --- /dev/null +++ b/src/callbacks/unspentcsvdump.rs @@ -0,0 +1,173 @@ +use std::fs::{self, File}; +use std::path::PathBuf; +use std::io::{BufWriter, Write}; +use std::collections::HashMap; +use std::collections::hash_map::Entry::{Occupied, Vacant}; + +use clap::{Arg, ArgMatches, App, SubCommand}; + +use callbacks::Callback; +use errors::{OpError, OpResult}; + +use blockchain::parser::types::CoinType; +use blockchain::proto::block::Block; +use blockchain::utils; + + +/// Dumps the whole blockchain into csv files +pub struct UnspentCsvDump { + // Each structure gets stored in a seperate csv file + dump_folder: PathBuf, + unspent_writer: BufWriter, + + transactions_unspent: HashMap, + + start_height: usize, + end_height: usize, + tx_count: u64, + in_count: u64, + out_count: u64 +} + +struct HashMapVal { +/* txid: String, + index: usize,*/ + block_height: usize, + output_val: u64, + address: String +} + +impl UnspentCsvDump { + fn create_writer(cap: usize, path: PathBuf) -> OpResult> { + let file = match File::create(&path) { + Ok(f) => f, + Err(err) => return Err(OpError::from(err)) + }; + Ok(BufWriter::with_capacity(cap, file)) + } +} + +impl Callback for UnspentCsvDump { + + fn build_subcommand<'a, 'b>() -> App<'a, 'b> where Self: Sized { + SubCommand::with_name("unspentcsvdump") + .about("Dumps the unspent outputs to CSV file") + .version("0.1") + .author("fsvm88 ") + .arg(Arg::with_name("dump-folder") + .help("Folder to store csv file") + .index(1) + .required(true)) + } + + fn new(matches: &ArgMatches) -> OpResult where Self: Sized { + let ref dump_folder = PathBuf::from(matches.value_of("dump-folder").unwrap()); // Save to unwrap + match (|| -> OpResult { + let cap = 4000000; + let cb = UnspentCsvDump { + dump_folder: PathBuf::from(dump_folder), + unspent_writer: try!(UnspentCsvDump::create_writer(cap, dump_folder.join("unspent.csv.tmp"))), + transactions_unspent: HashMap::with_capacity(10000000), // Init hashmap for tracking the unspent transactions (with 10'000'000 mln preallocated entries) + start_height: 0, end_height: 0, tx_count: 0, in_count: 0, out_count: 0 + }; + Ok(cb) + })() { + Ok(s) => return Ok(s), + Err(e) => return Err( + tag_err!(e, "Couldn't initialize csvdump with folder: `{}`", dump_folder + .as_path() + .display())) + } + } + + fn on_start(&mut self, _: CoinType, block_height: usize) { + self.start_height = block_height; + info!(target: "callback", "Using `unspentcsvdump` with dump folder: {} ...", &self.dump_folder.display()); + } + + fn on_block(&mut self, block: Block, block_height: usize) { + // serialize transaction + for tx in block.txs { + // For each transaction in the block, + // 1. apply input transactions (remove (TxID == prevTxIDOut and prevOutID == spentOutID)) + // 2. apply output transactions (add (TxID + curOutID -> HashMapVal)) + // For each address, retain: + // * block height as "last modified" + // * output_val + // * address + + //self.tx_writer.write_all(tx.as_csv(&block_hash).as_bytes()).unwrap(); + let txid_str = utils::arr_to_hex_swapped(&tx.hash); + + for input in &tx.value.inputs { + let input_outpoint_txid_idx = utils::arr_to_hex_swapped(&input.outpoint.txid) + &input.outpoint.index.to_string(); + let val: bool = match self.transactions_unspent.entry(input_outpoint_txid_idx.clone()) { + Occupied(_) => true, + Vacant(_) => false, + }; + + if val { + self.transactions_unspent.remove(&input_outpoint_txid_idx); + }; + } + self.in_count += tx.value.in_count.value; + + // serialize outputs + for (i, output) in tx.value.outputs.iter().enumerate() { + let hash_val: HashMapVal = HashMapVal { + block_height: block_height, + output_val: output.out.value, + address: output.script.address.clone(), + //script_pubkey: utils::arr_to_hex(&output.out.script_pubkey) + }; + self.transactions_unspent.insert(txid_str.clone() + &i.to_string(), hash_val); + } + self.out_count += tx.value.out_count.value; + } + self.tx_count += block.tx_count.value; + } + + fn on_complete(&mut self, block_height: usize) { + self.end_height = block_height; + + self.unspent_writer.write_all(format!( + "{};{};{};{};{}\n", + "txid", + "indexOut", + "height", + "value", + "address" + ).as_bytes() + ).unwrap(); + for (key, value) in self.transactions_unspent.iter() { + let txid = &key[0..63]; + let index = &key[64..key.len()-1]; + //let = key.len(); + //let mut mut_key = key.clone(); + //let index: String = mut_key.pop().unwrap().to_string(); + self.unspent_writer.write_all(format!( + "{};{};{};{};{}\n", + txid, + index, + value.block_height, + value.output_val, + value.address + ).as_bytes() + ).unwrap(); + } + + // Keep in sync with c'tor + for f in vec!["unspent"] { + // Rename temp files + fs::rename(self.dump_folder.as_path().join(format!("{}.csv.tmp", f)), + self.dump_folder.as_path().join(format!("{}-{}-{}.csv", f, self.start_height, self.end_height))) + .expect("Unable to rename tmp file!"); + } + + info!(target: "callback", "Done.\nDumped all {} blocks:\n\ + \t-> transactions: {:9}\n\ + \t-> inputs: {:9}\n\ + \t-> outputs: {:9}", + self.end_height + 1, self.tx_count, self.in_count, self.out_count); + } +} diff --git a/src/main.rs b/src/main.rs index 6fea284..6765ed3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,6 +38,7 @@ use errors::{OpError, OpErrorKind, OpResult}; use callbacks::Callback; use callbacks::stats::SimpleStats; use callbacks::csvdump::CsvDump; +use callbacks::unspentcsvdump::UnspentCsvDump; /// Holds all available user arguments @@ -221,6 +222,7 @@ fn parse_args() -> OpResult { .help("Sets maximum worker backlog (default: 100)") .takes_value(true)) // Add callbacks + .subcommand(UnspentCsvDump::build_subcommand()) .subcommand(CsvDump::build_subcommand()) .subcommand(SimpleStats::build_subcommand()) .get_matches(); @@ -251,6 +253,8 @@ fn parse_args() -> OpResult { callback = Box::new(try!(SimpleStats::new(matches))); } else if let Some(ref matches) = matches.subcommand_matches("csvdump") { callback = Box::new(try!(CsvDump::new(matches))); + } else if let Some(ref matches) = matches.subcommand_matches("unspentcsvdump") { + callback = Box::new(try!(UnspentCsvDump::new(matches))); } else { clap::Error { message: String::from("error: No Callback specified.\nFor more information try --help"),