use std::sync::Arc; use anyhow::Result; use bip39::Mnemonic; use cdk::amount::SplitTarget; use cdk::cdk_database::WalletMemoryDatabase; use cdk::nuts::{ CurrencyUnit, MeltBolt11Request, MeltQuoteState, MintQuoteState, NotificationPayload, PreMintSecrets, State, }; use cdk::wallet::client::{HttpClient, MintConnector}; use cdk::wallet::{Wallet, WalletSubscription}; use cdk_fake_wallet::{create_fake_invoice, FakeInvoiceDescription}; use cdk_integration_tests::attempt_to_swap_pending; const MINT_URL: &str = "http://127.0.0.1:8086"; // If both pay and check return pending input proofs should remain pending #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_fake_tokens_pending() -> Result<()> { let wallet = Wallet::new( MINT_URL, CurrencyUnit::Sat, Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet .mint(&mint_quote.id, SplitTarget::default(), None) .await?; let fake_description = FakeInvoiceDescription { pay_invoice_state: MeltQuoteState::Pending, check_payment_state: MeltQuoteState::Pending, pay_err: false, check_err: false, }; let invoice = create_fake_invoice(1000, serde_json::to_string(&fake_description).unwrap()); let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; let melt = wallet.melt(&melt_quote.id).await; assert!(melt.is_err()); attempt_to_swap_pending(&wallet).await?; Ok(()) } // If the pay error fails and the check returns unknown or failed // The inputs proofs should be unset as spending #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_fake_melt_payment_fail() -> Result<()> { let wallet = Wallet::new( MINT_URL, CurrencyUnit::Sat, Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet .mint(&mint_quote.id, SplitTarget::default(), None) .await?; let fake_description = FakeInvoiceDescription { pay_invoice_state: MeltQuoteState::Unknown, check_payment_state: MeltQuoteState::Unknown, pay_err: true, check_err: false, }; let invoice = create_fake_invoice(1000, serde_json::to_string(&fake_description).unwrap()); let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; // The melt should error at the payment invoice command let melt = wallet.melt(&melt_quote.id).await; assert!(melt.is_err()); let fake_description = FakeInvoiceDescription { pay_invoice_state: MeltQuoteState::Failed, check_payment_state: MeltQuoteState::Failed, pay_err: true, check_err: false, }; let invoice = create_fake_invoice(1000, serde_json::to_string(&fake_description).unwrap()); let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; // The melt should error at the payment invoice command let melt = wallet.melt(&melt_quote.id).await; assert!(melt.is_err()); // The mint should have unset proofs from pending since payment failed let all_proof = wallet.get_unspent_proofs().await?; let states = wallet.check_proofs_spent(all_proof).await?; for state in states { assert!(state.state == State::Unspent); } let wallet_bal = wallet.total_balance().await?; assert!(wallet_bal == 100.into()); Ok(()) } // When both the pay_invoice and check_invoice both fail // the proofs should remain as pending #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_fake_melt_payment_fail_and_check() -> Result<()> { let wallet = Wallet::new( MINT_URL, CurrencyUnit::Sat, Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet .mint(&mint_quote.id, SplitTarget::default(), None) .await?; let fake_description = FakeInvoiceDescription { pay_invoice_state: MeltQuoteState::Unknown, check_payment_state: MeltQuoteState::Unknown, pay_err: true, check_err: true, }; let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap()); let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; // The melt should error at the payment invoice command let melt = wallet.melt(&melt_quote.id).await; assert!(melt.is_err()); let pending = wallet .localstore .get_proofs(None, None, Some(vec![State::Pending]), None) .await?; assert!(!pending.is_empty()); Ok(()) } // In the case that the ln backend returns a failed status but does not error // The mint should do a second check, then remove proofs from pending #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_fake_melt_payment_return_fail_status() -> Result<()> { let wallet = Wallet::new( MINT_URL, CurrencyUnit::Sat, Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet .mint(&mint_quote.id, SplitTarget::default(), None) .await?; let fake_description = FakeInvoiceDescription { pay_invoice_state: MeltQuoteState::Failed, check_payment_state: MeltQuoteState::Failed, pay_err: false, check_err: false, }; let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap()); let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; // The melt should error at the payment invoice command let melt = wallet.melt(&melt_quote.id).await; assert!(melt.is_err()); let fake_description = FakeInvoiceDescription { pay_invoice_state: MeltQuoteState::Unknown, check_payment_state: MeltQuoteState::Unknown, pay_err: false, check_err: false, }; let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap()); let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; // The melt should error at the payment invoice command let melt = wallet.melt(&melt_quote.id).await; assert!(melt.is_err()); let pending = wallet .localstore .get_proofs(None, None, Some(vec![State::Pending]), None) .await?; assert!(pending.is_empty()); Ok(()) } // In the case that the ln backend returns a failed status but does not error // The mint should do a second check, then remove proofs from pending #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_fake_melt_payment_error_unknown() -> Result<()> { let wallet = Wallet::new( MINT_URL, CurrencyUnit::Sat, Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet .mint(&mint_quote.id, SplitTarget::default(), None) .await?; let fake_description = FakeInvoiceDescription { pay_invoice_state: MeltQuoteState::Failed, check_payment_state: MeltQuoteState::Unknown, pay_err: true, check_err: false, }; let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap()); let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; // The melt should error at the payment invoice command let melt = wallet.melt(&melt_quote.id).await; assert!(melt.is_err()); let fake_description = FakeInvoiceDescription { pay_invoice_state: MeltQuoteState::Unknown, check_payment_state: MeltQuoteState::Unknown, pay_err: true, check_err: false, }; let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap()); let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; // The melt should error at the payment invoice command let melt = wallet.melt(&melt_quote.id).await; assert!(melt.is_err()); let pending = wallet .localstore .get_proofs(None, None, Some(vec![State::Pending]), None) .await?; assert!(pending.is_empty()); Ok(()) } // In the case that the ln backend returns an err // The mint should do a second check, that returns paid // Proofs should remain pending #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_fake_melt_payment_err_paid() -> Result<()> { let wallet = Wallet::new( MINT_URL, CurrencyUnit::Sat, Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet .mint(&mint_quote.id, SplitTarget::default(), None) .await?; let fake_description = FakeInvoiceDescription { pay_invoice_state: MeltQuoteState::Failed, check_payment_state: MeltQuoteState::Paid, pay_err: true, check_err: false, }; let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap()); let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; // The melt should error at the payment invoice command let melt = wallet.melt(&melt_quote.id).await; assert!(melt.is_err()); attempt_to_swap_pending(&wallet).await?; Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_fake_melt_change_in_quote() -> Result<()> { let wallet = Wallet::new( MINT_URL, CurrencyUnit::Sat, Arc::new(WalletMemoryDatabase::default()), &Mnemonic::generate(12)?.to_seed_normalized(""), None, )?; let mint_quote = wallet.mint_quote(100.into(), None).await?; wait_for_mint_to_be_paid(&wallet, &mint_quote.id).await?; let _mint_amount = wallet .mint(&mint_quote.id, SplitTarget::default(), None) .await?; let fake_description = FakeInvoiceDescription::default(); let invoice = create_fake_invoice(9000, serde_json::to_string(&fake_description).unwrap()); let proofs = wallet.get_unspent_proofs().await?; let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; let keyset = wallet.get_active_mint_keyset().await?; let premint_secrets = PreMintSecrets::random(keyset.id, 100.into(), &SplitTarget::default())?; let client = HttpClient::new(MINT_URL.parse()?); let melt_request = MeltBolt11Request { quote: melt_quote.id.clone(), inputs: proofs.clone(), outputs: Some(premint_secrets.blinded_messages()), }; let melt_response = client.post_melt(melt_request).await?; assert!(melt_response.change.is_some()); let check = wallet.melt_quote_status(&melt_quote.id).await?; let mut melt_change = melt_response.change.unwrap(); melt_change.sort_by(|a, b| a.amount.cmp(&b.amount)); let mut check = check.change.unwrap(); check.sort_by(|a, b| a.amount.cmp(&b.amount)); assert_eq!(melt_change, check); Ok(()) } // 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(()) }