use std::sync::Arc;
use async_trait::async_trait;
use cdk_common::database::{self, WalletDatabase};
use tracing::instrument;
use crate::nuts::PublicKey;
use crate::wallet::saga::CompensatingAction;
use crate::Error;
pub struct RemovePendingProofs {
pub localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
pub proof_ys: Vec<PublicKey>,
pub saga_id: uuid::Uuid,
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl CompensatingAction for RemovePendingProofs {
#[instrument(skip_all)]
async fn execute(&self) -> Result<(), Error> {
tracing::info!(
"Compensation: Removing {} pending proofs from receive",
self.proof_ys.len()
);
self.localstore
.update_proofs(vec![], self.proof_ys.clone())
.await
.map_err(Error::Database)?;
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 {
"RemovePendingProofs"
}
}
#[cfg(test)]
mod tests {
use cdk_common::nuts::{CurrencyUnit, State};
use cdk_common::wallet::{
OperationData, ReceiveOperationData, ReceiveSagaState, WalletSaga, WalletSagaState,
};
use cdk_common::Amount;
use super::*;
use crate::wallet::saga::test_utils::*;
use crate::wallet::saga::CompensatingAction;
fn test_receive_saga(mint_url: cdk_common::mint_url::MintUrl) -> WalletSaga {
WalletSaga::new(
uuid::Uuid::new_v4(),
WalletSagaState::Receive(ReceiveSagaState::ProofsPending),
Amount::from(1000),
mint_url,
CurrencyUnit::Sat,
OperationData::Receive(ReceiveOperationData {
token: Some("test_token".to_string()),
amount: Some(Amount::from(1000)),
counter_start: Some(0),
counter_end: Some(10),
blinded_messages: None,
}),
)
}
#[tokio::test]
async fn test_remove_pending_proofs_is_idempotent() {
let db = create_test_db().await;
let mint_url = test_mint_url();
let keyset_id = test_keyset_id();
let proof_info = test_proof_info(keyset_id, 100, mint_url.clone(), State::Pending);
let proof_y = proof_info.y;
db.update_proofs(vec![proof_info], vec![]).await.unwrap();
let saga = test_receive_saga(mint_url);
let saga_id = saga.id;
db.add_saga(saga).await.unwrap();
let compensation = RemovePendingProofs {
localstore: db.clone(),
proof_ys: vec![proof_y],
saga_id,
};
compensation.execute().await.unwrap();
compensation.execute().await.unwrap();
let all_proofs = db.get_proofs(None, None, None, None).await.unwrap();
assert!(all_proofs.is_empty());
}
#[tokio::test]
async fn test_remove_pending_proofs_handles_missing_saga() {
let db = create_test_db().await;
let mint_url = test_mint_url();
let keyset_id = test_keyset_id();
let proof_info = test_proof_info(keyset_id, 100, mint_url.clone(), State::Pending);
let proof_y = proof_info.y;
db.update_proofs(vec![proof_info], vec![]).await.unwrap();
let saga_id = uuid::Uuid::new_v4();
let compensation = RemovePendingProofs {
localstore: db.clone(),
proof_ys: vec![proof_y],
saga_id,
};
compensation.execute().await.unwrap();
let all_proofs = db.get_proofs(None, None, None, None).await.unwrap();
assert!(all_proofs.is_empty());
}
#[tokio::test]
async fn test_remove_pending_proofs_only_affects_specified_proofs() {
let db = create_test_db().await;
let mint_url = test_mint_url();
let keyset_id = test_keyset_id();
let proof_info_1 = test_proof_info(keyset_id, 100, mint_url.clone(), State::Pending);
let proof_info_2 = test_proof_info(keyset_id, 200, mint_url.clone(), State::Pending);
let proof_y_1 = proof_info_1.y;
let proof_y_2 = proof_info_2.y;
db.update_proofs(vec![proof_info_1, proof_info_2], vec![])
.await
.unwrap();
let saga = test_receive_saga(mint_url);
let saga_id = saga.id;
db.add_saga(saga).await.unwrap();
let compensation = RemovePendingProofs {
localstore: db.clone(),
proof_ys: vec![proof_y_1],
saga_id,
};
compensation.execute().await.unwrap();
let remaining_proofs = db.get_proofs(None, None, None, None).await.unwrap();
assert_eq!(remaining_proofs.len(), 1);
assert_eq!(remaining_proofs[0].y, proof_y_2);
}
}