| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316 |
- //! This file contains integration tests for the Cashu Development Kit (CDK)
- //!
- //! These tests verify the interaction between mint and wallet components, simulating real-world usage scenarios.
- //! They test the complete flow of operations including wallet funding, token swapping, sending tokens between wallets,
- //! and other operations that require client-mint interaction.
- //!
- //! Test Environment:
- //! - Uses pure in-memory mint instances for fast execution
- //! - Tests run concurrently with multi-threaded tokio runtime
- //! - No external dependencies (Lightning nodes, databases) required
- use std::assert_eq;
- use std::collections::{HashMap, HashSet};
- use std::hash::RandomState;
- use std::str::FromStr;
- use std::sync::Arc;
- use std::time::Duration;
- use cashu::amount::SplitTarget;
- use cashu::dhke::construct_proofs;
- use cashu::mint_url::MintUrl;
- use cashu::{
- CurrencyUnit, Id, MeltRequest, NotificationPayload, PaymentMethod, PreMintSecrets, ProofState,
- SecretKey, SpendingConditions, State, SwapRequest,
- };
- use cdk::mint::Mint;
- use cdk::nuts::nut00::ProofsMethods;
- use cdk::subscription::Params;
- use cdk::wallet::types::{TransactionDirection, TransactionId};
- use cdk::wallet::{ReceiveOptions, SendMemo, SendOptions, WalletTrait};
- use cdk::Amount;
- use cdk_fake_wallet::create_fake_invoice;
- use cdk_integration_tests::init_pure_tests::*;
- use tokio::time::sleep;
- /// Tests the token swap and send functionality:
- /// 1. Alice gets funded with 64 sats
- /// 2. Alice prepares to send 40 sats (which requires internal swapping)
- /// 3. Alice sends the token
- /// 4. Carol receives the token and has the correct balance
- #[tokio::test]
- async fn test_swap_to_send() {
- setup_tracing();
- let mint_bob = create_and_start_test_mint()
- .await
- .expect("Failed to create test mint");
- let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
- .await
- .expect("Failed to create test wallet");
- // Alice gets 64 sats
- fund_wallet(wallet_alice.clone(), 64, None)
- .await
- .expect("Failed to fund wallet");
- let balance_alice = wallet_alice
- .total_balance()
- .await
- .expect("Failed to get balance");
- assert_eq!(Amount::from(64), balance_alice);
- // Alice wants to send 40 sats, which internally swaps
- let prepared_send = wallet_alice
- .prepare_send(Amount::from(40), SendOptions::default())
- .await
- .expect("Failed to prepare send");
- assert_eq!(
- HashSet::<_, RandomState>::from_iter(
- prepared_send.proofs().ys().expect("Failed to get ys")
- ),
- HashSet::from_iter(
- wallet_alice
- .get_reserved_proofs()
- .await
- .expect("Failed to get reserved proofs")
- .ys()
- .expect("Failed to get ys")
- )
- );
- let token = prepared_send
- .confirm(Some(SendMemo::for_token("test_swapt_to_send")))
- .await
- .expect("Failed to send token");
- let keysets_info = wallet_alice.get_mint_keysets().await.unwrap();
- let token_proofs = token.proofs(&keysets_info).unwrap();
- assert_eq!(
- Amount::from(40),
- token_proofs
- .total_amount()
- .expect("Failed to get total amount")
- );
- assert_eq!(
- Amount::from(24),
- wallet_alice
- .total_balance()
- .await
- .expect("Failed to get balance")
- );
- assert_eq!(
- HashSet::<_, RandomState>::from_iter(token_proofs.ys().expect("Failed to get ys")),
- HashSet::from_iter(
- wallet_alice
- .get_pending_spent_proofs()
- .await
- .expect("Failed to get pending spent proofs")
- .ys()
- .expect("Failed to get ys")
- )
- );
- let transaction_id =
- TransactionId::from_proofs(token_proofs.clone()).expect("Failed to get tx id");
- let transaction = wallet_alice
- .get_transaction(transaction_id)
- .await
- .expect("Failed to get transaction")
- .expect("Transaction not found");
- assert_eq!(wallet_alice.mint_url, transaction.mint_url);
- assert_eq!(TransactionDirection::Outgoing, transaction.direction);
- assert_eq!(Amount::from(40), transaction.amount);
- assert_eq!(Amount::from(0), transaction.fee);
- assert_eq!(CurrencyUnit::Sat, transaction.unit);
- assert_eq!(token_proofs.ys().unwrap(), transaction.ys);
- // Alice sends cashu, Carol receives
- let wallet_carol = create_test_wallet_for_mint(mint_bob.clone())
- .await
- .expect("Failed to create Carol's wallet");
- let received_amount = wallet_carol
- .receive_proofs(
- token_proofs.clone(),
- ReceiveOptions::default(),
- token.memo().clone(),
- Some(token.to_string()),
- )
- .await
- .expect("Failed to receive proofs");
- assert_eq!(Amount::from(40), received_amount);
- assert_eq!(
- Amount::from(40),
- wallet_carol
- .total_balance()
- .await
- .expect("Failed to get Carol's balance")
- );
- let transaction = wallet_carol
- .get_transaction(transaction_id)
- .await
- .expect("Failed to get transaction")
- .expect("Transaction not found");
- assert_eq!(wallet_carol.mint_url, transaction.mint_url);
- assert_eq!(TransactionDirection::Incoming, transaction.direction);
- assert_eq!(Amount::from(40), transaction.amount);
- assert_eq!(Amount::from(0), transaction.fee);
- assert_eq!(CurrencyUnit::Sat, transaction.unit);
- assert_eq!(token_proofs.ys().unwrap(), transaction.ys);
- assert_eq!(token.memo().clone(), transaction.memo);
- }
- /// Tests the NUT-06 functionality (mint discovery):
- /// 1. Alice gets funded with 64 sats
- /// 2. Verifies the initial mint URL is in the mint info
- /// 3. Updates the mint URL to a new value
- /// 4. Verifies the wallet balance is maintained after changing the mint URL
- #[tokio::test]
- async fn test_mint_nut06() {
- setup_tracing();
- let mint_bob = create_and_start_test_mint()
- .await
- .expect("Failed to create test mint");
- let mut wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
- .await
- .expect("Failed to create test wallet");
- // Alice gets 64 sats
- fund_wallet(wallet_alice.clone(), 64, None)
- .await
- .expect("Failed to fund wallet");
- let balance_alice = wallet_alice
- .total_balance()
- .await
- .expect("Failed to get balance");
- assert_eq!(Amount::from(64), balance_alice);
- // Verify keyset amounts after minting
- let keyset_id = mint_bob.pubkeys().keysets.first().unwrap().id;
- let total_issued = mint_bob.total_issued().await.unwrap();
- let issued_amount = total_issued
- .get(&keyset_id)
- .copied()
- .unwrap_or(Amount::ZERO);
- assert_eq!(
- issued_amount,
- Amount::from(64),
- "Should have issued 64 sats"
- );
- let transaction = wallet_alice
- .list_transactions(None)
- .await
- .expect("Failed to list transactions")
- .pop()
- .expect("No transactions found");
- assert_eq!(wallet_alice.mint_url, transaction.mint_url);
- assert_eq!(TransactionDirection::Incoming, transaction.direction);
- assert_eq!(Amount::from(64), transaction.amount);
- assert_eq!(Amount::from(0), transaction.fee);
- assert_eq!(CurrencyUnit::Sat, transaction.unit);
- let initial_mint_url = wallet_alice.mint_url.clone();
- let mint_info_before = wallet_alice
- .fetch_mint_info()
- .await
- .expect("Failed to get mint info")
- .unwrap();
- assert!(mint_info_before
- .urls
- .unwrap()
- .contains(&initial_mint_url.to_string()));
- // Wallet updates mint URL
- let new_mint_url = MintUrl::from_str("https://new-mint-url").expect("Failed to parse mint URL");
- wallet_alice
- .update_mint_url(new_mint_url.clone())
- .await
- .expect("Failed to update mint URL");
- // Check balance after mint URL was updated
- let balance_alice_after = wallet_alice
- .total_balance()
- .await
- .expect("Failed to get balance after URL update");
- assert_eq!(Amount::from(64), balance_alice_after);
- }
- /// Attempt to double spend proofs on swap
- #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
- async fn test_mint_double_spend() {
- setup_tracing();
- let mint_bob = create_and_start_test_mint()
- .await
- .expect("Failed to create test mint");
- let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
- .await
- .expect("Failed to create test wallet");
- // Alice gets 64 sats
- fund_wallet(wallet_alice.clone(), 64, None)
- .await
- .expect("Failed to fund wallet");
- let proofs = wallet_alice
- .get_unspent_proofs()
- .await
- .expect("Could not get proofs");
- let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
- let keyset_id = keys.id;
- let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
- let preswap = PreMintSecrets::random(
- keyset_id,
- proofs.total_amount().unwrap(),
- &SplitTarget::default(),
- &fee_and_amounts,
- )
- .unwrap();
- let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
- let swap = mint_bob.process_swap_request(swap_request).await;
- assert!(swap.is_ok());
- let preswap_two = PreMintSecrets::random(
- keyset_id,
- proofs.total_amount().unwrap(),
- &SplitTarget::default(),
- &fee_and_amounts,
- )
- .unwrap();
- let swap_two_request = SwapRequest::new(proofs, preswap_two.blinded_messages());
- match mint_bob.process_swap_request(swap_two_request).await {
- Ok(_) => panic!("Proofs double spent"),
- Err(err) => match err {
- cdk::Error::TokenAlreadySpent => (),
- _ => panic!("Wrong error returned"),
- },
- }
- }
- /// 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() {
- setup_tracing();
- let mint_bob = create_and_start_test_mint()
- .await
- .expect("Failed to create test mint");
- let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
- .await
- .expect("Failed to create test wallet");
- // Alice gets 64 sats
- fund_wallet(wallet_alice.clone(), 64, None)
- .await
- .expect("Failed to fund wallet");
- let proofs = wallet_alice
- .get_unspent_proofs()
- .await
- .expect("Could not get proofs");
- let amount = 2_u64.pow(63);
- let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
- let keyset_id = keys.id;
- let pre_mint_amount = PreMintSecrets::from_secrets(
- keyset_id,
- vec![amount.into()],
- vec![cashu::secret::Secret::generate()],
- )
- .unwrap();
- let pre_mint_amount_two = PreMintSecrets::from_secrets(
- keyset_id,
- vec![amount.into()],
- vec![cashu::secret::Secret::generate()],
- )
- .unwrap();
- let mut pre_mint = PreMintSecrets::from_secrets(
- keyset_id,
- vec![1.into()],
- vec![cashu::secret::Secret::generate()],
- )
- .unwrap();
- 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_bob.process_swap_request(swap_request).await {
- Ok(_) => panic!("Swap occurred with overflow"),
- Err(err) => match err {
- cdk::Error::NUT03(cdk::nuts::nut03::Error::Amount(_)) => (),
- cdk::Error::AmountOverflow => (),
- cdk::Error::AmountError(_) => (),
- cdk::Error::TransactionUnbalanced(_, _, _) => (),
- _ => {
- panic!("Wrong error returned in swap overflow {:?}", err);
- }
- },
- }
- }
- /// Tests that the mint correctly rejects unbalanced swap requests:
- /// 1. Attempts to swap for less than the input amount (95 < 100)
- /// 2. Attempts to swap for more than the input amount (101 > 100)
- /// 3. Both should fail with TransactionUnbalanced error
- #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
- async fn test_swap_unbalanced() {
- setup_tracing();
- let mint_bob = create_and_start_test_mint()
- .await
- .expect("Failed to create test mint");
- let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
- .await
- .expect("Failed to create test wallet");
- // Alice gets 100 sats
- fund_wallet(wallet_alice.clone(), 100, None)
- .await
- .expect("Failed to fund wallet");
- let proofs = wallet_alice
- .get_unspent_proofs()
- .await
- .expect("Could not get proofs");
- let keyset_id = get_keyset_id(&mint_bob).await;
- let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
- // Try to swap for less than the input amount (95 < 100)
- let preswap = PreMintSecrets::random(
- keyset_id,
- 95.into(),
- &SplitTarget::default(),
- &fee_and_amounts,
- )
- .expect("Failed to create preswap");
- let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
- match mint_bob.process_swap_request(swap_request).await {
- Ok(_) => panic!("Swap was allowed unbalanced"),
- Err(err) => match err {
- cdk::Error::TransactionUnbalanced(_, _, _) => (),
- _ => panic!("Wrong error returned"),
- },
- }
- // Try to swap for more than the input amount (101 > 100)
- let preswap = PreMintSecrets::random(
- keyset_id,
- 101.into(),
- &SplitTarget::default(),
- &fee_and_amounts,
- )
- .expect("Failed to create preswap");
- let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
- match mint_bob.process_swap_request(swap_request).await {
- Ok(_) => panic!("Swap was allowed unbalanced"),
- Err(err) => match err {
- cdk::Error::TransactionUnbalanced(_, _, _) => (),
- _ => panic!("Wrong error returned"),
- },
- }
- }
- #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
- pub async fn test_p2pk_swap() {
- setup_tracing();
- let mint_bob = create_and_start_test_mint()
- .await
- .expect("Failed to create test mint");
- let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
- .await
- .expect("Failed to create test wallet");
- // Alice gets 100 sats
- fund_wallet(wallet_alice.clone(), 100, None)
- .await
- .expect("Failed to fund wallet");
- let proofs = wallet_alice
- .get_unspent_proofs()
- .await
- .expect("Could not get proofs");
- let keyset_id = get_keyset_id(&mint_bob).await;
- let secret = SecretKey::generate();
- let spending_conditions = SpendingConditions::new_p2pk(secret.public_key(), None);
- let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
- let pre_swap = PreMintSecrets::with_conditions(
- keyset_id,
- 100.into(),
- &SplitTarget::default(),
- &spending_conditions,
- &fee_and_amounts,
- )
- .unwrap();
- let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages());
- let keys = mint_bob.pubkeys().keysets.first().cloned().unwrap().keys;
- let post_swap = mint_bob.process_swap_request(swap_request).await.unwrap();
- let mut proofs = construct_proofs(
- post_swap.signatures,
- pre_swap.rs(),
- pre_swap.secrets(),
- &keys,
- )
- .unwrap();
- let pre_swap = PreMintSecrets::random(
- keyset_id,
- 100.into(),
- &SplitTarget::default(),
- &fee_and_amounts,
- )
- .unwrap();
- let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages());
- // Listen for status updates on all input proof pks
- let public_keys_to_listen: Vec<_> = swap_request
- .inputs()
- .ys()
- .unwrap()
- .iter()
- .map(|pk| pk.to_string())
- .collect();
- let mut listener = mint_bob
- .pubsub_manager()
- .subscribe(Params {
- kind: cdk::nuts::nut17::Kind::ProofState,
- filters: public_keys_to_listen.clone(),
- id: Arc::new("test".into()),
- })
- .expect("valid subscription");
- match mint_bob.process_swap_request(swap_request).await {
- Ok(_) => panic!("Proofs spent without sig"),
- Err(err) => match err {
- cdk::Error::NUT11(cdk::nuts::nut11::Error::SignaturesNotProvided) => (),
- _ => {
- println!("{:?}", err);
- panic!("Wrong error returned")
- }
- },
- }
- for proof in &mut proofs {
- proof.sign_p2pk(secret.clone()).unwrap();
- }
- let swap_request = SwapRequest::new(proofs.clone(), pre_swap.blinded_messages());
- let attempt_swap = mint_bob.process_swap_request(swap_request).await;
- assert!(attempt_swap.is_ok());
- sleep(Duration::from_secs(1)).await;
- let mut msgs = HashMap::new();
- while let Some(msg) = listener.try_recv() {
- match msg.into_inner() {
- NotificationPayload::ProofState(ProofState { y, state, .. }) => {
- msgs.entry(y.to_string())
- .or_insert_with(Vec::new)
- .push(state);
- }
- _ => panic!("Wrong message received"),
- }
- }
- for (i, key) in public_keys_to_listen.into_iter().enumerate() {
- let statuses = msgs.remove(&key).expect("some events");
- // Every input pk receives two state updates, as there are only two state transitions
- assert_eq!(
- statuses,
- vec![State::Pending, State::Spent],
- "failed to test key {:?} (pos {})",
- key,
- i,
- );
- }
- assert!(listener.try_recv().is_none(), "no other event is happening");
- assert!(msgs.is_empty(), "Only expected key events are received");
- }
- #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
- async fn test_swap_overpay_underpay_fee() {
- setup_tracing();
- let mint_bob = create_and_start_test_mint()
- .await
- .expect("Failed to create test mint");
- mint_bob
- .rotate_keyset(
- CurrencyUnit::Sat,
- cdk_integration_tests::standard_keyset_amounts(32),
- 1,
- true,
- )
- .await
- .unwrap();
- let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
- .await
- .expect("Failed to create test wallet");
- // Alice gets 100 sats
- fund_wallet(wallet_alice.clone(), 1000, None)
- .await
- .expect("Failed to fund wallet");
- let proofs = wallet_alice
- .get_unspent_proofs()
- .await
- .expect("Could not get proofs");
- let keyset_id = mint_bob.pubkeys().keysets.first().unwrap().id;
- let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
- let preswap = PreMintSecrets::random(
- keyset_id,
- 9998.into(),
- &SplitTarget::default(),
- &fee_and_amounts,
- )
- .unwrap();
- let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
- // Attempt to swap overpaying fee
- match mint_bob.process_swap_request(swap_request).await {
- Ok(_) => panic!("Swap was allowed unbalanced"),
- Err(err) => match err {
- cdk::Error::TransactionUnbalanced(_, _, _) => (),
- _ => {
- println!("{:?}", err);
- panic!("Wrong error returned")
- }
- },
- }
- let preswap = PreMintSecrets::random(
- keyset_id,
- 1000.into(),
- &SplitTarget::default(),
- &fee_and_amounts,
- )
- .unwrap();
- let swap_request = SwapRequest::new(proofs.clone(), preswap.blinded_messages());
- // Attempt to swap underpaying fee
- match mint_bob.process_swap_request(swap_request).await {
- Ok(_) => panic!("Swap was allowed unbalanced"),
- Err(err) => match err {
- cdk::Error::TransactionUnbalanced(_, _, _) => (),
- _ => {
- println!("{:?}", err);
- panic!("Wrong error returned")
- }
- },
- }
- }
- #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
- async fn test_mint_enforce_fee() {
- setup_tracing();
- let mint_bob = create_and_start_test_mint()
- .await
- .expect("Failed to create test mint");
- mint_bob
- .rotate_keyset(
- CurrencyUnit::Sat,
- cdk_integration_tests::standard_keyset_amounts(32),
- 1,
- true,
- )
- .await
- .unwrap();
- let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
- .await
- .expect("Failed to create test wallet");
- // Alice gets 100 sats
- fund_wallet(
- wallet_alice.clone(),
- 1010,
- Some(SplitTarget::Value(Amount::ONE)),
- )
- .await
- .expect("Failed to fund wallet");
- let mut proofs = wallet_alice
- .get_unspent_proofs()
- .await
- .expect("Could not get proofs");
- let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
- let keyset_id = keys.id;
- let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
- let five_proofs: Vec<_> = proofs.drain(..5).collect();
- let preswap = PreMintSecrets::random(
- keyset_id,
- 5.into(),
- &SplitTarget::default(),
- &fee_and_amounts,
- )
- .unwrap();
- let swap_request = SwapRequest::new(five_proofs.clone(), preswap.blinded_messages());
- // Attempt to swap underpaying fee
- match mint_bob.process_swap_request(swap_request).await {
- Ok(_) => panic!("Swap was allowed unbalanced"),
- Err(err) => match err {
- cdk::Error::TransactionUnbalanced(_, _, _) => (),
- _ => {
- println!("{:?}", err);
- panic!("Wrong error returned")
- }
- },
- }
- let preswap = PreMintSecrets::random(
- keyset_id,
- 4.into(),
- &SplitTarget::default(),
- &fee_and_amounts,
- )
- .unwrap();
- let swap_request = SwapRequest::new(five_proofs.clone(), preswap.blinded_messages());
- let res = mint_bob.process_swap_request(swap_request).await;
- assert!(res.is_ok());
- let thousnad_proofs: Vec<_> = proofs.drain(..1001).collect();
- let preswap = PreMintSecrets::random(
- keyset_id,
- 1000.into(),
- &SplitTarget::default(),
- &fee_and_amounts,
- )
- .unwrap();
- let swap_request = SwapRequest::new(thousnad_proofs.clone(), preswap.blinded_messages());
- // Attempt to swap underpaying fee
- match mint_bob.process_swap_request(swap_request).await {
- Ok(_) => panic!("Swap was allowed unbalanced"),
- Err(err) => match err {
- cdk::Error::TransactionUnbalanced(_, _, _) => (),
- _ => {
- println!("{:?}", err);
- panic!("Wrong error returned")
- }
- },
- }
- let preswap = PreMintSecrets::random(
- keyset_id,
- 999.into(),
- &SplitTarget::default(),
- &fee_and_amounts,
- )
- .unwrap();
- let swap_request = SwapRequest::new(thousnad_proofs.clone(), preswap.blinded_messages());
- let _ = mint_bob.process_swap_request(swap_request).await.unwrap();
- }
- #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
- async fn test_mint_max_outputs_exceeded_mint() {
- setup_tracing();
- // Set max outputs to 5
- let mint_bob = create_mint_with_limits(Some((100, 5)))
- .await
- .expect("Failed to create test mint");
- let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
- .await
- .expect("Failed to create test wallet");
- // Alice tries to mint 10 sats with split target 1 (requesting 10 outputs)
- // This should fail because max_outputs is 5
- let result = fund_wallet(
- wallet_alice.clone(),
- 10,
- Some(SplitTarget::Value(Amount::ONE)),
- )
- .await;
- match result {
- Ok(_) => panic!("Mint allowed exceeding max outputs"),
- Err(err) => {
- if let Some(cdk::Error::MaxOutputsExceeded { actual, max }) =
- err.downcast_ref::<cdk::Error>()
- {
- // actual might be more than 10 depending on internal splitting logic, but certainly > 5
- assert!(*actual >= 10);
- assert_eq!(*max, 5);
- } else {
- panic!("Wrong error returned: {:?}", err);
- }
- }
- }
- }
- #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
- async fn test_mint_max_inputs_exceeded_melt() {
- setup_tracing();
- // Set max inputs to 5
- let mint_bob = create_mint_with_limits(Some((5, 100)))
- .await
- .expect("Failed to create test mint");
- let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
- .await
- .expect("Failed to create test wallet");
- // Alice gets 10 sats with small outputs to have enough proofs
- fund_wallet(
- wallet_alice.clone(),
- 10,
- Some(SplitTarget::Value(Amount::ONE)),
- )
- .await
- .expect("Failed to fund wallet");
- let proofs = wallet_alice
- .get_unspent_proofs()
- .await
- .expect("Could not get proofs");
- // Use 6 proofs (limit is 5)
- let six_proofs: Vec<_> = proofs.iter().take(6).cloned().collect();
- assert_eq!(six_proofs.len(), 6);
- let fake_invoice = create_fake_invoice(1000, "".to_string());
- let melt_quote = wallet_alice
- .melt_quote(PaymentMethod::BOLT11, fake_invoice.to_string(), None, None)
- .await
- .expect("Failed to create melt quote");
- let melt_request = MeltRequest::new(melt_quote.id.parse().unwrap(), six_proofs, None);
- match mint_bob.melt(&melt_request).await {
- Ok(_) => panic!("Melt allowed exceeding max inputs"),
- Err(err) => match err {
- cdk::Error::MaxInputsExceeded { actual, max } => {
- assert_eq!(actual, 6);
- assert_eq!(max, 5);
- }
- _ => panic!("Wrong error returned: {:?}", err),
- },
- }
- }
- #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
- async fn test_mint_max_outputs_exceeded_melt() {
- setup_tracing();
- // Set max outputs to 20
- let mint_bob = create_mint_with_limits(Some((100, 20)))
- .await
- .expect("Failed to create test mint");
- let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
- .await
- .expect("Failed to create test wallet");
- // Alice gets 100 sats
- fund_wallet(wallet_alice.clone(), 100, None)
- .await
- .expect("Failed to fund wallet");
- let proofs = wallet_alice
- .get_unspent_proofs()
- .await
- .expect("Could not get proofs");
- let fake_invoice = create_fake_invoice(1000, "".to_string()); // 1000 msat = 1 sat
- let melt_quote = wallet_alice
- .melt_quote(PaymentMethod::BOLT11, fake_invoice.to_string(), None, None)
- .await
- .expect("Failed to create melt quote");
- let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
- let keyset_id = keys.id;
- let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
- // Create 21 blinded messages for change (limit is 20)
- let preswap = PreMintSecrets::random(
- keyset_id,
- 21.into(),
- &SplitTarget::Value(Amount::ONE),
- &fee_and_amounts,
- )
- .unwrap();
- let change_messages = preswap.blinded_messages();
- assert!(change_messages.len() >= 21);
- let excessive_change: Vec<_> = change_messages.into_iter().take(21).collect();
- let melt_request = MeltRequest::new(
- melt_quote.id.parse().unwrap(),
- proofs,
- Some(excessive_change),
- );
- match mint_bob.melt(&melt_request).await {
- Ok(_) => panic!("Melt allowed exceeding max outputs"),
- Err(err) => match err {
- cdk::Error::MaxOutputsExceeded { actual, max } => {
- assert_eq!(actual, 21);
- assert_eq!(max, 20);
- }
- _ => panic!("Wrong error returned: {:?}", err),
- },
- }
- }
- #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
- async fn test_mint_max_inputs_exceeded() {
- setup_tracing();
- let mint_bob = create_mint_with_limits(Some((5, 100)))
- .await
- .expect("Failed to create test mint");
- let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
- .await
- .expect("Failed to create test wallet");
- // Alice gets 100 sats with small outputs to have enough proofs
- fund_wallet(
- wallet_alice.clone(),
- 100,
- Some(SplitTarget::Value(Amount::ONE)),
- )
- .await
- .expect("Failed to fund wallet");
- let proofs = wallet_alice
- .get_unspent_proofs()
- .await
- .expect("Could not get proofs");
- let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
- let keyset_id = keys.id;
- let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
- // Use 6 proofs (limit is 5)
- let six_proofs: Vec<_> = proofs.iter().take(6).cloned().collect();
- assert_eq!(six_proofs.len(), 6);
- let preswap = PreMintSecrets::random(
- keyset_id,
- 6.into(),
- &SplitTarget::default(),
- &fee_and_amounts,
- )
- .unwrap();
- let swap_request = SwapRequest::new(six_proofs, preswap.blinded_messages());
- match mint_bob.process_swap_request(swap_request).await {
- Ok(_) => panic!("Swap allowed exceeding max inputs"),
- Err(err) => match err {
- cdk::Error::MaxInputsExceeded { actual, max } => {
- assert_eq!(actual, 6);
- assert_eq!(max, 5);
- }
- _ => panic!("Wrong error returned: {:?}", err),
- },
- }
- }
- #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
- async fn test_mint_max_outputs_exceeded() {
- setup_tracing();
- // Set max outputs to 20
- let mint_bob = create_mint_with_limits(Some((100, 20)))
- .await
- .expect("Failed to create test mint");
- let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
- .await
- .expect("Failed to create test wallet");
- // Alice gets 50 sats
- fund_wallet(wallet_alice.clone(), 50, None)
- .await
- .expect("Failed to fund wallet");
- let proofs = wallet_alice
- .get_unspent_proofs()
- .await
- .expect("Could not get proofs");
- let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
- let keyset_id = keys.id;
- let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
- // Try to split into 21 outputs (limit is 20)
- let preswap = PreMintSecrets::random(
- keyset_id,
- 21.into(),
- &SplitTarget::Value(Amount::ONE),
- &fee_and_amounts,
- )
- .unwrap();
- // Verify we generated enough messages
- let messages = preswap.blinded_messages();
- // We expect 21 messages because we asked for split target of 1 sat for 21 sats total.
- assert!(messages.len() >= 21);
- // Just take 21 messages to trigger the limit
- let excessive_messages: Vec<_> = messages.into_iter().take(21).collect();
- let swap_request = SwapRequest::new(proofs, excessive_messages);
- match mint_bob.process_swap_request(swap_request).await {
- Ok(_) => panic!("Swap allowed exceeding max outputs"),
- Err(err) => match err {
- cdk::Error::MaxOutputsExceeded { actual, max } => {
- assert_eq!(actual, 21);
- assert_eq!(max, 20);
- }
- _ => panic!("Wrong error returned: {:?}", err),
- },
- }
- }
- #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
- async fn test_mint_change_with_fee_melt() {
- setup_tracing();
- let mint_bob = create_and_start_test_mint()
- .await
- .expect("Failed to create test mint");
- mint_bob
- .rotate_keyset(
- CurrencyUnit::Sat,
- cdk_integration_tests::standard_keyset_amounts(32),
- 1,
- true,
- )
- .await
- .unwrap();
- let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
- .await
- .expect("Failed to create test wallet");
- // Alice gets 100 sats
- fund_wallet(
- wallet_alice.clone(),
- 100,
- Some(SplitTarget::Value(Amount::ONE)),
- )
- .await
- .expect("Failed to fund wallet");
- let keyset_id = mint_bob.pubkeys().keysets.first().unwrap().id;
- // Check amounts after minting
- let total_issued = mint_bob.total_issued().await.unwrap();
- let total_redeemed = mint_bob.total_redeemed().await.unwrap();
- let initial_issued = total_issued.get(&keyset_id).copied().unwrap_or_default();
- let initial_redeemed = total_redeemed
- .get(&keyset_id)
- .copied()
- .unwrap_or(Amount::ZERO);
- assert_eq!(
- initial_issued,
- Amount::from(100),
- "Should have issued 100 sats, got {:?}",
- total_issued
- );
- assert_eq!(
- initial_redeemed,
- Amount::ZERO,
- "Should have redeemed 0 sats initially, "
- );
- let proofs = wallet_alice
- .get_unspent_proofs()
- .await
- .expect("Could not get proofs");
- let fake_invoice = create_fake_invoice(1000, "".to_string());
- let melt_quote = wallet_alice
- .melt_quote(PaymentMethod::BOLT11, fake_invoice.to_string(), None, None)
- .await
- .unwrap();
- let prepared = wallet_alice
- .prepare_melt_proofs(&melt_quote.id, proofs, std::collections::HashMap::new())
- .await
- .unwrap();
- let w = prepared.confirm().await.unwrap();
- assert_eq!(w.change().unwrap().total_amount().unwrap(), 97.into());
- // Check amounts after melting
- // Melting redeems 100 sats and issues 97 sats as change
- let total_issued = mint_bob.total_issued().await.unwrap();
- let total_redeemed = mint_bob.total_redeemed().await.unwrap();
- let after_issued = total_issued
- .get(&keyset_id)
- .copied()
- .unwrap_or(Amount::ZERO);
- let after_redeemed = total_redeemed
- .get(&keyset_id)
- .copied()
- .unwrap_or(Amount::ZERO);
- assert_eq!(
- after_issued,
- Amount::from(197),
- "Should have issued 197 sats total (100 initial + 97 change)"
- );
- assert_eq!(
- after_redeemed,
- Amount::from(100),
- "Should have redeemed 100 sats from the melt"
- );
- }
- /// Tests concurrent double-spending attempts by trying to use the same proofs
- /// in 3 swap transactions simultaneously using tokio tasks
- #[tokio::test(flavor = "multi_thread", worker_threads = 3)]
- async fn test_concurrent_double_spend_swap() {
- setup_tracing();
- let mint_bob = create_and_start_test_mint()
- .await
- .expect("Failed to create test mint");
- let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
- .await
- .expect("Failed to create test wallet");
- // Alice gets 100 sats
- fund_wallet(wallet_alice.clone(), 100, None)
- .await
- .expect("Failed to fund wallet");
- let proofs = wallet_alice
- .get_unspent_proofs()
- .await
- .expect("Could not get proofs");
- let keyset_id = get_keyset_id(&mint_bob).await;
- let fee_and_amounts = (0, ((0..32).map(|x| 2u64.pow(x)).collect::<Vec<_>>())).into();
- // Create 3 identical swap requests with the same proofs
- let preswap1 = PreMintSecrets::random(
- keyset_id,
- 100.into(),
- &SplitTarget::default(),
- &fee_and_amounts,
- )
- .expect("Failed to create preswap");
- let swap_request1 = SwapRequest::new(proofs.clone(), preswap1.blinded_messages());
- let preswap2 = PreMintSecrets::random(
- keyset_id,
- 100.into(),
- &SplitTarget::default(),
- &fee_and_amounts,
- )
- .expect("Failed to create preswap");
- let swap_request2 = SwapRequest::new(proofs.clone(), preswap2.blinded_messages());
- let preswap3 = PreMintSecrets::random(
- keyset_id,
- 100.into(),
- &SplitTarget::default(),
- &fee_and_amounts,
- )
- .expect("Failed to create preswap");
- let swap_request3 = SwapRequest::new(proofs.clone(), preswap3.blinded_messages());
- // Spawn 3 concurrent tasks to process the swap requests
- let mint_clone1 = mint_bob.clone();
- let mint_clone2 = mint_bob.clone();
- let mint_clone3 = mint_bob.clone();
- let task1 = tokio::spawn(async move { mint_clone1.process_swap_request(swap_request1).await });
- let task2 = tokio::spawn(async move { mint_clone2.process_swap_request(swap_request2).await });
- let task3 = tokio::spawn(async move { mint_clone3.process_swap_request(swap_request3).await });
- // Wait for all tasks to complete
- let results = tokio::try_join!(task1, task2, task3).expect("Tasks failed to complete");
- // Count successes and failures
- let mut success_count = 0;
- let mut token_already_spent_count = 0;
- for result in [results.0, results.1, results.2] {
- match result {
- Ok(_) => success_count += 1,
- Err(err) => match err {
- cdk::Error::TokenAlreadySpent | cdk::Error::TokenPending => {
- token_already_spent_count += 1
- }
- other_err => panic!("Unexpected error: {:?}", other_err),
- },
- }
- }
- // Only one swap should succeed, the other two should fail with TokenAlreadySpent
- assert_eq!(1, success_count, "Expected exactly one successful swap");
- assert_eq!(
- 2, token_already_spent_count,
- "Expected exactly two TokenAlreadySpent errors"
- );
- // Verify that all proofs are marked as spent in the mint
- let states = mint_bob
- .localstore()
- .get_proofs_states(&proofs.iter().map(|p| p.y().unwrap()).collect::<Vec<_>>())
- .await
- .expect("Failed to get proof state");
- for state in states {
- assert_eq!(
- State::Spent,
- state.expect("Known state"),
- "Expected proof to be marked as spent, but got {:?}",
- state
- );
- }
- }
- /// Tests concurrent double-spending attempts by trying to use the same proofs
- /// in 3 melt transactions simultaneously using tokio tasks
- #[tokio::test(flavor = "multi_thread", worker_threads = 3)]
- async fn test_concurrent_double_spend_melt() {
- setup_tracing();
- let mint_bob = create_and_start_test_mint()
- .await
- .expect("Failed to create test mint");
- let wallet_alice = create_test_wallet_for_mint(mint_bob.clone())
- .await
- .expect("Failed to create test wallet");
- // Alice gets 100 sats
- fund_wallet(wallet_alice.clone(), 100, None)
- .await
- .expect("Failed to fund wallet");
- let proofs = wallet_alice
- .get_unspent_proofs()
- .await
- .expect("Could not get proofs");
- // Create a Lightning invoice for the melt
- let invoice = create_fake_invoice(1000, "".to_string());
- // Create a melt quote
- let melt_quote = wallet_alice
- .melt_quote(PaymentMethod::BOLT11, invoice.to_string(), None, None)
- .await
- .expect("Failed to create melt quote");
- // Get the quote ID and payment request
- let quote_id = melt_quote.id.clone();
- // Create 3 identical melt requests with the same proofs
- let mint_clone1 = mint_bob.clone();
- let mint_clone2 = mint_bob.clone();
- let mint_clone3 = mint_bob.clone();
- let melt_request = MeltRequest::new(quote_id.parse().unwrap(), proofs.clone(), None);
- let melt_request2 = melt_request.clone();
- let melt_request3 = melt_request.clone();
- // Spawn 3 concurrent tasks to process the melt requests
- let task1 = tokio::spawn(async move { mint_clone1.melt(&melt_request).await });
- let task2 = tokio::spawn(async move { mint_clone2.melt(&melt_request2).await });
- let task3 = tokio::spawn(async move { mint_clone3.melt(&melt_request3).await });
- // Wait for all tasks to complete
- let results = tokio::try_join!(task1, task2, task3).expect("Tasks failed to complete");
- // Count successes and failures
- let mut success_count = 0;
- let mut token_already_spent_count = 0;
- for result in [results.0, results.1, results.2] {
- match result {
- Ok(_) => success_count += 1,
- Err(err) => match err {
- cdk::Error::TokenAlreadySpent | cdk::Error::TokenPending => {
- token_already_spent_count += 1;
- println!("Got expected error: {:?}", err);
- }
- other_err => {
- println!("Got unexpected error: {:?}", other_err);
- token_already_spent_count += 1;
- }
- },
- }
- }
- // Only one melt should succeed, the other two should fail
- assert_eq!(1, success_count, "Expected exactly one successful melt");
- assert_eq!(
- 2, token_already_spent_count,
- "Expected exactly two TokenAlreadySpent errors"
- );
- // Verify that all proofs are marked as spent in the mint
- let states = mint_bob
- .localstore()
- .get_proofs_states(&proofs.iter().map(|p| p.y().unwrap()).collect::<Vec<_>>())
- .await
- .expect("Failed to get proof state");
- for state in states {
- assert_eq!(
- State::Spent,
- state.expect("Known state"),
- "Expected proof to be marked as spent, but got {:?}",
- state
- );
- }
- }
- async fn get_keyset_id(mint: &Mint) -> Id {
- let keys = mint.pubkeys().keysets.first().unwrap().clone();
- keys.verify_id()
- .expect("Keyset ID generation is successful");
- keys.id
- }
|