diff --git a/crates/cdk-axum/src/router_handlers.rs b/crates/cdk-axum/src/router_handlers.rs index 5c571059..ec2a5c37 100644 --- a/crates/cdk-axum/src/router_handlers.rs +++ b/crates/cdk-axum/src/router_handlers.rs @@ -12,6 +12,7 @@ use cdk::nuts::{ }; use cdk::util::unix_time; use paste::paste; +use tracing::instrument; use uuid::Uuid; use crate::ws::main_websocket; @@ -232,6 +233,7 @@ pub async fn post_mint_bolt11( (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json") ) ))] +#[instrument(skip_all)] /// Request a quote for melting tokens pub async fn post_melt_bolt11_quote( State(state): State, @@ -261,6 +263,7 @@ pub async fn post_melt_bolt11_quote( /// Get melt quote by ID /// /// Get melt quote state. +#[instrument(skip_all)] pub async fn get_check_melt_bolt11_quote( State(state): State, Path(quote_id): Path, @@ -290,6 +293,7 @@ pub async fn get_check_melt_bolt11_quote( /// Melt tokens for a Bitcoin payment that the mint will make for the user in exchange /// /// Requests tokens to be destroyed and sent out via Lightning. +#[instrument(skip_all)] pub async fn post_melt_bolt11( State(state): State, Json(payload): Json>, diff --git a/crates/cdk-integration-tests/Cargo.toml b/crates/cdk-integration-tests/Cargo.toml index 6914fc00..b7f23b6c 100644 --- a/crates/cdk-integration-tests/Cargo.toml +++ b/crates/cdk-integration-tests/Cargo.toml @@ -21,6 +21,7 @@ bip39 = { version = "2.0", features = ["rand"] } anyhow = "1" cdk = { path = "../cdk", features = ["mint", "wallet"] } cdk-cln = { path = "../cdk-cln" } +cdk-lnd = { path = "../cdk-lnd" } cdk-axum = { path = "../cdk-axum" } cdk-sqlite = { path = "../cdk-sqlite" } cdk-redb = { path = "../cdk-redb" } @@ -34,7 +35,7 @@ uuid = { version = "1", features = ["v4"] } serde = "1" serde_json = "1" # ln-regtest-rs = { path = "../../../../ln-regtest-rs" } -ln-regtest-rs = { git = "https://github.com/thesimplekid/ln-regtest-rs", rev = "166038b5" } +ln-regtest-rs = { git = "https://github.com/thesimplekid/ln-regtest-rs", rev = "f9e7bbbb" } lightning-invoice = { version = "0.32.0", features = ["serde", "std"] } tracing = { version = "0.1", default-features = false, features = [ "attributes", diff --git a/crates/cdk-integration-tests/src/bin/regtest_mint.rs b/crates/cdk-integration-tests/src/bin/regtest_mint.rs index 27faddb1..5e3205ae 100644 --- a/crates/cdk-integration-tests/src/bin/regtest_mint.rs +++ b/crates/cdk-integration-tests/src/bin/regtest_mint.rs @@ -3,13 +3,15 @@ use std::env; use anyhow::Result; use cdk::cdk_database::mint_memory::MintMemoryDatabase; use cdk_integration_tests::init_regtest::{ - fund_ln, get_bitcoin_dir, get_cln_dir, get_temp_dir, init_bitcoin_client, init_bitcoind, - init_lnd, init_lnd_client, open_channel, start_cln_mint, BITCOIN_RPC_PASS, BITCOIN_RPC_USER, + create_cln_backend, create_lnd_backend, create_mint, fund_ln, generate_block, get_bitcoin_dir, + get_cln_dir, get_lnd_cert_file_path, get_lnd_dir, get_lnd_macaroon_path, get_temp_dir, + init_bitcoin_client, init_bitcoind, init_lnd, open_channel, BITCOIN_RPC_PASS, BITCOIN_RPC_USER, + LND_ADDR, LND_RPC_ADDR, LND_TWO_ADDR, LND_TWO_RPC_ADDR, }; use cdk_redb::MintRedbDatabase; use cdk_sqlite::MintSqliteDatabase; use ln_regtest_rs::cln::Clnd; -use ln_regtest_rs::ln_client::{ClnClient, LightningClient}; +use ln_regtest_rs::ln_client::{ClnClient, LightningClient, LndClient}; use tracing_subscriber::EnvFilter; const CLN_ADDR: &str = "127.0.0.1:19846"; @@ -22,10 +24,11 @@ async fn main() -> Result<()> { let sqlx_filter = "sqlx=warn"; let hyper_filter = "hyper=warn"; let h2_filter = "h2=warn"; + let rustls_filter = "rustls=warn"; let env_filter = EnvFilter::new(format!( - "{},{},{},{}", - default_filter, sqlx_filter, hyper_filter, h2_filter + "{},{},{},{},{}", + default_filter, sqlx_filter, hyper_filter, h2_filter, rustls_filter )); tracing_subscriber::fmt().with_env_filter(env_filter).init(); @@ -69,45 +72,136 @@ async fn main() -> Result<()> { let cln_two_client = ClnClient::new(cln_two_dir.clone(), None).await?; - cln_client.wait_chain_sync().await.unwrap(); + cln_two_client.wait_chain_sync().await.unwrap(); fund_ln(&bitcoin_client, &cln_two_client).await.unwrap(); - let mut lnd = init_lnd().await; + let lnd_dir = get_lnd_dir("one"); + println!("{}", lnd_dir.display()); + + let mut lnd = init_lnd(lnd_dir.clone(), LND_ADDR, LND_RPC_ADDR).await; lnd.start_lnd().unwrap(); tracing::info!("Started lnd node"); - let lnd_client = init_lnd_client().await.unwrap(); + let lnd_client = LndClient::new( + format!("https://{}", LND_RPC_ADDR), + get_lnd_cert_file_path(&lnd_dir), + get_lnd_macaroon_path(&lnd_dir), + ) + .await?; lnd_client.wait_chain_sync().await.unwrap(); fund_ln(&bitcoin_client, &lnd_client).await.unwrap(); - open_channel(&bitcoin_client, &cln_client, &lnd_client) - .await - .unwrap(); + // create second lnd node + let lnd_two_dir = get_lnd_dir("two"); + let mut lnd_two = init_lnd(lnd_two_dir.clone(), LND_TWO_ADDR, LND_TWO_RPC_ADDR).await; + lnd_two.start_lnd().unwrap(); + tracing::info!("Started second lnd node"); - let addr = "127.0.0.1"; - let port = 8085; + let lnd_two_client = LndClient::new( + format!("https://{}", LND_TWO_RPC_ADDR), + get_lnd_cert_file_path(&lnd_two_dir), + get_lnd_macaroon_path(&lnd_two_dir), + ) + .await?; + + lnd_two_client.wait_chain_sync().await.unwrap(); + + fund_ln(&bitcoin_client, &lnd_two_client).await.unwrap(); + + // Open channels concurrently + // Open channels + { + open_channel(&cln_client, &lnd_client).await.unwrap(); + tracing::info!("Opened channel between cln and lnd one"); + generate_block(&bitcoin_client)?; + // open_channel(&bitcoin_client, &cln_client, &cln_two_client) + // .await + // .unwrap(); + // tracing::info!("Opened channel between cln and cln two"); + + open_channel(&lnd_client, &lnd_two_client).await.unwrap(); + tracing::info!("Opened channel between lnd and lnd two"); + generate_block(&bitcoin_client)?; + + // open_channel(&cln_client, &lnd_two_client).await.unwrap(); + // tracing::info!("Opened channel between cln and lnd two"); + open_channel(&cln_two_client, &lnd_client).await.unwrap(); + tracing::info!("Opened channel between cln two and lnd"); + generate_block(&bitcoin_client)?; + + open_channel(&cln_client, &lnd_two_client).await.unwrap(); + 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?; + } + + let mint_addr = "127.0.0.1"; + let cln_mint_port = 8085; let mint_db_kind = env::var("MINT_DATABASE")?; - let temp_dir_path = get_temp_dir(); - let db_path = get_temp_dir().join("mint"); - let cln_path = temp_dir_path.join("one"); + let lnd_mint_db_path = get_temp_dir().join("lnd_mint"); + let cln_mint_db_path = get_temp_dir().join("cln_mint"); + + let cln_backend = create_cln_backend(&cln_client).await?; + let lnd_mint_port = 8087; + + let lnd_backend = create_lnd_backend(&lnd_two_client).await?; match mint_db_kind.as_str() { "MEMORY" => { - start_cln_mint(addr, port, MintMemoryDatabase::default(), cln_path).await?; + tokio::spawn(async move { + create_mint( + mint_addr, + cln_mint_port, + MintMemoryDatabase::default(), + cln_backend, + ) + .await + .expect("Could not start cln mint"); + }); + + create_mint( + mint_addr, + lnd_mint_port, + MintMemoryDatabase::default(), + lnd_backend, + ) + .await?; } "SQLITE" => { - let sqlite_db = MintSqliteDatabase::new(&db_path).await?; + tokio::spawn(async move { + let sqlite_db = MintSqliteDatabase::new(&cln_mint_db_path) + .await + .expect("Could not create mint db"); + sqlite_db.migrate().await; + create_mint(mint_addr, cln_mint_port, sqlite_db, cln_backend) + .await + .expect("Could not start cln mint"); + }); + + let sqlite_db = MintSqliteDatabase::new(&lnd_mint_db_path).await?; sqlite_db.migrate().await; - start_cln_mint(addr, port, sqlite_db, cln_path).await?; + create_mint(mint_addr, lnd_mint_port, sqlite_db, lnd_backend).await?; } "REDB" => { - let redb_db = MintRedbDatabase::new(&db_path).unwrap(); - start_cln_mint(addr, port, redb_db, cln_path).await?; + tokio::spawn(async move { + let redb_db = MintRedbDatabase::new(&cln_mint_db_path).unwrap(); + create_mint(mint_addr, cln_mint_port, redb_db, cln_backend) + .await + .expect("Could not start cln mint"); + }); + + let redb_db = MintRedbDatabase::new(&lnd_mint_db_path).unwrap(); + + create_mint(mint_addr, lnd_mint_port, redb_db, lnd_backend).await?; } _ => panic!("Unknown mint db type: {}", mint_db_kind), }; diff --git a/crates/cdk-integration-tests/src/init_mint.rs b/crates/cdk-integration-tests/src/init_mint.rs index 0ca0d883..6ff23803 100644 --- a/crates/cdk-integration-tests/src/init_mint.rs +++ b/crates/cdk-integration-tests/src/init_mint.rs @@ -5,7 +5,9 @@ use axum::Router; use cdk::mint::Mint; use tokio::sync::Notify; use tower_http::cors::CorsLayer; +use tracing::instrument; +#[instrument(skip_all)] pub async fn start_mint(addr: &str, port: u16, mint: Mint) -> Result<()> { let mint_arc = Arc::new(mint); diff --git a/crates/cdk-integration-tests/src/init_regtest.rs b/crates/cdk-integration-tests/src/init_regtest.rs index 5b4d946f..839775f4 100644 --- a/crates/cdk-integration-tests/src/init_regtest.rs +++ b/crates/cdk-integration-tests/src/init_regtest.rs @@ -1,17 +1,20 @@ use std::env; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use anyhow::Result; use bip39::Mnemonic; use cdk::cdk_database::{self, MintDatabase}; +use cdk::cdk_lightning::{self, MintLightning}; use cdk::mint::{FeeReserve, MintBuilder, MintMeltLimits}; use cdk::nuts::{CurrencyUnit, PaymentMethod}; use cdk_cln::Cln as CdkCln; +use cdk_lnd::Lnd as CdkLnd; use ln_regtest_rs::bitcoin_client::BitcoinClient; use ln_regtest_rs::bitcoind::Bitcoind; use ln_regtest_rs::ln_client::{ClnClient, LightningClient, LndClient}; use ln_regtest_rs::lnd::Lnd; +use tracing::instrument; use crate::init_mint::start_mint; @@ -20,11 +23,14 @@ pub const ZMQ_RAW_BLOCK: &str = "tcp://127.0.0.1:28332"; pub const ZMQ_RAW_TX: &str = "tcp://127.0.0.1:28333"; pub const BITCOIN_RPC_USER: &str = "testuser"; pub const BITCOIN_RPC_PASS: &str = "testpass"; -const LND_ADDR: &str = "0.0.0.0:18449"; -const LND_RPC_ADDR: &str = "localhost:10009"; const BITCOIN_DIR: &str = "bitcoin"; -const LND_DIR: &str = "lnd"; + +pub const LND_ADDR: &str = "0.0.0.0:18449"; +pub const LND_RPC_ADDR: &str = "localhost:10009"; + +pub const LND_TWO_ADDR: &str = "0.0.0.0:18410"; +pub const LND_TWO_RPC_ADDR: &str = "localhost:10010"; pub fn get_mint_addr() -> String { env::var("cdk_itests_mint_addr").expect("Temp dir set") @@ -77,23 +83,31 @@ pub fn init_bitcoin_client() -> Result { } pub fn get_cln_dir(name: &str) -> PathBuf { - let dir = get_temp_dir().join(name); + let dir = get_temp_dir().join("cln").join(name); std::fs::create_dir_all(&dir).unwrap(); dir } -pub fn get_lnd_dir() -> PathBuf { - let dir = get_temp_dir().join(LND_DIR); +pub fn get_lnd_dir(name: &str) -> PathBuf { + let dir = get_temp_dir().join("lnd").join(name); std::fs::create_dir_all(&dir).unwrap(); dir } -pub async fn init_lnd() -> Lnd { +pub fn get_lnd_cert_file_path(lnd_dir: &Path) -> PathBuf { + lnd_dir.join("tls.cert") +} + +pub fn get_lnd_macaroon_path(lnd_dir: &Path) -> PathBuf { + lnd_dir.join("data/chain/bitcoin/regtest/admin.macaroon") +} + +pub async fn init_lnd(lnd_dir: PathBuf, lnd_addr: &str, lnd_rpc_addr: &str) -> Lnd { Lnd::new( get_bitcoin_dir(), - get_lnd_dir(), - LND_ADDR.parse().unwrap(), - LND_RPC_ADDR.to_string(), + lnd_dir, + lnd_addr.parse().unwrap(), + lnd_rpc_addr.to_string(), BITCOIN_RPC_USER.to_string(), BITCOIN_RPC_PASS.to_string(), ZMQ_RAW_BLOCK.to_string(), @@ -101,16 +115,11 @@ pub async fn init_lnd() -> Lnd { ) } -pub async fn init_lnd_client() -> Result { - let lnd_dir = get_lnd_dir(); - let cert_file = lnd_dir.join("tls.cert"); - let macaroon_file = lnd_dir.join("data/chain/bitcoin/regtest/admin.macaroon"); - LndClient::new( - format!("https://{}", LND_RPC_ADDR).parse().unwrap(), - cert_file, - macaroon_file, - ) - .await +pub fn generate_block(bitcoin_client: &BitcoinClient) -> Result<()> { + let mine_to_address = bitcoin_client.get_new_address()?; + bitcoin_client.generate_blocks(&mine_to_address, 10)?; + + Ok(()) } pub async fn create_cln_backend(cln_client: &ClnClient) -> Result { @@ -124,14 +133,27 @@ pub async fn create_cln_backend(cln_client: &ClnClient) -> Result { Ok(CdkCln::new(rpc_path, fee_reserve).await?) } -pub async fn start_cln_mint(addr: &str, port: u16, database: D, dir: PathBuf) -> Result<()> +pub async fn create_lnd_backend(lnd_client: &LndClient) -> Result { + let fee_reserve = FeeReserve { + min_fee_reserve: 1.into(), + percent_fee_reserve: 1.0, + }; + + Ok(CdkLnd::new( + lnd_client.address.clone(), + lnd_client.cert_file.clone(), + lnd_client.macaroon_file.clone(), + fee_reserve, + ) + .await?) +} + +#[instrument(skip_all)] +pub async fn create_mint(addr: &str, port: u16, database: D, lighting: L) -> Result<()> where D: MintDatabase + Send + Sync + 'static, + L: MintLightning + Send + Sync + 'static, { - let cln_client = ClnClient::new(dir.clone(), None).await?; - - let cln_backend = create_cln_backend(&cln_client).await?; - let mut mint_builder = MintBuilder::new(); mint_builder = mint_builder.with_localstore(Arc::new(database)); @@ -140,7 +162,7 @@ where CurrencyUnit::Sat, PaymentMethod::Bolt11, MintMeltLimits::new(1, 5_000), - Arc::new(cln_backend), + Arc::new(lighting), ); let mnemonic = Mnemonic::generate(12)?; @@ -165,7 +187,7 @@ where { let ln_address = ln_client.get_new_onchain_address().await?; - bitcoin_client.send_to_address(&ln_address, 2_000_000)?; + bitcoin_client.send_to_address(&ln_address, 5_000_000)?; ln_client.wait_chain_sync().await?; @@ -177,11 +199,7 @@ where Ok(()) } -pub async fn open_channel( - bitcoin_client: &BitcoinClient, - cln_client: &C1, - lnd_client: &C2, -) -> Result<()> +pub async fn open_channel(cln_client: &C1, lnd_client: &C2) -> Result<()> where C1: LightningClient, C2: LightningClient, @@ -197,19 +215,13 @@ where .await .unwrap(); + cln_client.wait_chain_sync().await?; + lnd_client.wait_chain_sync().await?; + lnd_client .open_channel(1_500_000, &cln_pubkey.to_string(), Some(750_000)) .await .unwrap(); - let mine_to_address = bitcoin_client.get_new_address()?; - bitcoin_client.generate_blocks(&mine_to_address, 10)?; - - cln_client.wait_chain_sync().await?; - lnd_client.wait_chain_sync().await?; - - cln_client.wait_channels_active().await?; - lnd_client.wait_channels_active().await?; - Ok(()) } diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs index 71e07f57..73d3010e 100644 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ b/crates/cdk-integration-tests/tests/regtest.rs @@ -15,17 +15,32 @@ use cdk::nuts::{ use cdk::wallet::client::{HttpClient, MintConnector}; use cdk::wallet::{Wallet, WalletSubscription}; use cdk_integration_tests::init_regtest::{ - get_cln_dir, get_mint_url, get_mint_ws_url, init_lnd_client, + get_cln_dir, get_lnd_cert_file_path, get_lnd_dir, get_lnd_macaroon_path, get_mint_port, + get_mint_url, get_mint_ws_url, LND_RPC_ADDR, LND_TWO_RPC_ADDR, }; use futures::{SinkExt, StreamExt}; use lightning_invoice::Bolt11Invoice; -use ln_regtest_rs::ln_client::{ClnClient, LightningClient}; +use ln_regtest_rs::ln_client::{ClnClient, LightningClient, LndClient}; use ln_regtest_rs::InvoiceStatus; use serde_json::json; use tokio::time::timeout; use tokio_tungstenite::connect_async; use tokio_tungstenite::tungstenite::protocol::Message; +// This is the ln wallet we use to send/receive ln payements as the wallet +async fn init_lnd_client() -> LndClient { + let lnd_dir = get_lnd_dir("one"); + let cert_file = lnd_dir.join("tls.cert"); + let macaroon_file = lnd_dir.join("data/chain/bitcoin/regtest/admin.macaroon"); + LndClient::new( + format!("https://{}", LND_RPC_ADDR), + cert_file, + macaroon_file, + ) + .await + .unwrap() +} + async fn get_notification> + Unpin, E: Debug>( reader: &mut T, timeout_to_wait: Duration, @@ -60,7 +75,7 @@ async fn get_notification> + Unpin, E: De #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_regtest_mint_melt_round_trip() -> Result<()> { - let lnd_client = init_lnd_client().await.unwrap(); + let lnd_client = init_lnd_client().await; let wallet = Wallet::new( &get_mint_url(), @@ -143,7 +158,7 @@ async fn test_regtest_mint_melt_round_trip() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_regtest_mint_melt() -> Result<()> { - let lnd_client = init_lnd_client().await?; + let lnd_client = init_lnd_client().await; let wallet = Wallet::new( &get_mint_url(), @@ -174,7 +189,7 @@ async fn test_regtest_mint_melt() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_restore() -> Result<()> { - let lnd_client = init_lnd_client().await?; + let lnd_client = init_lnd_client().await; let seed = Mnemonic::generate(12)?.to_seed_normalized(""); let wallet = Wallet::new( @@ -231,7 +246,8 @@ async fn test_restore() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_pay_invoice_twice() -> Result<()> { - let lnd_client = init_lnd_client().await?; + let lnd_client = init_lnd_client().await; + let seed = Mnemonic::generate(12)?.to_seed_normalized(""); let wallet = Wallet::new( &get_mint_url(), @@ -243,7 +259,10 @@ async fn test_pay_invoice_twice() -> Result<()> { let mint_quote = wallet.mint_quote(100.into(), None).await?; - lnd_client.pay_invoice(mint_quote.request).await?; + lnd_client + .pay_invoice(mint_quote.request) + .await + .expect("Could not pay invoice"); let proofs = wallet .mint(&mint_quote.id, SplitTarget::default(), None) @@ -266,8 +285,8 @@ async fn test_pay_invoice_twice() -> Result<()> { match melt_two { Err(err) => match err { cdk::Error::RequestAlreadyPaid => (), - _ => { - bail!("Wrong invoice already paid"); + err => { + bail!("Wrong invoice already paid: {}", err.to_string()); } }, Ok(_) => { @@ -284,7 +303,7 @@ async fn test_pay_invoice_twice() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_internal_payment() -> Result<()> { - let lnd_client = init_lnd_client().await?; + let lnd_client = init_lnd_client().await; let seed = Mnemonic::generate(12)?.to_seed_normalized(""); let wallet = Wallet::new( @@ -328,13 +347,33 @@ async fn test_internal_payment() -> Result<()> { .await .unwrap(); - let cln_one_dir = get_cln_dir("one"); - let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?; + let check_paid = match get_mint_port() { + 8085 => { + let cln_one_dir = get_cln_dir("one"); + let cln_client = ClnClient::new(cln_one_dir.clone(), None).await?; - let payment_hash = Bolt11Invoice::from_str(&mint_quote.request)?; - let check_paid = cln_client - .check_incoming_payment_status(&payment_hash.payment_hash().to_string()) - .await?; + let payment_hash = Bolt11Invoice::from_str(&mint_quote.request)?; + cln_client + .check_incoming_payment_status(&payment_hash.payment_hash().to_string()) + .await + .expect("Could not check invoice") + } + 8087 => { + let lnd_two_dir = get_lnd_dir("two"); + let lnd_client = LndClient::new( + format!("https://{}", LND_TWO_RPC_ADDR), + get_lnd_cert_file_path(&lnd_two_dir), + get_lnd_macaroon_path(&lnd_two_dir), + ) + .await?; + let payment_hash = Bolt11Invoice::from_str(&mint_quote.request)?; + lnd_client + .check_incoming_payment_status(&payment_hash.payment_hash().to_string()) + .await + .expect("Could not check invoice") + } + _ => panic!("Unknown mint port"), + }; match check_paid { InvoiceStatus::Unpaid => (), @@ -356,7 +395,7 @@ async fn test_internal_payment() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_cached_mint() -> Result<()> { - let lnd_client = init_lnd_client().await.unwrap(); + let lnd_client = init_lnd_client().await; let wallet = Wallet::new( &get_mint_url(), diff --git a/crates/cdk-lnd/src/lib.rs b/crates/cdk-lnd/src/lib.rs index 04888e1a..0f379915 100644 --- a/crates/cdk-lnd/src/lib.rs +++ b/crates/cdk-lnd/src/lib.rs @@ -25,10 +25,12 @@ use error::Error; use fedimint_tonic_lnd::lnrpc::fee_limit::Limit; use fedimint_tonic_lnd::lnrpc::payment::PaymentStatus; use fedimint_tonic_lnd::lnrpc::FeeLimit; +use fedimint_tonic_lnd::tonic::Code; use fedimint_tonic_lnd::Client; use futures::{Stream, StreamExt}; use tokio::sync::Mutex; use tokio_util::sync::CancellationToken; +use tracing::instrument; pub mod error; @@ -75,6 +77,7 @@ impl Lnd { impl MintLightning for Lnd { type Err = cdk_lightning::Error; + #[instrument(skip_all)] fn get_settings(&self) -> Settings { Settings { mpp: false, @@ -83,14 +86,17 @@ impl MintLightning for Lnd { } } + #[instrument(skip_all)] fn is_wait_invoice_active(&self) -> bool { self.wait_invoice_is_active.load(Ordering::SeqCst) } + #[instrument(skip_all)] fn cancel_wait_invoice(&self) { self.wait_invoice_cancel_token.cancel() } + #[instrument(skip_all)] async fn wait_any_invoice( &self, ) -> Result + Send>>, Self::Err> { @@ -163,6 +169,7 @@ impl MintLightning for Lnd { .boxed()) } + #[instrument(skip_all)] async fn get_payment_quote( &self, melt_quote_request: &MeltQuoteBolt11Request, @@ -189,6 +196,7 @@ impl MintLightning for Lnd { }) } + #[instrument(skip_all)] async fn pay_invoice( &self, melt_quote: mint::MeltQuote, @@ -196,6 +204,23 @@ impl MintLightning for Lnd { max_fee: Option, ) -> Result { let payment_request = melt_quote.request; + let bolt11 = Bolt11Invoice::from_str(&payment_request)?; + + let pay_state = self + .check_outgoing_payment(&bolt11.payment_hash().to_string()) + .await?; + + match pay_state.status { + MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (), + MeltQuoteState::Paid => { + tracing::debug!("Melt attempted on invoice already paid"); + return Err(Self::Err::InvoiceAlreadyPaid); + } + MeltQuoteState::Pending => { + tracing::debug!("Melt attempted on invoice already pending"); + return Err(Self::Err::InvoicePaymentPending); + } + } let amount_msat: u64 = match melt_quote.msat_to_pay { Some(amount_msat) => amount_msat.into(), @@ -225,7 +250,10 @@ impl MintLightning for Lnd { .lightning() .send_payment_sync(fedimint_tonic_lnd::tonic::Request::new(pay_req)) .await - .map_err(|_| Error::PaymentFailed)? + .map_err(|err| { + tracing::warn!("Lightning payment failed: {}", err); + Error::PaymentFailed + })? .into_inner(); let total_amount = payment_response @@ -250,6 +278,7 @@ impl MintLightning for Lnd { }) } + #[instrument(skip(self, description))] async fn create_invoice( &self, amount: Amount, @@ -287,6 +316,7 @@ impl MintLightning for Lnd { }) } + #[instrument(skip(self))] async fn check_incoming_invoice_status( &self, request_lookup_id: &str, @@ -319,6 +349,7 @@ impl MintLightning for Lnd { } } + #[instrument(skip(self))] async fn check_outgoing_payment( &self, payment_hash: &str, @@ -327,15 +358,32 @@ impl MintLightning for Lnd { payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?, no_inflight_updates: true, }; - let mut payment_stream = self + + let payment_response = self .client .lock() .await .router() .track_payment_v2(track_request) - .await - .unwrap() - .into_inner(); + .await; + + let mut payment_stream = match payment_response { + Ok(stream) => stream.into_inner(), + Err(err) => { + let err_code = err.code(); + if err_code == Code::NotFound { + return Ok(PayInvoiceResponse { + payment_lookup_id: payment_hash.to_string(), + payment_preimage: None, + status: MeltQuoteState::Unknown, + total_spent: Amount::ZERO, + unit: self.get_settings().unit, + }); + } else { + return Err(cdk_lightning::Error::UnknownPaymentState); + } + } + }; while let Some(update_result) = payment_stream.next().await { match update_result { diff --git a/crates/cdk/src/mint/melt.rs b/crates/cdk/src/mint/melt.rs index f2679b52..064efdc6 100644 --- a/crates/cdk/src/mint/melt.rs +++ b/crates/cdk/src/mint/melt.rs @@ -21,6 +21,7 @@ use crate::util::unix_time; use crate::{cdk_lightning, Amount, Error}; impl Mint { + #[instrument(skip_all)] fn check_melt_request_acceptable( &self, amount: Amount, diff --git a/misc/itests.sh b/misc/itests.sh index 95446fa1..cb127606 100755 --- a/misc/itests.sh +++ b/misc/itests.sh @@ -12,11 +12,11 @@ cleanup() { wait $CDK_ITEST_MINT_BIN_PID echo "Mint binary terminated" - # Kill processes - lncli --lnddir="$cdk_itests/lnd" --network=regtest stop - lightning-cli --regtest --lightning-dir="$cdk_itests/one/" stop - lightning-cli --regtest --lightning-dir="$cdk_itests/two/" stop + lncli --lnddir="$cdk_itests/lnd/one" --network=regtest stop + lncli --lnddir="$cdk_itests/lnd/two" --network=regtest --rpcserver=localhost:10010 stop + lightning-cli --regtest --lightning-dir="$cdk_itests/cln/one/" stop + lightning-cli --regtest --lightning-dir="$cdk_itests/cln/two/" stop bitcoin-cli --datadir="$cdk_itests/bitcoin" -rpcuser=testuser -rpcpassword=testpass -rpcport=18443 stop # Remove the temporary directory @@ -47,7 +47,10 @@ export MINT_DATABASE="$1"; cargo build -p cdk-integration-tests cargo build --bin regtest_mint +# cargo run --bin regtest_mint > "$cdk_itests/mint.log" 2>&1 & cargo run --bin regtest_mint & + +echo $cdk_itests # Capture its PID CDK_ITEST_MINT_BIN_PID=$! @@ -84,9 +87,13 @@ done # Run cargo test cargo test -p cdk-integration-tests --test regtest -# Run cargo test with the http_subscription feature +# # Run cargo test with the http_subscription feature cargo test -p cdk-integration-tests --test regtest --features http_subscription +# Run tests with lnd mint +export cdk_itests_mint_port=8087; +cargo test -p cdk-integration-tests --test regtest + # Capture the exit status of cargo test test_status=$?