github.com/mit-dci/lit@v0.0.0-20221102210550-8c3d3b49f2ce/cmd/rs-af/main.rs (about) 1 extern crate chrono; 2 #[macro_use] extern crate clap; 3 extern crate cursive; 4 extern crate reqwest; 5 extern crate serde; 6 #[macro_use] extern crate serde_derive; 7 extern crate serde_json; 8 9 use std::cmp; 10 use std::panic; 11 12 use chrono::prelude::*; 13 14 use cursive::Cursive; 15 use cursive::direction::*; 16 use cursive::event::*; 17 use cursive::theme::*; 18 use cursive::view::*; 19 use cursive::views::*; 20 21 mod litrpc; 22 23 fn main() { 24 25 let matches = clap_app!(lit_af_rs => 26 (version: "0.1.0") 27 (author: "Trey Del Bonis <j.delbonis.3@gmail.com>") 28 (about: "CLI client for Lit") 29 (@arg a: +takes_value "Address to connect to. Default: localhost") 30 (@arg p: +takes_value "Port to connect to lit to. Default: idk yet lmao") 31 ).get_matches(); // TODO Make these optional. 32 33 let addr = matches.value_of("a").unwrap_or("localhost"); 34 let port = match matches.value_of("p").map(str::parse) { 35 Some(Ok(p)) => p, 36 Some(Err(_)) => panic!("port is not a number"), 37 None => 12345 // FIXME 38 }; 39 40 println!("addr: {}, port {}", addr, port); 41 42 match panic::catch_unwind(|| run_ui(addr, port)) { 43 Ok(_) => {}, // we're ok 44 Err(_) => run_bsod() 45 } 46 47 } 48 49 fn run_ui(addr: &str, port: u16) { 50 51 let mut client = Box::new(litrpc::LitRpcClient::new(addr, port)); 52 53 let mut layout = LinearLayout::new(Orientation::Horizontal); 54 55 let mut c_view = LinearLayout::new(Orientation::Vertical); 56 c_view.add_child(TextView::new("Channels")); 57 layout.add_child( 58 BoxView::new( 59 SizeConstraint::Full, 60 SizeConstraint::Full, 61 IdView::new("chans", Panel::new(c_view)))); 62 63 let mut right_view = LinearLayout::new(Orientation::Vertical); 64 65 // Balances 66 let mut bal_view = LinearLayout::new(Orientation::Vertical); 67 bal_view.add_child(TextView::new("Balances")); 68 right_view.add_child( 69 BoxView::new( 70 SizeConstraint::Full, 71 SizeConstraint::Full, 72 IdView::new("bals", Panel::new(bal_view))) 73 .squishable()); 74 75 // Txos 76 let mut txo_view = LinearLayout::new(Orientation::Vertical); 77 txo_view.add_child(TextView::new("Txos")); 78 right_view.add_child( 79 BoxView::new( 80 SizeConstraint::Full, 81 SizeConstraint::Full, 82 IdView::new("txos", Panel::new(txo_view))) 83 .squishable()); 84 85 layout.add_child(BoxView::new(SizeConstraint::Full, SizeConstraint::Full, right_view)); 86 87 let mut siv = Cursive::new(); 88 siv.add_layer(BoxView::new(SizeConstraint::Full, SizeConstraint::Full, layout)); 89 90 siv.set_theme(load_theme(include_str!("ncurses_theme.toml")).unwrap()); 91 siv.add_global_callback(Event::Refresh, make_update_ui_callback_with_client(&mut client)); 92 siv.set_fps(1); 93 94 siv.run() 95 96 } 97 98 fn run_bsod() { 99 100 let mut siv = Cursive::new(); 101 102 let d = Dialog::around(TextView::new("RS-AF has encountered an error and needs to exit.")) 103 .title("Panic") 104 .button("Exit", |s| s.quit()); 105 106 siv.add_layer(d); 107 siv.run(); 108 109 } 110 111 fn generate_view_for_chan(chan: litrpc::ChanInfo) -> impl View { 112 113 let ndt = NaiveDateTime::from_timestamp( 114 (chan.LastUpdate / 1000) as i64, 115 ((chan.LastUpdate % 1000) * 1000) as u32); 116 let dt: DateTime<Utc> = DateTime::from_utc(ndt, chrono::Utc); 117 118 let mut data = LinearLayout::new(Orientation::Vertical); 119 data.add_child(TextView::new(format!("Channel # {}", chan.CIdx))); 120 data.add_child(TextView::new(format!("Outpoint: {}", chan.OutPoint))); 121 data.add_child(TextView::new(format!("Peer: {}", chan.PeerIdx))); 122 data.add_child(TextView::new(format!("Coin Type: {}", chan.CoinType))); 123 data.add_child(TextView::new(format!("Last Activity: {}", dt.to_rfc3339()))); 124 data.add_child(DummyView); 125 126 data.add_child(TextView::new(format!("Balance: {}/{}", chan.MyBalance, chan.Capacity))); 127 let mut bar = ProgressBar::new().range(0, chan.Capacity as usize); 128 bar.set_value(chan.MyBalance as usize); 129 data.add_child(bar); 130 131 let cbox = BoxView::new(SizeConstraint::Full, SizeConstraint::AtLeast(5), data); 132 Panel::new(cbox) 133 134 } 135 136 fn generate_view_for_bal(bal: &litrpc::CoinBalInfo, addrs: Vec<String>) -> impl View { 137 138 let mut data = LinearLayout::new(Orientation::Vertical); 139 140 let grand_total = bal.ChanTotal + bal.TxoTotal; 141 let bal_str = format!( 142 " Funds: chans {} + txos {} = total {} (sat)", 143 bal.ChanTotal, 144 bal.TxoTotal, 145 grand_total); 146 147 data.add_child(TextView::new(format!("- Type {} @ height {}", bal.CoinType, bal.SyncHeight))); 148 data.add_child(TextView::new(bal_str)); 149 addrs.into_iter() 150 .map(|a| format!(" - {}", a)) 151 .map(TextView::new) 152 .for_each(|l| data.add_child(l)); 153 data.add_child(DummyView); 154 155 data 156 157 } 158 159 fn generate_view_for_txo(txo: litrpc::TxoInfo) -> impl View { 160 161 // TODO Make this prettier 162 let strs = vec![ 163 ("Outpoint", txo.OutPoint), 164 ("Amount", format!("{}", txo.Amt)), 165 ("Height", format!("{}", txo.Height)), 166 ("Coin Type", txo.CoinType), 167 ("Key Path", txo.KeyPath) 168 ]; 169 170 let mut data = LinearLayout::new(Orientation::Vertical); 171 for (k, v) in strs { 172 data.add_child(TextView::new(format!("{}: {}", k, v))); 173 } 174 175 let cbox = BoxView::new(SizeConstraint::Full, SizeConstraint::Free, data); 176 Panel::new(cbox) 177 178 } 179 180 fn make_update_ui_callback_with_client(cl: &mut litrpc::LitRpcClient) -> impl Fn(&mut Cursive) { 181 182 use std::mem; 183 184 let clp: *mut litrpc::LitRpcClient = unsafe { mem::transmute(cl) }; 185 186 move |c: &mut Cursive| { 187 188 let clrc: &mut litrpc::LitRpcClient = unsafe { mem::transmute(clp) }; 189 190 // Channels. 191 let chans: Vec<litrpc::ChanInfo> = match clrc.call_chan_list(0) { 192 Ok(clr) => { 193 let mut cls = clr.Channels; 194 // Reversed because we want the newest at the top. 195 cls.sort_by(|a, b| cmp::Ord::cmp(&b.LastUpdate, &a.LastUpdate)); 196 cls 197 }, 198 Err(err) => panic!("{:?}", err) 199 }; 200 201 c.call_on_id("chans", |cpan: &mut Panel<LinearLayout>| { 202 203 let mut c_view = LinearLayout::new(Orientation::Vertical); 204 c_view.add_child(TextView::new("Channels")); 205 chans.into_iter() 206 .map(generate_view_for_chan) 207 .for_each(|e| c_view.add_child(e)); 208 209 *cpan.get_inner_mut() = c_view; 210 211 }); 212 213 // Bals 214 let bals: Vec<litrpc::CoinBalInfo> = match clrc.call_bal() { 215 Ok(br) => { 216 let mut bals = br.Balances; 217 bals.sort_by(|a, b| cmp::Ord::cmp(&a.CoinType, &b.CoinType)); 218 bals 219 }, 220 Err(err) => panic!("{:?}", err) 221 }; 222 223 // Addrs 224 let addrs: Vec<(u32, String)> = match clrc.call_get_addresses() { 225 Ok(ar) => { 226 let mut addrs: Vec<(u32, String)> = ar.CoinTypes.into_iter() 227 .zip(ar.WitAddresses.into_iter()) 228 .collect(); 229 addrs.sort_by(|a, b| match cmp::Ord::cmp(&a.0, &b.0) { 230 cmp::Ordering::Equal => cmp::Ord::cmp(&a.1, &b.1), 231 o => o 232 }); 233 addrs 234 }, 235 Err(err) => panic!("{:?}", err) 236 }; 237 238 c.call_on_id("bals", |balpan: &mut Panel<LinearLayout>| { 239 240 let mut bal_view = LinearLayout::new(Orientation::Vertical); 241 bal_view.add_child(TextView::new("Balances")); 242 bal_view.add_child(DummyView); 243 bals.into_iter() 244 .map(|b| generate_view_for_bal( 245 &b, 246 addrs.clone().into_iter() 247 .filter(|(t, _)| *t == b.CoinType) 248 .map(|(_, a)| a) 249 .collect())) 250 .for_each(|e| bal_view.add_child(e)); 251 252 *balpan.get_inner_mut() = bal_view; 253 254 }); 255 256 // Txos 257 let txos: Vec<litrpc::TxoInfo> = match clrc.call_get_txo_list() { 258 Ok(txr) => txr.Txos, 259 Err(err) => { 260 eprintln!("error: {:?}", err); 261 Vec::new() 262 } 263 }; 264 265 c.call_on_id("txos", |txopan: &mut Panel<LinearLayout>| { 266 267 let mut txo_view = LinearLayout::new(Orientation::Vertical); 268 txo_view.add_child(TextView::new("Txos")); 269 txos.into_iter() 270 .map(generate_view_for_txo) 271 .for_each(|e| txo_view.add_child(e)); 272 273 *txopan.get_inner_mut() = txo_view; 274 275 }); 276 277 } 278 }