Răsfoiți Sursa

feat(tests): integration test

thesimplekid 8 luni în urmă
părinte
comite
e8114f3383

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

@@ -36,6 +36,7 @@ jobs:
             -p cdk-cln,
             -p cdk-fake-wallet,
             -p cdk-strike,
+            -p cdk-integration-tests,
             --bin cdk-cli,
             --bin cdk-mintd,
             --examples

+ 1 - 0
.github/workflows/test.yml

@@ -19,6 +19,7 @@ jobs:
         build-args:
           [
             -p cdk,
+            -p cdk-integration-tests,
           ]
     steps:
     - name: Checkout Crate

+ 1 - 0
Cargo.toml

@@ -47,6 +47,7 @@ futures = { version = "0.3.28", default-feature = false }
 web-sys =  { version = "0.3.69", default-features = false, features = ["console"] }
 uuid = { version = "1", features = ["v4"] }
 lightning-invoice = { version = "0.31", features = ["serde"] }
+tower-http = { version = "0.5.2", features = ["cors"] }
 home = "0.5.9"
 rand = "0.8.5"
 url = "2.3"

+ 1 - 1
crates/cdk-axum/Cargo.toml

@@ -14,6 +14,6 @@ async-trait.workspace = true
 axum.workspace = true
 cdk = { workspace = true, default-features = false, features = ["mint"] }
 tokio.workspace = true
-tower-http = { version = "0.5.2", features = ["cors"] }
+tower-http.workspace = true
 tracing.workspace = true
 futures.workspace = true

+ 50 - 0
crates/cdk-integration-tests/Cargo.toml

@@ -0,0 +1,50 @@
+[package]
+name = "cdk-integration-tests"
+version = "0.2.0"
+edition = "2021"
+authors = ["CDK Developers"]
+description = "Core Cashu Development Kit library implementing the Cashu protocol"
+homepage.workspace = true
+repository.workspace = true
+rust-version.workspace = true # MSRV
+license.workspace = true
+
+
+[features]
+
+
+[dependencies]
+axum.workspace = true
+rand.workspace = true
+bip39 = { workspace = true, features = ["rand"] }
+anyhow.workspace = true
+cdk = { workspace = true, features = ["mint", "wallet"] }
+cdk-axum.workspace = true
+cdk-fake-wallet.workspace = true
+tower-http.workspace = true
+futures.workspace = true
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+tokio = { workspace = true, features = [
+    "rt-multi-thread",
+    "time",
+    "macros",
+    "sync",
+] }
+
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+tokio = { workspace = true, features = ["rt", "macros", "sync", "time"] }
+getrandom = { version = "0.2", features = ["js"] }
+instant = { version = "0.1", features = ["wasm-bindgen", "inaccurate"] }
+
+[dev-dependencies]
+axum.workspace = true
+rand.workspace = true
+bip39 = { workspace = true, features = ["rand"] }
+anyhow.workspace = true
+cdk = { workspace = true, features = ["mint", "wallet"] }
+cdk-axum.workspace = true
+cdk-fake-wallet.workspace = true
+tower-http.workspace = true
+# cdk-redb.workspace = true
+# cdk-sqlite.workspace = true

+ 164 - 0
crates/cdk-integration-tests/src/lib.rs

@@ -0,0 +1,164 @@
+use std::collections::HashMap;
+use std::sync::Arc;
+use std::time::Duration;
+
+use anyhow::Result;
+use axum::Router;
+use bip39::Mnemonic;
+use cdk::amount::{Amount, SplitTarget};
+use cdk::cdk_database::mint_memory::MintMemoryDatabase;
+use cdk::cdk_lightning::{MintLightning, MintMeltSettings};
+use cdk::mint::FeeReserve;
+use cdk::nuts::{CurrencyUnit, MintInfo, MintQuoteState, Nuts, PaymentMethod};
+use cdk::{Mint, Wallet};
+use cdk_axum::LnKey;
+use cdk_fake_wallet::FakeWallet;
+use futures::StreamExt;
+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 fn create_backends_fake_wallet(
+) -> HashMap<LnKey, Arc<dyn MintLightning<Err = cdk::cdk_lightning::Error> + Sync + Send>> {
+    let fee_reserve = FeeReserve {
+        min_fee_reserve: 1.into(),
+        percent_fee_reserve: 1.0,
+    };
+    let mut ln_backends: HashMap<
+        LnKey,
+        Arc<dyn MintLightning<Err = cdk::cdk_lightning::Error> + Sync + Send>,
+    > = HashMap::new();
+    let ln_key = LnKey::new(CurrencyUnit::Sat, PaymentMethod::Bolt11);
+
+    let wallet = Arc::new(FakeWallet::new(
+        fee_reserve.clone(),
+        MintMeltSettings::default(),
+        MintMeltSettings::default(),
+    ));
+
+    ln_backends.insert(ln_key, wallet.clone());
+
+    ln_backends
+}
+
+pub async fn start_mint(
+    ln_backends: HashMap<
+        LnKey,
+        Arc<dyn MintLightning<Err = cdk::cdk_lightning::Error> + Sync + Send>,
+    >,
+) -> Result<()> {
+    let 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::new();
+    supported_units.insert(CurrencyUnit::Sat, (0, 64));
+
+    let mint = Mint::new(
+        MINT_URL,
+        &mnemonic.to_seed_normalized(""),
+        mint_info,
+        Arc::new(MintMemoryDatabase::default()),
+        supported_units,
+    )
+    .await?;
+
+    let quote_ttl = 2000;
+
+    let mint_arc = Arc::new(mint);
+
+    let v1_service = cdk_axum::create_mint_router(
+        MINT_URL,
+        Arc::clone(&mint_arc),
+        ln_backends.clone(),
+        quote_ttl,
+    )
+    .await?;
+
+    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);
+                }
+            }
+        });
+    }
+    let listener =
+        tokio::net::TcpListener::bind(format!("{}:{}", LISTEN_ADDR, LISTEN_PORT)).await?;
+
+    println!("Starting mint");
+    axum::serve(listener, mint_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 wallet_mint(wallet: Arc<Wallet>, amount: Amount) -> Result<()> {
+    let quote = wallet.mint_quote(amount).await?;
+
+    loop {
+        let status = wallet.mint_quote_state(&quote.id).await?;
+
+        if status.state == MintQuoteState::Paid {
+            break;
+        }
+        println!("{:?}", status);
+
+        sleep(Duration::from_secs(2)).await;
+    }
+    let receive_amount = wallet.mint(&quote.id, SplitTarget::default(), None).await?;
+
+    println!("Minted: {}", receive_amount);
+
+    Ok(())
+}

+ 80 - 0
crates/cdk-integration-tests/tests/mint.rs

@@ -0,0 +1,80 @@
+//! Mint integration tests
+
+use std::sync::Arc;
+use std::time::Duration;
+
+use anyhow::{bail, Result};
+use bip39::Mnemonic;
+use cdk::amount::SplitTarget;
+use cdk::cdk_database::WalletMemoryDatabase;
+use cdk::nuts::CurrencyUnit;
+use cdk::wallet::error::Error;
+use cdk::wallet::SendKind;
+use cdk::Wallet;
+use cdk_integration_tests::{create_backends_fake_wallet, start_mint, wallet_mint, MINT_URL};
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+pub async fn test_mint_double_receive() -> Result<()> {
+    tokio::spawn(async move {
+        let ln_backends = create_backends_fake_wallet();
+
+        start_mint(ln_backends).await.expect("Could not start mint")
+    });
+
+    tokio::time::sleep(Duration::from_millis(500)).await;
+
+    let mnemonic = Mnemonic::generate(12)?;
+
+    let wallet = Wallet::new(
+        &MINT_URL,
+        CurrencyUnit::Sat,
+        Arc::new(WalletMemoryDatabase::default()),
+        &mnemonic.to_seed_normalized(""),
+        None,
+    );
+
+    let wallet = Arc::new(wallet);
+
+    wallet_mint(Arc::clone(&wallet), 100.into()).await?;
+
+    let token = wallet
+        .send(
+            10.into(),
+            None,
+            None,
+            &SplitTarget::default(),
+            &SendKind::default(),
+            false,
+        )
+        .await?;
+
+    let mnemonic = Mnemonic::generate(12)?;
+
+    let wallet_two = Wallet::new(
+        &MINT_URL,
+        CurrencyUnit::Sat,
+        Arc::new(WalletMemoryDatabase::default()),
+        &mnemonic.to_seed_normalized(""),
+        None,
+    );
+
+    let rec = wallet_two
+        .receive(&token.to_string(), SplitTarget::default(), &[], &[])
+        .await?;
+    println!("Received: {}", rec);
+
+    // Attempt to receive again
+    if let Err(err) = wallet
+        .receive(&token.to_string(), SplitTarget::default(), &[], &[])
+        .await
+    {
+        match err {
+            Error::TokenAlreadySpent => (),
+            _ => {
+                bail!("Expected an already spent error");
+            }
+        }
+    }
+
+    Ok(())
+}

+ 82 - 0
crates/cdk-integration-tests/tests/p2pk.rs

@@ -0,0 +1,82 @@
+use std::sync::Arc;
+use std::time::Duration;
+
+use anyhow::Result;
+use bip39::Mnemonic;
+use cdk::amount::SplitTarget;
+use cdk::cdk_database::WalletMemoryDatabase;
+use cdk::nuts::{CurrencyUnit, SecretKey, SpendingConditions};
+use cdk::wallet::SendKind;
+use cdk::{Amount, Wallet};
+use cdk_integration_tests::{create_backends_fake_wallet, start_mint, wallet_mint, MINT_URL};
+
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+pub async fn test_p2pk_swap() -> Result<()> {
+    tokio::spawn(async move {
+        let ln_backends = create_backends_fake_wallet();
+
+        start_mint(ln_backends).await.expect("Could not start mint")
+    });
+    tokio::time::sleep(Duration::from_millis(500)).await;
+
+    let mnemonic = Mnemonic::generate(12)?;
+
+    let wallet = Wallet::new(
+        &MINT_URL,
+        CurrencyUnit::Sat,
+        Arc::new(WalletMemoryDatabase::default()),
+        &mnemonic.to_seed_normalized(""),
+        None,
+    );
+
+    let wallet = Arc::new(wallet);
+
+    // Mint 100 sats for the wallet
+    wallet_mint(Arc::clone(&wallet), 100.into()).await?;
+
+    let secret = SecretKey::generate();
+
+    let spending_conditions = SpendingConditions::new_p2pk(secret.public_key(), None);
+
+    let amount = Amount::from(10);
+
+    let token = wallet
+        .send(
+            amount,
+            None,
+            Some(spending_conditions),
+            &SplitTarget::None,
+            &SendKind::default(),
+            false,
+        )
+        .await?;
+
+    let attempt_amount = wallet
+        .receive(&token.to_string(), SplitTarget::default(), &[], &[])
+        .await;
+
+    // This should fail since the token is not signed
+    assert!(attempt_amount.is_err());
+
+    let wrong_secret = SecretKey::generate();
+
+    let received_amount = wallet
+        .receive(
+            &token.to_string(),
+            SplitTarget::default(),
+            &[wrong_secret],
+            &[],
+        )
+        .await;
+
+    assert!(received_amount.is_err());
+
+    let received_amount = wallet
+        .receive(&token.to_string(), SplitTarget::default(), &[secret], &[])
+        .await
+        .unwrap();
+
+    assert_eq!(received_amount, amount);
+
+    Ok(())
+}

+ 1 - 0
misc/scripts/check-crates.sh

@@ -23,6 +23,7 @@ if [ "$is_msrv" == true ]; then
 fi
 
 buildargs=(
+    "-p cdk-integration-tests"
     "-p cdk"
     "-p cdk --no-default-features"
     "-p cdk --no-default-features --features wallet"