|
@@ -0,0 +1,686 @@
|
|
|
|
|
+//! Integration tests for MultiMintWallet
|
|
|
|
|
+//!
|
|
|
|
|
+//! These tests verify the multi-mint wallet functionality including:
|
|
|
|
|
+//! - Basic mint/melt operations across multiple mints
|
|
|
|
|
+//! - Token receive and send operations
|
|
|
|
|
+//! - Automatic mint selection for melts
|
|
|
|
|
+//! - Cross-mint transfers
|
|
|
|
|
+//!
|
|
|
|
|
+//! Tests use the fake wallet backend for deterministic behavior.
|
|
|
|
|
+
|
|
|
|
|
+use std::env;
|
|
|
|
|
+use std::path::PathBuf;
|
|
|
|
|
+use std::str::FromStr;
|
|
|
|
|
+use std::sync::Arc;
|
|
|
|
|
+
|
|
|
|
|
+use bip39::Mnemonic;
|
|
|
|
|
+use cdk::amount::{Amount, SplitTarget};
|
|
|
|
|
+use cdk::mint_url::MintUrl;
|
|
|
|
|
+use cdk::nuts::nut00::ProofsMethods;
|
|
|
|
|
+use cdk::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState, Token};
|
|
|
|
|
+use cdk::wallet::{MultiMintReceiveOptions, MultiMintSendOptions, MultiMintWallet};
|
|
|
|
|
+use cdk_integration_tests::{create_invoice_for_env, get_mint_url_from_env, pay_if_regtest};
|
|
|
|
|
+use cdk_sqlite::wallet::memory;
|
|
|
|
|
+use lightning_invoice::Bolt11Invoice;
|
|
|
|
|
+
|
|
|
|
|
+// Helper function to get temp directory from environment or fallback
|
|
|
|
|
+fn get_test_temp_dir() -> PathBuf {
|
|
|
|
|
+ match env::var("CDK_ITESTS_DIR") {
|
|
|
|
|
+ Ok(dir) => PathBuf::from(dir),
|
|
|
|
|
+ Err(_) => panic!("Unknown test dir"),
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Helper to create a MultiMintWallet with a fresh seed and in-memory database
|
|
|
|
|
+async fn create_test_multi_mint_wallet() -> MultiMintWallet {
|
|
|
|
|
+ let seed = Mnemonic::generate(12).unwrap().to_seed_normalized("");
|
|
|
|
|
+ let localstore = Arc::new(memory::empty().await.unwrap());
|
|
|
|
|
+
|
|
|
|
|
+ MultiMintWallet::new(localstore, seed, CurrencyUnit::Sat)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .expect("failed to create multi mint wallet")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Helper to fund a MultiMintWallet at a specific mint
|
|
|
|
|
+async fn fund_multi_mint_wallet(
|
|
|
|
|
+ wallet: &MultiMintWallet,
|
|
|
|
|
+ mint_url: &MintUrl,
|
|
|
|
|
+ amount: Amount,
|
|
|
|
|
+) -> Amount {
|
|
|
|
|
+ let mint_quote = wallet.mint_quote(mint_url, amount, None).await.unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
|
|
|
|
|
+ pay_if_regtest(&get_test_temp_dir(), &invoice)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ let proofs = wallet
|
|
|
|
|
+ .wait_for_mint_quote(mint_url, &mint_quote.id, SplitTarget::default(), None, 60)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .expect("mint failed");
|
|
|
|
|
+
|
|
|
|
|
+ proofs.total_amount().unwrap()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Test the direct mint() function on MultiMintWallet
|
|
|
|
|
+///
|
|
|
|
|
+/// This test verifies:
|
|
|
|
|
+/// 1. Create a mint quote
|
|
|
|
|
+/// 2. Pay the invoice
|
|
|
|
|
+/// 3. Poll until quote is paid (like a real wallet would)
|
|
|
|
|
+/// 4. Call mint() directly (not wait_for_mint_quote)
|
|
|
|
|
+/// 5. Verify tokens are received
|
|
|
|
|
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
|
|
|
+async fn test_multi_mint_wallet_mint() {
|
|
|
|
|
+ let multi_mint_wallet = create_test_multi_mint_wallet().await;
|
|
|
|
|
+
|
|
|
|
|
+ let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
|
|
|
|
|
+ multi_mint_wallet
|
|
|
|
|
+ .add_mint(mint_url.clone())
|
|
|
|
|
+ .await
|
|
|
|
|
+ .expect("failed to add mint");
|
|
|
|
|
+
|
|
|
|
|
+ // Create mint quote
|
|
|
|
|
+ let mint_quote = multi_mint_wallet
|
|
|
|
|
+ .mint_quote(&mint_url, 100.into(), None)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ // Pay the invoice (in regtest mode) - for fake wallet, payment is simulated automatically
|
|
|
|
|
+ let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
|
|
|
|
|
+ pay_if_regtest(&get_test_temp_dir(), &invoice)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ // Poll for quote to be paid (like a real wallet would)
|
|
|
|
|
+ let mut quote_status = multi_mint_wallet
|
|
|
|
|
+ .check_mint_quote(&mint_url, &mint_quote.id)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ let timeout = tokio::time::Duration::from_secs(30);
|
|
|
|
|
+ let start = tokio::time::Instant::now();
|
|
|
|
|
+ while quote_status.state != MintQuoteState::Paid && quote_status.state != MintQuoteState::Issued
|
|
|
|
|
+ {
|
|
|
|
|
+ if start.elapsed() > timeout {
|
|
|
|
|
+ panic!(
|
|
|
|
|
+ "Timeout waiting for quote to be paid, state: {:?}",
|
|
|
|
|
+ quote_status.state
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
|
|
|
|
+ quote_status = multi_mint_wallet
|
|
|
|
|
+ .check_mint_quote(&mint_url, &mint_quote.id)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Call mint() directly (quote should be Paid at this point)
|
|
|
|
|
+ let proofs = multi_mint_wallet
|
|
|
|
|
+ .mint(&mint_url, &mint_quote.id, None)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ let minted_amount = proofs.total_amount().unwrap();
|
|
|
|
|
+ assert_eq!(minted_amount, 100.into(), "Should mint exactly 100 sats");
|
|
|
|
|
+
|
|
|
|
|
+ // Verify balance
|
|
|
|
|
+ let balance = multi_mint_wallet.total_balance().await.unwrap();
|
|
|
|
|
+ assert_eq!(balance, 100.into(), "Total balance should be 100 sats");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Test the melt() function with automatic mint selection
|
|
|
|
|
+///
|
|
|
|
|
+/// This test verifies:
|
|
|
|
|
+/// 1. Fund wallet at a mint
|
|
|
|
|
+/// 2. Call melt() without specifying mint (auto-selection)
|
|
|
|
|
+/// 3. Verify payment is made
|
|
|
|
|
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
|
|
|
+async fn test_multi_mint_wallet_melt_auto_select() {
|
|
|
|
|
+ let multi_mint_wallet = create_test_multi_mint_wallet().await;
|
|
|
|
|
+
|
|
|
|
|
+ let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
|
|
|
|
|
+ multi_mint_wallet
|
|
|
|
|
+ .add_mint(mint_url.clone())
|
|
|
|
|
+ .await
|
|
|
|
|
+ .expect("failed to add mint");
|
|
|
|
|
+
|
|
|
|
|
+ // Fund the wallet
|
|
|
|
|
+ let funded_amount = fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
|
|
|
|
|
+ assert_eq!(funded_amount, 100.into());
|
|
|
|
|
+
|
|
|
|
|
+ // Create an invoice to pay
|
|
|
|
|
+ let invoice = create_invoice_for_env(Some(50)).await.unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ // Use melt() with auto-selection (no specific mint specified)
|
|
|
|
|
+ let melt_result = multi_mint_wallet.melt(&invoice, None, None).await.unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ assert_eq!(
|
|
|
|
|
+ melt_result.state,
|
|
|
|
|
+ MeltQuoteState::Paid,
|
|
|
|
|
+ "Melt should be paid"
|
|
|
|
|
+ );
|
|
|
|
|
+ assert_eq!(melt_result.amount, 50.into(), "Should melt 50 sats");
|
|
|
|
|
+
|
|
|
|
|
+ // Verify balance decreased
|
|
|
|
|
+ let balance = multi_mint_wallet.total_balance().await.unwrap();
|
|
|
|
|
+ assert!(
|
|
|
|
|
+ balance < 100.into(),
|
|
|
|
|
+ "Balance should be less than 100 after melt"
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Test the receive() function on MultiMintWallet
|
|
|
|
|
+///
|
|
|
|
|
+/// This test verifies:
|
|
|
|
|
+/// 1. Create a token from a wallet
|
|
|
|
|
+/// 2. Receive the token in a different MultiMintWallet
|
|
|
|
|
+/// 3. Verify the token value is received
|
|
|
|
|
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
|
|
|
+async fn test_multi_mint_wallet_receive() {
|
|
|
|
|
+ // Create sender wallet and fund it
|
|
|
|
|
+ let sender_wallet = create_test_multi_mint_wallet().await;
|
|
|
|
|
+ let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
|
|
|
|
|
+ sender_wallet
|
|
|
|
|
+ .add_mint(mint_url.clone())
|
|
|
|
|
+ .await
|
|
|
|
|
+ .expect("failed to add mint");
|
|
|
|
|
+
|
|
|
|
|
+ let funded_amount = fund_multi_mint_wallet(&sender_wallet, &mint_url, 100.into()).await;
|
|
|
|
|
+ assert_eq!(funded_amount, 100.into());
|
|
|
|
|
+
|
|
|
|
|
+ // Create a token to send
|
|
|
|
|
+ let send_options = MultiMintSendOptions::default();
|
|
|
|
|
+ let prepared_send = sender_wallet
|
|
|
|
|
+ .prepare_send(mint_url.clone(), 50.into(), send_options)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ let token = prepared_send.confirm(None).await.unwrap();
|
|
|
|
|
+ let token_string = token.to_string();
|
|
|
|
|
+
|
|
|
|
|
+ // Create receiver wallet
|
|
|
|
|
+ let receiver_wallet = create_test_multi_mint_wallet().await;
|
|
|
|
|
+ // Add the same mint as trusted
|
|
|
|
|
+ receiver_wallet
|
|
|
|
|
+ .add_mint(mint_url.clone())
|
|
|
|
|
+ .await
|
|
|
|
|
+ .expect("failed to add mint");
|
|
|
|
|
+
|
|
|
|
|
+ // Receive the token
|
|
|
|
|
+ let receive_options = MultiMintReceiveOptions::default();
|
|
|
|
|
+ let received_amount = receiver_wallet
|
|
|
|
|
+ .receive(&token_string, receive_options)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ // Note: received amount may be slightly less due to fees
|
|
|
|
|
+ assert!(
|
|
|
|
|
+ received_amount > Amount::ZERO,
|
|
|
|
|
+ "Should receive some amount, got {:?}",
|
|
|
|
|
+ received_amount
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // Verify receiver balance
|
|
|
|
|
+ let receiver_balance = receiver_wallet.total_balance().await.unwrap();
|
|
|
|
|
+ assert!(
|
|
|
|
|
+ receiver_balance > Amount::ZERO,
|
|
|
|
|
+ "Receiver should have balance"
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // Verify sender balance decreased
|
|
|
|
|
+ let sender_balance = sender_wallet.total_balance().await.unwrap();
|
|
|
|
|
+ assert!(
|
|
|
|
|
+ sender_balance < 100.into(),
|
|
|
|
|
+ "Sender balance should be less than 100 after send"
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Test the receive() function with allow_untrusted option
|
|
|
|
|
+///
|
|
|
|
|
+/// This test verifies:
|
|
|
|
|
+/// 1. Create a token from a known mint
|
|
|
|
|
+/// 2. Receive with a wallet that doesn't have the mint added
|
|
|
|
|
+/// 3. With allow_untrusted=true, the mint should be added automatically
|
|
|
|
|
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
|
|
|
+async fn test_multi_mint_wallet_receive_untrusted() {
|
|
|
|
|
+ // Create sender wallet and fund it
|
|
|
|
|
+ let sender_wallet = create_test_multi_mint_wallet().await;
|
|
|
|
|
+ let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
|
|
|
|
|
+ sender_wallet
|
|
|
|
|
+ .add_mint(mint_url.clone())
|
|
|
|
|
+ .await
|
|
|
|
|
+ .expect("failed to add mint");
|
|
|
|
|
+
|
|
|
|
|
+ let funded_amount = fund_multi_mint_wallet(&sender_wallet, &mint_url, 100.into()).await;
|
|
|
|
|
+ assert_eq!(funded_amount, 100.into());
|
|
|
|
|
+
|
|
|
|
|
+ // Create a token to send
|
|
|
|
|
+ let send_options = MultiMintSendOptions::default();
|
|
|
|
|
+ let prepared_send = sender_wallet
|
|
|
|
|
+ .prepare_send(mint_url.clone(), 50.into(), send_options)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ let token = prepared_send.confirm(None).await.unwrap();
|
|
|
|
|
+ let token_string = token.to_string();
|
|
|
|
|
+
|
|
|
|
|
+ // Create receiver wallet WITHOUT adding the mint
|
|
|
|
|
+ let receiver_wallet = create_test_multi_mint_wallet().await;
|
|
|
|
|
+
|
|
|
|
|
+ // First, verify that receiving without allow_untrusted fails
|
|
|
|
|
+ let receive_options = MultiMintReceiveOptions::default();
|
|
|
|
|
+ let result = receiver_wallet
|
|
|
|
|
+ .receive(&token_string, receive_options)
|
|
|
|
|
+ .await;
|
|
|
|
|
+ assert!(result.is_err(), "Should fail without allow_untrusted");
|
|
|
|
|
+
|
|
|
|
|
+ // Now receive with allow_untrusted=true
|
|
|
|
|
+ let receive_options = MultiMintReceiveOptions::default().allow_untrusted(true);
|
|
|
|
|
+ let received_amount = receiver_wallet
|
|
|
|
|
+ .receive(&token_string, receive_options)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ assert!(received_amount > Amount::ZERO, "Should receive some amount");
|
|
|
|
|
+
|
|
|
|
|
+ // Verify the mint was added to the wallet
|
|
|
|
|
+ assert!(
|
|
|
|
|
+ receiver_wallet.has_mint(&mint_url).await,
|
|
|
|
|
+ "Mint should be added to wallet"
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Test prepare_send() happy path
|
|
|
|
|
+///
|
|
|
|
|
+/// This test verifies:
|
|
|
|
|
+/// 1. Fund wallet
|
|
|
|
|
+/// 2. Call prepare_send() successfully
|
|
|
|
|
+/// 3. Confirm the send and get a token
|
|
|
|
|
+/// 4. Verify the token is valid
|
|
|
|
|
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
|
|
|
+async fn test_multi_mint_wallet_prepare_send_happy_path() {
|
|
|
|
|
+ let multi_mint_wallet = create_test_multi_mint_wallet().await;
|
|
|
|
|
+
|
|
|
|
|
+ let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
|
|
|
|
|
+ multi_mint_wallet
|
|
|
|
|
+ .add_mint(mint_url.clone())
|
|
|
|
|
+ .await
|
|
|
|
|
+ .expect("failed to add mint");
|
|
|
|
|
+
|
|
|
|
|
+ // Fund the wallet
|
|
|
|
|
+ let funded_amount = fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
|
|
|
|
|
+ assert_eq!(funded_amount, 100.into());
|
|
|
|
|
+
|
|
|
|
|
+ // Prepare send
|
|
|
|
|
+ let send_options = MultiMintSendOptions::default();
|
|
|
|
|
+ let prepared_send = multi_mint_wallet
|
|
|
|
|
+ .prepare_send(mint_url.clone(), 50.into(), send_options)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ // Get the token
|
|
|
|
|
+ let token = prepared_send.confirm(None).await.unwrap();
|
|
|
|
|
+ let token_string = token.to_string();
|
|
|
|
|
+
|
|
|
|
|
+ // Verify the token can be parsed back
|
|
|
|
|
+ let parsed_token = Token::from_str(&token_string).unwrap();
|
|
|
|
|
+ let token_mint_url = parsed_token.mint_url().unwrap();
|
|
|
|
|
+ assert_eq!(token_mint_url, mint_url, "Token mint URL should match");
|
|
|
|
|
+
|
|
|
|
|
+ // Get token data to verify value
|
|
|
|
|
+ let token_data = multi_mint_wallet
|
|
|
|
|
+ .get_token_data(&parsed_token)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+ assert_eq!(token_data.value, 50.into(), "Token value should be 50 sats");
|
|
|
|
|
+
|
|
|
|
|
+ // Verify wallet balance decreased
|
|
|
|
|
+ let balance = multi_mint_wallet.total_balance().await.unwrap();
|
|
|
|
|
+ assert_eq!(balance, 50.into(), "Remaining balance should be 50 sats");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Test get_balances() across multiple operations
|
|
|
|
|
+///
|
|
|
|
|
+/// This test verifies:
|
|
|
|
|
+/// 1. Empty wallet has zero balances
|
|
|
|
|
+/// 2. After minting, balance is updated
|
|
|
|
|
+/// 3. get_balances() returns per-mint breakdown
|
|
|
|
|
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
|
|
|
+async fn test_multi_mint_wallet_get_balances() {
|
|
|
|
|
+ let multi_mint_wallet = create_test_multi_mint_wallet().await;
|
|
|
|
|
+
|
|
|
|
|
+ let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
|
|
|
|
|
+ multi_mint_wallet
|
|
|
|
|
+ .add_mint(mint_url.clone())
|
|
|
|
|
+ .await
|
|
|
|
|
+ .expect("failed to add mint");
|
|
|
|
|
+
|
|
|
|
|
+ // Check initial balances
|
|
|
|
|
+ let balances = multi_mint_wallet.get_balances().await.unwrap();
|
|
|
|
|
+ let initial_balance = balances.get(&mint_url).cloned().unwrap_or(Amount::ZERO);
|
|
|
|
|
+ assert_eq!(initial_balance, Amount::ZERO, "Initial balance should be 0");
|
|
|
|
|
+
|
|
|
|
|
+ // Fund the wallet
|
|
|
|
|
+ fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
|
|
|
|
|
+
|
|
|
|
|
+ // Check balances again
|
|
|
|
|
+ let balances = multi_mint_wallet.get_balances().await.unwrap();
|
|
|
|
|
+ let balance = balances.get(&mint_url).cloned().unwrap_or(Amount::ZERO);
|
|
|
|
|
+ assert_eq!(balance, 100.into(), "Balance should be 100 sats");
|
|
|
|
|
+
|
|
|
|
|
+ // Verify total_balance matches
|
|
|
|
|
+ let total = multi_mint_wallet.total_balance().await.unwrap();
|
|
|
|
|
+ assert_eq!(total, 100.into(), "Total balance should match");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Test list_proofs() function
|
|
|
|
|
+///
|
|
|
|
|
+/// This test verifies:
|
|
|
|
|
+/// 1. Empty wallet has no proofs
|
|
|
|
|
+/// 2. After minting, proofs are listed correctly
|
|
|
|
|
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
|
|
|
+async fn test_multi_mint_wallet_list_proofs() {
|
|
|
|
|
+ let multi_mint_wallet = create_test_multi_mint_wallet().await;
|
|
|
|
|
+
|
|
|
|
|
+ let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
|
|
|
|
|
+ multi_mint_wallet
|
|
|
|
|
+ .add_mint(mint_url.clone())
|
|
|
|
|
+ .await
|
|
|
|
|
+ .expect("failed to add mint");
|
|
|
|
|
+
|
|
|
|
|
+ // Check initial proofs
|
|
|
|
|
+ let proofs = multi_mint_wallet.list_proofs().await.unwrap();
|
|
|
|
|
+ let mint_proofs = proofs.get(&mint_url).cloned().unwrap_or_default();
|
|
|
|
|
+ assert!(mint_proofs.is_empty(), "Should have no proofs initially");
|
|
|
|
|
+
|
|
|
|
|
+ // Fund the wallet
|
|
|
|
|
+ fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
|
|
|
|
|
+
|
|
|
|
|
+ // Check proofs again
|
|
|
|
|
+ let proofs = multi_mint_wallet.list_proofs().await.unwrap();
|
|
|
|
|
+ let mint_proofs = proofs.get(&mint_url).cloned().unwrap_or_default();
|
|
|
|
|
+ assert!(!mint_proofs.is_empty(), "Should have proofs after minting");
|
|
|
|
|
+
|
|
|
|
|
+ // Verify proof total matches balance
|
|
|
|
|
+ let proof_total: Amount = mint_proofs.total_amount().unwrap();
|
|
|
|
|
+ assert_eq!(proof_total, 100.into(), "Proof total should be 100 sats");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Test mint management functions (add_mint, remove_mint, has_mint)
|
|
|
|
|
+///
|
|
|
|
|
+/// This test verifies:
|
|
|
|
|
+/// 1. has_mint returns false for unknown mints
|
|
|
|
|
+/// 2. add_mint adds the mint
|
|
|
|
|
+/// 3. has_mint returns true after adding
|
|
|
|
|
+/// 4. remove_mint removes the mint
|
|
|
|
|
+/// 5. has_mint returns false after removal
|
|
|
|
|
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
|
|
|
+async fn test_multi_mint_wallet_mint_management() {
|
|
|
|
|
+ let multi_mint_wallet = create_test_multi_mint_wallet().await;
|
|
|
|
|
+
|
|
|
|
|
+ let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
|
|
|
|
|
+
|
|
|
|
|
+ // Initially mint should not be in wallet
|
|
|
|
|
+ assert!(
|
|
|
|
|
+ !multi_mint_wallet.has_mint(&mint_url).await,
|
|
|
|
|
+ "Mint should not be in wallet initially"
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // Add the mint
|
|
|
|
|
+ multi_mint_wallet
|
|
|
|
|
+ .add_mint(mint_url.clone())
|
|
|
|
|
+ .await
|
|
|
|
|
+ .expect("failed to add mint");
|
|
|
|
|
+
|
|
|
|
|
+ // Now mint should be in wallet
|
|
|
|
|
+ assert!(
|
|
|
|
|
+ multi_mint_wallet.has_mint(&mint_url).await,
|
|
|
|
|
+ "Mint should be in wallet after adding"
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // Get wallets should include this mint
|
|
|
|
|
+ let wallets = multi_mint_wallet.get_wallets().await;
|
|
|
|
|
+ assert!(!wallets.is_empty(), "Should have at least one wallet");
|
|
|
|
|
+
|
|
|
|
|
+ // Get specific wallet
|
|
|
|
|
+ let wallet = multi_mint_wallet.get_wallet(&mint_url).await;
|
|
|
|
|
+ assert!(wallet.is_some(), "Should be able to get wallet for mint");
|
|
|
|
|
+
|
|
|
|
|
+ // Remove the mint
|
|
|
|
|
+ multi_mint_wallet.remove_mint(&mint_url).await;
|
|
|
|
|
+
|
|
|
|
|
+ // Now mint should not be in wallet
|
|
|
|
|
+ assert!(
|
|
|
|
|
+ !multi_mint_wallet.has_mint(&mint_url).await,
|
|
|
|
|
+ "Mint should not be in wallet after removal"
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Test check_all_mint_quotes() function
|
|
|
|
|
+///
|
|
|
|
|
+/// This test verifies:
|
|
|
|
|
+/// 1. Create a mint quote
|
|
|
|
|
+/// 2. Pay the quote
|
|
|
|
|
+/// 3. Poll until quote is paid (like a real wallet would)
|
|
|
|
|
+/// 4. check_all_mint_quotes() processes paid quotes
|
|
|
|
|
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
|
|
|
+async fn test_multi_mint_wallet_check_all_mint_quotes() {
|
|
|
|
|
+ let multi_mint_wallet = create_test_multi_mint_wallet().await;
|
|
|
|
|
+
|
|
|
|
|
+ let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
|
|
|
|
|
+ multi_mint_wallet
|
|
|
|
|
+ .add_mint(mint_url.clone())
|
|
|
|
|
+ .await
|
|
|
|
|
+ .expect("failed to add mint");
|
|
|
|
|
+
|
|
|
|
|
+ // Create a mint quote
|
|
|
|
|
+ let mint_quote = multi_mint_wallet
|
|
|
|
|
+ .mint_quote(&mint_url, 100.into(), None)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ // Pay the invoice (in regtest mode) - for fake wallet, payment is simulated automatically
|
|
|
|
|
+ let invoice = Bolt11Invoice::from_str(&mint_quote.request).unwrap();
|
|
|
|
|
+ pay_if_regtest(&get_test_temp_dir(), &invoice)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ // Poll for quote to be paid (like a real wallet would)
|
|
|
|
|
+ let mut quote_status = multi_mint_wallet
|
|
|
|
|
+ .check_mint_quote(&mint_url, &mint_quote.id)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ let timeout = tokio::time::Duration::from_secs(30);
|
|
|
|
|
+ let start = tokio::time::Instant::now();
|
|
|
|
|
+ while quote_status.state != MintQuoteState::Paid {
|
|
|
|
|
+ if start.elapsed() > timeout {
|
|
|
|
|
+ panic!(
|
|
|
|
|
+ "Timeout waiting for quote to be paid, state: {:?}",
|
|
|
|
|
+ quote_status.state
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
|
|
|
|
+ quote_status = multi_mint_wallet
|
|
|
|
|
+ .check_mint_quote(&mint_url, &mint_quote.id)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check all mint quotes - this should find the paid quote and mint
|
|
|
|
|
+ let minted_amount = multi_mint_wallet
|
|
|
|
|
+ .check_all_mint_quotes(Some(mint_url.clone()))
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ assert_eq!(
|
|
|
|
|
+ minted_amount,
|
|
|
|
|
+ 100.into(),
|
|
|
|
|
+ "Should mint 100 sats from paid quote"
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // Verify balance
|
|
|
|
|
+ let balance = multi_mint_wallet.total_balance().await.unwrap();
|
|
|
|
|
+ assert_eq!(balance, 100.into(), "Balance should be 100 sats");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Test restore() function
|
|
|
|
|
+///
|
|
|
|
|
+/// This test verifies:
|
|
|
|
|
+/// 1. Create and fund a wallet with a specific seed
|
|
|
|
|
+/// 2. Create a new wallet with the same seed
|
|
|
|
|
+/// 3. Call restore() to recover the proofs
|
|
|
|
|
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
|
|
|
+async fn test_multi_mint_wallet_restore() {
|
|
|
|
|
+ let seed = Mnemonic::generate(12).unwrap().to_seed_normalized("");
|
|
|
|
|
+ let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
|
|
|
|
|
+
|
|
|
|
|
+ // Create first wallet and fund it
|
|
|
|
|
+ {
|
|
|
|
|
+ let localstore = Arc::new(memory::empty().await.unwrap());
|
|
|
|
|
+ let wallet1 = MultiMintWallet::new(localstore, seed, CurrencyUnit::Sat)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .expect("failed to create wallet");
|
|
|
|
|
+
|
|
|
|
|
+ wallet1
|
|
|
|
|
+ .add_mint(mint_url.clone())
|
|
|
|
|
+ .await
|
|
|
|
|
+ .expect("failed to add mint");
|
|
|
|
|
+
|
|
|
|
|
+ let funded = fund_multi_mint_wallet(&wallet1, &mint_url, 100.into()).await;
|
|
|
|
|
+ assert_eq!(funded, 100.into());
|
|
|
|
|
+ }
|
|
|
|
|
+ // wallet1 goes out of scope
|
|
|
|
|
+
|
|
|
|
|
+ // Create second wallet with same seed but fresh storage
|
|
|
|
|
+ let localstore2 = Arc::new(memory::empty().await.unwrap());
|
|
|
|
|
+ let wallet2 = MultiMintWallet::new(localstore2, seed, CurrencyUnit::Sat)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .expect("failed to create wallet");
|
|
|
|
|
+
|
|
|
|
|
+ wallet2
|
|
|
|
|
+ .add_mint(mint_url.clone())
|
|
|
|
|
+ .await
|
|
|
|
|
+ .expect("failed to add mint");
|
|
|
|
|
+
|
|
|
|
|
+ // Initially should have no balance
|
|
|
|
|
+ let balance_before = wallet2.total_balance().await.unwrap();
|
|
|
|
|
+ assert_eq!(balance_before, Amount::ZERO, "Should start with no balance");
|
|
|
|
|
+
|
|
|
|
|
+ // Restore from mint
|
|
|
|
|
+ let restored = wallet2.restore(&mint_url).await.unwrap();
|
|
|
|
|
+ assert_eq!(restored, 100.into(), "Should restore 100 sats");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Test melt_with_mint() with explicit mint selection
|
|
|
|
|
+///
|
|
|
|
|
+/// This test verifies:
|
|
|
|
|
+/// 1. Fund wallet
|
|
|
|
|
+/// 2. Create melt quote at specific mint
|
|
|
|
|
+/// 3. Execute melt_with_mint()
|
|
|
|
|
+/// 4. Verify payment succeeded
|
|
|
|
|
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
|
|
|
+async fn test_multi_mint_wallet_melt_with_mint() {
|
|
|
|
|
+ let multi_mint_wallet = create_test_multi_mint_wallet().await;
|
|
|
|
|
+
|
|
|
|
|
+ let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
|
|
|
|
|
+ multi_mint_wallet
|
|
|
|
|
+ .add_mint(mint_url.clone())
|
|
|
|
|
+ .await
|
|
|
|
|
+ .expect("failed to add mint");
|
|
|
|
|
+
|
|
|
|
|
+ // Fund the wallet
|
|
|
|
|
+ fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
|
|
|
|
|
+
|
|
|
|
|
+ // Create an invoice to pay
|
|
|
|
|
+ let invoice = create_invoice_for_env(Some(50)).await.unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ // Create melt quote at specific mint
|
|
|
|
|
+ let melt_quote = multi_mint_wallet
|
|
|
|
|
+ .melt_quote(&mint_url, invoice, None)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ // Execute melt with specific mint
|
|
|
|
|
+ let melt_result = multi_mint_wallet
|
|
|
|
|
+ .melt_with_mint(&mint_url, &melt_quote.id)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ assert_eq!(
|
|
|
|
|
+ melt_result.state,
|
|
|
|
|
+ MeltQuoteState::Paid,
|
|
|
|
|
+ "Melt should be paid"
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // Check melt quote status
|
|
|
|
|
+ let quote_status = multi_mint_wallet
|
|
|
|
|
+ .check_melt_quote(&mint_url, &melt_quote.id)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ assert_eq!(
|
|
|
|
|
+ quote_status.state,
|
|
|
|
|
+ MeltQuoteState::Paid,
|
|
|
|
|
+ "Quote status should be paid"
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Test unit() function returns correct currency unit
|
|
|
|
|
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
|
|
|
+async fn test_multi_mint_wallet_unit() {
|
|
|
|
|
+ let seed = Mnemonic::generate(12).unwrap().to_seed_normalized("");
|
|
|
|
|
+ let localstore = Arc::new(memory::empty().await.unwrap());
|
|
|
|
|
+
|
|
|
|
|
+ let wallet = MultiMintWallet::new(localstore, seed, CurrencyUnit::Sat)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .expect("failed to create wallet");
|
|
|
|
|
+
|
|
|
|
|
+ assert_eq!(wallet.unit(), &CurrencyUnit::Sat, "Unit should be Sat");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/// Test list_transactions() function
|
|
|
|
|
+///
|
|
|
|
|
+/// This test verifies:
|
|
|
|
|
+/// 1. Initially no transactions
|
|
|
|
|
+/// 2. After minting, transaction is recorded
|
|
|
|
|
+/// 3. After melting, transaction is recorded
|
|
|
|
|
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
|
|
|
|
+async fn test_multi_mint_wallet_list_transactions() {
|
|
|
|
|
+ let multi_mint_wallet = create_test_multi_mint_wallet().await;
|
|
|
|
|
+
|
|
|
|
|
+ let mint_url = MintUrl::from_str(&get_mint_url_from_env()).expect("invalid mint url");
|
|
|
|
|
+ multi_mint_wallet
|
|
|
|
|
+ .add_mint(mint_url.clone())
|
|
|
|
|
+ .await
|
|
|
|
|
+ .expect("failed to add mint");
|
|
|
|
|
+
|
|
|
|
|
+ // Fund the wallet (this creates a mint transaction)
|
|
|
|
|
+ fund_multi_mint_wallet(&multi_mint_wallet, &mint_url, 100.into()).await;
|
|
|
|
|
+
|
|
|
|
|
+ // List all transactions
|
|
|
|
|
+ let transactions = multi_mint_wallet.list_transactions(None).await.unwrap();
|
|
|
|
|
+ assert!(
|
|
|
|
|
+ !transactions.is_empty(),
|
|
|
|
|
+ "Should have at least one transaction after minting"
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // Create an invoice and melt (this creates a melt transaction)
|
|
|
|
|
+ let invoice = create_invoice_for_env(Some(50)).await.unwrap();
|
|
|
|
|
+ let melt_quote = multi_mint_wallet
|
|
|
|
|
+ .melt_quote(&mint_url, invoice, None)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+ multi_mint_wallet
|
|
|
|
|
+ .melt_with_mint(&mint_url, &melt_quote.id)
|
|
|
|
|
+ .await
|
|
|
|
|
+ .unwrap();
|
|
|
|
|
+
|
|
|
|
|
+ // List transactions again
|
|
|
|
|
+ let transactions_after = multi_mint_wallet.list_transactions(None).await.unwrap();
|
|
|
|
|
+ assert!(
|
|
|
|
|
+ transactions_after.len() > transactions.len(),
|
|
|
|
|
+ "Should have more transactions after melt"
|
|
|
|
|
+ );
|
|
|
|
|
+}
|