From 04f62392b083d20bd7374770db2d9223ed84f151 Mon Sep 17 00:00:00 2001 From: amackillop Date: Wed, 21 Jan 2026 09:10:04 -0800 Subject: [PATCH 1/2] Wait for all channels to be usable before payout Usable channels are a strict superset of ready channels and are ultimately what is required for payouts to succeed. LDK node starts fresh on each request and could reach the point of trying to pay before the channels are marked as usable. Usable means that the channel is ready, the peer is connected, and the channel is not negotiating a shutdown. Wait for all channels to be marked as usable to ensure the entire balance is available. Since by design the client only has one peer (us, the LSP) all channels should be marked usable at the same time. Some logs are there to tell us if this assumption is wrong. --- src/lib.rs | 70 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 630a113..86e1cb3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -830,13 +830,8 @@ impl MdkNode { panic!("failed to sync wallets: {err}"); } - let available_balance_msat: u64 = self - .node - .list_channels() - .into_iter() - .filter(|channel| channel.is_channel_ready) - .map(|channel| channel.outbound_capacity_msat) - .sum(); + wait_for_usable_channels(&self.node, 5000, 100); + let available_balance_msat = usable_outbound_capacity_msat(&self.node); eprintln!("[lightning-js] pay_lnurl available_balance_msat={available_balance_msat}"); if available_balance_msat == 0 { @@ -927,13 +922,9 @@ impl MdkNode { ) })?; - let available_balance_msat: u64 = self - .node - .list_channels() - .into_iter() - .filter(|channel| channel.is_channel_ready) - .map(|channel| channel.outbound_capacity_msat) - .sum(); + wait_for_usable_channels(&self.node, 5000, 100); + let available_balance_msat = usable_outbound_capacity_msat(&self.node); + eprintln!("[lightning-js] pay_bolt11 available_balance_msat={available_balance_msat}"); if available_balance_msat == 0 { if let Err(err) = self.node.stop() { @@ -1063,20 +1054,11 @@ impl MdkNode { } eprintln!("[lightning-js] pay_bolt12_offer wallet sync complete"); - let channels = self.node.list_channels(); - let ready_channels: Vec<_> = channels - .iter() - .filter(|channel| channel.is_channel_ready) - .collect(); - let available_balance_msat: u64 = ready_channels - .iter() - .map(|channel| channel.outbound_capacity_msat) - .sum(); + wait_for_usable_channels(&self.node, 5000, 100); + let available_balance_msat = usable_outbound_capacity_msat(&self.node); eprintln!( - "[lightning-js] pay_bolt12_offer channels: total={} ready={} available_balance_msat={}", - channels.len(), - ready_channels.len(), + "[lightning-js] pay_bolt12_offer available_balance_msat={}", available_balance_msat ); @@ -1186,6 +1168,42 @@ impl MdkNode { } } +/// Wait for all channels to become usable after node startup/sync. +fn wait_for_usable_channels(node: &Node, max_wait_ms: u64, poll_interval_ms: u64) { + let start = Instant::now(); + + loop { + let channels = node.list_channels(); + let total = channels.len(); + let usable = channels.iter().filter(|c| c.is_usable).count(); + + if total > 0 && usable == total { + eprintln!( + "[lightning-js] All channels usable ({usable}/{total}) after {}ms", + start.elapsed().as_millis() + ); + return; + } + + if start.elapsed().as_millis() as u64 >= max_wait_ms { + eprintln!("[lightning-js] Timeout: {usable}/{total} channels usable after {max_wait_ms}ms",); + return; + } + + std::thread::sleep(Duration::from_millis(poll_interval_ms)); + } +} + +/// Compute total outbound capacity across all usable channels. +fn usable_outbound_capacity_msat(node: &Node) -> u64 { + node + .list_channels() + .iter() + .filter(|c| c.is_usable) + .map(|c| c.outbound_capacity_msat) + .sum() +} + fn scid_from_human_readable_string(human_readable_scid: &str) -> Result { let mut parts = human_readable_scid.split('x'); From 771a61ae26e2a676f2f80e71ea6fa94e4628211c Mon Sep 17 00:00:00 2001 From: amackillop Date: Wed, 21 Jan 2026 09:14:15 -0800 Subject: [PATCH 2/2] Centralize polling interval and channel timeout constants Extract hardcoded polling intervals (10ms, 50ms, 100ms) and channel readiness timeout (5000ms) into Duration constants at module level. This improves consistency and makes tuning easier. POLL_INTERVAL: 10ms for all event loop polling CHANNEL_USABLE_TIMEOUT: 10s max wait for channels to become usable --- src/lib.rs | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 86e1cb3..fe6e28f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,6 +46,12 @@ use tokio::runtime::Runtime; #[macro_use] extern crate napi_derive; +/// Polling interval for event loops and state checks. +const POLL_INTERVAL: Duration = Duration::from_millis(10); + +/// Max time to wait for channels to become usable after sync. +const CHANNEL_USABLE_TIMEOUT: Duration = Duration::from_secs(10); + static GLOBAL_LOGGER: OnceLock> = OnceLock::new(); fn logger_instance() -> &'static Arc { @@ -566,7 +572,7 @@ impl MdkNode { last_event_time = now; } - std::thread::sleep(std::time::Duration::from_millis(10)); + std::thread::sleep(POLL_INTERVAL); } if let Err(err) = self.node.stop() { @@ -753,7 +759,7 @@ impl MdkNode { return Ok(()); } - std::thread::sleep(Duration::from_millis(50)); + std::thread::sleep(POLL_INTERVAL); } } @@ -830,7 +836,7 @@ impl MdkNode { panic!("failed to sync wallets: {err}"); } - wait_for_usable_channels(&self.node, 5000, 100); + wait_for_usable_channels(&self.node); let available_balance_msat = usable_outbound_capacity_msat(&self.node); eprintln!("[lightning-js] pay_lnurl available_balance_msat={available_balance_msat}"); @@ -922,7 +928,7 @@ impl MdkNode { ) })?; - wait_for_usable_channels(&self.node, 5000, 100); + wait_for_usable_channels(&self.node); let available_balance_msat = usable_outbound_capacity_msat(&self.node); eprintln!("[lightning-js] pay_bolt11 available_balance_msat={available_balance_msat}"); @@ -1054,7 +1060,7 @@ impl MdkNode { } eprintln!("[lightning-js] pay_bolt12_offer wallet sync complete"); - wait_for_usable_channels(&self.node, 5000, 100); + wait_for_usable_channels(&self.node); let available_balance_msat = usable_outbound_capacity_msat(&self.node); eprintln!( @@ -1169,7 +1175,7 @@ impl MdkNode { } /// Wait for all channels to become usable after node startup/sync. -fn wait_for_usable_channels(node: &Node, max_wait_ms: u64, poll_interval_ms: u64) { +fn wait_for_usable_channels(node: &Node) { let start = Instant::now(); loop { @@ -1185,12 +1191,15 @@ fn wait_for_usable_channels(node: &Node, max_wait_ms: u64, poll_interval_ms: u64 return; } - if start.elapsed().as_millis() as u64 >= max_wait_ms { - eprintln!("[lightning-js] Timeout: {usable}/{total} channels usable after {max_wait_ms}ms",); + if start.elapsed() >= CHANNEL_USABLE_TIMEOUT { + eprintln!( + "[lightning-js] Timeout: {usable}/{total} channels usable after {}s", + CHANNEL_USABLE_TIMEOUT.as_secs() + ); return; } - std::thread::sleep(Duration::from_millis(poll_interval_ms)); + std::thread::sleep(POLL_INTERVAL); } }