From 18cde831ad87cc1594c174587d7c0f4226d87656 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 11 May 2026 11:49:05 -0500 Subject: [PATCH 1/2] Splice and open channels with all on-chain funds Use ldk-node's new splice_in_with_all and open_channel_with_all APIs, which compute the maximum drain amount net of fees and anchor reserves. This replaces the hand-rolled fee subtraction on splice and the hardcoded fee constant on channel open. Drop the amt parameter from the LightningWallet trait's open_channel_with_lsp and splice_to_lsp_channel methods. Every implementation discarded it; the signature now matches the contract. Co-Authored-By: Claude Opus 4.7 (1M context) --- graduated-rebalancer/src/lib.rs | 14 ++++---- orange-sdk/src/lightning_wallet.rs | 51 ++++++------------------------ 2 files changed, 17 insertions(+), 48 deletions(-) diff --git a/graduated-rebalancer/src/lib.rs b/graduated-rebalancer/src/lib.rs index 2afe6e3..3774eac 100644 --- a/graduated-rebalancer/src/lib.rs +++ b/graduated-rebalancer/src/lib.rs @@ -110,9 +110,10 @@ pub trait LightningWallet: Send + Sync { /// Check if we already have a channel with the LSP fn has_channel_with_lsp(&self) -> bool; - /// Open a channel with the LSP using on-chain funds + /// Open a channel with the LSP using all available on-chain funds + /// (minus fees and anchor reserves). fn open_channel_with_lsp( - &self, amt: Amount, + &self, ) -> Pin> + Send + '_>>; /// Wait for a channel pending notification, returns the new channel's outpoint @@ -120,9 +121,10 @@ pub trait LightningWallet: Send + Sync { &self, channel_id: u128, ) -> Pin + Send + '_>>; - /// Splice funds from on-chain to an existing channel with the LSP + /// Splice all available on-chain funds (minus fees and anchor reserves) into + /// an existing channel with the LSP. fn splice_to_lsp_channel( - &self, amt: Amount, + &self, ) -> Pin> + Send + '_>>; /// Wait for a splice pending notification, returns the splice outpoint @@ -344,7 +346,7 @@ where let (channel_outpoint, user_channel_id) = if self.ln_wallet.has_channel_with_lsp() { log_info!(self.logger, "Splicing into channel with LSP with on-chain funds"); - let user_chan_id = match self.ln_wallet.splice_to_lsp_channel(params.amount).await { + let user_chan_id = match self.ln_wallet.splice_to_lsp_channel().await { Ok(chan_id) => chan_id, Err(e) => { log_error!(self.logger, "Failed to open channel with LSP: {e:?}"); @@ -362,7 +364,7 @@ where } else { log_info!(self.logger, "Opening channel with LSP with on-chain funds"); - let user_chan_id = match self.ln_wallet.open_channel_with_lsp(params.amount).await { + let user_chan_id = match self.ln_wallet.open_channel_with_lsp().await { Ok(chan_id) => chan_id, Err(e) => { log_error!(self.logger, "Failed to open channel with LSP: {e:?}"); diff --git a/orange-sdk/src/lightning_wallet.rs b/orange-sdk/src/lightning_wallet.rs index 37961f6..21939c7 100644 --- a/orange-sdk/src/lightning_wallet.rs +++ b/orange-sdk/src/lightning_wallet.rs @@ -400,20 +400,16 @@ impl LightningWallet { } } - pub(crate) async fn splice_balance_into_channel( - &self, amount: Amount, - ) -> Result { + pub(crate) async fn splice_all_into_channel(&self) -> Result { // find existing channel to splice into let channels = self.inner.ldk_node.list_channels(); let channel = channels.iter().find(|c| c.counterparty_node_id == self.inner.lsp_node_id); match channel { Some(chan) => { - self.inner.ldk_node.splice_in( - &chan.user_channel_id, - chan.counterparty_node_id, - amount.sats_rounding_up(), - )?; + self.inner + .ldk_node + .splice_in_with_all(&chan.user_channel_id, chan.counterparty_node_id)?; Ok(chan.user_channel_id) }, None => { @@ -424,23 +420,9 @@ impl LightningWallet { } pub(crate) async fn open_channel_with_lsp(&self) -> Result { - let bal = self.inner.ldk_node.list_balances().spendable_onchain_balance_sats; - - // need a dummy p2wsh address to estimate the fee, p2wsh is used for LN channels - // let fake_addr = Address::p2wsh(Script::new(), self.inner.ldk_node.config().network); - // - // let fee = self - // .inner - // .ldk_node - // .onchain_payment() - // .estimate_send_all_to_address(&fake_addr, true, None)?; - // todo get real fee - let fee = 1000; - - let id = self.inner.ldk_node.open_channel( + let id = self.inner.ldk_node.open_channel_with_all( self.inner.lsp_node_id, self.inner.lsp_socket_addr.clone(), - bal - fee, None, None, )?; @@ -541,12 +523,9 @@ impl graduated_rebalancer::LightningWallet for LightningWallet { } fn open_channel_with_lsp( - &self, _amt: Amount, + &self, ) -> Pin> + Send + '_>> { - Box::pin(async move { - // we don't use the amount and just use our full spendable balance in open_channel_with_lsp - self.open_channel_with_lsp().await.map(|c| c.0) - }) + Box::pin(async move { self.open_channel_with_lsp().await.map(|c| c.0) }) } fn await_channel_pending( @@ -570,21 +549,9 @@ impl graduated_rebalancer::LightningWallet for LightningWallet { } fn splice_to_lsp_channel( - &self, amt: Amount, + &self, ) -> Pin> + Send + '_>> { - let bal = self.inner.ldk_node.list_balances(); - // if we don't have enough onchain balance, return error - // if we are within 1,000 sats of the amount, reduce the amount to account for fees - if bal.spendable_onchain_balance_sats < amt.sats_rounding_up() { - return Box::pin(async move { Err(NodeError::InsufficientFunds) }); - } else if bal.spendable_onchain_balance_sats < amt.sats_rounding_up() + 1_000 { - let reduced_amt = amt.saturating_sub(Amount::from_sats(1_000).expect("valid amount")); - return Box::pin(async move { - self.splice_balance_into_channel(reduced_amt).await.map(|c| c.0) - }); - } - - Box::pin(async move { self.splice_balance_into_channel(amt).await.map(|c| c.0) }) + Box::pin(async move { self.splice_all_into_channel().await.map(|c| c.0) }) } fn await_splice_pending( From df2bb05a0c1e131682579a7c1eca460e102d9ead Mon Sep 17 00:00:00 2001 From: benthecarman Date: Tue, 12 May 2026 16:05:38 -0500 Subject: [PATCH 2/2] Fix dummy wallet test stalls The dummy trusted wallet can miss payment wakeups or leave the rebalancer waiting after a failed LDK payment. Bound the dummy payment wait path and route test-only LDK shutdown through async timeouts so failures surface as test errors instead of CI stalls. --- orange-sdk/src/trusted_wallet/dummy.rs | 17 ++++++++++++----- orange-sdk/tests/integration_tests.rs | 2 +- orange-sdk/tests/test_utils.rs | 26 ++++++++++++++++++++++---- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/orange-sdk/src/trusted_wallet/dummy.rs b/orange-sdk/src/trusted_wallet/dummy.rs index e223e01..a9b5bcd 100644 --- a/orange-sdk/src/trusted_wallet/dummy.rs +++ b/orange-sdk/src/trusted_wallet/dummy.rs @@ -166,6 +166,8 @@ impl DummyTrustedWallet { .await .unwrap(); } + + let _ = payment_success_sender.send(()); }, Event::PaymentReceived { payment_id, amount_msat, payment_hash, .. } => { // convert id @@ -258,10 +260,8 @@ impl DummyTrustedWallet { DummyTrustedWallet { current_bal_msats, payments, ldk_node, payment_success_flag } } - pub(crate) async fn await_payment_success(&self) { - let mut flag = self.payment_success_flag.clone(); - flag.mark_unchanged(); - let _ = flag.changed().await; + fn payment_wait_timeout() -> Duration { + if std::env::var("CI").is_ok() { Duration::from_secs(120) } else { Duration::from_secs(20) } } } @@ -384,6 +384,8 @@ impl TrustedWalletInterface for DummyTrustedWallet { ) -> Pin> + Send + '_>> { Box::pin(async move { let id = channelmanager::PaymentId(payment_hash); + let mut flag = self.payment_success_flag.clone(); + flag.mark_unchanged(); loop { if let Some(payment) = self.ldk_node.payment(&id) { let counterparty_skimmed_fee_msat = match payment.kind { @@ -408,7 +410,12 @@ impl TrustedWalletInterface for DummyTrustedWallet { PaymentStatus::Failed => return None, } } - self.await_payment_success().await; + if !matches!( + tokio::time::timeout(Self::payment_wait_timeout(), flag.changed()).await, + Ok(Ok(())) + ) { + return None; + } } }) } diff --git a/orange-sdk/tests/integration_tests.rs b/orange-sdk/tests/integration_tests.rs index 2ca01e6..eed4ff7 100644 --- a/orange-sdk/tests/integration_tests.rs +++ b/orange-sdk/tests/integration_tests.rs @@ -2208,7 +2208,7 @@ async fn test_lsp_connectivity_fallback() { assert!(!uri_with_lsp.from_trusted); // Now simulate LSP being offline by stopping it - let _ = lsp.stop(); + test_utils::stop_ldk_node("lsp", Arc::clone(&lsp)).await; // Wait a moment for the stop to take effect tokio::time::sleep(Duration::from_secs(2)).await; diff --git a/orange-sdk/tests/test_utils.rs b/orange-sdk/tests/test_utils.rs index c9defd5..c105f08 100644 --- a/orange-sdk/tests/test_utils.rs +++ b/orange-sdk/tests/test_utils.rs @@ -267,7 +267,7 @@ pub struct TestParams { impl TestParams { async fn stop(&self) { - self.wallet.stop().await; + stop_wallet("wallet", Arc::clone(&self.wallet)).await; #[cfg(feature = "_cashu-tests")] let _ = self._mint.stop().await; @@ -279,7 +279,25 @@ impl TestParams { } } -async fn stop_ldk_node(name: &'static str, node: Arc) { +async fn stop_wallet(name: &'static str, wallet: Arc) { + let (sender, receiver) = tokio::sync::oneshot::channel(); + std::thread::spawn(move || { + let res = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .map(|runtime| runtime.block_on(async move { wallet.stop().await })); + if let Err(e) = res { + eprintln!("Warning: failed to create runtime for {name} stop: {e}"); + } + let _ = sender.send(()); + }); + + if tokio::time::timeout(Duration::from_secs(20), receiver).await.is_err() { + eprintln!("Warning: {name} stop timed out"); + } +} + +pub(crate) async fn stop_ldk_node(name: &'static str, node: Arc) { let (sender, receiver) = tokio::sync::oneshot::channel(); std::thread::spawn(move || { let _ = node.stop(); @@ -317,13 +335,13 @@ where res = &mut test_task => Ok(res), _ = tokio::time::sleep(test_timeout) => { test_task.abort(); - let _ = test_task.await; + let _ = tokio::time::timeout(Duration::from_secs(5), &mut test_task).await; Err(()) }, }; // Always clean up - let timeout = Duration::from_secs(30); + let timeout = Duration::from_secs(45); if tokio::time::timeout(timeout, params.stop()).await.is_err() { eprintln!("Warning: params stop timed out after {timeout:?}"); }