//! Mint tests use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use anyhow::{bail, Result}; use bip39::Mnemonic; use cdk::amount::{Amount, SplitTarget}; use cdk::cdk_database::mint_memory::MintMemoryDatabase; use cdk::dhke::construct_proofs; use cdk::mint::MintQuote; use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::nut17::Params; use cdk::nuts::{ CurrencyUnit, Id, MintBolt11Request, MintInfo, NotificationPayload, Nuts, PreMintSecrets, ProofState, Proofs, SecretKey, SpendingConditions, State, SwapRequest, }; use cdk::types::QuoteTTL; use cdk::util::unix_time; use cdk::Mint; use tokio::sync::OnceCell; use tokio::time::sleep; pub const MINT_URL: &str = "http://127.0.0.1:8088"; static INSTANCE: OnceCell = OnceCell::const_new(); 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 mnemonic = Mnemonic::generate(12).unwrap(); let quote_ttl = QuoteTTL::new(10000, 10000); Mint::new( MINT_URL, &mnemonic.to_seed_normalized(""), mint_info, quote_ttl, Arc::new(MintMemoryDatabase::default()), HashMap::new(), supported_units, HashMap::new(), ) .await .unwrap() } async fn initialize() -> &'static Mint { INSTANCE.get_or_init(|| 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( mint.mint_url.clone(), "".to_string(), CurrencyUnit::Sat, amount, unix_time() + 36000, request_lookup.to_string(), ); 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(), }; 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(_)) => (), _ => { 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(), }) .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(()) }