use std::sync::Arc;
use async_trait::async_trait;
use cdk_common::database::{self, WalletDatabase};
use tracing::instrument;
use uuid::Uuid;
use crate::wallet::saga::CompensatingAction;
use crate::Error;
pub struct ReleaseMintQuote {
pub localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
pub operation_id: Uuid,
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl CompensatingAction for ReleaseMintQuote {
#[instrument(skip_all)]
async fn execute(&self) -> Result<(), Error> {
tracing::info!(
"Compensation: Releasing mint quote reserved by operation {}",
self.operation_id
);
self.localstore
.release_mint_quote(&self.operation_id)
.await
.map_err(Error::Database)?;
Ok(())
}
fn name(&self) -> &'static str {
"ReleaseMintQuote"
}
}
pub struct MintCompensation {
pub localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
pub quote_id: String,
pub saga_id: uuid::Uuid,
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl CompensatingAction for MintCompensation {
#[instrument(skip_all)]
async fn execute(&self) -> Result<(), Error> {
tracing::info!(
"Compensation: Mint operation for quote {} failed, no rollback needed",
self.quote_id
);
if let Err(e) = self.localstore.delete_saga(&self.saga_id).await {
tracing::warn!(
"Compensation: Failed to delete saga {}: {}. Will be cleaned up on recovery.",
self.saga_id,
e
);
}
Ok(())
}
fn name(&self) -> &'static str {
"MintCompensation"
}
}
#[cfg(test)]
mod tests {
use cdk_common::nut00::KnownMethod;
use cdk_common::nuts::CurrencyUnit;
use cdk_common::wallet::{
MintQuote, OperationData, SwapOperationData, SwapSagaState, WalletSaga, WalletSagaState,
};
use cdk_common::{Amount, PaymentMethod};
use super::*;
use crate::wallet::saga::test_utils::*;
use crate::wallet::saga::CompensatingAction;
fn test_issue_saga(mint_url: cdk_common::mint_url::MintUrl) -> WalletSaga {
WalletSaga::new(
uuid::Uuid::new_v4(),
WalletSagaState::Swap(SwapSagaState::ProofsReserved),
Amount::from(1000),
mint_url,
CurrencyUnit::Sat,
OperationData::Swap(SwapOperationData {
input_amount: Amount::from(1000),
output_amount: Amount::from(990),
counter_start: Some(0),
counter_end: Some(10),
blinded_messages: None,
}),
)
}
fn test_mint_quote(mint_url: cdk_common::mint_url::MintUrl) -> MintQuote {
MintQuote::new(
format!("test_quote_{}", uuid::Uuid::new_v4()),
mint_url,
PaymentMethod::Known(KnownMethod::Bolt11),
Some(Amount::from(1000)),
CurrencyUnit::Sat,
"lnbc1000...".to_string(),
9999999999,
None,
)
}
#[tokio::test]
async fn test_release_mint_quote_is_idempotent() {
let db = create_test_db().await;
let mint_url = test_mint_url();
let operation_id = uuid::Uuid::new_v4();
let mut quote = test_mint_quote(mint_url);
quote.used_by_operation = Some(operation_id.to_string());
db.add_mint_quote(quote.clone()).await.unwrap();
let compensation = ReleaseMintQuote {
localstore: db.clone(),
operation_id,
};
compensation.execute().await.unwrap();
compensation.execute().await.unwrap();
let retrieved_quote = db.get_mint_quote("e.id).await.unwrap().unwrap();
assert!(retrieved_quote.used_by_operation.is_none());
}
#[tokio::test]
async fn test_release_mint_quote_handles_no_matching_quote() {
let db = create_test_db().await;
let operation_id = uuid::Uuid::new_v4();
let compensation = ReleaseMintQuote {
localstore: db.clone(),
operation_id,
};
let result = compensation.execute().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_mint_compensation_is_idempotent() {
let db = create_test_db().await;
let mint_url = test_mint_url();
let saga = test_issue_saga(mint_url);
let saga_id = saga.id;
db.add_saga(saga).await.unwrap();
let compensation = MintCompensation {
localstore: db.clone(),
quote_id: "test_quote".to_string(),
saga_id,
};
compensation.execute().await.unwrap();
compensation.execute().await.unwrap();
assert!(db.get_saga(&saga_id).await.unwrap().is_none());
}
#[tokio::test]
async fn test_mint_compensation_handles_missing_saga() {
let db = create_test_db().await;
let saga_id = uuid::Uuid::new_v4();
let compensation = MintCompensation {
localstore: db.clone(),
quote_id: "test_quote".to_string(),
saga_id,
};
let result = compensation.execute().await;
assert!(result.is_ok());
}
}