Selaa lähdekoodia

Refactor update_proofs_state to accept new_state parameter

- Rename update_proofs() to update_proofs_state() and add new_state parameter
- Remove set_new_state() and get_state() methods from ProofsWithState
- Make ProofsWithState.state field public
- Use check_state_transition() directly at call sites
- Update ProofsWithState.state after database write succeeds
- Add test to verify state field is updated after update_proofs_state()
Cesar Rodas 3 viikkoa sitten
vanhempi
säilyke
fbf288649a

+ 6 - 2
crates/cdk-common/src/database/mint/mod.rs

@@ -301,10 +301,14 @@ pub trait ProofsTransaction {
         operation: &Operation,
     ) -> Result<Acquired<ProofsWithState>, Self::Err>;
 
-    /// Updates the proofs to a given states and return the previous states
-    async fn update_proofs(
+    /// Updates the proofs to the given state in the database.
+    ///
+    /// Also updates the `state` field on the [`ProofsWithState`] wrapper to reflect
+    /// the new state after the database update succeeds.
+    async fn update_proofs_state(
         &mut self,
         proofs: &mut Acquired<ProofsWithState>,
+        new_state: State,
     ) -> Result<(), Self::Err>;
 
     /// get proofs states

+ 1 - 0
crates/cdk-common/src/database/mint/test/mod.rs

@@ -257,6 +257,7 @@ macro_rules! mint_db_test {
             remove_spent_proofs_should_fail,
             get_proofs_with_inconsistent_states_fails,
             get_proofs_fails_when_some_not_found,
+            update_proofs_state_updates_proofs_with_state,
         );
     };
     ($make_db_fn:ident, $($name:ident),+ $(,)?) => {

+ 89 - 14
crates/cdk-common/src/database/mint/test/proofs.rs

@@ -8,6 +8,7 @@ use cashu::{Amount, Id, SecretKey};
 use crate::database::mint::test::setup_keyset;
 use crate::database::mint::{Database, Error, KeysDatabase, Proof, QuoteId};
 use crate::mint::Operation;
+use crate::state::check_state_transition;
 
 /// Test get proofs by keyset id
 pub async fn get_proofs_by_keyset_id<DB>(db: DB)
@@ -218,8 +219,10 @@ where
     // Update to pending
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut proofs = tx.get_proofs(&ys).await.unwrap();
-    proofs.set_new_state(State::Pending).unwrap();
-    tx.update_proofs(&mut proofs).await.unwrap();
+    check_state_transition(proofs.state, State::Pending).unwrap();
+    tx.update_proofs_state(&mut proofs, State::Pending)
+        .await
+        .unwrap();
     tx.commit().await.unwrap();
 
     // Verify new state
@@ -230,8 +233,10 @@ where
     // Update to spent
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut proofs = tx.get_proofs(&ys).await.unwrap();
-    proofs.set_new_state(State::Spent).unwrap();
-    tx.update_proofs(&mut proofs).await.unwrap();
+    check_state_transition(proofs.state, State::Spent).unwrap();
+    tx.update_proofs_state(&mut proofs, State::Spent)
+        .await
+        .unwrap();
     tx.commit().await.unwrap();
 
     // Verify final state
@@ -240,6 +245,66 @@ where
     assert_eq!(states[1], Some(State::Spent));
 }
 
+/// Test that update_proofs_state updates the ProofsWithState.state field
+pub async fn update_proofs_state_updates_proofs_with_state<DB>(db: DB)
+where
+    DB: Database<Error> + KeysDatabase<Err = Error>,
+{
+    use cashu::State;
+
+    let keyset_id = setup_keyset(&db).await;
+    let quote_id = QuoteId::new_uuid();
+
+    let proofs = vec![Proof {
+        amount: Amount::from(100),
+        keyset_id,
+        secret: Secret::generate(),
+        c: SecretKey::generate().public_key(),
+        witness: None,
+        dleq: None,
+    }];
+
+    let ys: Vec<_> = proofs.iter().map(|p| p.y().unwrap()).collect();
+
+    // Add proofs and verify initial state
+    let mut tx = Database::begin_transaction(&db).await.unwrap();
+    let mut proofs = tx
+        .add_proofs(
+            proofs.clone(),
+            Some(quote_id),
+            &Operation::new_swap(Amount::ZERO, Amount::ZERO, Amount::ZERO),
+        )
+        .await
+        .unwrap();
+    assert_eq!(proofs.state, State::Unspent);
+
+    // Update to Pending and verify ProofsWithState.state is updated
+    tx.update_proofs_state(&mut proofs, State::Pending)
+        .await
+        .unwrap();
+    assert_eq!(
+        proofs.state,
+        State::Pending,
+        "ProofsWithState.state should be updated to Pending after update_proofs_state"
+    );
+    tx.commit().await.unwrap();
+
+    // Get proofs again and update to Spent
+    let mut tx = Database::begin_transaction(&db).await.unwrap();
+    let mut proofs = tx.get_proofs(&ys).await.unwrap();
+    assert_eq!(proofs.state, State::Pending);
+
+    tx.update_proofs_state(&mut proofs, State::Spent)
+        .await
+        .unwrap();
+    assert_eq!(
+        proofs.state,
+        State::Spent,
+        "ProofsWithState.state should be updated to Spent after update_proofs_state"
+    );
+    tx.commit().await.unwrap();
+}
+
 /// Test removing proofs
 pub async fn remove_proofs<DB>(db: DB)
 where
@@ -350,15 +415,19 @@ where
     // First update to Pending (valid state transition)
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut proofs = tx.get_proofs(&[ys[0], ys[1]]).await.unwrap();
-    proofs.set_new_state(State::Pending).unwrap();
-    tx.update_proofs(&mut proofs).await.unwrap();
+    check_state_transition(proofs.state, State::Pending).unwrap();
+    tx.update_proofs_state(&mut proofs, State::Pending)
+        .await
+        .unwrap();
     tx.commit().await.unwrap();
 
     // Then mark some as spent
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut proofs = tx.get_proofs(&[ys[0], ys[1]]).await.unwrap();
-    proofs.set_new_state(State::Spent).unwrap();
-    tx.update_proofs(&mut proofs).await.unwrap();
+    check_state_transition(proofs.state, State::Spent).unwrap();
+    tx.update_proofs_state(&mut proofs, State::Spent)
+        .await
+        .unwrap();
     tx.commit().await.unwrap();
 
     // Get total redeemed
@@ -709,8 +778,10 @@ where
     // Transition proofs to Pending state
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut records = tx.get_proofs(&ys).await.expect("valid records");
-    records.set_new_state(State::Pending).unwrap();
-    tx.update_proofs(&mut records).await.unwrap();
+    check_state_transition(records.state, State::Pending).unwrap();
+    tx.update_proofs_state(&mut records, State::Pending)
+        .await
+        .unwrap();
     tx.commit().await.unwrap();
 
     // Removing Pending proofs should also succeed
@@ -726,8 +797,10 @@ where
     // Now transition proofs to Spent state
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut records = tx.get_proofs(&ys).await.expect("valid records");
-    records.set_new_state(State::Spent).unwrap();
-    tx.update_proofs(&mut records).await.unwrap();
+    check_state_transition(records.state, State::Spent).unwrap();
+    tx.update_proofs_state(&mut records, State::Spent)
+        .await
+        .unwrap();
     tx.commit().await.unwrap();
 
     // Verify proofs are now in Spent state
@@ -824,8 +897,10 @@ where
     // Transition only the first two proofs to Pending state
     let mut tx = Database::begin_transaction(&db).await.unwrap();
     let mut first_two_proofs = tx.get_proofs(&ys[0..2]).await.unwrap();
-    first_two_proofs.set_new_state(State::Pending).unwrap();
-    tx.update_proofs(&mut first_two_proofs).await.unwrap();
+    check_state_transition(first_two_proofs.state, State::Pending).unwrap();
+    tx.update_proofs_state(&mut first_two_proofs, State::Pending)
+        .await
+        .unwrap();
     tx.commit().await.unwrap();
 
     // Verify the states are now inconsistent via get_proofs_states

+ 8 - 29
crates/cdk-common/src/mint.rs

@@ -18,7 +18,6 @@ use uuid::Uuid;
 
 use crate::nuts::{MeltQuoteState, MintQuoteState};
 use crate::payment::PaymentIdentifier;
-use crate::state::check_state_transition;
 use crate::{Amount, CurrencyUnit, Error, Id, KeySetInfo, PublicKey};
 
 /// Operation kind for saga persistence
@@ -48,9 +47,9 @@ pub enum OperationKind {
 ///
 /// # State Transitions
 ///
-/// State changes are performed atomically on the entire collection via [`set_new_state`](Self::set_new_state),
-/// which validates the transition before applying it. The database layer then persists
-/// the new state for all proofs in a single transaction.
+/// State transitions are validated using [`check_state_transition`](crate::state::check_state_transition)
+/// before updating. The database layer then persists the new state for all proofs in a single transaction
+/// via [`update_proofs_state`](crate::database::mint::ProofsTransaction::update_proofs_state).
 ///
 /// # Example
 ///
@@ -58,16 +57,17 @@ pub enum OperationKind {
 /// // Database layer ensures all proofs have the same state
 /// let mut proofs = tx.get_proofs(&ys).await?;
 ///
-/// // Transition all proofs to a new state
-/// let old_state = proofs.set_new_state(State::Spent)?;
+/// // Validate the state transition
+/// check_state_transition(proofs.state, State::Spent)?;
 ///
 /// // Persist the state change
-/// tx.update_proofs(&mut proofs).await?;
+/// tx.update_proofs_state(&mut proofs, State::Spent).await?;
 /// ```
 #[derive(Debug)]
 pub struct ProofsWithState {
     proofs: Proofs,
-    state: State,
+    /// The current state of the proofs
+    pub state: State,
 }
 
 impl Deref for ProofsWithState {
@@ -91,27 +91,6 @@ impl ProofsWithState {
             state: current_state,
         }
     }
-
-    /// Returns the current state shared by all proofs in the collection.
-    pub fn get_state(&self) -> State {
-        self.state
-    }
-
-    /// Transitions all proofs to a new state.
-    ///
-    /// Validates that the state transition is allowed before applying it.
-    /// Returns the previous state on success.
-    ///
-    /// # Errors
-    ///
-    /// Returns [`Error::UnexpectedProofState`] if the transition from the current
-    /// state to the new state is not permitted.
-    pub fn set_new_state(&mut self, new_state: State) -> Result<State, Error> {
-        check_state_transition(self.state, new_state).map_err(|_| Error::UnexpectedProofState)?;
-        let old_state = self.state;
-        self.state = new_state;
-        Ok(old_state)
-    }
 }
 
 impl fmt::Display for OperationKind {

+ 7 - 6
crates/cdk-sql-common/src/mint/proofs.rs

@@ -186,11 +186,10 @@ where
         Ok(ProofsWithState::new(proofs, State::Unspent).into())
     }
 
-    /// Persists the current state of the proofs to the database.
+    /// Updates all proofs to the given state in the database.
     ///
-    /// Reads the state from the [`ProofsWithState`] wrapper (previously set via
-    /// [`ProofsWithState::set_new_state`]) and updates all proofs in the database
-    /// to that state.
+    /// Also updates the `state` field on the [`ProofsWithState`] wrapper to reflect
+    /// the new state after the database update succeeds.
     ///
     /// When the new state is `Spent`, this method also updates the `keyset_amounts`
     /// table to track the total redeemed amount per keyset for analytics purposes.
@@ -199,12 +198,12 @@ where
     ///
     /// The proofs must have been previously acquired via `add_proofs`
     /// or `get_proofs` to ensure they are locked within the current transaction.
-    async fn update_proofs(
+    async fn update_proofs_state(
         &mut self,
         proofs: &mut Acquired<ProofsWithState>,
+        new_state: State,
     ) -> Result<(), Self::Err> {
         let ys = proofs.ys()?;
-        let new_state = proofs.get_state();
 
         query(r#"UPDATE proof SET state = :new_state WHERE y IN (:ys)"#)?
             .bind("new_state", new_state.to_string())
@@ -229,6 +228,8 @@ where
                 .await?;
         }
 
+        proofs.state = new_state;
+
         Ok(())
     }
 

+ 5 - 3
crates/cdk/src/mint/melt/melt_saga/mod.rs

@@ -7,6 +7,7 @@ use cdk_common::database::DynMintDatabase;
 use cdk_common::mint::{MeltSagaState, Operation, Saga, SagaStateEnum};
 use cdk_common::nut00::KnownMethod;
 use cdk_common::nuts::MeltQuoteState;
+use cdk_common::state::check_state_transition;
 use cdk_common::{Amount, Error, ProofsMethods, PublicKey, QuoteId, State};
 #[cfg(feature = "prometheus")]
 use cdk_prometheus::METRICS;
@@ -247,7 +248,7 @@ impl MeltSaga<Initial> {
 
         let mut proofs = tx.get_proofs(&input_ys).await?;
 
-        let original_state = proofs.get_state();
+        let original_state = proofs.state;
 
         if matches!(original_state, State::Pending | State::Spent) {
             tx.rollback().await?;
@@ -258,10 +259,11 @@ impl MeltSaga<Initial> {
             });
         }
 
-        proofs.set_new_state(State::Pending)?;
+        check_state_transition(proofs.state, State::Pending)
+            .map_err(|_| Error::UnexpectedProofState)?;
 
         // Update proof states to Pending
-        match tx.update_proofs(&mut proofs).await {
+        match tx.update_proofs_state(&mut proofs, State::Pending).await {
             Ok(states) => states,
             Err(cdk_common::database::Error::AttemptUpdateSpentProof)
             | Err(cdk_common::database::Error::AttemptRemoveSpentProof) => {

+ 3 - 2
crates/cdk/src/mint/melt/shared.rs

@@ -8,6 +8,7 @@
 
 use cdk_common::database::{self, Acquired, DynMintDatabase};
 use cdk_common::nuts::{BlindSignature, BlindedMessage, MeltQuoteState, State};
+use cdk_common::state::check_state_transition;
 use cdk_common::{Amount, Error, PublicKey, QuoteId};
 use cdk_signatory::signatory::SignatoryKeySet;
 
@@ -393,10 +394,10 @@ pub async fn finalize_melt_core(
     }
 
     let mut proofs = tx.get_proofs(input_ys).await?;
-    proofs.set_new_state(State::Spent)?;
+    check_state_transition(proofs.state, State::Spent).map_err(|_| Error::UnexpectedProofState)?;
 
     // Mark input proofs as spent
-    match tx.update_proofs(&mut proofs).await {
+    match tx.update_proofs_state(&mut proofs, State::Spent).await {
         Ok(_) => {}
         Err(database::Error::AttemptUpdateSpentProof) => {
             tracing::info!("Proofs for quote {} already marked as spent", quote.id);

+ 9 - 5
crates/cdk/src/mint/swap/swap_saga/mod.rs

@@ -4,6 +4,7 @@ use std::sync::Arc;
 use cdk_common::database::DynMintDatabase;
 use cdk_common::mint::{Operation, Saga, SwapSagaState};
 use cdk_common::nuts::BlindedMessage;
+use cdk_common::state::check_state_transition;
 use cdk_common::{database, Amount, Error, Proofs, ProofsMethods, PublicKey, QuoteId, State};
 use tokio::sync::Mutex;
 use tracing::instrument;
@@ -191,9 +192,9 @@ impl<'a> SwapSaga<'a, Initial> {
             }
         };
 
-        let original_state = new_proofs.get_state();
+        let original_state = new_proofs.state;
 
-        if new_proofs.set_new_state(State::Pending).is_err() {
+        if check_state_transition(new_proofs.state, State::Pending).is_err() {
             tx.rollback().await?;
             return Err(if original_state == State::Pending {
                 Error::TokenPending
@@ -208,7 +209,10 @@ impl<'a> SwapSaga<'a, Initial> {
         };
 
         // Update input proof states to Pending
-        match tx.update_proofs(&mut new_proofs).await {
+        match tx
+            .update_proofs_state(&mut new_proofs, State::Pending)
+            .await
+        {
             Ok(states) => states,
             Err(err) => {
                 tx.rollback().await?;
@@ -420,13 +424,13 @@ impl SwapSaga<'_, Signed> {
                 return Err(err.into());
             }
         };
-        if proofs.set_new_state(State::Spent).is_err() {
+        if check_state_transition(proofs.state, State::Spent).is_err() {
             tx.rollback().await?;
             self.compensate_all().await?;
             return Err(Error::TokenAlreadySpent);
         }
 
-        match tx.update_proofs(&mut proofs).await {
+        match tx.update_proofs_state(&mut proofs, State::Spent).await {
             Ok(_) => {}
             Err(err) => {
                 tx.rollback().await?;