mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-18 21:25:09 +01:00
Cdk ldk node (#904)
* feat: add LDK Node Lightning backend with comprehensive integration - Add new cdk-ldk-node crate implementing Lightning backend using LDK Node - Extend MintPayment trait with start/stop methods for processor lifecycle management - Add LDK Node configuration support to cdk-mintd with chain source and gossip options - Enhance mint startup/shutdown to properly manage payment processor lifecycle --------- Co-authored-by: Erik <78821053+swedishfrenchpress@users.noreply.github.com>
This commit is contained in:
@@ -23,12 +23,13 @@ cashu = { workspace = true, features = ["mint", "wallet"] }
|
||||
cdk = { workspace = true, features = ["mint", "wallet", "auth"] }
|
||||
cdk-cln = { workspace = true }
|
||||
cdk-lnd = { workspace = true }
|
||||
cdk-ldk-node = { workspace = true }
|
||||
cdk-axum = { workspace = true, features = ["auth"] }
|
||||
cdk-sqlite = { workspace = true }
|
||||
cdk-redb = { workspace = true }
|
||||
cdk-fake-wallet = { workspace = true }
|
||||
cdk-common = { workspace = true, features = ["mint", "wallet", "auth"] }
|
||||
cdk-mintd = { workspace = true, features = ["cln", "lnd", "fakewallet", "grpc-processor", "auth", "lnbits", "management-rpc", "sqlite", "postgres"] }
|
||||
cdk-mintd = { workspace = true, features = ["cln", "lnd", "fakewallet", "grpc-processor", "auth", "lnbits", "management-rpc", "sqlite", "postgres", "ldk-node"] }
|
||||
futures = { workspace = true, default-features = false, features = [
|
||||
"executor",
|
||||
] }
|
||||
@@ -39,11 +40,13 @@ serde_json.workspace = true
|
||||
# ln-regtest-rs = { path = "../../../../ln-regtest-rs" }
|
||||
ln-regtest-rs = { git = "https://github.com/thesimplekid/ln-regtest-rs", rev = "df81424" }
|
||||
lightning-invoice.workspace = true
|
||||
ldk-node.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tokio-tungstenite.workspace = true
|
||||
tower-http = { workspace = true, features = ["cors"] }
|
||||
tower-service = "0.3.3"
|
||||
tokio-util.workspace = true
|
||||
reqwest.workspace = true
|
||||
bitcoin = "0.32.0"
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -20,6 +20,7 @@ use cdk_integration_tests::shared;
|
||||
use cdk_mintd::config::AuthType;
|
||||
use clap::Parser;
|
||||
use tokio::sync::Notify;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "start-fake-auth-mint")]
|
||||
@@ -100,7 +101,8 @@ async fn start_fake_auth_mint(
|
||||
println!("Fake auth mint shutdown signal received");
|
||||
};
|
||||
|
||||
match cdk_mintd::run_mintd_with_shutdown(&temp_dir, &settings, shutdown_future, None).await
|
||||
match cdk_mintd::run_mintd_with_shutdown(&temp_dir, &settings, shutdown_future, None, None)
|
||||
.await
|
||||
{
|
||||
Ok(_) => println!("Fake auth mint exited normally"),
|
||||
Err(e) => eprintln!("Fake auth mint exited with error: {e}"),
|
||||
@@ -132,8 +134,10 @@ async fn main() -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let cancel_token = Arc::new(CancellationToken::new());
|
||||
|
||||
// Wait for fake auth mint to be ready
|
||||
if let Err(e) = shared::wait_for_mint_ready(args.port, 100).await {
|
||||
if let Err(e) = shared::wait_for_mint_ready_with_shutdown(args.port, 100, cancel_token).await {
|
||||
eprintln!("Error waiting for fake auth mint: {e}");
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use cdk_integration_tests::cli::CommonArgs;
|
||||
use cdk_integration_tests::shared;
|
||||
use clap::Parser;
|
||||
use tokio::sync::Notify;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "start-fake-mint")]
|
||||
@@ -99,7 +100,8 @@ async fn start_fake_mint(
|
||||
println!("Fake mint shutdown signal received");
|
||||
};
|
||||
|
||||
match cdk_mintd::run_mintd_with_shutdown(&temp_dir, &settings, shutdown_future, None).await
|
||||
match cdk_mintd::run_mintd_with_shutdown(&temp_dir, &settings, shutdown_future, None, None)
|
||||
.await
|
||||
{
|
||||
Ok(_) => println!("Fake mint exited normally"),
|
||||
Err(e) => eprintln!("Fake mint exited with error: {e}"),
|
||||
@@ -141,8 +143,10 @@ async fn main() -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let cancel_token = Arc::new(CancellationToken::new());
|
||||
|
||||
// Wait for fake mint to be ready
|
||||
if let Err(e) = shared::wait_for_mint_ready(args.port, 100).await {
|
||||
if let Err(e) = shared::wait_for_mint_ready_with_shutdown(args.port, 100, cancel_token).await {
|
||||
eprintln!("Error waiting for fake mint: {e}");
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,12 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use cashu::Amount;
|
||||
use cdk_integration_tests::cli::{init_logging, CommonArgs};
|
||||
use cdk_integration_tests::init_regtest::start_regtest_end;
|
||||
use cdk_ldk_node::CdkLdkNode;
|
||||
use clap::Parser;
|
||||
use ldk_node::lightning::ln::msgs::SocketAddress;
|
||||
use tokio::signal;
|
||||
use tokio::sync::{oneshot, Notify};
|
||||
use tokio::time::timeout;
|
||||
@@ -42,15 +45,40 @@ async fn main() -> Result<()> {
|
||||
init_logging(args.common.enable_logging, args.common.log_level);
|
||||
|
||||
let temp_dir = PathBuf::from_str(&args.work_dir)?;
|
||||
let temp_dir_clone = temp_dir.clone();
|
||||
|
||||
let shutdown_regtest = Arc::new(Notify::new());
|
||||
let shutdown_clone = Arc::clone(&shutdown_regtest);
|
||||
let shutdown_clone_two = Arc::clone(&shutdown_regtest);
|
||||
|
||||
let ldk_work_dir = temp_dir.join("ldk_mint");
|
||||
let cdk_ldk = CdkLdkNode::new(
|
||||
bitcoin::Network::Regtest,
|
||||
cdk_ldk_node::ChainSource::BitcoinRpc(cdk_ldk_node::BitcoinRpcConfig {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 18443,
|
||||
user: "testuser".to_string(),
|
||||
password: "testpass".to_string(),
|
||||
}),
|
||||
cdk_ldk_node::GossipSource::P2P,
|
||||
ldk_work_dir.to_string_lossy().to_string(),
|
||||
cdk_common::common::FeeReserve {
|
||||
min_fee_reserve: Amount::ZERO,
|
||||
percent_fee_reserve: 0.0,
|
||||
},
|
||||
vec![SocketAddress::TcpIpV4 {
|
||||
addr: [127, 0, 0, 1],
|
||||
port: 8092,
|
||||
}],
|
||||
None,
|
||||
)?;
|
||||
|
||||
let inner_node = cdk_ldk.node();
|
||||
|
||||
let temp_dir_clone = temp_dir.clone();
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
tokio::spawn(async move {
|
||||
start_regtest_end(&temp_dir_clone, tx, shutdown_clone)
|
||||
start_regtest_end(&temp_dir_clone, tx, shutdown_clone, Some(inner_node))
|
||||
.await
|
||||
.expect("Error starting regtest");
|
||||
});
|
||||
|
||||
@@ -16,15 +16,22 @@ use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{bail, Result};
|
||||
use bip39::Mnemonic;
|
||||
use cashu::Amount;
|
||||
use cdk_integration_tests::cli::CommonArgs;
|
||||
use cdk_integration_tests::init_regtest::start_regtest_end;
|
||||
use cdk_integration_tests::shared;
|
||||
use cdk_ldk_node::CdkLdkNode;
|
||||
use cdk_mintd::config::LoggingConfig;
|
||||
use clap::Parser;
|
||||
use ldk_node::lightning::ln::msgs::SocketAddress;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::signal;
|
||||
use tokio::signal::unix::SignalKind;
|
||||
use tokio::signal::{self};
|
||||
use tokio::sync::{oneshot, Notify};
|
||||
use tokio::time::timeout;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "start-regtest-mints")]
|
||||
@@ -50,6 +57,10 @@ struct Args {
|
||||
/// LND port (default: 8087)
|
||||
#[arg(default_value_t = 8087)]
|
||||
lnd_port: u16,
|
||||
|
||||
/// LDK port (default: 8089)
|
||||
#[arg(default_value_t = 8089)]
|
||||
ldk_port: u16,
|
||||
}
|
||||
|
||||
/// Start regtest CLN mint using the library
|
||||
@@ -96,7 +107,8 @@ async fn start_cln_mint(
|
||||
println!("CLN mint shutdown signal received");
|
||||
};
|
||||
|
||||
match cdk_mintd::run_mintd_with_shutdown(&temp_dir, &settings, shutdown_future, None).await
|
||||
match cdk_mintd::run_mintd_with_shutdown(&temp_dir, &settings, shutdown_future, None, None)
|
||||
.await
|
||||
{
|
||||
Ok(_) => println!("CLN mint exited normally"),
|
||||
Err(e) => eprintln!("CLN mint exited with error: {e}"),
|
||||
@@ -154,8 +166,14 @@ async fn start_lnd_mint(
|
||||
println!("LND mint shutdown signal received");
|
||||
};
|
||||
|
||||
match cdk_mintd::run_mintd_with_shutdown(&lnd_work_dir, &settings, shutdown_future, None)
|
||||
.await
|
||||
match cdk_mintd::run_mintd_with_shutdown(
|
||||
&lnd_work_dir,
|
||||
&settings,
|
||||
shutdown_future,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => println!("LND mint exited normally"),
|
||||
Err(e) => eprintln!("LND mint exited with error: {e}"),
|
||||
@@ -165,141 +183,336 @@ async fn start_lnd_mint(
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
/// Start regtest LDK mint using the library
|
||||
async fn start_ldk_mint(
|
||||
temp_dir: &Path,
|
||||
port: u16,
|
||||
shutdown: Arc<Notify>,
|
||||
runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
||||
) -> Result<tokio::task::JoinHandle<()>> {
|
||||
let ldk_work_dir = temp_dir.join("ldk_mint");
|
||||
|
||||
// Initialize logging based on CLI arguments
|
||||
shared::setup_logging(&args.common);
|
||||
// Create work directory for LDK mint
|
||||
fs::create_dir_all(&ldk_work_dir)?;
|
||||
|
||||
let temp_dir = shared::init_working_directory(&args.work_dir)?;
|
||||
// Configure LDK node for regtest
|
||||
let ldk_config = cdk_mintd::config::LdkNode {
|
||||
fee_percent: 0.0,
|
||||
reserve_fee_min: 0.into(),
|
||||
bitcoin_network: Some("regtest".to_string()),
|
||||
// Use bitcoind RPC for regtest
|
||||
chain_source_type: Some("bitcoinrpc".to_string()),
|
||||
bitcoind_rpc_host: Some("127.0.0.1".to_string()),
|
||||
bitcoind_rpc_port: Some(18443),
|
||||
bitcoind_rpc_user: Some("testuser".to_string()),
|
||||
bitcoind_rpc_password: Some("testpass".to_string()),
|
||||
esplora_url: 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
|
||||
gossip_source_type: None,
|
||||
rgs_url: None,
|
||||
webserver_host: Some("127.0.0.1".to_string()),
|
||||
webserver_port: Some(port + 1), // Use next port for web interface
|
||||
};
|
||||
|
||||
// Write environment variables to a .env file in the temp_dir
|
||||
let mint_url_1 = format!("http://{}:{}", args.mint_addr, args.cln_port);
|
||||
let mint_url_2 = format!("http://{}:{}", args.mint_addr, args.lnd_port);
|
||||
let env_vars: Vec<(&str, &str)> = vec![
|
||||
("CDK_TEST_MINT_URL", &mint_url_1),
|
||||
("CDK_TEST_MINT_URL_2", &mint_url_2),
|
||||
];
|
||||
// Create settings struct for LDK mint using a new shared function
|
||||
let settings = create_ldk_settings(port, ldk_config, Mnemonic::generate(12)?.to_string());
|
||||
|
||||
shared::write_env_file(&temp_dir, &env_vars)?;
|
||||
println!("Starting LDK mintd on port {port}");
|
||||
|
||||
// Start regtest
|
||||
println!("Starting regtest...");
|
||||
let ldk_work_dir = ldk_work_dir.clone();
|
||||
let shutdown_clone = shutdown.clone();
|
||||
|
||||
let shutdown_regtest = shared::create_shutdown_handler();
|
||||
let shutdown_clone = shutdown_regtest.clone();
|
||||
let (tx, rx) = oneshot::channel();
|
||||
// Run the mint in a separate task
|
||||
let handle = tokio::spawn(async move {
|
||||
// Create a future that resolves when the shutdown signal is received
|
||||
let shutdown_future = async move {
|
||||
shutdown_clone.notified().await;
|
||||
println!("LDK mint shutdown signal received");
|
||||
};
|
||||
|
||||
let shutdown_clone_one = Arc::clone(&shutdown_clone);
|
||||
|
||||
let temp_dir_clone = temp_dir.clone();
|
||||
tokio::spawn(async move {
|
||||
start_regtest_end(&temp_dir_clone, tx, shutdown_clone_one)
|
||||
.await
|
||||
.expect("Error starting regtest");
|
||||
match cdk_mintd::run_mintd_with_shutdown(
|
||||
&ldk_work_dir,
|
||||
&settings,
|
||||
shutdown_future,
|
||||
None,
|
||||
runtime,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => println!("LDK mint exited normally"),
|
||||
Err(e) => eprintln!("LDK mint exited with error: {e}"),
|
||||
}
|
||||
});
|
||||
|
||||
match timeout(Duration::from_secs(300), rx).await {
|
||||
Ok(_) => {
|
||||
tracing::info!("Regtest set up");
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::error!("regtest setup timed out after 5 minutes");
|
||||
anyhow::bail!("Could not set up regtest");
|
||||
}
|
||||
}
|
||||
|
||||
// Start CLN mint
|
||||
let cln_handle = start_cln_mint(&temp_dir, args.cln_port, shutdown_clone.clone()).await?;
|
||||
|
||||
// Wait for CLN mint to be ready
|
||||
if let Err(e) = shared::wait_for_mint_ready(args.cln_port, 100).await {
|
||||
eprintln!("Error waiting for CLN mint: {e}");
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// Start LND mint
|
||||
let lnd_handle = start_lnd_mint(&temp_dir, args.lnd_port, shutdown_clone.clone()).await?;
|
||||
|
||||
// Wait for LND mint to be ready
|
||||
if let Err(e) = shared::wait_for_mint_ready(args.lnd_port, 100).await {
|
||||
eprintln!("Error waiting for LND mint: {e}");
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
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);
|
||||
shared::display_mint_info(args.cln_port, &temp_dir, &args.database_type); // Using CLN port for display
|
||||
println!();
|
||||
println!("Environment variables set:");
|
||||
println!(
|
||||
" CDK_TEST_MINT_URL=http://{}:{}",
|
||||
args.mint_addr, args.cln_port
|
||||
);
|
||||
println!(
|
||||
" CDK_TEST_MINT_URL_2=http://{}:{}",
|
||||
args.mint_addr, args.lnd_port
|
||||
);
|
||||
println!(" CDK_ITESTS_DIR={}", temp_dir.display());
|
||||
println!();
|
||||
println!("You can now run integration tests with:");
|
||||
println!(" cargo test -p cdk-integration-tests --test regtest");
|
||||
println!(" cargo test -p cdk-integration-tests --test happy_path_mint_wallet");
|
||||
println!(" etc.");
|
||||
println!();
|
||||
|
||||
println!("Press Ctrl+C to stop the mints...");
|
||||
|
||||
// Create a future to wait for either Ctrl+C signal or unexpected mint termination
|
||||
let shutdown_future = async {
|
||||
// Wait for either SIGINT (Ctrl+C) or SIGTERM
|
||||
let mut sigterm = signal::unix::signal(SignalKind::terminate())
|
||||
.expect("Failed to create SIGTERM signal handler");
|
||||
tokio::select! {
|
||||
_ = signal::ctrl_c() => {
|
||||
tracing::info!("Received SIGINT (Ctrl+C), shutting down mints...");
|
||||
}
|
||||
_ = sigterm.recv() => {
|
||||
tracing::info!("Received SIGTERM, shutting down mints...");
|
||||
}
|
||||
}
|
||||
println!("\nShutdown signal received, shutting down mints...");
|
||||
shutdown_clone.notify_waiters();
|
||||
};
|
||||
|
||||
// Monitor mint handles for unexpected termination
|
||||
let monitor_mints = async {
|
||||
loop {
|
||||
if cln_handle.is_finished() {
|
||||
println!("CLN mint finished unexpectedly");
|
||||
return;
|
||||
}
|
||||
if lnd_handle.is_finished() {
|
||||
println!("LND mint finished unexpectedly");
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
};
|
||||
|
||||
// Wait for either shutdown signal or mint termination
|
||||
tokio::select! {
|
||||
_ = shutdown_future => {
|
||||
println!("Shutdown signal received, waiting for mints to stop...");
|
||||
}
|
||||
_ = monitor_mints => {
|
||||
println!("One or more mints terminated unexpectedly");
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for mints to finish gracefully
|
||||
if let Err(e) = tokio::try_join!(cln_handle, lnd_handle) {
|
||||
eprintln!("Error waiting for mints to shut down: {e}");
|
||||
}
|
||||
|
||||
println!("All services shut down successfully");
|
||||
|
||||
Ok(())
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
/// Create settings for an LDK mint
|
||||
fn create_ldk_settings(
|
||||
port: u16,
|
||||
ldk_config: cdk_mintd::config::LdkNode,
|
||||
mnemonic: String,
|
||||
) -> cdk_mintd::config::Settings {
|
||||
cdk_mintd::config::Settings {
|
||||
info: cdk_mintd::config::Info {
|
||||
url: format!("http://127.0.0.1:{port}"),
|
||||
listen_host: "127.0.0.1".to_string(),
|
||||
listen_port: port,
|
||||
mnemonic: Some(mnemonic),
|
||||
signatory_url: None,
|
||||
signatory_certs: None,
|
||||
input_fee_ppk: None,
|
||||
http_cache: cdk_axum::cache::Config::default(),
|
||||
enable_swagger_ui: None,
|
||||
logging: LoggingConfig::default(),
|
||||
},
|
||||
mint_info: cdk_mintd::config::MintInfo::default(),
|
||||
ln: cdk_mintd::config::Ln {
|
||||
ln_backend: cdk_mintd::config::LnBackend::LdkNode,
|
||||
invoice_description: None,
|
||||
min_mint: 1.into(),
|
||||
max_mint: 500_000.into(),
|
||||
min_melt: 1.into(),
|
||||
max_melt: 500_000.into(),
|
||||
},
|
||||
cln: None,
|
||||
lnbits: None,
|
||||
lnd: None,
|
||||
ldk_node: Some(ldk_config),
|
||||
fake_wallet: None,
|
||||
grpc_processor: None,
|
||||
database: cdk_mintd::config::Database::default(),
|
||||
mint_management_rpc: None,
|
||||
auth: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let rt = Arc::new(Runtime::new()?);
|
||||
|
||||
let rt_clone = Arc::clone(&rt);
|
||||
|
||||
rt.block_on(async {
|
||||
let args = Args::parse();
|
||||
|
||||
// Initialize logging based on CLI arguments
|
||||
shared::setup_logging(&args.common);
|
||||
|
||||
let temp_dir = shared::init_working_directory(&args.work_dir)?;
|
||||
|
||||
// Write environment variables to a .env file in the temp_dir
|
||||
let mint_url_1 = format!("http://{}:{}", args.mint_addr, args.cln_port);
|
||||
let mint_url_2 = format!("http://{}:{}", args.mint_addr, args.lnd_port);
|
||||
let mint_url_3 = format!("http://{}:{}", args.mint_addr, args.ldk_port);
|
||||
let env_vars: Vec<(&str, &str)> = vec![
|
||||
("CDK_TEST_MINT_URL", &mint_url_1),
|
||||
("CDK_TEST_MINT_URL_2", &mint_url_2),
|
||||
("CDK_TEST_MINT_URL_3", &mint_url_3),
|
||||
];
|
||||
|
||||
shared::write_env_file(&temp_dir, &env_vars)?;
|
||||
|
||||
// Start regtest
|
||||
println!("Starting regtest...");
|
||||
|
||||
let shutdown_regtest = shared::create_shutdown_handler();
|
||||
let shutdown_clone = shutdown_regtest.clone();
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
let shutdown_clone_one = Arc::clone(&shutdown_clone);
|
||||
|
||||
let ldk_work_dir = temp_dir.join("ldk_mint");
|
||||
let cdk_ldk = CdkLdkNode::new(
|
||||
bitcoin::Network::Regtest,
|
||||
cdk_ldk_node::ChainSource::BitcoinRpc(cdk_ldk_node::BitcoinRpcConfig {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 18443,
|
||||
user: "testuser".to_string(),
|
||||
password: "testpass".to_string(),
|
||||
}),
|
||||
cdk_ldk_node::GossipSource::P2P,
|
||||
ldk_work_dir.to_string_lossy().to_string(),
|
||||
cdk_common::common::FeeReserve {
|
||||
min_fee_reserve: Amount::ZERO,
|
||||
percent_fee_reserve: 0.0,
|
||||
},
|
||||
vec![SocketAddress::TcpIpV4 {
|
||||
addr: [127, 0, 0, 1],
|
||||
port: 8092,
|
||||
}],
|
||||
Some(Arc::clone(&rt_clone)),
|
||||
)?;
|
||||
|
||||
let inner_node = cdk_ldk.node();
|
||||
|
||||
let temp_dir_clone = temp_dir.clone();
|
||||
let shutdown_clone_two = Arc::clone(&shutdown_clone);
|
||||
tokio::spawn(async move {
|
||||
start_regtest_end(&temp_dir_clone, tx, shutdown_clone_two, Some(inner_node))
|
||||
.await
|
||||
.expect("Error starting regtest");
|
||||
});
|
||||
|
||||
match timeout(Duration::from_secs(300), rx).await {
|
||||
Ok(k) => {
|
||||
k?;
|
||||
tracing::info!("Regtest set up");
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::error!("regtest setup timed out after 5 minutes");
|
||||
anyhow::bail!("Could not set up regtest");
|
||||
}
|
||||
}
|
||||
|
||||
println!("lnd port: {}", args.ldk_port);
|
||||
|
||||
// Start LND mint
|
||||
let lnd_handle = start_lnd_mint(&temp_dir, args.lnd_port, shutdown_clone.clone()).await?;
|
||||
|
||||
// Start LDK mint
|
||||
let ldk_handle = start_ldk_mint(
|
||||
&temp_dir,
|
||||
args.ldk_port,
|
||||
shutdown_clone.clone(),
|
||||
Some(rt_clone),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Start CLN mint
|
||||
let cln_handle = start_cln_mint(&temp_dir, args.cln_port, shutdown_clone.clone()).await?;
|
||||
|
||||
let cancel_token = Arc::new(CancellationToken::new());
|
||||
|
||||
// Set up Ctrl+C handler before waiting for mints to be ready
|
||||
let ctrl_c_token = Arc::clone(&cancel_token);
|
||||
|
||||
let s_u = shutdown_clone.clone();
|
||||
tokio::spawn(async move {
|
||||
signal::ctrl_c()
|
||||
.await
|
||||
.expect("failed to install CTRL+C handler");
|
||||
tracing::info!("Shutdown signal received during mint setup");
|
||||
println!("\nReceived Ctrl+C, shutting down...");
|
||||
ctrl_c_token.cancel();
|
||||
s_u.notify_waiters();
|
||||
});
|
||||
|
||||
match tokio::try_join!(
|
||||
shared::wait_for_mint_ready_with_shutdown(
|
||||
args.lnd_port,
|
||||
100,
|
||||
Arc::clone(&cancel_token)
|
||||
),
|
||||
shared::wait_for_mint_ready_with_shutdown(
|
||||
args.ldk_port,
|
||||
100,
|
||||
Arc::clone(&cancel_token)
|
||||
),
|
||||
shared::wait_for_mint_ready_with_shutdown(
|
||||
args.cln_port,
|
||||
100,
|
||||
Arc::clone(&cancel_token)
|
||||
),
|
||||
) {
|
||||
Ok(_) => println!("All mints are ready!"),
|
||||
Err(e) => {
|
||||
if cancel_token.is_cancelled() {
|
||||
bail!("Startup canceled by user");
|
||||
}
|
||||
eprintln!("Error waiting for mints to be ready: {e}");
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
if cancel_token.is_cancelled() {
|
||||
bail!("Token canceled");
|
||||
}
|
||||
|
||||
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);
|
||||
println!("LDK mint: http://{}:{}", args.mint_addr, args.ldk_port);
|
||||
shared::display_mint_info(args.cln_port, &temp_dir, &args.database_type); // Using CLN port for display
|
||||
println!();
|
||||
println!("Environment variables set:");
|
||||
println!(
|
||||
" CDK_TEST_MINT_URL=http://{}:{}",
|
||||
args.mint_addr, args.cln_port
|
||||
);
|
||||
println!(
|
||||
" CDK_TEST_MINT_URL_2=http://{}:{}",
|
||||
args.mint_addr, args.lnd_port
|
||||
);
|
||||
println!(
|
||||
" CDK_TEST_MINT_URL_3=http://{}:{}",
|
||||
args.mint_addr, args.ldk_port
|
||||
);
|
||||
println!(" CDK_ITESTS_DIR={}", temp_dir.display());
|
||||
println!();
|
||||
println!("You can now run integration tests with:");
|
||||
println!(" cargo test -p cdk-integration-tests --test regtest");
|
||||
println!(" cargo test -p cdk-integration-tests --test happy_path_mint_wallet");
|
||||
println!(" etc.");
|
||||
println!();
|
||||
|
||||
println!("Press Ctrl+C to stop the mints...");
|
||||
|
||||
// Create a future to wait for either Ctrl+C signal or unexpected mint termination
|
||||
let shutdown_future = async {
|
||||
// Wait for either SIGINT (Ctrl+C) or SIGTERM
|
||||
let mut sigterm = signal::unix::signal(SignalKind::terminate())
|
||||
.expect("Failed to create SIGTERM signal handler");
|
||||
tokio::select! {
|
||||
_ = signal::ctrl_c() => {
|
||||
tracing::info!("Received SIGINT (Ctrl+C), shutting down mints...");
|
||||
}
|
||||
_ = sigterm.recv() => {
|
||||
tracing::info!("Received SIGTERM, shutting down mints...");
|
||||
}
|
||||
}
|
||||
println!("\nShutdown signal received, shutting down mints...");
|
||||
shutdown_clone.notify_waiters();
|
||||
};
|
||||
|
||||
// Monitor mint handles for unexpected termination
|
||||
let monitor_mints = async {
|
||||
loop {
|
||||
if cln_handle.is_finished() {
|
||||
println!("CLN mint finished unexpectedly");
|
||||
return;
|
||||
}
|
||||
if lnd_handle.is_finished() {
|
||||
println!("LND mint finished unexpectedly");
|
||||
return;
|
||||
}
|
||||
if ldk_handle.is_finished() {
|
||||
println!("LDK mint finished unexpectedly");
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
};
|
||||
|
||||
// Wait for either shutdown signal or mint termination
|
||||
tokio::select! {
|
||||
_ = shutdown_clone_one.notified() => {
|
||||
println!("Shutdown signal received, waiting for mints to stop...");
|
||||
}
|
||||
_ = monitor_mints => {
|
||||
println!("One or more mints terminated unexpectedly");
|
||||
}
|
||||
_ = shutdown_future => ()
|
||||
}
|
||||
|
||||
// Wait for mints to finish gracefully
|
||||
if let Err(e) = tokio::try_join!(ldk_handle, cln_handle, lnd_handle) {
|
||||
eprintln!("Error waiting for mints to shut down: {e}");
|
||||
}
|
||||
|
||||
println!("All services shut down successfully");
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ pub fn init_logging(enable_logging: bool, log_level: tracing::Level) {
|
||||
let default_filter = log_level.to_string();
|
||||
|
||||
// Common filters to reduce noise
|
||||
let sqlx_filter = "sqlx=warn";
|
||||
let hyper_filter = "hyper=warn";
|
||||
let h2_filter = "h2=warn";
|
||||
let rustls_filter = "rustls=warn";
|
||||
@@ -32,7 +31,7 @@ pub fn init_logging(enable_logging: bool, log_level: tracing::Level) {
|
||||
let tower_filter = "tower_http=warn";
|
||||
|
||||
let env_filter = EnvFilter::new(format!(
|
||||
"{default_filter},{sqlx_filter},{hyper_filter},{h2_filter},{rustls_filter},{reqwest_filter},{tower_filter}"
|
||||
"{default_filter},{hyper_filter},{h2_filter},{rustls_filter},{reqwest_filter},{tower_filter}"
|
||||
));
|
||||
|
||||
// Ok if successful, Err if already initialized
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::env;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -6,6 +7,8 @@ use anyhow::Result;
|
||||
use cdk::types::FeeReserve;
|
||||
use cdk_cln::Cln as CdkCln;
|
||||
use cdk_lnd::Lnd as CdkLnd;
|
||||
use ldk_node::lightning::ln::msgs::SocketAddress;
|
||||
use ldk_node::Node;
|
||||
use ln_regtest_rs::bitcoin_client::BitcoinClient;
|
||||
use ln_regtest_rs::bitcoind::Bitcoind;
|
||||
use ln_regtest_rs::cln::Clnd;
|
||||
@@ -145,6 +148,9 @@ pub async fn init_lnd(
|
||||
|
||||
pub fn generate_block(bitcoin_client: &BitcoinClient) -> Result<()> {
|
||||
let mine_to_address = bitcoin_client.get_new_address()?;
|
||||
let blocks = 10;
|
||||
tracing::info!("Mining {blocks} blocks to {mine_to_address}");
|
||||
|
||||
bitcoin_client.generate_blocks(&mine_to_address, 10)?;
|
||||
|
||||
Ok(())
|
||||
@@ -225,6 +231,7 @@ pub async fn start_regtest_end(
|
||||
work_dir: &Path,
|
||||
sender: Sender<()>,
|
||||
notify: Arc<Notify>,
|
||||
ldk_node: Option<Arc<Node>>,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut bitcoind = init_bitcoind(work_dir);
|
||||
bitcoind.start_bitcoind()?;
|
||||
@@ -285,6 +292,13 @@ pub async fn start_regtest_end(
|
||||
|
||||
lnd_client.wait_chain_sync().await.unwrap();
|
||||
|
||||
if let Some(node) = ldk_node.as_ref() {
|
||||
tracing::info!("Starting ldk node");
|
||||
node.start()?;
|
||||
let addr = node.onchain_payment().new_address().unwrap();
|
||||
bitcoin_client.send_to_address(&addr.to_string(), 5_000_000)?;
|
||||
}
|
||||
|
||||
fund_ln(&bitcoin_client, &lnd_client).await.unwrap();
|
||||
|
||||
// create second lnd node
|
||||
@@ -336,12 +350,108 @@ pub async fn start_regtest_end(
|
||||
tracing::info!("Opened channel between cln and lnd two");
|
||||
generate_block(&bitcoin_client)?;
|
||||
|
||||
cln_client.wait_channels_active().await?;
|
||||
cln_two_client.wait_channels_active().await?;
|
||||
lnd_client.wait_channels_active().await?;
|
||||
lnd_two_client.wait_channels_active().await?;
|
||||
if let Some(node) = ldk_node {
|
||||
let pubkey = node.node_id();
|
||||
let listen_addr = node.listening_addresses();
|
||||
let listen_addr = listen_addr.as_ref().unwrap().first().unwrap();
|
||||
|
||||
let (listen_addr, port) = match listen_addr {
|
||||
SocketAddress::TcpIpV4 { addr, port } => (Ipv4Addr::from(*addr).to_string(), port),
|
||||
_ => panic!(),
|
||||
};
|
||||
|
||||
tracing::info!("Opening channel from cln to ldk");
|
||||
|
||||
cln_client
|
||||
.connect_peer(pubkey.to_string(), listen_addr.clone(), *port)
|
||||
.await?;
|
||||
|
||||
cln_client
|
||||
.open_channel(1_500_000, &pubkey.to_string(), Some(750_000))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
generate_block(&bitcoin_client)?;
|
||||
|
||||
let cln_two_info = cln_two_client.get_connect_info().await?;
|
||||
|
||||
cln_client
|
||||
.connect_peer(cln_two_info.pubkey, listen_addr.clone(), cln_two_info.port)
|
||||
.await?;
|
||||
|
||||
tracing::info!("Opening channel from lnd to ldk");
|
||||
|
||||
let cln_info = cln_client.get_connect_info().await?;
|
||||
|
||||
node.connect(
|
||||
cln_info.pubkey.parse()?,
|
||||
SocketAddress::TcpIpV4 {
|
||||
addr: cln_info
|
||||
.address
|
||||
.split('.')
|
||||
.map(|part| part.parse())
|
||||
.collect::<Result<Vec<u8>, _>>()?
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
port: cln_info.port,
|
||||
},
|
||||
true,
|
||||
)?;
|
||||
|
||||
let lnd_info = lnd_client.get_connect_info().await?;
|
||||
|
||||
node.connect(
|
||||
lnd_info.pubkey.parse()?,
|
||||
SocketAddress::TcpIpV4 {
|
||||
addr: [127, 0, 0, 1],
|
||||
port: lnd_info.port,
|
||||
},
|
||||
true,
|
||||
)?;
|
||||
|
||||
// lnd_client
|
||||
// .open_channel(1_500_000, &pubkey.to_string(), Some(750_000))
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
generate_block(&bitcoin_client)?;
|
||||
lnd_client.wait_chain_sync().await?;
|
||||
|
||||
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,
|
||||
)?;
|
||||
|
||||
generate_block(&bitcoin_client)?;
|
||||
|
||||
tracing::info!("Ldk channels opened");
|
||||
|
||||
node.sync_wallets()?;
|
||||
|
||||
tracing::info!("Ldk wallet synced");
|
||||
|
||||
cln_client.wait_channels_active().await?;
|
||||
|
||||
lnd_client.wait_channels_active().await?;
|
||||
|
||||
node.stop()?;
|
||||
} else {
|
||||
cln_client.wait_channels_active().await?;
|
||||
|
||||
lnd_client.wait_channels_active().await?;
|
||||
|
||||
generate_block(&bitcoin_client)?;
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("Regtest channels active");
|
||||
|
||||
// Send notification that regtest set up is complete
|
||||
sender.send(()).expect("Could not send oneshot");
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
//! - Proof state management utilities
|
||||
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
@@ -27,9 +27,11 @@ use cdk::nuts::State;
|
||||
use cdk::Wallet;
|
||||
use cdk_fake_wallet::create_fake_invoice;
|
||||
use init_regtest::{get_lnd_dir, LND_RPC_ADDR};
|
||||
use ln_regtest_rs::ln_client::{LightningClient, LndClient};
|
||||
use ln_regtest_rs::ln_client::{ClnClient, LightningClient, LndClient};
|
||||
use tokio::time::Duration;
|
||||
|
||||
use crate::init_regtest::get_cln_dir;
|
||||
|
||||
pub mod cli;
|
||||
pub mod init_auth_mint;
|
||||
pub mod init_pure_tests;
|
||||
@@ -118,11 +120,18 @@ pub async fn init_lnd_client(work_dir: &Path) -> LndClient {
|
||||
///
|
||||
/// This is useful for tests that need to pay invoices in regtest mode but
|
||||
/// should be skipped in other environments.
|
||||
pub async fn pay_if_regtest(work_dir: &Path, invoice: &Bolt11Invoice) -> Result<()> {
|
||||
pub async fn pay_if_regtest(_work_dir: &Path, invoice: &Bolt11Invoice) -> Result<()> {
|
||||
// Check if the invoice is for the regtest network
|
||||
if invoice.network() == bitcoin::Network::Regtest {
|
||||
let lnd_client = init_lnd_client(work_dir).await;
|
||||
lnd_client.pay_invoice(invoice.to_string()).await?;
|
||||
let client = get_test_client().await;
|
||||
let mut tries = 0;
|
||||
while let Err(err) = client.pay_invoice(invoice.to_string()).await {
|
||||
println!("Could not pay invoice.retrying {err}");
|
||||
tries += 1;
|
||||
if tries > 10 {
|
||||
bail!("Could not pay invoice");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
// Not a regtest invoice, just return Ok
|
||||
@@ -149,11 +158,10 @@ pub fn is_regtest_env() -> bool {
|
||||
///
|
||||
/// Uses the is_regtest_env() function to determine whether to
|
||||
/// create a real regtest invoice or a fake one for testing.
|
||||
pub async fn create_invoice_for_env(work_dir: &Path, amount_sat: Option<u64>) -> Result<String> {
|
||||
pub async fn create_invoice_for_env(amount_sat: Option<u64>) -> Result<String> {
|
||||
if is_regtest_env() {
|
||||
// In regtest mode, create a real invoice
|
||||
let lnd_client = init_lnd_client(work_dir).await;
|
||||
lnd_client
|
||||
let client = get_test_client().await;
|
||||
client
|
||||
.create_invoice(amount_sat)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to create regtest invoice: {}", e))
|
||||
@@ -166,3 +174,80 @@ pub async fn create_invoice_for_env(work_dir: &Path, amount_sat: Option<u64>) ->
|
||||
Ok(fake_invoice.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// This is the ln wallet we use to send/receive ln payements as the wallet
|
||||
async fn _get_lnd_client() -> LndClient {
|
||||
let temp_dir = get_work_dir();
|
||||
|
||||
// The LND mint uses the second LND node (LND_TWO_RPC_ADDR = localhost:10010)
|
||||
let lnd_dir = get_lnd_dir(&temp_dir, "one");
|
||||
let cert_file = lnd_dir.join("tls.cert");
|
||||
let macaroon_file = lnd_dir.join("data/chain/bitcoin/regtest/admin.macaroon");
|
||||
|
||||
println!("Looking for LND cert file: {cert_file:?}");
|
||||
println!("Looking for LND macaroon file: {macaroon_file:?}");
|
||||
println!("Connecting to LND at: https://{LND_RPC_ADDR}");
|
||||
|
||||
// Connect to LND
|
||||
LndClient::new(
|
||||
format!("https://{LND_RPC_ADDR}"),
|
||||
cert_file.clone(),
|
||||
macaroon_file.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("Could not connect to lnd rpc")
|
||||
}
|
||||
|
||||
/// Returns a Lightning client based on the CDK_TEST_LIGHTNING_CLIENT environment variable.
|
||||
///
|
||||
/// Reads the CDK_TEST_LIGHTNING_CLIENT environment variable:
|
||||
/// - "cln" or "CLN": returns a CLN client
|
||||
/// - Anything else (or unset): returns an LND client (default)
|
||||
pub async fn get_test_client() -> Box<dyn LightningClient> {
|
||||
match env::var("CDK_TEST_LIGHTNING_CLIENT") {
|
||||
Ok(val) => {
|
||||
let val = val.to_lowercase();
|
||||
match val.as_str() {
|
||||
"cln" => Box::new(create_cln_client_with_retry().await),
|
||||
_ => Box::new(_get_lnd_client().await),
|
||||
}
|
||||
}
|
||||
Err(_) => Box::new(_get_lnd_client().await), // Default to LND
|
||||
}
|
||||
}
|
||||
|
||||
fn get_work_dir() -> PathBuf {
|
||||
match env::var("CDK_ITESTS_DIR") {
|
||||
Ok(dir) => {
|
||||
let path = PathBuf::from(dir);
|
||||
println!("Using temp directory from CDK_ITESTS_DIR: {path:?}");
|
||||
path
|
||||
}
|
||||
Err(_) => {
|
||||
panic!("Unknown temp dir");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create CLN client with retries
|
||||
async fn create_cln_client_with_retry() -> ClnClient {
|
||||
let mut retries = 0;
|
||||
let max_retries = 10;
|
||||
|
||||
let cln_dir = get_cln_dir(&get_work_dir(), "one");
|
||||
loop {
|
||||
match ClnClient::new(cln_dir.clone(), None).await {
|
||||
Ok(client) => return client,
|
||||
Err(e) => {
|
||||
retries += 1;
|
||||
if retries >= max_retries {
|
||||
panic!("Could not connect to CLN client after {max_retries} retries: {e}");
|
||||
}
|
||||
println!(
|
||||
"Failed to connect to CLN (attempt {retries}/{max_retries}): {e}. Retrying in 7 seconds..."
|
||||
);
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(7)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use cdk_axum::cache;
|
||||
use cdk_mintd::config::{Database, DatabaseEngine};
|
||||
use tokio::signal;
|
||||
use tokio::sync::Notify;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::cli::{init_logging, CommonArgs};
|
||||
|
||||
@@ -26,8 +27,12 @@ const DEFAULT_MIN_MELT: u64 = 1;
|
||||
/// Default maximum melt amount for test mints
|
||||
const DEFAULT_MAX_MELT: u64 = 500_000;
|
||||
|
||||
/// Wait for mint to be ready by checking its info endpoint
|
||||
pub async fn wait_for_mint_ready(port: u16, timeout_secs: u64) -> Result<()> {
|
||||
/// Wait for mint to be ready by checking its info endpoint, with optional shutdown signal
|
||||
pub async fn wait_for_mint_ready_with_shutdown(
|
||||
port: u16,
|
||||
timeout_secs: u64,
|
||||
shutdown_notify: Arc<CancellationToken>,
|
||||
) -> Result<()> {
|
||||
let url = format!("http://127.0.0.1:{port}/v1/info");
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
@@ -39,26 +44,43 @@ pub async fn wait_for_mint_ready(port: u16, timeout_secs: u64) -> Result<()> {
|
||||
return Err(anyhow::anyhow!("Timeout waiting for mint on port {}", port));
|
||||
}
|
||||
|
||||
// Try to make a request to the mint info endpoint
|
||||
match reqwest::get(&url).await {
|
||||
Ok(response) => {
|
||||
if response.status().is_success() {
|
||||
println!("Mint on port {port} is ready");
|
||||
return Ok(());
|
||||
} else {
|
||||
println!(
|
||||
"Mint on port {} returned status: {}",
|
||||
port,
|
||||
response.status()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error connecting to mint on port {port}: {e}");
|
||||
}
|
||||
if shutdown_notify.is_cancelled() {
|
||||
return Err(anyhow::anyhow!("Canceled waiting for {}", port));
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
tokio::select! {
|
||||
// Try to make a request to the mint info endpoint
|
||||
result = reqwest::get(&url) => {
|
||||
match result {
|
||||
Ok(response) => {
|
||||
if response.status().is_success() {
|
||||
println!("Mint on port {port} is ready");
|
||||
return Ok(());
|
||||
} else {
|
||||
println!(
|
||||
"Mint on port {} returned status: {}",
|
||||
port,
|
||||
response.status()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error connecting to mint on port {port}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for shutdown signal
|
||||
_ = shutdown_notify.cancelled() => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Shutdown requested while waiting for mint on port {}",
|
||||
port
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +209,7 @@ pub fn create_fake_wallet_settings(
|
||||
cln: None,
|
||||
lnbits: None,
|
||||
lnd: None,
|
||||
ldk_node: None,
|
||||
fake_wallet: fake_wallet_config,
|
||||
grpc_processor: None,
|
||||
database: Database {
|
||||
@@ -234,6 +257,7 @@ pub fn create_cln_settings(
|
||||
cln: Some(cln_config),
|
||||
lnbits: None,
|
||||
lnd: None,
|
||||
ldk_node: None,
|
||||
fake_wallet: None,
|
||||
grpc_processor: None,
|
||||
database: cdk_mintd::config::Database::default(),
|
||||
@@ -276,6 +300,7 @@ pub fn create_lnd_settings(
|
||||
},
|
||||
cln: None,
|
||||
lnbits: None,
|
||||
ldk_node: None,
|
||||
lnd: Some(lnd_config),
|
||||
fake_wallet: None,
|
||||
grpc_processor: None,
|
||||
|
||||
@@ -385,3 +385,66 @@ async fn test_regtest_bolt12_mint_extra() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn test_attempt_to_mint_unpaid() {
|
||||
let wallet = Wallet::new(
|
||||
&get_mint_url_from_env(),
|
||||
CurrencyUnit::Sat,
|
||||
Arc::new(memory::empty().await.unwrap()),
|
||||
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
|
||||
None,
|
||||
)
|
||||
.expect("failed to create new wallet");
|
||||
|
||||
let mint_amount = Amount::from(100);
|
||||
|
||||
let mint_quote = wallet
|
||||
.mint_bolt12_quote(Some(mint_amount), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(mint_quote.amount, Some(mint_amount));
|
||||
|
||||
let proofs = wallet
|
||||
.mint_bolt12(&mint_quote.id, None, SplitTarget::default(), None)
|
||||
.await;
|
||||
|
||||
match proofs {
|
||||
Err(err) => {
|
||||
if !matches!(err, cdk::Error::UnpaidQuote) {
|
||||
panic!("Wrong error quote should be unpaid: {}", err);
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
panic!("Minting should not be allowed");
|
||||
}
|
||||
}
|
||||
|
||||
let mint_quote = wallet
|
||||
.mint_bolt12_quote(Some(mint_amount), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let state = wallet
|
||||
.mint_bolt12_quote_state(&mint_quote.id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(state.amount_paid == Amount::ZERO);
|
||||
|
||||
let proofs = wallet
|
||||
.mint_bolt12(&mint_quote.id, None, SplitTarget::default(), None)
|
||||
.await;
|
||||
|
||||
match proofs {
|
||||
Err(err) => {
|
||||
if !matches!(err, cdk::Error::UnpaidQuote) {
|
||||
panic!("Wrong error quote should be unpaid: {}", err);
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
panic!("Minting should not be allowed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,9 +123,7 @@ async fn test_happy_mint_melt_round_trip() {
|
||||
|
||||
assert!(mint_amount == 100.into());
|
||||
|
||||
let invoice = create_invoice_for_env(&get_test_temp_dir(), Some(50))
|
||||
.await
|
||||
.unwrap();
|
||||
let invoice = create_invoice_for_env(Some(50)).await.unwrap();
|
||||
|
||||
let melt = wallet.melt_quote(invoice, None).await.unwrap();
|
||||
|
||||
@@ -371,9 +369,7 @@ async fn test_fake_melt_change_in_quote() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let invoice = create_invoice_for_env(&get_test_temp_dir(), Some(9))
|
||||
.await
|
||||
.unwrap();
|
||||
let invoice = create_invoice_for_env(Some(9)).await.unwrap();
|
||||
|
||||
let proofs = wallet.get_unspent_proofs().await.unwrap();
|
||||
|
||||
@@ -447,9 +443,7 @@ async fn test_pay_invoice_twice() {
|
||||
|
||||
assert_eq!(mint_amount, 100.into());
|
||||
|
||||
let invoice = create_invoice_for_env(&get_test_temp_dir(), Some(25))
|
||||
.await
|
||||
.unwrap();
|
||||
let invoice = create_invoice_for_env(Some(25)).await.unwrap();
|
||||
|
||||
let melt_quote = wallet.melt_quote(invoice.clone(), None).await.unwrap();
|
||||
|
||||
|
||||
63
crates/cdk-integration-tests/tests/ldk_node.rs
Normal file
63
crates/cdk-integration-tests/tests/ldk_node.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use anyhow::Result;
|
||||
use cdk_integration_tests::get_mint_url_from_env;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ldk_node_mint_info() -> Result<()> {
|
||||
// This test just verifies that the LDK-Node mint is running and responding
|
||||
let mint_url = get_mint_url_from_env();
|
||||
|
||||
// Create an HTTP client
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Make a request to the info endpoint
|
||||
let response = client.get(&format!("{}/v1/info", mint_url)).send().await?;
|
||||
|
||||
// Check that we got a successful response
|
||||
assert_eq!(response.status(), 200);
|
||||
|
||||
// Try to parse the response as JSON
|
||||
let info: serde_json::Value = response.json().await?;
|
||||
|
||||
// Verify that we got some basic fields
|
||||
assert!(info.get("name").is_some());
|
||||
assert!(info.get("version").is_some());
|
||||
assert!(info.get("description").is_some());
|
||||
|
||||
println!("LDK-Node mint info: {:?}", info);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ldk_node_mint_quote() -> Result<()> {
|
||||
// This test verifies that we can create a mint quote with the LDK-Node mint
|
||||
let mint_url = get_mint_url_from_env();
|
||||
|
||||
// Create an HTTP client
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Create a mint quote request
|
||||
let quote_request = serde_json::json!({
|
||||
"amount": 1000,
|
||||
"unit": "sat"
|
||||
});
|
||||
|
||||
// Make a request to create a mint quote
|
||||
let response = client
|
||||
.post(&format!("{}/v1/mint/quote/bolt11", mint_url))
|
||||
.json("e_request)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
// Print the response for debugging
|
||||
let status = response.status();
|
||||
let text = response.text().await?;
|
||||
println!("Mint quote response status: {}", status);
|
||||
println!("Mint quote response body: {}", text);
|
||||
|
||||
// For now, we'll just check that we get a response (even if it's an error)
|
||||
// In a real test, we'd want to verify the quote was created correctly
|
||||
assert!(status.is_success() || status.as_u16() < 500);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -13,8 +13,6 @@
|
||||
//! - Requires properly configured LND nodes with TLS certificates and macaroons
|
||||
//! - Uses real Bitcoin transactions in regtest mode
|
||||
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -26,49 +24,16 @@ use cdk::nuts::{
|
||||
NotificationPayload, PreMintSecrets,
|
||||
};
|
||||
use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletSubscription};
|
||||
use cdk_integration_tests::init_regtest::{get_lnd_dir, LND_RPC_ADDR};
|
||||
use cdk_integration_tests::{get_mint_url_from_env, get_second_mint_url_from_env};
|
||||
use cdk_integration_tests::{get_mint_url_from_env, get_second_mint_url_from_env, get_test_client};
|
||||
use cdk_sqlite::wallet::{self, memory};
|
||||
use futures::join;
|
||||
use ln_regtest_rs::ln_client::{LightningClient, LndClient};
|
||||
use tokio::time::timeout;
|
||||
|
||||
// This is the ln wallet we use to send/receive ln payements as the wallet
|
||||
async fn init_lnd_client() -> LndClient {
|
||||
// Try to get the temp directory from environment variable first (from .env file)
|
||||
let temp_dir = match env::var("CDK_ITESTS_DIR") {
|
||||
Ok(dir) => {
|
||||
let path = PathBuf::from(dir);
|
||||
println!("Using temp directory from CDK_ITESTS_DIR: {:?}", path);
|
||||
path
|
||||
}
|
||||
Err(_) => {
|
||||
panic!("Unknown temp dir");
|
||||
}
|
||||
};
|
||||
|
||||
// The LND mint uses the second LND node (LND_TWO_RPC_ADDR = localhost:10010)
|
||||
let lnd_dir = get_lnd_dir(&temp_dir, "one");
|
||||
let cert_file = lnd_dir.join("tls.cert");
|
||||
let macaroon_file = lnd_dir.join("data/chain/bitcoin/regtest/admin.macaroon");
|
||||
|
||||
println!("Looking for LND cert file: {:?}", cert_file);
|
||||
println!("Looking for LND macaroon file: {:?}", macaroon_file);
|
||||
println!("Connecting to LND at: https://{}", LND_RPC_ADDR);
|
||||
|
||||
// Connect to LND
|
||||
LndClient::new(
|
||||
format!("https://{}", LND_RPC_ADDR),
|
||||
cert_file.clone(),
|
||||
macaroon_file.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("Could not connect to lnd rpc")
|
||||
}
|
||||
const LDK_URL: &str = "http://127.0.0.1:8089";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn test_internal_payment() {
|
||||
let lnd_client = init_lnd_client().await;
|
||||
let ln_client = get_test_client().await;
|
||||
|
||||
let wallet = Wallet::new(
|
||||
&get_mint_url_from_env(),
|
||||
@@ -81,7 +46,7 @@ async fn test_internal_payment() {
|
||||
|
||||
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
|
||||
|
||||
lnd_client
|
||||
ln_client
|
||||
.pay_invoice(mint_quote.request.clone())
|
||||
.await
|
||||
.expect("failed to pay invoice");
|
||||
@@ -208,8 +173,8 @@ async fn test_websocket_connection() {
|
||||
_ => panic!("Unexpected notification type"),
|
||||
}
|
||||
|
||||
let lnd_client = init_lnd_client().await;
|
||||
lnd_client
|
||||
let ln_client = get_test_client().await;
|
||||
ln_client
|
||||
.pay_invoice(mint_quote.request)
|
||||
.await
|
||||
.expect("failed to pay invoice");
|
||||
@@ -231,7 +196,11 @@ async fn test_websocket_connection() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn test_multimint_melt() {
|
||||
let lnd_client = init_lnd_client().await;
|
||||
if get_mint_url_from_env() == LDK_URL {
|
||||
return;
|
||||
}
|
||||
|
||||
let ln_client = get_test_client().await;
|
||||
|
||||
let db = Arc::new(memory::empty().await.unwrap());
|
||||
let wallet1 = Wallet::new(
|
||||
@@ -257,7 +226,7 @@ async fn test_multimint_melt() {
|
||||
|
||||
// Fund the wallets
|
||||
let quote = wallet1.mint_quote(mint_amount, None).await.unwrap();
|
||||
lnd_client
|
||||
ln_client
|
||||
.pay_invoice(quote.request.clone())
|
||||
.await
|
||||
.expect("failed to pay invoice");
|
||||
@@ -271,7 +240,7 @@ async fn test_multimint_melt() {
|
||||
.unwrap();
|
||||
|
||||
let quote = wallet2.mint_quote(mint_amount, None).await.unwrap();
|
||||
lnd_client
|
||||
ln_client
|
||||
.pay_invoice(quote.request.clone())
|
||||
.await
|
||||
.expect("failed to pay invoice");
|
||||
@@ -285,7 +254,7 @@ async fn test_multimint_melt() {
|
||||
.unwrap();
|
||||
|
||||
// Get an invoice
|
||||
let invoice = lnd_client.create_invoice(Some(50)).await.unwrap();
|
||||
let invoice = ln_client.create_invoice(Some(50)).await.unwrap();
|
||||
|
||||
// Get multi-part melt quotes
|
||||
let melt_options = MeltOptions::Mpp {
|
||||
@@ -318,7 +287,7 @@ async fn test_multimint_melt() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn test_cached_mint() {
|
||||
let lnd_client = init_lnd_client().await;
|
||||
let ln_client = get_test_client().await;
|
||||
let wallet = Wallet::new(
|
||||
&get_mint_url_from_env(),
|
||||
CurrencyUnit::Sat,
|
||||
@@ -331,7 +300,7 @@ async fn test_cached_mint() {
|
||||
let mint_amount = Amount::from(100);
|
||||
|
||||
let quote = wallet.mint_quote(mint_amount, None).await.unwrap();
|
||||
lnd_client
|
||||
ln_client
|
||||
.pay_invoice(quote.request.clone())
|
||||
.await
|
||||
.expect("failed to pay invoice");
|
||||
@@ -366,7 +335,7 @@ async fn test_cached_mint() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn test_regtest_melt_amountless() {
|
||||
let lnd_client = init_lnd_client().await;
|
||||
let ln_client = get_test_client().await;
|
||||
|
||||
let wallet = Wallet::new(
|
||||
&get_mint_url_from_env(),
|
||||
@@ -383,7 +352,7 @@ async fn test_regtest_melt_amountless() {
|
||||
|
||||
assert_eq!(mint_quote.amount, Some(mint_amount));
|
||||
|
||||
lnd_client
|
||||
ln_client
|
||||
.pay_invoice(mint_quote.request)
|
||||
.await
|
||||
.expect("failed to pay invoice");
|
||||
@@ -397,7 +366,7 @@ async fn test_regtest_melt_amountless() {
|
||||
|
||||
assert!(mint_amount == amount);
|
||||
|
||||
let invoice = lnd_client.create_invoice(None).await.unwrap();
|
||||
let invoice = ln_client.create_invoice(None).await.unwrap();
|
||||
|
||||
let options = MeltOptions::new_amountless(5_000);
|
||||
|
||||
@@ -410,3 +379,57 @@ async fn test_regtest_melt_amountless() {
|
||||
|
||||
assert!(melt.amount == 5.into());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn test_attempt_to_mint_unpaid() {
|
||||
let wallet = Wallet::new(
|
||||
&get_mint_url_from_env(),
|
||||
CurrencyUnit::Sat,
|
||||
Arc::new(memory::empty().await.unwrap()),
|
||||
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
|
||||
None,
|
||||
)
|
||||
.expect("failed to create new wallet");
|
||||
|
||||
let mint_amount = Amount::from(100);
|
||||
|
||||
let mint_quote = wallet.mint_quote(mint_amount, None).await.unwrap();
|
||||
|
||||
assert_eq!(mint_quote.amount, Some(mint_amount));
|
||||
|
||||
let proofs = wallet
|
||||
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||
.await;
|
||||
|
||||
match proofs {
|
||||
Err(err) => {
|
||||
if !matches!(err, cdk::Error::UnpaidQuote) {
|
||||
panic!("Wrong error quote should be unpaid: {}", err);
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
panic!("Minting should not be allowed");
|
||||
}
|
||||
}
|
||||
|
||||
let mint_quote = wallet.mint_quote(mint_amount, None).await.unwrap();
|
||||
|
||||
let state = wallet.mint_quote_state(&mint_quote.id).await.unwrap();
|
||||
|
||||
assert!(state.state == MintQuoteState::Unpaid);
|
||||
|
||||
let proofs = wallet
|
||||
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||
.await;
|
||||
|
||||
match proofs {
|
||||
Err(err) => {
|
||||
if !matches!(err, cdk::Error::UnpaidQuote) {
|
||||
panic!("Wrong error quote should be unpaid: {}", err);
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
panic!("Minting should not be allowed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,9 +108,7 @@ async fn test_fake_melt_change_in_quote() {
|
||||
|
||||
let invoice_amount = 9;
|
||||
|
||||
let invoice = create_invoice_for_env(&get_temp_dir(), Some(invoice_amount))
|
||||
.await
|
||||
.unwrap();
|
||||
let invoice = create_invoice_for_env(Some(invoice_amount)).await.unwrap();
|
||||
|
||||
let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user