diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aba811c1..a47c0a56 100644 --- a/.github/workflows/ci.yml +++ b/.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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a89f3274..63af88fe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,6 +19,7 @@ jobs: build-args: [ -p cdk, + -p cdk-integration-tests, ] steps: - name: Checkout Crate diff --git a/Cargo.toml b/Cargo.toml index db5a5cf2..a95ca061 100644 --- a/Cargo.toml +++ b/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" diff --git a/crates/cdk-axum/Cargo.toml b/crates/cdk-axum/Cargo.toml index 13fc34ce..c0d3bbdf 100644 --- a/crates/cdk-axum/Cargo.toml +++ b/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 diff --git a/crates/cdk-integration-tests/Cargo.toml b/crates/cdk-integration-tests/Cargo.toml new file mode 100644 index 00000000..aa5dda7e --- /dev/null +++ b/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 diff --git a/crates/cdk-integration-tests/src/lib.rs b/crates/cdk-integration-tests/src/lib.rs new file mode 100644 index 00000000..c346caba --- /dev/null +++ b/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 + Sync + Send>> { + let fee_reserve = FeeReserve { + min_fee_reserve: 1.into(), + percent_fee_reserve: 1.0, + }; + let mut ln_backends: HashMap< + LnKey, + Arc + 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 + 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, 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, amount: Amount) -> Result<()> { + let quote = wallet.mint_quote(amount).await?; + + loop { + let status = wallet.mint_quote_state("e.id).await?; + + if status.state == MintQuoteState::Paid { + break; + } + println!("{:?}", status); + + sleep(Duration::from_secs(2)).await; + } + let receive_amount = wallet.mint("e.id, SplitTarget::default(), None).await?; + + println!("Minted: {}", receive_amount); + + Ok(()) +} diff --git a/crates/cdk-integration-tests/tests/mint.rs b/crates/cdk-integration-tests/tests/mint.rs new file mode 100644 index 00000000..e60a78d4 --- /dev/null +++ b/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(()) +} diff --git a/crates/cdk-integration-tests/tests/p2pk.rs b/crates/cdk-integration-tests/tests/p2pk.rs new file mode 100644 index 00000000..ee9e6198 --- /dev/null +++ b/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(()) +} diff --git a/misc/scripts/check-crates.sh b/misc/scripts/check-crates.sh index 34a38e98..25ccfab7 100755 --- a/misc/scripts/check-crates.sh +++ b/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"