mirror of
https://github.com/aljazceru/notedeck.git
synced 2025-12-17 08:44:20 +01:00
clndash: invoice loading
Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -3035,6 +3035,7 @@ dependencies = [
|
|||||||
"bech32",
|
"bech32",
|
||||||
"bitcoin",
|
"bitcoin",
|
||||||
"lightning-types",
|
"lightning-types",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3072,9 +3073,9 @@ checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lnsocket"
|
name = "lnsocket"
|
||||||
version = "0.4.0"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a373bcde8b65d6db11a0cd0f70dd4a24af854dd7a112b0a51258593c65f48ff"
|
checksum = "724c7fba2188a49ab31316e52dd410d4d3168b8e6482aa2ac3889dd840d28712"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcoin",
|
"bitcoin",
|
||||||
"hashbrown 0.13.2",
|
"hashbrown 0.13.2",
|
||||||
@@ -3588,6 +3589,8 @@ version = "0.6.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
|
"egui_extras",
|
||||||
|
"lightning-invoice",
|
||||||
"lnsocket",
|
"lnsocket",
|
||||||
"notedeck",
|
"notedeck",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ mime_guess = "2.0.5"
|
|||||||
pretty_assertions = "1.4.1"
|
pretty_assertions = "1.4.1"
|
||||||
jni = "0.21.1"
|
jni = "0.21.1"
|
||||||
profiling = "1.0"
|
profiling = "1.0"
|
||||||
lightning-invoice = "0.33.1"
|
lightning-invoice = { version = "0.33.1", features = ["serde"] }
|
||||||
secp256k1 = "0.30.0"
|
secp256k1 = "0.30.0"
|
||||||
hashbrown = "0.15.2"
|
hashbrown = "0.15.2"
|
||||||
openai-api-rs = "6.0.3"
|
openai-api-rs = "6.0.3"
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ egui = { workspace = true }
|
|||||||
notedeck = { workspace = true }
|
notedeck = { workspace = true }
|
||||||
#notedeck_ui = { workspace = true }
|
#notedeck_ui = { workspace = true }
|
||||||
eframe = { workspace = true }
|
eframe = { workspace = true }
|
||||||
lnsocket = "0.4.0"
|
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
egui_extras = { workspace = true }
|
||||||
|
lightning-invoice = { workspace = true }
|
||||||
|
|
||||||
|
lnsocket = "0.5.1"
|
||||||
|
|||||||
71
crates/notedeck_clndash/src/event.rs
Normal file
71
crates/notedeck_clndash/src/event.rs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
use lightning_invoice::Bolt11Invoice;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Clone)]
|
||||||
|
pub struct WaitRequest {
|
||||||
|
pub indexname: String,
|
||||||
|
pub subsystem: String,
|
||||||
|
pub nextvalue: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Request {
|
||||||
|
GetInfo,
|
||||||
|
ListPeerChannels,
|
||||||
|
PaidInvoices(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct ListPeerChannel {
|
||||||
|
pub short_channel_id: String,
|
||||||
|
pub our_reserve_msat: i64,
|
||||||
|
pub to_us_msat: i64,
|
||||||
|
pub total_msat: i64,
|
||||||
|
pub their_reserve_msat: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Channel {
|
||||||
|
pub to_us: i64,
|
||||||
|
pub to_them: i64,
|
||||||
|
pub original: ListPeerChannel,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Channels {
|
||||||
|
pub max_total_msat: i64,
|
||||||
|
pub avail_in: i64,
|
||||||
|
pub avail_out: i64,
|
||||||
|
pub channels: Vec<Channel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Invoice {
|
||||||
|
pub lastpay_index: Option<u64>,
|
||||||
|
pub label: String,
|
||||||
|
pub bolt11: Bolt11Invoice,
|
||||||
|
pub payment_hash: String,
|
||||||
|
pub amount_msat: u64,
|
||||||
|
pub status: String,
|
||||||
|
pub description: String,
|
||||||
|
pub expires_at: u64,
|
||||||
|
pub created_index: u64,
|
||||||
|
pub updated_index: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Responses from the socket
|
||||||
|
pub enum ClnResponse {
|
||||||
|
GetInfo(Value),
|
||||||
|
ListPeerChannels(Result<Channels, lnsocket::Error>),
|
||||||
|
PaidInvoices(Result<Vec<Invoice>, lnsocket::Error>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
/// We lost the socket somehow
|
||||||
|
Ended {
|
||||||
|
reason: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
Connected,
|
||||||
|
|
||||||
|
Response(ClnResponse),
|
||||||
|
}
|
||||||
@@ -1,32 +1,72 @@
|
|||||||
|
use crate::event::Channel;
|
||||||
|
use crate::event::Channels;
|
||||||
|
use crate::event::ClnResponse;
|
||||||
|
use crate::event::Event;
|
||||||
|
use crate::event::Invoice;
|
||||||
|
use crate::event::ListPeerChannel;
|
||||||
|
use crate::event::Request;
|
||||||
|
use crate::watch::fetch_paid_invoices;
|
||||||
|
|
||||||
use egui::{Color32, Label, RichText};
|
use egui::{Color32, Label, RichText};
|
||||||
use lnsocket::bitcoin::secp256k1::{PublicKey, SecretKey, rand};
|
use lnsocket::bitcoin::secp256k1::{PublicKey, SecretKey, rand};
|
||||||
use lnsocket::{CommandoClient, LNSocket};
|
use lnsocket::{CommandoClient, LNSocket};
|
||||||
use notedeck::{AppAction, AppContext};
|
use notedeck::{AppAction, AppContext};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde_json::json;
|
||||||
use serde_json::{Value, json};
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
|
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
|
||||||
|
|
||||||
struct Channel {
|
mod event;
|
||||||
to_us: i64,
|
mod watch;
|
||||||
to_them: i64,
|
|
||||||
original: ListPeerChannel,
|
pub enum LoadingState<T, E> {
|
||||||
|
Loading,
|
||||||
|
Failed(E),
|
||||||
|
Loaded(T),
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Channels {
|
impl<T, E> Default for LoadingState<T, E> {
|
||||||
max_total_msat: i64,
|
fn default() -> Self {
|
||||||
avail_in: i64,
|
Self::Loading
|
||||||
avail_out: i64,
|
}
|
||||||
channels: Vec<Channel>,
|
}
|
||||||
|
|
||||||
|
impl<T, E> LoadingState<T, E> {
|
||||||
|
fn _as_ref(&self) -> LoadingState<&T, &E> {
|
||||||
|
match self {
|
||||||
|
Self::Loading => LoadingState::<&T, &E>::Loading,
|
||||||
|
Self::Failed(err) => LoadingState::<&T, &E>::Failed(err),
|
||||||
|
Self::Loaded(t) => LoadingState::<&T, &E>::Loaded(t),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_result(res: Result<T, E>) -> LoadingState<T, E> {
|
||||||
|
match res {
|
||||||
|
Ok(r) => LoadingState::Loaded(r),
|
||||||
|
Err(err) => LoadingState::Failed(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
fn unwrap(self) -> T {
|
||||||
|
let Self::Loaded(t) = self else {
|
||||||
|
panic!("unwrap in LoadingState");
|
||||||
|
};
|
||||||
|
|
||||||
|
t
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ClnDash {
|
pub struct ClnDash {
|
||||||
initialized: bool,
|
initialized: bool,
|
||||||
connection_state: ConnectionState,
|
connection_state: ConnectionState,
|
||||||
get_info: Option<String>,
|
summary: LoadingState<Summary, lnsocket::Error>,
|
||||||
channels: Option<Result<Channels, lnsocket::Error>>,
|
get_info: LoadingState<String, lnsocket::Error>,
|
||||||
|
channels: LoadingState<Channels, lnsocket::Error>,
|
||||||
channel: Option<CommChannel>,
|
channel: Option<CommChannel>,
|
||||||
|
invoices: LoadingState<Vec<Invoice>, lnsocket::Error>,
|
||||||
last_summary: Option<Summary>,
|
last_summary: Option<Summary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,44 +81,12 @@ struct CommChannel {
|
|||||||
event_rx: UnboundedReceiver<Event>,
|
event_rx: UnboundedReceiver<Event>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Responses from the socket
|
|
||||||
enum ClnResponse {
|
|
||||||
GetInfo(Value),
|
|
||||||
ListPeerChannels(Result<Channels, lnsocket::Error>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
|
||||||
struct ListPeerChannel {
|
|
||||||
short_channel_id: String,
|
|
||||||
our_reserve_msat: i64,
|
|
||||||
to_us_msat: i64,
|
|
||||||
total_msat: i64,
|
|
||||||
their_reserve_msat: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ConnectionState {
|
enum ConnectionState {
|
||||||
Dead(String),
|
Dead(String),
|
||||||
Connecting,
|
Connecting,
|
||||||
Active,
|
Active,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Clone, Debug)]
|
|
||||||
enum Request {
|
|
||||||
GetInfo,
|
|
||||||
ListPeerChannels,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Event {
|
|
||||||
/// We lost the socket somehow
|
|
||||||
Ended {
|
|
||||||
reason: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
Connected,
|
|
||||||
|
|
||||||
Response(ClnResponse),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl notedeck::App for ClnDash {
|
impl notedeck::App for ClnDash {
|
||||||
fn update(&mut self, _ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
|
fn update(&mut self, _ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
|
||||||
if !self.initialized {
|
if !self.initialized {
|
||||||
@@ -116,6 +124,25 @@ fn connection_state_ui(ui: &mut egui::Ui, state: &ConnectionState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn summary_ui(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
last_summary: Option<&Summary>,
|
||||||
|
summary: &LoadingState<Summary, lnsocket::Error>,
|
||||||
|
) {
|
||||||
|
match summary {
|
||||||
|
LoadingState::Loading => {
|
||||||
|
ui.label("loading summary");
|
||||||
|
}
|
||||||
|
LoadingState::Failed(err) => {
|
||||||
|
ui.label(format!("Failed to get summary: {err}"));
|
||||||
|
}
|
||||||
|
LoadingState::Loaded(summary) => {
|
||||||
|
summary_cards_ui(ui, summary, last_summary);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ClnDash {
|
impl ClnDash {
|
||||||
fn show(&mut self, ui: &mut egui::Ui) {
|
fn show(&mut self, ui: &mut egui::Ui) {
|
||||||
egui::Frame::new()
|
egui::Frame::new()
|
||||||
@@ -123,16 +150,10 @@ impl ClnDash {
|
|||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
connection_state_ui(ui, &self.connection_state);
|
connection_state_ui(ui, &self.connection_state);
|
||||||
if let Some(Ok(ch)) = self.channels.as_ref() {
|
summary_ui(ui, self.last_summary.as_ref(), &self.summary);
|
||||||
let summary = compute_summary(ch);
|
invoices_ui(ui, &self.invoices);
|
||||||
summary_cards_ui(ui, &summary, self.last_summary.as_ref());
|
|
||||||
ui.add_space(8.0);
|
|
||||||
}
|
|
||||||
channels_ui(ui, &self.channels);
|
channels_ui(ui, &self.channels);
|
||||||
|
get_info_ui(ui, &self.get_info);
|
||||||
if let Some(info) = self.get_info.as_ref() {
|
|
||||||
get_info_ui(ui, info);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -167,7 +188,7 @@ impl ClnDash {
|
|||||||
let rune = std::env::var("RUNE").unwrap_or(
|
let rune = std::env::var("RUNE").unwrap_or(
|
||||||
"Vns1Zxvidr4J8pP2ZCg3Wjp2SyGyyf5RHgvFG8L36yM9MzMmbWV0aG9kPWdldGluZm8=".to_string(),
|
"Vns1Zxvidr4J8pP2ZCg3Wjp2SyGyyf5RHgvFG8L36yM9MzMmbWV0aG9kPWdldGluZm8=".to_string(),
|
||||||
);
|
);
|
||||||
let commando = CommandoClient::spawn(lnsocket, &rune);
|
let commando = Arc::new(CommandoClient::spawn(lnsocket, &rune));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match req_rx.recv().await {
|
match req_rx.recv().await {
|
||||||
@@ -181,25 +202,47 @@ impl ClnDash {
|
|||||||
Some(req) => {
|
Some(req) => {
|
||||||
tracing::debug!("calling {req:?}");
|
tracing::debug!("calling {req:?}");
|
||||||
match req {
|
match req {
|
||||||
Request::GetInfo => match commando.call("getinfo", json!({})).await {
|
Request::GetInfo => {
|
||||||
Ok(v) => {
|
let event_tx = event_tx.clone();
|
||||||
let _ = event_tx.send(Event::Response(ClnResponse::GetInfo(v)));
|
let commando = commando.clone();
|
||||||
}
|
tokio::spawn(async move {
|
||||||
Err(err) => {
|
match commando.call("getinfo", json!({})).await {
|
||||||
tracing::error!("get_info error {}", err);
|
Ok(v) => {
|
||||||
}
|
let _ = event_tx
|
||||||
},
|
.send(Event::Response(ClnResponse::GetInfo(v)));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("get_info error {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Request::PaidInvoices(n) => {
|
||||||
|
let event_tx = event_tx.clone();
|
||||||
|
let commando = commando.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let invoices = fetch_paid_invoices(commando, n).await;
|
||||||
|
let _ = event_tx
|
||||||
|
.send(Event::Response(ClnResponse::PaidInvoices(invoices)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Request::ListPeerChannels => {
|
Request::ListPeerChannels => {
|
||||||
let peer_channels =
|
let event_tx = event_tx.clone();
|
||||||
commando.call("listpeerchannels", json!({})).await;
|
let commando = commando.clone();
|
||||||
let channels = peer_channels.map(|v| {
|
tokio::spawn(async move {
|
||||||
let peer_channels: Vec<ListPeerChannel> =
|
let peer_channels =
|
||||||
serde_json::from_value(v["channels"].clone()).unwrap();
|
commando.call("listpeerchannels", json!({})).await;
|
||||||
to_channels(peer_channels)
|
let channels = peer_channels.map(|v| {
|
||||||
|
let peer_channels: Vec<ListPeerChannel> =
|
||||||
|
serde_json::from_value(v["channels"].clone()).unwrap();
|
||||||
|
to_channels(peer_channels)
|
||||||
|
});
|
||||||
|
let _ = event_tx.send(Event::Response(
|
||||||
|
ClnResponse::ListPeerChannels(channels),
|
||||||
|
));
|
||||||
});
|
});
|
||||||
let _ = event_tx
|
|
||||||
.send(Event::Response(ClnResponse::ListPeerChannels(channels)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,20 +266,30 @@ impl ClnDash {
|
|||||||
self.connection_state = ConnectionState::Active;
|
self.connection_state = ConnectionState::Active;
|
||||||
let _ = channel.req_tx.send(Request::GetInfo);
|
let _ = channel.req_tx.send(Request::GetInfo);
|
||||||
let _ = channel.req_tx.send(Request::ListPeerChannels);
|
let _ = channel.req_tx.send(Request::ListPeerChannels);
|
||||||
|
let _ = channel.req_tx.send(Request::PaidInvoices(30));
|
||||||
}
|
}
|
||||||
|
|
||||||
Event::Response(resp) => match resp {
|
Event::Response(resp) => match resp {
|
||||||
ClnResponse::ListPeerChannels(chans) => {
|
ClnResponse::ListPeerChannels(chans) => {
|
||||||
if let Some(Ok(prev)) = self.channels.as_ref() {
|
if let LoadingState::Loaded(prev) = &self.channels {
|
||||||
self.last_summary = Some(compute_summary(prev));
|
self.last_summary = Some(compute_summary(prev));
|
||||||
}
|
}
|
||||||
self.channels = Some(chans);
|
|
||||||
|
self.summary = match &chans {
|
||||||
|
Ok(chans) => LoadingState::Loaded(compute_summary(chans)),
|
||||||
|
Err(err) => LoadingState::Failed(err.clone()),
|
||||||
|
};
|
||||||
|
self.channels = LoadingState::from_result(chans);
|
||||||
}
|
}
|
||||||
|
|
||||||
ClnResponse::GetInfo(value) => {
|
ClnResponse::GetInfo(value) => {
|
||||||
if let Ok(s) = serde_json::to_string_pretty(&value) {
|
let res = serde_json::to_string_pretty(&value);
|
||||||
self.get_info = Some(s);
|
self.get_info =
|
||||||
}
|
LoadingState::from_result(res.map_err(|_| lnsocket::Error::Json));
|
||||||
|
}
|
||||||
|
|
||||||
|
ClnResponse::PaidInvoices(invoices) => {
|
||||||
|
self.invoices = LoadingState::from_result(invoices);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -244,9 +297,15 @@ impl ClnDash {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_info_ui(ui: &mut egui::Ui, info: &str) {
|
fn get_info_ui(ui: &mut egui::Ui, info: &LoadingState<String, lnsocket::Error>) {
|
||||||
ui.horizontal_wrapped(|ui| {
|
ui.horizontal_wrapped(|ui| match info {
|
||||||
ui.add(Label::new(info).wrap_mode(egui::TextWrapMode::Wrap));
|
LoadingState::Loading => {}
|
||||||
|
LoadingState::Failed(err) => {
|
||||||
|
ui.label(format!("failed to fetch node info: {err}"));
|
||||||
|
}
|
||||||
|
LoadingState::Loaded(info) => {
|
||||||
|
ui.add(Label::new(info).wrap_mode(egui::TextWrapMode::Wrap));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,9 +392,25 @@ fn human_sat(msat: i64) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn channels_ui(ui: &mut egui::Ui, channels: &Option<Result<Channels, lnsocket::Error>>) {
|
fn human_verbose_sat(msat: i64) -> String {
|
||||||
|
if msat < 1_000 {
|
||||||
|
// less than 1 sat
|
||||||
|
format!("{msat} msat")
|
||||||
|
} else {
|
||||||
|
let sats = msat / 1_000;
|
||||||
|
if sats < 100_000_000 {
|
||||||
|
// less than 1 BTC
|
||||||
|
format!("{sats} sat")
|
||||||
|
} else {
|
||||||
|
let btc = sats / 100_000_000;
|
||||||
|
format!("{btc} BTC")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn channels_ui(ui: &mut egui::Ui, channels: &LoadingState<Channels, lnsocket::Error>) {
|
||||||
match channels {
|
match channels {
|
||||||
Some(Ok(channels)) => {
|
LoadingState::Loaded(channels) => {
|
||||||
if channels.channels.is_empty() {
|
if channels.channels.is_empty() {
|
||||||
ui.label("no channels yet...");
|
ui.label("no channels yet...");
|
||||||
return;
|
return;
|
||||||
@@ -348,11 +423,11 @@ fn channels_ui(ui: &mut egui::Ui, channels: &Option<Result<Channels, lnsocket::E
|
|||||||
ui.label(format!("available out {}", human_sat(channels.avail_out)));
|
ui.label(format!("available out {}", human_sat(channels.avail_out)));
|
||||||
ui.label(format!("available in {}", human_sat(channels.avail_in)));
|
ui.label(format!("available in {}", human_sat(channels.avail_in)));
|
||||||
}
|
}
|
||||||
Some(Err(err)) => {
|
LoadingState::Failed(err) => {
|
||||||
ui.label(format!("error fetching channels: {err}"));
|
ui.label(format!("error fetching channels: {err}"));
|
||||||
}
|
}
|
||||||
None => {
|
LoadingState::Loading => {
|
||||||
ui.label("no channels yet...");
|
ui.label("fetching channels...");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -524,3 +599,48 @@ fn delta_str(new: i64, old: i64) -> String {
|
|||||||
std::cmp::Ordering::Equal => "·".into(),
|
std::cmp::Ordering::Equal => "·".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn invoices_ui(ui: &mut egui::Ui, invoices: &LoadingState<Vec<Invoice>, lnsocket::Error>) {
|
||||||
|
match invoices {
|
||||||
|
LoadingState::Loading => {
|
||||||
|
ui.label("loading invoices...");
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingState::Failed(err) => {
|
||||||
|
ui.label(format!("failed to load invoices: {err}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingState::Loaded(invoices) => {
|
||||||
|
use egui_extras::{Column, TableBuilder};
|
||||||
|
|
||||||
|
TableBuilder::new(ui)
|
||||||
|
.column(Column::auto().resizable(true))
|
||||||
|
.column(Column::remainder())
|
||||||
|
.header(20.0, |mut header| {
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.heading("Description");
|
||||||
|
});
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.heading("Amount");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.body(|mut body| {
|
||||||
|
for invoice in invoices {
|
||||||
|
body.row(30.0, |mut row| {
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.label(invoice.description.clone());
|
||||||
|
});
|
||||||
|
row.col(|ui| match invoice.bolt11.amount_milli_satoshis() {
|
||||||
|
None => {
|
||||||
|
ui.label("any");
|
||||||
|
}
|
||||||
|
Some(amt) => {
|
||||||
|
ui.label(human_verbose_sat(amt as i64));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
198
crates/notedeck_clndash/src/watch.rs
Normal file
198
crates/notedeck_clndash/src/watch.rs
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
use crate::event::Invoice;
|
||||||
|
use lnsocket::CallOpts;
|
||||||
|
use lnsocket::CommandoClient;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::json;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct UpdatedInvoicesResponse {
|
||||||
|
updated: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct PayIndexInvoices {
|
||||||
|
invoices: Vec<PayIndexScan>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct PayIndexScan {
|
||||||
|
pay_index: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_lastpay_index(commando: Arc<CommandoClient>) -> Result<Option<u64>, lnsocket::Error> {
|
||||||
|
const PAGE: u64 = 250;
|
||||||
|
// 1) get the current updated tail
|
||||||
|
let created_value = commando
|
||||||
|
.call(
|
||||||
|
"wait",
|
||||||
|
json!({"subsystem":"invoices","indexname":"updated","nextvalue":0}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let response: UpdatedInvoicesResponse =
|
||||||
|
serde_json::from_value(created_value).map_err(|_| lnsocket::Error::Json)?;
|
||||||
|
|
||||||
|
// start our window at the tail
|
||||||
|
let mut start_at = response
|
||||||
|
.updated
|
||||||
|
.saturating_add(1) // +1 because we want max(1, updated - PAGE + 1)
|
||||||
|
.saturating_sub(PAGE)
|
||||||
|
.max(1);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// 2) fetch a window (indexed by "updated")
|
||||||
|
let val = commando
|
||||||
|
.call_with_opts(
|
||||||
|
"listinvoices",
|
||||||
|
json!({
|
||||||
|
"index": "updated",
|
||||||
|
"start": start_at,
|
||||||
|
"limit": PAGE,
|
||||||
|
}),
|
||||||
|
// only fetch the one field we care about
|
||||||
|
CallOpts::default().filter(json!({
|
||||||
|
"invoices": [{"pay_index": true}]
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let parsed: PayIndexInvoices =
|
||||||
|
serde_json::from_value(val).map_err(|_| lnsocket::Error::Json)?;
|
||||||
|
|
||||||
|
if let Some(pi) = parsed.invoices.iter().filter_map(|inv| inv.pay_index).max() {
|
||||||
|
return Ok(Some(pi));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) no paid invoice in this slice—step back or bail
|
||||||
|
if start_at == 1 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
start_at = start_at.saturating_sub(PAGE).max(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_paid_invoices(
|
||||||
|
commando: Arc<CommandoClient>,
|
||||||
|
limit: u32,
|
||||||
|
) -> Result<Vec<Invoice>, lnsocket::Error> {
|
||||||
|
use tokio::task::JoinSet;
|
||||||
|
|
||||||
|
// look for an invoice with the last paid index
|
||||||
|
let Some(lastpay_index) = find_lastpay_index(commando.clone()).await? else {
|
||||||
|
// no paid invoices
|
||||||
|
return Ok(vec![]);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut set: JoinSet<Result<Invoice, lnsocket::Error>> = JoinSet::new();
|
||||||
|
let start = lastpay_index.saturating_sub(limit as u64);
|
||||||
|
|
||||||
|
// 3) Fire off at most `concurrency` `waitanyinvoice` calls at a time,
|
||||||
|
// collect all successful responses into a Vec.
|
||||||
|
// fire them ALL at once
|
||||||
|
for idx in start..lastpay_index {
|
||||||
|
let c = commando.clone();
|
||||||
|
set.spawn(async move {
|
||||||
|
let val = c
|
||||||
|
.call(
|
||||||
|
"waitanyinvoice",
|
||||||
|
serde_json::json!({ "lastpay_index": idx }),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let parsed: Invoice = serde_json::from_value(val).map_err(|_| lnsocket::Error::Json)?;
|
||||||
|
Ok(parsed)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut results = Vec::with_capacity(limit as usize);
|
||||||
|
while let Some(res) = set.join_next().await {
|
||||||
|
results.push(res.map_err(|_| lnsocket::Error::Io(std::io::ErrorKind::Interrupted))??);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.sort_by(|a, b| a.updated_index.cmp(&b.updated_index));
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wip watch subsystem
|
||||||
|
/*
|
||||||
|
async fn watch_subsystem(
|
||||||
|
commando: CommandoClient,
|
||||||
|
subsystem: WaitSubsystem,
|
||||||
|
index: WaitIndex,
|
||||||
|
event_tx: UnboundedSender<Event>,
|
||||||
|
mut cancel_rx: Receiver<()>,
|
||||||
|
) {
|
||||||
|
// Step 1: Fetch current index value so we can back up ~20
|
||||||
|
let mut nextvalue: u64 = match commando
|
||||||
|
.call(
|
||||||
|
"wait",
|
||||||
|
serde_json::json!({
|
||||||
|
"indexname": index.as_str(),
|
||||||
|
"subsystem": subsystem.as_str(),
|
||||||
|
"nextvalue": 0
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(v) => {
|
||||||
|
// You showed the result has `updated` as the current highest index
|
||||||
|
let current = v.get("updated").and_then(|x| x.as_u64()).unwrap_or(0);
|
||||||
|
current.saturating_sub(20) // back up 20, clamp at 0
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!("initial wait(…nextvalue=0) failed: {}", err);
|
||||||
|
0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// You can add a timeout to avoid hanging forever in weird network states.
|
||||||
|
let fut = commando.call(
|
||||||
|
"wait",
|
||||||
|
serde_json::to_value(WaitRequest {
|
||||||
|
indexname: "invoices".into(),
|
||||||
|
subsystem: "lightningd".into(),
|
||||||
|
nextvalue,
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = &mut cancel_rx => {
|
||||||
|
// graceful shutdown
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
res = fut => {
|
||||||
|
match res {
|
||||||
|
Ok(v) => {
|
||||||
|
// Typical shape: { "nextvalue": n, "invoicestatus": { ... } } (varies by plugin/index)
|
||||||
|
// Adjust these lookups for your node’s actual wait payload.
|
||||||
|
if let Some(nv) = v.get("nextvalue").and_then(|x| x.as_u64()) {
|
||||||
|
nextvalue = nv + 1;
|
||||||
|
} else {
|
||||||
|
// Defensive: never get stuck — bump at least by 1
|
||||||
|
nextvalue += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inspect/route
|
||||||
|
let kind = v.get("status").and_then(|s| s.as_str());
|
||||||
|
let ev = match kind {
|
||||||
|
Some("paid") => ClnResponse::Invoice(InvoiceEvent::Paid(v.clone())),
|
||||||
|
Some("created") => ClnResponse::Invoice(InvoiceEvent::Created(v.clone())),
|
||||||
|
_ => ClnResponse::Invoice(InvoiceEvent::Other(v.clone())),
|
||||||
|
};
|
||||||
|
let _ = event_tx.send(Event::Response(ev));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!("wait(invoices) error: {err}");
|
||||||
|
// small backoff so we don't tight-loop on persistent errors
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
Reference in New Issue
Block a user