use std::sync::Arc;
use cdk_common::nuts::{Proofs, ProofsMethods};
use cdk_common::{Amount, State};
use super::SwapSaga;
use crate::mint::swap::Mint;
use crate::mint::Verification;
use crate::test_helpers::mint::{
clear_fail_for, create_test_blinded_messages, create_test_mint, set_fail_for,
};
fn create_verification(amount: Amount) -> Verification {
Verification {
amount: amount.with_unit(cdk_common::nuts::CurrencyUnit::Sat),
}
}
async fn create_swap_inputs(mint: &Mint, amount: Amount) -> (Proofs, Verification) {
let proofs = crate::test_helpers::mint::mint_test_proofs(mint, amount)
.await
.expect("Failed to create test proofs");
let verification = create_verification(amount);
(proofs, verification)
}
#[tokio::test]
async fn test_swap_saga_initial_state_creation() {
let mint = create_test_mint().await.unwrap();
let db = mint.localstore();
let pubsub = mint.pubsub_manager();
let _saga = SwapSaga::new(&mint, db, pubsub);
}
#[tokio::test]
async fn test_swap_saga_full_flow_success() {
let mint = create_test_mint().await.unwrap();
let amount = Amount::from(100);
let (input_proofs, input_verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _pre_mint) =
create_test_blinded_messages(&mint, amount).await.unwrap();
let db = mint.localstore();
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db, pubsub);
let saga = saga
.setup_swap(
&input_proofs,
&output_blinded_messages,
None,
input_verification,
)
.await
.expect("Setup should succeed");
let saga = saga.sign_outputs().await.expect("Signing should succeed");
let response = saga.finalize().await.expect("Finalize should succeed");
assert_eq!(
response.signatures.len(),
output_blinded_messages.len(),
"Should have signatures for all outputs"
);
let ys = input_proofs.ys().unwrap();
let states = mint
.localstore()
.get_proofs_states(&ys)
.await
.expect("Failed to get proof states");
for state in states {
assert_eq!(
state.unwrap(),
State::Spent,
"Input proofs should be marked as spent"
);
}
}
#[tokio::test]
async fn test_swap_saga_setup_transition() {
let mint = create_test_mint().await.unwrap();
let amount = Amount::from(64);
let (input_proofs, input_verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let db = mint.localstore();
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db, pubsub);
let saga = saga
.setup_swap(
&input_proofs,
&output_blinded_messages,
None,
input_verification,
)
.await
.expect("Setup should succeed");
assert_eq!(
saga.state_data.blinded_messages.len(),
output_blinded_messages.len(),
"SetupComplete state should contain blinded messages"
);
assert_eq!(
saga.state_data.ys.len(),
input_proofs.len(),
"SetupComplete state should contain input ys"
);
let ys = input_proofs.ys().unwrap();
let states = mint
.localstore()
.get_proofs_states(&ys)
.await
.expect("Failed to get proof states");
for state in states {
assert_eq!(
state.unwrap(),
State::Pending,
"Input proofs should be marked as pending after setup"
);
}
}
#[tokio::test]
async fn test_swap_saga_sign_outputs_transition() {
let mint = create_test_mint().await.unwrap();
let amount = Amount::from(128);
let (input_proofs, input_verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let db = mint.localstore();
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db, pubsub);
let saga = saga
.setup_swap(
&input_proofs,
&output_blinded_messages,
None,
input_verification,
)
.await
.expect("Setup should succeed");
let saga = saga.sign_outputs().await.expect("Signing should succeed");
assert_eq!(
saga.state_data.signatures.len(),
output_blinded_messages.len(),
"Signed state should contain signatures for all outputs"
);
}
#[tokio::test]
async fn test_swap_saga_duplicate_inputs() {
let mint = create_test_mint().await.unwrap();
let amount = Amount::from(100);
let (mut input_proofs, input_verification) = create_swap_inputs(&mint, amount).await;
input_proofs.push(input_proofs[0].clone());
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let db = mint.localstore();
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db, pubsub);
let result = saga
.setup_swap(
&input_proofs,
&output_blinded_messages,
None,
input_verification,
)
.await;
assert!(result.is_err(), "Setup should fail with duplicate inputs");
}
#[tokio::test]
async fn test_swap_saga_duplicate_outputs() {
let mint = create_test_mint().await.unwrap();
let amount = Amount::from(100);
let (input_proofs, input_verification) = create_swap_inputs(&mint, amount).await;
let (mut output_blinded_messages, _) =
create_test_blinded_messages(&mint, amount).await.unwrap();
output_blinded_messages.push(output_blinded_messages[0].clone());
let db = mint.localstore();
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db, pubsub);
let result = saga
.setup_swap(
&input_proofs,
&output_blinded_messages,
None,
input_verification,
)
.await;
assert!(result.is_err(), "Setup should fail with duplicate outputs");
}
#[tokio::test]
async fn test_swap_saga_unbalanced_transaction_more_outputs() {
let mint = create_test_mint().await.unwrap();
let input_amount = Amount::from(100);
let (input_proofs, input_verification) = create_swap_inputs(&mint, input_amount).await;
let output_amount = Amount::from(150);
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, output_amount)
.await
.unwrap();
let db = mint.localstore();
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db, pubsub);
let result = saga
.setup_swap(
&input_proofs,
&output_blinded_messages,
None,
input_verification,
)
.await;
assert!(
result.is_err(),
"Setup should fail when outputs exceed inputs"
);
}
#[tokio::test]
async fn test_swap_saga_compensation_clears_on_success() {
let mint = create_test_mint().await.unwrap();
let amount = Amount::from(100);
let (input_proofs, input_verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let db = mint.localstore();
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db, pubsub);
let compensations_before = saga.compensations.len();
let saga = saga
.setup_swap(
&input_proofs,
&output_blinded_messages,
None,
input_verification,
)
.await
.expect("Setup should succeed");
let compensations_after_setup = saga.compensations.len();
assert_eq!(
compensations_after_setup, 1,
"Should have one compensation after setup"
);
let saga = saga.sign_outputs().await.expect("Signing should succeed");
let compensations_after_sign = saga.compensations.len();
assert_eq!(
compensations_after_sign, 1,
"Should still have one compensation after signing"
);
let _response = saga.finalize().await.expect("Finalize should succeed");
assert_eq!(
compensations_before, 0,
"Should start with no compensations"
);
}
#[tokio::test]
async fn test_swap_saga_empty_inputs() {
let mint = create_test_mint().await.unwrap();
let amount = Amount::from(100);
let empty_proofs = Proofs::new();
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let verification = create_verification(Amount::from(0));
let db = mint.localstore();
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db, pubsub);
let result = saga
.setup_swap(&empty_proofs, &output_blinded_messages, None, verification)
.await;
assert!(
result.is_err(),
"Empty inputs with non-empty outputs should be rejected (unbalanced)"
);
}
#[tokio::test]
async fn test_swap_saga_empty_outputs() {
let mint = create_test_mint().await.unwrap();
let amount = Amount::from(100);
let (input_proofs, input_verification) = create_swap_inputs(&mint, amount).await;
let empty_blinded_messages = vec![];
let db = mint.localstore();
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db, pubsub);
let result = saga
.setup_swap(
&input_proofs,
&empty_blinded_messages,
None,
input_verification,
)
.await;
assert!(result.is_err(), "Empty outputs should be rejected");
}
#[tokio::test]
async fn test_swap_saga_both_empty() {
let mint = create_test_mint().await.unwrap();
let empty_proofs = Proofs::new();
let empty_blinded_messages = vec![];
let verification = create_verification(Amount::from(0));
let db = mint.localstore();
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db, pubsub);
let result = saga
.setup_swap(&empty_proofs, &empty_blinded_messages, None, verification)
.await;
assert!(result.is_err(), "Empty swap should be rejected");
}
#[tokio::test]
async fn test_swap_saga_drop_without_finalize() {
let mint = create_test_mint().await.unwrap();
let amount = Amount::from(100);
let (input_proofs, input_verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let db = mint.localstore();
let ys = input_proofs.ys().unwrap();
{
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let _saga = saga
.setup_swap(
&input_proofs,
&output_blinded_messages,
None,
input_verification,
)
.await
.expect("Setup should succeed");
let states = db.get_proofs_states(&ys).await.unwrap();
assert!(states.iter().all(|s| s == &Some(State::Pending)));
}
let states_after = db.get_proofs_states(&ys).await.unwrap();
assert!(
states_after.iter().all(|s| s == &Some(State::Pending)),
"Proofs should remain Pending after saga drop (no auto-cleanup)"
);
}
#[tokio::test]
async fn test_swap_saga_drop_after_signing() {
let mint = create_test_mint().await.unwrap();
let amount = Amount::from(100);
let (input_proofs, input_verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let db = mint.localstore();
let ys = input_proofs.ys().unwrap();
let _blinded_secrets: Vec<_> = output_blinded_messages
.iter()
.map(|bm| bm.blinded_secret)
.collect();
{
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(
&input_proofs,
&output_blinded_messages,
None,
input_verification,
)
.await
.expect("Setup should succeed");
let saga = saga.sign_outputs().await.expect("Signing should succeed");
assert_eq!(
saga.state_data.signatures.len(),
output_blinded_messages.len()
);
}
let states_after = db.get_proofs_states(&ys).await.unwrap();
assert!(states_after.iter().all(|s| s == &Some(State::Pending)));
let signatures = db.get_blind_signatures(&_blinded_secrets).await.unwrap();
assert!(
signatures.iter().all(|s| s.is_none()),
"Signatures should be lost when saga is dropped (never persisted)"
);
}
#[tokio::test]
async fn test_swap_saga_compensation_on_signing_failure() {
let mint = create_test_mint().await.unwrap();
let amount = Amount::from(100);
let (input_proofs, input_verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let db = mint.localstore();
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(
&input_proofs,
&output_blinded_messages,
None,
input_verification,
)
.await
.expect("Setup should succeed");
let ys = input_proofs.ys().unwrap();
let states = db.get_proofs_states(&ys).await.unwrap();
assert!(states.iter().all(|s| s == &Some(State::Pending)));
set_fail_for("GENERAL");
let result = saga.sign_outputs().await;
clear_fail_for("GENERAL");
assert!(result.is_err(), "Signing should fail");
let states_after = db.get_proofs_states(&ys).await.unwrap();
assert!(
states_after.iter().all(|s| s.is_none()),
"Proofs should be removed"
);
let _blinded_secrets: Vec<_> = output_blinded_messages
.iter()
.map(|bm| bm.blinded_secret)
.collect();
let signatures = db.get_blind_signatures(&_blinded_secrets).await.unwrap();
assert!(
signatures.iter().all(|s| s.is_none()),
"No signatures should exist (never created)"
);
}
#[tokio::test]
async fn test_swap_saga_double_spend_detection() {
let mint = create_test_mint().await.unwrap();
let amount = Amount::from(100);
let (input_proofs, input_verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages_1, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let (output_blinded_messages_2, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let db = mint.localstore();
let pubsub = mint.pubsub_manager();
let saga1 = SwapSaga::new(&mint, db.clone(), pubsub.clone());
let saga1 = saga1
.setup_swap(
&input_proofs,
&output_blinded_messages_1,
None,
input_verification.clone(),
)
.await
.expect("First setup should succeed");
let saga1 = saga1
.sign_outputs()
.await
.expect("First signing should succeed");
let _response1 = saga1
.finalize()
.await
.expect("First finalize should succeed");
let saga2 = SwapSaga::new(&mint, db, pubsub);
let result = saga2
.setup_swap(
&input_proofs,
&output_blinded_messages_2,
None,
input_verification,
)
.await;
assert!(
result.is_err(),
"Second setup should fail due to double-spend"
);
}
#[tokio::test]
async fn test_swap_saga_pending_proof_detection() {
let mint = create_test_mint().await.unwrap();
let amount = Amount::from(100);
let (input_proofs, input_verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages_1, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let (output_blinded_messages_2, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let db = mint.localstore();
let pubsub = mint.pubsub_manager();
let saga1 = SwapSaga::new(&mint, db.clone(), pubsub.clone());
let saga1 = saga1
.setup_swap(
&input_proofs,
&output_blinded_messages_1,
None,
input_verification.clone(),
)
.await
.expect("First setup should succeed");
drop(saga1);
let saga2 = SwapSaga::new(&mint, db, pubsub);
let result = saga2
.setup_swap(
&input_proofs,
&output_blinded_messages_2,
None,
input_verification,
)
.await;
assert!(
result.is_err(),
"Second setup should fail because proofs are pending"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 3)]
async fn test_swap_saga_concurrent_swaps() {
let mint = Arc::new(create_test_mint().await.unwrap());
let amount = Amount::from(100);
let (input_proofs, input_verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages_1, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let (output_blinded_messages_2, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let (output_blinded_messages_3, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let mint1 = Arc::clone(&mint);
let mint2 = Arc::clone(&mint);
let mint3 = Arc::clone(&mint);
let proofs1 = input_proofs.clone();
let proofs2 = input_proofs.clone();
let proofs3 = input_proofs.clone();
let verification1 = input_verification.clone();
let verification2 = input_verification.clone();
let verification3 = input_verification.clone();
let task1 = tokio::spawn(async move {
let db = mint1.localstore();
let pubsub = mint1.pubsub_manager();
let saga = SwapSaga::new(&mint1, db, pubsub);
let saga = saga
.setup_swap(&proofs1, &output_blinded_messages_1, None, verification1)
.await?;
let saga = saga.sign_outputs().await?;
saga.finalize().await
});
let task2 = tokio::spawn(async move {
let db = mint2.localstore();
let pubsub = mint2.pubsub_manager();
let saga = SwapSaga::new(&mint2, db, pubsub);
let saga = saga
.setup_swap(&proofs2, &output_blinded_messages_2, None, verification2)
.await?;
let saga = saga.sign_outputs().await?;
saga.finalize().await
});
let task3 = tokio::spawn(async move {
let db = mint3.localstore();
let pubsub = mint3.pubsub_manager();
let saga = SwapSaga::new(&mint3, db, pubsub);
let saga = saga
.setup_swap(&proofs3, &output_blinded_messages_3, None, verification3)
.await?;
let saga = saga.sign_outputs().await?;
saga.finalize().await
});
let results = tokio::try_join!(task1, task2, task3).expect("Tasks should complete");
let mut success_count = 0;
let mut error_count = 0;
for result in [results.0, results.1, results.2] {
match result {
Ok(_) => success_count += 1,
Err(_) => error_count += 1,
}
}
assert_eq!(success_count, 1, "Only one concurrent swap should succeed");
assert_eq!(error_count, 2, "Two concurrent swaps should fail");
let ys = input_proofs.ys().unwrap();
let states = mint
.localstore()
.get_proofs_states(&ys)
.await
.expect("Failed to get proof states");
for state in states {
assert_eq!(
state.unwrap(),
State::Spent,
"Proofs should be marked as spent after successful swap"
);
}
}
#[tokio::test]
async fn test_swap_saga_compensation_on_finalize_add_signatures_failure() {
let mint = create_test_mint().await.unwrap();
let amount = Amount::from(100);
let (input_proofs, input_verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let db = mint.localstore();
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(
&input_proofs,
&output_blinded_messages,
None,
input_verification,
)
.await
.expect("Setup should succeed");
let saga = saga.sign_outputs().await.expect("Signing should succeed");
assert_eq!(
saga.state_data.signatures.len(),
output_blinded_messages.len()
);
set_fail_for("ADD_SIGNATURES");
let result = saga.finalize().await;
clear_fail_for("ADD_SIGNATURES");
assert!(result.is_err(), "Finalize should fail");
let ys = input_proofs.ys().unwrap();
let states_after = db.get_proofs_states(&ys).await.unwrap();
assert!(
states_after.iter().all(|s| s.is_none()),
"Proofs should be removed by compensation"
);
let blinded_secrets: Vec<_> = output_blinded_messages
.iter()
.map(|bm| bm.blinded_secret)
.collect();
let signatures = db.get_blind_signatures(&blinded_secrets).await.unwrap();
assert!(
signatures.iter().all(|s| s.is_none()),
"Signatures should not be persisted after rollback"
);
}
#[tokio::test]
async fn test_swap_saga_compensation_on_finalize_update_proofs_failure() {
let mint = create_test_mint().await.unwrap();
let amount = Amount::from(100);
let (input_proofs, input_verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let db = mint.localstore();
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(
&input_proofs,
&output_blinded_messages,
None,
input_verification,
)
.await
.expect("Setup should succeed");
let saga = saga.sign_outputs().await.expect("Signing should succeed");
assert_eq!(
saga.state_data.signatures.len(),
output_blinded_messages.len()
);
set_fail_for("UPDATE_PROOFS");
let result = saga.finalize().await;
clear_fail_for("UPDATE_PROOFS");
assert!(result.is_err(), "Finalize should fail");
let ys = input_proofs.ys().unwrap();
let states_after = db.get_proofs_states(&ys).await.unwrap();
assert!(
states_after.iter().all(|s| s.is_none()),
"Proofs should be removed by compensation"
);
let blinded_secrets: Vec<_> = output_blinded_messages
.iter()
.map(|bm| bm.blinded_secret)
.collect();
let signatures = db.get_blind_signatures(&blinded_secrets).await.unwrap();
assert!(
signatures.iter().all(|s| s.is_none()),
"Signatures should not be persisted after rollback"
);
}
#[tokio::test]
async fn test_saga_state_persistence_after_setup() {
let mint = create_test_mint().await.unwrap();
let db = mint.localstore();
let amount = Amount::from(100);
let (input_proofs, verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(&input_proofs, &output_blinded_messages, None, verification)
.await
.expect("Setup should succeed");
let operation_id = saga.state_data.operation.id();
let saga = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(operation_id).await.expect("Failed to get saga");
tx.commit().await.unwrap();
result.expect("Saga should exist after setup")
};
use cdk_common::mint::{SagaStateEnum, SwapSagaState};
assert_eq!(
saga.state,
SagaStateEnum::Swap(SwapSagaState::SetupComplete),
"Saga should be SetupComplete"
);
assert_eq!(saga.operation_id, *operation_id);
let expected_blinded_secrets: Vec<_> = output_blinded_messages
.iter()
.map(|bm| bm.blinded_secret)
.collect();
let stored_blinded_secrets = mint
.localstore()
.get_blinded_secrets_by_operation_id(&saga.operation_id)
.await
.unwrap();
assert_eq!(stored_blinded_secrets.len(), expected_blinded_secrets.len());
for bs in &expected_blinded_secrets {
assert!(
stored_blinded_secrets.contains(bs),
"Blinded secret should be stored"
);
}
let expected_ys = input_proofs.ys().unwrap();
let stored_input_ys = mint
.localstore()
.get_proof_ys_by_operation_id(&saga.operation_id)
.await
.unwrap();
assert_eq!(stored_input_ys.len(), expected_ys.len());
for y in &expected_ys {
assert!(stored_input_ys.contains(y), "Input Y should be stored");
}
}
#[tokio::test]
async fn test_saga_deletion_on_success() {
let mint = create_test_mint().await.unwrap();
let db = mint.localstore();
let amount = Amount::from(100);
let (input_proofs, verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(&input_proofs, &output_blinded_messages, None, verification)
.await
.expect("Setup should succeed");
let operation_id = *saga.state_data.operation.id();
let saga_after_setup = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx
.get_saga(&operation_id)
.await
.expect("Failed to get saga");
tx.commit().await.unwrap();
result
};
assert!(saga_after_setup.is_some(), "Saga should exist after setup");
let saga = saga.sign_outputs().await.expect("Signing should succeed");
let saga_after_sign = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx
.get_saga(&operation_id)
.await
.expect("Failed to get saga");
tx.commit().await.unwrap();
result
};
assert!(
saga_after_sign.is_some(),
"Saga should still exist after signing"
);
let _response = saga.finalize().await.expect("Finalize should succeed");
let saga_after_finalize = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx
.get_saga(&operation_id)
.await
.expect("Failed to get saga");
tx.commit().await.unwrap();
result
};
assert!(
saga_after_finalize.is_none(),
"Saga should be deleted after successful finalization"
);
use cdk_common::mint::OperationKind;
let incomplete = db
.get_incomplete_sagas(OperationKind::Swap)
.await
.expect("Failed to get incomplete sagas");
assert_eq!(incomplete.len(), 0, "No incomplete sagas should exist");
}
#[tokio::test]
async fn test_get_incomplete_sagas_basic() {
let mint = create_test_mint().await.unwrap();
let db = mint.localstore();
let amount = Amount::from(100);
let (input_proofs_1, verification_1) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages_1, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let (input_proofs_2, verification_2) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages_2, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
use cdk_common::mint::OperationKind;
let incomplete_initial = db
.get_incomplete_sagas(OperationKind::Swap)
.await
.expect("Failed to get incomplete sagas");
assert_eq!(incomplete_initial.len(), 0);
let pubsub = mint.pubsub_manager();
let saga_1 = SwapSaga::new(&mint, db.clone(), pubsub.clone());
let saga_1 = saga_1
.setup_swap(
&input_proofs_1,
&output_blinded_messages_1,
None,
verification_1,
)
.await
.expect("Setup should succeed");
let op_id_1 = *saga_1.state_data.operation.id();
let incomplete_after_1 = db
.get_incomplete_sagas(OperationKind::Swap)
.await
.expect("Failed to get incomplete sagas");
assert_eq!(incomplete_after_1.len(), 1);
assert_eq!(incomplete_after_1[0].operation_id, op_id_1);
let saga_2 = SwapSaga::new(&mint, db.clone(), pubsub.clone());
let saga_2 = saga_2
.setup_swap(
&input_proofs_2,
&output_blinded_messages_2,
None,
verification_2,
)
.await
.expect("Setup should succeed");
let op_id_2 = *saga_2.state_data.operation.id();
let incomplete_after_2 = db
.get_incomplete_sagas(OperationKind::Swap)
.await
.expect("Failed to get incomplete sagas");
assert_eq!(incomplete_after_2.len(), 2);
let saga_1 = saga_1.sign_outputs().await.expect("Signing should succeed");
let _response_1 = saga_1.finalize().await.expect("Finalize should succeed");
let incomplete_after_finalize = db
.get_incomplete_sagas(OperationKind::Swap)
.await
.expect("Failed to get incomplete sagas");
assert_eq!(incomplete_after_finalize.len(), 1);
assert_eq!(incomplete_after_finalize[0].operation_id, op_id_2);
let saga_2 = saga_2.sign_outputs().await.expect("Signing should succeed");
let _response_2 = saga_2.finalize().await.expect("Finalize should succeed");
let incomplete_final = db
.get_incomplete_sagas(OperationKind::Swap)
.await
.expect("Failed to get incomplete sagas");
assert_eq!(incomplete_final.len(), 0);
}
#[tokio::test]
async fn test_saga_content_validation() {
let mint = create_test_mint().await.unwrap();
let db = mint.localstore();
let amount = Amount::from(100);
let (input_proofs, verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let expected_ys: Vec<_> = input_proofs.ys().unwrap();
let expected_blinded_secrets: Vec<_> = output_blinded_messages
.iter()
.map(|bm| bm.blinded_secret)
.collect();
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(&input_proofs, &output_blinded_messages, None, verification)
.await
.expect("Setup should succeed");
let operation_id = *saga.state_data.operation.id();
let saga = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx
.get_saga(&operation_id)
.await
.expect("Failed to get saga");
tx.commit().await.unwrap();
result.expect("Saga should exist after setup")
};
use cdk_common::mint::{OperationKind, SagaStateEnum, SwapSagaState};
assert_eq!(saga.operation_id, operation_id);
assert_eq!(saga.operation_kind, OperationKind::Swap);
assert_eq!(
saga.state,
SagaStateEnum::Swap(SwapSagaState::SetupComplete)
);
let stored_blinded_secrets = mint
.localstore()
.get_blinded_secrets_by_operation_id(&saga.operation_id)
.await
.unwrap();
assert_eq!(stored_blinded_secrets.len(), expected_blinded_secrets.len());
for bs in &expected_blinded_secrets {
assert!(stored_blinded_secrets.contains(bs));
}
let stored_input_ys = mint
.localstore()
.get_proof_ys_by_operation_id(&saga.operation_id)
.await
.unwrap();
assert_eq!(stored_input_ys.len(), expected_ys.len());
for y in &expected_ys {
assert!(stored_input_ys.contains(y));
}
use cdk_common::util::unix_time;
let now = unix_time();
assert!(
saga.created_at <= now,
"created_at should be <= current time"
);
assert!(
saga.updated_at <= now,
"updated_at should be <= current time"
);
assert!(
saga.created_at <= saga.updated_at,
"created_at should be <= updated_at"
);
}
#[tokio::test]
async fn test_saga_state_updates_persisted() {
let mint = create_test_mint().await.unwrap();
let db = mint.localstore();
let amount = Amount::from(100);
let (input_proofs, verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(&input_proofs, &output_blinded_messages, None, verification)
.await
.expect("Setup should succeed");
let operation_id = *saga.state_data.operation.id();
let state_after_setup = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx
.get_saga(&operation_id)
.await
.expect("Failed to get saga");
tx.commit().await.unwrap();
result.expect("Saga should exist after setup")
};
use cdk_common::mint::{SagaStateEnum, SwapSagaState};
assert_eq!(
state_after_setup.state,
SagaStateEnum::Swap(SwapSagaState::SetupComplete)
);
let initial_created_at = state_after_setup.created_at;
let initial_updated_at = state_after_setup.updated_at;
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
let saga = saga.sign_outputs().await.expect("Signing should succeed");
let state_after_sign = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx
.get_saga(&operation_id)
.await
.expect("Failed to get saga");
tx.commit().await.unwrap();
result.expect("Saga should exist after setup")
};
assert_eq!(
state_after_sign.state,
SagaStateEnum::Swap(SwapSagaState::SetupComplete),
"Saga remains SetupComplete (signing doesn't update DB)"
);
assert_eq!(state_after_sign.operation_id, operation_id);
assert_eq!(state_after_sign.created_at, initial_created_at);
assert_eq!(state_after_sign.updated_at, initial_updated_at);
let _response = saga.finalize().await.expect("Finalize should succeed");
let state_after_finalize = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx
.get_saga(&operation_id)
.await
.expect("Failed to get saga");
tx.commit().await.unwrap();
result
};
assert!(
state_after_finalize.is_none(),
"Saga should be deleted after finalize"
);
}
#[tokio::test]
async fn test_startup_recovery_saga_dropped_before_signing() {
let mint = create_test_mint().await.unwrap();
let amount = Amount::from(100);
let (input_proofs, input_verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let db = mint.localstore();
let ys = input_proofs.ys().unwrap();
{
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let _saga = saga
.setup_swap(
&input_proofs,
&output_blinded_messages,
None,
input_verification.clone(),
)
.await
.expect("Setup should succeed");
let states = db.get_proofs_states(&ys).await.unwrap();
assert!(states.iter().all(|s| s == &Some(State::Pending)));
}
let states_before_recovery = db.get_proofs_states(&ys).await.unwrap();
assert!(states_before_recovery
.iter()
.all(|s| s == &Some(State::Pending)));
mint.stop().await.expect("Recovery should succeed");
mint.start().await.expect("Recovery should succeed");
let states_after_recovery = db.get_proofs_states(&ys).await.unwrap();
assert!(
states_after_recovery.iter().all(|s| s.is_none()),
"Proofs should be removed after recovery (no signatures exist)"
);
let (new_output_blinded_messages, _) =
create_test_blinded_messages(&mint, amount).await.unwrap();
let pubsub = mint.pubsub_manager();
let new_saga = SwapSaga::new(&mint, db, pubsub);
let new_saga = new_saga
.setup_swap(
&input_proofs,
&new_output_blinded_messages,
None,
input_verification,
)
.await
.expect("Second swap should succeed after recovery");
let new_saga = new_saga
.sign_outputs()
.await
.expect("Signing should succeed");
let _response = new_saga.finalize().await.expect("Finalize should succeed");
let final_states = mint.localstore().get_proofs_states(&ys).await.unwrap();
assert!(final_states.iter().all(|s| s == &Some(State::Spent)));
}
#[tokio::test]
async fn test_startup_recovery_saga_dropped_after_signing() {
let mint = create_test_mint().await.unwrap();
let amount = Amount::from(100);
let (input_proofs, input_verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let db = mint.localstore();
let ys = input_proofs.ys().unwrap();
let blinded_secrets: Vec<_> = output_blinded_messages
.iter()
.map(|bm| bm.blinded_secret)
.collect();
{
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(
&input_proofs,
&output_blinded_messages,
None,
input_verification.clone(),
)
.await
.expect("Setup should succeed");
let _saga = saga.sign_outputs().await.expect("Signing should succeed");
}
let states_before = db.get_proofs_states(&ys).await.unwrap();
assert!(states_before.iter().all(|s| s == &Some(State::Pending)));
let sigs_before = db.get_blind_signatures(&blinded_secrets).await.unwrap();
assert!(sigs_before.iter().all(|s| s.is_none()));
mint.stop().await.expect("Recovery should succeed");
mint.start().await.expect("Recovery should succeed");
let states_after = db.get_proofs_states(&ys).await.unwrap();
assert!(
states_after.iter().all(|s| s.is_none()),
"Proofs should be removed (no signatures in DB)"
);
let (new_output_blinded_messages, _) =
create_test_blinded_messages(&mint, amount).await.unwrap();
let pubsub = mint.pubsub_manager();
let new_saga = SwapSaga::new(&mint, db, pubsub);
let new_saga = new_saga
.setup_swap(
&input_proofs,
&new_output_blinded_messages,
None,
input_verification,
)
.await
.expect("Second swap should succeed after recovery");
let new_saga = new_saga
.sign_outputs()
.await
.expect("Signing should succeed");
let _response = new_saga.finalize().await.expect("Finalize should succeed");
}
#[tokio::test]
async fn test_startup_recovery_multiple_operations() {
let mint = create_test_mint().await.unwrap();
let amount = Amount::from(100);
let (proofs_a, verification_a) = create_swap_inputs(&mint, amount).await;
let (proofs_b, verification_b) = create_swap_inputs(&mint, amount).await;
let (proofs_c, verification_c) = create_swap_inputs(&mint, amount).await;
let (outputs_a, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let (outputs_b, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let (outputs_c, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let db = mint.localstore();
let pubsub = mint.pubsub_manager();
let ys_a = proofs_a.ys().unwrap();
let ys_b = proofs_b.ys().unwrap();
let ys_c = proofs_c.ys().unwrap();
{
let saga_a = SwapSaga::new(&mint, db.clone(), pubsub.clone());
let _saga_a = saga_a
.setup_swap(&proofs_a, &outputs_a, None, verification_a)
.await
.expect("Operation A setup should succeed");
}
{
let saga_b = SwapSaga::new(&mint, db.clone(), pubsub.clone());
let saga_b = saga_b
.setup_swap(&proofs_b, &outputs_b, None, verification_b)
.await
.expect("Operation B setup should succeed");
let _saga_b = saga_b
.sign_outputs()
.await
.expect("Operation B signing should succeed");
}
{
let saga_c = SwapSaga::new(&mint, db.clone(), pubsub.clone());
let saga_c = saga_c
.setup_swap(&proofs_c, &outputs_c, None, verification_c)
.await
.expect("Operation C setup should succeed");
let saga_c = saga_c
.sign_outputs()
.await
.expect("Operation C signing should succeed");
let _response = saga_c
.finalize()
.await
.expect("Operation C finalize should succeed");
}
let states_a_before = db.get_proofs_states(&ys_a).await.unwrap();
let states_b_before = db.get_proofs_states(&ys_b).await.unwrap();
let states_c_before = db.get_proofs_states(&ys_c).await.unwrap();
assert!(states_a_before.iter().all(|s| s == &Some(State::Pending)));
assert!(states_b_before.iter().all(|s| s == &Some(State::Pending)));
assert!(states_c_before.iter().all(|s| s == &Some(State::Spent)));
mint.stop().await.expect("Recovery should succeed");
mint.start().await.expect("Recovery should succeed");
let states_a_after = db.get_proofs_states(&ys_a).await.unwrap();
let states_b_after = db.get_proofs_states(&ys_b).await.unwrap();
let states_c_after = db.get_proofs_states(&ys_c).await.unwrap();
assert!(
states_a_after.iter().all(|s| s.is_none()),
"Operation A proofs should be removed (no signatures)"
);
assert!(
states_b_after.iter().all(|s| s.is_none()),
"Operation B proofs should be removed (no signatures in DB)"
);
assert!(
states_c_after.iter().all(|s| s == &Some(State::Spent)),
"Operation C proofs should remain SPENT (completed successfully)"
);
}
#[tokio::test]
async fn test_operation_id_uniqueness_and_tracking() {
let mint = Arc::new(create_test_mint().await.unwrap());
let amount = Amount::from(100);
let (proofs_1, verification_1) = create_swap_inputs(&mint, amount).await;
let (proofs_2, verification_2) = create_swap_inputs(&mint, amount).await;
let (proofs_3, verification_3) = create_swap_inputs(&mint, amount).await;
let (outputs_1, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let (outputs_2, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let (outputs_3, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let db = mint.localstore();
let ys_1 = proofs_1.ys().unwrap();
let ys_2 = proofs_2.ys().unwrap();
let ys_3 = proofs_3.ys().unwrap();
{
let pubsub = mint.pubsub_manager();
let saga_1 = SwapSaga::new(&mint, db.clone(), pubsub.clone());
let _saga_1 = saga_1
.setup_swap(&proofs_1, &outputs_1, None, verification_1)
.await
.expect("Swap 1 setup should succeed");
let saga_2 = SwapSaga::new(&mint, db.clone(), pubsub.clone());
let _saga_2 = saga_2
.setup_swap(&proofs_2, &outputs_2, None, verification_2)
.await
.expect("Swap 2 setup should succeed");
let saga_3 = SwapSaga::new(&mint, db.clone(), pubsub.clone());
let _saga_3 = saga_3
.setup_swap(&proofs_3, &outputs_3, None, verification_3)
.await
.expect("Swap 3 setup should succeed");
}
let states_1 = db.get_proofs_states(&ys_1).await.unwrap();
let states_2 = db.get_proofs_states(&ys_2).await.unwrap();
let states_3 = db.get_proofs_states(&ys_3).await.unwrap();
assert!(states_1.iter().all(|s| s == &Some(State::Pending)));
assert!(states_2.iter().all(|s| s == &Some(State::Pending)));
assert!(states_3.iter().all(|s| s == &Some(State::Pending)));
mint.stop().await.expect("Recovery should succeed");
mint.start().await.expect("Recovery should succeed");
let states_1_after = db.get_proofs_states(&ys_1).await.unwrap();
let states_2_after = db.get_proofs_states(&ys_2).await.unwrap();
let states_3_after = db.get_proofs_states(&ys_3).await.unwrap();
assert!(
states_1_after.iter().all(|s| s.is_none()),
"Swap 1 proofs should be removed"
);
assert!(
states_2_after.iter().all(|s| s.is_none()),
"Swap 2 proofs should be removed"
);
assert!(
states_3_after.iter().all(|s| s.is_none()),
"Swap 3 proofs should be removed"
);
let (new_outputs_1, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let verification = create_verification(amount);
let pubsub = mint.pubsub_manager();
let new_saga = SwapSaga::new(&mint, db, pubsub);
let result = new_saga
.setup_swap(&proofs_1, &new_outputs_1, None, verification)
.await;
assert!(
result.is_ok(),
"Should be able to reuse proofs after recovery"
);
}
#[tokio::test]
async fn test_crash_recovery_without_compensation() {
let mint = create_test_mint().await.unwrap();
let db = mint.localstore();
let amount = Amount::from(100);
let (input_proofs, verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let operation_id;
let ys = input_proofs.ys().unwrap();
let _blinded_secrets: Vec<_> = output_blinded_messages
.iter()
.map(|bm| bm.blinded_secret)
.collect();
{
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(&input_proofs, &output_blinded_messages, None, verification)
.await
.expect("Setup should succeed");
operation_id = *saga.state_data.operation.id();
drop(saga);
}
let saga = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx
.get_saga(&operation_id)
.await
.expect("Failed to get saga");
tx.commit().await.unwrap();
result
};
assert!(saga.is_some(), "Saga should persist after crash");
let states = db.get_proofs_states(&ys).await.unwrap();
assert!(
states.iter().all(|s| s == &Some(State::Pending)),
"Proofs should still be Pending after crash (compensation didn't run)"
);
mint.stop().await.expect("Stop should succeed");
mint.start().await.expect("Start should succeed");
let states_after = db.get_proofs_states(&ys).await.unwrap();
assert!(
states_after.iter().all(|s| s.is_none()),
"Recovery should remove proofs"
);
let saga_after = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx
.get_saga(&operation_id)
.await
.expect("Failed to get saga");
tx.commit().await.unwrap();
result
};
assert!(saga_after.is_none(), "Recovery should delete saga");
}
#[tokio::test]
async fn test_crash_recovery_after_setup_only() {
let mint = create_test_mint().await.unwrap();
let db = mint.localstore();
let amount = Amount::from(100);
let (input_proofs, verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let operation_id;
let ys = input_proofs.ys().unwrap();
let _blinded_secrets: Vec<_> = output_blinded_messages
.iter()
.map(|bm| bm.blinded_secret)
.collect();
{
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(&input_proofs, &output_blinded_messages, None, verification)
.await
.expect("Setup should succeed");
operation_id = *saga.state_data.operation.id();
let saga = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&operation_id).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(saga.is_some());
drop(saga);
}
let saga_before = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&operation_id).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(saga_before.is_some());
let states_before = db.get_proofs_states(&ys).await.unwrap();
assert!(states_before.iter().all(|s| s == &Some(State::Pending)));
mint.stop().await.expect("Stop should succeed");
mint.start().await.expect("Start should succeed");
let saga_after = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&operation_id).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(saga_after.is_none(), "Saga should be deleted");
let states_after = db.get_proofs_states(&ys).await.unwrap();
assert!(
states_after.iter().all(|s| s.is_none()),
"Proofs should be removed"
);
}
#[tokio::test]
async fn test_crash_recovery_after_signing() {
let mint = create_test_mint().await.unwrap();
let db = mint.localstore();
let amount = Amount::from(100);
let (input_proofs, verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let operation_id;
let ys = input_proofs.ys().unwrap();
let blinded_secrets: Vec<_> = output_blinded_messages
.iter()
.map(|bm| bm.blinded_secret)
.collect();
{
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(&input_proofs, &output_blinded_messages, None, verification)
.await
.expect("Setup should succeed");
operation_id = *saga.state_data.operation.id();
let saga = saga.sign_outputs().await.expect("Signing should succeed");
assert_eq!(
saga.state_data.signatures.len(),
output_blinded_messages.len()
);
drop(saga);
}
let saga_before = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&operation_id).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(saga_before.is_some());
let sigs_before = db.get_blind_signatures(&blinded_secrets).await.unwrap();
assert!(
sigs_before.iter().all(|s| s.is_none()),
"Signatures should not be in DB (never persisted)"
);
mint.stop().await.expect("Stop should succeed");
mint.start().await.expect("Start should succeed");
let saga_after = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&operation_id).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(saga_after.is_none(), "Saga should be deleted");
let states_after = db.get_proofs_states(&ys).await.unwrap();
assert!(
states_after.iter().all(|s| s.is_none()),
"Proofs should be removed"
);
}
#[tokio::test]
async fn test_recovery_multiple_incomplete_sagas() {
let mint = create_test_mint().await.unwrap();
let db = mint.localstore();
let amount = Amount::from(100);
let (proofs_a, verification_a) = create_swap_inputs(&mint, amount).await;
let (proofs_b, verification_b) = create_swap_inputs(&mint, amount).await;
let (proofs_c, verification_c) = create_swap_inputs(&mint, amount).await;
let (outputs_a, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let (outputs_b, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let (outputs_c, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let ys_a = proofs_a.ys().unwrap();
let ys_b = proofs_b.ys().unwrap();
let ys_c = proofs_c.ys().unwrap();
let op_id_a;
let op_id_b;
let op_id_c;
{
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(&proofs_a, &outputs_a, None, verification_a)
.await
.expect("Setup A should succeed");
op_id_a = *saga.state_data.operation.id();
drop(saga);
}
{
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(&proofs_b, &outputs_b, None, verification_b)
.await
.expect("Setup B should succeed");
op_id_b = *saga.state_data.operation.id();
let saga = saga.sign_outputs().await.expect("Sign B should succeed");
drop(saga);
}
{
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(&proofs_c, &outputs_c, None, verification_c)
.await
.expect("Setup C should succeed");
op_id_c = *saga.state_data.operation.id();
let saga = saga.sign_outputs().await.expect("Sign C should succeed");
let _response = saga.finalize().await.expect("Finalize C should succeed");
}
use cdk_common::mint::OperationKind;
let incomplete_before = db.get_incomplete_sagas(OperationKind::Swap).await.unwrap();
assert_eq!(
incomplete_before.len(),
2,
"Should have 2 incomplete sagas (A and B)"
);
let states_a_before = db.get_proofs_states(&ys_a).await.unwrap();
let states_b_before = db.get_proofs_states(&ys_b).await.unwrap();
let states_c_before = db.get_proofs_states(&ys_c).await.unwrap();
assert!(states_a_before.iter().all(|s| s == &Some(State::Pending)));
assert!(states_b_before.iter().all(|s| s == &Some(State::Pending)));
assert!(states_c_before.iter().all(|s| s == &Some(State::Spent)));
mint.stop().await.expect("Stop should succeed");
mint.start().await.expect("Start should succeed");
let incomplete_after = db.get_incomplete_sagas(OperationKind::Swap).await.unwrap();
assert_eq!(
incomplete_after.len(),
0,
"No incomplete sagas after recovery"
);
let saga_a = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&op_id_a).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(saga_a.is_none());
let states_a_after = db.get_proofs_states(&ys_a).await.unwrap();
assert!(states_a_after.iter().all(|s| s.is_none()));
let saga_b = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&op_id_b).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(saga_b.is_none());
let states_b_after = db.get_proofs_states(&ys_b).await.unwrap();
assert!(states_b_after.iter().all(|s| s.is_none()));
let saga_c = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&op_id_c).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(saga_c.is_none(), "Completed saga was deleted");
let states_c_after = db.get_proofs_states(&ys_c).await.unwrap();
assert!(
states_c_after.iter().all(|s| s == &Some(State::Spent)),
"Completed saga proofs remain spent"
);
}
#[tokio::test]
async fn test_recovery_idempotence() {
let mint = create_test_mint().await.unwrap();
let db = mint.localstore();
let amount = Amount::from(100);
let (input_proofs, verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let operation_id;
let ys = input_proofs.ys().unwrap();
{
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(&input_proofs, &output_blinded_messages, None, verification)
.await
.expect("Setup should succeed");
operation_id = *saga.state_data.operation.id();
drop(saga);
}
let saga_before = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&operation_id).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(saga_before.is_some());
mint.stop().await.expect("First stop should succeed");
mint.start().await.expect("First start should succeed");
let saga_after_1 = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&operation_id).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(saga_after_1.is_none());
let states_after_1 = db.get_proofs_states(&ys).await.unwrap();
assert!(states_after_1.iter().all(|s| s.is_none()));
mint.stop().await.expect("Second stop should succeed");
mint.start().await.expect("Second start should succeed");
let saga_after_2 = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&operation_id).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(saga_after_2.is_none());
let states_after_2 = db.get_proofs_states(&ys).await.unwrap();
assert!(states_after_2.iter().all(|s| s.is_none()));
mint.stop().await.expect("Third stop should succeed");
mint.start().await.expect("Third start should succeed");
let saga_after_3 = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&operation_id).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(saga_after_3.is_none());
}
#[tokio::test]
async fn test_orphaned_saga_cleanup() {
let mint = create_test_mint().await.unwrap();
let db = mint.localstore();
let amount = Amount::from(100);
let (input_proofs, verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(&input_proofs, &output_blinded_messages, None, verification)
.await
.expect("Setup should succeed");
let operation_id = *saga.state_data.operation.id();
let ys = input_proofs.ys().unwrap();
let saga = saga.sign_outputs().await.expect("Signing should succeed");
let _response = saga.finalize().await.expect("Finalize should succeed");
let states = db.get_proofs_states(&ys).await.unwrap();
assert!(
states.iter().all(|s| s == &Some(State::Spent)),
"Proofs should be SPENT after successful swap"
);
let saga = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&operation_id).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(
saga.is_none(),
"Saga should be deleted after successful swap"
);
}
#[tokio::test]
async fn test_recovery_with_orphaned_proofs() {
let mint = create_test_mint().await.unwrap();
let db = mint.localstore();
let amount = Amount::from(100);
let (input_proofs, verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let ys = input_proofs.ys().unwrap();
let operation_id = {
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(&input_proofs, &output_blinded_messages, None, verification)
.await
.expect("Setup should succeed");
let op_id = *saga.state_data.operation.id();
drop(saga);
op_id
};
let states_before = db.get_proofs_states(&ys).await.unwrap();
assert!(states_before.iter().all(|s| s == &Some(State::Pending)));
let saga_before = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&operation_id).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(saga_before.is_some());
{
let mut tx = db.begin_transaction().await.unwrap();
tx.delete_saga(&operation_id).await.unwrap();
tx.commit().await.unwrap();
}
let saga_after_delete = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&operation_id).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(saga_after_delete.is_none(), "Saga should be deleted");
let states_after_delete = db.get_proofs_states(&ys).await.unwrap();
assert!(
states_after_delete
.iter()
.all(|s| s == &Some(State::Pending)),
"Proofs should still be PENDING (orphaned)"
);
mint.stop().await.expect("Stop should succeed");
mint.start().await.expect("Start should succeed");
let states_after_recovery = db.get_proofs_states(&ys).await.unwrap();
assert!(
states_after_recovery
.iter()
.all(|s| s == &Some(State::Pending)),
"Orphaned proofs remain PENDING (recovery doesn't clean up proofs without saga)"
);
}
#[tokio::test]
async fn test_recovery_with_partial_state() {
let mint = create_test_mint().await.unwrap();
let db = mint.localstore();
let amount = Amount::from(100);
let (input_proofs, verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let ys = input_proofs.ys().unwrap();
let blinded_secrets: Vec<_> = output_blinded_messages
.iter()
.map(|bm| bm.blinded_secret)
.collect();
let operation_id = {
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(&input_proofs, &output_blinded_messages, None, verification)
.await
.expect("Setup should succeed");
let op_id = *saga.state_data.operation.id();
drop(saga);
op_id
};
let saga_before = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&operation_id).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(saga_before.is_some());
let states_before = db.get_proofs_states(&ys).await.unwrap();
assert!(states_before.iter().all(|s| s == &Some(State::Pending)));
{
let mut tx = db.begin_transaction().await.unwrap();
tx.delete_blinded_messages(&blinded_secrets).await.unwrap();
tx.commit().await.unwrap();
}
mint.stop().await.expect("Stop should succeed");
mint.start().await.expect("Start should succeed");
let saga_after = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&operation_id).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(saga_after.is_none(), "Saga should be deleted");
let states_after = db.get_proofs_states(&ys).await.unwrap();
assert!(
states_after.iter().all(|s| s.is_none()),
"Proofs should be removed"
);
}
#[tokio::test]
async fn test_recovery_with_missing_blinded_messages() {
let mint = create_test_mint().await.unwrap();
let db = mint.localstore();
let amount = Amount::from(100);
let (input_proofs, verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let ys = input_proofs.ys().unwrap();
let blinded_secrets: Vec<_> = output_blinded_messages
.iter()
.map(|bm| bm.blinded_secret)
.collect();
let operation_id = {
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(&input_proofs, &output_blinded_messages, None, verification)
.await
.expect("Setup should succeed");
let op_id = *saga.state_data.operation.id();
drop(saga);
op_id
};
let saga = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&operation_id).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(saga.is_some(), "Saga should exist");
{
let mut tx = db.begin_transaction().await.unwrap();
tx.delete_blinded_messages(&blinded_secrets).await.unwrap();
tx.commit().await.unwrap();
}
mint.stop().await.expect("Stop should succeed");
mint.start()
.await
.expect("Start should succeed despite missing blinded messages");
let saga_after = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&operation_id).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(saga_after.is_none(), "Saga should be cleaned up");
let states_after = db.get_proofs_states(&ys).await.unwrap();
assert!(
states_after.iter().all(|s| s.is_none()),
"Proofs should be removed"
);
}
#[tokio::test]
async fn test_saga_deletion_failure_handling() {
let mint = create_test_mint().await.unwrap();
let db = mint.localstore();
let amount = Amount::from(100);
let (input_proofs, verification) = create_swap_inputs(&mint, amount).await;
let (output_blinded_messages, _) = create_test_blinded_messages(&mint, amount).await.unwrap();
let pubsub = mint.pubsub_manager();
let saga = SwapSaga::new(&mint, db.clone(), pubsub);
let saga = saga
.setup_swap(&input_proofs, &output_blinded_messages, None, verification)
.await
.expect("Setup should succeed");
let operation_id = *saga.state_data.operation.id();
let ys = input_proofs.ys().unwrap();
let saga = saga.sign_outputs().await.expect("Signing should succeed");
let response = saga.finalize().await.expect("Finalize should succeed");
assert_eq!(
response.signatures.len(),
output_blinded_messages.len(),
"Should have signatures for all outputs"
);
let states = db.get_proofs_states(&ys).await.unwrap();
assert!(
states.iter().all(|s| s == &Some(State::Spent)),
"Proofs should be SPENT"
);
let saga = {
let mut tx = db.begin_transaction().await.unwrap();
let result = tx.get_saga(&operation_id).await.unwrap();
tx.commit().await.unwrap();
result
};
assert!(saga.is_none(), "Saga should be deleted");
}