bitcoin_terminal_dashboard/
lib.rs1use std::cell::RefCell;
2use std::env;
3use std::io::stdout;
4use std::rc::Rc;
5use std::thread::{self, sleep};
6use std::time::Duration;
7mod utils;
8
9use app::{App, AppReturn};
10use bitcoin_node_query::{self, Client};
11use eyre::Result;
12use inputs::events::Events;
13use inputs::{FetchEvent, InputEvent, Resource};
14use tui::backend::CrosstermBackend;
15use tui::Terminal;
16
17use crate::app::ui;
18
19pub mod app;
20pub mod inputs;
21
22fn get_client() -> Client {
23 let password = env::var("BITCOIND_PASSWORD").expect("BITCOIND_PASSWORD env variable not set");
24 let username = env::var("BITCOIND_USERNAME").expect("BITCOIND_USERNAME env variable not set");
25 let url = env::var("BITCOIND_URL").expect("BITCOIND_URL env variable not set");
26 let client = Client::new(&url, &username, &password).expect("failed to create client");
27 client
28}
29
30fn start_loop_for_fetching_seconds_since_last_block(events: &Events) {
31 let tx = events.tx.clone();
32 let event_tx_for_seconds_since_last_block = tx.clone();
33 let c = get_client();
34 thread::spawn(move || loop {
35 let _ = event_tx_for_seconds_since_last_block
37 .clone()
38 .send(InputEvent::FetchResource(Resource::SecondsSinceLastBlock(
39 FetchEvent::Start,
40 )));
41 let seconds_since_last_block = bitcoin_node_query::get_time_since_last_block_in_seconds(&c);
42 let _ = event_tx_for_seconds_since_last_block
44 .clone()
45 .send(InputEvent::FetchResource(Resource::SecondsSinceLastBlock(
46 FetchEvent::Complete(seconds_since_last_block as u64),
47 )));
48 sleep(Duration::from_secs(1));
49 });
50}
51
52fn start_loop_for_fetching_bitcoin_price(events: &Events) {
53 let tx = events.tx.clone();
54 let event = tx.clone();
55 thread::spawn(move || loop {
56 let _ = event
58 .clone()
59 .send(InputEvent::FetchResource(Resource::BitcoinPrice(
60 FetchEvent::Start,
61 )));
62 let bitcoin_price = bitcoin_price::get_average_exchange_spot_price();
63 let _ = event
65 .clone()
66 .send(InputEvent::FetchResource(Resource::BitcoinPrice(
67 FetchEvent::Complete(bitcoin_price),
68 )));
69 sleep(Duration::from_secs(30));
70 });
71}
72fn start_loop_for_fetching_new_block_height(events: &Events) {
73 let tx = events.tx.clone();
74 let event_tx_for_new_block_height = tx.clone();
75 let c = get_client();
76 thread::spawn(move || loop {
77 let _ = event_tx_for_new_block_height
79 .clone()
80 .send(InputEvent::FetchResource(Resource::NewBlockHeight(
81 FetchEvent::Start,
82 )));
83 let block_height = bitcoin_node_query::get_block_height(&c);
84 let _ = event_tx_for_new_block_height
86 .clone()
87 .send(InputEvent::FetchResource(Resource::NewBlockHeight(
88 FetchEvent::Complete(block_height as u64),
89 )));
90 sleep(Duration::from_secs(1));
91 });
92}
93fn start_loop_for_fetching_transactions_count_over_last_30_days(events: &Events) {
94 let tx = events.tx.clone();
95 let c = get_client();
96 thread::spawn(move || loop {
97 let _ = tx.clone().send(InputEvent::FetchResource(
98 Resource::TransactionsCountOverLast30Days(FetchEvent::Start),
99 ));
100 let transactions_count_over_last_30_days =
101 bitcoin_node_query::get_transactions_count_over_last_30_days(&c);
102 let _ = tx.clone().send(InputEvent::FetchResource(
103 Resource::TransactionsCountOverLast30Days(FetchEvent::Complete(
104 transactions_count_over_last_30_days,
105 )),
106 ));
107 sleep(Duration::from_secs(5 * 60));
108 });
109}
110fn start_loop_for_fetching_average_block_time_for_last_2016_blocks(events: &Events) {
111 let tx = events.tx.clone();
112 let c = get_client();
113 thread::spawn(move || loop {
114 let _ = tx.clone().send(InputEvent::FetchResource(
115 Resource::AverageBlockTimeForLast2016Blocks(FetchEvent::Start),
116 ));
117 let average_block_time_for_last_2016_blocks =
118 bitcoin_node_query::get_average_block_time_for_last_2016_blocks(&c);
119 let _ = tx.clone().send(InputEvent::FetchResource(
120 Resource::AverageBlockTimeForLast2016Blocks(FetchEvent::Complete(
121 average_block_time_for_last_2016_blocks,
122 )),
123 ));
124 sleep(Duration::from_secs(5 * 60));
125 });
126}
127
128fn start_loop_for_fetching_chain_size(events: &Events) {
129 let tx = events.tx.clone();
130 let c = get_client();
131 thread::spawn(move || loop {
132 let _ = tx
133 .clone()
134 .send(InputEvent::FetchResource(Resource::ChainSize(
135 FetchEvent::Start,
136 )));
137 let chain_size = bitcoin_node_query::get_chain_size(&c);
138 let _ = tx
139 .clone()
140 .send(InputEvent::FetchResource(Resource::ChainSize(
141 FetchEvent::Complete(chain_size),
142 )));
143 sleep(Duration::from_secs(5 * 60));
144 });
145}
146fn start_loop_for_fetching_utxo_set_size(events: &Events, sleep_duration_seconds: u64) {
148 let tx = events.tx.clone();
149 let c = get_client();
150 thread::spawn(move || loop {
151 sleep(Duration::from_secs(sleep_duration_seconds));
154 let _ = tx
155 .clone()
156 .send(InputEvent::FetchResource(Resource::UtxoSetSize(
157 FetchEvent::Start,
158 )));
159 let utxo_set_size = bitcoin_node_query::get_utxo_set_size(&c);
160 let _ = tx
161 .clone()
162 .send(InputEvent::FetchResource(Resource::UtxoSetSize(
163 FetchEvent::Complete(utxo_set_size),
164 )));
165 sleep(Duration::from_secs(20 * 60));
166 });
167}
168fn start_loop_for_fetching_total_money_supply(events: &Events, sleep_duration_seconds: u64) {
170 let tx = events.tx.clone();
171 let c = get_client();
172 thread::spawn(move || loop {
173 sleep(Duration::from_secs(sleep_duration_seconds));
176 let _ = tx
177 .clone()
178 .send(InputEvent::FetchResource(Resource::TotalMoneySupply(
179 FetchEvent::Start,
180 )));
181 let total_money_supply = bitcoin_node_query::get_total_money_supply(&c);
182 let _ = tx
183 .clone()
184 .send(InputEvent::FetchResource(Resource::TotalMoneySupply(
185 FetchEvent::Complete(total_money_supply),
186 )));
187 sleep(Duration::from_secs(20 * 60));
188 });
189}
190fn start_loop_for_fetching_total_transaction_count(events: &Events) {
191 let tx = events.tx.clone();
192 let c = get_client();
193 thread::spawn(move || loop {
194 let _ = tx
195 .clone()
196 .send(InputEvent::FetchResource(Resource::TotalTransactionCount(
197 FetchEvent::Start,
198 )));
199 let total_transactions_count = bitcoin_node_query::get_total_transactions_count(&c);
200 let _ = tx
201 .clone()
202 .send(InputEvent::FetchResource(Resource::TotalTransactionCount(
203 FetchEvent::Complete(total_transactions_count),
204 )));
205 sleep(Duration::from_secs(5 * 60));
206 });
207}
208
209fn start_loop_for_fetching_tps_for_last_30_days(events: &Events) {
210 let tx = events.tx.clone();
211 let c = get_client();
212 thread::spawn(move || loop {
213 let _ = tx
214 .clone()
215 .send(InputEvent::FetchResource(Resource::TpsForLast30Days(
216 FetchEvent::Start,
217 )));
218 let tps_for_last_30_days = bitcoin_node_query::get_tps_for_last_30_days(&c);
219 let _ = tx
220 .clone()
221 .send(InputEvent::FetchResource(Resource::TpsForLast30Days(
222 FetchEvent::Complete(tps_for_last_30_days),
223 )));
224 sleep(Duration::from_secs(5 * 60));
225 });
226}
227fn start_loop_for_fetching_total_fees_for_last_24_hours(events: &Events) {
228 let tx = events.tx.clone();
229 let c = get_client();
230 thread::spawn(move || loop {
231 let _ = tx.clone().send(InputEvent::FetchResource(
232 Resource::TotalFeesForLast24Hours(FetchEvent::Start),
233 ));
234 let total_fees_for_last_24_hours = bitcoin_node_query::get_total_fee_for_24_hours(&c);
235 let _ = tx.clone().send(InputEvent::FetchResource(
236 Resource::TotalFeesForLast24Hours(FetchEvent::Complete(total_fees_for_last_24_hours)),
237 ));
238 sleep(Duration::from_secs(5 * 60));
239 });
240}
241fn start_loop_for_fetching_difficulty(events: &Events) {
242 let tx = events.tx.clone();
243 let c = get_client();
244 thread::spawn(move || loop {
245 let _ = tx
246 .clone()
247 .send(InputEvent::FetchResource(Resource::Difficulty(
248 FetchEvent::Start,
249 )));
250 let difficulty = bitcoin_node_query::get_difficulty(&c);
251 let _ = tx
252 .clone()
253 .send(InputEvent::FetchResource(Resource::Difficulty(
254 FetchEvent::Complete(difficulty),
255 )));
256 sleep(Duration::from_secs(5 * 60));
257 });
258}
259
260fn start_loop_for_fetching_current_difficulty_epoch(events: &Events) {
261 let tx = events.tx.clone();
262 let c = get_client();
263 thread::spawn(move || loop {
264 let _ = tx
265 .clone()
266 .send(InputEvent::FetchResource(Resource::CurrentDifficultyEpoch(
267 FetchEvent::Start,
268 )));
269 let difficulty = bitcoin_node_query::get_current_difficulty_epoch(&c);
270 let _ = tx
271 .clone()
272 .send(InputEvent::FetchResource(Resource::CurrentDifficultyEpoch(
273 FetchEvent::Complete(difficulty),
274 )));
275 sleep(Duration::from_secs(5 * 60));
276 });
277}
278fn start_loop_for_fetching_block_count_until_retarget(events: &Events) {
279 let tx = events.tx.clone();
280 let c = get_client();
281 thread::spawn(move || loop {
282 let _ = tx.clone().send(InputEvent::FetchResource(
283 Resource::BlockCountUntilRetarget(FetchEvent::Start),
284 ));
285 let block_count_until_retarget = bitcoin_node_query::get_blocks_count_until_retarget(&c);
286 let _ = tx.clone().send(InputEvent::FetchResource(
287 Resource::BlockCountUntilRetarget(FetchEvent::Complete(block_count_until_retarget)),
288 ));
289 sleep(Duration::from_secs(5 * 60));
290 });
291}
292fn start_loop_for_fetching_estimated_seconds_until_retarget(events: &Events) {
293 let tx = events.tx.clone();
294 let c = get_client();
295 thread::spawn(move || loop {
296 let _ = tx.clone().send(InputEvent::FetchResource(
297 Resource::EstimatedSecondsUntilRetarget(FetchEvent::Start),
298 ));
299 let estimated_seconds_until_retarget =
300 bitcoin_node_query::get_estimated_seconds_until_retarget(&c);
301 let _ = tx.clone().send(InputEvent::FetchResource(
302 Resource::EstimatedSecondsUntilRetarget(FetchEvent::Complete(
303 estimated_seconds_until_retarget,
304 )),
305 ));
306 sleep(Duration::from_secs(5 * 60));
307 });
308}
309fn start_loop_for_fetching_average_block_time_since_last_difficulty_adjustement(events: &Events) {
310 let tx = events.tx.clone();
311 let c = get_client();
312 thread::spawn(move || loop {
313 let _ = tx.clone().send(InputEvent::FetchResource(
314 Resource::AverageBlockTimeSinceLastDifficultyAdjustment(FetchEvent::Start),
315 ));
316 let average_block_time_since_last_difficulty_adjustement =
317 bitcoin_node_query::get_average_block_time_for_since_last_difficulty_adjustement(&c);
318 let _ = tx.clone().send(InputEvent::FetchResource(
319 Resource::AverageBlockTimeSinceLastDifficultyAdjustment(FetchEvent::Complete(
320 average_block_time_since_last_difficulty_adjustement,
321 )),
322 ));
323 sleep(Duration::from_secs(5 * 60));
324 });
325}
326
327fn start_loop_for_fetching_hash_rate_per_second_for_last_2016_blocks(events: &Events) {
328 let tx = events.tx.clone();
329 let c = get_client();
330 thread::spawn(move || loop {
331 let _ = tx.clone().send(InputEvent::FetchResource(
332 Resource::EstimatedHashRatePerSecondForLast2016Blocks(FetchEvent::Start),
333 ));
334 let estimated_hash_rate_per_second_for_last_2016_blocks =
335 bitcoin_node_query::get_estimated_hash_rate_per_second_for_last_2016_blocks(&c);
336 let _ = tx.clone().send(InputEvent::FetchResource(
337 Resource::EstimatedHashRatePerSecondForLast2016Blocks(FetchEvent::Complete(
338 estimated_hash_rate_per_second_for_last_2016_blocks,
339 )),
340 ));
341 sleep(Duration::from_secs(5 * 60));
342 });
343}
344fn start_loop_for_fetching_block_subsidy_of_most_recent_block(events: &Events) {
345 let tx = events.tx.clone();
346 let c = get_client();
347 thread::spawn(move || loop {
348 let _ = tx.clone().send(InputEvent::FetchResource(
349 Resource::BlockSubsidyOfMostRecentBlock(FetchEvent::Start),
350 ));
351 let block_subsidy_of_most_recent_block =
352 bitcoin_node_query::get_block_subsidy_of_most_recent_block(&c);
353 let _ = tx.clone().send(InputEvent::FetchResource(
354 Resource::BlockSubsidyOfMostRecentBlock(FetchEvent::Complete(
355 block_subsidy_of_most_recent_block,
356 )),
357 ));
358 sleep(Duration::from_secs(5 * 60));
359 });
360}
361fn start_loop_for_fetching_blocks_mined_over_last_24_hours(events: &Events) {
362 let tx = events.tx.clone();
363 let c = get_client();
364 thread::spawn(move || loop {
365 let _ = tx.clone().send(InputEvent::FetchResource(
366 Resource::BlocksMinedOverLast24Hours(FetchEvent::Start),
367 ));
368 let blocks_mined_over_last_24_hours =
369 bitcoin_node_query::get_blocks_mined_over_last_24_hours_count(&c);
370 let _ = tx.clone().send(InputEvent::FetchResource(
371 Resource::BlocksMinedOverLast24Hours(FetchEvent::Complete(
372 blocks_mined_over_last_24_hours,
373 )),
374 ));
375 sleep(Duration::from_secs(5 * 60));
376 });
377}
378fn start_loop_for_fetching_average_fees_per_block_over_last_24_hours(events: &Events) {
379 let tx = events.tx.clone();
380 let c = get_client();
381 thread::spawn(move || loop {
382 let _ = tx.clone().send(InputEvent::FetchResource(
383 Resource::AverageFeesPerBlockOverLast24Hours(FetchEvent::Start),
384 ));
385 let average_fees_per_block_over_last_24_hours =
386 bitcoin_node_query::get_average_fees_per_block_over_last_24_hours(&c);
387 let _ = tx.clone().send(InputEvent::FetchResource(
388 Resource::AverageFeesPerBlockOverLast24Hours(FetchEvent::Complete(
389 average_fees_per_block_over_last_24_hours,
390 )),
391 ));
392 sleep(Duration::from_secs(5 * 60));
393 });
394}
395fn start_loop_for_fetching_average_fees_per_block_over_last_2016_blocks(events: &Events) {
396 let tx = events.tx.clone();
397 let c = get_client();
398 thread::spawn(move || loop {
399 let _ = tx.clone().send(InputEvent::FetchResource(
400 Resource::AverageFeesPerBlockOverLast2016Blocks(FetchEvent::Start),
401 ));
402 let average_fees_per_block_over_last_2016_blocks =
403 bitcoin_node_query::get_average_fees_per_block_over_last_2016_blocks(&c);
404 let _ = tx.clone().send(InputEvent::FetchResource(
405 Resource::AverageFeesPerBlockOverLast2016Blocks(FetchEvent::Complete(
406 average_fees_per_block_over_last_2016_blocks,
407 )),
408 ));
409 sleep(Duration::from_secs(5 * 60));
410 });
411}
412fn start_loop_for_fetching_fees_as_a_percent_of_reward_for_last_2016_blocks(events: &Events) {
413 let tx = events.tx.clone();
414 let c = get_client();
415 thread::spawn(move || loop {
416 let _ = tx.clone().send(InputEvent::FetchResource(
417 Resource::FeesAsAPercentOfRewardForLast2016Blocks(FetchEvent::Start),
418 ));
419 let fees_as_a_percent_of_reward_for_last_2016_blocks =
420 bitcoin_node_query::get_fees_as_a_percent_of_reward_for_last_2016_blocks(&c);
421 let _ = tx.clone().send(InputEvent::FetchResource(
422 Resource::FeesAsAPercentOfRewardForLast2016Blocks(FetchEvent::Complete(
423 fees_as_a_percent_of_reward_for_last_2016_blocks,
424 )),
425 ));
426 sleep(Duration::from_secs(5 * 60));
427 });
428}
429fn start_loop_for_fetching_fees_as_a_percent_of_reward_for_last_24_hours(events: &Events) {
430 let tx = events.tx.clone();
431 let c = get_client();
432 thread::spawn(move || loop {
433 let _ = tx.clone().send(InputEvent::FetchResource(
434 Resource::FeesAsAPercentOfRewardForLast24Hours(FetchEvent::Start),
435 ));
436 let fees_as_a_percent_of_reward_for_last_24_hours =
437 bitcoin_node_query::get_fees_as_a_percent_of_reward_for_last_24_hours(&c);
438 let _ = tx.clone().send(InputEvent::FetchResource(
439 Resource::FeesAsAPercentOfRewardForLast24Hours(FetchEvent::Complete(
440 fees_as_a_percent_of_reward_for_last_24_hours,
441 )),
442 ));
443 sleep(Duration::from_secs(5 * 60));
444 });
445}
446fn start_loop_for_fetching_segwit_stats_for_last_24_hours(events: &Events) {
447 let tx = events.tx.clone();
448 let c = get_client();
449 thread::spawn(move || loop {
450 let _ = tx.clone().send(InputEvent::FetchResource(
451 Resource::SegwitPercentLast24Hours(FetchEvent::Start),
452 ));
453 let _ = tx.clone().send(InputEvent::FetchResource(
454 Resource::SegwitSpendingPaymentsPercentLast24Hours(FetchEvent::Start),
455 ));
456 let _ = tx.clone().send(InputEvent::FetchResource(
457 Resource::SegwitSpendingTransactionsPercentLast24Hours(FetchEvent::Start),
458 ));
459 let (
462 percent_of_transactions_with_a_segwit_vout,
463 percent_of_transactions_with_a_segwit_vin_or_vout,
464 percent_based_on_transaction_hexes,
465 percent_of_payments_spending_segwit_per_day,
466 percent_of_segwit_spending_transactions_per_day,
467 ) = bitcoin_node_query::get_percent_of_vouts_used_segwit_over_last_24_hours(&c);
468 let _ = tx.clone().send(InputEvent::FetchResource(
469 Resource::SegwitPercentLast24Hours(FetchEvent::Complete(
470 percent_based_on_transaction_hexes,
471 )),
472 ));
473 let _ = tx.clone().send(InputEvent::FetchResource(
474 Resource::SegwitSpendingPaymentsPercentLast24Hours(FetchEvent::Complete(
475 percent_of_payments_spending_segwit_per_day,
476 )),
477 ));
478 let _ = tx.clone().send(InputEvent::FetchResource(
479 Resource::SegwitSpendingTransactionsPercentLast24Hours(FetchEvent::Complete(
480 percent_of_segwit_spending_transactions_per_day,
481 )),
482 ));
483 sleep(Duration::from_secs(3 * 60 * 60));
485 });
486}
487
488pub fn start_ui(app: Rc<RefCell<App>>) -> Result<()> {
489 let stdout = stdout();
491 crossterm::terminal::enable_raw_mode()?;
492 let backend = CrosstermBackend::new(stdout);
493 let mut terminal = Terminal::new(backend)?;
494 terminal.clear()?;
495 terminal.hide_cursor()?;
496
497 let tick_rate = Duration::from_millis(200);
499 let events = Events::new(tick_rate);
500
501 start_loop_for_fetching_bitcoin_price(&events);
502 start_loop_for_fetching_seconds_since_last_block(&events);
503 start_loop_for_fetching_transactions_count_over_last_30_days(&events);
504 start_loop_for_fetching_new_block_height(&events);
505 start_loop_for_fetching_average_block_time_for_last_2016_blocks(&events);
506 start_loop_for_fetching_chain_size(&events);
507 start_loop_for_fetching_total_transaction_count(&events);
508 start_loop_for_fetching_tps_for_last_30_days(&events);
509 start_loop_for_fetching_total_fees_for_last_24_hours(&events);
510 start_loop_for_fetching_difficulty(&events);
511 start_loop_for_fetching_current_difficulty_epoch(&events);
512 start_loop_for_fetching_block_count_until_retarget(&events);
513 start_loop_for_fetching_estimated_seconds_until_retarget(&events);
514 start_loop_for_fetching_average_block_time_since_last_difficulty_adjustement(&events);
515 start_loop_for_fetching_hash_rate_per_second_for_last_2016_blocks(&events);
516 start_loop_for_fetching_block_subsidy_of_most_recent_block(&events);
517 start_loop_for_fetching_blocks_mined_over_last_24_hours(&events);
518 start_loop_for_fetching_average_fees_per_block_over_last_24_hours(&events);
519 start_loop_for_fetching_average_fees_per_block_over_last_2016_blocks(&events);
520 start_loop_for_fetching_fees_as_a_percent_of_reward_for_last_2016_blocks(&events);
521 start_loop_for_fetching_fees_as_a_percent_of_reward_for_last_24_hours(&events);
522 start_loop_for_fetching_segwit_stats_for_last_24_hours(&events);
523 start_loop_for_fetching_total_money_supply(&events, 5);
524 start_loop_for_fetching_utxo_set_size(&events, 100);
525
526 loop {
527 let mut app = app.borrow_mut();
528
529 terminal.draw(|rect| ui::draw(rect, &app))?;
531
532 let result = match events.next()? {
534 InputEvent::Input(key) => app.do_action(key),
535 InputEvent::Tick => app.update_on_tick(),
536 InputEvent::NextBlockFound(block_height) => app.update_on_new_block_found(block_height),
537 InputEvent::FetchResource(resource) => app.on_fetch_resource(resource),
538 };
539 if result == AppReturn::Exit {
541 break;
542 }
543 }
544
545 terminal.clear()?;
547 terminal.show_cursor()?;
548 crossterm::terminal::disable_raw_mode()?;
549
550 Ok(())
551}