bitcoin_terminal_dashboard/
lib.rs

1use 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        // TODO: Handle error
36        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        // TODO: Handle error
43        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        // TODO: Handle error
57        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        // TODO: Handle error
64        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        // TODO: Handle error
78        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        // TODO: Handle error
85        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}
146// TODO: This takes a very long time and blocks other rpc command from being run.
147fn 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        // TODO: Since the call takes forever and blocks other commands, we'll wait a little bit of
152        // time before we call it so the initial values of other metics can load
153        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}
168// TODO: This takes a very long time and blocks other rpc command from being run.
169fn 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        // TODO: Since the call takes forever and blocks other commands, we'll wait a little bit of
174        // time before we call it so the initial values of other metics can load
175        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        // TODO: Show the other segwit stats
460        // We're retrieving a lot of other segwit information.
461        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        // Every three hours
484        sleep(Duration::from_secs(3 * 60 * 60));
485    });
486}
487
488pub fn start_ui(app: Rc<RefCell<App>>) -> Result<()> {
489    // Configure Crossterm backend for tui
490    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    // User event handler
498    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        // Render
530        terminal.draw(|rect| ui::draw(rect, &app))?;
531
532        // Handle inputs
533        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        // Check if we should exit
540        if result == AppReturn::Exit {
541            break;
542        }
543    }
544
545    // Restore the terminal and close application
546    terminal.clear()?;
547    terminal.show_cursor()?;
548    crossterm::terminal::disable_raw_mode()?;
549
550    Ok(())
551}