diff --git a/crates/cdk-integration-tests/src/bin/start_regtest.rs b/crates/cdk-integration-tests/src/bin/start_regtest.rs index 210e3ab0fe..b4b6c27ae3 100644 --- a/crates/cdk-integration-tests/src/bin/start_regtest.rs +++ b/crates/cdk-integration-tests/src/bin/start_regtest.rs @@ -84,10 +84,14 @@ async fn main() -> Result<()> { }); match timeout(Duration::from_secs(300), rx).await { - Ok(_) => { + Ok(Ok(())) => { tracing::info!("Regtest set up"); signal_progress(&temp_dir); } + Ok(Err(err)) => { + tracing::error!("regtest setup task exited before signaling readiness: {err}"); + anyhow::bail!("Could not set up regtest"); + } Err(_) => { tracing::error!("regtest setup timed out after 5 minutes"); anyhow::bail!("Could not set up regtest"); diff --git a/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs b/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs index 8e69f24c3b..17277e321c 100644 --- a/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs +++ b/crates/cdk-integration-tests/src/bin/start_regtest_mints.rs @@ -19,21 +19,33 @@ use std::time::Duration; use anyhow::{bail, Result}; use bip39::Mnemonic; -use cashu::Amount; +use cashu::{Amount, CurrencyUnit, PaymentMethod}; +use cdk::wallet::Wallet; use cdk_integration_tests::cli::CommonArgs; -use cdk_integration_tests::init_regtest::start_regtest_end; +use cdk_integration_tests::init_regtest::{get_cln_dir, start_regtest_end}; +use cdk_integration_tests::ln_regtest::ln_client::{ClnClient, LightningClient}; use cdk_integration_tests::shared; -use cdk_ldk_node::{CdkLdkNode, CdkLdkNodeBuilder}; +use cdk_ldk_node::CdkLdkNodeBuilder; use cdk_mintd::config::LoggingConfig; +use cdk_sqlite::wallet::memory; use clap::Parser; use ldk_node::lightning::ln::msgs::SocketAddress; use tokio::runtime::Runtime; use tokio::signal; use tokio::signal::unix::SignalKind; use tokio::sync::{oneshot, Notify}; -use tokio::time::timeout; +use tokio::time::{sleep, timeout}; use tokio_util::sync::CancellationToken; +const LDK_NODE_P2P_PORT_OFFSET: u16 = 10; + +fn derive_ldk_node_p2p_port(ldk_port: u16) -> Result { + match ldk_port.checked_add(LDK_NODE_P2P_PORT_OFFSET) { + Some(port) => Ok(port), + None => bail!("LDK mint port {ldk_port} is too high to derive a P2P port"), + } +} + #[derive(Parser)] #[command(name = "start-regtest-mints")] #[command(about = "Start regtest mints", long_about = None)] @@ -205,18 +217,16 @@ async fn start_lnd_mint( Ok(handle) } -/// Start regtest LDK mint using the library -/// If `existing_node` is provided, it will be used instead of creating a new one. -/// This allows the mint to use a node that was already set up (e.g., with channels). +/// Start regtest LDK mint using the library. async fn start_ldk_mint( temp_dir: &Path, port: u16, database_type: &str, shutdown: Arc, runtime: Option>, - existing_node: Option, ) -> Result> { let ldk_work_dir = temp_dir.join("ldk_mint"); + let ldk_node_p2p_port = derive_ldk_node_p2p_port(port)?; // Create work directory for LDK mint fs::create_dir_all(&ldk_work_dir)?; @@ -237,7 +247,7 @@ async fn start_ldk_mint( ldk_node_announce_addresses: None, storage_dir_path: Some(ldk_work_dir.to_string_lossy().to_string()), ldk_node_host: Some("127.0.0.1".to_string()), - ldk_node_port: Some(port + 10), // Use a different port for the LDK node P2P connections + ldk_node_port: Some(ldk_node_p2p_port), gossip_source_type: None, rgs_url: None, webserver_host: Some("127.0.0.1".to_string()), @@ -267,13 +277,6 @@ async fn start_ldk_mint( println!("LDK mint shutdown signal received"); }; - // Both nodes now use the same seed, so the standard flow should work. - // The existing_node parameter is kept for API compatibility but not used - // since run_mintd_with_shutdown will create its own node with the same seed. - if existing_node.is_some() { - println!("Using existing LDK node configuration (same seed as mint)"); - } - match cdk_mintd::run_mintd_with_shutdown( &ldk_work_dir, &settings, @@ -292,6 +295,140 @@ async fn start_ldk_mint( Ok(handle) } +async fn wait_for_ldk_bolt12_ready( + temp_dir: &Path, + ldk_port: u16, + ldk_node_p2p_port: u16, + ldk_node_id: &str, + shutdown_notify: Arc, +) -> Result<()> { + let mint_url = format!("http://127.0.0.1:{ldk_port}"); + let readiness_amount = Amount::from(1); + let start_time = std::time::Instant::now(); + let max_wait = Duration::from_secs(120); + let mut attempt = 1; + let mut last_error = None; + + println!("Waiting for LDK mint BOLT12 readiness on port {ldk_port}..."); + + let wallet = Wallet::new( + &mint_url, + CurrencyUnit::Sat, + Arc::new(memory::empty().await?), + Mnemonic::generate(12)?.to_seed_normalized(""), + None, + )?; + + let cln_client = ClnClient::new(get_cln_dir(temp_dir, "one"), None).await?; + + loop { + if shutdown_notify.is_cancelled() { + bail!("Canceled waiting for LDK mint BOLT12 readiness"); + } + + if start_time.elapsed() > max_wait { + let last_error = last_error + .as_deref() + .unwrap_or("no readiness attempt completed before timeout"); + bail!("Timeout waiting for LDK mint BOLT12 readiness: {last_error}"); + } + + match cln_client + .connect_peer( + ldk_node_id.to_string(), + "127.0.0.1".to_string(), + ldk_node_p2p_port, + ) + .await + { + Ok(_) => tracing::info!("CLN reconnected to LDK mint node for readiness check"), + Err(err) => { + let err = err.to_string(); + if !err.to_lowercase().contains("already connected") { + last_error = Some(format!("peer reconnect failed: {err}")); + tracing::warn!( + "LDK BOLT12 readiness attempt {attempt}: CLN reconnect to LDK mint node failed: {err}" + ); + sleep(Duration::from_secs(2)).await; + attempt += 1; + continue; + } + } + } + + let mint_quote = match wallet + .mint_quote(PaymentMethod::BOLT12, Some(readiness_amount), None, None) + .await + { + Ok(quote) => quote, + Err(err) => { + last_error = Some(format!("quote creation failed: {err}")); + tracing::warn!( + "LDK BOLT12 readiness attempt {attempt}: quote creation failed: {err}" + ); + sleep(Duration::from_secs(2)).await; + attempt += 1; + continue; + } + }; + + match cln_client + .pay_bolt12_offer(None, mint_quote.request.clone()) + .await + { + Ok(_) => (), + Err(err) => { + last_error = Some(format!("payment failed: {err}")); + tracing::warn!("LDK BOLT12 readiness attempt {attempt}: payment failed: {err}"); + sleep(Duration::from_secs(2)).await; + attempt += 1; + continue; + } + } + + let quote_poll_start = std::time::Instant::now(); + let mut last_status_error = None; + while quote_poll_start.elapsed() <= Duration::from_secs(20) { + if shutdown_notify.is_cancelled() { + bail!("Canceled waiting for LDK mint BOLT12 readiness"); + } + + match wallet.check_mint_quote_status(&mint_quote.id).await { + Ok(quote_state) => { + if quote_state.amount_paid >= readiness_amount { + println!("LDK mint BOLT12 readiness confirmed on port {ldk_port}"); + return Ok(()); + } + } + Err(err) => { + last_status_error = Some(err.to_string()); + tracing::warn!( + "LDK BOLT12 readiness attempt {attempt}: quote status check failed: {err}" + ); + } + } + + sleep(Duration::from_secs(1)).await; + } + + tracing::warn!( + "LDK BOLT12 readiness attempt {attempt}: payment was sent but quote {} was not observed as paid", + mint_quote.id + ); + last_error = Some(match last_status_error { + Some(err) => format!( + "payment was sent but quote {} was not observed as paid; last status check failed: {err}", + mint_quote.id + ), + None => format!( + "payment was sent but quote {} was not observed as paid", + mint_quote.id + ), + }); + attempt += 1; + } +} + /// Create settings for an LDK mint fn create_ldk_settings( port: u16, @@ -400,6 +537,11 @@ fn apply_database_settings( Ok(()) } +fn signal_mints_ready(temp_dir: &Path) -> Result<()> { + fs::write(temp_dir.join(".ready"), "ready\n")?; + Ok(()) +} + /// Create settings for an onchain-only mint fn create_onchain_settings(port: u16) -> cdk_mintd::config::Settings { cdk_mintd::config::Settings { @@ -563,6 +705,7 @@ fn main() -> Result<()> { .await?; println!("Onchain-only mint is ready on port {}!", args.cln_port); + signal_mints_ready(&temp_dir)?; // Wait for shutdown shutdown_clone_one.notified().await; @@ -572,6 +715,7 @@ fn main() -> Result<()> { let ldk_work_dir = temp_dir.join("ldk_mint"); fs::create_dir_all(ldk_work_dir.join("logs"))?; + let ldk_node_p2p_port = derive_ldk_node_p2p_port(args.ldk_port)?; let test_mnemonic: Mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" .parse() .expect("Failed to parse test mnemonic"); @@ -591,7 +735,7 @@ fn main() -> Result<()> { }, vec![SocketAddress::TcpIpV4 { addr: [127, 0, 0, 1], - port: 8092, + port: ldk_node_p2p_port, }], ) .with_seed(test_mnemonic.clone()); @@ -624,7 +768,7 @@ fn main() -> Result<()> { }, vec![SocketAddress::TcpIpV4 { addr: [127, 0, 0, 1], - port: 8092, + port: ldk_node_p2p_port, }], ) .with_seed(test_mnemonic); @@ -634,6 +778,12 @@ fn main() -> Result<()> { }; let inner_node = cdk_ldk.node(); + let ldk_node_id = inner_node.node_id().to_string(); + // The setup node is stopped by `start_regtest_end` after channels are + // opened. `ldk-node` 0.7 can panic when dropping a stopped node, so keep + // this setup wrapper alive for the short-lived regtest process and let + // the mint restart from the same storage and seed. + std::mem::forget(cdk_ldk); let temp_dir_clone = temp_dir.clone(); let shutdown_clone_two = Arc::clone(&shutdown_clone); @@ -667,14 +817,13 @@ fn main() -> Result<()> { start_lnd_mint(&temp_dir, args.lnd_port, &args.database_type, shutdown_clone.clone()) .await?; - // Start LDK mint (using the existing node that was already set up with channels) + // Start LDK mint from the node storage that was set up with channels. let ldk_handle = start_ldk_mint( &temp_dir, args.ldk_port, &args.database_type, shutdown_clone.clone(), Some(rt_clone), - Some(cdk_ldk), ) .await?; @@ -716,7 +865,7 @@ fn main() -> Result<()> { Arc::clone(&cancel_token) ), ) { - Ok(_) => println!("All mints are ready!"), + Ok(_) => println!("All mint HTTP endpoints are ready!"), Err(e) => { if cancel_token.is_cancelled() { bail!("Startup canceled by user"); @@ -730,6 +879,16 @@ fn main() -> Result<()> { bail!("Token canceled"); } + wait_for_ldk_bolt12_ready( + &temp_dir, + args.ldk_port, + ldk_node_p2p_port, + &ldk_node_id, + Arc::clone(&cancel_token), + ) + .await?; + signal_mints_ready(&temp_dir)?; + println!("All regtest mints started successfully!"); println!("CLN mint: http://{}:{}", args.mint_addr, args.cln_port); println!("LND mint: http://{}:{}", args.mint_addr, args.lnd_port); diff --git a/crates/cdk-integration-tests/src/init_regtest.rs b/crates/cdk-integration-tests/src/init_regtest.rs index 8ac874376d..fcee5a3240 100644 --- a/crates/cdk-integration-tests/src/init_regtest.rs +++ b/crates/cdk-integration-tests/src/init_regtest.rs @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; -use anyhow::Result; +use anyhow::{bail, Result}; use cdk::types::FeeReserve; use cdk_cln::Cln as CdkCln; use cdk_common::database::DynKVStore; @@ -286,6 +286,70 @@ where Ok(()) } +fn summarize_ldk_channels(node: &Node) -> String { + let channel_summary = node + .list_channels() + .iter() + .map(|channel| { + let confirmations = match channel.confirmations { + Some(confirmations) => confirmations.to_string(), + None => "none".to_string(), + }; + format!( + "peer={} usable={} ready={} confirmations={} inbound_msat={} outbound_msat={}", + channel.counterparty_node_id, + channel.is_usable, + channel.is_channel_ready, + confirmations, + channel.inbound_capacity_msat, + channel.outbound_capacity_msat + ) + }) + .collect::>() + .join("; "); + + if channel_summary.is_empty() { + "none".to_string() + } else { + channel_summary + } +} + +async fn wait_ldk_channels_usable(node: &Node, peer_ids: &[&str], max_checks: u32) -> Result<()> { + let mut count = 0; + while count < max_checks { + node.sync_wallets()?; + let channels = node.list_channels(); + let missing_peers = peer_ids + .iter() + .copied() + .filter(|peer_id| { + !channels.iter().any(|channel| { + channel.counterparty_node_id.to_string() == *peer_id && channel.is_usable + }) + }) + .collect::>(); + + if missing_peers.is_empty() { + tracing::info!("All expected LDK channels are usable"); + return Ok(()); + } + + tracing::warn!( + "Waiting for usable LDK channels with peers: {}", + missing_peers.join(", ") + ); + + count += 1; + tokio::time::sleep(Duration::from_secs(2)).await; + } + + bail!( + "Timeout waiting for usable LDK channels; channels: {}", + summarize_ldk_channels(node) + ) +} + pub async fn start_regtest_end( work_dir: &Path, sender: Sender<()>, @@ -493,7 +557,7 @@ pub async fn start_regtest_end( .connect_peer(cln_two_info.pubkey, listen_addr.clone(), cln_two_info.port) .await?; - tracing::info!("Opening channel from lnd to ldk"); + tracing::info!("Preparing channel from ldk to lnd"); let cln_info = cln_client.get_connect_info().await?; @@ -523,26 +587,83 @@ pub async fn start_regtest_end( true, )?; - // lnd_client - // .open_channel(1_500_000, &pubkey.to_string(), Some(750_000)) - // .await - // .unwrap(); + let ldk_pubkey = pubkey.to_string(); + let lnd_pubkey = lnd_info.pubkey.clone(); + let mut ldk_lnd_channel_error = None; + let mut delay = Duration::from_millis(500); + let max_retries = 5; + for attempt in 1..=max_retries { + generate_block(&bitcoin_client)?; + lnd_client.wait_chain_sync().await?; + + tracing::info!("Opening channel from ldk to lnd (attempt {attempt}/{max_retries})"); + let open_result = node.open_announced_channel( + lnd_pubkey.parse()?, + SocketAddress::TcpIpV4 { + addr: [127, 0, 0, 1], + port: lnd_info.port, + }, + 1_000_000, + Some(500_000_000), + None, + ); - generate_block(&bitcoin_client)?; - lnd_client.wait_chain_sync().await?; + match open_result { + Ok(_) => { + generate_block(&bitcoin_client)?; + cln_client.wait_chain_sync().await?; + lnd_client.wait_chain_sync().await?; + + match wait_ldk_channels_usable(&node, &[lnd_pubkey.as_str()], 10).await { + Ok(_) => match lnd_client + .wait_channel_active_with_peer_attempts(&ldk_pubkey, 10) + .await + { + Ok(_) => { + ldk_lnd_channel_error = None; + break; + } + Err(err) => { + tracing::warn!( + "LND did not report an active channel with LDK after attempt {attempt}/{max_retries}: {err}" + ); + ldk_lnd_channel_error = Some(err); + } + }, + Err(err) => { + tracing::warn!( + "LDK did not report a usable channel with LND after attempt {attempt}/{max_retries}: {err}" + ); + ldk_lnd_channel_error = Some(err); + } + } + } + Err(err) => { + let err = anyhow::Error::from(err); + tracing::warn!( + "LDK failed to open a channel with LND on attempt {attempt}/{max_retries}: {err}" + ); + ldk_lnd_channel_error = Some(err); + } + } - node.open_announced_channel( - lnd_info.pubkey.parse()?, - SocketAddress::TcpIpV4 { - addr: [127, 0, 0, 1], - port: lnd_info.port, - }, - 1_000_000, - Some(500_000_000), - None, - )?; + if attempt < max_retries { + tracing::warn!( + "Retrying LDK to LND channel open in {} ms; LDK channels: {}", + delay.as_millis(), + summarize_ldk_channels(&node) + ); + tokio::time::sleep(delay).await; + delay = std::cmp::min(delay * 2, Duration::from_secs(5)); + } + } - generate_block(&bitcoin_client)?; + if let Some(err) = ldk_lnd_channel_error { + bail!( + "Failed to open a usable LDK to LND channel after {max_retries} attempts: {err}; LDK channels: {}", + summarize_ldk_channels(&node) + ); + } tracing::info!("Ldk channels opened"); @@ -550,11 +671,31 @@ pub async fn start_regtest_end( tracing::info!("Ldk wallet synced"); + wait_ldk_channels_usable( + &node, + &[cln_info.pubkey.as_str(), lnd_info.pubkey.as_str()], + 100, + ) + .await?; + + cln_client + .wait_channel_active_with_peer(&pubkey.to_string()) + .await?; + + lnd_client + .wait_channel_active_with_peer(&pubkey.to_string()) + .await?; + cln_client.wait_channels_active().await?; lnd_client.wait_channels_active().await?; - node.stop()?; + let stop_result = node.stop(); + // This setup-only node is intentionally restarted by the mint from + // the same storage and seed. `ldk-node` 0.7 can panic when dropping + // a stopped node, so avoid running its Drop implementation here. + std::mem::forget(node); + stop_result?; } else { cln_client.wait_channels_active().await?; diff --git a/crates/cdk-integration-tests/src/ln_regtest/ln_client/cln_client.rs b/crates/cdk-integration-tests/src/ln_regtest/ln_client/cln_client.rs index 81f66857ff..238e17881e 100644 --- a/crates/cdk-integration-tests/src/ln_regtest/ln_client/cln_client.rs +++ b/crates/cdk-integration-tests/src/ln_regtest/ln_client/cln_client.rs @@ -21,7 +21,7 @@ use cln_rpc::primitives::{Amount, AmountOrAll, AmountOrAny, PublicKey}; use cln_rpc::{ClnRpc, Request}; use lightning::offers::offer::Offer; use tokio::sync::Mutex; -use tokio::time::sleep; +use tokio::time::{sleep, timeout}; use uuid::Uuid; use super::types::{Balance, ConnectInfo}; @@ -36,6 +36,12 @@ pub struct ClnClient { } impl ClnClient { + async fn reconnect(&self) -> Result<()> { + let cln_client = cln_rpc::ClnRpc::new(&self.rpc_path).await?; + *self.client.lock().await = cln_client; + Ok(()) + } + /// Open a private (unannounced) channel to a peer pub async fn open_private_channel( &self, @@ -136,33 +142,110 @@ impl ClnClient { } } + pub async fn wait_channel_active_with_peer(&self, peer_id: &str) -> Result<()> { + let peer_id = PublicKey::from_str(peer_id)?.to_string(); + let mut count = 0; + while count < 100 { + let channels = self.list_channels().await?; + let peer_channels = channels + .channels + .iter() + .filter(|channel| { + channel.source.to_string() == peer_id + || channel.destination.to_string() == peer_id + }) + .collect::>(); + + if peer_channels.iter().any(|channel| channel.active) { + tracing::info!("CLN channel with peer {peer_id} is active"); + return Ok(()); + } + + if peer_channels.is_empty() { + tracing::warn!("No CLN channel found with peer {peer_id}"); + } else { + tracing::warn!("CLN channel with peer {peer_id} is not active yet"); + } + + count += 1; + sleep(Duration::from_secs(2)).await; + } + + bail!("Time out exceeded waiting for active CLN channel with peer {peer_id}") + } + pub async fn pay_bolt12_offer( &self, amount_msat: Option, offer: String, ) -> Result { - let mut cln_client = self.client.lock().await; - - let cln_response = cln_client - .call_typed(&FetchinvoiceRequest { - amount_msat: amount_msat.map(Amount::from_msat), - payer_note: None, - quantity: None, - recurrence_counter: None, - recurrence_label: None, - recurrence_start: None, - timeout: None, - offer, - bip353: None, - payer_metadata: None, + let max_attempts = 5; + let mut attempts_remaining = max_attempts; + let mut delay = Duration::from_millis(500); + + let invoice = loop { + let attempt = max_attempts - attempts_remaining + 1; + let cln_response = timeout(Duration::from_secs(15), async { + let mut cln_client = self.client.lock().await; + cln_client + .call_typed(&FetchinvoiceRequest { + amount_msat: amount_msat.map(Amount::from_msat), + payer_note: None, + quantity: None, + recurrence_counter: None, + recurrence_label: None, + recurrence_start: None, + timeout: None, + offer: offer.clone(), + bip353: None, + payer_metadata: None, + }) + .await }) - .await?; + .await; + + match cln_response { + Ok(Ok(response)) => break response.invoice, + Ok(Err(err)) => { + let err = anyhow!("CLN fetchinvoice for BOLT12 offer failed: {err}"); + tracing::warn!( + "CLN fetchinvoice for BOLT12 offer failed on attempt {attempt}/{max_attempts}: {err}" + ); + + attempts_remaining -= 1; + if attempts_remaining == 0 { + return Err(err); + } + + sleep(delay).await; + delay = std::cmp::min(delay * 2, Duration::from_secs(5)); + } + Err(_) => { + let err = + anyhow!("CLN fetchinvoice for BOLT12 offer timed out after 15 seconds"); + tracing::warn!( + "CLN fetchinvoice for BOLT12 offer failed on attempt {attempt}/{max_attempts}: {err}" + ); + if let Err(reconnect_err) = self.reconnect().await { + tracing::warn!( + "Failed to reconnect CLN RPC after fetchinvoice timeout: {reconnect_err}" + ); + } - let invoice = cln_response.invoice; + attempts_remaining -= 1; + if attempts_remaining == 0 { + return Err(err); + } - drop(cln_client); + sleep(delay).await; + delay = std::cmp::min(delay * 2, Duration::from_secs(5)); + } + } + }; - self.pay_invoice(invoice).await + self.pay_invoice(invoice) + .await + .map_err(|err| anyhow!("CLN failed to pay fetched BOLT12 invoice: {err}")) } pub async fn get_bolt12_offer( @@ -416,7 +499,7 @@ impl LightningClient for ClnClient { label: None, riskfactor: None, maxfeepercent: None, - retry_for: None, + retry_for: Some(10), maxdelay: None, exemptfee: None, localinvreqid: None, @@ -608,7 +691,7 @@ impl LightningClient for ClnClient { label: None, riskfactor: None, maxfeepercent: None, - retry_for: None, + retry_for: Some(10), maxdelay: None, exemptfee: None, localinvreqid: None, diff --git a/crates/cdk-integration-tests/src/ln_regtest/ln_client/lnd_client.rs b/crates/cdk-integration-tests/src/ln_regtest/ln_client/lnd_client.rs index 25d7ecbd40..78ac0d51d4 100644 --- a/crates/cdk-integration-tests/src/ln_regtest/ln_client/lnd_client.rs +++ b/crates/cdk-integration-tests/src/ln_regtest/ln_client/lnd_client.rs @@ -114,17 +114,96 @@ impl LndClient { Ok(balance as u64) } + + pub async fn wait_channel_active_with_peer_attempts( + &self, + peer_id: &str, + max_checks: u32, + ) -> Result<()> { + let peer = hex::decode(peer_id)?; + let mut count = 0; + while count < max_checks { + let channels = self + .client + .lock() + .await + .lightning() + .list_channels(ListChannelsRequest { + active_only: true, + inactive_only: false, + public_only: false, + private_only: false, + peer: peer.clone(), + ..Default::default() + }) + .await? + .into_inner(); + + if !channels.channels.is_empty() { + tracing::info!("LND channel with peer {peer_id} is active"); + return Ok(()); + } + + tracing::warn!("LND channel with peer {peer_id} is not active yet"); + count += 1; + sleep(Duration::from_secs(2)).await; + } + + let channels = self + .client + .lock() + .await + .lightning() + .list_channels(ListChannelsRequest { + active_only: false, + inactive_only: false, + public_only: false, + private_only: false, + peer, + ..Default::default() + }) + .await? + .into_inner(); + let channel_summary = channels + .channels + .iter() + .map(|channel| { + format!( + "remote_pubkey={} active={} capacity={} local_balance={}", + channel.remote_pubkey, channel.active, channel.capacity, channel.local_balance + ) + }) + .collect::>() + .join("; "); + let channel_summary = if channel_summary.is_empty() { + "none".to_string() + } else { + channel_summary + }; + + bail!("Timeout waiting for active LND channel with peer {peer_id}; channels: {channel_summary}") + } + + pub async fn wait_channel_active_with_peer(&self, peer_id: &str) -> Result<()> { + self.wait_channel_active_with_peer_attempts(peer_id, 100) + .await + } } #[async_trait] impl LightningClient for LndClient { async fn get_connect_info(&self) -> Result { let info = self.get_info().await?; - let uri = info.uris.first().unwrap(); + let uri = info + .uris + .first() + .ok_or_else(|| anyhow!("LND did not report a connect URI"))?; - let parsed = parse_uri(uri); + let mut parsed = + parse_uri(uri).ok_or_else(|| anyhow!("Could not parse LND connect URI: {uri}"))?; + parsed.pubkey = info.identity_pubkey; - Ok(parsed.unwrap()) + Ok(parsed) } async fn get_new_onchain_address(&self) -> Result { diff --git a/crates/cdk-integration-tests/tests/bolt12.rs b/crates/cdk-integration-tests/tests/bolt12.rs index 9a5b0b5259..02641fcc98 100644 --- a/crates/cdk-integration-tests/tests/bolt12.rs +++ b/crates/cdk-integration-tests/tests/bolt12.rs @@ -320,20 +320,40 @@ async fn test_regtest_bolt12_melt() -> Result<()> { ) .await?; - let offer = cln_client - .get_bolt12_offer(Some(100_000), true, "hhhhhhhh".to_string()) - .await?; - - let quote = wallet - .melt_quote(PaymentMethod::BOLT12, offer.to_string(), None, None) - .await?; - - let prepared = wallet - .prepare_melt("e.id, std::collections::HashMap::new()) - .await?; - let melt = prepared.confirm().await?; - - assert_eq!(melt.amount(), 100.into()); + let max_attempts = if is_ldk_mint() { 3 } else { 1 }; + let mut attempt = 1; + loop { + let offer = cln_client + .get_bolt12_offer( + Some(100_000), + true, + format!("test_regtest_bolt12_melt_{attempt}"), + ) + .await?; + + let quote = wallet + .melt_quote(PaymentMethod::BOLT12, offer.to_string(), None, None) + .await?; + + let prepared = wallet + .prepare_melt("e.id, std::collections::HashMap::new()) + .await?; + + match prepared.confirm().await { + Ok(melt) => { + assert_eq!(melt.amount(), 100.into()); + break; + } + Err(err) if attempt < max_attempts => { + tracing::warn!( + "BOLT12 melt attempt {attempt}/{max_attempts} failed; retrying with a fresh offer: {err}" + ); + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + attempt += 1; + } + Err(err) => return Err(err.into()), + } + } Ok(()) } diff --git a/misc/fake_itests.sh b/misc/fake_itests.sh index 8c1580c3ff..bfd3467710 100755 --- a/misc/fake_itests.sh +++ b/misc/fake_itests.sh @@ -176,7 +176,7 @@ fi # Run second test only if the first one succeeded echo "Running happy_path_mint_wallet test" -run_test happy_path_mint_wallet -- --nocapture +run_test happy_path_mint_wallet -- --nocapture --test-threads 1 status2=$? # Exit if the second test failed diff --git a/misc/itests.sh b/misc/itests.sh index 08f07d24f7..a6e83a5a28 100755 --- a/misc/itests.sh +++ b/misc/itests.sh @@ -209,6 +209,33 @@ if [[ "$SUITE" != "onchain" ]]; then done fi +READY_FILE_PATH="$CDK_ITESTS_DIR/.ready" +max_wait=300 +wait_count=0 +while [ $wait_count -lt $max_wait ]; do + if [ -f "$READY_FILE_PATH" ]; then + echo "Regtest mints readiness file found at: $READY_FILE_PATH" + break + fi + if ! ps -p "$CDK_REGTEST_PID" > /dev/null 2>&1; then + echo "ERROR: Regtest mints process exited before readiness file was created" + if wait "$CDK_REGTEST_PID"; then + exit 1 + else + status=$? + echo "Regtest mints exited with status $status" + exit "$status" + fi + fi + wait_count=$((wait_count + 1)) + sleep 1 +done + +if [ ! -f "$READY_FILE_PATH" ]; then + echo "ERROR: Regtest mints did not become ready within $max_wait seconds" + exit 1 +fi + # Run cargo test if [[ "$SUITE" == "all" || "$SUITE" == "ln" ]]; then echo "Running regtest test with CLN mint and CLN client" @@ -220,7 +247,7 @@ if [[ "$SUITE" == "all" || "$SUITE" == "ln" ]]; then fi echo "Running happy_path_mint_wallet test with CLN mint and CLN client" - run_test happy_path_mint_wallet + run_test happy_path_mint_wallet -- --test-threads 1 if [ $? -ne 0 ]; then echo "happy_path_mint_wallet with cln mint test failed, exiting" exit 1 @@ -271,7 +298,7 @@ if [[ "$SUITE" == "all" || "$SUITE" == "ln" ]]; then fi echo "Running happy_path_mint_wallet test with LND mint and LND client" - run_test happy_path_mint_wallet + run_test happy_path_mint_wallet -- --test-threads 1 if [ $? -ne 0 ]; then echo "happy_path_mint_wallet test with LND mint failed, exiting" exit 1 @@ -326,7 +353,7 @@ fi if [[ "$SUITE" == "all" || "$SUITE" == "ln" ]]; then echo "Running happy_path_mint_wallet test with LDK mint and CLN client" export CDK_TEST_LIGHTNING_CLIENT="cln" # Use CLN client for LDK tests - run_test happy_path_mint_wallet + run_test happy_path_mint_wallet -- --test-threads 1 if [ $? -ne 0 ]; then echo "happy_path_mint_wallet test with LDK mint failed, exiting" exit 1 diff --git a/misc/mintd_payment_processor.sh b/misc/mintd_payment_processor.sh index b4c848bd02..01b8eb20d1 100755 --- a/misc/mintd_payment_processor.sh +++ b/misc/mintd_payment_processor.sh @@ -255,7 +255,7 @@ while true; do done -run_test happy_path_mint_wallet +run_test happy_path_mint_wallet -- --test-threads 1 # Capture the exit status of cargo test test_status=$?