From d59456d5ba0ec1f25c4d7b531c1e2a588ce263e3 Mon Sep 17 00:00:00 2001 From: ajaysehwal Date: Tue, 13 Oct 2026 15:02:51 +0530 Subject: [PATCH] Return `NotRunning` from the remaining live-node APIs Several public APIs that need a live node didn't check the running state, e.g., `OnchainPayment::bump_fee_rbf` could reach wallet mutation and signing after `Node::stop`, while the on-chain send methods returned `Error::NotRunning`. Add the missing checks before any side effects, using the same pattern as the existing ones: RBF fee bumps, splicing, channel closure and config updates, BOLT11 claim/fail and JIT receive, BOLT12 refunds, LSPS1 requests and `add_liquidity_source`. The unified payment flow no longer falls back to the next payment method if the node isn't running. Methods that intentionally work offline (e.g., address generation, invoice and offer creation, listings) are left as is. Adds a unit test for the not-running errors and an integration test covering calls after shutdown and normal operation after a restart. AI-assisted: this change was prepared with Claude Code. --- src/builder.rs | 94 +++++++++++++++++++++++++++++ src/lib.rs | 53 +++++++++++++++++ src/liquidity/client/lsps1.rs | 14 ++++- src/liquidity/mod.rs | 11 +++- src/payment/bolt11.rs | 20 +++++++ src/payment/bolt12.rs | 16 +++++ src/payment/onchain.rs | 8 +++ src/payment/unified.rs | 20 ++++++- tests/integration_tests_rust.rs | 102 ++++++++++++++++++++++++++++++++ 9 files changed, 331 insertions(+), 7 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 2dd542ba3e..73f9dd17d4 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -2743,6 +2743,100 @@ mod tests { assert!(matches!(result, Err(BuildError::ReadFailed))); } + #[test] + fn unstarted_node_rejects_live_node_apis() { + use std::str::FromStr; + + use bitcoin::secp256k1::PublicKey; + use lightning::ln::channelmanager::PaymentId; + use lightning::ln::msgs::SocketAddress; + use lightning_types::payment::PaymentPreimage; + + use crate::{Error, UserChannelId}; + + let builder = NodeBuilder::new(); + let logger = Arc::new(Logger::new_log_facade()); + #[cfg(not(feature = "uniffi"))] + let node_entropy = NodeEntropy::from_seed_bytes([42; 64]); + #[cfg(feature = "uniffi")] + let node_entropy = NodeEntropy::from_seed_bytes(vec![42; 64]).unwrap(); + let node = builder + .build_with_store_and_logger(node_entropy, InMemoryStore::new(), logger) + .unwrap(); + + let node_id = PublicKey::from_str( + "0276607124ebe6a6c9338517b6f485825b27c2dcc0b9fc2aa6a4c0df91194e5993", + ) + .unwrap(); + let payment_id = PaymentId([0; 32]); + let user_channel_id = UserChannelId(0); + + assert_eq!(node.onchain_payment().bump_fee_rbf(payment_id, None), Err(Error::NotRunning)); + assert_eq!(node.close_channel(&user_channel_id, node_id), Err(Error::NotRunning)); + assert_eq!( + node.force_close_channel(&user_channel_id, node_id, None), + Err(Error::NotRunning) + ); + assert_eq!( + node.update_channel_config(&user_channel_id, node_id, Default::default()), + Err(Error::NotRunning) + ); + assert_eq!(node.splice_in(&user_channel_id, node_id, 1_000), Err(Error::NotRunning)); + assert_eq!( + node.bump_channel_funding_fee(&user_channel_id, node_id), + Err(Error::NotRunning) + ); + assert_eq!(node.bolt11_payment().fail_for_id(payment_id), Err(Error::NotRunning)); + assert_eq!( + node.bolt11_payment().claim_for_id(payment_id, 0, PaymentPreimage([0; 32])), + Err(Error::NotRunning) + ); + + let address = SocketAddress::from_str("127.0.0.1:9735").unwrap(); + assert_eq!( + node.open_channel(node_id, address.clone(), 1_000, None, None, None), + Err(Error::NotRunning) + ); + assert_eq!( + node.open_channel_with_all(node_id, address.clone(), None, None, None), + Err(Error::NotRunning) + ); + assert_eq!( + node.open_announced_channel(node_id, address.clone(), 1_000, None, None, None), + Err(Error::NotRunning) + ); + assert_eq!( + node.open_announced_channel_with_all(node_id, address.clone(), None, None, None), + Err(Error::NotRunning) + ); + assert_eq!( + node.open_0reserve_channel(node_id, address.clone(), 1_000, None, None, None), + Err(Error::NotRunning) + ); + assert_eq!( + node.open_0reserve_channel_with_all(node_id, address, None, None, None), + Err(Error::NotRunning) + ); + + // Async receive state is persisted via the background processor. + assert_eq!(node.bolt12_payment().receive_async().err(), Some(Error::NotRunning)); + #[cfg(not(feature = "uniffi"))] + let paths = Vec::new(); + #[cfg(feature = "uniffi")] + let paths = Vec::::new(); + assert_eq!( + node.bolt12_payment().set_paths_to_static_invoice_server(paths), + Err(Error::NotRunning) + ); + + // The running state is checked before any URI or HRN resolution happens. + #[cfg(feature = "unified-payments")] + { + let res = node.runtime.block_on(node.unified_payment().send("not a uri", None, None)); + assert_eq!(res.err(), Some(Error::NotRunning)); + } + } + #[test] fn sanitize_empty_node_alias() { // Empty node alias diff --git a/src/lib.rs b/src/lib.rs index e738c27207..eb5d9d9491 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1175,6 +1175,7 @@ impl Node { self.bolt11_payment().into(), self.bolt12_payment().into(), Arc::clone(&self.config), + Arc::clone(&self.is_running), Arc::clone(&self.logger), self.hrn_resolver.clone(), ) @@ -1199,6 +1200,7 @@ impl Node { self.bolt11_payment(), self.bolt12_payment(), Arc::clone(&self.config), + Arc::clone(&self.is_running), Arc::clone(&self.logger), self.hrn_resolver.clone(), )) @@ -1254,6 +1256,7 @@ impl Node { Arc::clone(&self.wallet), Arc::clone(&self.connection_manager), Arc::clone(&self.liquidity_source), + Arc::clone(&self.is_running), Arc::clone(&self.logger), ) } @@ -1266,6 +1269,7 @@ impl Node { Arc::clone(&self.wallet), Arc::clone(&self.connection_manager), Arc::clone(&self.liquidity_source), + Arc::clone(&self.is_running), Arc::clone(&self.logger), )) } @@ -1538,6 +1542,10 @@ impl Node { push_to_counterparty_msat: Option, fee_rate: Option, channel_config: Option, ) -> Result { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + let fee_rate_opt = maybe_map_fee_rate_opt(fee_rate); self.open_channel_inner( node_id, @@ -1580,6 +1588,10 @@ impl Node { push_to_counterparty_msat: Option, fee_rate: Option, channel_config: Option, ) -> Result { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + if let Err(err) = may_announce_channel(&self.config) { log_error!(self.logger, "Failed to open announced channel as the node hasn't been sufficiently configured to act as a forwarding node: {}", err); return Err(Error::ChannelCreationFailed); @@ -1619,6 +1631,10 @@ impl Node { &self, node_id: PublicKey, address: SocketAddress, push_to_counterparty_msat: Option, fee_rate: Option, channel_config: Option, ) -> Result { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + let fee_rate_opt = maybe_map_fee_rate_opt(fee_rate); self.open_channel_inner( node_id, @@ -1657,6 +1673,10 @@ impl Node { &self, node_id: PublicKey, address: SocketAddress, push_to_counterparty_msat: Option, fee_rate: Option, channel_config: Option, ) -> Result { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + if let Err(err) = may_announce_channel(&self.config) { log_error!(self.logger, "Failed to open announced channel as the node hasn't been sufficiently configured to act as a forwarding node: {err}"); return Err(Error::ChannelCreationFailed); @@ -1702,6 +1722,10 @@ impl Node { push_to_counterparty_msat: Option, fee_rate: Option, channel_config: Option, ) -> Result { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + let fee_rate_opt = maybe_map_fee_rate_opt(fee_rate); self.open_channel_inner( node_id, @@ -1736,6 +1760,10 @@ impl Node { &self, node_id: PublicKey, address: SocketAddress, push_to_counterparty_msat: Option, fee_rate: Option, channel_config: Option, ) -> Result { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + let fee_rate_opt = maybe_map_fee_rate_opt(fee_rate); self.open_channel_inner( node_id, @@ -1893,6 +1921,10 @@ impl Node { &self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, splice_amount_sats: u64, ) -> Result<(), Error> { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + self.splice_in_inner( user_channel_id, counterparty_node_id, @@ -1916,6 +1948,9 @@ impl Node { pub fn splice_in_with_all( &self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, ) -> Result<(), Error> { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } self.splice_in_inner(user_channel_id, counterparty_node_id, FundingAmount::Max) } @@ -1934,6 +1969,10 @@ impl Node { &self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, address: &Address, splice_amount_sats: u64, ) -> Result<(), Error> { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + let open_channels = self.channel_manager.list_channels_with_counterparty(&counterparty_node_id); if let Some(channel_details) = @@ -2015,6 +2054,10 @@ impl Node { pub fn bump_channel_funding_fee( &self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, ) -> Result<(), Error> { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + let open_channels = self.channel_manager.list_channels_with_counterparty(&counterparty_node_id); if let Some(channel_details) = @@ -2130,6 +2173,9 @@ impl Node { pub fn close_channel( &self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, ) -> Result<(), Error> { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } self.close_channel_internal(user_channel_id, counterparty_node_id, false, None) } @@ -2150,6 +2196,9 @@ impl Node { &self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, reason: Option, ) -> Result<(), Error> { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } self.close_channel_internal(user_channel_id, counterparty_node_id, true, reason) } @@ -2210,6 +2259,10 @@ impl Node { &self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, channel_config: ChannelConfig, ) -> Result<(), Error> { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + let open_channels: Vec = self.channel_manager.list_channels_with_counterparty(&counterparty_node_id); if let Some(channel_details) = diff --git a/src/liquidity/client/lsps1.rs b/src/liquidity/client/lsps1.rs index d6549ccbf8..79dc655fe6 100644 --- a/src/liquidity/client/lsps1.rs +++ b/src/liquidity/client/lsps1.rs @@ -463,6 +463,7 @@ pub struct LSPS1Liquidity { wallet: Arc, connection_manager: Arc>>, liquidity_source: Arc>>, + is_running: Arc>, logger: Arc, } @@ -470,9 +471,10 @@ impl LSPS1Liquidity { pub(crate) fn new( runtime: Arc, wallet: Arc, connection_manager: Arc>>, - liquidity_source: Arc>>, logger: Arc, + liquidity_source: Arc>>, is_running: Arc>, + logger: Arc, ) -> Self { - Self { runtime, wallet, connection_manager, liquidity_source, logger } + Self { runtime, wallet, connection_manager, liquidity_source, is_running, logger } } } @@ -489,6 +491,10 @@ impl LSPS1Liquidity { &self, lsp_balance_sat: u64, client_balance_sat: u64, channel_expiry_blocks: u32, announce_channel: bool, node_id: Option, ) -> Result { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + let lsps1_node = self .runtime .block_on(async { self.liquidity_source.get_lsps1_node(node_id.as_ref()).await })?; @@ -528,6 +534,10 @@ impl LSPS1Liquidity { pub fn check_order_status( &self, order_id: LSPS1OrderId, lsp_node_id: PublicKey, ) -> Result { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + let lsps1_node = self .runtime .block_on(async { self.liquidity_source.get_lsps1_node(Some(&lsp_node_id)).await })?; diff --git a/src/liquidity/mod.rs b/src/liquidity/mod.rs index ffc1f878bc..c1442b0fc9 100644 --- a/src/liquidity/mod.rs +++ b/src/liquidity/mod.rs @@ -134,6 +134,7 @@ pub struct Liquidity { wallet: Arc, connection_manager: Arc>>, liquidity_source: Arc>>, + is_running: Arc>, logger: Arc, } @@ -141,9 +142,10 @@ impl Liquidity { pub(crate) fn new( runtime: Arc, wallet: Arc, connection_manager: Arc>>, - liquidity_source: Arc>>, logger: Arc, + liquidity_source: Arc>>, is_running: Arc>, + logger: Arc, ) -> Self { - Self { runtime, wallet, connection_manager, liquidity_source, logger } + Self { runtime, wallet, connection_manager, liquidity_source, is_running, logger } } } @@ -159,6 +161,10 @@ impl Liquidity { &self, node_id: PublicKey, address: SocketAddress, token: Option, trust_peer_0conf: bool, ) -> Result<(), Error> { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + { let mut lsp_nodes = self.liquidity_source.lsp_nodes.write().expect("lock"); if lsp_nodes.iter().any(|n| n.node_id == node_id) { @@ -212,6 +218,7 @@ impl Liquidity { Arc::clone(&self.wallet), Arc::clone(&self.connection_manager), self.liquidity_source.lsps1_client(), + Arc::clone(&self.is_running), Arc::clone(&self.logger), ) } diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 5f0a70c2c6..4d2d210a7f 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -423,6 +423,10 @@ impl Bolt11Payment { pub fn claim_for_id( &self, payment_id: PaymentId, claimable_amount_msat: u64, preimage: PaymentPreimage, ) -> Result<(), Error> { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + let details = self.runtime.block_on(self.payment_store.get(&payment_id))?.ok_or_else(|| { log_error!( @@ -489,6 +493,10 @@ impl Bolt11Payment { /// /// [`PaymentClaimable`]: crate::Event::PaymentClaimable pub fn fail_for_id(&self, payment_id: PaymentId) -> Result<(), Error> { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + let details = self.runtime.block_on(self.payment_store.get(&payment_id))?.ok_or_else(|| { log_error!( @@ -637,6 +645,9 @@ impl Bolt11Payment { &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, ) -> Result { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } let description = maybe_try_convert_enum(description)?; let invoice = self.receive_via_jit_channel_inner( Some(amount_msat), @@ -682,6 +693,9 @@ impl Bolt11Payment { &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, payment_hash: PaymentHash, ) -> Result { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } let description = maybe_try_convert_enum(description)?; let invoice = self.receive_via_jit_channel_inner( Some(amount_msat), @@ -709,6 +723,9 @@ impl Bolt11Payment { &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, max_proportional_lsp_fee_limit_ppm_msat: Option, ) -> Result { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } let description = maybe_try_convert_enum(description)?; let invoice = self.receive_via_jit_channel_inner( None, @@ -755,6 +772,9 @@ impl Bolt11Payment { &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: PaymentHash, ) -> Result { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } let description = maybe_try_convert_enum(description)?; let invoice = self.receive_via_jit_channel_inner( None, diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 0e994d67ac..dcac54c172 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -569,6 +569,10 @@ impl Bolt12Payment { &self, amount_msat: u64, expiry_secs: u32, quantity: Option, payer_note: Option, route_parameters: Option, ) -> Result { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + let payment_id = PaymentId(self.keys_manager.get_secure_random_bytes()); let absolute_expiry = (SystemTime::now() + Duration::from_secs(expiry_secs as u64)) @@ -640,6 +644,10 @@ impl Bolt12Payment { /// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice /// [`Offer`]: lightning::offers::offer::Offer pub fn receive_async(&self) -> Result { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + self.channel_manager .get_async_receive_offer() .map(maybe_wrap) @@ -659,6 +667,10 @@ impl Bolt12Payment { pub fn set_paths_to_static_invoice_server( &self, paths: Vec, ) -> Result<(), Error> { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + self.channel_manager .set_paths_to_static_invoice_server(paths) .or(Err(Error::InvalidBlindedPaths)) @@ -689,6 +701,10 @@ impl Bolt12Payment { /// [`Offer`]: lightning::offers::offer::Offer /// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice pub fn set_paths_to_static_invoice_server(&self, paths: Vec) -> Result<(), Error> { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + let decoded_paths = as Readable>::read(&mut &paths[..]) .or(Err(Error::InvalidBlindedPaths))?; diff --git a/src/payment/onchain.rs b/src/payment/onchain.rs index 141b2e3b23..98f5240220 100644 --- a/src/payment/onchain.rs +++ b/src/payment/onchain.rs @@ -79,6 +79,10 @@ impl OnchainPayment { pub fn send_to_address( &self, address: &bitcoin::Address, amount_sats: u64, fee_rate: Option, ) -> Result { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + let fee_rate_opt = maybe_map_fee_rate_opt(fee_rate); self.runtime.block_on(self.send_to_address_inner(address, amount_sats, fee_rate_opt)) } @@ -134,6 +138,10 @@ impl OnchainPayment { pub fn bump_fee_rbf( &self, payment_id: PaymentId, fee_rate: Option, ) -> Result { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + let cur_anchor_reserve_sats = crate::total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); let fee_rate_opt = maybe_map_fee_rate_opt(fee_rate); diff --git a/src/payment/unified.rs b/src/payment/unified.rs index 0eb4f41a28..1595427f9e 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -14,7 +14,7 @@ //! [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki //! [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md //! [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md -use std::sync::Arc; +use std::sync::{Arc, RwLock}; use std::time::Duration; use std::vec::IntoIter; @@ -73,6 +73,7 @@ pub struct UnifiedPayment { bolt12_payment: Arc, #[cfg(not(hrn_tests))] config: Arc, + is_running: Arc>, logger: Arc, hrn_resolver: HRNResolver, #[cfg(hrn_tests)] @@ -82,8 +83,8 @@ pub struct UnifiedPayment { impl UnifiedPayment { pub(crate) fn new( onchain_payment: Arc, bolt11_invoice: Arc, - bolt12_payment: Arc, config: Arc, logger: Arc, - hrn_resolver: HRNResolver, + bolt12_payment: Arc, config: Arc, is_running: Arc>, + logger: Arc, hrn_resolver: HRNResolver, ) -> Self { #[cfg(hrn_tests)] let _ = config; @@ -94,6 +95,7 @@ impl UnifiedPayment { bolt12_payment, #[cfg(not(hrn_tests))] config, + is_running, logger, hrn_resolver, #[cfg(hrn_tests)] @@ -189,6 +191,10 @@ impl UnifiedPayment { &self, uri_str: &str, amount_msat: Option, route_parameters: Option, ) -> Result { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + let target_network; #[cfg(hrn_tests)] @@ -321,6 +327,10 @@ impl UnifiedPayment { log_error!(self.logger, "Failed to send BOLT12 offer: DuplicatePayment. This is part of a unified payment. Aborting to avoid duplicate payment."); return Err(Error::DuplicatePayment); }, + // There is nothing to fall back to if the node isn't running. + Err(Error::NotRunning) => { + return Err(Error::NotRunning); + }, // A persistence failure may occur after the Lightning payment has // already been initiated with the ChannelManager. Falling back to // the BOLT11 invoice in that case would double-pay, so we abort @@ -348,6 +358,10 @@ impl UnifiedPayment { log_error!(self.logger, "Failed to send BOLT11 invoice: DuplicatePayment. This is part of a unified payment. Aborting to avoid duplicate payment."); return Err(Error::DuplicatePayment); }, + // There is nothing to fall back to if the node isn't running. + Err(Error::NotRunning) => { + return Err(Error::NotRunning); + }, // A persistence failure may occur after the Lightning payment has // already been initiated with the ChannelManager. Falling back to // the on-chain method in that case would double-pay, so we abort diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index e33682dea0..b21b749acf 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -4289,6 +4289,108 @@ async fn spontaneous_send_with_custom_preimage() { } } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn live_node_apis_return_not_running_after_stop() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); + + let address_a = node_a.onchain_payment().new_address().unwrap(); + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address_a], + Amount::from_sat(1_000_000), + ) + .await; + node_a.sync_wallets().unwrap(); + + let description = + Bolt11InvoiceDescription::Direct(Description::new(String::from("test")).unwrap()); + let invoice = node_b.bolt11_payment().receive(1_000_000, &description.clone().into(), 3600); + let invoice = invoice.unwrap(); + let node_b_id = node_b.node_id(); + let node_b_addr = node_b.listening_addresses().unwrap().first().unwrap().clone(); + let btc_address = node_b.onchain_payment().new_address().unwrap(); + + node_a.stop().unwrap(); + + let payment_id = PaymentId([0; 32]); + let preimage = PaymentPreimage([0; 32]); + let user_channel_id = ldk_node::UserChannelId(0); + + // On-chain. + assert_eq!( + node_a.onchain_payment().send_to_address(&btc_address, 10_000, None), + Err(NodeError::NotRunning) + ); + assert_eq!( + node_a.onchain_payment().send_all_to_address(&btc_address, true, None), + Err(NodeError::NotRunning) + ); + assert_eq!(node_a.onchain_payment().bump_fee_rbf(payment_id, None), Err(NodeError::NotRunning)); + + // Channels. + assert_eq!( + node_a.open_channel(node_b_id, node_b_addr.clone(), 100_000, None, None, None), + Err(NodeError::NotRunning) + ); + assert_eq!(node_a.splice_in(&user_channel_id, node_b_id, 10_000), Err(NodeError::NotRunning)); + assert_eq!( + node_a.splice_out(&user_channel_id, node_b_id, &btc_address, 10_000), + Err(NodeError::NotRunning) + ); + assert_eq!( + node_a.bump_channel_funding_fee(&user_channel_id, node_b_id), + Err(NodeError::NotRunning) + ); + assert_eq!(node_a.close_channel(&user_channel_id, node_b_id), Err(NodeError::NotRunning)); + assert_eq!( + node_a.force_close_channel(&user_channel_id, node_b_id, None), + Err(NodeError::NotRunning) + ); + assert_eq!( + node_a.update_channel_config(&user_channel_id, node_b_id, Default::default()), + Err(NodeError::NotRunning) + ); + assert_eq!(node_a.connect(node_b_id, node_b_addr.clone(), false), Err(NodeError::NotRunning)); + assert_eq!(node_a.sync_wallets(), Err(NodeError::NotRunning)); + + // Payments. + assert_eq!(node_a.bolt11_payment().send(&invoice, None), Err(NodeError::NotRunning)); + assert_eq!( + node_a.bolt11_payment().claim_for_id(payment_id, 0, preimage), + Err(NodeError::NotRunning) + ); + assert_eq!(node_a.bolt11_payment().fail_for_id(payment_id), Err(NodeError::NotRunning)); + assert_eq!( + node_a.bolt11_payment().receive_via_jit_channel(1_000_000, &description.into(), 3600, None), + Err(NodeError::NotRunning) + ); + assert_eq!( + node_a.bolt12_payment().initiate_refund(1_000, 3600, None, None, None).map(|_| ()), + Err(NodeError::NotRunning) + ); + assert_eq!( + node_a.spontaneous_payment().send(1_000, node_b_id, None), + Err(NodeError::NotRunning) + ); + + // Liquidity. + assert_eq!( + node_a.liquidity().add_liquidity_source(node_b_id, node_b_addr, None, false), + Err(NodeError::NotRunning) + ); + + // The node is fully functional again after a restart. + node_a.start().unwrap(); + node_a.sync_wallets().unwrap(); + assert!(node_a.onchain_payment().send_to_address(&btc_address, 10_000, None).is_ok()); + + node_a.stop().unwrap(); + node_b.stop().unwrap(); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn drop_in_async_context() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();