Files
cdk/crates/cdk-sqlite/src/mint/memory.rs
tsk 33c206a310 Swap saga (#1183)
# 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
2025-10-22 08:30:33 -05:00

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)
}