Files
cdk/crates/cdk-integration-tests/tests/fake_wallet.rs
C f8d58e419f Merge pull request #963 from crodas/feature/wait-for-invoice
Add `wait_for_payment` function in the wallet
2025-08-18 12:57:45 +01:00

1303 lines
41 KiB
Rust

//! Fake Wallet Integration Tests
//!
//! This file contains tests for the fake wallet backend functionality.
//! The fake wallet simulates Lightning Network behavior for testing purposes,
//! allowing verification of mint behavior in various payment scenarios without
//! requiring a real Lightning node.
//!
//! Test Scenarios:
//! - Pending payment states and proof handling
//! - Payment failure cases and proof state management
//! - Change output verification in melt operations
//! - Witness signature validation
//! - Cross-unit transaction validation
//! - Overflow and balance validation
//! - Duplicate proof detection
use std::sync::Arc;
use std::time::Duration;
use bip39::Mnemonic;
use cashu::Amount;
use cdk::amount::SplitTarget;
use cdk::nuts::nut00::ProofsMethods;
use cdk::nuts::{
CurrencyUnit, MeltQuoteState, MeltRequest, MintRequest, PreMintSecrets, Proofs, SecretKey,
State, SwapRequest,
};
use cdk::wallet::types::TransactionDirection;
use cdk::wallet::{HttpClient, MintConnector, Wallet};
use cdk_fake_wallet::{create_fake_invoice, FakeInvoiceDescription};
use cdk_integration_tests::attempt_to_swap_pending;
use cdk_sqlite::wallet::memory;
const MINT_URL: &str = "http://127.0.0.1:8086";
/// Tests that when both pay and check return pending status, input proofs should remain pending
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_tokens_pending() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("failed to create new wallet");
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let _mint_amount = wallet
.mint(&mint_quote.id, SplitTarget::default(), None)
.await
.unwrap();
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.unwrap();
let melt = wallet.melt(&melt_quote.id).await;
assert!(melt.is_err());
attempt_to_swap_pending(&wallet).await.unwrap();
}
/// Tests that if the pay error fails and the check returns unknown or failed,
/// the input proofs should be unset as spending (returned to unspent state)
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_melt_payment_fail() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("Failed to create new wallet");
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let _mint_amount = wallet
.mint(&mint_quote.id, SplitTarget::default(), None)
.await
.unwrap();
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.unwrap();
// 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.unwrap();
// 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.unwrap();
let states = wallet.check_proofs_spent(all_proof).await.unwrap();
for state in states {
assert!(state.state == State::Unspent);
}
let wallet_bal = wallet.total_balance().await.unwrap();
assert_eq!(wallet_bal, 100.into());
}
/// Tests that when both the pay_invoice and check_invoice both fail,
/// the proofs should remain in pending state
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_melt_payment_fail_and_check() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("Failed to create new wallet");
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let _mint_amount = wallet
.mint(&mint_quote.id, SplitTarget::default(), None)
.await
.unwrap();
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.unwrap();
// 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
.unwrap();
assert!(!pending.is_empty());
}
/// Tests that when the ln backend returns a failed status but does not error,
/// the mint should do a second check, then remove proofs from pending state
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_melt_payment_return_fail_status() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("Failed to create new wallet");
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let _mint_amount = wallet
.mint(&mint_quote.id, SplitTarget::default(), None)
.await
.unwrap();
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.unwrap();
// 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.unwrap();
// 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
.unwrap();
assert!(pending.is_empty());
}
/// Tests that when the ln backend returns an error with unknown status,
/// the mint should do a second check, then remove proofs from pending state
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_melt_payment_error_unknown() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.unwrap();
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let _mint_amount = wallet
.mint(&mint_quote.id, SplitTarget::default(), None)
.await
.unwrap();
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.unwrap();
// The melt should error at the payment invoice command
let melt = wallet.melt(&melt_quote.id).await;
assert_eq!(melt.unwrap_err().to_string(), "Payment failed");
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.unwrap();
// The melt should error at the payment invoice command
let melt = wallet.melt(&melt_quote.id).await;
assert_eq!(melt.unwrap_err().to_string(), "Payment failed");
let pending = wallet
.localstore
.get_proofs(None, None, Some(vec![State::Pending]), None)
.await
.unwrap();
assert!(pending.is_empty());
}
/// Tests that when the ln backend returns an error but the second check returns paid,
/// proofs should remain in pending state
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_melt_payment_err_paid() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("Failed to create new wallet");
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let _mint_amount = wallet
.mint(&mint_quote.id, SplitTarget::default(), None)
.await
.unwrap();
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.unwrap();
// 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.unwrap();
}
/// Tests that change outputs in a melt quote are correctly handled
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_melt_change_in_quote() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("Failed to create new wallet");
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let _mint_amount = wallet
.mint(&mint_quote.id, SplitTarget::default(), None)
.await
.unwrap();
let transaction = wallet
.list_transactions(Some(TransactionDirection::Incoming))
.await
.unwrap()
.pop()
.expect("No transaction found");
assert_eq!(wallet.mint_url, transaction.mint_url);
assert_eq!(TransactionDirection::Incoming, transaction.direction);
assert_eq!(Amount::from(100), transaction.amount);
assert_eq!(Amount::from(0), transaction.fee);
assert_eq!(CurrencyUnit::Sat, transaction.unit);
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.unwrap();
let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
let keyset = wallet.fetch_active_keyset().await.unwrap();
let premint_secrets =
PreMintSecrets::random(keyset.id, 100.into(), &SplitTarget::default()).unwrap();
let client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let melt_request = MeltRequest::new(
melt_quote.id.clone(),
proofs.clone(),
Some(premint_secrets.blinded_messages()),
);
let melt_response = client.post_melt(melt_request).await.unwrap();
assert!(melt_response.change.is_some());
let check = wallet.melt_quote_status(&melt_quote.id).await.unwrap();
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);
}
/// Tests minting tokens with a valid witness signature
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_with_witness() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("failed to create new wallet");
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let proofs = wallet
.mint(&mint_quote.id, SplitTarget::default(), None)
.await
.unwrap();
let mint_amount = proofs.total_amount().unwrap();
assert!(mint_amount == 100.into());
}
/// Tests that minting without a witness signature fails with the correct error
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_without_witness() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("failed to create new wallet");
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
let premint_secrets =
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap();
let request = MintRequest {
quote: mint_quote.id,
outputs: premint_secrets.blinded_messages(),
signature: None,
};
let response = http_client.post_mint(request.clone()).await;
match response {
Err(cdk::error::Error::SignatureMissingOrInvalid) => {} //pass
Err(err) => panic!("Wrong mint response for minting without witness: {}", err),
Ok(_) => panic!("Minting should not have succeed without a witness"),
}
}
/// Tests that minting with an incorrect witness signature fails with the correct error
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_with_wrong_witness() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("failed to create new wallet");
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
let premint_secrets =
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap();
let mut request = MintRequest {
quote: mint_quote.id,
outputs: premint_secrets.blinded_messages(),
signature: None,
};
let secret_key = SecretKey::generate();
request
.sign(secret_key)
.expect("failed to sign the mint request");
let response = http_client.post_mint(request.clone()).await;
match response {
Err(cdk::error::Error::SignatureMissingOrInvalid) => {} //pass
Err(err) => panic!("Wrong mint response for minting without witness: {}", err),
Ok(_) => panic!("Minting should not have succeed without a witness"),
}
}
/// Tests that attempting to mint more tokens than allowed by the quote fails
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_inflated() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("failed to create new wallet");
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
let pre_mint =
PreMintSecrets::random(active_keyset_id, 500.into(), &SplitTarget::None).unwrap();
let quote_info = wallet
.localstore
.get_mint_quote(&mint_quote.id)
.await
.unwrap()
.expect("there is a quote");
let mut mint_request = MintRequest {
quote: mint_quote.id,
outputs: pre_mint.blinded_messages(),
signature: None,
};
if let Some(secret_key) = quote_info.secret_key {
mint_request
.sign(secret_key)
.expect("failed to sign the mint request");
}
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_mint(mint_request.clone()).await;
match response {
Err(err) => match err {
cdk::Error::TransactionUnbalanced(_, _, _) => (),
err => {
panic!("Wrong mint error returned: {}", err);
}
},
Ok(_) => {
panic!("Should not have allowed second payment");
}
}
}
/// Tests that attempting to mint with multiple currency units in the same request fails
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_multiple_units() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("failed to create new wallet");
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
let pre_mint = PreMintSecrets::random(active_keyset_id, 50.into(), &SplitTarget::None).unwrap();
let wallet_usd = Wallet::new(
MINT_URL,
CurrencyUnit::Usd,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("failed to create new wallet");
let active_keyset_id = wallet_usd.fetch_active_keyset().await.unwrap().id;
let usd_pre_mint =
PreMintSecrets::random(active_keyset_id, 50.into(), &SplitTarget::None).unwrap();
let quote_info = wallet
.localstore
.get_mint_quote(&mint_quote.id)
.await
.unwrap()
.expect("there is a quote");
let mut sat_outputs = pre_mint.blinded_messages();
let mut usd_outputs = usd_pre_mint.blinded_messages();
sat_outputs.append(&mut usd_outputs);
let mut mint_request = MintRequest {
quote: mint_quote.id,
outputs: sat_outputs,
signature: None,
};
if let Some(secret_key) = quote_info.secret_key {
mint_request
.sign(secret_key)
.expect("failed to sign the mint request");
}
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_mint(mint_request.clone()).await;
match response {
Err(err) => match err {
cdk::Error::MultipleUnits => (),
err => {
panic!("Wrong mint error returned: {}", err);
}
},
Ok(_) => {
panic!("Should not have allowed to mint with multiple units");
}
}
}
/// Tests that attempting to swap tokens with multiple currency units fails
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_multiple_unit_swap() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("failed to create new wallet");
wallet.refresh_keysets().await.unwrap();
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let proofs = wallet
.mint(&mint_quote.id, SplitTarget::None, None)
.await
.unwrap();
let wallet_usd = Wallet::new(
MINT_URL,
CurrencyUnit::Usd,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("failed to create usd wallet");
wallet_usd.refresh_keysets().await.unwrap();
let mint_quote = wallet_usd.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let usd_proofs = wallet_usd
.mint(&mint_quote.id, SplitTarget::None, None)
.await
.unwrap();
let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
{
let inputs: Proofs = vec![
proofs.first().expect("There is a proof").clone(),
usd_proofs.first().expect("There is a proof").clone(),
];
let pre_mint = PreMintSecrets::random(
active_keyset_id,
inputs.total_amount().unwrap(),
&SplitTarget::None,
)
.unwrap();
let swap_request = SwapRequest::new(inputs, pre_mint.blinded_messages());
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_swap(swap_request.clone()).await;
match response {
Err(err) => match err {
cdk::Error::MultipleUnits => (),
err => {
panic!("Wrong mint error returned: {}", err);
}
},
Ok(_) => {
panic!("Should not have allowed to mint with multiple units");
}
}
}
{
let usd_active_keyset_id = wallet_usd.fetch_active_keyset().await.unwrap().id;
let inputs: Proofs = proofs.into_iter().take(2).collect();
let total_inputs = inputs.total_amount().unwrap();
let half = total_inputs / 2.into();
let usd_pre_mint =
PreMintSecrets::random(usd_active_keyset_id, half, &SplitTarget::None).unwrap();
let pre_mint =
PreMintSecrets::random(active_keyset_id, total_inputs - half, &SplitTarget::None)
.unwrap();
let mut usd_outputs = usd_pre_mint.blinded_messages();
let mut sat_outputs = pre_mint.blinded_messages();
usd_outputs.append(&mut sat_outputs);
let swap_request = SwapRequest::new(inputs, usd_outputs);
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_swap(swap_request.clone()).await;
match response {
Err(err) => match err {
cdk::Error::MultipleUnits => (),
err => {
panic!("Wrong mint error returned: {}", err);
}
},
Ok(_) => {
panic!("Should not have allowed to mint with multiple units");
}
}
}
}
/// Tests that attempting to melt tokens with multiple currency units fails
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_multiple_unit_melt() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("failed to create new wallet");
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let proofs = wallet
.mint(&mint_quote.id, SplitTarget::None, None)
.await
.unwrap();
println!("Minted sat");
let wallet_usd = Wallet::new(
MINT_URL,
CurrencyUnit::Usd,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("failed to create new wallet");
let mint_quote = wallet_usd.mint_quote(100.into(), None).await.unwrap();
println!("Minted quote usd");
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let usd_proofs = wallet_usd
.mint(&mint_quote.id, SplitTarget::None, None)
.await
.unwrap();
{
let inputs: Proofs = vec![
proofs.first().expect("There is a proof").clone(),
usd_proofs.first().expect("There is a proof").clone(),
];
let input_amount: u64 = inputs.total_amount().unwrap().into();
let invoice = create_fake_invoice((input_amount - 1) * 1000, "".to_string());
let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
let melt_request = MeltRequest::new(melt_quote.id, inputs, None);
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_melt(melt_request.clone()).await;
match response {
Err(err) => match err {
cdk::Error::MultipleUnits => (),
err => {
panic!("Wrong mint error returned: {}", err);
}
},
Ok(_) => {
panic!("Should not have allowed to melt with multiple units");
}
}
}
{
let inputs: Proofs = vec![proofs.first().expect("There is a proof").clone()];
let input_amount: u64 = inputs.total_amount().unwrap().into();
let invoice = create_fake_invoice((input_amount - 1) * 1000, "".to_string());
let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
let usd_active_keyset_id = wallet_usd.fetch_active_keyset().await.unwrap().id;
let usd_pre_mint = PreMintSecrets::random(
usd_active_keyset_id,
inputs.total_amount().unwrap() + 100.into(),
&SplitTarget::None,
)
.unwrap();
let pre_mint =
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::None).unwrap();
let mut usd_outputs = usd_pre_mint.blinded_messages();
let mut sat_outputs = pre_mint.blinded_messages();
usd_outputs.append(&mut sat_outputs);
let quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
let melt_request = MeltRequest::new(quote.id, inputs, Some(usd_outputs));
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_melt(melt_request.clone()).await;
match response {
Err(err) => match err {
cdk::Error::MultipleUnits => (),
err => {
panic!("Wrong mint error returned: {}", err);
}
},
Ok(_) => {
panic!("Should not have allowed to melt with multiple units");
}
}
}
}
/// Tests that swapping tokens where input unit doesn't match output unit fails
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_input_output_mismatch() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("failed to create new wallet");
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let proofs = wallet
.mint(&mint_quote.id, SplitTarget::None, None)
.await
.unwrap();
let wallet_usd = Wallet::new(
MINT_URL,
CurrencyUnit::Usd,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("failed to create new usd wallet");
let usd_active_keyset_id = wallet_usd.fetch_active_keyset().await.unwrap().id;
let inputs = proofs;
let pre_mint = PreMintSecrets::random(
usd_active_keyset_id,
inputs.total_amount().unwrap(),
&SplitTarget::None,
)
.unwrap();
let swap_request = SwapRequest::new(inputs, pre_mint.blinded_messages());
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_swap(swap_request.clone()).await;
match response {
Err(err) => match err {
cdk::Error::UnitMismatch => (),
err => panic!("Wrong error returned: {}", err),
},
Ok(_) => {
panic!("Should not have allowed to mint with multiple units");
}
}
}
/// Tests that swapping tokens where output amount is greater than input amount fails
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_swap_inflated() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("failed to create new wallet");
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let proofs = wallet
.mint(&mint_quote.id, SplitTarget::None, None)
.await
.unwrap();
let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
let pre_mint =
PreMintSecrets::random(active_keyset_id, 101.into(), &SplitTarget::None).unwrap();
let swap_request = SwapRequest::new(proofs, pre_mint.blinded_messages());
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_swap(swap_request.clone()).await;
match response {
Err(err) => match err {
cdk::Error::TransactionUnbalanced(_, _, _) => (),
err => {
panic!("Wrong mint error returned: {}", err);
}
},
Ok(_) => {
panic!("Should not have allowed to mint with multiple units");
}
}
}
/// Tests that tokens cannot be spent again after a failed swap attempt
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_swap_spend_after_fail() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("failed to create new wallet");
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let proofs = wallet
.mint(&mint_quote.id, SplitTarget::None, None)
.await
.unwrap();
let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
let pre_mint =
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::None).unwrap();
let swap_request = SwapRequest::new(proofs.clone(), pre_mint.blinded_messages());
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_swap(swap_request.clone()).await;
assert!(response.is_ok());
let pre_mint =
PreMintSecrets::random(active_keyset_id, 101.into(), &SplitTarget::None).unwrap();
let swap_request = SwapRequest::new(proofs.clone(), pre_mint.blinded_messages());
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_swap(swap_request.clone()).await;
match response {
Err(err) => match err {
cdk::Error::TransactionUnbalanced(_, _, _) => (),
err => panic!("Wrong mint error returned expected TransactionUnbalanced, got: {err}"),
},
Ok(_) => panic!("Should not have allowed swap with unbalanced"),
}
let pre_mint =
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::None).unwrap();
let swap_request = SwapRequest::new(proofs, pre_mint.blinded_messages());
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_swap(swap_request.clone()).await;
match response {
Err(err) => match err {
cdk::Error::TokenAlreadySpent => (),
err => {
panic!("Wrong mint error returned: {}", err);
}
},
Ok(_) => {
panic!("Should not have allowed to mint with multiple units");
}
}
}
/// Tests that tokens cannot be melted after a failed swap attempt
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_melt_spend_after_fail() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("failed to create new wallet");
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let proofs = wallet
.mint(&mint_quote.id, SplitTarget::None, None)
.await
.unwrap();
let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
let pre_mint =
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::None).unwrap();
let swap_request = SwapRequest::new(proofs.clone(), pre_mint.blinded_messages());
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_swap(swap_request.clone()).await;
assert!(response.is_ok());
let pre_mint =
PreMintSecrets::random(active_keyset_id, 101.into(), &SplitTarget::None).unwrap();
let swap_request = SwapRequest::new(proofs.clone(), pre_mint.blinded_messages());
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_swap(swap_request.clone()).await;
match response {
Err(err) => match err {
cdk::Error::TransactionUnbalanced(_, _, _) => (),
err => panic!("Wrong mint error returned expected TransactionUnbalanced, got: {err}"),
},
Ok(_) => panic!("Should not have allowed swap with unbalanced"),
}
let input_amount: u64 = proofs.total_amount().unwrap().into();
let invoice = create_fake_invoice((input_amount - 1) * 1000, "".to_string());
let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
let melt_request = MeltRequest::new(melt_quote.id, proofs, None);
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_melt(melt_request.clone()).await;
match response {
Err(err) => match err {
cdk::Error::TokenAlreadySpent => (),
err => {
panic!("Wrong mint error returned: {}", err);
}
},
Ok(_) => {
panic!("Should not have allowed to melt with multiple units");
}
}
}
/// Tests that attempting to swap with duplicate proofs fails
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_duplicate_proofs_swap() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("failed to create new wallet");
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let proofs = wallet
.mint(&mint_quote.id, SplitTarget::None, None)
.await
.unwrap();
let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id;
let inputs = vec![proofs[0].clone(), proofs[0].clone()];
let pre_mint = PreMintSecrets::random(
active_keyset_id,
inputs.total_amount().unwrap(),
&SplitTarget::None,
)
.unwrap();
let swap_request = SwapRequest::new(inputs.clone(), pre_mint.blinded_messages());
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_swap(swap_request.clone()).await;
match response {
Err(err) => match err {
cdk::Error::DuplicateInputs => (),
err => {
panic!(
"Wrong mint error returned, expected duplicate inputs: {}",
err
);
}
},
Ok(_) => {
panic!("Should not have allowed duplicate inputs");
}
}
let blinded_message = pre_mint.blinded_messages();
let inputs = vec![proofs[0].clone()];
let outputs = vec![blinded_message[0].clone(), blinded_message[0].clone()];
let swap_request = SwapRequest::new(inputs, outputs);
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_swap(swap_request.clone()).await;
match response {
Err(err) => match err {
cdk::Error::DuplicateOutputs => (),
err => {
panic!(
"Wrong mint error returned, expected duplicate outputs: {}",
err
);
}
},
Ok(_) => {
panic!("Should not have allow duplicate inputs");
}
}
}
/// Tests that attempting to melt with duplicate proofs fails
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fake_mint_duplicate_proofs_melt() {
let wallet = Wallet::new(
MINT_URL,
CurrencyUnit::Sat,
Arc::new(memory::empty().await.unwrap()),
Mnemonic::generate(12).unwrap().to_seed_normalized(""),
None,
)
.expect("failed to create new wallet");
let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
wallet
.wait_for_payment(&mint_quote, Duration::from_secs(60))
.await
.unwrap();
let proofs = wallet
.mint(&mint_quote.id, SplitTarget::None, None)
.await
.unwrap();
let inputs = vec![proofs[0].clone(), proofs[0].clone()];
let invoice = create_fake_invoice(7000, "".to_string());
let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
let melt_request = MeltRequest::new(melt_quote.id, inputs, None);
let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
let response = http_client.post_melt(melt_request.clone()).await;
match response {
Err(err) => match err {
cdk::Error::DuplicateInputs => (),
err => {
panic!("Wrong mint error returned: {}", err);
}
},
Ok(_) => {
panic!("Should not have allow duplicate inputs");
}
}
}