mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-15 10:06:12 +01:00
# Implement Saga Pattern for Swap Operations with Recovery Mechanism ## Overview This PR refactors the swap operation implementation to use the saga pattern - a distributed transaction pattern that provides reliable transaction management through explicit state tracking and compensation-based error handling. The implementation includes a robust recovery mechanism that automatically handles swap operations interrupted by crashes, power loss, or network failures. ## What Changed **Saga Pattern Implementation:** - Introduced a strict linear state machine for swaps: `Initial` → `SetupComplete` → `Signed` → `Completed` - New modular `swap_saga` module with state validation, compensation logic, and saga orchestration - Automatic rollback of database changes on failure, ensuring atomic swap operations - Replaced previous swap implementation (`swap.rs`, `blinded_message_writer.rs`) with saga-based approach **Recovery Mechanism:** - Added `operation_id` and `operation_kind` columns to database schema for tracking which operation proofs belong to - New `recover_from_bad_swaps()` method that runs on mint startup to handle incomplete swaps - For proofs left in `PENDING` state from swap operations: - If blind signatures exist: marks proofs as `SPENT` (swap completed but not finalized) - If no blind signatures exist: removes proofs from database (swap failed partway through) - Database migrations included for both PostgreSQL and SQLite
75 lines
2.2 KiB
Rust
75 lines
2.2 KiB
Rust
//! In-memory database that is provided by the `cdk-sqlite` crate, mainly for testing purposes.
|
|
use std::collections::HashMap;
|
|
|
|
use cdk_common::database::{self, MintDatabase, MintKeysDatabase};
|
|
use cdk_common::mint::{self, MintKeySetInfo, MintQuote, Operation};
|
|
use cdk_common::nuts::{CurrencyUnit, Id, Proofs};
|
|
use cdk_common::MintInfo;
|
|
|
|
use super::MintSqliteDatabase;
|
|
|
|
const CDK_MINT_PRIMARY_NAMESPACE: &str = "cdk_mint";
|
|
const CDK_MINT_CONFIG_SECONDARY_NAMESPACE: &str = "config";
|
|
const CDK_MINT_CONFIG_KV_KEY: &str = "mint_info";
|
|
|
|
/// Creates a new in-memory [`MintSqliteDatabase`] instance
|
|
pub async fn empty() -> Result<MintSqliteDatabase, database::Error> {
|
|
#[cfg(not(feature = "sqlcipher"))]
|
|
let path = ":memory:";
|
|
#[cfg(feature = "sqlcipher")]
|
|
let path = (":memory:", "memory");
|
|
|
|
MintSqliteDatabase::new(path).await
|
|
}
|
|
|
|
/// Creates a new in-memory [`MintSqliteDatabase`] instance with the given state
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn new_with_state(
|
|
active_keysets: HashMap<CurrencyUnit, Id>,
|
|
keysets: Vec<MintKeySetInfo>,
|
|
mint_quotes: Vec<MintQuote>,
|
|
melt_quotes: Vec<mint::MeltQuote>,
|
|
pending_proofs: Proofs,
|
|
spent_proofs: Proofs,
|
|
mint_info: MintInfo,
|
|
) -> Result<MintSqliteDatabase, database::Error> {
|
|
let db = empty().await?;
|
|
let mut tx = MintKeysDatabase::begin_transaction(&db).await?;
|
|
|
|
for active_keyset in active_keysets {
|
|
tx.set_active_keyset(active_keyset.0, active_keyset.1)
|
|
.await?;
|
|
}
|
|
|
|
for keyset in keysets {
|
|
tx.add_keyset_info(keyset).await?;
|
|
}
|
|
tx.commit().await?;
|
|
|
|
let mut tx = MintDatabase::begin_transaction(&db).await?;
|
|
|
|
for quote in mint_quotes {
|
|
tx.add_mint_quote(quote).await?;
|
|
}
|
|
|
|
for quote in melt_quotes {
|
|
tx.add_melt_quote(quote).await?;
|
|
}
|
|
|
|
tx.add_proofs(pending_proofs, None, &Operation::new_swap())
|
|
.await?;
|
|
tx.add_proofs(spent_proofs, None, &Operation::new_swap())
|
|
.await?;
|
|
let mint_info_bytes = serde_json::to_vec(&mint_info)?;
|
|
tx.kv_write(
|
|
CDK_MINT_PRIMARY_NAMESPACE,
|
|
CDK_MINT_CONFIG_SECONDARY_NAMESPACE,
|
|
CDK_MINT_CONFIG_KV_KEY,
|
|
&mint_info_bytes,
|
|
)
|
|
.await?;
|
|
tx.commit().await?;
|
|
|
|
Ok(db)
|
|
}
|