Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Terminal Bitcoin Monitor

**!Work in progres!**
![btcmon](share/screenshots/demo.gif?raw=true)

Command line monitor for the Bitcoin Network and your Bitcoin and Lightning node.

Expand Down Expand Up @@ -52,6 +52,8 @@ macaroon_hex = "replaceme"
enabled = true
currency = "USD"
big_text = true
variation = "minute"
variation_threshold = 0.0

[fees]
enabled = true
Expand Down
2 changes: 2 additions & 0 deletions share/config/example-multiple.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ tick_rate = 250
enabled = true
currency = "USD"
big_text = true
variation = "minute"
variation_threshold = 0.0

[fees]
enabled = true
Expand Down
10 changes: 2 additions & 8 deletions share/config/example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,12 @@ rpc_user = "polaruser"
rpc_password = "polarpass"
zmq_port = 28334

[core_lightning]
rest_address = "http://127.0.0.1:3010"
rest_rune = "replaceme"

[lnd]
rest_address = "https://127.0.0.1:8080"
macaroon_hex = "replaceme"

[price]
enabled = true
currency = "USD"
big_text = true
variation = "minute"
variation_threshold = 0.0

[fees]
enabled = true
12 changes: 12 additions & 0 deletions share/config/price-only.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
tick_rate = 250
streamer_mode = false

[price]
enabled = true
currency = "USD"
big_text = true
variation = "minute"
variation_threshold = 0.0

[fees]
enabled = false
Binary file added share/screenshots/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 16 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
use std::collections::VecDeque;
use std::error;
use std::str::FromStr;
use tokio::sync::mpsc;
Expand Down Expand Up @@ -40,6 +41,7 @@ pub struct AppState {
pub price: PriceState,
pub fees: FeesState,
pub node_states: Vec<NodeState>,
pub price_history: VecDeque<(Instant, f64)>,
}

pub struct App {
Expand Down Expand Up @@ -81,6 +83,7 @@ impl App {
counter: 0,
price: PriceState::new(),
fees: FeesState::new(),
price_history: VecDeque::new(),
node_states: widget_states
.into_iter()
.map(|ws| {
Expand Down Expand Up @@ -150,6 +153,18 @@ impl App {

pub fn handle_price_update(&mut self, state: PriceState) {
self.state.price = state;
let now = Instant::now();
if let Some(price) = self.state.price.last_price_in_currency {
self.state.price_history.push_back((now, price));
let max_age = Duration::from_secs(60 * 60 * 24);
while let Some((timestamp, _)) = self.state.price_history.front() {
if now.duration_since(*timestamp) > max_age {
self.state.price_history.pop_front();
} else {
break;
}
}
}
}

pub fn handle_node_update(
Expand Down Expand Up @@ -234,4 +249,4 @@ impl App {
}
Ok(())
}
}
}
39 changes: 34 additions & 5 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub struct PriceSettings {
pub enabled: bool,
pub currency: String,
pub big_text: bool,
pub variation: String,
pub variation_threshold: f64,
}

#[derive(Debug, Deserialize, Clone)]
Expand Down Expand Up @@ -99,6 +101,8 @@ impl AppConfig {
.set_default("price.enabled", true)?
.set_default("price.big_text", true)?
.set_default("price.currency", "USD")?
.set_default("price.variation", "minute")?
.set_default("price.variation_threshold", 0.0)?
// fees
.set_default("fees.enabled", true)?;

Expand All @@ -108,21 +112,23 @@ impl AppConfig {
(true, false) => argv
.get("c")
.and_then(|v| Some(v.first().unwrap().as_str()))
.unwrap(),
.unwrap()
.to_string(),
(false, true) | (true, true) => argv
.get("config")
.and_then(|v| Some(v.first().unwrap().as_str()))
.unwrap(),
.unwrap()
.to_string(),
_ => match home_path {
Some(home_path) => {
default_config_file = vec![home_path, "/.btcmon/btcmon.toml"].join("");
default_config_file.as_str()
default_config_file
}
_ => default_config_file.as_str(),
_ => default_config_file,
},
};

s = s.add_source(File::with_name(config_file).required(false));
s = s.add_source(File::with_name(&config_file).required(false));

let args = argv.clone();
for key in argv.into_keys() {
Expand All @@ -143,6 +149,29 @@ impl AppConfig {

let mut config: AppConfig = s.build()?.try_deserialize()?;

let has_node_args = args.keys().any(|key| {
key.starts_with("bitcoin_core.")
|| key.starts_with("core_lightning.")
|| key.starts_with("lnd.")
|| key.starts_with("nodes")
|| key.starts_with("node.")
});
let file_has_node_settings = std::fs::read_to_string(&config_file)
.map(|contents| {
contents.contains("[bitcoin_core]")
|| contents.contains("[core_lightning]")
|| contents.contains("[lnd]")
|| contents.contains("[nodes]")
|| contents.contains("[node]")
})
.unwrap_or(false);

if config.nodes.is_empty() && !has_node_args && !file_has_node_settings {
config.bitcoin_core = BitcoinCoreSettings::default();
config.core_lightning = CoreLightningSettings::default();
config.lnd = LndSettings::default();
}

// Clear legacy providers if nodes array is used
if !config.nodes.is_empty() {
config.bitcoin_core = BitcoinCoreSettings::default();
Expand Down
23 changes: 16 additions & 7 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ async fn main() -> AppResult<()> {
let mut widgets: Vec<Box<dyn DynamicNodeStatefulWidget>> = vec![];
let mut widget_states: Vec<Box<dyn DynamicState>> = vec![];

let mut price_only = false;

// Use nodes from config.nodes if present, otherwise use single node configuration
if !config.nodes.is_empty() {
for node in &config.nodes {
Expand Down Expand Up @@ -68,7 +70,6 @@ async fn main() -> AppResult<()> {
}
}
} else {
// Use single node configuration, prioritizing lnd
if !config.lnd.rest_address.is_empty() {
providers.push(Box::new(LndNode::new(&config.lnd)));
widgets.push(Box::new(LndWidget));
Expand All @@ -82,14 +83,22 @@ async fn main() -> AppResult<()> {
widgets.push(Box::new(BitcoinCoreWidget));
widget_states.push(Box::new(BitcoinCoreWidgetState::default()));
} else {
eprintln!("No nodes or single node configuration found.");
std::process::exit(1);
price_only = true;
}
}

if providers.is_empty() {
eprintln!("No valid nodes configured.");
std::process::exit(1);
let no_node_config = config.nodes.is_empty()
&& config.lnd.rest_address.is_empty()
&& config.core_lightning.rest_address.is_empty()
&& config.bitcoin_core.host.is_empty();

if no_node_config {
price_only = true;
} else {
eprintln!("No valid nodes configured.");
std::process::exit(1);
}
}

let mut app = App::new(thread, widgets, widget_states, config.clone());
Expand All @@ -111,11 +120,11 @@ async fn main() -> AppResult<()> {
app.nodes[i].init(provider, i);
}

if config.price.enabled {
if config.price.enabled || price_only {
app.init_price();
}

if config.fees.enabled {
if config.fees.enabled && !price_only {
app.init_fees();
}

Expand Down
Loading
Loading