Просмотр исходного кода

feat(NUT05): update with quote state

feat(NUT04): update with quote state

feat: db migrations for mint state

chore: remove logging
thesimplekid 8 месяцев назад
Родитель
Сommit
7223c5bda8
37 измененных файлов с 1050 добавлено и 199 удалено
  1. 2 2
      bindings/cdk-js/src/nuts/nut04.rs
  2. 2 20
      bindings/cdk-js/src/nuts/nut05.rs
  3. 2 2
      bindings/cdk-js/src/types/melt_quote.rs
  4. 2 2
      bindings/cdk-js/src/types/melted.rs
  5. 2 2
      bindings/cdk-js/src/types/mint_quote.rs
  6. 1 1
      bindings/cdk-js/src/wallet.rs
  7. 1 1
      crates/cdk-cli/src/main.rs
  8. 3 1
      crates/cdk-cli/src/sub_commands/melt.rs
  9. 3 3
      crates/cdk-cli/src/sub_commands/mint.rs
  10. 0 2
      crates/cdk-cli/src/sub_commands/send.rs
  11. 3 0
      crates/cdk-redb/src/error.rs
  12. 1 0
      crates/cdk-redb/src/lib.rs
  13. 191 0
      crates/cdk-redb/src/migrations.rs
  14. 159 27
      crates/cdk-redb/src/mint/mod.rs
  15. 72 26
      crates/cdk-redb/src/wallet/mod.rs
  16. 8 2
      crates/cdk-sqlite/src/mint/error.rs
  17. 5 0
      crates/cdk-sqlite/src/mint/migrations/20240618195700_quote_state.sql
  18. 3 0
      crates/cdk-sqlite/src/mint/migrations/20240626092101_nut04_state.sql
  19. 92 10
      crates/cdk-sqlite/src/mint/mod.rs
  20. 6 0
      crates/cdk-sqlite/src/wallet/error.rs
  21. 5 0
      crates/cdk-sqlite/src/wallet/migrations/20240618200350_quote_state.sql
  22. 3 0
      crates/cdk-sqlite/src/wallet/migrations/20240626091921_nut04_state.sql
  23. 15 12
      crates/cdk-sqlite/src/wallet/mod.rs
  24. 4 4
      crates/cdk/examples/mint-token.rs
  25. 4 4
      crates/cdk/examples/p2pk.rs
  26. 45 1
      crates/cdk/src/cdk_database/mint_memory.rs
  27. 13 1
      crates/cdk/src/cdk_database/mod.rs
  28. 6 0
      crates/cdk/src/mint/error.rs
  29. 56 21
      crates/cdk/src/mint/mod.rs
  30. 3 3
      crates/cdk/src/nuts/mod.rs
  31. 124 4
      crates/cdk/src/nuts/nut04.rs
  32. 158 4
      crates/cdk/src/nuts/nut05.rs
  33. 4 4
      crates/cdk/src/nuts/nut07.rs
  34. 2 2
      crates/cdk/src/nuts/nut08.rs
  35. 12 15
      crates/cdk/src/types.rs
  36. 21 9
      crates/cdk/src/wallet/client.rs
  37. 17 14
      crates/cdk/src/wallet/mod.rs

+ 2 - 2
bindings/cdk-js/src/nuts/nut04.rs

@@ -46,8 +46,8 @@ impl From<MintQuoteBolt11Response> for JsMintQuoteBolt11Response {
 #[wasm_bindgen(js_class = MintQuoteBolt11Response)]
 impl JsMintQuoteBolt11Response {
     #[wasm_bindgen(getter)]
-    pub fn paid(&self) -> bool {
-        self.inner.paid
+    pub fn state(&self) -> String {
+        self.inner.state.to_string()
     }
 
     #[wasm_bindgen(getter)]

+ 2 - 20
bindings/cdk-js/src/nuts/nut05.rs

@@ -1,8 +1,8 @@
 use std::ops::Deref;
 
 use cdk::nuts::{
-    MeltBolt11Request, MeltBolt11Response, MeltMethodSettings, MeltQuoteBolt11Request,
-    MeltQuoteBolt11Response, NUT05Settings,
+    MeltBolt11Request, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
+    NUT05Settings,
 };
 use wasm_bindgen::prelude::*;
 
@@ -60,24 +60,6 @@ impl From<MeltBolt11Request> for JsMeltBolt11Request {
     }
 }
 
-#[wasm_bindgen(js_name = PostMeltResponse)]
-pub struct JsMeltBolt11Response {
-    inner: MeltBolt11Response,
-}
-
-impl Deref for JsMeltBolt11Response {
-    type Target = MeltBolt11Response;
-    fn deref(&self) -> &Self::Target {
-        &self.inner
-    }
-}
-
-impl From<MeltBolt11Response> for JsMeltBolt11Response {
-    fn from(inner: MeltBolt11Response) -> JsMeltBolt11Response {
-        JsMeltBolt11Response { inner }
-    }
-}
-
 #[wasm_bindgen(js_name = MeltMethodSettings)]
 pub struct JsMeltMethodSettings {
     inner: MeltMethodSettings,

+ 2 - 2
bindings/cdk-js/src/types/melt_quote.rs

@@ -52,8 +52,8 @@ impl JsMeltQuote {
     }
 
     #[wasm_bindgen(getter)]
-    pub fn paid(&self) -> bool {
-        self.inner.paid
+    pub fn state(&self) -> String {
+        self.inner.state.to_string()
     }
 
     #[wasm_bindgen(getter)]

+ 2 - 2
bindings/cdk-js/src/types/melted.rs

@@ -26,8 +26,8 @@ impl From<Melted> for JsMelted {
 #[wasm_bindgen(js_class = Melted)]
 impl JsMelted {
     #[wasm_bindgen(getter)]
-    pub fn paid(&self) -> bool {
-        self.inner.paid
+    pub fn paid(&self) -> String {
+        self.inner.state.to_string()
     }
 
     #[wasm_bindgen(getter)]

+ 2 - 2
bindings/cdk-js/src/types/mint_quote.rs

@@ -47,8 +47,8 @@ impl JsMintQuote {
     }
 
     #[wasm_bindgen(getter)]
-    pub fn paid(&self) -> bool {
-        self.inner.paid
+    pub fn state(&self) -> String {
+        self.inner.state.to_string()
     }
 
     #[wasm_bindgen(getter)]

+ 1 - 1
bindings/cdk-js/src/wallet.rs

@@ -102,7 +102,7 @@ impl JsWallet {
     pub async fn mint_quote_status(&self, quote_id: String) -> Result<JsMintQuoteBolt11Response> {
         let quote = self
             .inner
-            .mint_quote_status(&quote_id)
+            .mint_quote_state(&quote_id)
             .await
             .map_err(into_err)?;
 

+ 1 - 1
crates/cdk-cli/src/main.rs

@@ -66,7 +66,7 @@ enum Commands {
 #[tokio::main]
 async fn main() -> Result<()> {
     tracing_subscriber::fmt()
-        .with_max_level(tracing::Level::WARN)
+        .with_max_level(tracing::Level::INFO)
         .init();
 
     // Parse input

+ 3 - 1
crates/cdk-cli/src/sub_commands/melt.rs

@@ -52,12 +52,14 @@ pub async fn melt(
     }
     let quote = wallet.melt_quote(bolt11.to_string(), None).await?;
 
+    println!("{:?}", quote);
+
     let melt = wallet
         .melt(&quote.id, SplitTarget::default())
         .await
         .unwrap();
 
-    println!("Paid invoice: {}", melt.paid);
+    println!("Paid invoice: {}", melt.state);
     if let Some(preimage) = melt.preimage {
         println!("Payment preimage: {}", preimage);
     }

+ 3 - 3
crates/cdk-cli/src/sub_commands/mint.rs

@@ -5,7 +5,7 @@ use std::time::Duration;
 use anyhow::Result;
 use cdk::amount::SplitTarget;
 use cdk::cdk_database::{Error, WalletDatabase};
-use cdk::nuts::CurrencyUnit;
+use cdk::nuts::{CurrencyUnit, MintQuoteState};
 use cdk::url::UncheckedUrl;
 use cdk::wallet::Wallet;
 use cdk::Amount;
@@ -43,9 +43,9 @@ pub async fn mint(
     println!("Please pay: {}", quote.request);
 
     loop {
-        let status = wallet.mint_quote_status(&quote.id).await?;
+        let status = wallet.mint_quote_state(&quote.id).await?;
 
-        if status.paid {
+        if status.state == MintQuoteState::Paid {
             break;
         }
 

+ 0 - 2
crates/cdk-cli/src/sub_commands/send.rs

@@ -133,8 +133,6 @@ pub async fn send(
                 )
                 .unwrap();
 
-                tracing::debug!("{}", data_pubkey.to_string());
-
                 Some(SpendingConditions::P2PKConditions {
                     data: data_pubkey,
                     conditions: Some(conditions),

+ 3 - 0
crates/cdk-redb/src/error.rs

@@ -46,6 +46,9 @@ pub enum Error {
     /// Unknown Proof Y
     #[error("Unknown Proof Y")]
     UnknownY,
+    /// Unknown Database Version
+    #[error("Unknown Database Version")]
+    UnknownDatabaseVersion,
 }
 
 impl From<Error> for cdk::cdk_database::Error {

+ 1 - 0
crates/cdk-redb/src/lib.rs

@@ -1,4 +1,5 @@
 pub mod error;
+mod migrations;
 
 #[cfg(feature = "mint")]
 pub mod mint;

+ 191 - 0
crates/cdk-redb/src/migrations.rs

@@ -0,0 +1,191 @@
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use cdk::nuts::{CurrencyUnit, MeltQuoteState, MintQuoteState};
+use cdk::types::{MeltQuote, MintQuote};
+use cdk::{Amount, UncheckedUrl};
+use redb::{Database, ReadableTable, TableDefinition};
+use serde::{Deserialize, Serialize};
+
+use super::error::Error;
+
+const MINT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_quotes");
+const MELT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("melt_quotes");
+
+/// Mint Quote Info
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct V0MintQuote {
+    pub id: String,
+    pub mint_url: UncheckedUrl,
+    pub amount: Amount,
+    pub unit: CurrencyUnit,
+    pub request: String,
+    pub paid: bool,
+    pub expiry: u64,
+}
+
+impl From<V0MintQuote> for MintQuote {
+    fn from(quote: V0MintQuote) -> MintQuote {
+        let state = match quote.paid {
+            true => MintQuoteState::Paid,
+            false => MintQuoteState::Unpaid,
+        };
+        MintQuote {
+            id: quote.id,
+            mint_url: quote.mint_url,
+            amount: quote.amount,
+            unit: quote.unit,
+            request: quote.request,
+            state,
+            expiry: quote.expiry,
+        }
+    }
+}
+
+/// Melt Quote Info
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct V0MeltQuote {
+    pub id: String,
+    pub unit: CurrencyUnit,
+    pub amount: Amount,
+    pub request: String,
+    pub fee_reserve: Amount,
+    pub paid: bool,
+    pub expiry: u64,
+}
+
+impl From<V0MeltQuote> for MeltQuote {
+    fn from(quote: V0MeltQuote) -> MeltQuote {
+        let state = match quote.paid {
+            true => MeltQuoteState::Paid,
+            false => MeltQuoteState::Unpaid,
+        };
+        MeltQuote {
+            id: quote.id,
+            amount: quote.amount,
+            unit: quote.unit,
+            request: quote.request,
+            state,
+            expiry: quote.expiry,
+            fee_reserve: quote.fee_reserve,
+            payment_preimage: None,
+        }
+    }
+}
+
+fn migrate_mint_quotes_00_to_01(db: Arc<Database>) -> Result<(), Error> {
+    let read_txn = db.begin_read().map_err(Error::from)?;
+    let table = read_txn
+        .open_table(MINT_QUOTES_TABLE)
+        .map_err(Error::from)?;
+
+    let mint_quotes: HashMap<String, Option<V0MintQuote>>;
+    {
+        mint_quotes = table
+            .iter()
+            .map_err(Error::from)?
+            .flatten()
+            .map(|(quote_id, mint_quote)| {
+                (
+                    quote_id.value().to_string(),
+                    serde_json::from_str(mint_quote.value()).ok(),
+                )
+            })
+            .collect();
+    }
+
+    let migrated_mint_quotes: HashMap<String, Option<MintQuote>> = mint_quotes
+        .into_iter()
+        .map(|(quote_id, quote)| (quote_id, quote.map(|q| q.into())))
+        .collect();
+
+    {
+        let write_txn = db.begin_write()?;
+
+        {
+            let mut table = write_txn
+                .open_table(MINT_QUOTES_TABLE)
+                .map_err(Error::from)?;
+            for (quote_id, quote) in migrated_mint_quotes {
+                match quote {
+                    Some(quote) => {
+                        let quote_str = serde_json::to_string(&quote)?;
+
+                        table.insert(quote_id.as_str(), quote_str.as_str())?;
+                    }
+                    None => {
+                        table.remove(quote_id.as_str())?;
+                    }
+                }
+            }
+        }
+
+        write_txn.commit()?;
+    }
+
+    Ok(())
+}
+
+fn migrate_melt_quotes_00_to_01(db: Arc<Database>) -> Result<(), Error> {
+    let read_txn = db.begin_read().map_err(Error::from)?;
+    let table = read_txn
+        .open_table(MELT_QUOTES_TABLE)
+        .map_err(Error::from)?;
+
+    let melt_quotes: HashMap<String, Option<V0MeltQuote>>;
+    {
+        melt_quotes = table
+            .iter()
+            .map_err(Error::from)?
+            .flatten()
+            .map(|(quote_id, melt_quote)| {
+                (
+                    quote_id.value().to_string(),
+                    serde_json::from_str(melt_quote.value()).ok(),
+                )
+            })
+            .collect();
+    }
+
+    let migrated_melt_quotes: HashMap<String, Option<MeltQuote>> = melt_quotes
+        .into_iter()
+        .map(|(quote_id, quote)| (quote_id, quote.map(|q| q.into())))
+        .collect();
+
+    {
+        let write_txn = db.begin_write()?;
+
+        {
+            let mut table = write_txn
+                .open_table(MELT_QUOTES_TABLE)
+                .map_err(Error::from)?;
+            for (quote_id, quote) in migrated_melt_quotes {
+                match quote {
+                    Some(quote) => {
+                        let quote_str = serde_json::to_string(&quote)?;
+
+                        table.insert(quote_id.as_str(), quote_str.as_str())?;
+                    }
+                    None => {
+                        table.remove(quote_id.as_str())?;
+                    }
+                }
+            }
+        }
+
+        write_txn.commit()?;
+    }
+
+    Ok(())
+}
+
+pub(crate) fn migrate_00_to_01(db: Arc<Database>) -> Result<u32, Error> {
+    tracing::info!("Starting Migrations of mint quotes from 00 to 01");
+    migrate_mint_quotes_00_to_01(Arc::clone(&db))?;
+    tracing::info!("Finished Migrations of mint quotes from 00 to 01");
+
+    tracing::info!("Starting Migrations of melt quotes from 00 to 01");
+    migrate_melt_quotes_00_to_01(Arc::clone(&db))?;
+    tracing::info!("Finished Migrations of melt quotes from 00 to 01");
+    Ok(1)
+}

+ 159 - 27
crates/cdk-redb/src/mint.rs → crates/cdk-redb/src/mint/mod.rs

@@ -1,3 +1,4 @@
+use std::cmp::Ordering;
 use std::collections::HashMap;
 use std::path::Path;
 use std::str::FromStr;
@@ -8,14 +9,16 @@ use cdk::cdk_database;
 use cdk::cdk_database::MintDatabase;
 use cdk::dhke::hash_to_curve;
 use cdk::mint::MintKeySetInfo;
-use cdk::nuts::{BlindSignature, CurrencyUnit, Id, Proof, PublicKey};
+use cdk::nuts::{
+    BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, PublicKey,
+};
 use cdk::secret::Secret;
 use cdk::types::{MeltQuote, MintQuote};
 use redb::{Database, ReadableTable, TableDefinition};
 use tokio::sync::Mutex;
-use tracing::debug;
 
 use super::error::Error;
+use crate::migrations::migrate_00_to_01;
 
 const ACTIVE_KEYSETS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("active_keysets");
 const KEYSETS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("keysets");
@@ -29,7 +32,7 @@ const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config")
 const BLINDED_SIGNATURES: TableDefinition<[u8; 33], &str> =
     TableDefinition::new("blinded_signatures");
 
-const DATABASE_VERSION: u64 = 0;
+const DATABASE_VERSION: u32 = 0;
 
 #[derive(Debug, Clone)]
 pub struct MintRedbDatabase {
@@ -38,41 +41,78 @@ pub struct MintRedbDatabase {
 
 impl MintRedbDatabase {
     pub fn new(path: &Path) -> Result<Self, Error> {
-        let db = Database::create(path)?;
-
-        let write_txn = db.begin_write()?;
-        // Check database version
         {
-            let _ = write_txn.open_table(CONFIG_TABLE)?;
-            let mut table = write_txn.open_table(CONFIG_TABLE)?;
+            // Check database version
 
-            let db_version = table.get("db_version")?;
-            let db_version = db_version.map(|v| v.value().to_owned());
+            let db = Arc::new(Database::create(path)?);
 
+            // Check database version
+            let read_txn = db.begin_read()?;
+            let table = read_txn.open_table(CONFIG_TABLE);
+
+            let db_version = match table {
+                Ok(table) => table.get("db_version")?.map(|v| v.value().to_owned()),
+                Err(_) => None,
+            };
             match db_version {
                 Some(db_version) => {
-                    let current_file_version = u64::from_str(&db_version)?;
-                    if current_file_version.ne(&DATABASE_VERSION) {
-                        // Database needs to be upgraded
-                        todo!()
+                    let mut current_file_version = u32::from_str(&db_version)?;
+                    match current_file_version.cmp(&DATABASE_VERSION) {
+                        Ordering::Less => {
+                            tracing::info!(
+                                "Database needs to be upgraded at {} current is {}",
+                                current_file_version,
+                                DATABASE_VERSION
+                            );
+                            if current_file_version == 0 {
+                                current_file_version = migrate_00_to_01(Arc::clone(&db))?;
+                            }
+
+                            if current_file_version != DATABASE_VERSION {
+                                tracing::warn!(
+                                    "Database upgrade did not complete at {} current is {}",
+                                    current_file_version,
+                                    DATABASE_VERSION
+                                );
+                                return Err(Error::UnknownDatabaseVersion);
+                            }
+                        }
+                        Ordering::Equal => {
+                            tracing::info!("Database is at current version {}", DATABASE_VERSION);
+                        }
+                        Ordering::Greater => {
+                            tracing::warn!(
+                                "Database upgrade did not complete at {} current is {}",
+                                current_file_version,
+                                DATABASE_VERSION
+                            );
+                            return Err(Error::UnknownDatabaseVersion);
+                        }
                     }
                 }
                 None => {
-                    // Open all tables to init a new db
-                    let _ = write_txn.open_table(ACTIVE_KEYSETS_TABLE)?;
-                    let _ = write_txn.open_table(KEYSETS_TABLE)?;
-                    let _ = write_txn.open_table(MINT_QUOTES_TABLE)?;
-                    let _ = write_txn.open_table(MELT_QUOTES_TABLE)?;
-                    let _ = write_txn.open_table(PENDING_PROOFS_TABLE)?;
-                    let _ = write_txn.open_table(SPENT_PROOFS_TABLE)?;
-                    let _ = write_txn.open_table(BLINDED_SIGNATURES)?;
-
-                    table.insert("db_version", "0")?;
+                    let write_txn = db.begin_write()?;
+                    {
+                        let mut table = write_txn.open_table(CONFIG_TABLE)?;
+                        // Open all tables to init a new db
+                        let _ = write_txn.open_table(ACTIVE_KEYSETS_TABLE)?;
+                        let _ = write_txn.open_table(KEYSETS_TABLE)?;
+                        let _ = write_txn.open_table(MINT_QUOTES_TABLE)?;
+                        let _ = write_txn.open_table(MELT_QUOTES_TABLE)?;
+                        let _ = write_txn.open_table(PENDING_PROOFS_TABLE)?;
+                        let _ = write_txn.open_table(SPENT_PROOFS_TABLE)?;
+                        let _ = write_txn.open_table(BLINDED_SIGNATURES)?;
+
+                        table.insert("db_version", DATABASE_VERSION.to_string().as_str())?;
+                    }
+
+                    write_txn.commit()?;
                 }
             }
+            drop(db);
         }
 
-        write_txn.commit()?;
+        let db = Database::create(path)?;
         Ok(Self {
             db: Arc::new(Mutex::new(db)),
         })
@@ -219,6 +259,53 @@ impl MintDatabase for MintRedbDatabase {
         }
     }
 
+    async fn update_mint_quote_state(
+        &self,
+        quote_id: &str,
+        state: MintQuoteState,
+    ) -> Result<MintQuoteState, Self::Err> {
+        let db = self.db.lock().await;
+
+        let mut mint_quote: MintQuote;
+        {
+            let read_txn = db.begin_read().map_err(Error::from)?;
+            let table = read_txn
+                .open_table(MINT_QUOTES_TABLE)
+                .map_err(Error::from)?;
+
+            let quote_guard = table
+                .get(quote_id)
+                .map_err(Error::from)?
+                .ok_or(Error::UnknownMintInfo)?;
+
+            let quote = quote_guard.value();
+
+            mint_quote = serde_json::from_str(quote).map_err(Error::from)?;
+        }
+
+        let current_state = mint_quote.state;
+        mint_quote.state = state;
+
+        let write_txn = db.begin_write().map_err(Error::from)?;
+        {
+            let mut table = write_txn
+                .open_table(MINT_QUOTES_TABLE)
+                .map_err(Error::from)?;
+
+            table
+                .insert(
+                    quote_id,
+                    serde_json::to_string(&mint_quote)
+                        .map_err(Error::from)?
+                        .as_str(),
+                )
+                .map_err(Error::from)?;
+        }
+        write_txn.commit().map_err(Error::from)?;
+
+        Ok(current_state)
+    }
+
     async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
         let db = self.db.lock().await;
         let read_txn = db.begin_read().map_err(Error::from)?;
@@ -286,6 +373,52 @@ impl MintDatabase for MintRedbDatabase {
         Ok(quote.map(|q| serde_json::from_str(q.value()).unwrap()))
     }
 
+    async fn update_melt_quote_state(
+        &self,
+        quote_id: &str,
+        state: MeltQuoteState,
+    ) -> Result<MeltQuoteState, Self::Err> {
+        let db = self.db.lock().await;
+        let mut melt_quote: MeltQuote;
+        {
+            let read_txn = db.begin_read().map_err(Error::from)?;
+            let table = read_txn
+                .open_table(MELT_QUOTES_TABLE)
+                .map_err(Error::from)?;
+
+            let quote_guard = table
+                .get(quote_id)
+                .map_err(Error::from)?
+                .ok_or(Error::UnknownMintInfo)?;
+
+            let quote = quote_guard.value();
+
+            melt_quote = serde_json::from_str(quote).map_err(Error::from)?;
+        }
+
+        let current_state = melt_quote.state;
+        melt_quote.state = state;
+
+        let write_txn = db.begin_write().map_err(Error::from)?;
+        {
+            let mut table = write_txn
+                .open_table(MELT_QUOTES_TABLE)
+                .map_err(Error::from)?;
+
+            table
+                .insert(
+                    quote_id,
+                    serde_json::to_string(&melt_quote)
+                        .map_err(Error::from)?
+                        .as_str(),
+                )
+                .map_err(Error::from)?;
+        }
+        write_txn.commit().map_err(Error::from)?;
+
+        Ok(current_state)
+    }
+
     async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, Self::Err> {
         let db = self.db.lock().await;
         let read_txn = db.begin_read().map_err(Error::from)?;
@@ -338,7 +471,6 @@ impl MintDatabase for MintRedbDatabase {
                 .map_err(Error::from)?;
         }
         write_txn.commit().map_err(Error::from)?;
-        debug!("Added spend secret: {}", proof.secret.to_string());
 
         Ok(())
     }

+ 72 - 26
crates/cdk-redb/src/wallet.rs → crates/cdk-redb/src/wallet/mod.rs

@@ -1,3 +1,6 @@
+//! Redb Wallet
+
+use std::cmp::Ordering;
 use std::collections::HashMap;
 use std::path::Path;
 use std::str::FromStr;
@@ -17,6 +20,7 @@ use tokio::sync::Mutex;
 use tracing::instrument;
 
 use super::error::Error;
+use crate::migrations::migrate_00_to_01;
 
 const MINTS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mints_table");
 // <Mint_Url, Keyset_id>
@@ -24,7 +28,9 @@ const MINT_KEYSETS_TABLE: MultimapTableDefinition<&str, &[u8]> =
     MultimapTableDefinition::new("mint_keysets");
 // <Keyset_id, KeysetInfo>
 const KEYSETS_TABLE: TableDefinition<&[u8], &str> = TableDefinition::new("keysets");
+// <Quote_id, quote>
 const MINT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_quotes");
+// <Quote_id, quote>
 const MELT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("melt_quotes");
 const MINT_KEYS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_keys");
 // <Y, Proof Info>
@@ -33,7 +39,7 @@ const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config")
 const KEYSET_COUNTER: TableDefinition<&str, u32> = TableDefinition::new("keyset_counter");
 const NOSTR_LAST_CHECKED: TableDefinition<&str, u32> = TableDefinition::new("keyset_counter");
 
-const DATABASE_VERSION: u32 = 0;
+const DATABASE_VERSION: u32 = 1;
 
 #[derive(Debug, Clone)]
 pub struct RedbWalletDatabase {
@@ -42,42 +48,82 @@ pub struct RedbWalletDatabase {
 
 impl RedbWalletDatabase {
     pub fn new(path: &Path) -> Result<Self, Error> {
-        let db = Database::create(path)?;
-
-        let write_txn = db.begin_write()?;
-
-        // Check database version
         {
-            let _ = write_txn.open_table(CONFIG_TABLE)?;
-            let mut table = write_txn.open_table(CONFIG_TABLE)?;
+            let db = Arc::new(Database::create(path)?);
 
-            let db_version = table.get("db_version")?.map(|v| v.value().to_owned());
+            let db_version: Option<String>;
+            {
+                // Check database version
+                let read_txn = db.begin_read()?;
+                let table = read_txn.open_table(CONFIG_TABLE);
+
+                db_version = match table {
+                    Ok(table) => table.get("db_version")?.map(|v| v.value().to_string()),
+                    Err(_) => None,
+                };
+            }
 
             match db_version {
                 Some(db_version) => {
-                    let current_file_version = u32::from_str(&db_version)?;
-                    if current_file_version.ne(&DATABASE_VERSION) {
-                        // Database needs to be upgraded
-                        todo!()
+                    let mut current_file_version = u32::from_str(&db_version)?;
+
+                    match current_file_version.cmp(&DATABASE_VERSION) {
+                        Ordering::Less => {
+                            tracing::info!(
+                                "Database needs to be upgraded at {} current is {}",
+                                current_file_version,
+                                DATABASE_VERSION
+                            );
+                            if current_file_version == 0 {
+                                current_file_version = migrate_00_to_01(Arc::clone(&db))?;
+                            }
+
+                            if current_file_version != DATABASE_VERSION {
+                                tracing::warn!(
+                                    "Database upgrade did not complete at {} current is {}",
+                                    current_file_version,
+                                    DATABASE_VERSION
+                                );
+                                return Err(Error::UnknownDatabaseVersion);
+                            }
+                        }
+                        Ordering::Equal => {
+                            tracing::info!("Database is at current version {}", DATABASE_VERSION);
+                        }
+                        Ordering::Greater => {
+                            tracing::warn!(
+                                "Database upgrade did not complete at {} current is {}",
+                                current_file_version,
+                                DATABASE_VERSION
+                            );
+                            return Err(Error::UnknownDatabaseVersion);
+                        }
                     }
-                    let _ = write_txn.open_table(KEYSET_COUNTER)?;
                 }
                 None => {
-                    // Open all tables to init a new db
-                    let _ = write_txn.open_table(MINTS_TABLE)?;
-                    let _ = write_txn.open_multimap_table(MINT_KEYSETS_TABLE)?;
-                    let _ = write_txn.open_table(KEYSETS_TABLE)?;
-                    let _ = write_txn.open_table(MINT_QUOTES_TABLE)?;
-                    let _ = write_txn.open_table(MELT_QUOTES_TABLE)?;
-                    let _ = write_txn.open_table(MINT_KEYS_TABLE)?;
-                    let _ = write_txn.open_table(PROOFS_TABLE)?;
-                    let _ = write_txn.open_table(KEYSET_COUNTER)?;
-                    let _ = write_txn.open_table(NOSTR_LAST_CHECKED)?;
-                    table.insert("db_version", "0")?;
+                    let write_txn = db.begin_write()?;
+                    {
+                        let mut table = write_txn.open_table(CONFIG_TABLE)?;
+                        // Open all tables to init a new db
+                        let _ = write_txn.open_table(MINTS_TABLE)?;
+                        let _ = write_txn.open_multimap_table(MINT_KEYSETS_TABLE)?;
+                        let _ = write_txn.open_table(KEYSETS_TABLE)?;
+                        let _ = write_txn.open_table(MINT_QUOTES_TABLE)?;
+                        let _ = write_txn.open_table(MELT_QUOTES_TABLE)?;
+                        let _ = write_txn.open_table(MINT_KEYS_TABLE)?;
+                        let _ = write_txn.open_table(PROOFS_TABLE)?;
+                        let _ = write_txn.open_table(KEYSET_COUNTER)?;
+                        let _ = write_txn.open_table(NOSTR_LAST_CHECKED)?;
+                        table.insert("db_version", DATABASE_VERSION.to_string().as_str())?;
+                    }
+
+                    write_txn.commit()?;
                 }
             }
+            drop(db);
         }
-        write_txn.commit()?;
+
+        let db = Database::create(path)?;
 
         Ok(Self {
             db: Arc::new(Mutex::new(db)),

+ 8 - 2
crates/cdk-sqlite/src/mint/error.rs

@@ -5,12 +5,18 @@ pub enum Error {
     /// SQLX Error
     #[error(transparent)]
     SQLX(#[from] sqlx::Error),
+    /// NUT01 Error
+    #[error(transparent)]
+    CDKNUT01(#[from] cdk::nuts::nut01::Error),
     /// NUT02 Error
     #[error(transparent)]
     CDKNUT02(#[from] cdk::nuts::nut02::Error),
-    /// NUT01 Error
+    /// NUT04 Error
     #[error(transparent)]
-    CDKNUT01(#[from] cdk::nuts::nut01::Error),
+    CDKNUT04(#[from] cdk::nuts::nut04::Error),
+    /// NUT05 Error
+    #[error(transparent)]
+    CDKNUT05(#[from] cdk::nuts::nut05::Error),
     /// Secret Error
     #[error(transparent)]
     CDKSECRET(#[from] cdk::secret::Error),

+ 5 - 0
crates/cdk-sqlite/src/mint/migrations/20240618195700_quote_state.sql

@@ -0,0 +1,5 @@
+ALTER TABLE melt_quote ADD state TEXT CHECK ( state IN ('UNPAID', 'PENDING', 'PAID' ) ) NOT NULL;
+ALTER TABLE melt_quote ADD payment_preimage TEXT;
+ALTER TABLE melt_quote DROP COLUMN paid;
+CREATE INDEX IF NOT EXISTS melt_quote_state_index ON melt_quote(state);
+DROP INDEX IF EXISTS paid_index;

+ 3 - 0
crates/cdk-sqlite/src/mint/migrations/20240626092101_nut04_state.sql

@@ -0,0 +1,3 @@
+ALTER TABLE mint_quote ADD state TEXT CHECK ( state IN ('UNPAID', 'PENDING', 'PAID', 'ISSUED' ) ) NOT NULL;
+ALTER TABLE mint_quote DROP COLUMN paid;
+CREATE INDEX IF NOT EXISTS mint_quote_state_index ON mint_quote(state);

+ 92 - 10
crates/cdk-sqlite/src/mint/mod.rs

@@ -8,7 +8,10 @@ use async_trait::async_trait;
 use bitcoin::bip32::DerivationPath;
 use cdk::cdk_database::{self, MintDatabase};
 use cdk::mint::MintKeySetInfo;
-use cdk::nuts::{BlindSignature, CurrencyUnit, Id, Proof, PublicKey};
+use cdk::nuts::nut05::QuoteState;
+use cdk::nuts::{
+    BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, PublicKey,
+};
 use cdk::secret::Secret;
 use cdk::types::{MeltQuote, MintQuote};
 use cdk::Amount;
@@ -122,7 +125,7 @@ WHERE active = 1
         sqlx::query(
             r#"
 INSERT OR REPLACE INTO mint_quote
-(id, mint_url, amount, unit, request, paid, expiry)
+(id, mint_url, amount, unit, request, state, expiry)
 VALUES (?, ?, ?, ?, ?, ?, ?);
         "#,
         )
@@ -131,7 +134,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?);
         .bind(u64::from(quote.amount) as i64)
         .bind(quote.unit.to_string())
         .bind(quote.request)
-        .bind(quote.paid)
+        .bind(quote.state.to_string())
         .bind(quote.expiry as i64)
         .execute(&self.pool)
         .await
@@ -162,6 +165,44 @@ WHERE id=?;
 
         Ok(Some(sqlite_row_to_mint_quote(rec)?))
     }
+
+    async fn update_mint_quote_state(
+        &self,
+        quote_id: &str,
+        state: MintQuoteState,
+    ) -> Result<MintQuoteState, Self::Err> {
+        let mut transaction = self.pool.begin().await.map_err(Error::from)?;
+
+        let rec = sqlx::query(
+            r#"
+SELECT *
+FROM mint_quote
+WHERE id=?;
+        "#,
+        )
+        .bind(quote_id)
+        .fetch_one(&mut transaction)
+        .await
+        .map_err(Error::from)?;
+
+        let quote = sqlite_row_to_mint_quote(rec)?;
+
+        sqlx::query(
+            r#"
+        UPDATE mint_quote SET state = ? WHERE id = ?
+        "#,
+        )
+        .bind(state.to_string())
+        .bind(quote_id)
+        .execute(&mut transaction)
+        .await
+        .map_err(Error::from)?;
+
+        transaction.commit().await.map_err(Error::from)?;
+
+        Ok(quote.state)
+    }
+
     async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
         let rec = sqlx::query(
             r#"
@@ -196,8 +237,8 @@ WHERE id=?
         sqlx::query(
             r#"
 INSERT OR REPLACE INTO melt_quote
-(id, unit, amount, request, fee_reserve, paid, expiry)
-VALUES (?, ?, ?, ?, ?, ?, ?);
+(id, unit, amount, request, fee_reserve, state, expiry, payment_preimage)
+VALUES (?, ?, ?, ?, ?, ?, ?, ?);
         "#,
         )
         .bind(quote.id.to_string())
@@ -205,8 +246,9 @@ VALUES (?, ?, ?, ?, ?, ?, ?);
         .bind(u64::from(quote.amount) as i64)
         .bind(quote.request)
         .bind(u64::from(quote.fee_reserve) as i64)
-        .bind(quote.paid)
+        .bind(quote.state.to_string())
         .bind(quote.expiry as i64)
+        .bind(quote.payment_preimage)
         .execute(&self.pool)
         .await
         .map_err(Error::from)?;
@@ -250,6 +292,44 @@ FROM melt_quote
 
         Ok(melt_quotes)
     }
+
+    async fn update_melt_quote_state(
+        &self,
+        quote_id: &str,
+        state: MeltQuoteState,
+    ) -> Result<MeltQuoteState, Self::Err> {
+        let mut transaction = self.pool.begin().await.map_err(Error::from)?;
+
+        let rec = sqlx::query(
+            r#"
+SELECT *
+FROM melt_quote
+WHERE id=?;
+        "#,
+        )
+        .bind(quote_id)
+        .fetch_one(&mut transaction)
+        .await
+        .map_err(Error::from)?;
+
+        let quote = sqlite_row_to_melt_quote(rec)?;
+
+        sqlx::query(
+            r#"
+        UPDATE melt_quote SET state = ? WHERE id = ?
+        "#,
+        )
+        .bind(state.to_string())
+        .bind(quote_id)
+        .execute(&mut transaction)
+        .await
+        .map_err(Error::from)?;
+
+        transaction.commit().await.map_err(Error::from)?;
+
+        Ok(quote.state)
+    }
+
     async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
         sqlx::query(
             r#"
@@ -581,7 +661,7 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result<MintQuote, Error> {
     let row_amount: i64 = row.try_get("amount").map_err(Error::from)?;
     let row_unit: String = row.try_get("unit").map_err(Error::from)?;
     let row_request: String = row.try_get("request").map_err(Error::from)?;
-    let row_paid: bool = row.try_get("paid").map_err(Error::from)?;
+    let row_state: String = row.try_get("state").map_err(Error::from)?;
     let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?;
 
     Ok(MintQuote {
@@ -590,7 +670,7 @@ fn sqlite_row_to_mint_quote(row: SqliteRow) -> Result<MintQuote, Error> {
         amount: Amount::from(row_amount as u64),
         unit: CurrencyUnit::from(row_unit),
         request: row_request,
-        paid: row_paid,
+        state: MintQuoteState::from_str(&row_state).map_err(Error::from)?,
         expiry: row_expiry as u64,
     })
 }
@@ -601,8 +681,9 @@ fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result<MeltQuote, Error> {
     let row_amount: i64 = row.try_get("amount").map_err(Error::from)?;
     let row_request: String = row.try_get("request").map_err(Error::from)?;
     let row_fee_reserve: i64 = row.try_get("fee_reserve").map_err(Error::from)?;
-    let row_paid: bool = row.try_get("paid").map_err(Error::from)?;
+    let row_state: String = row.try_get("state").map_err(Error::from)?;
     let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?;
+    let row_preimage: Option<String> = row.try_get("payment_preimage").map_err(Error::from)?;
 
     Ok(MeltQuote {
         id: row_id,
@@ -610,8 +691,9 @@ fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result<MeltQuote, Error> {
         unit: CurrencyUnit::from(row_unit),
         request: row_request,
         fee_reserve: Amount::from(row_fee_reserve as u64),
-        paid: row_paid,
+        state: QuoteState::from_str(&row_state)?,
         expiry: row_expiry as u64,
+        payment_preimage: row_preimage,
     })
 }
 

+ 6 - 0
crates/cdk-sqlite/src/wallet/error.rs

@@ -17,6 +17,12 @@ pub enum Error {
     /// NUT02 Error
     #[error(transparent)]
     CDKNUT02(#[from] cdk::nuts::nut02::Error),
+    /// NUT04 Error
+    #[error(transparent)]
+    CDKNUT04(#[from] cdk::nuts::nut04::Error),
+    /// NUT05 Error
+    #[error(transparent)]
+    CDKNUT05(#[from] cdk::nuts::nut05::Error),
     /// NUT07 Error
     #[error(transparent)]
     CDKNUT07(#[from] cdk::nuts::nut07::Error),

+ 5 - 0
crates/cdk-sqlite/src/wallet/migrations/20240618200350_quote_state.sql

@@ -0,0 +1,5 @@
+ALTER TABLE melt_quote ADD state TEXT CHECK ( state IN ('UNPAID', 'PENDING', 'PAID' ) ) NOT NULL;
+ALTER TABLE melt_quote ADD payment_preimage TEXT;
+ALTER TABLE melt_quote DROP COLUMN paid;
+CREATE INDEX IF NOT EXISTS melt_quote_state_index ON melt_quote(state);
+DROP INDEX IF EXISTS paid_index;

+ 3 - 0
crates/cdk-sqlite/src/wallet/migrations/20240626091921_nut04_state.sql

@@ -0,0 +1,3 @@
+ALTER TABLE mint_quote ADD state TEXT CHECK ( state IN ('UNPAID', 'PENDING', 'PAID', 'ISSUED' ) ) NOT NULL;
+ALTER TABLE mint_quote DROP COLUMN paid;
+CREATE INDEX IF NOT EXISTS mint_quote_state_index ON mint_quote(state);

+ 15 - 12
crates/cdk-sqlite/src/wallet/mod.rs

@@ -7,8 +7,8 @@ use std::str::FromStr;
 use async_trait::async_trait;
 use cdk::cdk_database::{self, WalletDatabase};
 use cdk::nuts::{
-    CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proof, Proofs, PublicKey, SpendingConditions,
-    State,
+    CurrencyUnit, Id, KeySetInfo, Keys, MeltQuoteState, MintInfo, MintQuoteState, Proof, Proofs,
+    PublicKey, SpendingConditions, State,
 };
 use cdk::secret::Secret;
 use cdk::types::{MeltQuote, MintQuote, ProofInfo};
@@ -278,7 +278,7 @@ WHERE id=?
         sqlx::query(
             r#"
 INSERT OR REPLACE INTO mint_quote
-(id, mint_url, amount, unit, request, paid, expiry)
+(id, mint_url, amount, unit, request, state, expiry)
 VALUES (?, ?, ?, ?, ?, ?, ?);
         "#,
         )
@@ -287,7 +287,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?);
         .bind(u64::from(quote.amount) as i64)
         .bind(quote.unit.to_string())
         .bind(quote.request)
-        .bind(quote.paid)
+        .bind(quote.state.to_string())
         .bind(quote.expiry as i64)
         .execute(&self.pool)
         .await
@@ -351,7 +351,7 @@ WHERE id=?
         sqlx::query(
             r#"
 INSERT OR REPLACE INTO melt_quote
-(id, unit, amount, request, fee_reserve, paid, expiry)
+(id, unit, amount, request, fee_reserve, state, expiry)
 VALUES (?, ?, ?, ?, ?, ?, ?);
         "#,
         )
@@ -360,7 +360,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?);
         .bind(u64::from(quote.amount) as i64)
         .bind(quote.request)
         .bind(u64::from(quote.fee_reserve) as i64)
-        .bind(quote.paid)
+        .bind(quote.state.to_string())
         .bind(quote.expiry as i64)
         .execute(&self.pool)
         .await
@@ -502,8 +502,6 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
         state: Option<Vec<State>>,
         spending_conditions: Option<Vec<SpendingConditions>>,
     ) -> Result<Option<Vec<ProofInfo>>, Self::Err> {
-        tracing::debug!("{:?}", mint_url);
-        tracing::debug!("{:?}", unit);
         let recs = sqlx::query(
             r#"
 SELECT *
@@ -719,16 +717,18 @@ fn sqlite_row_to_mint_quote(row: &SqliteRow) -> Result<MintQuote, Error> {
     let row_amount: i64 = row.try_get("amount").map_err(Error::from)?;
     let row_unit: String = row.try_get("unit").map_err(Error::from)?;
     let row_request: String = row.try_get("request").map_err(Error::from)?;
-    let row_paid: bool = row.try_get("paid").map_err(Error::from)?;
+    let row_state: String = row.try_get("state").map_err(Error::from)?;
     let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?;
 
+    let state = MintQuoteState::from_str(&row_state)?;
+
     Ok(MintQuote {
         id: row_id,
         mint_url: row_mint_url.into(),
         amount: Amount::from(row_amount as u64),
         unit: CurrencyUnit::from(row_unit),
         request: row_request,
-        paid: row_paid,
+        state,
         expiry: row_expiry as u64,
     })
 }
@@ -739,17 +739,20 @@ fn sqlite_row_to_melt_quote(row: &SqliteRow) -> Result<MeltQuote, Error> {
     let row_amount: i64 = row.try_get("amount").map_err(Error::from)?;
     let row_request: String = row.try_get("request").map_err(Error::from)?;
     let row_fee_reserve: i64 = row.try_get("fee_reserve").map_err(Error::from)?;
-    let row_paid: bool = row.try_get("paid").map_err(Error::from)?;
+    let row_state: String = row.try_get("state").map_err(Error::from)?;
     let row_expiry: i64 = row.try_get("expiry").map_err(Error::from)?;
+    let row_preimage: Option<String> = row.try_get("payment_preimage").map_err(Error::from)?;
 
+    let state = MeltQuoteState::from_str(&row_state)?;
     Ok(MeltQuote {
         id: row_id,
         amount: Amount::from(row_amount as u64),
         unit: CurrencyUnit::from(row_unit),
         request: row_request,
         fee_reserve: Amount::from(row_fee_reserve as u64),
-        paid: row_paid,
+        state,
         expiry: row_expiry as u64,
+        payment_preimage: row_preimage,
     })
 }
 

+ 4 - 4
crates/cdk/examples/mint-token.rs

@@ -4,7 +4,7 @@ use std::time::Duration;
 use cdk::amount::SplitTarget;
 use cdk::cdk_database::WalletMemoryDatabase;
 use cdk::error::Error;
-use cdk::nuts::CurrencyUnit;
+use cdk::nuts::{CurrencyUnit, MintQuoteState};
 use cdk::wallet::Wallet;
 use cdk::Amount;
 use rand::Rng;
@@ -26,11 +26,11 @@ async fn main() -> Result<(), Error> {
     println!("Quote: {:#?}", quote);
 
     loop {
-        let status = wallet.mint_quote_status(&quote.id).await.unwrap();
+        let status = wallet.mint_quote_state(&quote.id).await.unwrap();
 
-        println!("Quote status: {}", status.paid);
+        println!("Quote status: {}", status.state);
 
-        if status.paid {
+        if status.state == MintQuoteState::Paid {
             break;
         }
 

+ 4 - 4
crates/cdk/examples/p2pk.rs

@@ -4,7 +4,7 @@ use std::time::Duration;
 use cdk::amount::SplitTarget;
 use cdk::cdk_database::WalletMemoryDatabase;
 use cdk::error::Error;
-use cdk::nuts::{CurrencyUnit, SecretKey, SpendingConditions};
+use cdk::nuts::{CurrencyUnit, MintQuoteState, SecretKey, SpendingConditions};
 use cdk::wallet::Wallet;
 use cdk::Amount;
 use rand::Rng;
@@ -26,11 +26,11 @@ async fn main() -> Result<(), Error> {
     println!("Minting nuts ...");
 
     loop {
-        let status = wallet.mint_quote_status(&quote.id).await.unwrap();
+        let status = wallet.mint_quote_state(&quote.id).await.unwrap();
 
-        println!("Quote status: {}", status.paid);
+        println!("Quote status: {}", status.state);
 
-        if status.paid {
+        if status.state == MintQuoteState::Paid {
             break;
         }
 

+ 45 - 1
crates/cdk/src/cdk_database/mint_memory.rs

@@ -7,7 +7,9 @@ use tokio::sync::RwLock;
 use super::{Error, MintDatabase};
 use crate::dhke::hash_to_curve;
 use crate::mint::MintKeySetInfo;
-use crate::nuts::{BlindSignature, CurrencyUnit, Id, Proof, Proofs, PublicKey};
+use crate::nuts::{
+    BlindSignature, CurrencyUnit, Id, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey,
+};
 use crate::secret::Secret;
 use crate::types::{MeltQuote, MintQuote};
 
@@ -103,6 +105,27 @@ impl MintDatabase for MintMemoryDatabase {
         Ok(self.mint_quotes.read().await.get(quote_id).cloned())
     }
 
+    async fn update_mint_quote_state(
+        &self,
+        quote_id: &str,
+        state: MintQuoteState,
+    ) -> Result<MintQuoteState, Self::Err> {
+        let mut mint_quotes = self.mint_quotes.write().await;
+
+        let mut quote = mint_quotes
+            .get(quote_id)
+            .cloned()
+            .ok_or(Error::UnknownQuote)?;
+
+        let current_state = quote.state;
+
+        quote.state = state;
+
+        mint_quotes.insert(quote_id.to_string(), quote.clone());
+
+        Ok(current_state)
+    }
+
     async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
         Ok(self.mint_quotes.read().await.values().cloned().collect())
     }
@@ -125,6 +148,27 @@ impl MintDatabase for MintMemoryDatabase {
         Ok(self.melt_quotes.read().await.get(quote_id).cloned())
     }
 
+    async fn update_melt_quote_state(
+        &self,
+        quote_id: &str,
+        state: MeltQuoteState,
+    ) -> Result<MeltQuoteState, Self::Err> {
+        let mut melt_quotes = self.melt_quotes.write().await;
+
+        let mut quote = melt_quotes
+            .get(quote_id)
+            .cloned()
+            .ok_or(Error::UnknownQuote)?;
+
+        let current_state = quote.state;
+
+        quote.state = state;
+
+        melt_quotes.insert(quote_id.to_string(), quote.clone());
+
+        Ok(current_state)
+    }
+
     async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, Self::Err> {
         Ok(self.melt_quotes.read().await.values().cloned().collect())
     }

+ 13 - 1
crates/cdk/src/cdk_database/mod.rs

@@ -13,7 +13,7 @@ use crate::mint::MintKeySetInfo;
 #[cfg(feature = "wallet")]
 use crate::nuts::State;
 #[cfg(feature = "mint")]
-use crate::nuts::{BlindSignature, Proof};
+use crate::nuts::{BlindSignature, MeltQuoteState, MintQuoteState, Proof};
 #[cfg(any(feature = "wallet", feature = "mint"))]
 use crate::nuts::{CurrencyUnit, Id, PublicKey};
 #[cfg(feature = "wallet")]
@@ -43,6 +43,8 @@ pub enum Error {
     Cdk(#[from] crate::error::Error),
     #[error(transparent)]
     NUT01(#[from] crate::nuts::nut00::Error),
+    #[error("Unknown Quote")]
+    UnknownQuote,
 }
 
 #[cfg(feature = "wallet")]
@@ -126,11 +128,21 @@ pub trait MintDatabase {
 
     async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Self::Err>;
     async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, Self::Err>;
+    async fn update_mint_quote_state(
+        &self,
+        quote_id: &str,
+        state: MintQuoteState,
+    ) -> Result<MintQuoteState, Self::Err>;
     async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err>;
     async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
 
     async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Self::Err>;
     async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<MeltQuote>, Self::Err>;
+    async fn update_melt_quote_state(
+        &self,
+        quote_id: &str,
+        state: MeltQuoteState,
+    ) -> Result<MeltQuoteState, Self::Err>;
     async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, Self::Err>;
     async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
 

+ 6 - 0
crates/cdk/src/mint/error.rs

@@ -26,8 +26,14 @@ pub enum Error {
     TokenPending,
     #[error("Quote not paid")]
     UnpaidQuote,
+    #[error("Quote is already paid")]
+    PaidQuote,
     #[error("Unknown quote")]
     UnknownQuote,
+    #[error("Quote pending")]
+    PendingQuote,
+    #[error("Quote already issued")]
+    IssuedQuote,
     #[error("Unknown secret kind")]
     UnknownSecretKind,
     #[error("Cannot have multiple units")]

+ 56 - 21
crates/cdk/src/mint/mod.rs

@@ -9,6 +9,7 @@ use error::Error;
 use serde::{Deserialize, Serialize};
 use tokio::sync::RwLock;
 
+use self::nut05::QuoteState;
 use crate::cdk_database::{self, MintDatabase};
 use crate::dhke::{hash_to_curve, sign_message, verify_message};
 use crate::nuts::nut11::enforce_sig_flag;
@@ -130,10 +131,13 @@ impl Mint {
             .await?
             .ok_or(Error::UnknownQuote)?;
 
+        let paid = quote.state == MintQuoteState::Paid;
+
         Ok(MintQuoteBolt11Response {
             quote: quote.id,
             request: quote.request,
-            paid: quote.paid,
+            paid: Some(paid),
+            state: quote.state,
             expiry: Some(quote.expiry),
         })
     }
@@ -183,10 +187,13 @@ impl Mint {
 
         Ok(MeltQuoteBolt11Response {
             quote: quote.id,
-            paid: quote.paid,
+            paid: Some(quote.state == QuoteState::Paid),
+            state: quote.state,
             expiry: quote.expiry,
             amount: quote.amount,
             fee_reserve: quote.fee_reserve,
+            payment_preimage: quote.payment_preimage,
+            change: None,
         })
     }
 
@@ -294,6 +301,24 @@ impl Mint {
         &self,
         mint_request: nut04::MintBolt11Request,
     ) -> Result<nut04::MintBolt11Response, Error> {
+        let state = self
+            .localstore
+            .update_mint_quote_state(&mint_request.quote, MintQuoteState::Pending)
+            .await?;
+
+        match state {
+            MintQuoteState::Unpaid => {
+                return Err(Error::UnpaidQuote);
+            }
+            MintQuoteState::Pending => {
+                return Err(Error::PendingQuote);
+            }
+            MintQuoteState::Issued => {
+                return Err(Error::IssuedQuote);
+            }
+            MintQuoteState::Paid => (),
+        }
+
         for blinded_message in &mint_request.outputs {
             if self
                 .localstore
@@ -309,16 +334,6 @@ impl Mint {
             }
         }
 
-        let quote = self
-            .localstore
-            .get_mint_quote(&mint_request.quote)
-            .await?
-            .ok_or(Error::UnknownQuote)?;
-
-        if !quote.paid {
-            return Err(Error::UnpaidQuote);
-        }
-
         let mut blind_signatures = Vec::with_capacity(mint_request.outputs.len());
 
         for blinded_message in mint_request.outputs.into_iter() {
@@ -330,7 +345,7 @@ impl Mint {
         }
 
         self.localstore
-            .remove_mint_quote(&mint_request.quote)
+            .update_mint_quote_state(&mint_request.quote, MintQuoteState::Issued)
             .await?;
 
         Ok(nut04::MintBolt11Response {
@@ -568,6 +583,21 @@ impl Mint {
         &self,
         melt_request: &MeltBolt11Request,
     ) -> Result<MeltQuote, Error> {
+        let state = self
+            .localstore
+            .update_melt_quote_state(&melt_request.quote, MeltQuoteState::Pending)
+            .await?;
+
+        match state {
+            MeltQuoteState::Unpaid => (),
+            MeltQuoteState::Pending => {
+                return Err(Error::PendingQuote);
+            }
+            MeltQuoteState::Paid => {
+                return Err(Error::PaidQuote);
+            }
+        }
+
         let quote = self
             .localstore
             .get_melt_quote(&melt_request.quote)
@@ -664,8 +694,8 @@ impl Mint {
         melt_request: &MeltBolt11Request,
         preimage: &str,
         total_spent: Amount,
-    ) -> Result<MeltBolt11Response, Error> {
-        self.verify_melt_request(melt_request).await?;
+    ) -> Result<MeltQuoteBolt11Response, Error> {
+        let quote = self.verify_melt_request(melt_request).await?;
 
         if let Some(outputs) = &melt_request.outputs {
             for blinded_message in outputs {
@@ -688,10 +718,6 @@ impl Mint {
             self.localstore.add_spent_proof(input.clone()).await?;
         }
 
-        self.localstore
-            .remove_melt_quote(&melt_request.quote)
-            .await?;
-
         let mut change = None;
 
         if let Some(outputs) = melt_request.outputs.clone() {
@@ -734,10 +760,19 @@ impl Mint {
             );
         }
 
-        Ok(MeltBolt11Response {
-            paid: true,
+        self.localstore
+            .update_melt_quote_state(&melt_request.quote, MeltQuoteState::Paid)
+            .await?;
+
+        Ok(MeltQuoteBolt11Response {
+            amount: quote.amount,
+            paid: Some(true),
             payment_preimage: Some(preimage.to_string()),
             change,
+            quote: quote.id,
+            fee_reserve: quote.fee_reserve,
+            state: QuoteState::Paid,
+            expiry: quote.expiry,
         })
     }
 

+ 3 - 3
crates/cdk/src/nuts/mod.rs

@@ -28,11 +28,11 @@ pub use nut03::PreSwap;
 pub use nut03::{SwapRequest, SwapResponse};
 pub use nut04::{
     MintBolt11Request, MintBolt11Response, MintMethodSettings, MintQuoteBolt11Request,
-    MintQuoteBolt11Response, Settings as NUT04Settings,
+    MintQuoteBolt11Response, QuoteState as MintQuoteState, Settings as NUT04Settings,
 };
 pub use nut05::{
-    MeltBolt11Request, MeltBolt11Response, MeltMethodSettings, MeltQuoteBolt11Request,
-    MeltQuoteBolt11Response, Settings as NUT05Settings,
+    MeltBolt11Request, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
+    QuoteState as MeltQuoteState, Settings as NUT05Settings,
 };
 pub use nut06::{MintInfo, MintVersion, Nuts};
 pub use nut07::{CheckStateRequest, CheckStateResponse, ProofState, State};

+ 124 - 4
crates/cdk/src/nuts/nut04.rs

@@ -2,12 +2,25 @@
 //!
 //! <https://github.com/cashubtc/nuts/blob/main/04.md>
 
-use serde::{Deserialize, Serialize};
+use std::fmt;
+use std::str::FromStr;
+
+use serde::{Deserialize, Deserializer, Serialize};
+use serde_json::Value;
+use thiserror::Error;
 
 use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod};
+use super::MintQuoteState;
 use crate::types::MintQuote;
 use crate::Amount;
 
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Unknown Quote State
+    #[error("Unknown Quote State")]
+    UnknownState,
+}
+
 /// Mint quote request [NUT-04]
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct MintQuoteBolt11Request {
@@ -17,25 +30,132 @@ pub struct MintQuoteBolt11Request {
     pub unit: CurrencyUnit,
 }
 
+/// Possible states of a quote
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
+#[serde(rename_all = "UPPERCASE")]
+pub enum QuoteState {
+    #[default]
+    Unpaid,
+    Paid,
+    Pending,
+    Issued,
+}
+
+impl fmt::Display for QuoteState {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::Unpaid => write!(f, "UNPAID"),
+            Self::Paid => write!(f, "PAID"),
+            Self::Pending => write!(f, "PENDING"),
+            Self::Issued => write!(f, "ISSUED"),
+        }
+    }
+}
+
+impl FromStr for QuoteState {
+    type Err = Error;
+
+    fn from_str(state: &str) -> Result<Self, Self::Err> {
+        match state {
+            "PENDING" => Ok(Self::Pending),
+            "PAID" => Ok(Self::Paid),
+            "UNPAID" => Ok(Self::Unpaid),
+            "ISSUED" => Ok(Self::Issued),
+            _ => Err(Error::UnknownState),
+        }
+    }
+}
+
 /// Mint quote response [NUT-04]
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
 pub struct MintQuoteBolt11Response {
     /// Quote Id
     pub quote: String,
     /// Payment request to fulfil
     pub request: String,
+    // TODO: To be deprecated
     /// Whether the the request haas be paid
-    pub paid: bool,
+    /// Deprecated
+    pub paid: Option<bool>,
+    /// Quote State
+    pub state: MintQuoteState,
     /// Unix timestamp until the quote is valid
     pub expiry: Option<u64>,
 }
 
+// A custom deserializer is needed until all mints
+// update some will return without the required state.
+impl<'de> Deserialize<'de> for MintQuoteBolt11Response {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let value = Value::deserialize(deserializer)?;
+
+        let quote: String = serde_json::from_value(
+            value
+                .get("quote")
+                .ok_or(serde::de::Error::missing_field("quote"))?
+                .clone(),
+        )
+        .map_err(|_| serde::de::Error::custom("Invalid quote id string"))?;
+
+        let request: String = serde_json::from_value(
+            value
+                .get("request")
+                .ok_or(serde::de::Error::missing_field("request"))?
+                .clone(),
+        )
+        .map_err(|_| serde::de::Error::custom("Invalid request string"))?;
+
+        let paid: Option<bool> = value.get("paid").and_then(|p| p.as_bool());
+
+        let state: Option<String> = value
+            .get("state")
+            .and_then(|s| serde_json::from_value(s.clone()).ok());
+
+        let (state, paid) = match (state, paid) {
+            (None, None) => return Err(serde::de::Error::custom("State or paid must be defined")),
+            (Some(state), _) => {
+                let state: QuoteState = QuoteState::from_str(&state)
+                    .map_err(|_| serde::de::Error::custom("Unknown state"))?;
+                let paid = state == QuoteState::Paid;
+
+                (state, paid)
+            }
+            (None, Some(paid)) => {
+                let state = if paid {
+                    QuoteState::Paid
+                } else {
+                    QuoteState::Unpaid
+                };
+                (state, paid)
+            }
+        };
+
+        let expiry = value
+            .get("expiry")
+            .ok_or(serde::de::Error::missing_field("expiry"))?
+            .as_u64();
+
+        Ok(Self {
+            quote,
+            request,
+            paid: Some(paid),
+            state,
+            expiry,
+        })
+    }
+}
+
 impl From<MintQuote> for MintQuoteBolt11Response {
     fn from(mint_quote: MintQuote) -> MintQuoteBolt11Response {
+        let paid = mint_quote.state == QuoteState::Paid;
         MintQuoteBolt11Response {
             quote: mint_quote.id,
             request: mint_quote.request,
-            paid: mint_quote.paid,
+            paid: Some(paid),
+            state: mint_quote.state,
             expiry: Some(mint_quote.expiry),
         }
     }

+ 158 - 4
crates/cdk/src/nuts/nut05.rs

@@ -2,13 +2,25 @@
 //!
 //! <https://github.com/cashubtc/nuts/blob/main/05.md>
 
-use serde::{Deserialize, Serialize};
+use std::fmt;
+use std::str::FromStr;
+
+use serde::{Deserialize, Deserializer, Serialize};
+use serde_json::Value;
+use thiserror::Error;
 
 use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
 use super::nut15::Mpp;
 use crate::types::MeltQuote;
 use crate::{Amount, Bolt11Invoice};
 
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Unknown Quote State
+    #[error("Unknown Quote State")]
+    UnknownState,
+}
+
 /// Melt quote request [NUT-05]
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct MeltQuoteBolt11Request {
@@ -20,8 +32,41 @@ pub struct MeltQuoteBolt11Request {
     pub options: Option<Mpp>,
 }
 
+/// Possible states of a quote
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
+#[serde(rename_all = "UPPERCASE")]
+pub enum QuoteState {
+    #[default]
+    Unpaid,
+    Paid,
+    Pending,
+}
+
+impl fmt::Display for QuoteState {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::Unpaid => write!(f, "UNPAID"),
+            Self::Paid => write!(f, "PAID"),
+            Self::Pending => write!(f, "PENDING"),
+        }
+    }
+}
+
+impl FromStr for QuoteState {
+    type Err = Error;
+
+    fn from_str(state: &str) -> Result<Self, Self::Err> {
+        match state {
+            "PENDING" => Ok(Self::Pending),
+            "PAID" => Ok(Self::Paid),
+            "UNPAID" => Ok(Self::Unpaid),
+            _ => Err(Error::UnknownState),
+        }
+    }
+}
+
 /// Melt quote response [NUT-05]
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
 pub struct MeltQuoteBolt11Response {
     /// Quote Id
     pub quote: String,
@@ -30,19 +75,117 @@ pub struct MeltQuoteBolt11Response {
     /// The fee reserve that is required
     pub fee_reserve: Amount,
     /// Whether the the request haas be paid
-    pub paid: bool,
+    // TODO: To be deprecated
+    /// Deprecated
+    pub paid: Option<bool>,
+    /// Quote State
+    pub state: QuoteState,
     /// Unix timestamp until the quote is valid
     pub expiry: u64,
+    /// Payment preimage
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub payment_preimage: Option<String>,
+    /// Change
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub change: Option<Vec<BlindSignature>>,
+}
+
+// A custom deserializer is needed until all mints
+// update some will return without the required state.
+impl<'de> Deserialize<'de> for MeltQuoteBolt11Response {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let value = Value::deserialize(deserializer)?;
+
+        let quote: String = serde_json::from_value(
+            value
+                .get("quote")
+                .ok_or(serde::de::Error::missing_field("quote"))?
+                .clone(),
+        )
+        .map_err(|_| serde::de::Error::custom("Invalid quote if string"))?;
+
+        let amount = value
+            .get("amount")
+            .ok_or(serde::de::Error::missing_field("amount"))?
+            .as_u64()
+            .ok_or(serde::de::Error::missing_field("amount"))?;
+        let amount = Amount::from(amount);
+
+        let fee_reserve = value
+            .get("fee_reserve")
+            .ok_or(serde::de::Error::missing_field("fee_reserve"))?
+            .as_u64()
+            .ok_or(serde::de::Error::missing_field("fee_reserve"))?;
+
+        let fee_reserve = Amount::from(fee_reserve);
+
+        let paid: Option<bool> = value.get("paid").and_then(|p| p.as_bool());
+
+        let state: Option<String> = value
+            .get("state")
+            .and_then(|s| serde_json::from_value(s.clone()).ok());
+
+        let (state, paid) = match (state, paid) {
+            (None, None) => return Err(serde::de::Error::custom("State or paid must be defined")),
+            (Some(state), _) => {
+                let state: QuoteState = QuoteState::from_str(&state)
+                    .map_err(|_| serde::de::Error::custom("Unknown state"))?;
+                let paid = state == QuoteState::Paid;
+
+                (state, paid)
+            }
+            (None, Some(paid)) => {
+                let state = if paid {
+                    QuoteState::Paid
+                } else {
+                    QuoteState::Unpaid
+                };
+                (state, paid)
+            }
+        };
+
+        let expiry = value
+            .get("expiry")
+            .ok_or(serde::de::Error::missing_field("expiry"))?
+            .as_u64()
+            .ok_or(serde::de::Error::missing_field("expiry"))?;
+
+        let payment_preimage: Option<String> = value
+            .get("payment_preimage")
+            .and_then(|p| serde_json::from_value(p.clone()).ok());
+
+        let change: Option<Vec<BlindSignature>> = value
+            .get("change")
+            .and_then(|b| serde_json::from_value(b.clone()).ok());
+
+        Ok(Self {
+            quote,
+            amount,
+            fee_reserve,
+            paid: Some(paid),
+            state,
+            expiry,
+            payment_preimage,
+            change,
+        })
+    }
 }
 
 impl From<MeltQuote> for MeltQuoteBolt11Response {
     fn from(melt_quote: MeltQuote) -> MeltQuoteBolt11Response {
+        let paid = melt_quote.state == QuoteState::Paid;
         MeltQuoteBolt11Response {
             quote: melt_quote.id,
             amount: melt_quote.amount,
             fee_reserve: melt_quote.fee_reserve,
-            paid: melt_quote.paid,
+            paid: Some(paid),
+            state: melt_quote.state,
             expiry: melt_quote.expiry,
+            payment_preimage: melt_quote.payment_preimage,
+            change: None,
         }
     }
 }
@@ -65,6 +208,7 @@ impl MeltBolt11Request {
     }
 }
 
+// TODO: to be deprecated
 /// Melt Response [NUT-05]
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct MeltBolt11Response {
@@ -76,6 +220,16 @@ pub struct MeltBolt11Response {
     pub change: Option<Vec<BlindSignature>>,
 }
 
+impl From<MeltQuoteBolt11Response> for MeltBolt11Response {
+    fn from(quote_response: MeltQuoteBolt11Response) -> MeltBolt11Response {
+        MeltBolt11Response {
+            paid: quote_response.paid.unwrap(),
+            payment_preimage: quote_response.payment_preimage,
+            change: quote_response.change,
+        }
+    }
+}
+
 /// Melt Method Settings
 #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub struct MeltMethodSettings {

+ 4 - 4
crates/cdk/src/nuts/nut07.rs

@@ -30,10 +30,10 @@ pub enum State {
 impl fmt::Display for State {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         let s = match self {
-            State::Spent => "SPENT",
-            State::Unspent => "UNSPENT",
-            State::Pending => "PENDING",
-            State::Reserved => "RESERVED",
+            Self::Spent => "SPENT",
+            Self::Unspent => "UNSPENT",
+            Self::Pending => "PENDING",
+            Self::Reserved => "RESERVED",
         };
 
         write!(f, "{}", s)

+ 2 - 2
crates/cdk/src/nuts/nut08.rs

@@ -2,7 +2,7 @@
 //!
 //! <https://github.com/cashubtc/nuts/blob/main/08.md>
 
-use super::nut05::{MeltBolt11Request, MeltBolt11Response};
+use super::nut05::{MeltBolt11Request, MeltQuoteBolt11Response};
 use crate::Amount;
 
 impl MeltBolt11Request {
@@ -13,7 +13,7 @@ impl MeltBolt11Request {
     }
 }
 
-impl MeltBolt11Response {
+impl MeltQuoteBolt11Response {
     pub fn change_amount(&self) -> Option<Amount> {
         self.change
             .as_ref()

+ 12 - 15
crates/cdk/src/types.rs

@@ -4,27 +4,21 @@ use serde::{Deserialize, Serialize};
 use uuid::Uuid;
 
 use crate::error::Error;
-use crate::nuts::{CurrencyUnit, Proof, Proofs, PublicKey, SpendingConditions, State};
+use crate::nuts::{
+    CurrencyUnit, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey, SpendingConditions,
+    State,
+};
 use crate::url::UncheckedUrl;
 use crate::Amount;
 
 /// Melt response with proofs
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
 pub struct Melted {
-    pub paid: bool,
+    pub state: MeltQuoteState,
     pub preimage: Option<String>,
     pub change: Option<Proofs>,
 }
 
-/// Possible states of an invoice
-#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
-pub enum InvoiceStatus {
-    Unpaid,
-    Paid,
-    Expired,
-    InFlight,
-}
-
 /// Mint Quote Info
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct MintQuote {
@@ -33,7 +27,7 @@ pub struct MintQuote {
     pub amount: Amount,
     pub unit: CurrencyUnit,
     pub request: String,
-    pub paid: bool,
+    pub state: MintQuoteState,
     pub expiry: u64,
 }
 
@@ -53,7 +47,7 @@ impl MintQuote {
             amount,
             unit,
             request,
-            paid: false,
+            state: MintQuoteState::Unpaid,
             expiry,
         }
     }
@@ -67,10 +61,12 @@ pub struct MeltQuote {
     pub amount: Amount,
     pub request: String,
     pub fee_reserve: Amount,
-    pub paid: bool,
+    pub state: MeltQuoteState,
     pub expiry: u64,
+    pub payment_preimage: Option<String>,
 }
 
+#[cfg(feature = "mint")]
 impl MeltQuote {
     pub fn new(
         request: String,
@@ -87,8 +83,9 @@ impl MeltQuote {
             unit,
             request,
             fee_reserve,
-            paid: false,
+            state: MeltQuoteState::Unpaid,
             expiry,
+            payment_preimage: None,
         }
     }
 }

+ 21 - 9
crates/cdk/src/wallet/client.rs

@@ -7,13 +7,14 @@ use url::Url;
 
 use super::Error;
 use crate::error::ErrorResponse;
+use crate::nuts::nut05::MeltBolt11Response;
 use crate::nuts::nut15::Mpp;
 use crate::nuts::{
     BlindedMessage, CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysResponse,
-    KeysetResponse, MeltBolt11Request, MeltBolt11Response, MeltQuoteBolt11Request,
-    MeltQuoteBolt11Response, MintBolt11Request, MintBolt11Response, MintInfo,
-    MintQuoteBolt11Request, MintQuoteBolt11Response, PreMintSecrets, Proof, PublicKey,
-    RestoreRequest, RestoreResponse, SwapRequest, SwapResponse,
+    KeysetResponse, MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
+    MintBolt11Request, MintBolt11Response, MintInfo, MintQuoteBolt11Request,
+    MintQuoteBolt11Response, PreMintSecrets, Proof, PublicKey, RestoreRequest, RestoreResponse,
+    SwapRequest, SwapResponse,
 };
 use crate::{Amount, Bolt11Invoice};
 
@@ -112,7 +113,10 @@ impl HttpClient {
 
         match serde_json::from_value::<MintQuoteBolt11Response>(res.clone()) {
             Ok(mint_quote_response) => Ok(mint_quote_response),
-            Err(_) => Err(ErrorResponse::from_value(res)?.into()),
+            Err(err) => {
+                tracing::warn!("{}", err);
+                Err(ErrorResponse::from_value(res)?.into())
+            }
         }
     }
 
@@ -129,7 +133,10 @@ impl HttpClient {
 
         match serde_json::from_value::<MintQuoteBolt11Response>(res.clone()) {
             Ok(mint_quote_response) => Ok(mint_quote_response),
-            Err(_) => Err(ErrorResponse::from_value(res)?.into()),
+            Err(err) => {
+                tracing::warn!("{}", err);
+                Err(ErrorResponse::from_value(res)?.into())
+            }
         }
     }
 
@@ -241,9 +248,14 @@ impl HttpClient {
             .json::<Value>()
             .await?;
 
-        match serde_json::from_value::<MeltBolt11Response>(res.clone()) {
-            Ok(melt_quote_response) => Ok(melt_quote_response),
-            Err(_) => Err(ErrorResponse::from_value(res)?.into()),
+        match serde_json::from_value::<MeltQuoteBolt11Response>(res.clone()) {
+            Ok(melt_quote_response) => Ok(melt_quote_response.into()),
+            Err(_) => {
+                if let Ok(res) = serde_json::from_value::<MeltBolt11Response>(res.clone()) {
+                    return Ok(res);
+                }
+                Err(ErrorResponse::from_value(res)?.into())
+            }
         }
     }
 

+ 17 - 14
crates/cdk/src/wallet/mod.rs

@@ -20,9 +20,9 @@ use crate::cdk_database::{self, WalletDatabase};
 use crate::dhke::{construct_proofs, hash_to_curve};
 use crate::nuts::{
     nut10, nut12, Conditions, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, Kind,
-    MeltQuoteBolt11Response, MintInfo, MintQuoteBolt11Response, PreMintSecrets, PreSwap, Proof,
-    ProofState, Proofs, PublicKey, RestoreRequest, SecretKey, SigFlag, SpendingConditions, State,
-    SwapRequest, Token,
+    MeltQuoteBolt11Response, MeltQuoteState, MintInfo, MintQuoteBolt11Response, MintQuoteState,
+    PreMintSecrets, PreSwap, Proof, ProofState, Proofs, PublicKey, RestoreRequest, SecretKey,
+    SigFlag, SpendingConditions, State, SwapRequest, Token,
 };
 use crate::types::{MeltQuote, Melted, MintQuote, ProofInfo};
 use crate::url::UncheckedUrl;
@@ -380,7 +380,7 @@ impl Wallet {
             amount,
             unit: unit.clone(),
             request: quote_res.request,
-            paid: quote_res.paid,
+            state: quote_res.state,
             expiry: quote_res.expiry.unwrap_or(0),
         };
 
@@ -391,10 +391,7 @@ impl Wallet {
 
     /// Mint quote status
     #[instrument(skip(self, quote_id))]
-    pub async fn mint_quote_status(
-        &self,
-        quote_id: &str,
-    ) -> Result<MintQuoteBolt11Response, Error> {
+    pub async fn mint_quote_state(&self, quote_id: &str) -> Result<MintQuoteBolt11Response, Error> {
         let response = self
             .client
             .get_mint_quote_status(self.mint_url.clone().try_into()?, quote_id)
@@ -404,7 +401,7 @@ impl Wallet {
             Some(quote) => {
                 let mut quote = quote;
 
-                quote.paid = response.paid;
+                quote.state = response.state;
                 self.localstore.add_mint_quote(quote).await?;
             }
             None => {
@@ -422,9 +419,9 @@ impl Wallet {
         let mut total_amount = Amount::ZERO;
 
         for mint_quote in mint_quotes {
-            let mint_quote_response = self.mint_quote_status(&mint_quote.id).await?;
+            let mint_quote_response = self.mint_quote_state(&mint_quote.id).await?;
 
-            if mint_quote_response.paid {
+            if mint_quote_response.state == MintQuoteState::Paid {
                 let amount = self
                     .mint(&mint_quote.id, SplitTarget::default(), None)
                     .await?;
@@ -864,8 +861,9 @@ impl Wallet {
             request,
             unit: self.unit.clone(),
             fee_reserve: quote_res.fee_reserve,
-            paid: quote_res.paid,
+            state: quote_res.state,
             expiry: quote_res.expiry,
+            payment_preimage: quote_res.payment_preimage,
         };
 
         self.localstore.add_melt_quote(quote.clone()).await?;
@@ -888,7 +886,7 @@ impl Wallet {
             Some(quote) => {
                 let mut quote = quote;
 
-                quote.paid = response.paid;
+                quote.state = response.state;
                 self.localstore.add_melt_quote(quote).await?;
             }
             None => {
@@ -976,8 +974,13 @@ impl Wallet {
             None => None,
         };
 
+        let state = match melt_response.paid {
+            true => MeltQuoteState::Paid,
+            false => MeltQuoteState::Unpaid,
+        };
+
         let melted = Melted {
-            paid: true,
+            state,
             preimage: melt_response.payment_preimage,
             change: change_proofs.clone(),
         };