diff --git a/crates/cdk-integration-tests/Cargo.toml b/crates/cdk-integration-tests/Cargo.toml index 0e37ce51..064c363c 100644 --- a/crates/cdk-integration-tests/Cargo.toml +++ b/crates/cdk-integration-tests/Cargo.toml @@ -14,6 +14,7 @@ rust-version = "1.63.0" http_subscription = ["cdk/http_subscription"] [dependencies] +async-trait = "0.1" axum = "0.6.20" rand = "0.8.5" bip39 = { version = "2.0", features = ["rand"] } @@ -57,8 +58,6 @@ getrandom = { version = "0.2", features = ["js"] } instant = { version = "0.1", features = ["wasm-bindgen", "inaccurate"] } [dev-dependencies] -async-trait = "0.1" -rand = "0.8.5" bip39 = { version = "2.0", features = ["rand"] } anyhow = "1" cdk = { path = "../cdk", features = ["mint", "wallet"] } diff --git a/crates/cdk-integration-tests/src/init_pure_tests.rs b/crates/cdk-integration-tests/src/init_pure_tests.rs new file mode 100644 index 00000000..85b0f09b --- /dev/null +++ b/crates/cdk-integration-tests/src/init_pure_tests.rs @@ -0,0 +1,223 @@ +use std::collections::{HashMap, HashSet}; +use std::fmt::{Debug, Formatter}; +use std::str::FromStr; +use std::sync::Arc; + +use async_trait::async_trait; +use bip39::Mnemonic; +use cdk::amount::SplitTarget; +use cdk::cdk_database::mint_memory::MintMemoryDatabase; +use cdk::cdk_database::WalletMemoryDatabase; +use cdk::mint::{FeeReserve, MintBuilder, MintMeltLimits}; +use cdk::nuts::nut00::ProofsMethods; +use cdk::nuts::{ + CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysetResponse, + MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request, + MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, PaymentMethod, + RestoreRequest, RestoreResponse, SwapRequest, SwapResponse, +}; +use cdk::util::unix_time; +use cdk::wallet::client::MintConnector; +use cdk::wallet::Wallet; +use cdk::{Amount, Error, Mint}; +use cdk_fake_wallet::FakeWallet; +use tokio::sync::Notify; +use uuid::Uuid; + +use crate::wait_for_mint_to_be_paid; + +pub struct DirectMintConnection { + pub mint: Arc, +} + +impl DirectMintConnection { + pub fn new(mint: Arc) -> Self { + Self { mint } + } +} + +impl Debug for DirectMintConnection { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "DirectMintConnection {{ mint_info: {:?} }}", + self.mint.config.mint_info() + ) + } +} + +/// Implements the generic [MintConnector] (i.e. use the interface that expects to communicate +/// to a generic mint, where we don't know that quote ID's are [Uuid]s) for [DirectMintConnection], +/// where we know we're dealing with a mint that uses [Uuid]s for quotes. +/// Convert the requests and responses between the [String] and [Uuid] variants as necessary. +#[async_trait] +impl MintConnector for DirectMintConnection { + async fn get_mint_keys(&self) -> Result, Error> { + self.mint.pubkeys().await.map(|pks| pks.keysets) + } + + async fn get_mint_keyset(&self, keyset_id: Id) -> Result { + self.mint + .keyset(&keyset_id) + .await + .and_then(|res| res.ok_or(Error::UnknownKeySet)) + } + + async fn get_mint_keysets(&self) -> Result { + self.mint.keysets().await + } + + async fn post_mint_quote( + &self, + request: MintQuoteBolt11Request, + ) -> Result, Error> { + self.mint + .get_mint_bolt11_quote(request) + .await + .map(Into::into) + } + + async fn get_mint_quote_status( + &self, + quote_id: &str, + ) -> Result, Error> { + let quote_id_uuid = Uuid::from_str(quote_id).unwrap(); + self.mint + .check_mint_quote("e_id_uuid) + .await + .map(Into::into) + } + + async fn post_mint( + &self, + request: MintBolt11Request, + ) -> Result { + let request_uuid = request.try_into().unwrap(); + self.mint.process_mint_request(request_uuid).await + } + + async fn post_melt_quote( + &self, + request: MeltQuoteBolt11Request, + ) -> Result, Error> { + self.mint + .get_melt_bolt11_quote(&request) + .await + .map(Into::into) + } + + async fn get_melt_quote_status( + &self, + quote_id: &str, + ) -> Result, Error> { + let quote_id_uuid = Uuid::from_str(quote_id).unwrap(); + self.mint + .check_melt_quote("e_id_uuid) + .await + .map(Into::into) + } + + async fn post_melt( + &self, + request: MeltBolt11Request, + ) -> Result, Error> { + let request_uuid = request.try_into().unwrap(); + self.mint.melt_bolt11(&request_uuid).await.map(Into::into) + } + + async fn post_swap(&self, swap_request: SwapRequest) -> Result { + self.mint.process_swap_request(swap_request).await + } + + async fn get_mint_info(&self) -> Result { + Ok(self.mint.mint_info().clone().time(unix_time())) + } + + async fn post_check_state( + &self, + request: CheckStateRequest, + ) -> Result { + self.mint.check_state(&request).await + } + + async fn post_restore(&self, request: RestoreRequest) -> Result { + self.mint.restore(request).await + } +} + +pub async fn create_and_start_test_mint() -> anyhow::Result> { + let mut mint_builder = MintBuilder::new(); + + let database = MintMemoryDatabase::default(); + + mint_builder = mint_builder.with_localstore(Arc::new(database)); + + let fee_reserve = FeeReserve { + min_fee_reserve: 1.into(), + percent_fee_reserve: 1.0, + }; + + let ln_fake_backend = Arc::new(FakeWallet::new( + fee_reserve.clone(), + HashMap::default(), + HashSet::default(), + 0, + )); + + mint_builder = mint_builder.add_ln_backend( + CurrencyUnit::Sat, + PaymentMethod::Bolt11, + MintMeltLimits::default(), + ln_fake_backend, + ); + + let mnemonic = Mnemonic::generate(12)?; + + mint_builder = mint_builder + .with_name("pure test mint".to_string()) + .with_mint_url("http://aa".to_string()) + .with_description("pure test mint".to_string()) + .with_quote_ttl(10000, 10000) + .with_seed(mnemonic.to_seed_normalized("").to_vec()); + + let mint = mint_builder.build().await?; + + let mint_arc = Arc::new(mint); + + let mint_arc_clone = Arc::clone(&mint_arc); + let shutdown = Arc::new(Notify::new()); + tokio::spawn({ + let shutdown = Arc::clone(&shutdown); + async move { mint_arc_clone.wait_for_paid_invoices(shutdown).await } + }); + + Ok(mint_arc) +} + +pub fn create_test_wallet_for_mint(mint: Arc) -> anyhow::Result> { + let connector = DirectMintConnection::new(mint); + + let seed = Mnemonic::generate(12)?.to_seed_normalized(""); + let mint_url = connector.mint.config.mint_url().to_string(); + let unit = CurrencyUnit::Sat; + let localstore = WalletMemoryDatabase::default(); + let mut wallet = Wallet::new(&mint_url, unit, Arc::new(localstore), &seed, None)?; + + wallet.set_client(connector); + + Ok(Arc::new(wallet)) +} + +/// Creates a mint quote for the given amount and checks its state in a loop. Returns when +/// amount is minted. +pub async fn fund_wallet(wallet: Arc, amount: u64) -> anyhow::Result { + let desired_amount = Amount::from(amount); + let quote = wallet.mint_quote(desired_amount, None).await?; + + wait_for_mint_to_be_paid(&wallet, "e.id).await?; + + Ok(wallet + .mint("e.id, SplitTarget::default(), None) + .await? + .total_amount()?) +} diff --git a/crates/cdk-integration-tests/src/lib.rs b/crates/cdk-integration-tests/src/lib.rs index b6ec4f52..2ea5db59 100644 --- a/crates/cdk-integration-tests/src/lib.rs +++ b/crates/cdk-integration-tests/src/lib.rs @@ -1,54 +1,26 @@ -use std::collections::{HashMap, HashSet}; use std::str::FromStr; use std::sync::Arc; use anyhow::{bail, Result}; use cdk::amount::{Amount, SplitTarget}; -use cdk::cdk_lightning::MintLightning; use cdk::dhke::construct_proofs; -use cdk::mint::FeeReserve; use cdk::mint_url::MintUrl; use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::nut17::Params; use cdk::nuts::{ CurrencyUnit, Id, KeySet, MintBolt11Request, MintQuoteBolt11Request, MintQuoteState, - NotificationPayload, PaymentMethod, PreMintSecrets, Proofs, State, + NotificationPayload, PreMintSecrets, Proofs, State, }; -use cdk::types::LnKey; use cdk::wallet::client::{HttpClient, MintConnector}; use cdk::wallet::subscription::SubscriptionManager; use cdk::wallet::WalletSubscription; use cdk::Wallet; -use cdk_fake_wallet::FakeWallet; pub mod init_fake_wallet; pub mod init_mint; +pub mod init_pure_tests; pub mod init_regtest; -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(), - HashMap::default(), - HashSet::default(), - 0, - )); - - ln_backends.insert(ln_key, wallet.clone()); - - ln_backends -} - pub async fn wallet_mint( wallet: Arc, amount: Amount, @@ -180,3 +152,22 @@ pub async fn attempt_to_swap_pending(wallet: &Wallet) -> Result<()> { Ok(()) } + +// Keep polling the state of the mint quote id until it's paid +pub async fn wait_for_mint_to_be_paid(wallet: &Wallet, mint_quote_id: &str) -> Result<()> { + let mut subscription = wallet + .subscribe(WalletSubscription::Bolt11MintQuoteState(vec![ + mint_quote_id.to_owned(), + ])) + .await; + + while let Some(msg) = subscription.recv().await { + if let NotificationPayload::MintQuoteBolt11Response(response) = msg { + if response.state == MintQuoteState::Paid { + break; + } + } + } + + Ok(()) +} diff --git a/crates/cdk-integration-tests/tests/fake_wallet.rs b/crates/cdk-integration-tests/tests/fake_wallet.rs index a12a07ab..5d2be2ec 100644 --- a/crates/cdk-integration-tests/tests/fake_wallet.rs +++ b/crates/cdk-integration-tests/tests/fake_wallet.rs @@ -6,13 +6,13 @@ use cdk::amount::SplitTarget; use cdk::cdk_database::WalletMemoryDatabase; use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::{ - CurrencyUnit, MeltBolt11Request, MeltQuoteState, MintBolt11Request, MintQuoteState, - NotificationPayload, PreMintSecrets, SecretKey, State, + CurrencyUnit, MeltBolt11Request, MeltQuoteState, MintBolt11Request, PreMintSecrets, SecretKey, + State, }; use cdk::wallet::client::{HttpClient, MintConnector}; -use cdk::wallet::{Wallet, WalletSubscription}; +use cdk::wallet::Wallet; use cdk_fake_wallet::{create_fake_invoice, FakeInvoiceDescription}; -use cdk_integration_tests::attempt_to_swap_pending; +use cdk_integration_tests::{attempt_to_swap_pending, wait_for_mint_to_be_paid}; const MINT_URL: &str = "http://127.0.0.1:8086"; @@ -477,22 +477,3 @@ async fn test_fake_mint_with_wrong_witness() -> Result<()> { Ok(_) => bail!("Minting should not have succeed without a witness"), } } - -// Keep polling the state of the mint quote id until it's paid -async fn wait_for_mint_to_be_paid(wallet: &Wallet, mint_quote_id: &str) -> Result<()> { - let mut subscription = wallet - .subscribe(WalletSubscription::Bolt11MintQuoteState(vec![ - mint_quote_id.to_owned(), - ])) - .await; - - while let Some(msg) = subscription.recv().await { - if let NotificationPayload::MintQuoteBolt11Response(response) = msg { - if response.state == MintQuoteState::Paid { - break; - } - } - } - - Ok(()) -} diff --git a/crates/cdk-integration-tests/tests/integration_tests_pure.rs b/crates/cdk-integration-tests/tests/integration_tests_pure.rs index 275219a1..3190f48c 100644 --- a/crates/cdk-integration-tests/tests/integration_tests_pure.rs +++ b/crates/cdk-integration-tests/tests/integration_tests_pure.rs @@ -1,267 +1,45 @@ -#[cfg(test)] -mod integration_tests_pure { - use std::assert_eq; - use std::collections::HashMap; - use std::fmt::{Debug, Formatter}; - use std::str::FromStr; - use std::sync::Arc; +use std::assert_eq; - use async_trait::async_trait; - use cdk::amount::SplitTarget; - use cdk::cdk_database::mint_memory::MintMemoryDatabase; - use cdk::cdk_database::WalletMemoryDatabase; - use cdk::nuts::nut00::ProofsMethods; - use cdk::nuts::{ - CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysetResponse, - MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request, - MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, - MintQuoteState, Nuts, RestoreRequest, RestoreResponse, SwapRequest, SwapResponse, - }; - use cdk::types::QuoteTTL; - use cdk::util::unix_time; - use cdk::wallet::client::MintConnector; - use cdk::{Amount, Error, Mint, Wallet}; - use cdk_integration_tests::create_backends_fake_wallet; - use rand::random; - use tokio::sync::Notify; - use uuid::Uuid; +use cdk::amount::SplitTarget; +use cdk::nuts::nut00::ProofsMethods; +use cdk::wallet::SendKind; +use cdk::Amount; +use cdk_integration_tests::init_pure_tests::{ + create_and_start_test_mint, create_test_wallet_for_mint, fund_wallet, +}; - struct DirectMintConnection { - mint: Arc, - } +#[tokio::test] +async fn test_swap_to_send() -> anyhow::Result<()> { + let mint_bob = create_and_start_test_mint().await?; + let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())?; - impl Debug for DirectMintConnection { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "DirectMintConnection {{ mint_info: {:?} }}", - self.mint.config.mint_info() - ) - } - } + // Alice gets 64 sats + fund_wallet(wallet_alice.clone(), 64).await?; + let balance_alice = wallet_alice.total_balance().await?; + assert_eq!(Amount::from(64), balance_alice); - /// Implements the generic [MintConnector] (i.e. use the interface that expects to communicate - /// to a generic mint, where we don't know that quote ID's are [Uuid]s) for [DirectMintConnection], - /// where we know we're dealing with a mint that uses [Uuid]s for quotes. - /// Convert the requests and responses between the [String] and [Uuid] variants as necessary. - #[async_trait] - impl MintConnector for DirectMintConnection { - async fn get_mint_keys(&self) -> Result, Error> { - self.mint.pubkeys().await.map(|pks| pks.keysets) - } - - async fn get_mint_keyset(&self, keyset_id: Id) -> Result { - self.mint - .keyset(&keyset_id) - .await - .and_then(|res| res.ok_or(Error::UnknownKeySet)) - } - - async fn get_mint_keysets(&self) -> Result { - self.mint.keysets().await - } - - async fn post_mint_quote( - &self, - request: MintQuoteBolt11Request, - ) -> Result, Error> { - self.mint - .get_mint_bolt11_quote(request) - .await - .map(Into::into) - } - - async fn get_mint_quote_status( - &self, - quote_id: &str, - ) -> Result, Error> { - let quote_id_uuid = Uuid::from_str(quote_id).unwrap(); - self.mint - .check_mint_quote("e_id_uuid) - .await - .map(Into::into) - } - - async fn post_mint( - &self, - request: MintBolt11Request, - ) -> Result { - let request_uuid = request.try_into().unwrap(); - self.mint.process_mint_request(request_uuid).await - } - - async fn post_melt_quote( - &self, - request: MeltQuoteBolt11Request, - ) -> Result, Error> { - self.mint - .get_melt_bolt11_quote(&request) - .await - .map(Into::into) - } - - async fn get_melt_quote_status( - &self, - quote_id: &str, - ) -> Result, Error> { - let quote_id_uuid = Uuid::from_str(quote_id).unwrap(); - self.mint - .check_melt_quote("e_id_uuid) - .await - .map(Into::into) - } - - async fn post_melt( - &self, - request: MeltBolt11Request, - ) -> Result, Error> { - let request_uuid = request.try_into().unwrap(); - self.mint.melt_bolt11(&request_uuid).await.map(Into::into) - } - - async fn post_swap(&self, swap_request: SwapRequest) -> Result { - self.mint.process_swap_request(swap_request).await - } - - async fn get_mint_info(&self) -> Result { - Ok(self.mint.mint_info().clone().time(unix_time())) - } - - async fn post_check_state( - &self, - request: CheckStateRequest, - ) -> Result { - self.mint.check_state(&request).await - } - - async fn post_restore(&self, request: RestoreRequest) -> Result { - self.mint.restore(request).await - } - } - - fn get_mint_connector(mint: Arc) -> DirectMintConnection { - DirectMintConnection { mint } - } - - async fn create_and_start_test_mint() -> anyhow::Result> { - let fee: u64 = 0; - let mut supported_units = HashMap::new(); - supported_units.insert(CurrencyUnit::Sat, (fee, 32)); - - 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 quote_ttl = QuoteTTL::new(10000, 10000); - - let mint_url = "http://aaa"; - - let seed = random::<[u8; 32]>(); - let mint: Mint = Mint::new( - mint_url, - &seed, - mint_info, - quote_ttl, - Arc::new(MintMemoryDatabase::default()), - create_backends_fake_wallet(), - supported_units, - HashMap::new(), + // Alice wants to send 40 sats, which internally swaps + let token = wallet_alice + .send( + Amount::from(40), + None, + None, + &SplitTarget::None, + &SendKind::OnlineExact, + false, ) .await?; + assert_eq!(Amount::from(40), token.proofs().total_amount()?); + assert_eq!(Amount::from(24), wallet_alice.total_balance().await?); - let mint_arc = Arc::new(mint); + // Alice sends cashu, Carol receives + let wallet_carol = create_test_wallet_for_mint(mint_bob.clone())?; + let received_amount = wallet_carol + .receive_proofs(token.proofs(), SplitTarget::None, &[], &[]) + .await?; - let mint_arc_clone = Arc::clone(&mint_arc); - let shutdown = Arc::new(Notify::new()); - tokio::spawn({ - let shutdown = Arc::clone(&shutdown); - async move { mint_arc_clone.wait_for_paid_invoices(shutdown).await } - }); + assert_eq!(Amount::from(40), received_amount); + assert_eq!(Amount::from(40), wallet_carol.total_balance().await?); - Ok(mint_arc) - } - - fn create_test_wallet_for_mint(mint: Arc) -> anyhow::Result> { - let connector = get_mint_connector(mint); - - let seed = random::<[u8; 32]>(); - let mint_url = connector.mint.config.mint_url().to_string(); - let unit = CurrencyUnit::Sat; - - let localstore = WalletMemoryDatabase::default(); - let mut wallet = Wallet::new(&mint_url, unit, Arc::new(localstore), &seed, None)?; - - wallet.set_client(connector); - - Ok(Arc::new(wallet)) - } - - /// Creates a mint quote for the given amount and checks its state in a loop. Returns when - /// amount is minted. - async fn receive(wallet: Arc, amount: u64) -> anyhow::Result { - let desired_amount = Amount::from(amount); - let quote = wallet.mint_quote(desired_amount, None).await?; - - loop { - let status = wallet.mint_quote_state("e.id).await?; - if status.state == MintQuoteState::Paid { - break; - } - } - - Ok(wallet - .mint("e.id, SplitTarget::default(), None) - .await? - .total_amount()?) - } - - mod nut03 { - use cdk::nuts::nut00::ProofsMethods; - use cdk::wallet::SendKind; - - use crate::integration_tests_pure::*; - - #[tokio::test] - async fn test_swap_to_send() -> anyhow::Result<()> { - let mint_bob = create_and_start_test_mint().await?; - let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())?; - - // Alice gets 64 sats - receive(wallet_alice.clone(), 64).await?; - let balance_alice = wallet_alice.total_balance().await?; - assert_eq!(Amount::from(64), balance_alice); - - // Alice wants to send 40 sats, which internally swaps - let token = wallet_alice - .send( - Amount::from(40), - None, - None, - &SplitTarget::None, - &SendKind::OnlineExact, - false, - ) - .await?; - assert_eq!(Amount::from(40), token.proofs().total_amount()?); - assert_eq!(Amount::from(24), wallet_alice.total_balance().await?); - - // Alice sends cashu, Carol receives - let wallet_carol = create_test_wallet_for_mint(mint_bob.clone())?; - let received_amount = wallet_carol - .receive_proofs(token.proofs(), SplitTarget::None, &[], &[]) - .await?; - - assert_eq!(Amount::from(40), received_amount); - assert_eq!(Amount::from(40), wallet_carol.total_balance().await?); - - Ok(()) - } - } + Ok(()) }