//! Mint tests use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::time::Duration; use anyhow::{bail, Result}; use bip39::Mnemonic; use cdk::amount::{Amount, SplitTarget}; use cdk::cdk_database::MintDatabase; use cdk::dhke::construct_proofs; use cdk::mint::{FeeReserve, MintBuilder, MintMeltLimits, MintQuote}; use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::{ CurrencyUnit, Id, MintBolt11Request, MintInfo, NotificationPayload, Nuts, PaymentMethod, PreMintSecrets, ProofState, Proofs, SecretKey, SpendingConditions, State, SwapRequest, }; use cdk::subscription::{IndexableParams, Params}; use cdk::types::QuoteTTL; use cdk::util::unix_time; use cdk::Mint; use cdk_fake_wallet::FakeWallet; use cdk_sqlite::mint::memory; use tokio::time::sleep; pub const MINT_URL: &str = "http://127.0.0.1:8088"; async fn new_mint(fee: u64) -> Mint { 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 localstore = memory::empty().await.expect("valid db instance"); localstore .set_mint_info(mint_info) .await .expect("Could not set mint info"); let mnemonic = Mnemonic::generate(12).unwrap(); Mint::new( &mnemonic.to_seed_normalized(""), Arc::new(localstore), HashMap::new(), supported_units, HashMap::new(), ) .await .unwrap() } async fn initialize() -> Mint { new_mint(0).await } async fn mint_proofs( mint: &Mint, amount: Amount, split_target: &SplitTarget, keys: cdk::nuts::Keys, ) -> Result { let request_lookup = uuid::Uuid::new_v4().to_string(); let quote = MintQuote::new( "".to_string(), CurrencyUnit::Sat, amount, unix_time() + 36000, request_lookup.to_string(), None, ); mint.localstore.add_mint_quote(quote.clone()).await?; mint.pay_mint_quote_for_request_id(&request_lookup).await?; let keyset_id = Id::from(&keys); let premint = PreMintSecrets::random(keyset_id, amount, split_target)?; let mint_request = MintBolt11Request { quote: quote.id, outputs: premint.blinded_messages(), signature: None, }; let after_mint = mint.process_mint_request(mint_request).await?; let proofs = construct_proofs( after_mint.signatures, premint.rs(), premint.secrets(), &keys, )?; Ok(proofs) } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_mint_double_spend() -> Result<()> { let mint = initialize().await; let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); let proofs = mint_proofs(&mint, 100.into(), &SplitTarget::default(), keys).await?; let preswap = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())?; let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages()); let swap = mint.process_swap_request(swap_request).await; assert!(swap.is_ok()); let preswap_two = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())?; let swap_two_request = SwapRequest::new(proofs, preswap_two.blinded_messages()); match mint.process_swap_request(swap_two_request).await { Ok(_) => bail!("Proofs double spent"), Err(err) => match err { cdk::Error::TokenAlreadySpent => (), _ => bail!("Wrong error returned"), }, } Ok(()) } /// This attempts to swap for more outputs then inputs. /// This will work if the mint does not check for outputs amounts overflowing #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_attempt_to_swap_by_overflowing() -> Result<()> { let mint = initialize().await; let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); let proofs = mint_proofs(&mint, 100.into(), &SplitTarget::default(), keys).await?; let amount = 2_u64.pow(63); let pre_mint_amount = PreMintSecrets::random(keyset_id, amount.into(), &SplitTarget::default())?; let pre_mint_amount_two = PreMintSecrets::random(keyset_id, amount.into(), &SplitTarget::default())?; let mut pre_mint = PreMintSecrets::random(keyset_id, 1.into(), &SplitTarget::default())?; pre_mint.combine(pre_mint_amount); pre_mint.combine(pre_mint_amount_two); let swap_request = SwapRequest::new(proofs.clone(), pre_mint.blinded_messages()); match mint.process_swap_request(swap_request).await { Ok(_) => bail!("Swap occurred with overflow"), Err(err) => match err { cdk::Error::NUT03(cdk::nuts::nut03::Error::Amount(_)) => (), cdk::Error::AmountOverflow => (), cdk::Error::AmountError(_) => (), _ => { println!("{:?}", err); bail!("Wrong error returned in swap overflow") } }, } Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] pub async fn test_p2pk_swap() -> Result<()> { let mint = initialize().await; let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); let proofs = mint_proofs(&mint, 100.into(), &SplitTarget::default(), keys).await?; let secret = SecretKey::generate(); let spending_conditions = SpendingConditions::new_p2pk(secret.public_key(), None); let pre_swap = PreMintSecrets::with_conditions( keyset_id, 100.into(), &SplitTarget::default(), &spending_conditions, )?; let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages()); let keys = mint.pubkeys().await?.keysets.first().cloned().unwrap().keys; let post_swap = mint.process_swap_request(swap_request).await?; let mut proofs = construct_proofs( post_swap.signatures, pre_swap.rs(), pre_swap.secrets(), &keys, )?; let pre_swap = PreMintSecrets::random(keyset_id, 100.into(), &SplitTarget::default())?; let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages()); let public_keys_to_listen: Vec<_> = swap_request .inputs .ys() .expect("key") .into_iter() .enumerate() .filter_map(|(key, pk)| { if key % 2 == 0 { // Only expect messages from every other key Some(pk.to_string()) } else { None } }) .collect(); let mut listener = mint .pubsub_manager .try_subscribe::( Params { kind: cdk::nuts::nut17::Kind::ProofState, filters: public_keys_to_listen.clone(), id: "test".into(), } .into(), ) .await .expect("valid subscription"); match mint.process_swap_request(swap_request).await { Ok(_) => bail!("Proofs spent without sig"), Err(err) => match err { cdk::Error::NUT11(cdk::nuts::nut11::Error::SignaturesNotProvided) => (), _ => { println!("{:?}", err); bail!("Wrong error returned") } }, } for proof in &mut proofs { proof.sign_p2pk(secret.clone())?; } let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages()); let attempt_swap = mint.process_swap_request(swap_request).await; assert!(attempt_swap.is_ok()); sleep(Duration::from_millis(10)).await; let mut msgs = HashMap::new(); while let Ok((sub_id, msg)) = listener.try_recv() { assert_eq!(sub_id, "test".into()); match msg { NotificationPayload::ProofState(ProofState { y, state, .. }) => { let pk = y.to_string(); msgs.get_mut(&pk) .map(|x: &mut Vec| { x.push(state); }) .unwrap_or_else(|| { msgs.insert(pk, vec![state]); }); } _ => bail!("Wrong message received"), } } for keys in public_keys_to_listen { let statuses = msgs.remove(&keys).expect("some events"); assert_eq!(statuses, vec![State::Pending, State::Pending, State::Spent]); } assert!(listener.try_recv().is_err(), "no other event is happening"); assert!(msgs.is_empty(), "Only expected key events are received"); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_swap_unbalanced() -> Result<()> { let mint = initialize().await; let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); let proofs = mint_proofs(&mint, 100.into(), &SplitTarget::default(), keys).await?; let preswap = PreMintSecrets::random(keyset_id, 95.into(), &SplitTarget::default())?; let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages()); match mint.process_swap_request(swap_request).await { Ok(_) => bail!("Swap was allowed unbalanced"), Err(err) => match err { cdk::Error::TransactionUnbalanced(_, _, _) => (), _ => bail!("Wrong error returned"), }, } let preswap = PreMintSecrets::random(keyset_id, 101.into(), &SplitTarget::default())?; let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages()); match mint.process_swap_request(swap_request).await { Ok(_) => bail!("Swap was allowed unbalanced"), Err(err) => match err { cdk::Error::TransactionUnbalanced(_, _, _) => (), _ => bail!("Wrong error returned"), }, } Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_swap_overpay_underpay_fee() -> Result<()> { let mint = new_mint(1).await; mint.rotate_keyset(CurrencyUnit::Sat, 1, 32, 1, &HashMap::new()) .await?; let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); let proofs = mint_proofs(&mint, 1000.into(), &SplitTarget::default(), keys).await?; let preswap = PreMintSecrets::random(keyset_id, 9998.into(), &SplitTarget::default())?; let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages()); // Attempt to swap overpaying fee match mint.process_swap_request(swap_request).await { Ok(_) => bail!("Swap was allowed unbalanced"), Err(err) => match err { cdk::Error::TransactionUnbalanced(_, _, _) => (), _ => { println!("{:?}", err); bail!("Wrong error returned") } }, } let preswap = PreMintSecrets::random(keyset_id, 1000.into(), &SplitTarget::default())?; let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages()); // Attempt to swap underpaying fee match mint.process_swap_request(swap_request).await { Ok(_) => bail!("Swap was allowed unbalanced"), Err(err) => match err { cdk::Error::TransactionUnbalanced(_, _, _) => (), _ => { println!("{:?}", err); bail!("Wrong error returned") } }, } Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_mint_enforce_fee() -> Result<()> { let mint = new_mint(1).await; let keys = mint.pubkeys().await?.keysets.first().unwrap().clone().keys; let keyset_id = Id::from(&keys); let mut proofs = mint_proofs(&mint, 1010.into(), &SplitTarget::Value(1.into()), keys).await?; let five_proofs: Vec<_> = proofs.drain(..5).collect(); let preswap = PreMintSecrets::random(keyset_id, 5.into(), &SplitTarget::default())?; let swap_request = SwapRequest::new(five_proofs.clone(), preswap.blinded_messages()); // Attempt to swap underpaying fee match mint.process_swap_request(swap_request).await { Ok(_) => bail!("Swap was allowed unbalanced"), Err(err) => match err { cdk::Error::TransactionUnbalanced(_, _, _) => (), _ => { println!("{:?}", err); bail!("Wrong error returned") } }, } let preswap = PreMintSecrets::random(keyset_id, 4.into(), &SplitTarget::default())?; let swap_request = SwapRequest::new(five_proofs.clone(), preswap.blinded_messages()); let _ = mint.process_swap_request(swap_request).await?; let thousnad_proofs: Vec<_> = proofs.drain(..1001).collect(); let preswap = PreMintSecrets::random(keyset_id, 1000.into(), &SplitTarget::default())?; let swap_request = SwapRequest::new(thousnad_proofs.clone(), preswap.blinded_messages()); // Attempt to swap underpaying fee match mint.process_swap_request(swap_request).await { Ok(_) => bail!("Swap was allowed unbalanced"), Err(err) => match err { cdk::Error::TransactionUnbalanced(_, _, _) => (), _ => { println!("{:?}", err); bail!("Wrong error returned") } }, } let preswap = PreMintSecrets::random(keyset_id, 999.into(), &SplitTarget::default())?; let swap_request = SwapRequest::new(thousnad_proofs.clone(), preswap.blinded_messages()); let _ = mint.process_swap_request(swap_request).await?; Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_correct_keyset() -> Result<()> { let mnemonic = Mnemonic::generate(12)?; let fee_reserve = FeeReserve { min_fee_reserve: 1.into(), percent_fee_reserve: 1.0, }; let database = memory::empty().await.expect("valid db instance"); let fake_wallet = FakeWallet::new(fee_reserve, HashMap::default(), HashSet::default(), 0); let mut mint_builder = MintBuilder::new(); let localstore = Arc::new(database); mint_builder = mint_builder.with_localstore(localstore.clone()); mint_builder = mint_builder.add_ln_backend( CurrencyUnit::Sat, PaymentMethod::Bolt11, MintMeltLimits::new(1, 5_000), Arc::new(fake_wallet), ); mint_builder = mint_builder .with_name("regtest mint".to_string()) .with_description("regtest mint".to_string()) .with_seed(mnemonic.to_seed_normalized("").to_vec()); let mint = mint_builder.build().await?; localstore .set_mint_info(mint_builder.mint_info.clone()) .await?; let quote_ttl = QuoteTTL::new(10000, 10000); localstore.set_quote_ttl(quote_ttl).await?; mint.rotate_next_keyset(CurrencyUnit::Sat, 32, 0).await?; mint.rotate_next_keyset(CurrencyUnit::Sat, 32, 0).await?; let active = mint.localstore.get_active_keysets().await?; let active = active .get(&CurrencyUnit::Sat) .expect("There is a keyset for unit"); let keyset_info = mint .localstore .get_keyset_info(active) .await? .expect("There is keyset"); assert!(keyset_info.derivation_path_index == Some(2)); let mint = mint_builder.build().await?; let active = mint.localstore.get_active_keysets().await?; let active = active .get(&CurrencyUnit::Sat) .expect("There is a keyset for unit"); let keyset_info = mint .localstore .get_keyset_info(active) .await? .expect("There is keyset"); assert!(keyset_info.derivation_path_index == Some(2)); Ok(()) }