|
|
@@ -4,11 +4,12 @@ use std::collections::HashMap;
|
|
|
use std::str::FromStr;
|
|
|
|
|
|
use async_trait::async_trait;
|
|
|
-use cdk_common::database::{self, Error, MintProofsDatabase};
|
|
|
-use cdk_common::mint::Operation;
|
|
|
+use cdk_common::database::{self, Acquired, Error, MintProofsDatabase};
|
|
|
+use cdk_common::mint::{Operation, ProofsWithState};
|
|
|
use cdk_common::nut00::ProofsMethods;
|
|
|
use cdk_common::quote_id::QuoteId;
|
|
|
use cdk_common::secret::Secret;
|
|
|
+use cdk_common::util::unix_time;
|
|
|
use cdk_common::{Amount, Id, Proof, Proofs, PublicKey, State};
|
|
|
|
|
|
use super::{SQLMintDatabase, SQLTransaction};
|
|
|
@@ -69,9 +70,7 @@ pub(super) fn sql_row_to_proof(row: Vec<Column>) -> Result<Proof, Error> {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-pub(super) fn sql_row_to_proof_with_state(
|
|
|
- row: Vec<Column>,
|
|
|
-) -> Result<(Proof, Option<State>), Error> {
|
|
|
+pub(super) fn sql_row_to_proof_with_state(row: Vec<Column>) -> Result<(Proof, State), Error> {
|
|
|
unpack_into!(
|
|
|
let (
|
|
|
keyset_id, amount, secret, c, witness, state
|
|
|
@@ -79,7 +78,9 @@ pub(super) fn sql_row_to_proof_with_state(
|
|
|
);
|
|
|
|
|
|
let amount: u64 = column_as_number!(amount);
|
|
|
- let state = column_as_nullable_string!(state).and_then(|s| State::from_str(&s).ok());
|
|
|
+ let state = column_as_nullable_string!(state)
|
|
|
+ .and_then(|s| State::from_str(&s).ok())
|
|
|
+ .unwrap_or(State::Pending);
|
|
|
|
|
|
Ok((
|
|
|
Proof {
|
|
|
@@ -116,13 +117,22 @@ where
|
|
|
{
|
|
|
type Err = Error;
|
|
|
|
|
|
+ /// Adds proofs to the database with initial state `Unspent`.
|
|
|
+ ///
|
|
|
+ /// This method first checks if any of the proofs already exist in the database.
|
|
|
+ /// If a proof exists and is spent, returns [`Error::AttemptUpdateSpentProof`].
|
|
|
+ /// If a proof exists in any other state, returns [`Error::Duplicate`].
|
|
|
+ ///
|
|
|
+ /// On success, returns the proofs wrapped in [`Acquired<ProofsWithState>`] with
|
|
|
+ /// state set to `Unspent`, indicating the rows are locked for the duration of
|
|
|
+ /// the transaction.
|
|
|
async fn add_proofs(
|
|
|
&mut self,
|
|
|
proofs: Proofs,
|
|
|
quote_id: Option<QuoteId>,
|
|
|
operation: &Operation,
|
|
|
- ) -> Result<(), Self::Err> {
|
|
|
- let current_time = cdk_common::util::unix_time();
|
|
|
+ ) -> Result<Acquired<ProofsWithState>, Self::Err> {
|
|
|
+ let current_time = unix_time();
|
|
|
|
|
|
// Check any previous proof, this query should return None in order to proceed storing
|
|
|
// Any result here would error
|
|
|
@@ -144,7 +154,7 @@ where
|
|
|
None => Ok(()), // no previous record
|
|
|
}?;
|
|
|
|
|
|
- for proof in proofs {
|
|
|
+ for proof in &proofs {
|
|
|
let y = proof.y()?;
|
|
|
|
|
|
query(
|
|
|
@@ -162,7 +172,7 @@ where
|
|
|
.bind("c", proof.c.to_bytes().to_vec())
|
|
|
.bind(
|
|
|
"witness",
|
|
|
- proof.witness.and_then(|w| serde_json::to_string(&w).inspect_err(|e| tracing::error!("Failed to serialize witness: {:?}", e)).ok()),
|
|
|
+ proof.witness.clone().and_then(|w| serde_json::to_string(&w).inspect_err(|e| tracing::error!("Failed to serialize witness: {:?}", e)).ok()),
|
|
|
)
|
|
|
.bind("state", "UNSPENT".to_string())
|
|
|
.bind("quote_id", quote_id.clone().map(|q| q.to_string()))
|
|
|
@@ -173,24 +183,28 @@ where
|
|
|
.await?;
|
|
|
}
|
|
|
|
|
|
- Ok(())
|
|
|
+ Ok(ProofsWithState::new(proofs, State::Unspent).into())
|
|
|
}
|
|
|
|
|
|
- async fn update_proofs_states(
|
|
|
+ /// Persists the current state of the proofs to 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.
|
|
|
+ ///
|
|
|
+ /// 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.
|
|
|
+ ///
|
|
|
+ /// # Prerequisites
|
|
|
+ ///
|
|
|
+ /// 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(
|
|
|
&mut self,
|
|
|
- ys: &[PublicKey],
|
|
|
- new_state: State,
|
|
|
- ) -> Result<Vec<Option<State>>, Self::Err> {
|
|
|
- let mut current_states = get_current_states(&self.inner, ys, true).await?;
|
|
|
-
|
|
|
- if current_states.len() != ys.len() {
|
|
|
- tracing::warn!(
|
|
|
- "Attempted to update state of non-existent proof {} {}",
|
|
|
- current_states.len(),
|
|
|
- ys.len()
|
|
|
- );
|
|
|
- return Err(database::Error::ProofNotFound);
|
|
|
- }
|
|
|
+ proofs: &mut Acquired<ProofsWithState>,
|
|
|
+ ) -> 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())
|
|
|
@@ -200,22 +214,22 @@ where
|
|
|
|
|
|
if new_state == State::Spent {
|
|
|
query(
|
|
|
- r#"
|
|
|
- INSERT INTO keyset_amounts (keyset_id, total_issued, total_redeemed)
|
|
|
- SELECT keyset_id, 0, COALESCE(SUM(amount), 0)
|
|
|
- FROM proof
|
|
|
- WHERE y IN (:ys)
|
|
|
- GROUP BY keyset_id
|
|
|
- ON CONFLICT (keyset_id)
|
|
|
- DO UPDATE SET total_redeemed = keyset_amounts.total_redeemed + EXCLUDED.total_redeemed
|
|
|
- "#,
|
|
|
- )?
|
|
|
- .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())
|
|
|
- .execute(&self.inner)
|
|
|
- .await?;
|
|
|
+ r#"
|
|
|
+ INSERT INTO keyset_amounts (keyset_id, total_issued, total_redeemed)
|
|
|
+ SELECT keyset_id, 0, COALESCE(SUM(amount), 0)
|
|
|
+ FROM proof
|
|
|
+ WHERE y IN (:ys)
|
|
|
+ GROUP BY keyset_id
|
|
|
+ ON CONFLICT (keyset_id)
|
|
|
+ DO UPDATE SET total_redeemed = keyset_amounts.total_redeemed + EXCLUDED.total_redeemed
|
|
|
+ "#,
|
|
|
+ )?
|
|
|
+ .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())
|
|
|
+ .execute(&self.inner)
|
|
|
+ .await?;
|
|
|
}
|
|
|
|
|
|
- Ok(ys.iter().map(|y| current_states.remove(y)).collect())
|
|
|
+ Ok(())
|
|
|
}
|
|
|
|
|
|
async fn remove_proofs(
|
|
|
@@ -332,13 +346,62 @@ where
|
|
|
.collect::<Result<Vec<_>, _>>()?)
|
|
|
}
|
|
|
|
|
|
- async fn get_proofs_states(
|
|
|
+ async fn get_proofs(
|
|
|
&mut self,
|
|
|
ys: &[PublicKey],
|
|
|
- ) -> Result<Vec<Option<State>>, Self::Err> {
|
|
|
- let mut current_states = get_current_states(&self.inner, ys, true).await?;
|
|
|
+ ) -> Result<Acquired<ProofsWithState>, Self::Err> {
|
|
|
+ if ys.is_empty() {
|
|
|
+ return Ok(ProofsWithState::new(vec![], State::Unspent).into());
|
|
|
+ }
|
|
|
|
|
|
- Ok(ys.iter().map(|y| current_states.remove(y)).collect())
|
|
|
+ let rows = query(
|
|
|
+ r#"
|
|
|
+ SELECT
|
|
|
+ keyset_id,
|
|
|
+ amount,
|
|
|
+ secret,
|
|
|
+ c,
|
|
|
+ witness,
|
|
|
+ state
|
|
|
+ FROM
|
|
|
+ proof
|
|
|
+ WHERE
|
|
|
+ y IN (:ys)
|
|
|
+ FOR UPDATE
|
|
|
+ "#,
|
|
|
+ )?
|
|
|
+ .bind_vec("ys", ys.iter().map(|y| y.to_bytes().to_vec()).collect())
|
|
|
+ .fetch_all(&self.inner)
|
|
|
+ .await?;
|
|
|
+
|
|
|
+ if rows.is_empty() {
|
|
|
+ return Err(database::Error::ProofNotFound);
|
|
|
+ }
|
|
|
+
|
|
|
+ let results: Vec<(Proof, State)> = rows
|
|
|
+ .into_iter()
|
|
|
+ .map(sql_row_to_proof_with_state)
|
|
|
+ .collect::<Result<Vec<_>, _>>()?;
|
|
|
+
|
|
|
+ let mut proofs = Vec::with_capacity(results.len());
|
|
|
+ let mut first_state: Option<State> = None;
|
|
|
+
|
|
|
+ for (proof, state) in results {
|
|
|
+ if let Some(first) = first_state {
|
|
|
+ if first != state {
|
|
|
+ return Err(database::Error::Internal(
|
|
|
+ "Proofs have inconsistent states".to_string(),
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ first_state = Some(state);
|
|
|
+ }
|
|
|
+
|
|
|
+ proofs.push(proof);
|
|
|
+ }
|
|
|
+
|
|
|
+ let state = first_state.unwrap_or(State::Unspent);
|
|
|
+ Ok(ProofsWithState::new(proofs, state).into())
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -425,7 +488,8 @@ where
|
|
|
keyset_id: &Id,
|
|
|
) -> Result<(Proofs, Vec<Option<State>>), Self::Err> {
|
|
|
let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
|
|
|
- Ok(query(
|
|
|
+
|
|
|
+ let (proofs, states): (Vec<Proof>, Vec<State>) = query(
|
|
|
r#"
|
|
|
SELECT
|
|
|
keyset_id,
|
|
|
@@ -447,7 +511,9 @@ where
|
|
|
.map(sql_row_to_proof_with_state)
|
|
|
.collect::<Result<Vec<_>, _>>()?
|
|
|
.into_iter()
|
|
|
- .unzip())
|
|
|
+ .unzip();
|
|
|
+
|
|
|
+ Ok((proofs, states.into_iter().map(Some).collect()))
|
|
|
}
|
|
|
|
|
|
/// Get total proofs redeemed by keyset id
|