瀏覽代碼

feat: itests

fix: melt change promises amount
thesimplekid 5 月之前
父節點
當前提交
f9bb5eb913

+ 23 - 1
.github/workflows/ci.yml

@@ -54,7 +54,6 @@ jobs:
             -p cdk-phoenixd,
             -p cdk-strike,
             -p cdk-lnbits,
-            -p cdk-integration-tests,
             -p cdk-fake-wallet,
             --bin cdk-cli,
             --bin cdk-mintd
@@ -75,6 +74,29 @@ jobs:
       - name: Test
         run: nix develop -i -L .#stable --command cargo test ${{ matrix.build-args }}
 
+  itest:
+    name: "Integration tests"
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        build-args:
+          [
+            -p cdk-integration-tests,
+          ]
+    steps:
+      - name: checkout
+        uses: actions/checkout@v4
+      - name: Install Nix
+        uses: DeterminateSystems/nix-installer-action@v11
+      - name: Nix Cache
+        uses: DeterminateSystems/magic-nix-cache-action@v6
+      - name: Rust Cache
+        uses: Swatinem/rust-cache@v2
+      - name: Clippy
+        run: nix develop -i -L .#stable --command cargo clippy ${{ matrix.build-args }} -- -D warnings
+      - name: Test
+        run: nix develop -i -L .#stable --command just itest
+
   msrv-build:
     name: "MSRV build"
     runs-on: ubuntu-latest

+ 12 - 9
crates/cdk-axum/src/router_handlers.rs

@@ -63,20 +63,16 @@ pub async fn get_mint_bolt11_quote(
             into_response(Error::UnitUnsupported)
         })?;
 
-    let amount =
-        to_unit(payload.amount, &payload.unit, &ln.get_settings().unit).map_err(|err| {
-            tracing::error!("Backend does not support unit: {}", err);
-            into_response(Error::UnitUnsupported)
-        })?;
-
     let quote_expiry = unix_time() + state.quote_ttl;
+
     if payload.description.is_some() && !ln.get_settings().invoice_description {
         tracing::error!("Backend does not support invoice description");
         return Err(into_response(Error::InvoiceDescriptionUnsupported));
     }
+
     let create_invoice_response = ln
         .create_invoice(
-            amount,
+            payload.amount,
             &payload.unit,
             payload.description.unwrap_or("".to_string()),
             quote_expiry,
@@ -391,8 +387,15 @@ pub async fn post_melt_bolt11(
             }
 
             // Convert from unit of backend to quote unit
-            let amount_spent = to_unit(pre.total_spent, &pre.unit, &quote.unit)
-                .map_err(|_| into_response(Error::UnitUnsupported))?;
+            let amount_spent = to_unit(pre.total_spent, &pre.unit, &quote.unit).map_err(|_| {
+                tracing::error!(
+                    "Could not convert from {} to {} in melt.",
+                    pre.unit,
+                    quote.unit
+                );
+
+                into_response(Error::UnitUnsupported)
+            })?;
 
             (pre.payment_preimage, amount_spent)
         }

+ 7 - 1
crates/cdk-integration-tests/Cargo.toml

@@ -19,12 +19,18 @@ rand = "0.8.5"
 bip39 = { version = "2.0", features = ["rand"] }
 anyhow = "1"
 cdk = { path = "../cdk", version = "0.4.0", features = ["mint", "wallet"] }
+cdk-cln = { path = "../cdk-cln", version = "0.4.0" }
 cdk-axum = { path = "../cdk-axum"}
 cdk-fake-wallet = { path = "../cdk-fake-wallet" }
 tower-http = { version = "0.4.4", features = ["cors"] }
-futures = { version = "0.3.28", default-features = false }
+futures = { version = "0.3.28", default-features = false, features = ["executor"] }
 once_cell = "1.19.0"
 uuid = { version = "1", features = ["v4"] }
+# ln-regtest-rs = { path = "../../../../ln-regtest-rs" }
+ln-regtest-rs = { git = "https://github.com/thesimplekid/ln-regtest-rs", rev = "1d88d3d0b" }
+lightning-invoice = { version = "0.32.0", features = ["serde", "std"] }
+tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
+tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 tokio = { version = "1", features = [

+ 326 - 0
crates/cdk-integration-tests/src/init_regtest.rs

@@ -0,0 +1,326 @@
+use std::{collections::HashMap, env, path::PathBuf, sync::Arc};
+
+use anyhow::Result;
+use axum::Router;
+use bip39::Mnemonic;
+use cdk::{
+    cdk_database::mint_memory::MintMemoryDatabase,
+    cdk_lightning::MintLightning,
+    mint::{FeeReserve, Mint},
+    nuts::{CurrencyUnit, MeltMethodSettings, MintInfo, MintMethodSettings},
+};
+use cdk_axum::LnKey;
+use cdk_cln::Cln as CdkCln;
+use futures::StreamExt;
+use ln_regtest_rs::{
+    bitcoin_client::BitcoinClient, bitcoind::Bitcoind, cln::Clnd, cln_client::ClnClient, lnd::Lnd,
+    lnd_client::LndClient,
+};
+use tower_http::cors::CorsLayer;
+use tracing_subscriber::EnvFilter;
+
+const BITCOIND_ADDR: &str = "127.0.0.1:18443";
+const ZMQ_RAW_BLOCK: &str = "tcp://127.0.0.1:28332";
+const ZMQ_RAW_TX: &str = "tcp://127.0.0.1:28333";
+const BITCOIN_RPC_USER: &str = "testuser";
+const BITCOIN_RPC_PASS: &str = "testpass";
+const CLN_ADDR: &str = "127.0.0.1:19846";
+const LND_ADDR: &str = "0.0.0.0:18444";
+const LND_RPC_ADDR: &str = "https://127.0.0.1:10009";
+
+const BITCOIN_DIR: &str = "bitcoin";
+const CLN_DIR: &str = "cln";
+const LND_DIR: &str = "lnd";
+
+pub fn get_mint_addr() -> String {
+    env::var("cdk_itests_mint_addr").expect("Temp dir set")
+}
+
+pub fn get_mint_port() -> u16 {
+    let dir = env::var("cdk_itests_mint_port").expect("Temp dir set");
+    dir.parse().unwrap()
+}
+
+pub fn get_mint_url() -> String {
+    format!("http://{}:{}", get_mint_addr(), get_mint_port())
+}
+
+pub fn get_temp_dir() -> PathBuf {
+    let dir = env::var("cdk_itests").expect("Temp dir set");
+    std::fs::create_dir_all(&dir).unwrap();
+    dir.parse().expect("Valid path buf")
+}
+
+pub fn get_bitcoin_dir() -> PathBuf {
+    let dir = get_temp_dir().join(BITCOIN_DIR);
+    std::fs::create_dir_all(&dir).unwrap();
+    dir
+}
+
+pub fn init_bitcoind() -> Bitcoind {
+    Bitcoind::new(
+        get_bitcoin_dir(),
+        BITCOIND_ADDR.parse().unwrap(),
+        BITCOIN_RPC_USER.to_string(),
+        BITCOIN_RPC_PASS.to_string(),
+        ZMQ_RAW_BLOCK.to_string(),
+        ZMQ_RAW_TX.to_string(),
+    )
+}
+
+pub fn init_bitcoin_client() -> Result<BitcoinClient> {
+    BitcoinClient::new(
+        "wallet".to_string(),
+        BITCOIND_ADDR.into(),
+        None,
+        Some(BITCOIN_RPC_USER.to_string()),
+        Some(BITCOIN_RPC_PASS.to_string()),
+    )
+}
+
+pub fn get_cln_dir() -> PathBuf {
+    let dir = get_temp_dir().join(CLN_DIR);
+    std::fs::create_dir_all(&dir).unwrap();
+    dir
+}
+
+pub fn init_cln() -> Clnd {
+    Clnd::new(
+        get_bitcoin_dir(),
+        get_cln_dir(),
+        CLN_ADDR.to_string().parse().unwrap(),
+        BITCOIN_RPC_USER.to_string(),
+        BITCOIN_RPC_PASS.to_string(),
+    )
+}
+
+pub async fn init_cln_client() -> Result<ClnClient> {
+    ClnClient::new(get_cln_dir(), None).await
+}
+
+pub fn get_lnd_dir() -> PathBuf {
+    let dir = get_temp_dir().join(LND_DIR);
+    std::fs::create_dir_all(&dir).unwrap();
+    dir
+}
+
+pub async fn init_lnd() -> Lnd {
+    Lnd::new(
+        get_bitcoin_dir(),
+        get_lnd_dir(),
+        LND_ADDR.parse().unwrap(),
+        BITCOIN_RPC_USER.to_string(),
+        BITCOIN_RPC_PASS.to_string(),
+        ZMQ_RAW_BLOCK.to_string(),
+        ZMQ_RAW_TX.to_string(),
+    )
+}
+
+pub async fn init_lnd_client() -> Result<LndClient> {
+    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(LND_RPC_ADDR.parse().unwrap(), cert_file, macaroon_file).await
+}
+
+pub async fn create_cln_backend(cln_client: &ClnClient) -> Result<CdkCln> {
+    let rpc_path = cln_client.rpc_path.clone();
+
+    let fee_reserve = FeeReserve {
+        min_fee_reserve: 1.into(),
+        percent_fee_reserve: 1.0,
+    };
+
+    Ok(CdkCln::new(
+        rpc_path,
+        fee_reserve,
+        MintMethodSettings::default(),
+        MeltMethodSettings::default(),
+    )
+    .await?)
+}
+
+pub async fn create_mint() -> Result<Mint> {
+    let nuts = cdk::nuts::Nuts::new()
+        .nut07(true)
+        .nut08(true)
+        .nut09(true)
+        .nut10(true)
+        .nut11(true)
+        .nut12(true)
+        .nut14(true);
+
+    let mint_info = MintInfo::new().nuts(nuts);
+
+    let mnemonic = Mnemonic::generate(12)?;
+
+    let mut supported_units: HashMap<CurrencyUnit, (u64, u8)> = HashMap::new();
+    supported_units.insert(CurrencyUnit::Sat, (0, 32));
+
+    let mint = Mint::new(
+        &get_mint_url(),
+        &mnemonic.to_seed_normalized(""),
+        mint_info,
+        Arc::new(MintMemoryDatabase::default()),
+        supported_units,
+    )
+    .await?;
+
+    Ok(mint)
+}
+
+pub async fn start_cln_mint() -> Result<()> {
+    let default_filter = "debug";
+
+    let sqlx_filter = "sqlx=warn";
+    let hyper_filter = "hyper=warn";
+
+    let env_filter = EnvFilter::new(format!(
+        "{},{},{}",
+        default_filter, sqlx_filter, hyper_filter
+    ));
+
+    // Parse input
+    tracing_subscriber::fmt().with_env_filter(env_filter).init();
+
+    let mint = create_mint().await?;
+    let cln_client = init_cln_client().await?;
+
+    let cln_backend = create_cln_backend(&cln_client).await?;
+
+    let mut ln_backends: HashMap<
+        LnKey,
+        Arc<dyn MintLightning<Err = cdk::cdk_lightning::Error> + Sync + Send>,
+    > = HashMap::new();
+
+    ln_backends.insert(
+        LnKey::new(CurrencyUnit::Sat, cdk::nuts::PaymentMethod::Bolt11),
+        Arc::new(cln_backend),
+    );
+
+    let quote_ttl = 100000;
+
+    let mint_arc = Arc::new(mint);
+
+    let v1_service = cdk_axum::create_mint_router(
+        &get_mint_url(),
+        Arc::clone(&mint_arc),
+        ln_backends.clone(),
+        quote_ttl,
+    )
+    .await
+    .unwrap();
+
+    let mint_service = Router::new()
+        .merge(v1_service)
+        .layer(CorsLayer::permissive());
+
+    let mint = Arc::clone(&mint_arc);
+
+    for wallet in ln_backends.values() {
+        let wallet_clone = Arc::clone(wallet);
+        let mint = Arc::clone(&mint);
+        tokio::spawn(async move {
+            match wallet_clone.wait_any_invoice().await {
+                Ok(mut stream) => {
+                    while let Some(request_lookup_id) = stream.next().await {
+                        if let Err(err) =
+                            handle_paid_invoice(Arc::clone(&mint), &request_lookup_id).await
+                        {
+                            // nosemgrep: direct-panic
+                            panic!("{:?}", err);
+                        }
+                    }
+                }
+                Err(err) => {
+                    // nosemgrep: direct-panic
+                    panic!("Could not get invoice stream: {}", err);
+                }
+            }
+        });
+    }
+    println!("Staring Axum server");
+    axum::Server::bind(
+        &format!("{}:{}", "127.0.0.1", 8085)
+            .as_str()
+            .parse()
+            .unwrap(),
+    )
+    .serve(mint_service.into_make_service())
+    .await?;
+
+    Ok(())
+}
+
+/// Update mint quote when called for a paid invoice
+async fn handle_paid_invoice(mint: Arc<Mint>, request_lookup_id: &str) -> Result<()> {
+    println!("Invoice with lookup id paid: {}", request_lookup_id);
+    if let Ok(Some(mint_quote)) = mint
+        .localstore
+        .get_mint_quote_by_request_lookup_id(request_lookup_id)
+        .await
+    {
+        println!(
+            "Quote {} paid by lookup id {}",
+            mint_quote.id, request_lookup_id
+        );
+        mint.localstore
+            .update_mint_quote_state(&mint_quote.id, cdk::nuts::MintQuoteState::Paid)
+            .await?;
+    }
+    Ok(())
+}
+
+pub async fn fund_ln(
+    bitcoin_client: &BitcoinClient,
+    cln_client: &ClnClient,
+    lnd_client: &LndClient,
+) -> Result<()> {
+    let lnd_address = lnd_client.get_new_address().await?;
+
+    bitcoin_client.send_to_address(&lnd_address, 2_000_000)?;
+
+    let cln_address = cln_client.get_new_address().await?;
+    bitcoin_client.send_to_address(&cln_address, 2_000_000)?;
+
+    let mining_address = bitcoin_client.get_new_address()?;
+    bitcoin_client.generate_blocks(&mining_address, 200)?;
+
+    cln_client.wait_chain_sync().await?;
+    lnd_client.wait_chain_sync().await?;
+
+    Ok(())
+}
+
+pub async fn open_channel(
+    bitcoin_client: &BitcoinClient,
+    cln_client: &ClnClient,
+    lnd_client: &LndClient,
+) -> Result<()> {
+    let cln_info = cln_client.get_info().await?;
+
+    let cln_pubkey = cln_info.id;
+    let cln_address = "127.0.0.1";
+    let cln_port = 19846;
+
+    lnd_client
+        .connect(cln_pubkey.to_string(), cln_address.to_string(), cln_port)
+        .await
+        .unwrap();
+
+    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(())
+}

+ 5 - 6
crates/cdk-integration-tests/src/lib.rs

@@ -19,12 +19,11 @@ use cdk::{Mint, Wallet};
 use cdk_axum::LnKey;
 use cdk_fake_wallet::FakeWallet;
 use futures::StreamExt;
+use init_regtest::{get_mint_addr, get_mint_port, get_mint_url};
 use tokio::time::sleep;
 use tower_http::cors::CorsLayer;
 
-pub const MINT_URL: &str = "http://127.0.0.1:8088";
-const LISTEN_ADDR: &str = "127.0.0.1";
-const LISTEN_PORT: u16 = 8088;
+pub mod init_regtest;
 
 pub fn create_backends_fake_wallet(
 ) -> HashMap<LnKey, Arc<dyn MintLightning<Err = cdk::cdk_lightning::Error> + Sync + Send>> {
@@ -70,7 +69,7 @@ pub async fn start_mint(
     let mnemonic = Mnemonic::generate(12)?;
 
     let mint = Mint::new(
-        MINT_URL,
+        &get_mint_url(),
         &mnemonic.to_seed_normalized(""),
         mint_info,
         Arc::new(MintMemoryDatabase::default()),
@@ -83,7 +82,7 @@ pub async fn start_mint(
     let mint_arc = Arc::new(mint);
 
     let v1_service = cdk_axum::create_mint_router(
-        MINT_URL,
+        &get_mint_url(),
         Arc::clone(&mint_arc),
         ln_backends.clone(),
         quote_ttl,
@@ -120,7 +119,7 @@ pub async fn start_mint(
     }
 
     axum::Server::bind(
-        &format!("{}:{}", LISTEN_ADDR, LISTEN_PORT)
+        &format!("{}:{}", get_mint_addr(), get_mint_port())
             .as_str()
             .parse()?,
     )

+ 40 - 0
crates/cdk-integration-tests/src/main.rs

@@ -0,0 +1,40 @@
+use anyhow::Result;
+use cdk_integration_tests::init_regtest::{
+    fund_ln, init_bitcoin_client, init_bitcoind, init_cln, init_cln_client, init_lnd,
+    init_lnd_client, open_channel, start_cln_mint,
+};
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    let mut bitcoind = init_bitcoind();
+    bitcoind.start_bitcoind()?;
+
+    let bitcoin_client = init_bitcoin_client()?;
+    bitcoin_client.create_wallet().ok();
+    bitcoin_client.load_wallet()?;
+
+    let new_add = bitcoin_client.get_new_address()?;
+    bitcoin_client.generate_blocks(&new_add, 200).unwrap();
+
+    let mut clnd = init_cln();
+    clnd.start_clnd()?;
+
+    let cln_client = init_cln_client().await?;
+
+    let mut lnd = init_lnd().await;
+    lnd.start_lnd().unwrap();
+
+    let lnd_client = init_lnd_client().await.unwrap();
+
+    fund_ln(&bitcoin_client, &cln_client, &lnd_client)
+        .await
+        .unwrap();
+
+    open_channel(&bitcoin_client, &cln_client, &lnd_client)
+        .await
+        .unwrap();
+
+    start_cln_mint().await?;
+
+    Ok(())
+}

+ 255 - 0
crates/cdk-integration-tests/tests/regtest.rs

@@ -0,0 +1,255 @@
+use std::{str::FromStr, sync::Arc};
+
+use anyhow::{bail, Result};
+use bip39::Mnemonic;
+use cdk::{
+    amount::{Amount, SplitTarget},
+    cdk_database::WalletMemoryDatabase,
+    nuts::{CurrencyUnit, MeltQuoteState, State},
+    wallet::Wallet,
+};
+use cdk_integration_tests::init_regtest::{get_mint_url, init_cln_client, init_lnd_client};
+use lightning_invoice::Bolt11Invoice;
+use ln_regtest_rs::InvoiceStatus;
+
+#[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 wallet = Wallet::new(
+        &get_mint_url(),
+        CurrencyUnit::Sat,
+        Arc::new(WalletMemoryDatabase::default()),
+        &Mnemonic::generate(12)?.to_seed_normalized(""),
+        None,
+    )?;
+
+    let mint_quote = wallet.mint_quote(100.into(), None).await?;
+
+    lnd_client.pay_invoice(mint_quote.request).await?;
+
+    let mint_amount = wallet
+        .mint(&mint_quote.id, SplitTarget::default(), None)
+        .await?;
+
+    assert!(mint_amount == 100.into());
+
+    let invoice = lnd_client.create_invoice(50).await?;
+
+    let melt = wallet.melt_quote(invoice, None).await?;
+
+    let melt = wallet.melt(&melt.id).await.unwrap();
+
+    assert!(melt.preimage.is_some());
+
+    assert!(melt.state == MeltQuoteState::Paid);
+
+    Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_regtest_mint_melt() -> Result<()> {
+    let lnd_client = init_lnd_client().await?;
+
+    let wallet = Wallet::new(
+        &get_mint_url(),
+        CurrencyUnit::Sat,
+        Arc::new(WalletMemoryDatabase::default()),
+        &Mnemonic::generate(12)?.to_seed_normalized(""),
+        None,
+    )?;
+
+    let mint_amount = Amount::from(100);
+
+    let mint_quote = wallet.mint_quote(mint_amount, None).await?;
+
+    assert_eq!(mint_quote.amount, mint_amount);
+
+    lnd_client.pay_invoice(mint_quote.request).await?;
+
+    let mint_amount = wallet
+        .mint(&mint_quote.id, SplitTarget::default(), None)
+        .await?;
+
+    assert!(mint_amount == 100.into());
+
+    Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_restore() -> Result<()> {
+    let lnd_client = init_lnd_client().await?;
+
+    let seed = Mnemonic::generate(12)?.to_seed_normalized("");
+    let wallet = Wallet::new(
+        &get_mint_url(),
+        CurrencyUnit::Sat,
+        Arc::new(WalletMemoryDatabase::default()),
+        &seed,
+        None,
+    )?;
+
+    let mint_quote = wallet.mint_quote(100.into(), None).await?;
+
+    lnd_client.pay_invoice(mint_quote.request).await?;
+
+    let _mint_amount = wallet
+        .mint(&mint_quote.id, SplitTarget::default(), None)
+        .await?;
+
+    assert!(wallet.total_balance().await? == 100.into());
+
+    let wallet_2 = Wallet::new(
+        &get_mint_url(),
+        CurrencyUnit::Sat,
+        Arc::new(WalletMemoryDatabase::default()),
+        &seed,
+        None,
+    )?;
+
+    assert!(wallet_2.total_balance().await? == 0.into());
+
+    let restored = wallet_2.restore().await?;
+    let proofs = wallet_2.get_proofs().await?;
+
+    wallet_2
+        .swap(None, SplitTarget::default(), proofs, None, false)
+        .await?;
+
+    assert!(restored == 100.into());
+
+    assert!(wallet_2.total_balance().await? == 100.into());
+
+    let proofs = wallet.get_proofs().await?;
+
+    let states = wallet.check_proofs_spent(proofs).await?;
+
+    for state in states {
+        if state.state != State::Spent {
+            bail!("All proofs should be spent");
+        }
+    }
+
+    Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_pay_invoice_twice() -> Result<()> {
+    let lnd_client = init_lnd_client().await?;
+    let seed = Mnemonic::generate(12)?.to_seed_normalized("");
+    let wallet = Wallet::new(
+        &get_mint_url(),
+        CurrencyUnit::Sat,
+        Arc::new(WalletMemoryDatabase::default()),
+        &seed,
+        None,
+    )?;
+
+    let mint_quote = wallet.mint_quote(100.into(), None).await?;
+
+    lnd_client.pay_invoice(mint_quote.request).await?;
+
+    let mint_amount = wallet
+        .mint(&mint_quote.id, SplitTarget::default(), None)
+        .await?;
+
+    assert_eq!(mint_amount, 100.into());
+
+    let invoice = lnd_client.create_invoice(10).await?;
+
+    let melt_quote = wallet.melt_quote(invoice.clone(), None).await?;
+
+    let melt = wallet.melt(&melt_quote.id).await.unwrap();
+
+    let melt_two = wallet.melt_quote(invoice, None).await?;
+
+    let melt_two = wallet.melt(&melt_two.id).await;
+
+    match melt_two {
+        Err(err) => match err {
+            cdk::Error::RequestAlreadyPaid => (),
+            _ => {
+                bail!("Wrong invoice already paid");
+            }
+        },
+        Ok(_) => {
+            bail!("Should not have allowed second payment");
+        }
+    }
+
+    let balance = wallet.total_balance().await?;
+
+    assert_eq!(balance, (Amount::from(100) - melt.fee_paid - melt.amount));
+
+    Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_internal_payment() -> Result<()> {
+    let lnd_client = init_lnd_client().await?;
+
+    let seed = Mnemonic::generate(12)?.to_seed_normalized("");
+    let wallet = Wallet::new(
+        &get_mint_url(),
+        CurrencyUnit::Sat,
+        Arc::new(WalletMemoryDatabase::default()),
+        &seed,
+        None,
+    )?;
+
+    let mint_quote = wallet.mint_quote(100.into(), None).await?;
+
+    lnd_client.pay_invoice(mint_quote.request).await?;
+
+    let _mint_amount = wallet
+        .mint(&mint_quote.id, SplitTarget::default(), None)
+        .await?;
+
+    assert!(wallet.total_balance().await? == 100.into());
+
+    let seed = Mnemonic::generate(12)?.to_seed_normalized("");
+
+    let wallet_2 = Wallet::new(
+        &get_mint_url(),
+        CurrencyUnit::Sat,
+        Arc::new(WalletMemoryDatabase::default()),
+        &seed,
+        None,
+    )?;
+
+    let mint_quote = wallet_2.mint_quote(10.into(), None).await?;
+
+    let melt = wallet.melt_quote(mint_quote.request.clone(), None).await?;
+
+    assert_eq!(melt.amount, 10.into());
+
+    let _melted = wallet.melt(&melt.id).await.unwrap();
+
+    let _wallet_2_mint = wallet_2
+        .mint(&mint_quote.id, SplitTarget::default(), None)
+        .await
+        .unwrap();
+
+    let cln_client = init_cln_client().await?;
+    let payment_hash = Bolt11Invoice::from_str(&mint_quote.request)?;
+    let check_paid = cln_client
+        .check_incoming_invoice(payment_hash.payment_hash().to_string())
+        .await?;
+
+    match check_paid {
+        InvoiceStatus::Unpaid => (),
+        _ => {
+            bail!("Invoice has incorrect status: {:?}", check_paid);
+        }
+    }
+
+    let wallet_2_balance = wallet_2.total_balance().await?;
+
+    assert!(wallet_2_balance == 10.into());
+
+    let wallet_1_balance = wallet.total_balance().await?;
+
+    assert!(wallet_1_balance == 90.into());
+
+    Ok(())
+}

+ 6 - 0
crates/cdk/src/dhke.rs

@@ -121,6 +121,12 @@ pub fn construct_proofs(
     keys: &Keys,
 ) -> Result<Proofs, Error> {
     if (promises.len() != rs.len()) || (promises.len() != secrets.len()) {
+        tracing::error!(
+            "Promises: {}, RS: {}, secrets:{}",
+            promises.len(),
+            rs.len(),
+            secrets.len()
+        );
         return Err(Error::Custom(
             "Lengths of promises, rs, and secrets must be equal".to_string(),
         ));

+ 1 - 0
crates/cdk/src/types.rs

@@ -41,6 +41,7 @@ impl Melted {
         let fee_paid = proofs_amount
             .checked_sub(amount + change_amount)
             .ok_or(Error::AmountOverflow)?;
+
         Ok(Self {
             state,
             preimage,

+ 21 - 6
crates/cdk/src/wallet/mod.rs

@@ -1442,12 +1442,27 @@ impl Wallet {
             .ok_or(Error::NoActiveKeyset)?;
 
         let change_proofs = match melt_response.change {
-            Some(change) => Some(construct_proofs(
-                change,
-                premint_secrets.rs(),
-                premint_secrets.secrets(),
-                &active_keys,
-            )?),
+            Some(change) => {
+                let num_change_proof = change.len();
+
+                let num_change_proof = match (
+                    premint_secrets.len() < num_change_proof,
+                    premint_secrets.secrets().len() < num_change_proof,
+                ) {
+                    (true, _) | (_, true) => {
+                        tracing::error!("Mismatch in change promises to change");
+                        premint_secrets.len()
+                    }
+                    _ => num_change_proof,
+                };
+
+                Some(construct_proofs(
+                    change,
+                    premint_secrets.rs()[..num_change_proof].to_vec(),
+                    premint_secrets.secrets()[..num_change_proof].to_vec(),
+                    &active_keys,
+                )?)
+            }
             None => None,
         };
 

+ 3 - 0
flake.nix

@@ -67,6 +67,9 @@
           nixpkgs-fmt
           rust-analyzer
           typos
+          lnd
+          clightning
+          bitcoind
         ] ++ libsDarwin;
 
         # WASM deps

+ 1 - 0
justfile

@@ -1,4 +1,5 @@
 import "./misc/justfile.custom.just"
+import "./misc/test.just"
 
 alias b := build
 alias c := check

+ 92 - 0
misc/itests.sh

@@ -0,0 +1,92 @@
+#!/usr/bin/env bash
+
+# Function to perform cleanup
+cleanup() {
+    echo "Cleaning up..."
+
+    # Kill the Rust binary process
+    echo "Killing the Rust binary with PID $RUST_BIN_PID"
+    kill $CDK_ITEST_MINT_BIN_PID
+
+    # Wait for the Rust binary to terminate
+    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/cln/" stop
+    bitcoin-cli --datadir="$cdk_itests/bitcoin"  -rpcuser=testuser -rpcpassword=testpass -rpcport=18443 stop
+
+    # Remove the temporary directory
+    rm -rf "$cdk_itests"
+    echo "Temp directory removed: $cdk_itests"
+    unset cdk_itests
+    unset cdk_itests_mint_addr
+    unset cdk_itests_mint_port
+}
+
+# Set up trap to call cleanup on script exit
+trap cleanup EXIT
+
+# Create a temporary directory
+export cdk_itests=$(mktemp -d)
+export cdk_itests_mint_addr="127.0.0.1";
+export cdk_itests_mint_port=8085;
+
+URL="http://$cdk_itests_mint_addr:$cdk_itests_mint_port/v1/info"
+# Check if the temporary directory was created successfully
+if [[ ! -d "$cdk_itests" ]]; then
+    echo "Failed to create temp directory"
+    exit 1
+fi
+
+echo "Temp directory created: $cdk_itests"
+
+cargo build -p cdk-integration-tests 
+cargo build --bin cdk-integration-tests 
+cargo run --bin cdk-integration-tests &
+# Capture its PID
+CDK_ITEST_MINT_BIN_PID=$!
+
+TIMEOUT=100
+START_TIME=$(date +%s)
+# Loop until the endpoint returns a 200 OK status or timeout is reached
+while true; do
+    # Get the current time
+    CURRENT_TIME=$(date +%s)
+    
+    # Calculate the elapsed time
+    ELAPSED_TIME=$((CURRENT_TIME - START_TIME))
+
+    # Check if the elapsed time exceeds the timeout
+    if [ $ELAPSED_TIME -ge $TIMEOUT ]; then
+        echo "Timeout of $TIMEOUT seconds reached. Exiting..."
+        exit 1
+    fi
+
+    # Make a request to the endpoint and capture the HTTP status code
+    HTTP_STATUS=$(curl -o /dev/null -s -w "%{http_code}" $URL)
+
+    # Check if the HTTP status is 200 OK
+    if [ "$HTTP_STATUS" -eq 200 ]; then
+        echo "Received 200 OK from $URL"
+        break
+    else
+        echo "Waiting for 200 OK response, current status: $HTTP_STATUS"
+        sleep 2  # Wait for 2 seconds before retrying
+    fi
+done
+
+
+# Run cargo test
+cargo test -p cdk-integration-tests
+
+# Capture the exit status of cargo test
+test_status=$?
+
+# Source PIDs from file
+source "$cdk_itests/pids.txt"
+
+# Exit with the status of the tests
+exit $test_status

+ 4 - 0
misc/test.just

@@ -0,0 +1,4 @@
+itest:
+  #!/usr/bin/env bash
+  ./misc/itests.sh
+