Skip to content

Commit 3bf0a44

Browse files
committed
make crate into a library
This allows interested parties to consume code in here for their own application, as well as the addition of integration tests in an upcoming change.
1 parent 4c86254 commit 3bf0a44

File tree

2 files changed

+305
-302
lines changed

2 files changed

+305
-302
lines changed

src/lib.rs

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
use clap::{Arg, Command};
2+
3+
use crate::callbacks::balances::Balances;
4+
use crate::callbacks::opreturn::OpReturn;
5+
use crate::callbacks::simplestats::SimpleStats;
6+
use crate::callbacks::unspentcsvdump::UnspentCsvDump;
7+
use crate::callbacks::Callback;
8+
9+
use crate::parser::types::{Bitcoin, CoinType};
10+
11+
pub mod callbacks;
12+
pub mod parser;
13+
14+
#[derive(Copy, Clone)]
15+
#[cfg_attr(test, derive(PartialEq, Debug))]
16+
pub struct BlockHeightRange {
17+
start: u64,
18+
end: Option<u64>,
19+
}
20+
21+
impl BlockHeightRange {
22+
pub fn new(start: u64, end: Option<u64>) -> anyhow::Result<Self> {
23+
if end.is_some() && start > end.unwrap() {
24+
anyhow::bail!("--start value must be lower than --end value",);
25+
}
26+
Ok(Self { start, end })
27+
}
28+
29+
#[must_use]
30+
pub fn is_default(&self) -> bool {
31+
self.start == 0 && self.end.is_none()
32+
}
33+
}
34+
35+
impl std::fmt::Display for BlockHeightRange {
36+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
37+
let end = match self.end {
38+
Some(e) => e.to_string(),
39+
None => String::from("HEAD"),
40+
};
41+
write!(f, "{}..{}", self.start, end)
42+
}
43+
}
44+
45+
/// Holds all available user arguments
46+
pub struct ParserOptions {
47+
// Name of the callback which gets executed for each block. (See callbacks/mod.rs)
48+
callback: Box<dyn Callback>,
49+
// Holds the relevant coin parameters we need for parsing
50+
coin: CoinType,
51+
// Enable this if you want to check the chain index integrity and merkle root for each block.
52+
pub verify: bool,
53+
// Path to directory where blk.dat files are stored
54+
pub blockchain_dir: std::path::PathBuf,
55+
// Range which is considered for parsing
56+
range: BlockHeightRange,
57+
}
58+
59+
#[must_use]
60+
pub fn command() -> Command {
61+
let coins = ["bitcoin", "testnet3"];
62+
Command::new("bitcoin-blockparser")
63+
.version(clap::crate_version!())
64+
// Add flags
65+
.arg(Arg::new("verify")
66+
.long("verify")
67+
.action(clap::ArgAction::SetTrue)
68+
.value_parser(clap::value_parser!(bool))
69+
.help("Verifies merkle roots and block hashes"))
70+
.arg(Arg::new("verbosity")
71+
.short('v')
72+
.action(clap::ArgAction::Count)
73+
.help("Increases verbosity level. Info=0, Debug=1, Trace=2 (default: 0)"))
74+
// Add options
75+
.arg(Arg::new("coin")
76+
.short('c')
77+
.long("coin")
78+
.value_name("NAME")
79+
.value_parser(clap::builder::PossibleValuesParser::new(coins))
80+
.help("Specify blockchain coin (default: bitcoin)"))
81+
.arg(Arg::new("blockchain-dir")
82+
.short('d')
83+
.long("blockchain-dir")
84+
.help("Sets blockchain directory which contains blk.dat files (default: ~/.bitcoin/blocks)"))
85+
.arg(Arg::new("start")
86+
.short('s')
87+
.long("start")
88+
.value_name("HEIGHT")
89+
.value_parser(clap::value_parser!(u64))
90+
.help("Specify starting block for parsing (inclusive)"))
91+
.arg(Arg::new("end")
92+
.short('e')
93+
.long("end")
94+
.value_name("HEIGHT")
95+
.value_parser(clap::value_parser!(u64))
96+
.help("Specify last block for parsing (inclusive) (default: all known blocks)"))
97+
// Add callbacks
98+
.subcommand(UnspentCsvDump::build_subcommand())
99+
.subcommand(SimpleStats::build_subcommand())
100+
.subcommand(Balances::build_subcommand())
101+
.subcommand(OpReturn::build_subcommand())
102+
}
103+
104+
/// Returns default directory. TODO: test on windows
105+
fn get_absolute_blockchain_dir(coin: &CoinType) -> std::path::PathBuf {
106+
dirs::home_dir()
107+
.expect("Unable to get home path from env!")
108+
.join(&coin.default_folder)
109+
}
110+
111+
/// Parses args or panics if some requirements are not met.
112+
pub fn parse_args(matches: &clap::ArgMatches) -> anyhow::Result<ParserOptions> {
113+
let verify = matches.get_flag("verify");
114+
115+
let coin = matches
116+
.get_one::<String>("coin")
117+
.map_or_else(|| CoinType::from(Bitcoin), |v| v.parse().unwrap());
118+
let blockchain_dir = match matches.get_one::<String>("blockchain-dir") {
119+
Some(p) => std::path::PathBuf::from(p),
120+
None => get_absolute_blockchain_dir(&coin),
121+
};
122+
let start = matches.get_one::<u64>("start").copied().unwrap_or(0);
123+
let end = matches.get_one::<u64>("end").copied();
124+
let range = BlockHeightRange::new(start, end)?;
125+
126+
// Set callback
127+
let callback: Box<dyn Callback>;
128+
if let Some(matches) = matches.subcommand_matches("simplestats") {
129+
callback = Box::new(SimpleStats::new(matches)?);
130+
} else if let Some(matches) = matches.subcommand_matches("unspentcsvdump") {
131+
callback = Box::new(UnspentCsvDump::new(matches)?);
132+
} else if let Some(matches) = matches.subcommand_matches("balances") {
133+
callback = Box::new(Balances::new(matches)?);
134+
} else if let Some(matches) = matches.subcommand_matches("opreturn") {
135+
callback = Box::new(OpReturn::new(matches)?);
136+
} else {
137+
clap::error::Error::<clap::error::DefaultFormatter>::raw(
138+
clap::error::ErrorKind::MissingSubcommand,
139+
"error: No valid callback specified.\nFor more information try --help",
140+
)
141+
.exit();
142+
}
143+
144+
let options = ParserOptions {
145+
callback,
146+
coin,
147+
verify,
148+
blockchain_dir,
149+
range,
150+
};
151+
Ok(options)
152+
}
153+
154+
#[cfg(test)]
155+
mod tests {
156+
use super::*;
157+
158+
#[test]
159+
fn test_args_subcommand() {
160+
let tmp_dir = tempfile::tempdir().unwrap();
161+
parse_args(&command().get_matches_from([
162+
"bitcoin-blockparser",
163+
"unspentcsvdump",
164+
tmp_dir.path().to_str().unwrap(),
165+
]))
166+
.unwrap();
167+
parse_args(&command().get_matches_from(["bitcoin-blockparser", "simplestats"])).unwrap();
168+
parse_args(&command().get_matches_from([
169+
"bitcoin-blockparser",
170+
"balances",
171+
tmp_dir.path().to_str().unwrap(),
172+
]))
173+
.unwrap();
174+
parse_args(&command().get_matches_from(["bitcoin-blockparser", "opreturn"])).unwrap();
175+
}
176+
177+
#[test]
178+
fn test_args_coin() {
179+
let args = ["bitcoin-blockparser", "simplestats"];
180+
let options = parse_args(&command().get_matches_from(args)).unwrap();
181+
assert_eq!(options.coin.name, "Bitcoin");
182+
183+
let args = ["bitcoin-blockparser", "-c", "testnet3", "simplestats"];
184+
let options = parse_args(&command().get_matches_from(args)).unwrap();
185+
assert_eq!(options.coin.name, "TestNet3");
186+
}
187+
188+
#[test]
189+
fn test_args_verify() {
190+
let args = ["bitcoin-blockparser", "simplestats"];
191+
let options = parse_args(&command().get_matches_from(args)).unwrap();
192+
assert!(!options.verify);
193+
194+
let args = ["bitcoin-blockparser", "--verify", "simplestats"];
195+
let options = parse_args(&command().get_matches_from(args)).unwrap();
196+
assert!(options.verify);
197+
}
198+
199+
#[test]
200+
fn test_args_blockchain_dir() {
201+
let args = ["bitcoin-blockparser", "simplestats"];
202+
let options = parse_args(&command().get_matches_from(args)).unwrap();
203+
assert_eq!(
204+
options.blockchain_dir,
205+
dirs::home_dir()
206+
.unwrap()
207+
.join(std::path::Path::new(".bitcoin").join("blocks"))
208+
);
209+
210+
let args = ["bitcoin-blockparser", "-d", "foo", "simplestats"];
211+
let options = parse_args(&command().get_matches_from(args)).unwrap();
212+
assert_eq!(options.blockchain_dir.to_str().unwrap(), "foo");
213+
214+
let args = [
215+
"bitcoin-blockparser",
216+
"--blockchain-dir",
217+
"foo",
218+
"simplestats",
219+
];
220+
let options = parse_args(&command().get_matches_from(args)).unwrap();
221+
assert_eq!(options.blockchain_dir.to_str().unwrap(), "foo");
222+
}
223+
224+
#[test]
225+
fn test_args_start() {
226+
let args = ["bitcoin-blockparser", "simplestats"];
227+
let options = parse_args(&command().get_matches_from(args)).unwrap();
228+
assert_eq!(
229+
options.range,
230+
BlockHeightRange {
231+
start: 0,
232+
end: None
233+
}
234+
);
235+
236+
let args = ["bitcoin-blockparser", "-s", "10", "simplestats"];
237+
let options = parse_args(&command().get_matches_from(args)).unwrap();
238+
assert_eq!(
239+
options.range,
240+
BlockHeightRange {
241+
start: 10,
242+
end: None
243+
}
244+
);
245+
246+
let args = ["bitcoin-blockparser", "--start", "10", "simplestats"];
247+
let options = parse_args(&command().get_matches_from(args)).unwrap();
248+
assert_eq!(
249+
options.range,
250+
BlockHeightRange {
251+
start: 10,
252+
end: None
253+
}
254+
);
255+
}
256+
257+
#[test]
258+
fn test_args_end() {
259+
let args = ["bitcoin-blockparser", "-e", "10", "simplestats"];
260+
let options = parse_args(&command().get_matches_from(args)).unwrap();
261+
assert_eq!(
262+
options.range,
263+
BlockHeightRange {
264+
start: 0,
265+
end: Some(10)
266+
}
267+
);
268+
269+
let args = ["bitcoin-blockparser", "--end", "10", "simplestats"];
270+
let options = parse_args(&command().get_matches_from(args)).unwrap();
271+
assert_eq!(
272+
options.range,
273+
BlockHeightRange {
274+
start: 0,
275+
end: Some(10)
276+
}
277+
);
278+
}
279+
280+
#[test]
281+
fn test_args_start_and_end() {
282+
let args = ["bitcoin-blockparser", "-s", "1", "-e", "2", "simplestats"];
283+
let options = parse_args(&command().get_matches_from(args)).unwrap();
284+
assert_eq!(
285+
options.range,
286+
BlockHeightRange {
287+
start: 1,
288+
end: Some(2)
289+
}
290+
);
291+
292+
let args = ["bitcoin-blockparser", "-s", "2", "-e", "1", "simplestats"];
293+
assert!(parse_args(&command().get_matches_from(args)).is_err());
294+
}
295+
}

0 commit comments

Comments
 (0)