notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 382ef772f59b3f50064ec2e8688baa0640c18bfa
parent 53b4a8da5c1eb6af89840f83f79b9ec3cb744efd
Author: William Casarin <jb55@jb55.com>
Date:   Fri,  8 Aug 2025 15:20:45 -0700

clndash: initial peer channel listing

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
MCargo.lock | 4++--
Mcrates/notedeck_clndash/Cargo.toml | 2+-
Mcrates/notedeck_clndash/src/lib.rs | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
3 files changed, 81 insertions(+), 25 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -3072,9 +3072,9 @@ checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "lnsocket" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a88bd51e5bb3753f89b0d3e73baa565064c5a9f5b2aad3ab3f3db5fffb89955" +checksum = "6a373bcde8b65d6db11a0cd0f70dd4a24af854dd7a112b0a51258593c65f48ff" dependencies = [ "bitcoin", "hashbrown 0.13.2", diff --git a/crates/notedeck_clndash/Cargo.toml b/crates/notedeck_clndash/Cargo.toml @@ -8,7 +8,7 @@ egui = { workspace = true } notedeck = { workspace = true } #notedeck_ui = { workspace = true } eframe = { workspace = true } -lnsocket = "0.3.0" +lnsocket = "0.4.0" tracing = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } diff --git a/crates/notedeck_clndash/src/lib.rs b/crates/notedeck_clndash/src/lib.rs @@ -3,14 +3,19 @@ use lnsocket::bitcoin::secp256k1::{PublicKey, SecretKey, rand}; use lnsocket::{CommandoClient, LNSocket}; use notedeck::{AppAction, AppContext}; use serde_json::{Value, json}; +use std::collections::HashMap; use std::str::FromStr; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}; +type JsonCache = HashMap<String, String>; + #[derive(Default)] pub struct ClnDash { initialized: bool, connection_state: ConnectionState, get_info: Option<String>, + peer_channels: Option<Vec<Value>>, + json_cache: JsonCache, channel: Option<Channel>, } @@ -27,7 +32,8 @@ struct Channel { /// Responses from the socket enum ClnResponse { - GetInfo(Result<Value, String>), + GetInfo(Value), + ListPeerChannels(Value), } enum ConnectionState { @@ -36,8 +42,10 @@ enum ConnectionState { Active, } +#[derive(Eq, PartialEq, Clone, Debug)] enum Request { GetInfo, + ListPeerChannels, } enum Event { @@ -92,11 +100,15 @@ impl ClnDash { egui::Frame::new() .inner_margin(egui::Margin::same(50)) .show(ui, |ui| { - connection_state_ui(ui, &self.connection_state); + egui::ScrollArea::vertical().show(ui, |ui| { + connection_state_ui(ui, &self.connection_state); - if let Some(info) = self.get_info.as_ref() { - get_info_ui(ui, info); - } + channels_ui(ui, &mut self.json_cache, &self.peer_channels); + + if let Some(info) = self.get_info.as_ref() { + get_info_ui(ui, info); + } + }); }); } @@ -127,8 +139,10 @@ impl ClnDash { } }; - let rune = "Vns1Zxvidr4J8pP2ZCg3Wjp2SyGyyf5RHgvFG8L36yM9MzMmbWV0aG9kPWdldGluZm8="; // getinfo only atm - let commando = CommandoClient::spawn(lnsocket, rune); + let rune = std::env::var("RUNE").unwrap_or( + "Vns1Zxvidr4J8pP2ZCg3Wjp2SyGyyf5RHgvFG8L36yM9MzMmbWV0aG9kPWdldGluZm8=".to_string(), + ); + let commando = CommandoClient::spawn(lnsocket, &rune); loop { match req_rx.recv().await { @@ -139,18 +153,32 @@ impl ClnDash { break; } - Some(req) => match req { - Request::GetInfo => match commando.call("getinfo", json!({})).await { - Ok(v) => { - let _ = event_tx.send(Event::Response(ClnResponse::GetInfo(Ok(v)))); - } - Err(err) => { - let _ = event_tx.send(Event::Ended { - reason: err.to_string(), - }); + Some(req) => { + tracing::debug!("calling {req:?}"); + match req { + Request::GetInfo => match commando.call("getinfo", json!({})).await { + Ok(v) => { + let _ = event_tx.send(Event::Response(ClnResponse::GetInfo(v))); + } + Err(err) => { + tracing::error!("get_info error {}", err); + } + }, + + Request::ListPeerChannels => { + match commando.call("listpeerchannels", json!({})).await { + Ok(v) => { + let _ = event_tx.send(Event::Response( + ClnResponse::ListPeerChannels(v), + )); + } + Err(err) => { + tracing::error!("listpeerchannels error {}", err); + } + } } - }, - }, + } + } } } }); @@ -170,14 +198,17 @@ impl ClnDash { Event::Connected => { self.connection_state = ConnectionState::Active; let _ = channel.req_tx.send(Request::GetInfo); + let _ = channel.req_tx.send(Request::ListPeerChannels); } Event::Response(resp) => match resp { - ClnResponse::GetInfo(value) => { - let Ok(value) = value else { - return; - }; + ClnResponse::ListPeerChannels(chans) => { + if let Some(vs) = chans["channels"].as_array() { + self.peer_channels = Some(vs.to_owned()); + } + } + ClnResponse::GetInfo(value) => { if let Ok(s) = serde_json::to_string_pretty(&value) { self.get_info = Some(s); } @@ -193,3 +224,28 @@ fn get_info_ui(ui: &mut egui::Ui, info: &str) { ui.add(Label::new(info).wrap_mode(egui::TextWrapMode::Wrap)); }); } + +fn channel_ui(ui: &mut egui::Ui, cache: &mut JsonCache, channel: &Value) { + let short_channel_id = channel["short_channel_id"].as_str().unwrap_or("??"); + + egui::CollapsingHeader::new(format!("channel {short_channel_id}")) + .id_salt(("section", short_channel_id)) + .show(ui, |ui| { + let json: &String = cache + .entry(short_channel_id.to_owned()) + .or_insert_with(|| serde_json::to_string_pretty(channel).unwrap()); + + ui.add(Label::new(json).wrap_mode(egui::TextWrapMode::Wrap)); + }); +} + +fn channels_ui(ui: &mut egui::Ui, json_cache: &mut JsonCache, channels: &Option<Vec<Value>>) { + let Some(channels) = channels else { + ui.label("no channels"); + return; + }; + + for channel in channels { + channel_ui(ui, json_cache, channel); + } +}