Cesar Rodas 3 months ago
parent
commit
735110c7de
48 changed files with 7576 additions and 68 deletions
  1. 20 0
      Cargo.lock
  2. 31 0
      crates/cdk-types/Cargo.toml
  3. 470 0
      crates/cdk-types/src/amount.rs
  4. 427 0
      crates/cdk-types/src/cdk_database/mint_memory.rs
  5. 275 0
      crates/cdk-types/src/cdk_database/mod.rs
  6. 358 0
      crates/cdk-types/src/cdk_database/wallet_memory.rs
  7. 21 0
      crates/cdk-types/src/dhke.rs
  8. 124 0
      crates/cdk-types/src/hex.rs
  9. 30 0
      crates/cdk-types/src/lib.rs
  10. 63 0
      crates/cdk-types/src/mint.rs
  11. 131 0
      crates/cdk-types/src/mint_url.rs
  12. 53 0
      crates/cdk-types/src/nuts/mod.rs
  13. 449 0
      crates/cdk-types/src/nuts/nut00/mod.rs
  14. 430 0
      crates/cdk-types/src/nuts/nut00/token.rs
  15. 165 0
      crates/cdk-types/src/nuts/nut01/mod.rs
  16. 153 0
      crates/cdk-types/src/nuts/nut01/public_key.rs
  17. 141 0
      crates/cdk-types/src/nuts/nut01/secret_key.rs
  18. 288 0
      crates/cdk-types/src/nuts/nut02.rs
  19. 91 0
      crates/cdk-types/src/nuts/nut03.rs
  20. 234 0
      crates/cdk-types/src/nuts/nut04.rs
  21. 360 0
      crates/cdk-types/src/nuts/nut05.rs
  22. 549 0
      crates/cdk-types/src/nuts/nut06.rs
  23. 107 0
      crates/cdk-types/src/nuts/nut07.rs
  24. 24 0
      crates/cdk-types/src/nuts/nut08.rs
  25. 41 0
      crates/cdk-types/src/nuts/nut09.rs
  26. 123 0
      crates/cdk-types/src/nuts/nut10.rs
  27. 612 0
      crates/cdk-types/src/nuts/nut11/mod.rs
  28. 22 0
      crates/cdk-types/src/nuts/nut11/serde_p2pk_witness.rs
  29. 63 0
      crates/cdk-types/src/nuts/nut12.rs
  30. 256 0
      crates/cdk-types/src/nuts/nut13.rs
  31. 51 0
      crates/cdk-types/src/nuts/nut14/mod.rs
  32. 22 0
      crates/cdk-types/src/nuts/nut14/serde_htlc_witness.rs
  33. 35 0
      crates/cdk-types/src/nuts/nut15.rs
  34. 225 0
      crates/cdk-types/src/nuts/nut17/manager.rs
  35. 189 0
      crates/cdk-types/src/nuts/nut17/mod.rs
  36. 93 0
      crates/cdk-types/src/nuts/nut17/on_subscription.rs
  37. 215 0
      crates/cdk-types/src/nuts/nut17/ws.rs
  38. 203 0
      crates/cdk-types/src/nuts/nut18.rs
  39. 55 0
      crates/cdk-types/src/nuts/nut19.rs
  40. 151 0
      crates/cdk-types/src/nuts/nut20.rs
  41. 151 0
      crates/cdk-types/src/secret.rs
  42. 5 2
      crates/cdk/Cargo.toml
  43. 17 6
      crates/cdk/src/cdk_database/mint_memory.rs
  44. 6 1
      crates/cdk/src/cdk_database/mod.rs
  45. 2 18
      crates/cdk/src/dhke.rs
  46. 1 29
      crates/cdk/src/mint/mod.rs
  47. 36 6
      crates/cdk/src/nuts/nut00/mod.rs
  48. 8 6
      crates/cdk/src/types.rs

+ 20 - 0
Cargo.lock

@@ -731,6 +731,7 @@ dependencies = [
  "bip39",
  "bitcoin 0.32.5",
  "cbor-diag",
+ "cdk-types",
  "ciborium",
  "criterion",
  "futures",
@@ -1012,6 +1013,25 @@ dependencies = [
 ]
 
 [[package]]
+name = "cdk-types"
+version = "0.6.0"
+dependencies = [
+ "async-trait",
+ "bitcoin 0.32.5",
+ "ciborium",
+ "lightning-invoice",
+ "once_cell",
+ "serde",
+ "serde_json",
+ "serde_with",
+ "thiserror 2.0.9",
+ "tracing",
+ "url",
+ "utoipa",
+ "uuid",
+]
+
+[[package]]
 name = "cfg-if"
 version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"

+ 31 - 0
crates/cdk-types/Cargo.toml

@@ -0,0 +1,31 @@
+[package]
+name = "cdk-types"
+version = "0.6.0"
+edition = "2021"
+description = "Shared types for the CDK ecosystem"
+license.workspace = true
+homepage.workspace = true
+repository.workspace = true
+
+[features]
+swagger = ["dep:utoipa"]
+
+[dependencies]
+async-trait = "0.1.83"
+bitcoin = { version = "0.32.2", features = [
+    "base64",
+    "serde",
+    "rand",
+    "rand-std",
+] }
+ciborium = { version = "0.2.2", default-features = false, features = ["std"] }
+lightning-invoice = { version = "0.32.0", features = ["serde", "std"] }
+once_cell = "1.20.2"
+serde = { version = "1.0.216", features = ["derive"] }
+serde_json = "1.0.134"
+serde_with = "3.11.0"
+thiserror = "2.0.9"
+tracing = "0.1.41"
+url = "2.5.4"
+utoipa = { version = "4", optional = true }
+uuid = { version = "1", features = ["v4", "serde"] }

+ 470 - 0
crates/cdk-types/src/amount.rs

@@ -0,0 +1,470 @@
+//! CDK Amount
+//!
+//! Is any unit and will be treated as the unit of the wallet
+
+use std::cmp::Ordering;
+use std::fmt;
+use std::str::FromStr;
+
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use thiserror::Error;
+
+use crate::nuts::CurrencyUnit;
+
+/// Amount Error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Split Values must be less then or equal to amount
+    #[error("Split Values must be less then or equal to amount")]
+    SplitValuesGreater,
+    /// Amount overflow
+    #[error("Amount Overflow")]
+    AmountOverflow,
+    /// Cannot convert units
+    #[error("Cannot convert units")]
+    CannotConvertUnits,
+}
+
+/// Amount can be any unit
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+#[serde(transparent)]
+pub struct Amount(u64);
+
+impl Amount {
+    /// Amount zero
+    pub const ZERO: Amount = Amount(0);
+
+    /// Split into parts that are powers of two
+    pub fn split(&self) -> Vec<Self> {
+        let sats = self.0;
+        (0_u64..64)
+            .rev()
+            .filter_map(|bit| {
+                let part = 1 << bit;
+                ((sats & part) == part).then_some(Self::from(part))
+            })
+            .collect()
+    }
+
+    /// Split into parts that are powers of two by target
+    pub fn split_targeted(&self, target: &SplitTarget) -> Result<Vec<Self>, Error> {
+        let mut parts = match target {
+            SplitTarget::None => self.split(),
+            SplitTarget::Value(amount) => {
+                if self.le(amount) {
+                    return Ok(self.split());
+                }
+
+                let mut parts_total = Amount::ZERO;
+                let mut parts = Vec::new();
+
+                // The powers of two that are need to create target value
+                let parts_of_value = amount.split();
+
+                while parts_total.lt(self) {
+                    for part in parts_of_value.iter().copied() {
+                        if (part + parts_total).le(self) {
+                            parts.push(part);
+                        } else {
+                            let amount_left = *self - parts_total;
+                            parts.extend(amount_left.split());
+                        }
+
+                        parts_total = Amount::try_sum(parts.clone().iter().copied())?;
+
+                        if parts_total.eq(self) {
+                            break;
+                        }
+                    }
+                }
+
+                parts
+            }
+            SplitTarget::Values(values) => {
+                let values_total: Amount = Amount::try_sum(values.clone().into_iter())?;
+
+                match self.cmp(&values_total) {
+                    Ordering::Equal => values.clone(),
+                    Ordering::Less => {
+                        return Err(Error::SplitValuesGreater);
+                    }
+                    Ordering::Greater => {
+                        let extra = *self - values_total;
+                        let mut extra_amount = extra.split();
+                        let mut values = values.clone();
+
+                        values.append(&mut extra_amount);
+                        values
+                    }
+                }
+            }
+        };
+
+        parts.sort();
+        Ok(parts)
+    }
+
+    /// Checked addition for Amount. Returns None if overflow occurs.
+    pub fn checked_add(self, other: Amount) -> Option<Amount> {
+        self.0.checked_add(other.0).map(Amount)
+    }
+
+    /// Checked subtraction for Amount. Returns None if overflow occurs.
+    pub fn checked_sub(self, other: Amount) -> Option<Amount> {
+        self.0.checked_sub(other.0).map(Amount)
+    }
+
+    /// Try sum to check for overflow
+    pub fn try_sum<I>(iter: I) -> Result<Self, Error>
+    where
+        I: IntoIterator<Item = Self>,
+    {
+        iter.into_iter().try_fold(Amount::ZERO, |acc, x| {
+            acc.checked_add(x).ok_or(Error::AmountOverflow)
+        })
+    }
+}
+
+impl Default for Amount {
+    fn default() -> Self {
+        Amount::ZERO
+    }
+}
+
+impl Default for &Amount {
+    fn default() -> Self {
+        &Amount::ZERO
+    }
+}
+
+impl fmt::Display for Amount {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if let Some(width) = f.width() {
+            write!(f, "{:width$}", self.0, width = width)
+        } else {
+            write!(f, "{}", self.0)
+        }
+    }
+}
+
+impl From<u64> for Amount {
+    fn from(value: u64) -> Self {
+        Self(value)
+    }
+}
+
+impl From<&u64> for Amount {
+    fn from(value: &u64) -> Self {
+        Self(*value)
+    }
+}
+
+impl From<Amount> for u64 {
+    fn from(value: Amount) -> Self {
+        value.0
+    }
+}
+
+impl AsRef<u64> for Amount {
+    fn as_ref(&self) -> &u64 {
+        &self.0
+    }
+}
+
+impl std::ops::Add for Amount {
+    type Output = Amount;
+
+    fn add(self, rhs: Amount) -> Self::Output {
+        Amount(self.0.checked_add(rhs.0).expect("Addition error"))
+    }
+}
+
+impl std::ops::AddAssign for Amount {
+    fn add_assign(&mut self, rhs: Self) {
+        self.0 = self.0.checked_add(rhs.0).expect("Addition error");
+    }
+}
+
+impl std::ops::Sub for Amount {
+    type Output = Amount;
+
+    fn sub(self, rhs: Amount) -> Self::Output {
+        Amount(self.0 - rhs.0)
+    }
+}
+
+impl std::ops::SubAssign for Amount {
+    fn sub_assign(&mut self, other: Self) {
+        self.0 -= other.0;
+    }
+}
+
+impl std::ops::Mul for Amount {
+    type Output = Self;
+
+    fn mul(self, other: Self) -> Self::Output {
+        Amount(self.0 * other.0)
+    }
+}
+
+impl std::ops::Div for Amount {
+    type Output = Self;
+
+    fn div(self, other: Self) -> Self::Output {
+        Amount(self.0 / other.0)
+    }
+}
+
+/// String wrapper for an [Amount].
+///
+/// It ser-/deserializes the inner [Amount] to a string, while at the same time using the [u64]
+/// value of the [Amount] for comparison and ordering. This helps automatically sort the keys of
+/// a [BTreeMap] when [AmountStr] is used as key.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct AmountStr(Amount);
+
+impl AmountStr {
+    pub(crate) fn from(amt: Amount) -> Self {
+        Self(amt)
+    }
+}
+
+impl PartialOrd<Self> for AmountStr {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for AmountStr {
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.0.cmp(&other.0)
+    }
+}
+
+impl<'de> Deserialize<'de> for AmountStr {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let s = String::deserialize(deserializer)?;
+        u64::from_str(&s)
+            .map(Amount)
+            .map(Self)
+            .map_err(serde::de::Error::custom)
+    }
+}
+
+impl Serialize for AmountStr {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        serializer.serialize_str(&self.0.to_string())
+    }
+}
+
+/// Kinds of targeting that are supported
+#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize)]
+pub enum SplitTarget {
+    /// Default target; least amount of proofs
+    #[default]
+    None,
+    /// Target amount for wallet to have most proofs that add up to value
+    Value(Amount),
+    /// Specific amounts to split into **MUST** equal amount being split
+    Values(Vec<Amount>),
+}
+
+/// Msats in sat
+pub const MSAT_IN_SAT: u64 = 1000;
+
+/// Helper function to convert units
+pub fn to_unit<T>(
+    amount: T,
+    current_unit: &CurrencyUnit,
+    target_unit: &CurrencyUnit,
+) -> Result<Amount, Error>
+where
+    T: Into<u64>,
+{
+    let amount = amount.into();
+    match (current_unit, target_unit) {
+        (CurrencyUnit::Sat, CurrencyUnit::Sat) => Ok(amount.into()),
+        (CurrencyUnit::Msat, CurrencyUnit::Msat) => Ok(amount.into()),
+        (CurrencyUnit::Sat, CurrencyUnit::Msat) => Ok((amount * MSAT_IN_SAT).into()),
+        (CurrencyUnit::Msat, CurrencyUnit::Sat) => Ok((amount / MSAT_IN_SAT).into()),
+        (CurrencyUnit::Usd, CurrencyUnit::Usd) => Ok(amount.into()),
+        (CurrencyUnit::Eur, CurrencyUnit::Eur) => Ok(amount.into()),
+        _ => Err(Error::CannotConvertUnits),
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_split_amount() {
+        assert_eq!(Amount::from(1).split(), vec![Amount::from(1)]);
+        assert_eq!(Amount::from(2).split(), vec![Amount::from(2)]);
+        assert_eq!(
+            Amount::from(3).split(),
+            vec![Amount::from(2), Amount::from(1)]
+        );
+        let amounts: Vec<Amount> = [8, 2, 1].iter().map(|a| Amount::from(*a)).collect();
+        assert_eq!(Amount::from(11).split(), amounts);
+        let amounts: Vec<Amount> = [128, 64, 32, 16, 8, 4, 2, 1]
+            .iter()
+            .map(|a| Amount::from(*a))
+            .collect();
+        assert_eq!(Amount::from(255).split(), amounts);
+    }
+
+    #[test]
+    fn test_split_target_amount() {
+        let amount = Amount(65);
+
+        let split = amount
+            .split_targeted(&SplitTarget::Value(Amount(32)))
+            .unwrap();
+        assert_eq!(vec![Amount(1), Amount(32), Amount(32)], split);
+
+        let amount = Amount(150);
+
+        let split = amount
+            .split_targeted(&SplitTarget::Value(Amount::from(50)))
+            .unwrap();
+        assert_eq!(
+            vec![
+                Amount(2),
+                Amount(2),
+                Amount(2),
+                Amount(16),
+                Amount(16),
+                Amount(16),
+                Amount(32),
+                Amount(32),
+                Amount(32)
+            ],
+            split
+        );
+
+        let amount = Amount::from(63);
+
+        let split = amount
+            .split_targeted(&SplitTarget::Value(Amount::from(32)))
+            .unwrap();
+        assert_eq!(
+            vec![
+                Amount(1),
+                Amount(2),
+                Amount(4),
+                Amount(8),
+                Amount(16),
+                Amount(32)
+            ],
+            split
+        );
+    }
+
+    #[test]
+    fn test_split_values() {
+        let amount = Amount(10);
+
+        let target = vec![Amount(2), Amount(4), Amount(4)];
+
+        let split_target = SplitTarget::Values(target.clone());
+
+        let values = amount.split_targeted(&split_target).unwrap();
+
+        assert_eq!(target, values);
+
+        let target = vec![Amount(2), Amount(4), Amount(4)];
+
+        let split_target = SplitTarget::Values(vec![Amount(2), Amount(4)]);
+
+        let values = amount.split_targeted(&split_target).unwrap();
+
+        assert_eq!(target, values);
+
+        let split_target = SplitTarget::Values(vec![Amount(2), Amount(10)]);
+
+        let values = amount.split_targeted(&split_target);
+
+        assert!(values.is_err())
+    }
+
+    #[test]
+    #[should_panic]
+    fn test_amount_addition() {
+        let amount_one: Amount = u64::MAX.into();
+        let amount_two: Amount = 1.into();
+
+        let amounts = vec![amount_one, amount_two];
+
+        let _total: Amount = Amount::try_sum(amounts).unwrap();
+    }
+
+    #[test]
+    fn test_try_amount_addition() {
+        let amount_one: Amount = u64::MAX.into();
+        let amount_two: Amount = 1.into();
+
+        let amounts = vec![amount_one, amount_two];
+
+        let total = Amount::try_sum(amounts);
+
+        assert!(total.is_err());
+        let amount_one: Amount = 10000.into();
+        let amount_two: Amount = 1.into();
+
+        let amounts = vec![amount_one, amount_two];
+        let total = Amount::try_sum(amounts).unwrap();
+
+        assert_eq!(total, 10001.into());
+    }
+
+    #[test]
+    fn test_amount_to_unit() {
+        let amount = Amount::from(1000);
+        let current_unit = CurrencyUnit::Sat;
+        let target_unit = CurrencyUnit::Msat;
+
+        let converted = to_unit(amount, &current_unit, &target_unit).unwrap();
+
+        assert_eq!(converted, 1000000.into());
+
+        let amount = Amount::from(1000);
+        let current_unit = CurrencyUnit::Msat;
+        let target_unit = CurrencyUnit::Sat;
+
+        let converted = to_unit(amount, &current_unit, &target_unit).unwrap();
+
+        assert_eq!(converted, 1.into());
+
+        let amount = Amount::from(1);
+        let current_unit = CurrencyUnit::Usd;
+        let target_unit = CurrencyUnit::Usd;
+
+        let converted = to_unit(amount, &current_unit, &target_unit).unwrap();
+
+        assert_eq!(converted, 1.into());
+
+        let amount = Amount::from(1);
+        let current_unit = CurrencyUnit::Eur;
+        let target_unit = CurrencyUnit::Eur;
+
+        let converted = to_unit(amount, &current_unit, &target_unit).unwrap();
+
+        assert_eq!(converted, 1.into());
+
+        let amount = Amount::from(1);
+        let current_unit = CurrencyUnit::Sat;
+        let target_unit = CurrencyUnit::Eur;
+
+        let converted = to_unit(amount, &current_unit, &target_unit);
+
+        assert!(converted.is_err());
+    }
+}

+ 427 - 0
crates/cdk-types/src/cdk_database/mint_memory.rs

@@ -0,0 +1,427 @@
+//! Mint in memory database
+
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use tokio::sync::{Mutex, RwLock};
+use uuid::Uuid;
+
+use super::{Error, MintDatabase};
+use crate::dhke::hash_to_curve;
+use crate::mint::{self, MintKeySetInfo, MintQuote};
+use crate::nuts::nut00::ProofsMethods;
+use crate::nuts::nut07::State;
+use crate::nuts::{
+    nut07, BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintQuoteState,
+    Proof, Proofs, PublicKey,
+};
+use crate::types::LnKey;
+
+/// Mint Memory Database
+#[derive(Debug, Clone, Default)]
+#[allow(clippy::type_complexity)]
+pub struct MintMemoryDatabase {
+    active_keysets: Arc<RwLock<HashMap<CurrencyUnit, Id>>>,
+    keysets: Arc<RwLock<HashMap<Id, MintKeySetInfo>>>,
+    mint_quotes: Arc<RwLock<HashMap<Uuid, MintQuote>>>,
+    melt_quotes: Arc<RwLock<HashMap<Uuid, mint::MeltQuote>>>,
+    proofs: Arc<RwLock<HashMap<[u8; 33], Proof>>>,
+    proof_state: Arc<Mutex<HashMap<[u8; 33], nut07::State>>>,
+    quote_proofs: Arc<Mutex<HashMap<Uuid, Vec<PublicKey>>>>,
+    blinded_signatures: Arc<RwLock<HashMap<[u8; 33], BlindSignature>>>,
+    quote_signatures: Arc<RwLock<HashMap<Uuid, Vec<BlindSignature>>>>,
+    melt_requests: Arc<RwLock<HashMap<Uuid, (MeltBolt11Request<Uuid>, LnKey)>>>,
+}
+
+impl MintMemoryDatabase {
+    /// Create new [`MintMemoryDatabase`]
+    #[allow(clippy::too_many_arguments)]
+    pub fn new<P: Into<Proofs>>(
+        active_keysets: HashMap<CurrencyUnit, Id>,
+        keysets: Vec<MintKeySetInfo>,
+        mint_quotes: Vec<MintQuote>,
+        melt_quotes: Vec<mint::MeltQuote>,
+        pending_proofs: P,
+        spent_proofs: P,
+        quote_proofs: HashMap<Uuid, Vec<PublicKey>>,
+        blinded_signatures: HashMap<[u8; 33], BlindSignature>,
+        quote_signatures: HashMap<Uuid, Vec<BlindSignature>>,
+        melt_request: Vec<(MeltBolt11Request<Uuid>, LnKey)>,
+    ) -> Result<Self, Error> {
+        let pending_proofs = pending_proofs.into();
+        let spent_proofs = spent_proofs.into();
+
+        let mut proofs = HashMap::new();
+        let mut proof_states = HashMap::new();
+
+        for proof in pending_proofs {
+            let y = hash_to_curve(&proof.secret.to_bytes())?.to_bytes();
+            proofs.insert(y, proof);
+            proof_states.insert(y, State::Pending);
+        }
+
+        for proof in spent_proofs {
+            let y = hash_to_curve(&proof.secret.to_bytes())?.to_bytes();
+            proofs.insert(y, proof);
+            proof_states.insert(y, State::Spent);
+        }
+
+        let melt_requests = melt_request
+            .into_iter()
+            .map(|(request, ln_key)| (request.quote, (request, ln_key)))
+            .collect();
+
+        Ok(Self {
+            active_keysets: Arc::new(RwLock::new(active_keysets)),
+            keysets: Arc::new(RwLock::new(
+                keysets.into_iter().map(|k| (k.id, k)).collect(),
+            )),
+            mint_quotes: Arc::new(RwLock::new(
+                mint_quotes.into_iter().map(|q| (q.id, q)).collect(),
+            )),
+            melt_quotes: Arc::new(RwLock::new(
+                melt_quotes.into_iter().map(|q| (q.id, q)).collect(),
+            )),
+            proofs: Arc::new(RwLock::new(proofs)),
+            proof_state: Arc::new(Mutex::new(proof_states)),
+            blinded_signatures: Arc::new(RwLock::new(blinded_signatures)),
+            quote_proofs: Arc::new(Mutex::new(quote_proofs)),
+            quote_signatures: Arc::new(RwLock::new(quote_signatures)),
+            melt_requests: Arc::new(RwLock::new(melt_requests)),
+        })
+    }
+}
+
+#[async_trait]
+impl MintDatabase for MintMemoryDatabase {
+    type Err = Error;
+
+    async fn set_active_keyset(&self, unit: CurrencyUnit, id: Id) -> Result<(), Self::Err> {
+        self.active_keysets.write().await.insert(unit, id);
+        Ok(())
+    }
+
+    async fn get_active_keyset_id(&self, unit: &CurrencyUnit) -> Result<Option<Id>, Self::Err> {
+        Ok(self.active_keysets.read().await.get(unit).cloned())
+    }
+
+    async fn get_active_keysets(&self) -> Result<HashMap<CurrencyUnit, Id>, Self::Err> {
+        Ok(self.active_keysets.read().await.clone())
+    }
+
+    async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err> {
+        self.keysets.write().await.insert(keyset.id, keyset);
+        Ok(())
+    }
+
+    async fn get_keyset_info(&self, keyset_id: &Id) -> Result<Option<MintKeySetInfo>, Self::Err> {
+        Ok(self.keysets.read().await.get(keyset_id).cloned())
+    }
+
+    async fn get_keyset_infos(&self) -> Result<Vec<MintKeySetInfo>, Self::Err> {
+        Ok(self.keysets.read().await.values().cloned().collect())
+    }
+
+    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Self::Err> {
+        self.mint_quotes.write().await.insert(quote.id, quote);
+        Ok(())
+    }
+
+    async fn get_mint_quote(&self, quote_id: &Uuid) -> Result<Option<MintQuote>, Self::Err> {
+        Ok(self.mint_quotes.read().await.get(quote_id).cloned())
+    }
+
+    async fn update_mint_quote_state(
+        &self,
+        quote_id: &Uuid,
+        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, quote.clone());
+
+        Ok(current_state)
+    }
+
+    async fn get_mint_quote_by_request_lookup_id(
+        &self,
+        request: &str,
+    ) -> Result<Option<MintQuote>, Self::Err> {
+        let quotes = self.get_mint_quotes().await?;
+
+        let quote = quotes
+            .into_iter()
+            .filter(|q| q.request_lookup_id.eq(request))
+            .collect::<Vec<MintQuote>>()
+            .first()
+            .cloned();
+
+        Ok(quote)
+    }
+    async fn get_mint_quote_by_request(
+        &self,
+        request: &str,
+    ) -> Result<Option<MintQuote>, Self::Err> {
+        let quotes = self.get_mint_quotes().await?;
+
+        let quote = quotes
+            .into_iter()
+            .filter(|q| q.request.eq(request))
+            .collect::<Vec<MintQuote>>()
+            .first()
+            .cloned();
+
+        Ok(quote)
+    }
+
+    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
+        Ok(self.mint_quotes.read().await.values().cloned().collect())
+    }
+
+    async fn remove_mint_quote(&self, quote_id: &Uuid) -> Result<(), Self::Err> {
+        self.mint_quotes.write().await.remove(quote_id);
+
+        Ok(())
+    }
+
+    async fn add_melt_quote(&self, quote: mint::MeltQuote) -> Result<(), Self::Err> {
+        self.melt_quotes.write().await.insert(quote.id, quote);
+        Ok(())
+    }
+
+    async fn get_melt_quote(&self, quote_id: &Uuid) -> Result<Option<mint::MeltQuote>, Self::Err> {
+        Ok(self.melt_quotes.read().await.get(quote_id).cloned())
+    }
+
+    async fn update_melt_quote_state(
+        &self,
+        quote_id: &Uuid,
+        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, quote.clone());
+
+        Ok(current_state)
+    }
+
+    async fn get_melt_quotes(&self) -> Result<Vec<mint::MeltQuote>, Self::Err> {
+        Ok(self.melt_quotes.read().await.values().cloned().collect())
+    }
+
+    async fn remove_melt_quote(&self, quote_id: &Uuid) -> Result<(), Self::Err> {
+        self.melt_quotes.write().await.remove(quote_id);
+
+        Ok(())
+    }
+
+    async fn add_melt_request(
+        &self,
+        melt_request: MeltBolt11Request<Uuid>,
+        ln_key: LnKey,
+    ) -> Result<(), Self::Err> {
+        let mut melt_requests = self.melt_requests.write().await;
+        melt_requests.insert(melt_request.quote, (melt_request, ln_key));
+        Ok(())
+    }
+
+    async fn get_melt_request(
+        &self,
+        quote_id: &Uuid,
+    ) -> Result<Option<(MeltBolt11Request<Uuid>, LnKey)>, Self::Err> {
+        let melt_requests = self.melt_requests.read().await;
+
+        let melt_request = melt_requests.get(quote_id);
+
+        Ok(melt_request.cloned())
+    }
+
+    async fn add_proofs<P: Into<Proofs>>(
+        &self,
+        proofs: P,
+        quote_id: Option<Uuid>,
+    ) -> Result<(), Self::Err> {
+        let proofs = proofs.into();
+        let mut db_proofs = self.proofs.write().await;
+
+        let mut ys = Vec::with_capacity(proofs.capacity());
+
+        for proof in proofs {
+            let y = hash_to_curve(&proof.secret.to_bytes())?;
+            ys.push(y);
+
+            let y = y.to_bytes();
+
+            db_proofs.insert(y, proof);
+        }
+
+        if let Some(quote_id) = quote_id {
+            let mut db_quote_proofs = self.quote_proofs.lock().await;
+
+            db_quote_proofs.insert(quote_id, ys);
+        }
+
+        Ok(())
+    }
+
+    async fn get_proofs_by_ys(
+        &self,
+        ys: &[PublicKey],
+    ) -> Result<Vec<Option<cdk_types::Proof>>, Self::Err> {
+        let spent_proofs = self.proofs.read().await;
+
+        let mut proofs = Vec::with_capacity(ys.len());
+
+        for y in ys {
+            let proof = spent_proofs.get(&y.to_bytes()).cloned();
+
+            proofs.push(proof.into());
+        }
+
+        Ok(proofs)
+    }
+
+    async fn get_proof_ys_by_quote_id(&self, quote_id: &Uuid) -> Result<Vec<PublicKey>, Self::Err> {
+        let quote_proofs = &__self.quote_proofs.lock().await;
+
+        match quote_proofs.get(quote_id) {
+            Some(ys) => Ok(ys.clone()),
+            None => Ok(vec![]),
+        }
+    }
+
+    async fn update_proofs_states(
+        &self,
+        ys: &[PublicKey],
+        proof_state: State,
+    ) -> Result<Vec<Option<State>>, Self::Err> {
+        let mut proofs_states = self.proof_state.lock().await;
+
+        let mut states = Vec::new();
+
+        for y in ys {
+            let state = proofs_states.insert(y.to_bytes(), proof_state);
+            states.push(state);
+        }
+
+        Ok(states)
+    }
+
+    async fn get_proofs_states(&self, ys: &[PublicKey]) -> Result<Vec<Option<State>>, Self::Err> {
+        let proofs_states = self.proof_state.lock().await;
+
+        let mut states = Vec::new();
+
+        for y in ys {
+            let state = proofs_states.get(&y.to_bytes()).cloned();
+            states.push(state);
+        }
+
+        Ok(states)
+    }
+
+    async fn get_proofs_by_keyset_id(
+        &self,
+        keyset_id: &Id,
+    ) -> Result<(Proofs, Vec<Option<State>>), Self::Err> {
+        let proofs = self.proofs.read().await;
+
+        let proofs_for_id: Proofs = proofs
+            .iter()
+            .filter_map(|(_, p)| match &p.keyset_id == keyset_id {
+                true => Some(p),
+                false => None,
+            })
+            .cloned()
+            .collect();
+
+        let proof_ys = proofs_for_id.ys()?;
+
+        assert_eq!(proofs_for_id.len(), proof_ys.len());
+
+        let states = self.get_proofs_states(&proof_ys).await?;
+
+        Ok((proofs_for_id, states))
+    }
+
+    async fn add_blind_signatures(
+        &self,
+        blinded_message: &[PublicKey],
+        blind_signatures: &[BlindSignature],
+        quote_id: Option<Uuid>,
+    ) -> Result<(), Self::Err> {
+        let mut current_blinded_signatures = self.blinded_signatures.write().await;
+
+        for (blinded_message, blind_signature) in blinded_message.iter().zip(blind_signatures) {
+            current_blinded_signatures.insert(blinded_message.to_bytes(), blind_signature.clone());
+        }
+
+        if let Some(quote_id) = quote_id {
+            let mut current_quote_signatures = self.quote_signatures.write().await;
+            current_quote_signatures.insert(quote_id, blind_signatures.to_vec());
+            let t = current_quote_signatures.get(&quote_id);
+            println!("after insert: {:?}", t);
+        }
+
+        Ok(())
+    }
+
+    async fn get_blind_signatures(
+        &self,
+        blinded_messages: &[PublicKey],
+    ) -> Result<Vec<Option<BlindSignature>>, Self::Err> {
+        let mut signatures = Vec::with_capacity(blinded_messages.len());
+
+        let blinded_signatures = self.blinded_signatures.read().await;
+
+        for blinded_message in blinded_messages {
+            let signature = blinded_signatures.get(&blinded_message.to_bytes()).cloned();
+
+            signatures.push(signature)
+        }
+
+        Ok(signatures)
+    }
+
+    async fn get_blind_signatures_for_keyset(
+        &self,
+        keyset_id: &Id,
+    ) -> Result<Vec<BlindSignature>, Self::Err> {
+        let blinded_signatures = self.blinded_signatures.read().await;
+
+        Ok(blinded_signatures
+            .values()
+            .filter(|b| &b.keyset_id == keyset_id)
+            .cloned()
+            .collect())
+    }
+
+    /// Get [`BlindSignature`]s for quote
+    async fn get_blind_signatures_for_quote(
+        &self,
+        quote_id: &Uuid,
+    ) -> Result<Vec<BlindSignature>, Self::Err> {
+        let ys = self.quote_signatures.read().await;
+
+        Ok(ys.get(quote_id).cloned().unwrap_or_default())
+    }
+}

+ 275 - 0
crates/cdk-types/src/cdk_database/mod.rs

@@ -0,0 +1,275 @@
+//! CDK Database
+
+use std::collections::HashMap;
+use std::fmt::Debug;
+
+use async_trait::async_trait;
+use thiserror::Error;
+use uuid::Uuid;
+
+use crate::mint;
+use crate::mint::MintKeySetInfo;
+use crate::mint::MintQuote as MintMintQuote;
+use crate::mint_url::MintUrl;
+use crate::nuts::MeltBolt11Request;
+use crate::nuts::{BlindSignature, MeltQuoteState, MintQuoteState, Proof, Proofs};
+use crate::nuts::{CurrencyUnit, Id, PublicKey, State};
+use crate::nuts::{KeySetInfo, Keys, MintInfo, SpendingConditions};
+use crate::types::LnKey;
+use crate::types::ProofInfo;
+use crate::wallet;
+use crate::wallet::MintQuote as WalletMintQuote;
+
+pub mod mint_memory;
+pub mod wallet_memory;
+
+pub use wallet_memory::WalletMemoryDatabase;
+
+/// CDK_database error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Database Error
+    #[error(transparent)]
+    Database(Box<dyn std::error::Error + Send + Sync>),
+    /// DHKE error
+    #[error(transparent)]
+    DHKE(#[from] crate::dhke::Error),
+    /// NUT00 Error
+    #[error(transparent)]
+    NUT00(#[from] crate::nuts::nut00::Error),
+    /// NUT02 Error
+    #[error(transparent)]
+    NUT02(#[from] crate::nuts::nut02::Error),
+    /// Serde Error
+    #[error(transparent)]
+    Serde(#[from] serde_json::Error),
+    /// Unknown Quote
+    #[error("Unknown Quote")]
+    UnknownQuote,
+}
+
+/// Wallet Database trait
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+pub trait WalletDatabase: Debug {
+    /// Wallet Database Error
+    type Err: Into<Error> + From<Error>;
+
+    /// Add Mint to storage
+    async fn add_mint(
+        &self,
+        mint_url: MintUrl,
+        mint_info: Option<MintInfo>,
+    ) -> Result<(), Self::Err>;
+    /// Remove Mint from storage
+    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), Self::Err>;
+    /// Get mint from storage
+    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, Self::Err>;
+    /// Get all mints from storage
+    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, Self::Err>;
+    /// Update mint url
+    async fn update_mint_url(
+        &self,
+        old_mint_url: MintUrl,
+        new_mint_url: MintUrl,
+    ) -> Result<(), Self::Err>;
+
+    /// Add mint keyset to storage
+    async fn add_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+        keysets: Vec<KeySetInfo>,
+    ) -> Result<(), Self::Err>;
+    /// Get mint keysets for mint url
+    async fn get_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+    ) -> Result<Option<Vec<KeySetInfo>>, Self::Err>;
+    /// Get mint keyset by id
+    async fn get_keyset_by_id(&self, keyset_id: &Id) -> Result<Option<KeySetInfo>, Self::Err>;
+
+    /// Add mint quote to storage
+    async fn add_mint_quote(&self, quote: WalletMintQuote) -> Result<(), Self::Err>;
+    /// Get mint quote from storage
+    async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<WalletMintQuote>, Self::Err>;
+    /// Get mint quotes from storage
+    async fn get_mint_quotes(&self) -> Result<Vec<WalletMintQuote>, Self::Err>;
+    /// Remove mint quote from storage
+    async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
+
+    /// Add melt quote to storage
+    async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), Self::Err>;
+    /// Get melt quote from storage
+    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<wallet::MeltQuote>, Self::Err>;
+    /// Remove melt quote from storage
+    async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
+
+    /// Add [`Keys`] to storage
+    async fn add_keys(&self, keys: Keys) -> Result<(), Self::Err>;
+    /// Get [`Keys`] from storage
+    async fn get_keys(&self, id: &Id) -> Result<Option<Keys>, Self::Err>;
+    /// Remove [`Keys`] from storage
+    async fn remove_keys(&self, id: &Id) -> Result<(), Self::Err>;
+
+    /// Update the proofs in storage by adding new proofs or removing proofs by
+    /// their Y value.
+    async fn update_proofs(
+        &self,
+        added: Vec<ProofInfo>,
+        removed_ys: Vec<PublicKey>,
+    ) -> Result<(), Self::Err>;
+    /// Set proofs as pending in storage. Proofs are identified by their Y
+    /// value.
+    async fn set_pending_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err>;
+    /// Reserve proofs in storage. Proofs are identified by their Y value.
+    async fn reserve_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err>;
+    /// Set proofs as unspent in storage. Proofs are identified by their Y
+    /// value.
+    async fn set_unspent_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Self::Err>;
+    /// Get proofs from storage
+    async fn get_proofs(
+        &self,
+        mint_url: Option<MintUrl>,
+        unit: Option<CurrencyUnit>,
+        state: Option<Vec<State>>,
+        spending_conditions: Option<Vec<SpendingConditions>>,
+    ) -> Result<Vec<ProofInfo>, Self::Err>;
+
+    /// Increment Keyset counter
+    async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err>;
+    /// Get current Keyset counter
+    async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u32>, Self::Err>;
+
+    /// Get when nostr key was last checked
+    async fn get_nostr_last_checked(
+        &self,
+        verifying_key: &PublicKey,
+    ) -> Result<Option<u32>, Self::Err>;
+    /// Update last checked time
+    async fn add_nostr_last_checked(
+        &self,
+        verifying_key: PublicKey,
+        last_checked: u32,
+    ) -> Result<(), Self::Err>;
+}
+
+/// Mint Database trait
+#[async_trait]
+pub trait MintDatabase {
+    /// Mint Database Error
+    type Err: Into<Error> + From<Error>;
+
+    /// Add Active Keyset
+    async fn set_active_keyset(&self, unit: CurrencyUnit, id: Id) -> Result<(), Self::Err>;
+    /// Get Active Keyset
+    async fn get_active_keyset_id(&self, unit: &CurrencyUnit) -> Result<Option<Id>, Self::Err>;
+    /// Get all Active Keyset
+    async fn get_active_keysets(&self) -> Result<HashMap<CurrencyUnit, Id>, Self::Err>;
+
+    /// Add [`MintMintQuote`]
+    async fn add_mint_quote(&self, quote: MintMintQuote) -> Result<(), Self::Err>;
+    /// Get [`MintMintQuote`]
+    async fn get_mint_quote(&self, quote_id: &Uuid) -> Result<Option<MintMintQuote>, Self::Err>;
+    /// Update state of [`MintMintQuote`]
+    async fn update_mint_quote_state(
+        &self,
+        quote_id: &Uuid,
+        state: MintQuoteState,
+    ) -> Result<MintQuoteState, Self::Err>;
+    /// Get all [`MintMintQuote`]s
+    async fn get_mint_quote_by_request(
+        &self,
+        request: &str,
+    ) -> Result<Option<MintMintQuote>, Self::Err>;
+    /// Get all [`MintMintQuote`]s
+    async fn get_mint_quote_by_request_lookup_id(
+        &self,
+        request_lookup_id: &str,
+    ) -> Result<Option<MintMintQuote>, Self::Err>;
+    /// Get Mint Quotes
+    async fn get_mint_quotes(&self) -> Result<Vec<MintMintQuote>, Self::Err>;
+    /// Remove [`MintMintQuote`]
+    async fn remove_mint_quote(&self, quote_id: &Uuid) -> Result<(), Self::Err>;
+
+    /// Add [`mint::MeltQuote`]
+    async fn add_melt_quote(&self, quote: mint::MeltQuote) -> Result<(), Self::Err>;
+    /// Get [`mint::MeltQuote`]
+    async fn get_melt_quote(&self, quote_id: &Uuid) -> Result<Option<mint::MeltQuote>, Self::Err>;
+    /// Update [`mint::MeltQuote`] state
+    async fn update_melt_quote_state(
+        &self,
+        quote_id: &Uuid,
+        state: MeltQuoteState,
+    ) -> Result<MeltQuoteState, Self::Err>;
+    /// Get all [`mint::MeltQuote`]s
+    async fn get_melt_quotes(&self) -> Result<Vec<mint::MeltQuote>, Self::Err>;
+    /// Remove [`mint::MeltQuote`]
+    async fn remove_melt_quote(&self, quote_id: &Uuid) -> Result<(), Self::Err>;
+
+    /// Add melt request
+    async fn add_melt_request(
+        &self,
+        melt_request: MeltBolt11Request<Uuid>,
+        ln_key: LnKey,
+    ) -> Result<(), Self::Err>;
+    /// Get melt request
+    async fn get_melt_request(
+        &self,
+        quote_id: &Uuid,
+    ) -> Result<Option<(MeltBolt11Request<Uuid>, LnKey)>, Self::Err>;
+
+    /// Add [`MintKeySetInfo`]
+    async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err>;
+    /// Get [`MintKeySetInfo`]
+    async fn get_keyset_info(&self, id: &Id) -> Result<Option<MintKeySetInfo>, Self::Err>;
+    /// Get [`MintKeySetInfo`]s
+    async fn get_keyset_infos(&self) -> Result<Vec<MintKeySetInfo>, Self::Err>;
+
+    /// Add spent [`Proofs`]
+    async fn add_proofs<P: Into<Proofs>>(
+        &self,
+        proof: P,
+        quote_id: Option<Uuid>,
+    ) -> Result<(), Self::Err>;
+
+    /// Get [`Proofs`] by ys
+    async fn get_proofs_by_ys(&self, ys: &[PublicKey]) -> Result<Vec<Option<Proof>>, Self::Err>;
+    /// Get ys by quote id
+    async fn get_proof_ys_by_quote_id(&self, quote_id: &Uuid) -> Result<Vec<PublicKey>, Self::Err>;
+    /// Get [`Proofs`] state
+    async fn get_proofs_states(&self, ys: &[PublicKey]) -> Result<Vec<Option<State>>, Self::Err>;
+    /// Get [`Proofs`] state
+    async fn update_proofs_states(
+        &self,
+        ys: &[PublicKey],
+        proofs_state: State,
+    ) -> Result<Vec<Option<State>>, Self::Err>;
+    /// Get [`Proofs`] by state
+    async fn get_proofs_by_keyset_id(
+        &self,
+        keyset_id: &Id,
+    ) -> Result<(Proofs, Vec<Option<State>>), Self::Err>;
+
+    /// Add [`BlindSignature`]
+    async fn add_blind_signatures(
+        &self,
+        blinded_messages: &[PublicKey],
+        blind_signatures: &[BlindSignature],
+        quote_id: Option<Uuid>,
+    ) -> Result<(), Self::Err>;
+    /// Get [`BlindSignature`]s
+    async fn get_blind_signatures(
+        &self,
+        blinded_messages: &[PublicKey],
+    ) -> Result<Vec<Option<BlindSignature>>, Self::Err>;
+    /// Get [`BlindSignature`]s for keyset_id
+    async fn get_blind_signatures_for_keyset(
+        &self,
+        keyset_id: &Id,
+    ) -> Result<Vec<BlindSignature>, Self::Err>;
+    /// Get [`BlindSignature`]s for quote
+    async fn get_blind_signatures_for_quote(
+        &self,
+        quote_id: &Uuid,
+    ) -> Result<Vec<BlindSignature>, Self::Err>;
+}

+ 358 - 0
crates/cdk-types/src/cdk_database/wallet_memory.rs

@@ -0,0 +1,358 @@
+//! Wallet in memory database
+
+use std::collections::{HashMap, HashSet};
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use tokio::sync::RwLock;
+
+use super::WalletDatabase;
+use crate::cdk_database::Error;
+use crate::mint_url::MintUrl;
+use crate::nuts::{
+    CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions, State,
+};
+use crate::types::ProofInfo;
+use crate::util::unix_time;
+use crate::wallet;
+use crate::wallet::types::MintQuote;
+
+/// Wallet in Memory Database
+#[derive(Debug, Clone, Default)]
+pub struct WalletMemoryDatabase {
+    mints: Arc<RwLock<HashMap<MintUrl, Option<MintInfo>>>>,
+    mint_keysets: Arc<RwLock<HashMap<MintUrl, HashSet<Id>>>>,
+    keysets: Arc<RwLock<HashMap<Id, KeySetInfo>>>,
+    mint_quotes: Arc<RwLock<HashMap<String, MintQuote>>>,
+    melt_quotes: Arc<RwLock<HashMap<String, wallet::MeltQuote>>>,
+    mint_keys: Arc<RwLock<HashMap<Id, Keys>>>,
+    proofs: Arc<RwLock<HashMap<PublicKey, ProofInfo>>>,
+    keyset_counter: Arc<RwLock<HashMap<Id, u32>>>,
+    nostr_last_checked: Arc<RwLock<HashMap<PublicKey, u32>>>,
+}
+
+impl WalletMemoryDatabase {
+    /// Create new [`WalletMemoryDatabase`]
+    pub fn new(
+        mint_quotes: Vec<MintQuote>,
+        melt_quotes: Vec<wallet::MeltQuote>,
+        mint_keys: Vec<Keys>,
+        keyset_counter: HashMap<Id, u32>,
+        nostr_last_checked: HashMap<PublicKey, u32>,
+    ) -> Self {
+        Self {
+            mints: Arc::new(RwLock::new(HashMap::new())),
+            mint_keysets: Arc::new(RwLock::new(HashMap::new())),
+            keysets: Arc::new(RwLock::new(HashMap::new())),
+            mint_quotes: Arc::new(RwLock::new(
+                mint_quotes.into_iter().map(|q| (q.id.clone(), q)).collect(),
+            )),
+            melt_quotes: Arc::new(RwLock::new(
+                melt_quotes.into_iter().map(|q| (q.id.clone(), q)).collect(),
+            )),
+            mint_keys: Arc::new(RwLock::new(
+                mint_keys.into_iter().map(|k| (Id::from(&k), k)).collect(),
+            )),
+            proofs: Arc::new(RwLock::new(HashMap::new())),
+            keyset_counter: Arc::new(RwLock::new(keyset_counter)),
+            nostr_last_checked: Arc::new(RwLock::new(nostr_last_checked)),
+        }
+    }
+}
+
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+impl WalletDatabase for WalletMemoryDatabase {
+    type Err = Error;
+
+    async fn add_mint(
+        &self,
+        mint_url: MintUrl,
+        mint_info: Option<MintInfo>,
+    ) -> Result<(), Self::Err> {
+        self.mints.write().await.insert(mint_url, mint_info);
+        Ok(())
+    }
+
+    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), Self::Err> {
+        let mut mints = self.mints.write().await;
+        mints.remove(&mint_url);
+
+        Ok(())
+    }
+
+    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, Self::Err> {
+        Ok(self.mints.read().await.get(&mint_url).cloned().flatten())
+    }
+
+    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, Error> {
+        Ok(self.mints.read().await.clone())
+    }
+
+    async fn update_mint_url(
+        &self,
+        old_mint_url: MintUrl,
+        new_mint_url: MintUrl,
+    ) -> Result<(), Self::Err> {
+        let proofs = self
+            .get_proofs(Some(old_mint_url), None, None, None)
+            .await
+            .map_err(Error::from)?;
+
+        // Update proofs
+        {
+            let updated_proofs: Vec<ProofInfo> = proofs
+                .into_iter()
+                .map(|mut p| {
+                    p.mint_url = new_mint_url.clone();
+                    p
+                })
+                .collect();
+
+            self.update_proofs(updated_proofs, vec![]).await?;
+        }
+
+        // Update mint quotes
+        {
+            let quotes = self.get_mint_quotes().await?;
+
+            let unix_time = unix_time();
+
+            let quotes: Vec<MintQuote> = quotes
+                .into_iter()
+                .filter_map(|mut q| {
+                    if q.expiry < unix_time {
+                        q.mint_url = new_mint_url.clone();
+                        Some(q)
+                    } else {
+                        None
+                    }
+                })
+                .collect();
+
+            for quote in quotes {
+                self.add_mint_quote(quote).await?;
+            }
+        }
+
+        Ok(())
+    }
+
+    async fn add_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+        keysets: Vec<KeySetInfo>,
+    ) -> Result<(), Error> {
+        let mut current_mint_keysets = self.mint_keysets.write().await;
+        let mut current_keysets = self.keysets.write().await;
+
+        for keyset in keysets {
+            current_mint_keysets
+                .entry(mint_url.clone())
+                .and_modify(|ks| {
+                    ks.insert(keyset.id);
+                })
+                .or_insert(HashSet::from_iter(vec![keyset.id]));
+
+            current_keysets.insert(keyset.id, keyset);
+        }
+
+        Ok(())
+    }
+
+    async fn get_mint_keysets(&self, mint_url: MintUrl) -> Result<Option<Vec<KeySetInfo>>, Error> {
+        match self.mint_keysets.read().await.get(&mint_url) {
+            Some(keyset_ids) => {
+                let mut keysets = vec![];
+
+                let db_keysets = self.keysets.read().await;
+
+                for id in keyset_ids {
+                    if let Some(keyset) = db_keysets.get(id) {
+                        keysets.push(keyset.clone());
+                    }
+                }
+
+                Ok(Some(keysets))
+            }
+            None => Ok(None),
+        }
+    }
+
+    async fn get_keyset_by_id(&self, keyset_id: &Id) -> Result<Option<KeySetInfo>, Error> {
+        Ok(self.keysets.read().await.get(keyset_id).cloned())
+    }
+
+    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Error> {
+        self.mint_quotes
+            .write()
+            .await
+            .insert(quote.id.clone(), quote);
+        Ok(())
+    }
+
+    async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, Error> {
+        Ok(self.mint_quotes.read().await.get(quote_id).cloned())
+    }
+
+    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
+        let quotes = self.mint_quotes.read().await;
+        Ok(quotes.values().cloned().collect())
+    }
+
+    async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Error> {
+        self.mint_quotes.write().await.remove(quote_id);
+
+        Ok(())
+    }
+
+    async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), Error> {
+        self.melt_quotes
+            .write()
+            .await
+            .insert(quote.id.clone(), quote);
+        Ok(())
+    }
+
+    async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<wallet::MeltQuote>, Error> {
+        Ok(self.melt_quotes.read().await.get(quote_id).cloned())
+    }
+
+    async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Error> {
+        self.melt_quotes.write().await.remove(quote_id);
+
+        Ok(())
+    }
+
+    async fn add_keys(&self, keys: Keys) -> Result<(), Error> {
+        self.mint_keys.write().await.insert(Id::from(&keys), keys);
+        Ok(())
+    }
+
+    async fn get_keys(&self, id: &Id) -> Result<Option<Keys>, Error> {
+        Ok(self.mint_keys.read().await.get(id).cloned())
+    }
+
+    async fn remove_keys(&self, id: &Id) -> Result<(), Error> {
+        self.mint_keys.write().await.remove(id);
+        Ok(())
+    }
+
+    async fn update_proofs(
+        &self,
+        added: Vec<ProofInfo>,
+        removed_ys: Vec<PublicKey>,
+    ) -> Result<(), Error> {
+        let mut all_proofs = self.proofs.write().await;
+
+        for proof_info in added.into_iter() {
+            all_proofs.insert(proof_info.y, proof_info);
+        }
+
+        for y in removed_ys.into_iter() {
+            all_proofs.remove(&y);
+        }
+
+        Ok(())
+    }
+
+    async fn set_pending_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Error> {
+        let mut all_proofs = self.proofs.write().await;
+
+        for y in ys.into_iter() {
+            if let Some(proof_info) = all_proofs.get_mut(&y) {
+                proof_info.state = State::Pending;
+            }
+        }
+
+        Ok(())
+    }
+
+    async fn reserve_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Error> {
+        let mut all_proofs = self.proofs.write().await;
+
+        for y in ys.into_iter() {
+            if let Some(proof_info) = all_proofs.get_mut(&y) {
+                proof_info.state = State::Reserved;
+            }
+        }
+
+        Ok(())
+    }
+
+    async fn set_unspent_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Error> {
+        let mut all_proofs = self.proofs.write().await;
+
+        for y in ys.into_iter() {
+            if let Some(proof_info) = all_proofs.get_mut(&y) {
+                proof_info.state = State::Unspent;
+            }
+        }
+
+        Ok(())
+    }
+
+    async fn get_proofs(
+        &self,
+        mint_url: Option<MintUrl>,
+        unit: Option<CurrencyUnit>,
+        state: Option<Vec<State>>,
+        spending_conditions: Option<Vec<SpendingConditions>>,
+    ) -> Result<Vec<ProofInfo>, Error> {
+        let proofs = self.proofs.read().await;
+
+        let proofs: Vec<ProofInfo> = proofs
+            .clone()
+            .into_values()
+            .filter_map(|proof_info| {
+                match proof_info.matches_conditions(&mint_url, &unit, &state, &spending_conditions)
+                {
+                    true => Some(proof_info),
+                    false => None,
+                }
+            })
+            .collect();
+
+        Ok(proofs)
+    }
+
+    async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Error> {
+        let keyset_counter = self.keyset_counter.read().await;
+        let current_counter = keyset_counter.get(keyset_id).cloned().unwrap_or(0);
+        drop(keyset_counter);
+
+        self.keyset_counter
+            .write()
+            .await
+            .insert(*keyset_id, current_counter + count);
+        Ok(())
+    }
+
+    async fn get_keyset_counter(&self, id: &Id) -> Result<Option<u32>, Error> {
+        Ok(self.keyset_counter.read().await.get(id).cloned())
+    }
+
+    async fn get_nostr_last_checked(
+        &self,
+        verifying_key: &PublicKey,
+    ) -> Result<Option<u32>, Self::Err> {
+        Ok(self
+            .nostr_last_checked
+            .read()
+            .await
+            .get(verifying_key)
+            .cloned())
+    }
+    async fn add_nostr_last_checked(
+        &self,
+        verifying_key: PublicKey,
+        last_checked: u32,
+    ) -> Result<(), Self::Err> {
+        self.nostr_last_checked
+            .write()
+            .await
+            .insert(verifying_key, last_checked);
+
+        Ok(())
+    }
+}

+ 21 - 0
crates/cdk-types/src/dhke.rs

@@ -0,0 +1,21 @@
+//! Diffie-Hellmann key exchange
+
+use thiserror::Error;
+
+/// NUT00 Error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Token could not be validated
+    #[error("Token not verified")]
+    TokenNotVerified,
+    /// No valid point on curve
+    #[error("No valid point found")]
+    NoValidPoint,
+    /// Secp256k1 error
+    #[error(transparent)]
+    Secp256k1(#[from] bitcoin::secp256k1::Error),
+    // TODO: Remove use anyhow
+    /// Custom Error
+    #[error("`{0}`")]
+    Custom(String),
+}

+ 124 - 0
crates/cdk-types/src/hex.rs

@@ -0,0 +1,124 @@
+// Copyright (c) 2022-2023 Yuki Kishimoto
+// Distributed under the MIT software license
+
+//! Hex
+
+use core::fmt;
+
+/// Hex error
+#[derive(Debug, PartialEq, Eq)]
+pub enum Error {
+    /// An invalid character was found
+    InvalidHexCharacter {
+        /// Char
+        c: char,
+        /// Char index
+        index: usize,
+    },
+    /// A hex string's length needs to be even, as two digits correspond to
+    /// one byte.
+    OddLength,
+}
+
+impl std::error::Error for Error {}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::InvalidHexCharacter { c, index } => {
+                write!(f, "Invalid character {} at position {}", c, index)
+            }
+            Self::OddLength => write!(f, "Odd number of digits"),
+        }
+    }
+}
+
+#[inline]
+fn from_digit(num: u8) -> char {
+    if num < 10 {
+        (b'0' + num) as char
+    } else {
+        (b'a' + num - 10) as char
+    }
+}
+
+/// Hex encode
+pub fn encode<T>(data: T) -> String
+where
+    T: AsRef<[u8]>,
+{
+    let bytes: &[u8] = data.as_ref();
+    let mut hex: String = String::with_capacity(2 * bytes.len());
+    for byte in bytes.iter() {
+        hex.push(from_digit(byte >> 4));
+        hex.push(from_digit(byte & 0xF));
+    }
+    hex
+}
+
+const fn val(c: u8, idx: usize) -> Result<u8, Error> {
+    match c {
+        b'A'..=b'F' => Ok(c - b'A' + 10),
+        b'a'..=b'f' => Ok(c - b'a' + 10),
+        b'0'..=b'9' => Ok(c - b'0'),
+        _ => Err(Error::InvalidHexCharacter {
+            c: c as char,
+            index: idx,
+        }),
+    }
+}
+
+/// Hex decode
+pub fn decode<T>(hex: T) -> Result<Vec<u8>, Error>
+where
+    T: AsRef<[u8]>,
+{
+    let hex = hex.as_ref();
+    let len = hex.len();
+
+    if len % 2 != 0 {
+        return Err(Error::OddLength);
+    }
+
+    let mut bytes: Vec<u8> = Vec::with_capacity(len / 2);
+
+    for i in (0..len).step_by(2) {
+        let high = val(hex[i], i)?;
+        let low = val(hex[i + 1], i + 1)?;
+        bytes.push(high << 4 | low);
+    }
+
+    Ok(bytes)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_encode() {
+        assert_eq!(encode("foobar"), "666f6f626172");
+    }
+
+    #[test]
+    fn test_decode() {
+        assert_eq!(
+            decode("666f6f626172"),
+            Ok(String::from("foobar").into_bytes())
+        );
+    }
+
+    #[test]
+    pub fn test_invalid_length() {
+        assert_eq!(decode("1").unwrap_err(), Error::OddLength);
+        assert_eq!(decode("666f6f6261721").unwrap_err(), Error::OddLength);
+    }
+
+    #[test]
+    pub fn test_invalid_char() {
+        assert_eq!(
+            decode("66ag").unwrap_err(),
+            Error::InvalidHexCharacter { c: 'g', index: 3 }
+        );
+    }
+}

+ 30 - 0
crates/cdk-types/src/lib.rs

@@ -0,0 +1,30 @@
+//! Common types used by the CDK.
+//!
+//! This crate is meant to be used by the CDK and other crates that need to interact with the CDK,
+//! to store all the common types and traits, to avoid circular dependencies, and to make it easier
+//! to extend the CDK, or build similar crates.
+
+use bitcoin::secp256k1::{rand, All, Secp256k1};
+use once_cell::sync::Lazy;
+
+mod amount;
+pub mod cdk_database;
+pub mod dhke;
+pub mod hex;
+pub mod mint;
+pub mod mint_url;
+pub mod nuts;
+pub mod secret;
+
+/// Secp256k1 global context
+pub static SECP256K1: Lazy<Secp256k1<All>> = Lazy::new(|| {
+    let mut ctx = Secp256k1::new();
+    let mut rng = rand::thread_rng();
+    ctx.randomize(&mut rng);
+    ctx
+});
+
+#[doc(hidden)]
+pub use lightning_invoice::{self, Bolt11Invoice};
+
+pub use self::{amount::Amount, mint_url::MintUrl, nuts::*, secret::Secret};

+ 63 - 0
crates/cdk-types/src/mint.rs

@@ -0,0 +1,63 @@
+//! Mint types
+
+use crate::{
+    nuts::{CurrencyUnit, Id, MintQuoteState},
+    Amount, MintUrl, PublicKey,
+};
+use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
+use serde::{Deserialize, Serialize};
+use uuid::Uuid;
+
+/// Mint Keyset Info
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct MintKeySetInfo {
+    /// Keyset [`Id`]
+    pub id: Id,
+    /// Keyset [`CurrencyUnit`]
+    pub unit: CurrencyUnit,
+    /// Keyset active or inactive
+    /// Mint will only issue new [`BlindSignature`] on active keysets
+    pub active: bool,
+    /// Starting unix time Keyset is valid from
+    pub valid_from: u64,
+    /// When the Keyset is valid to
+    /// This is not shown to the wallet and can only be used internally
+    pub valid_to: Option<u64>,
+    /// [`DerivationPath`] keyset
+    pub derivation_path: DerivationPath,
+    /// DerivationPath index of Keyset
+    pub derivation_path_index: Option<u32>,
+    /// Max order of keyset
+    pub max_order: u8,
+    /// Input Fee ppk
+    #[serde(default = "default_fee")]
+    pub input_fee_ppk: u64,
+}
+
+/// Mint Quote Info
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct MintQuote {
+    /// Quote id
+    pub id: Uuid,
+    /// Mint Url
+    pub mint_url: MintUrl,
+    /// Amount of quote
+    pub amount: Amount,
+    /// Unit of quote
+    pub unit: CurrencyUnit,
+    /// Quote payment request e.g. bolt11
+    pub request: String,
+    /// Quote state
+    pub state: MintQuoteState,
+    /// Expiration time of quote
+    pub expiry: u64,
+    /// Value used by ln backend to look up state of request
+    pub request_lookup_id: String,
+    /// Pubkey
+    pub pubkey: Option<PublicKey>,
+}
+
+/// Default fee
+pub fn default_fee() -> u64 {
+    0
+}

+ 131 - 0
crates/cdk-types/src/mint_url.rs

@@ -0,0 +1,131 @@
+// Copyright (c) 2022-2023 Yuki Kishimoto
+// Distributed under the MIT software license
+
+//! Url
+
+use core::fmt;
+use core::str::FromStr;
+
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+use url::{ParseError, Url};
+
+/// Url Error
+#[derive(Debug, Error, PartialEq, Eq)]
+pub enum Error {
+    /// Url error
+    #[error(transparent)]
+    Url(#[from] ParseError),
+    /// Invalid URL structure
+    #[error("Invalid URL")]
+    InvalidUrl,
+}
+
+/// MintUrl Url
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+pub struct MintUrl(String);
+
+impl MintUrl {
+    fn format_url(url: &str) -> Result<String, Error> {
+        if url.is_empty() {
+            return Err(Error::InvalidUrl);
+        }
+        let url = url.trim_end_matches('/');
+        // https://URL.com/path/TO/resource -> https://url.com/path/TO/resource
+        let protocol = url
+            .split("://")
+            .nth(0)
+            .ok_or(Error::InvalidUrl)?
+            .to_lowercase();
+        let host = url
+            .split("://")
+            .nth(1)
+            .ok_or(Error::InvalidUrl)?
+            .split('/')
+            .nth(0)
+            .ok_or(Error::InvalidUrl)?
+            .to_lowercase();
+        let path = url
+            .split("://")
+            .nth(1)
+            .ok_or(Error::InvalidUrl)?
+            .split('/')
+            .skip(1)
+            .collect::<Vec<&str>>()
+            .join("/");
+        let mut formatted_url = format!("{}://{}", protocol, host);
+        if !path.is_empty() {
+            formatted_url.push_str(&format!("/{}", path));
+        }
+        Ok(formatted_url)
+    }
+
+    /// Join onto url
+    pub fn join(&self, path: &str) -> Result<Url, Error> {
+        Url::parse(&self.0)
+            .and_then(|url| url.join(path))
+            .map_err(Into::into)
+    }
+
+    /// Append path elements onto the URL
+    pub fn join_paths(&self, path_elements: &[&str]) -> Result<Url, Error> {
+        self.join(&path_elements.join("/"))
+    }
+}
+
+impl FromStr for MintUrl {
+    type Err = Error;
+
+    fn from_str(url: &str) -> Result<Self, Self::Err> {
+        let formatted_url = Self::format_url(url);
+        match formatted_url {
+            Ok(url) => Ok(Self(url)),
+            Err(_) => Err(Error::InvalidUrl),
+        }
+    }
+}
+
+impl fmt::Display for MintUrl {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.0)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+
+    use super::*;
+
+    #[test]
+    fn test_trim_trailing_slashes() {
+        let very_unformatted_url = "http://url-to-check.com////";
+        let unformatted_url = "http://url-to-check.com/";
+        let formatted_url = "http://url-to-check.com";
+
+        let very_trimmed_url = MintUrl::from_str(very_unformatted_url).unwrap();
+        assert_eq!(formatted_url, very_trimmed_url.to_string());
+
+        let trimmed_url = MintUrl::from_str(unformatted_url).unwrap();
+        assert_eq!(formatted_url, trimmed_url.to_string());
+
+        let unchanged_url = MintUrl::from_str(formatted_url).unwrap();
+        assert_eq!(formatted_url, unchanged_url.to_string());
+    }
+    #[test]
+    fn test_case_insensitive() {
+        let wrong_cased_url = "http://URL-to-check.com";
+        let correct_cased_url = "http://url-to-check.com";
+
+        let cased_url_formatted = MintUrl::from_str(wrong_cased_url).unwrap();
+        assert_eq!(correct_cased_url, cased_url_formatted.to_string());
+
+        let wrong_cased_url_with_path = "http://URL-to-check.com/PATH/to/check";
+        let correct_cased_url_with_path = "http://url-to-check.com/PATH/to/check";
+
+        let cased_url_with_path_formatted = MintUrl::from_str(wrong_cased_url_with_path).unwrap();
+        assert_eq!(
+            correct_cased_url_with_path,
+            cased_url_with_path_formatted.to_string()
+        );
+    }
+}

+ 53 - 0
crates/cdk-types/src/nuts/mod.rs

@@ -0,0 +1,53 @@
+//! Nuts
+//!
+//! See all at <https://github.com/cashubtc/nuts>
+
+pub mod nut00;
+pub mod nut01;
+pub mod nut02;
+pub mod nut03;
+pub mod nut04;
+pub mod nut05;
+pub mod nut06;
+pub mod nut07;
+pub mod nut08;
+pub mod nut09;
+pub mod nut10;
+pub mod nut11;
+pub mod nut12;
+pub mod nut13;
+pub mod nut14;
+pub mod nut15;
+pub mod nut17;
+pub mod nut18;
+pub mod nut19;
+pub mod nut20;
+
+pub use nut00::{
+    BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, PreMint, PreMintSecrets, Proof,
+    Proofs, Token, TokenV3, TokenV4, Witness,
+};
+pub use nut01::{Keys, KeysResponse, PublicKey, SecretKey};
+pub use nut02::MintKeySet;
+pub use nut02::{Id, KeySet, KeySetInfo, KeysetResponse};
+pub use nut03::PreSwap;
+pub use nut03::{SwapRequest, SwapResponse};
+pub use nut04::{
+    MintBolt11Request, MintBolt11Response, MintMethodSettings, MintQuoteBolt11Request,
+    MintQuoteBolt11Response, QuoteState as MintQuoteState, Settings as NUT04Settings,
+};
+pub use nut05::{
+    MeltBolt11Request, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
+    QuoteState as MeltQuoteState, Settings as NUT05Settings,
+};
+pub use nut06::{ContactInfo, MintInfo, MintVersion, Nuts};
+pub use nut07::{CheckStateRequest, CheckStateResponse, ProofState, State};
+pub use nut09::{RestoreRequest, RestoreResponse};
+pub use nut10::{Kind, Secret as Nut10Secret, SecretData};
+pub use nut11::{Conditions, P2PKWitness, SigFlag, SpendingConditions};
+pub use nut12::{BlindSignatureDleq, ProofDleq};
+pub use nut14::HTLCWitness;
+pub use nut15::{Mpp, MppMethodSettings, Settings as NUT15Settings};
+pub use nut17::PubSubManager;
+pub use nut17::{NotificationPayload, SupportedSettings as Nut17SupportedSettings};
+pub use nut18::{PaymentRequest, PaymentRequestPayload, Transport};

+ 449 - 0
crates/cdk-types/src/nuts/nut00/mod.rs

@@ -0,0 +1,449 @@
+//! NUT-00: Notation and Models
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/00.md>
+
+use std::cmp::Ordering;
+use std::fmt;
+use std::hash::{Hash, Hasher};
+use std::str::FromStr;
+use std::string::FromUtf8Error;
+
+use serde::{de, Deserialize, Deserializer, Serialize};
+use thiserror::Error;
+
+use crate::nuts::nut01::{PublicKey, SecretKey};
+use crate::nuts::nut11::{serde_p2pk_witness, P2PKWitness};
+use crate::nuts::nut12::BlindSignatureDleq;
+use crate::nuts::nut14::{serde_htlc_witness, HTLCWitness};
+use crate::nuts::{Id, ProofDleq};
+use crate::secret::Secret;
+use crate::Amount;
+
+pub mod token;
+pub use token::{Token, TokenV3, TokenV4};
+
+/// List of [Proof]
+pub type Proofs = Vec<Proof>;
+
+/// Utility methods for [Proofs]
+pub trait ProofsMethods {
+    /// Try to sum up the amounts of all [Proof]s
+    fn total_amount(&self) -> Result<Amount, Error>;
+
+    /// Try to fetch the pubkeys of all [Proof]s
+    fn ys(&self) -> Result<Vec<PublicKey>, Error>;
+}
+
+impl ProofsMethods for Proofs {
+    fn total_amount(&self) -> Result<Amount, Error> {
+        Amount::try_sum(self.iter().map(|p| p.amount)).map_err(Into::into)
+    }
+
+    fn ys(&self) -> Result<Vec<PublicKey>, Error> {
+        self.iter()
+            .map(|p| p.y())
+            .collect::<Result<Vec<PublicKey>, _>>()
+            .map_err(Into::into)
+    }
+}
+
+/// NUT00 Error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Proofs required
+    #[error("Proofs required in token")]
+    ProofsRequired,
+    /// Unsupported token
+    #[error("Unsupported token")]
+    UnsupportedToken,
+    /// Unsupported token
+    #[error("Unsupported unit")]
+    UnsupportedUnit,
+    /// Unsupported token
+    #[error("Unsupported payment method")]
+    UnsupportedPaymentMethod,
+    /// Serde Json error
+    #[error(transparent)]
+    SerdeJsonError(#[from] serde_json::Error),
+    /// Utf8 parse error
+    #[error(transparent)]
+    Utf8ParseError(#[from] FromUtf8Error),
+    /// Base64 error
+    #[error(transparent)]
+    Base64Error(#[from] bitcoin::base64::DecodeError),
+    /// Ciborium deserialization error
+    #[error(transparent)]
+    CiboriumError(#[from] ciborium::de::Error<std::io::Error>),
+    /// Ciborium serialization error
+    #[error(transparent)]
+    CiboriumSerError(#[from] ciborium::ser::Error<std::io::Error>),
+    /// Amount Error
+    #[error(transparent)]
+    Amount(#[from] crate::amount::Error),
+    /// Secret error
+    #[error(transparent)]
+    Secret(#[from] crate::secret::Error),
+    /// DHKE error
+    #[error(transparent)]
+    DHKE(#[from] crate::dhke::Error),
+    /// NUT10 error
+    #[error(transparent)]
+    NUT10(#[from] crate::nuts::nut10::Error),
+    /// NUT11 error
+    #[error(transparent)]
+    NUT11(#[from] crate::nuts::nut11::Error),
+}
+
+/// Blinded Message (also called `output`)
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct BlindedMessage {
+    /// Amount
+    ///
+    /// The value for the requested [BlindSignature]
+    pub amount: Amount,
+    /// Keyset ID
+    ///
+    /// ID from which we expect a signature.
+    #[serde(rename = "id")]
+    pub keyset_id: Id,
+    /// Blinded secret message (B_)
+    ///
+    /// The blinded secret message generated by the sender.
+    #[serde(rename = "B_")]
+    #[cfg_attr(feature = "swagger", schema(value_type = String))]
+    pub blinded_secret: PublicKey,
+    /// Witness
+    ///
+    /// <https://github.com/cashubtc/nuts/blob/main/11.md>
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub witness: Option<Witness>,
+}
+
+/// Blind Signature (also called `promise`)
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct BlindSignature {
+    /// Amount
+    ///
+    /// The value of the blinded token.
+    pub amount: Amount,
+    /// Keyset ID
+    ///
+    /// ID of the mint keys that signed the token.
+    #[serde(rename = "id")]
+    pub keyset_id: Id,
+    /// Blinded signature (C_)
+    ///
+    /// The blinded signature on the secret message `B_` of [BlindedMessage].
+    #[serde(rename = "C_")]
+    #[cfg_attr(feature = "swagger", schema(value_type = String))]
+    pub c: PublicKey,
+    /// DLEQ Proof
+    ///
+    /// <https://github.com/cashubtc/nuts/blob/main/12.md>
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub dleq: Option<BlindSignatureDleq>,
+}
+
+impl Ord for BlindSignature {
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.amount.cmp(&other.amount)
+    }
+}
+
+impl PartialOrd for BlindSignature {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+/// Witness
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[serde(untagged)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub enum Witness {
+    /// P2PK Witness
+    #[serde(with = "serde_p2pk_witness")]
+    P2PKWitness(P2PKWitness),
+    /// HTLC Witness
+    #[serde(with = "serde_htlc_witness")]
+    HTLCWitness(HTLCWitness),
+}
+
+/// Proofs
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct Proof {
+    /// Amount
+    pub amount: Amount,
+    /// `Keyset id`
+    #[serde(rename = "id")]
+    pub keyset_id: Id,
+    /// Secret message
+    #[cfg_attr(feature = "swagger", schema(value_type = String))]
+    pub secret: Secret,
+    /// Unblinded signature
+    #[serde(rename = "C")]
+    #[cfg_attr(feature = "swagger", schema(value_type = String))]
+    pub c: PublicKey,
+    /// Witness
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub witness: Option<Witness>,
+    /// DLEQ Proof
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub dleq: Option<ProofDleq>,
+}
+
+impl Hash for Proof {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.secret.hash(state);
+    }
+}
+
+impl Ord for Proof {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.amount.cmp(&other.amount)
+    }
+}
+
+impl PartialOrd for Proof {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+/// Proof V4
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct ProofV4 {
+    /// Amount in satoshi
+    #[serde(rename = "a")]
+    pub amount: Amount,
+    /// Secret message
+    #[serde(rename = "s")]
+    pub secret: Secret,
+    /// Unblinded signature
+    #[serde(
+        serialize_with = "serialize_v4_pubkey",
+        deserialize_with = "deserialize_v4_pubkey"
+    )]
+    pub c: PublicKey,
+    /// Witness
+    #[serde(default)]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub witness: Option<Witness>,
+    /// DLEQ Proof
+    #[serde(rename = "d")]
+    pub dleq: Option<ProofDleq>,
+}
+
+impl ProofV4 {
+    /// [`ProofV4`] into [`Proof`]
+    pub fn into_proof(&self, keyset_id: &Id) -> Proof {
+        Proof {
+            amount: self.amount,
+            keyset_id: *keyset_id,
+            secret: self.secret.clone(),
+            c: self.c,
+            witness: self.witness.clone(),
+            dleq: self.dleq.clone(),
+        }
+    }
+}
+
+impl From<Proof> for ProofV4 {
+    fn from(proof: Proof) -> ProofV4 {
+        let Proof {
+            amount,
+            keyset_id: _,
+            secret,
+            c,
+            witness,
+            dleq,
+        } = proof;
+        ProofV4 {
+            amount,
+            secret,
+            c,
+            witness,
+            dleq,
+        }
+    }
+}
+
+fn serialize_v4_pubkey<S>(key: &PublicKey, serializer: S) -> Result<S::Ok, S::Error>
+where
+    S: serde::Serializer,
+{
+    serializer.serialize_bytes(&key.to_bytes())
+}
+
+fn deserialize_v4_pubkey<'de, D>(deserializer: D) -> Result<PublicKey, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    let bytes = Vec::<u8>::deserialize(deserializer)?;
+    PublicKey::from_slice(&bytes).map_err(serde::de::Error::custom)
+}
+
+/// Currency Unit
+#[non_exhaustive]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub enum CurrencyUnit {
+    /// Sat
+    #[default]
+    Sat,
+    /// Msat
+    Msat,
+    /// Usd
+    Usd,
+    /// Euro
+    Eur,
+    /// Custom currency unit
+    Custom(String),
+}
+
+impl CurrencyUnit {
+    /// Derivation index mint will use for unit
+    pub fn derivation_index(&self) -> Option<u32> {
+        match self {
+            Self::Sat => Some(0),
+            Self::Msat => Some(1),
+            Self::Usd => Some(2),
+            Self::Eur => Some(3),
+            _ => None,
+        }
+    }
+}
+
+impl FromStr for CurrencyUnit {
+    type Err = Error;
+    fn from_str(value: &str) -> Result<Self, Self::Err> {
+        let value = &value.to_uppercase();
+        match value.as_str() {
+            "SAT" => Ok(Self::Sat),
+            "MSAT" => Ok(Self::Msat),
+            "USD" => Ok(Self::Usd),
+            "EUR" => Ok(Self::Eur),
+            c => Ok(Self::Custom(c.to_string())),
+        }
+    }
+}
+
+impl fmt::Display for CurrencyUnit {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let s = match self {
+            CurrencyUnit::Sat => "SAT",
+            CurrencyUnit::Msat => "MSAT",
+            CurrencyUnit::Usd => "USD",
+            CurrencyUnit::Eur => "EUR",
+            CurrencyUnit::Custom(unit) => unit,
+        };
+        if let Some(width) = f.width() {
+            write!(f, "{:width$}", s.to_lowercase(), width = width)
+        } else {
+            write!(f, "{}", s.to_lowercase())
+        }
+    }
+}
+
+impl Serialize for CurrencyUnit {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        serializer.serialize_str(&self.to_string())
+    }
+}
+
+impl<'de> Deserialize<'de> for CurrencyUnit {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let currency: String = String::deserialize(deserializer)?;
+        Self::from_str(&currency).map_err(|_| serde::de::Error::custom("Unsupported unit"))
+    }
+}
+
+/// Payment Method
+#[non_exhaustive]
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub enum PaymentMethod {
+    /// Bolt11 payment type
+    #[default]
+    Bolt11,
+}
+
+impl FromStr for PaymentMethod {
+    type Err = Error;
+    fn from_str(value: &str) -> Result<Self, Self::Err> {
+        match value {
+            "bolt11" => Ok(Self::Bolt11),
+            _ => Err(Error::UnsupportedPaymentMethod),
+        }
+    }
+}
+
+impl fmt::Display for PaymentMethod {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            PaymentMethod::Bolt11 => write!(f, "bolt11"),
+        }
+    }
+}
+
+impl Serialize for PaymentMethod {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        serializer.serialize_str(&self.to_string())
+    }
+}
+
+impl<'de> Deserialize<'de> for PaymentMethod {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let payment_method: String = String::deserialize(deserializer)?;
+        Self::from_str(&payment_method).map_err(|_| de::Error::custom("Unsupported payment method"))
+    }
+}
+
+/// PreMint
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+pub struct PreMint {
+    /// Blinded message
+    pub blinded_message: BlindedMessage,
+    /// Secret
+    pub secret: Secret,
+    /// R
+    pub r: SecretKey,
+    /// Amount
+    pub amount: Amount,
+}
+
+impl Ord for PreMint {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.amount.cmp(&other.amount)
+    }
+}
+
+impl PartialOrd for PreMint {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+/// Premint Secrets
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+pub struct PreMintSecrets {
+    /// Secrets
+    pub secrets: Vec<PreMint>,
+    /// Keyset Id
+    pub keyset_id: Id,
+}

+ 430 - 0
crates/cdk-types/src/nuts/nut00/token.rs

@@ -0,0 +1,430 @@
+//! Cashu Token
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/00.md>
+
+use std::collections::HashMap;
+use std::fmt;
+use std::str::FromStr;
+
+use bitcoin::base64::engine::{general_purpose, GeneralPurpose};
+use bitcoin::base64::{alphabet, Engine as _};
+use serde::{Deserialize, Serialize};
+
+use super::{Error, Proof, ProofV4, Proofs};
+use crate::mint_url::MintUrl;
+use crate::nuts::{CurrencyUnit, Id};
+
+/// Token Enum
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum Token {
+    /// Token V3
+    TokenV3(TokenV3),
+    /// Token V4
+    TokenV4(TokenV4),
+}
+
+impl fmt::Display for Token {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let token = match self {
+            Self::TokenV3(token) => token.to_string(),
+            Self::TokenV4(token) => token.to_string(),
+        };
+
+        write!(f, "{}", token)
+    }
+}
+
+impl FromStr for Token {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let (is_v3, s) = match (s.strip_prefix("cashuA"), s.strip_prefix("cashuB")) {
+            (Some(s), None) => (true, s),
+            (None, Some(s)) => (false, s),
+            _ => return Err(Error::UnsupportedToken),
+        };
+
+        let decode_config = general_purpose::GeneralPurposeConfig::new()
+            .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
+        let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
+
+        match is_v3 {
+            true => {
+                let decoded_str = String::from_utf8(decoded)?;
+                let token: TokenV3 = serde_json::from_str(&decoded_str)?;
+                Ok(Token::TokenV3(token))
+            }
+            false => {
+                let token: TokenV4 = ciborium::from_reader(&decoded[..])?;
+                Ok(Token::TokenV4(token))
+            }
+        }
+    }
+}
+
+impl TryFrom<&Vec<u8>> for Token {
+    type Error = Error;
+
+    fn try_from(bytes: &Vec<u8>) -> Result<Self, Self::Error> {
+        if bytes.len() < 5 {
+            return Err(Error::UnsupportedToken);
+        }
+
+        let prefix = String::from_utf8(bytes[..5].to_vec())?;
+
+        match prefix.as_str() {
+            "crawB" => {
+                let token: TokenV4 = ciborium::from_reader(&bytes[5..])?;
+                Ok(Token::TokenV4(token))
+            }
+            _ => Err(Error::UnsupportedToken),
+        }
+    }
+}
+
+/// Token V3 Token
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct TokenV3Token {
+    /// Url of mint
+    pub mint: MintUrl,
+    /// [`Proofs`]
+    pub proofs: Proofs,
+}
+
+impl TokenV3Token {
+    /// Create new [`TokenV3Token`]
+    pub fn new(mint_url: MintUrl, proofs: Proofs) -> Self {
+        Self {
+            mint: mint_url,
+            proofs,
+        }
+    }
+}
+
+/// Token
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct TokenV3 {
+    /// Proofs in [`Token`] by mint
+    pub token: Vec<TokenV3Token>,
+    /// Memo for token
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub memo: Option<String>,
+    /// Token Unit
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub unit: Option<CurrencyUnit>,
+}
+
+impl FromStr for TokenV3 {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let s = s.strip_prefix("cashuA").ok_or(Error::UnsupportedToken)?;
+
+        let decode_config = general_purpose::GeneralPurposeConfig::new()
+            .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
+        let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
+        let decoded_str = String::from_utf8(decoded)?;
+        let token: TokenV3 = serde_json::from_str(&decoded_str)?;
+        Ok(token)
+    }
+}
+
+impl fmt::Display for TokenV3 {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let json_string = serde_json::to_string(self).map_err(|_| fmt::Error)?;
+        let encoded = general_purpose::URL_SAFE.encode(json_string);
+        write!(f, "cashuA{}", encoded)
+    }
+}
+
+impl From<TokenV4> for TokenV3 {
+    fn from(token: TokenV4) -> Self {
+        let proofs = token.proofs();
+
+        TokenV3 {
+            token: vec![TokenV3Token::new(token.mint_url, proofs)],
+            memo: token.memo,
+            unit: Some(token.unit),
+        }
+    }
+}
+
+/// Token V4
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct TokenV4 {
+    /// Mint Url
+    #[serde(rename = "m")]
+    pub mint_url: MintUrl,
+    /// Token Unit
+    #[serde(rename = "u")]
+    pub unit: CurrencyUnit,
+    /// Memo for token
+    #[serde(rename = "d", skip_serializing_if = "Option::is_none")]
+    pub memo: Option<String>,
+    /// Proofs grouped by keyset_id
+    #[serde(rename = "t")]
+    pub token: Vec<TokenV4Token>,
+}
+
+impl fmt::Display for TokenV4 {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        use serde::ser::Error;
+        let mut data = Vec::new();
+        ciborium::into_writer(self, &mut data).map_err(|e| fmt::Error::custom(e.to_string()))?;
+        let encoded = general_purpose::URL_SAFE.encode(data);
+        write!(f, "cashuB{}", encoded)
+    }
+}
+
+impl FromStr for TokenV4 {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let s = s.strip_prefix("cashuB").ok_or(Error::UnsupportedToken)?;
+
+        let decode_config = general_purpose::GeneralPurposeConfig::new()
+            .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
+        let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
+        let token: TokenV4 = ciborium::from_reader(&decoded[..])?;
+        Ok(token)
+    }
+}
+
+impl TryFrom<&Vec<u8>> for TokenV4 {
+    type Error = Error;
+
+    fn try_from(bytes: &Vec<u8>) -> Result<Self, Self::Error> {
+        if bytes.len() < 5 {
+            return Err(Error::UnsupportedToken);
+        }
+
+        let prefix = String::from_utf8(bytes[..5].to_vec())?;
+
+        if prefix.as_str() == "crawB" {
+            let token: TokenV4 = ciborium::from_reader(&bytes[5..])?;
+            Ok(token)
+        } else {
+            Err(Error::UnsupportedToken)
+        }
+    }
+}
+
+impl TryFrom<TokenV3> for TokenV4 {
+    type Error = Error;
+    fn try_from(token: TokenV3) -> Result<Self, Self::Error> {
+        let proofs = token.proofs();
+        let mint_urls = token.mint_urls();
+
+        if mint_urls.len() != 1 {
+            return Err(Error::UnsupportedToken);
+        }
+
+        let mint_url = mint_urls.first().expect("Len is checked");
+
+        let proofs = proofs
+            .iter()
+            .fold(HashMap::new(), |mut acc, val| {
+                acc.entry(val.keyset_id)
+                    .and_modify(|p: &mut Vec<Proof>| p.push(val.clone()))
+                    .or_insert(vec![val.clone()]);
+                acc
+            })
+            .into_iter()
+            .map(|(id, proofs)| TokenV4Token::new(id, proofs))
+            .collect();
+
+        Ok(TokenV4 {
+            mint_url: mint_url.clone(),
+            token: proofs,
+            memo: token.memo,
+            unit: token.unit.ok_or(Error::UnsupportedUnit)?,
+        })
+    }
+}
+
+/// Token V4 Token
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct TokenV4Token {
+    /// `Keyset id`
+    #[serde(
+        rename = "i",
+        serialize_with = "serialize_v4_keyset_id",
+        deserialize_with = "deserialize_v4_keyset_id"
+    )]
+    pub keyset_id: Id,
+    /// Proofs
+    #[serde(rename = "p")]
+    pub proofs: Vec<ProofV4>,
+}
+
+fn serialize_v4_keyset_id<S>(keyset_id: &Id, serializer: S) -> Result<S::Ok, S::Error>
+where
+    S: serde::Serializer,
+{
+    serializer.serialize_bytes(&keyset_id.to_bytes())
+}
+
+fn deserialize_v4_keyset_id<'de, D>(deserializer: D) -> Result<Id, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    let bytes = Vec::<u8>::deserialize(deserializer)?;
+    Id::from_bytes(&bytes).map_err(serde::de::Error::custom)
+}
+
+impl TokenV4Token {
+    /// Create new [`TokenV4Token`]
+    pub fn new(keyset_id: Id, proofs: Proofs) -> Self {
+        Self {
+            keyset_id,
+            proofs: proofs.into_iter().map(|p| p.into()).collect(),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::str::FromStr;
+
+    use super::*;
+    use crate::hex;
+    use crate::mint_url::MintUrl;
+
+    #[test]
+    fn test_token_padding() {
+        let token_str_with_padding = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91IHZlcnkgbXVjaC4ifQ==";
+
+        let token = TokenV3::from_str(token_str_with_padding).unwrap();
+
+        let token_str_without_padding = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91IHZlcnkgbXVjaC4ifQ";
+
+        let token_without = TokenV3::from_str(token_str_without_padding).unwrap();
+
+        assert_eq!(token, token_without);
+    }
+
+    #[test]
+    fn test_token_v4_str_round_trip() {
+        let token_str = "cashuBpGF0gaJhaUgArSaMTR9YJmFwgaNhYQFhc3hAOWE2ZGJiODQ3YmQyMzJiYTc2ZGIwZGYxOTcyMTZiMjlkM2I4Y2MxNDU1M2NkMjc4MjdmYzFjYzk0MmZlZGI0ZWFjWCEDhhhUP_trhpXfStS6vN6So0qWvc2X3O4NfM-Y1HISZ5JhZGlUaGFuayB5b3VhbXVodHRwOi8vbG9jYWxob3N0OjMzMzhhdWNzYXQ=";
+        let token = TokenV4::from_str(token_str).unwrap();
+
+        assert_eq!(
+            token.mint_url,
+            MintUrl::from_str("http://localhost:3338").unwrap()
+        );
+        assert_eq!(
+            token.token[0].keyset_id,
+            Id::from_str("00ad268c4d1f5826").unwrap()
+        );
+
+        let encoded = &token.to_string();
+
+        let token_data = TokenV4::from_str(encoded).unwrap();
+
+        assert_eq!(token_data, token);
+    }
+
+    #[test]
+    fn test_token_v4_multi_keyset() {
+        let token_str_multi_keysets = "cashuBo2F0gqJhaUgA_9SLj17PgGFwgaNhYQFhc3hAYWNjMTI0MzVlN2I4NDg0YzNjZjE4NTAxNDkyMThhZjkwZjcxNmE1MmJmNGE1ZWQzNDdlNDhlY2MxM2Y3NzM4OGFjWCECRFODGd5IXVW-07KaZCvuWHk3WrnnpiDhHki6SCQh88-iYWlIAK0mjE0fWCZhcIKjYWECYXN4QDEzMjNkM2Q0NzA3YTU4YWQyZTIzYWRhNGU5ZjFmNDlmNWE1YjRhYzdiNzA4ZWIwZDYxZjczOGY0ODMwN2U4ZWVhY1ghAjRWqhENhLSsdHrr2Cw7AFrKUL9Ffr1XN6RBT6w659lNo2FhAWFzeEA1NmJjYmNiYjdjYzY0MDZiM2ZhNWQ1N2QyMTc0ZjRlZmY4YjQ0MDJiMTc2OTI2ZDNhNTdkM2MzZGNiYjU5ZDU3YWNYIQJzEpxXGeWZN5qXSmJjY8MzxWyvwObQGr5G1YCCgHicY2FtdWh0dHA6Ly9sb2NhbGhvc3Q6MzMzOGF1Y3NhdA==";
+
+        let token = Token::from_str(token_str_multi_keysets).unwrap();
+        let amount = token.value().expect("Token should have value");
+
+        assert_eq!(amount, Amount::from(4));
+
+        let unit = token.unit().unwrap();
+        assert_eq!(CurrencyUnit::Sat, unit);
+
+        match token {
+            Token::TokenV4(token) => {
+                let tokens: Vec<Id> = token.token.iter().map(|t| t.keyset_id).collect();
+
+                assert_eq!(tokens.len(), 2);
+
+                assert!(tokens.contains(&Id::from_str("00ffd48b8f5ecf80").unwrap()));
+                assert!(tokens.contains(&Id::from_str("00ad268c4d1f5826").unwrap()));
+
+                let mint_url = token.mint_url;
+
+                assert_eq!("http://localhost:3338", &mint_url.to_string());
+            }
+            _ => {
+                panic!("Token should be a v4 token")
+            }
+        }
+    }
+
+    #[test]
+    fn test_tokenv4_from_tokenv3() {
+        let token_v3_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
+        let token_v3 =
+            TokenV3::from_str(token_v3_str).expect("TokenV3 should be created from string");
+        let token_v4 = TokenV4::try_from(token_v3).expect("TokenV3 should be converted to TokenV4");
+        let token_v4_expected = "cashuBpGFtd2h0dHBzOi8vODMzMy5zcGFjZTozMzM4YXVjc2F0YWRqVGhhbmsgeW91LmF0gaJhaUgAmh8pMlPkHmFwgqRhYQJhc3hANDA3OTE1YmMyMTJiZTYxYTc3ZTNlNmQyYWViNGM3Mjc5ODBiZGE1MWNkMDZhNmFmYzI5ZTI4NjE3NjhhNzgzN2FjWCECvJCXmX2Br7LMc0a15DRak0a9KlBut5WFmKcvDPhRY-phZPakYWEIYXN4QGZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmVhY1ghAp6OUFC4kKfWwJaNsWvB1dX6BA6h3ihPbsadYSmfZxBZYWT2";
+        assert_eq!(token_v4.to_string(), token_v4_expected);
+    }
+
+    #[test]
+    fn test_token_str_round_trip() {
+        let token_str = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
+
+        let token = TokenV3::from_str(token_str).unwrap();
+        assert_eq!(
+            token.token[0].mint,
+            MintUrl::from_str("https://8333.space:3338").unwrap()
+        );
+        assert_eq!(
+            token.token[0].proofs[0].clone().keyset_id,
+            Id::from_str("009a1f293253e41e").unwrap()
+        );
+        assert_eq!(token.unit.clone().unwrap(), CurrencyUnit::Sat);
+
+        let encoded = &token.to_string();
+
+        let token_data = TokenV3::from_str(encoded).unwrap();
+
+        assert_eq!(token_data, token);
+    }
+
+    #[test]
+    fn incorrect_tokens() {
+        let incorrect_prefix = "casshuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
+
+        let incorrect_prefix_token = TokenV3::from_str(incorrect_prefix);
+
+        assert!(incorrect_prefix_token.is_err());
+
+        let no_prefix = "eyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
+
+        let no_prefix_token = TokenV3::from_str(no_prefix);
+
+        assert!(no_prefix_token.is_err());
+
+        let correct_token = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9";
+
+        let correct_token = TokenV3::from_str(correct_token);
+
+        assert!(correct_token.is_ok());
+    }
+
+    #[test]
+    fn test_token_v4_raw_roundtrip() {
+        let token_raw = hex::decode("6372617742a4617481a261694800ad268c4d1f5826617081a3616101617378403961366462623834376264323332626137366462306466313937323136623239643362386363313435353363643237383237666331636339343266656462346561635821038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d4721267926164695468616e6b20796f75616d75687474703a2f2f6c6f63616c686f73743a33333338617563736174").unwrap();
+        let token = TokenV4::try_from(&token_raw).expect("Token deserialization error");
+        let token_raw_ = token.to_raw_bytes().expect("Token serialization error");
+        let token_ = TokenV4::try_from(&token_raw_).expect("Token deserialization error");
+        assert!(token_ == token)
+    }
+
+    #[test]
+    fn test_token_generic_raw_roundtrip() {
+        let tokenv4_raw = hex::decode("6372617742a4617481a261694800ad268c4d1f5826617081a3616101617378403961366462623834376264323332626137366462306466313937323136623239643362386363313435353363643237383237666331636339343266656462346561635821038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d4721267926164695468616e6b20796f75616d75687474703a2f2f6c6f63616c686f73743a33333338617563736174").unwrap();
+        let tokenv4 = Token::try_from(&tokenv4_raw).expect("Token deserialization error");
+        let tokenv4_ = TokenV4::try_from(&tokenv4_raw).expect("Token deserialization error");
+        let tokenv4_bytes = tokenv4.to_raw_bytes().expect("Serialization error");
+        let tokenv4_bytes_ = tokenv4_.to_raw_bytes().expect("Serialization error");
+        assert!(tokenv4_bytes_ == tokenv4_bytes);
+    }
+}

File diff suppressed because it is too large
+ 165 - 0
crates/cdk-types/src/nuts/nut01/mod.rs


+ 153 - 0
crates/cdk-types/src/nuts/nut01/public_key.rs

@@ -0,0 +1,153 @@
+use core::fmt;
+use core::ops::Deref;
+use core::str::FromStr;
+
+use bitcoin::hashes::sha256::Hash as Sha256Hash;
+use bitcoin::hashes::Hash;
+use bitcoin::secp256k1::schnorr::Signature;
+use bitcoin::secp256k1::{self, Message, XOnlyPublicKey};
+use serde::{Deserialize, Deserializer, Serialize};
+
+use super::Error;
+use crate::SECP256K1;
+
+/// PublicKey
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct PublicKey {
+    #[cfg_attr(feature = "swagger", schema(value_type = String))]
+    inner: secp256k1::PublicKey,
+}
+
+impl Deref for PublicKey {
+    type Target = secp256k1::PublicKey;
+
+    fn deref(&self) -> &Self::Target {
+        &self.inner
+    }
+}
+
+impl From<secp256k1::PublicKey> for PublicKey {
+    fn from(inner: secp256k1::PublicKey) -> Self {
+        Self { inner }
+    }
+}
+
+impl PublicKey {
+    /// Parse from `bytes`
+    #[inline]
+    pub fn from_slice(slice: &[u8]) -> Result<Self, Error> {
+        Ok(Self {
+            inner: secp256k1::PublicKey::from_slice(slice)?,
+        })
+    }
+
+    /// Parse from `hex` string
+    #[inline]
+    pub fn from_hex<S>(hex: S) -> Result<Self, Error>
+    where
+        S: AsRef<str>,
+    {
+        let hex: &str = hex.as_ref();
+
+        // Check size
+        if hex.len() != 33 * 2 {
+            return Err(Error::InvalidPublicKeySize {
+                expected: 33,
+                found: hex.len() / 2,
+            });
+        }
+
+        Ok(Self {
+            inner: secp256k1::PublicKey::from_str(hex)?,
+        })
+    }
+
+    /// [`PublicKey`] to bytes
+    #[inline]
+    pub fn to_bytes(&self) -> [u8; 33] {
+        self.inner.serialize()
+    }
+
+    /// To uncompressed bytes
+    #[inline]
+    pub fn to_uncompressed_bytes(&self) -> [u8; 65] {
+        self.inner.serialize_uncompressed()
+    }
+
+    /// To [`XOnlyPublicKey`]
+    #[inline]
+    pub fn x_only_public_key(&self) -> XOnlyPublicKey {
+        self.inner.x_only_public_key().0
+    }
+
+    /// Get public key as `hex` string
+    #[inline]
+    pub fn to_hex(&self) -> String {
+        self.inner.to_string()
+    }
+
+    /// Verify schnorr signature
+    pub fn verify(&self, msg: &[u8], sig: &Signature) -> Result<(), Error> {
+        let hash: Sha256Hash = Sha256Hash::hash(msg);
+        let msg = Message::from_digest_slice(hash.as_ref())?;
+        SECP256K1.verify_schnorr(sig, &msg, &self.inner.x_only_public_key().0)?;
+        Ok(())
+    }
+}
+
+impl FromStr for PublicKey {
+    type Err = Error;
+
+    fn from_str(hex: &str) -> Result<Self, Self::Err> {
+        Self::from_hex(hex)
+    }
+}
+
+impl fmt::Display for PublicKey {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.to_hex())
+    }
+}
+
+impl Serialize for PublicKey {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        serializer.serialize_str(&self.to_hex())
+    }
+}
+
+impl<'de> Deserialize<'de> for PublicKey {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let public_key: String = String::deserialize(deserializer)?;
+        Self::from_hex(public_key).map_err(serde::de::Error::custom)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    pub fn test_public_key_from_hex() {
+        // Compressed
+        assert!(
+            (PublicKey::from_hex(
+                "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104"
+            )
+            .is_ok())
+        );
+    }
+
+    #[test]
+    pub fn test_invalid_public_key_from_hex() {
+        // Uncompressed (is valid but is cashu must be compressed?)
+        assert!((PublicKey::from_hex("04fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de3625246cb2c27dac965cb7200a5986467eee92eb7d496bbf1453b074e223e481")
+            .is_err()))
+    }
+}

+ 141 - 0
crates/cdk-types/src/nuts/nut01/secret_key.rs

@@ -0,0 +1,141 @@
+use core::fmt;
+use core::ops::Deref;
+use core::str::FromStr;
+
+use bitcoin::hashes::sha256::Hash as Sha256Hash;
+use bitcoin::hashes::Hash;
+use bitcoin::secp256k1;
+use bitcoin::secp256k1::rand::rngs::OsRng;
+use bitcoin::secp256k1::schnorr::Signature;
+use bitcoin::secp256k1::{Keypair, Message, Scalar};
+use serde::{Deserialize, Deserializer, Serialize};
+
+use super::{Error, PublicKey};
+use crate::SECP256K1;
+
+/// SecretKey
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct SecretKey {
+    #[cfg_attr(feature = "swagger", schema(value_type = String))]
+    inner: secp256k1::SecretKey,
+}
+
+impl Deref for SecretKey {
+    type Target = secp256k1::SecretKey;
+
+    fn deref(&self) -> &Self::Target {
+        &self.inner
+    }
+}
+
+impl From<secp256k1::SecretKey> for SecretKey {
+    fn from(inner: secp256k1::SecretKey) -> Self {
+        Self { inner }
+    }
+}
+
+impl fmt::Display for SecretKey {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.to_secret_hex())
+    }
+}
+
+impl SecretKey {
+    /// Parse from `bytes`
+    pub fn from_slice(slice: &[u8]) -> Result<Self, Error> {
+        Ok(Self {
+            inner: secp256k1::SecretKey::from_slice(slice)?,
+        })
+    }
+
+    /// Parse from `hex` string
+    pub fn from_hex<S>(hex: S) -> Result<Self, Error>
+    where
+        S: AsRef<str>,
+    {
+        Ok(Self {
+            inner: secp256k1::SecretKey::from_str(hex.as_ref())?,
+        })
+    }
+
+    /// Generate random secret key
+    pub fn generate() -> Self {
+        let (secret_key, _) = SECP256K1.generate_keypair(&mut OsRng);
+        Self { inner: secret_key }
+    }
+
+    /// Get secret key as `hex` string
+    pub fn to_secret_hex(&self) -> String {
+        self.inner.display_secret().to_string()
+    }
+
+    /// Get secret key as `bytes`
+    pub fn as_secret_bytes(&self) -> &[u8] {
+        self.inner.as_ref()
+    }
+
+    /// Get secret key as `bytes`
+    pub fn to_secret_bytes(&self) -> [u8; 32] {
+        self.inner.secret_bytes()
+    }
+
+    /// Schnorr Signature on Message
+    pub fn sign(&self, msg: &[u8]) -> Result<Signature, Error> {
+        let hash: Sha256Hash = Sha256Hash::hash(msg);
+        let msg = Message::from_digest_slice(hash.as_ref())?;
+        Ok(SECP256K1.sign_schnorr(&msg, &Keypair::from_secret_key(&SECP256K1, &self.inner)))
+    }
+
+    /// Get public key
+    pub fn public_key(&self) -> PublicKey {
+        self.inner.public_key(&SECP256K1).into()
+    }
+
+    /// [`SecretKey`] to [`Scalar`]
+    #[inline]
+    pub fn to_scalar(self) -> Scalar {
+        Scalar::from(self.inner)
+    }
+
+    /// [`SecretKey`] as [`Scalar`]
+    #[inline]
+    pub fn as_scalar(&self) -> Scalar {
+        Scalar::from(self.inner)
+    }
+}
+
+impl FromStr for SecretKey {
+    type Err = Error;
+
+    /// Try to parse [SecretKey] from `hex` or `bech32`
+    fn from_str(secret_key: &str) -> Result<Self, Self::Err> {
+        Self::from_hex(secret_key)
+    }
+}
+
+impl Serialize for SecretKey {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        serializer.serialize_str(&self.to_secret_hex())
+    }
+}
+
+impl<'de> Deserialize<'de> for SecretKey {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let secret_key: String = String::deserialize(deserializer)?;
+        Self::from_hex(secret_key).map_err(serde::de::Error::custom)
+    }
+}
+
+impl Drop for SecretKey {
+    fn drop(&mut self) {
+        self.inner.non_secure_erase();
+        tracing::trace!("Secret Key dropped.");
+    }
+}

+ 288 - 0
crates/cdk-types/src/nuts/nut02.rs

@@ -0,0 +1,288 @@
+//! NUT-02: Keysets and keyset ID
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/02.md>
+
+use core::fmt;
+use core::str::FromStr;
+use std::array::TryFromSliceError;
+use std::collections::BTreeMap;
+
+use bitcoin::bip32::DerivationPath;
+use bitcoin::bip32::{ChildNumber, Xpriv};
+use bitcoin::hashes::sha256::Hash as Sha256;
+use bitcoin::hashes::Hash;
+use bitcoin::key::Secp256k1;
+use bitcoin::secp256k1;
+use serde::{Deserialize, Serialize};
+use serde_with::{serde_as, VecSkipError};
+use thiserror::Error;
+
+use super::nut01::Keys;
+use super::nut01::{MintKeyPair, MintKeys};
+use crate::amount::AmountStr;
+use crate::hex;
+use crate::nuts::nut00::CurrencyUnit;
+use crate::Amount;
+
+/// NUT02 Error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Hex Error
+    #[error(transparent)]
+    HexError(#[from] hex::Error),
+    /// Keyset length error
+    #[error("NUT02: ID length invalid")]
+    Length,
+    /// Unknown version
+    #[error("NUT02: Unknown Version")]
+    UnknownVersion,
+    /// Keyset id does not match
+    #[error("Keyset id incorrect")]
+    IncorrectKeysetId,
+    /// Slice Error
+    #[error(transparent)]
+    Slice(#[from] TryFromSliceError),
+}
+
+/// Keyset version
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub enum KeySetVersion {
+    /// Current Version 00
+    Version00,
+}
+
+impl KeySetVersion {
+    /// [`KeySetVersion`] to byte
+    pub fn to_byte(&self) -> u8 {
+        match self {
+            Self::Version00 => 0,
+        }
+    }
+
+    /// [`KeySetVersion`] from byte
+    pub fn from_byte(byte: &u8) -> Result<Self, Error> {
+        match byte {
+            0 => Ok(Self::Version00),
+            _ => Err(Error::UnknownVersion),
+        }
+    }
+}
+
+impl fmt::Display for KeySetVersion {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            KeySetVersion::Version00 => f.write_str("00"),
+        }
+    }
+}
+
+/// A keyset ID is an identifier for a specific keyset. It can be derived by
+/// anyone who knows the set of public keys of a mint. The keyset ID **CAN**
+/// be stored in a Cashu token such that the token can be used to identify
+/// which mint or keyset it was generated from.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+#[serde(into = "String", try_from = "String")]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = String))]
+pub struct Id {
+    version: KeySetVersion,
+    id: [u8; Self::BYTELEN],
+}
+
+impl Id {
+    const STRLEN: usize = 14;
+    const BYTELEN: usize = 7;
+
+    /// [`Id`] to bytes
+    pub fn to_bytes(&self) -> Vec<u8> {
+        [vec![self.version.to_byte()], self.id.to_vec()].concat()
+    }
+
+    /// [`Id`] from bytes
+    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
+        Ok(Self {
+            version: KeySetVersion::from_byte(&bytes[0])?,
+            id: bytes[1..].try_into()?,
+        })
+    }
+
+    /// [`Id`] as bytes
+    pub fn as_bytes(&self) -> [u8; Self::BYTELEN + 1] {
+        let mut bytes = [0u8; Self::BYTELEN + 1];
+        bytes[0] = self.version.to_byte();
+        bytes[1..].copy_from_slice(&self.id);
+        bytes
+    }
+}
+
+// Used to generate a compressed unique identifier as part of the NUT13 spec
+// This is a one-way function
+impl From<Id> for u32 {
+    fn from(value: Id) -> Self {
+        let hex_bytes: [u8; 8] = value.as_bytes();
+
+        let int = u64::from_be_bytes(hex_bytes);
+
+        (int % (2_u64.pow(31) - 1)) as u32
+    }
+}
+
+impl fmt::Display for Id {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_str(&format!("{}{}", self.version, hex::encode(self.id)))
+    }
+}
+
+impl TryFrom<String> for Id {
+    type Error = Error;
+
+    fn try_from(s: String) -> Result<Self, Self::Error> {
+        if s.len() != 16 {
+            return Err(Error::Length);
+        }
+
+        Ok(Self {
+            version: KeySetVersion::from_byte(&hex::decode(&s[..2])?[0])?,
+            id: hex::decode(&s[2..])?
+                .try_into()
+                .map_err(|_| Error::Length)?,
+        })
+    }
+}
+
+impl FromStr for Id {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Self::try_from(s.to_string())
+    }
+}
+
+impl From<Id> for String {
+    fn from(value: Id) -> Self {
+        value.to_string()
+    }
+}
+
+impl From<&Keys> for Id {
+    /// As per NUT-02:
+    ///   1. sort public keys by their amount in ascending order
+    ///   2. concatenate all public keys to one string
+    ///   3. HASH_SHA256 the concatenated public keys
+    ///   4. take the first 14 characters of the hex-encoded hash
+    ///   5. prefix it with a keyset ID version byte
+    fn from(map: &Keys) -> Self {
+        let mut keys: Vec<(&AmountStr, &super::PublicKey)> = map.iter().collect();
+        keys.sort_by_key(|(amt, _v)| *amt);
+
+        let pubkeys_concat: Vec<u8> = keys
+            .iter()
+            .map(|(_, pubkey)| pubkey.to_bytes())
+            .collect::<Vec<[u8; 33]>>()
+            .concat();
+
+        let hash = Sha256::hash(&pubkeys_concat);
+        let hex_of_hash = hex::encode(hash.to_byte_array());
+
+        Self {
+            version: KeySetVersion::Version00,
+            id: hex::decode(&hex_of_hash[0..Self::STRLEN])
+                .expect("Keys hash could not be hex decoded")
+                .try_into()
+                .expect("Invalid length of hex id"),
+        }
+    }
+}
+
+/// Mint Keysets [NUT-02]
+/// Ids of mints keyset ids
+#[serde_as]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct KeysetResponse {
+    /// set of public key ids that the mint generates
+    #[serde_as(as = "VecSkipError<_>")]
+    pub keysets: Vec<KeySetInfo>,
+}
+
+/// Keyset
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct KeySet {
+    /// Keyset [`Id`]
+    pub id: Id,
+    /// Keyset [`CurrencyUnit`]
+    pub unit: CurrencyUnit,
+    /// Keyset [`Keys`]
+    pub keys: Keys,
+}
+
+impl KeySet {
+    /// Verify the keyset is matches keys
+    pub fn verify_id(&self) -> Result<(), Error> {
+        let keys_id: Id = (&self.keys).into();
+
+        if keys_id != self.id {
+            return Err(Error::IncorrectKeysetId);
+        }
+
+        Ok(())
+    }
+}
+
+impl From<MintKeySet> for KeySet {
+    fn from(keyset: MintKeySet) -> Self {
+        Self {
+            id: keyset.id,
+            unit: keyset.unit,
+            keys: Keys::from(keyset.keys),
+        }
+    }
+}
+
+/// KeySetInfo
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize, Serialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct KeySetInfo {
+    /// Keyset [`Id`]
+    pub id: Id,
+    /// Keyset [`CurrencyUnit`]
+    pub unit: CurrencyUnit,
+    /// Keyset state
+    /// Mint will only sign from an active keyset
+    pub active: bool,
+    /// Input Fee PPK
+    #[serde(default = "default_input_fee_ppk")]
+    pub input_fee_ppk: u64,
+}
+
+fn default_input_fee_ppk() -> u64 {
+    0
+}
+
+/// MintKeyset
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct MintKeySet {
+    /// Keyset [`Id`]
+    pub id: Id,
+    /// Keyset [`CurrencyUnit`]
+    pub unit: CurrencyUnit,
+    /// Keyset [`MintKeys`]
+    pub keys: MintKeys,
+}
+
+impl From<MintKeySet> for Id {
+    fn from(keyset: MintKeySet) -> Id {
+        let keys: super::KeySet = keyset.into();
+
+        Id::from(&keys.keys)
+    }
+}
+
+impl From<&MintKeys> for Id {
+    fn from(map: &MintKeys) -> Self {
+        let keys: super::Keys = map.clone().into();
+
+        Id::from(&keys)
+    }
+}

+ 91 - 0
crates/cdk-types/src/nuts/nut03.rs

@@ -0,0 +1,91 @@
+//! NUT-03: Swap
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/03.md>
+
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+
+use super::nut00::{BlindSignature, BlindedMessage, PreMintSecrets, Proofs};
+use crate::Amount;
+
+/// NUT03 Error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// DHKE error
+    #[error(transparent)]
+    DHKE(#[from] crate::dhke::Error),
+    /// Amount Error
+    #[error(transparent)]
+    Amount(#[from] crate::amount::Error),
+}
+
+/// Preswap information
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+pub struct PreSwap {
+    /// Preswap mint secrets
+    pub pre_mint_secrets: PreMintSecrets,
+    /// Swap request
+    pub swap_request: SwapRequest,
+    /// Amount to increment keyset counter by
+    pub derived_secret_count: u32,
+    /// Fee amount
+    pub fee: Amount,
+}
+
+/// Swap Request [NUT-03]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct SwapRequest {
+    /// Proofs that are to be spent in a `Swap`
+    #[cfg_attr(feature = "swagger", schema(value_type = Vec<Proof>))]
+    pub inputs: Proofs,
+    /// Blinded Messages for Mint to sign
+    pub outputs: Vec<BlindedMessage>,
+}
+
+impl SwapRequest {
+    /// Create new [`SwapRequest`]
+    pub fn new(inputs: Proofs, outputs: Vec<BlindedMessage>) -> Self {
+        Self { inputs, outputs }
+    }
+
+    /// Total value of proofs in [`SwapRequest`]
+    pub fn input_amount(&self) -> Result<Amount, Error> {
+        Ok(Amount::try_sum(
+            self.inputs.iter().map(|proof| proof.amount),
+        )?)
+    }
+
+    /// Total value of outputs in [`SwapRequest`]
+    pub fn output_amount(&self) -> Result<Amount, Error> {
+        Ok(Amount::try_sum(
+            self.outputs.iter().map(|proof| proof.amount),
+        )?)
+    }
+}
+
+/// Split Response [NUT-06]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct SwapResponse {
+    /// Promises
+    pub signatures: Vec<BlindSignature>,
+}
+
+impl SwapResponse {
+    /// Create new [`SwapRequest`]
+    pub fn new(promises: Vec<BlindSignature>) -> SwapResponse {
+        SwapResponse {
+            signatures: promises,
+        }
+    }
+
+    /// Total [`Amount`] of promises
+    pub fn promises_amount(&self) -> Result<Amount, Error> {
+        Ok(Amount::try_sum(
+            self.signatures
+                .iter()
+                .map(|BlindSignature { amount, .. }| *amount),
+        )?)
+    }
+}

+ 234 - 0
crates/cdk-types/src/nuts/nut04.rs

@@ -0,0 +1,234 @@
+//! NUT-04: Mint Tokens via Bolt11
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/04.md>
+
+use std::fmt;
+use std::str::FromStr;
+
+use serde::de::DeserializeOwned;
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+use uuid::Uuid;
+
+use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod};
+use super::{MintQuoteState, PublicKey};
+use crate::Amount;
+
+/// NUT04 Error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Unknown Quote State
+    #[error("Unknown Quote State")]
+    UnknownState,
+    /// Amount overflow
+    #[error("Amount overflow")]
+    AmountOverflow,
+}
+
+/// Mint quote request [NUT-04]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct MintQuoteBolt11Request {
+    /// Amount
+    pub amount: Amount,
+    /// Unit wallet would like to pay with
+    pub unit: CurrencyUnit,
+    /// Memo to create the invoice with
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub description: Option<String>,
+    /// NUT-19 Pubkey
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub pubkey: Option<PublicKey>,
+}
+
+/// Possible states of a quote
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
+#[serde(rename_all = "UPPERCASE")]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = MintQuoteState))]
+pub enum QuoteState {
+    /// Quote has not been paid
+    #[default]
+    Unpaid,
+    /// Quote has been paid and wallet can mint
+    Paid,
+    /// Minting is in progress
+    /// **Note:** This state is to be used internally but is not part of the
+    /// nut.
+    Pending,
+    /// ecash issued for quote
+    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)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+#[serde(bound = "Q: Serialize + DeserializeOwned")]
+pub struct MintQuoteBolt11Response<Q> {
+    /// Quote Id
+    pub quote: Q,
+    /// Payment request to fulfil
+    pub request: String,
+    /// Quote State
+    pub state: MintQuoteState,
+    /// Unix timestamp until the quote is valid
+    pub expiry: Option<u64>,
+    /// NUT-19 Pubkey
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub pubkey: Option<PublicKey>,
+}
+
+impl<Q: ToString> MintQuoteBolt11Response<Q> {
+    /// Convert the MintQuote with a quote type Q to a String
+    pub fn to_string_id(&self) -> MintQuoteBolt11Response<String> {
+        MintQuoteBolt11Response {
+            quote: self.quote.to_string(),
+            request: self.request.clone(),
+            state: self.state,
+            expiry: self.expiry,
+            pubkey: self.pubkey,
+        }
+    }
+}
+
+impl From<MintQuoteBolt11Response<Uuid>> for MintQuoteBolt11Response<String> {
+    fn from(value: MintQuoteBolt11Response<Uuid>) -> Self {
+        Self {
+            quote: value.quote.to_string(),
+            request: value.request,
+            state: value.state,
+            expiry: value.expiry,
+            pubkey: value.pubkey,
+        }
+    }
+}
+
+impl From<crate::mint::MintQuote> for MintQuoteBolt11Response<Uuid> {
+    fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt11Response<Uuid> {
+        MintQuoteBolt11Response {
+            quote: mint_quote.id,
+            request: mint_quote.request,
+            state: mint_quote.state,
+            expiry: Some(mint_quote.expiry),
+            pubkey: mint_quote.pubkey,
+        }
+    }
+}
+
+/// Mint request [NUT-04]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+#[serde(bound = "Q: Serialize + DeserializeOwned")]
+pub struct MintBolt11Request<Q> {
+    /// Quote id
+    #[cfg_attr(feature = "swagger", schema(max_length = 1_000))]
+    pub quote: Q,
+    /// Outputs
+    #[cfg_attr(feature = "swagger", schema(max_items = 1_000))]
+    pub outputs: Vec<BlindedMessage>,
+    /// Signature
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub signature: Option<String>,
+}
+
+impl TryFrom<MintBolt11Request<String>> for MintBolt11Request<Uuid> {
+    type Error = uuid::Error;
+
+    fn try_from(value: MintBolt11Request<String>) -> Result<Self, Self::Error> {
+        Ok(Self {
+            quote: Uuid::from_str(&value.quote)?,
+            outputs: value.outputs,
+            signature: value.signature,
+        })
+    }
+}
+
+impl<Q> MintBolt11Request<Q> {
+    /// Total [`Amount`] of outputs
+    pub fn total_amount(&self) -> Result<Amount, Error> {
+        Amount::try_sum(
+            self.outputs
+                .iter()
+                .map(|BlindedMessage { amount, .. }| *amount),
+        )
+        .map_err(|_| Error::AmountOverflow)
+    }
+}
+
+/// Mint response [NUT-04]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct MintBolt11Response {
+    /// Blinded Signatures
+    pub signatures: Vec<BlindSignature>,
+}
+
+/// Mint Method Settings
+#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct MintMethodSettings {
+    /// Payment Method e.g. bolt11
+    pub method: PaymentMethod,
+    /// Currency Unit e.g. sat
+    pub unit: CurrencyUnit,
+    /// Min Amount
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub min_amount: Option<Amount>,
+    /// Max Amount
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub max_amount: Option<Amount>,
+    /// Quote Description
+    #[serde(default)]
+    pub description: bool,
+}
+
+/// Mint Settings
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = nut04::Settings))]
+pub struct Settings {
+    /// Methods to mint
+    pub methods: Vec<MintMethodSettings>,
+    /// Minting disabled
+    pub disabled: bool,
+}
+
+impl Default for Settings {
+    fn default() -> Self {
+        let bolt11_mint = MintMethodSettings {
+            method: PaymentMethod::Bolt11,
+            unit: CurrencyUnit::Sat,
+            min_amount: Some(Amount::from(1)),
+            max_amount: Some(Amount::from(1000000)),
+            description: true,
+        };
+
+        Settings {
+            methods: vec![bolt11_mint],
+            disabled: false,
+        }
+    }
+}

+ 360 - 0
crates/cdk-types/src/nuts/nut05.rs

@@ -0,0 +1,360 @@
+//! NUT-05: Melting Tokens
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/05.md>
+
+use std::fmt;
+use std::str::FromStr;
+
+use serde::de::DeserializeOwned;
+use serde::{Deserialize, Deserializer, Serialize};
+use serde_json::Value;
+use thiserror::Error;
+use uuid::Uuid;
+
+use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
+use super::nut15::Mpp;
+use crate::mint::{self, MeltQuote};
+use crate::nuts::MeltQuoteState;
+use crate::{Amount, Bolt11Invoice};
+
+/// NUT05 Error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Unknown Quote State
+    #[error("Unknown quote state")]
+    UnknownState,
+    /// Amount overflow
+    #[error("Amount Overflow")]
+    AmountOverflow,
+}
+
+/// Melt quote request [NUT-05]
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct MeltQuoteBolt11Request {
+    /// Bolt11 invoice to be paid
+    #[cfg_attr(feature = "swagger", schema(value_type = String))]
+    pub request: Bolt11Invoice,
+    /// Unit wallet would like to pay with
+    pub unit: CurrencyUnit,
+    /// Payment Options
+    pub options: Option<Mpp>,
+}
+
+/// Possible states of a quote
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
+#[serde(rename_all = "UPPERCASE")]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = MeltQuoteState))]
+pub enum QuoteState {
+    /// Quote has not been paid
+    #[default]
+    Unpaid,
+    /// Quote has been paid
+    Paid,
+    /// Paying quote is in progress
+    Pending,
+    /// Unknown state
+    Unknown,
+    /// Failed
+    Failed,
+}
+
+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::Unknown => write!(f, "UNKNOWN"),
+            Self::Failed => write!(f, "FAILED"),
+        }
+    }
+}
+
+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),
+            "UNKNOWN" => Ok(Self::Unknown),
+            "FAILED" => Ok(Self::Failed),
+            _ => Err(Error::UnknownState),
+        }
+    }
+}
+
+/// Melt quote response [NUT-05]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+#[serde(bound = "Q: Serialize")]
+pub struct MeltQuoteBolt11Response<Q> {
+    /// Quote Id
+    pub quote: Q,
+    /// The amount that needs to be provided
+    pub amount: Amount,
+    /// The fee reserve that is required
+    pub fee_reserve: Amount,
+    /// Whether the the request haas be paid
+    // TODO: To be deprecated
+    /// Deprecated
+    pub paid: Option<bool>,
+    /// Quote State
+    pub state: MeltQuoteState,
+    /// 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>>,
+}
+
+impl<Q: ToString> MeltQuoteBolt11Response<Q> {
+    /// Convert a `MeltQuoteBolt11Response` with type Q (generic/unknown) to a
+    /// `MeltQuoteBolt11Response` with `String`
+    pub fn to_string_id(self) -> MeltQuoteBolt11Response<String> {
+        MeltQuoteBolt11Response {
+            quote: self.quote.to_string(),
+            amount: self.amount,
+            fee_reserve: self.fee_reserve,
+            paid: self.paid,
+            state: self.state,
+            expiry: self.expiry,
+            payment_preimage: self.payment_preimage,
+            change: self.change,
+        }
+    }
+}
+
+impl From<MeltQuoteBolt11Response<Uuid>> for MeltQuoteBolt11Response<String> {
+    fn from(value: MeltQuoteBolt11Response<Uuid>) -> Self {
+        Self {
+            quote: value.quote.to_string(),
+            amount: value.amount,
+            fee_reserve: value.fee_reserve,
+            paid: value.paid,
+            state: value.state,
+            expiry: value.expiry,
+            payment_preimage: value.payment_preimage,
+            change: value.change,
+        }
+    }
+}
+
+impl From<&MeltQuote> for MeltQuoteBolt11Response<Uuid> {
+    fn from(melt_quote: &MeltQuote) -> MeltQuoteBolt11Response<Uuid> {
+        MeltQuoteBolt11Response {
+            quote: melt_quote.id,
+            payment_preimage: None,
+            change: None,
+            state: melt_quote.state,
+            paid: Some(melt_quote.state == MeltQuoteState::Paid),
+            expiry: melt_quote.expiry,
+            amount: melt_quote.amount,
+            fee_reserve: melt_quote.fee_reserve,
+        }
+    }
+}
+
+// A custom deserializer is needed until all mints
+// update some will return without the required state.
+impl<'de, Q: DeserializeOwned> Deserialize<'de> for MeltQuoteBolt11Response<Q> {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let value = Value::deserialize(deserializer)?;
+
+        let quote: Q = 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<mint::MeltQuote> for MeltQuoteBolt11Response<Uuid> {
+    fn from(melt_quote: mint::MeltQuote) -> MeltQuoteBolt11Response<Uuid> {
+        let paid = melt_quote.state == QuoteState::Paid;
+        MeltQuoteBolt11Response {
+            quote: melt_quote.id,
+            amount: melt_quote.amount,
+            fee_reserve: melt_quote.fee_reserve,
+            paid: Some(paid),
+            state: melt_quote.state,
+            expiry: melt_quote.expiry,
+            payment_preimage: melt_quote.payment_preimage,
+            change: None,
+        }
+    }
+}
+
+/// Melt Bolt11 Request [NUT-05]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+#[serde(bound = "Q: Serialize + DeserializeOwned")]
+pub struct MeltBolt11Request<Q> {
+    /// Quote ID
+    pub quote: Q,
+    /// Proofs
+    #[cfg_attr(feature = "swagger", schema(value_type = Vec<Proof>))]
+    pub inputs: Proofs,
+    /// Blinded Message that can be used to return change [NUT-08]
+    /// Amount field of BlindedMessages `SHOULD` be set to zero
+    pub outputs: Option<Vec<BlindedMessage>>,
+}
+
+impl TryFrom<MeltBolt11Request<String>> for MeltBolt11Request<Uuid> {
+    type Error = uuid::Error;
+
+    fn try_from(value: MeltBolt11Request<String>) -> Result<Self, Self::Error> {
+        Ok(Self {
+            quote: Uuid::from_str(&value.quote)?,
+            inputs: value.inputs,
+            outputs: value.outputs,
+        })
+    }
+}
+
+impl<Q: Serialize + DeserializeOwned> MeltBolt11Request<Q> {
+    /// Total [`Amount`] of [`Proofs`]
+    pub fn proofs_amount(&self) -> Result<Amount, Error> {
+        Amount::try_sum(self.inputs.iter().map(|proof| proof.amount))
+            .map_err(|_| Error::AmountOverflow)
+    }
+}
+
+/// Melt Method Settings
+#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct MeltMethodSettings {
+    /// Payment Method e.g. bolt11
+    pub method: PaymentMethod,
+    /// Currency Unit e.g. sat
+    pub unit: CurrencyUnit,
+    /// Min Amount
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub min_amount: Option<Amount>,
+    /// Max Amount
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub max_amount: Option<Amount>,
+}
+
+impl Settings {
+    /// Create new [`Settings`]
+    pub fn new(methods: Vec<MeltMethodSettings>, disabled: bool) -> Self {
+        Self { methods, disabled }
+    }
+
+    /// Get [`MeltMethodSettings`] for unit method pair
+    pub fn get_settings(
+        &self,
+        unit: &CurrencyUnit,
+        method: &PaymentMethod,
+    ) -> Option<MeltMethodSettings> {
+        for method_settings in self.methods.iter() {
+            if method_settings.method.eq(method) && method_settings.unit.eq(unit) {
+                return Some(method_settings.clone());
+            }
+        }
+
+        None
+    }
+}
+
+/// Melt Settings
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = nut05::Settings))]
+pub struct Settings {
+    /// Methods to melt
+    pub methods: Vec<MeltMethodSettings>,
+    /// Minting disabled
+    pub disabled: bool,
+}
+
+impl Default for Settings {
+    fn default() -> Self {
+        let bolt11_mint = MeltMethodSettings {
+            method: PaymentMethod::Bolt11,
+            unit: CurrencyUnit::Sat,
+            min_amount: Some(Amount::from(1)),
+            max_amount: Some(Amount::from(1000000)),
+        };
+
+        Settings {
+            methods: vec![bolt11_mint],
+            disabled: false,
+        }
+    }
+}

+ 549 - 0
crates/cdk-types/src/nuts/nut06.rs

@@ -0,0 +1,549 @@
+//! NUT-06: Mint Information
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/06.md>
+
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+
+use super::nut01::PublicKey;
+use super::nut17::SupportedMethods;
+use super::nut19::CachedEndpoint;
+use super::{nut04, nut05, nut15, nut19, MppMethodSettings};
+
+/// Mint Version
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct MintVersion {
+    /// Mint Software name
+    pub name: String,
+    /// Mint Version
+    pub version: String,
+}
+
+impl MintVersion {
+    /// Create new [`MintVersion`]
+    pub fn new(name: String, version: String) -> Self {
+        Self { name, version }
+    }
+}
+
+impl Serialize for MintVersion {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let combined = format!("{}/{}", self.name, self.version);
+        serializer.serialize_str(&combined)
+    }
+}
+
+impl<'de> Deserialize<'de> for MintVersion {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let combined = String::deserialize(deserializer)?;
+        let parts: Vec<&str> = combined.split('/').collect();
+        if parts.len() != 2 {
+            return Err(serde::de::Error::custom("Invalid input string"));
+        }
+        Ok(MintVersion {
+            name: parts[0].to_string(),
+            version: parts[1].to_string(),
+        })
+    }
+}
+
+/// Mint Info [NIP-06]
+#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct MintInfo {
+    /// name of the mint and should be recognizable
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub name: Option<String>,
+    /// hex pubkey of the mint
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub pubkey: Option<PublicKey>,
+    /// implementation name and the version running
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub version: Option<MintVersion>,
+    /// short description of the mint
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub description: Option<String>,
+    /// long description
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub description_long: Option<String>,
+    /// Contact info
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub contact: Option<Vec<ContactInfo>>,
+    /// shows which NUTs the mint supports
+    pub nuts: Nuts,
+    /// Mint's icon URL
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub icon_url: Option<String>,
+    /// Mint's endpoint URLs
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub urls: Option<Vec<String>>,
+    /// message of the day that the wallet must display to the user
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub motd: Option<String>,
+    /// server unix timestamp
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub time: Option<u64>,
+}
+
+impl MintInfo {
+    /// Create new [`MintInfo`]
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    /// Set name
+    pub fn name<S>(self, name: S) -> Self
+    where
+        S: Into<String>,
+    {
+        Self {
+            name: Some(name.into()),
+            ..self
+        }
+    }
+
+    /// Set pubkey
+    pub fn pubkey(self, pubkey: PublicKey) -> Self {
+        Self {
+            pubkey: Some(pubkey),
+            ..self
+        }
+    }
+
+    /// Set [`MintVersion`]
+    pub fn version(self, mint_version: MintVersion) -> Self {
+        Self {
+            version: Some(mint_version),
+            ..self
+        }
+    }
+
+    /// Set description
+    pub fn description<S>(self, description: S) -> Self
+    where
+        S: Into<String>,
+    {
+        Self {
+            description: Some(description.into()),
+            ..self
+        }
+    }
+
+    /// Set long description
+    pub fn long_description<S>(self, description_long: S) -> Self
+    where
+        S: Into<String>,
+    {
+        Self {
+            description_long: Some(description_long.into()),
+            ..self
+        }
+    }
+
+    /// Set contact info
+    pub fn contact_info(self, contact_info: Vec<ContactInfo>) -> Self {
+        Self {
+            contact: Some(contact_info),
+            ..self
+        }
+    }
+
+    /// Set nuts
+    pub fn nuts(self, nuts: Nuts) -> Self {
+        Self { nuts, ..self }
+    }
+
+    /// Set mint icon url
+    pub fn icon_url<S>(self, icon_url: S) -> Self
+    where
+        S: Into<String>,
+    {
+        Self {
+            icon_url: Some(icon_url.into()),
+            ..self
+        }
+    }
+
+    /// Set motd
+    pub fn motd<S>(self, motd: S) -> Self
+    where
+        S: Into<String>,
+    {
+        Self {
+            motd: Some(motd.into()),
+            ..self
+        }
+    }
+
+    /// Set time
+    pub fn time<S>(self, time: S) -> Self
+    where
+        S: Into<u64>,
+    {
+        Self {
+            time: Some(time.into()),
+            ..self
+        }
+    }
+}
+
+/// Supported nuts and settings
+#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct Nuts {
+    /// NUT04 Settings
+    #[serde(default)]
+    #[serde(rename = "4")]
+    pub nut04: nut04::Settings,
+    /// NUT05 Settings
+    #[serde(default)]
+    #[serde(rename = "5")]
+    pub nut05: nut05::Settings,
+    /// NUT07 Settings
+    #[serde(default)]
+    #[serde(rename = "7")]
+    pub nut07: SupportedSettings,
+    /// NUT08 Settings
+    #[serde(default)]
+    #[serde(rename = "8")]
+    pub nut08: SupportedSettings,
+    /// NUT09 Settings
+    #[serde(default)]
+    #[serde(rename = "9")]
+    pub nut09: SupportedSettings,
+    /// NUT10 Settings
+    #[serde(rename = "10")]
+    #[serde(default)]
+    pub nut10: SupportedSettings,
+    /// NUT11 Settings
+    #[serde(rename = "11")]
+    #[serde(default)]
+    pub nut11: SupportedSettings,
+    /// NUT12 Settings
+    #[serde(default)]
+    #[serde(rename = "12")]
+    pub nut12: SupportedSettings,
+    /// NUT14 Settings
+    #[serde(default)]
+    #[serde(rename = "14")]
+    pub nut14: SupportedSettings,
+    /// NUT15 Settings
+    #[serde(default)]
+    #[serde(rename = "15")]
+    pub nut15: nut15::Settings,
+    /// NUT17 Settings
+    #[serde(default)]
+    #[serde(rename = "17")]
+    pub nut17: super::nut17::SupportedSettings,
+    /// NUT19 Settings
+    #[serde(default)]
+    #[serde(rename = "19")]
+    pub nut19: nut19::Settings,
+    /// NUT20 Settings
+    #[serde(default)]
+    #[serde(rename = "20")]
+    pub nut20: SupportedSettings,
+}
+
+impl Nuts {
+    /// Create new [`Nuts`]
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    /// Nut04 settings
+    pub fn nut04(self, nut04_settings: nut04::Settings) -> Self {
+        Self {
+            nut04: nut04_settings,
+            ..self
+        }
+    }
+
+    /// Nut05 settings
+    pub fn nut05(self, nut05_settings: nut05::Settings) -> Self {
+        Self {
+            nut05: nut05_settings,
+            ..self
+        }
+    }
+
+    /// Nut07 settings
+    pub fn nut07(self, supported: bool) -> Self {
+        Self {
+            nut07: SupportedSettings { supported },
+            ..self
+        }
+    }
+
+    /// Nut08 settings
+    pub fn nut08(self, supported: bool) -> Self {
+        Self {
+            nut08: SupportedSettings { supported },
+            ..self
+        }
+    }
+
+    /// Nut09 settings
+    pub fn nut09(self, supported: bool) -> Self {
+        Self {
+            nut09: SupportedSettings { supported },
+            ..self
+        }
+    }
+
+    /// Nut10 settings
+    pub fn nut10(self, supported: bool) -> Self {
+        Self {
+            nut10: SupportedSettings { supported },
+            ..self
+        }
+    }
+
+    /// Nut11 settings
+    pub fn nut11(self, supported: bool) -> Self {
+        Self {
+            nut11: SupportedSettings { supported },
+            ..self
+        }
+    }
+
+    /// Nut12 settings
+    pub fn nut12(self, supported: bool) -> Self {
+        Self {
+            nut12: SupportedSettings { supported },
+            ..self
+        }
+    }
+
+    /// Nut14 settings
+    pub fn nut14(self, supported: bool) -> Self {
+        Self {
+            nut14: SupportedSettings { supported },
+            ..self
+        }
+    }
+
+    /// Nut15 settings
+    pub fn nut15(self, mpp_settings: Vec<MppMethodSettings>) -> Self {
+        Self {
+            nut15: nut15::Settings {
+                methods: mpp_settings,
+            },
+            ..self
+        }
+    }
+
+    /// Nut17 settings
+    pub fn nut17(self, supported: Vec<SupportedMethods>) -> Self {
+        Self {
+            nut17: super::nut17::SupportedSettings { supported },
+            ..self
+        }
+    }
+
+    /// Nut19 settings
+    pub fn nut19(self, ttl: Option<u64>, cached_endpoints: Vec<CachedEndpoint>) -> Self {
+        Self {
+            nut19: nut19::Settings {
+                ttl,
+                cached_endpoints,
+            },
+            ..self
+        }
+    }
+
+    /// Nut20 settings
+    pub fn nut20(self, supported: bool) -> Self {
+        Self {
+            nut20: SupportedSettings { supported },
+            ..self
+        }
+    }
+}
+
+/// Check state Settings
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct SupportedSettings {
+    supported: bool,
+}
+
+/// Contact Info
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct ContactInfo {
+    /// Contact Method i.e. nostr
+    pub method: String,
+    /// Contact info i.e. npub...
+    pub info: String,
+}
+
+impl ContactInfo {
+    /// Create new [`ContactInfo`]
+    pub fn new(method: String, info: String) -> Self {
+        Self { method, info }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+
+    use super::*;
+
+    #[test]
+    fn test_des_mint_into() {
+        let mint_info_str = r#"{
+    "name": "Cashu mint",
+    "pubkey": "0296d0aa13b6a31cf0cd974249f28c7b7176d7274712c95a41c7d8066d3f29d679",
+    "version": "Nutshell/0.15.3",
+    "contact": [
+        ["", ""],
+        ["", ""]
+    ],
+    "nuts": {
+        "4": {
+            "methods": [
+                {"method": "bolt11", "unit": "sat", "description": true},
+                {"method": "bolt11", "unit": "usd", "description": true}
+            ],
+            "disabled": false
+        },
+        "5": {
+            "methods": [
+                {"method": "bolt11", "unit": "sat"},
+                {"method": "bolt11", "unit": "usd"}
+            ],
+            "disabled": false
+        },
+        "7": {"supported": true},
+        "8": {"supported": true},
+        "9": {"supported": true},
+        "10": {"supported": true},
+        "11": {"supported": true}
+    }
+}"#;
+
+        let _mint_info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
+    }
+
+    #[test]
+    fn test_ser_mint_info() {
+        /*
+                let mint_info = serde_json::to_string(&MintInfo {
+                    name: Some("Cashu-crab".to_string()),
+                    pubkey: None,
+                    version: None,
+                    description: Some("A mint".to_string()),
+                    description_long: Some("Some longer test".to_string()),
+                    contact: None,
+                    nuts: Nuts::default(),
+                    motd: None,
+                })
+                .unwrap();
+
+                println!("{}", mint_info);
+        */
+        let mint_info_str = r#"{
+  "name": "Bob's Cashu mint",
+  "pubkey": "0283bf290884eed3a7ca2663fc0260de2e2064d6b355ea13f98dec004b7a7ead99",
+  "version": "Nutshell/0.15.0",
+  "description": "The short mint description",
+  "description_long": "A description that can be a long piece of text.",
+  "contact": [
+    {
+        "method": "nostr",
+        "info": "xxxxx"
+    },
+    {
+        "method": "email",
+        "info": "contact@me.com"
+    }
+  ],
+  "motd": "Message to display to users.",
+  "icon_url": "https://this-is-a-mint-icon-url.com/icon.png",
+  "nuts": {
+    "4": {
+      "methods": [
+        {
+        "method": "bolt11",
+        "unit": "sat",
+        "min_amount": 0,
+        "max_amount": 10000,
+        "description": true
+        }
+      ],
+      "disabled": false
+    },
+    "5": {
+      "methods": [
+        {
+        "method": "bolt11",
+        "unit": "sat",
+        "min_amount": 0,
+        "max_amount": 10000
+        }
+      ],
+      "disabled": false
+    },
+    "7": {"supported": true},
+    "8": {"supported": true},
+    "9": {"supported": true},
+    "10": {"supported": true},
+    "12": {"supported": true}
+  }
+}"#;
+        let info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
+        let mint_info_str = r#"{
+  "name": "Bob's Cashu mint",
+  "pubkey": "0283bf290884eed3a7ca2663fc0260de2e2064d6b355ea13f98dec004b7a7ead99",
+  "version": "Nutshell/0.15.0",
+  "description": "The short mint description",
+  "description_long": "A description that can be a long piece of text.",
+  "contact": [
+        ["nostr", "xxxxx"],
+        ["email", "contact@me.com"]
+  ],
+  "motd": "Message to display to users.",
+  "icon_url": "https://this-is-a-mint-icon-url.com/icon.png",
+  "nuts": {
+    "4": {
+      "methods": [
+        {
+        "method": "bolt11",
+        "unit": "sat",
+        "min_amount": 0,
+        "max_amount": 10000,
+        "description": true
+        }
+      ],
+      "disabled": false
+    },
+    "5": {
+      "methods": [
+        {
+        "method": "bolt11",
+        "unit": "sat",
+        "min_amount": 0,
+        "max_amount": 10000
+        }
+      ],
+      "disabled": false
+    },
+    "7": {"supported": true},
+    "8": {"supported": true},
+    "9": {"supported": true},
+    "10": {"supported": true},
+    "12": {"supported": true}
+  }
+}"#;
+        let mint_info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
+
+        assert_eq!(info, mint_info);
+    }
+}

+ 107 - 0
crates/cdk-types/src/nuts/nut07.rs

@@ -0,0 +1,107 @@
+//! NUT-07: Spendable Check
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/07.md>
+
+use std::fmt;
+use std::str::FromStr;
+
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+
+use super::nut01::PublicKey;
+
+/// NUT07 Error
+#[derive(Debug, Error, PartialEq, Eq)]
+pub enum Error {
+    /// Unknown State error
+    #[error("Unknown state")]
+    UnknownState,
+}
+
+/// State of Proof
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "UPPERCASE")]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub enum State {
+    /// Spent
+    Spent,
+    /// Unspent
+    Unspent,
+    /// Pending
+    ///
+    /// Currently being used in a transaction i.e. melt in progress
+    Pending,
+    /// Proof is reserved
+    ///
+    /// i.e. used to create a token
+    Reserved,
+}
+
+impl fmt::Display for State {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let s = match self {
+            Self::Spent => "SPENT",
+            Self::Unspent => "UNSPENT",
+            Self::Pending => "PENDING",
+            Self::Reserved => "RESERVED",
+        };
+
+        write!(f, "{}", s)
+    }
+}
+
+impl FromStr for State {
+    type Err = Error;
+
+    fn from_str(state: &str) -> Result<Self, Self::Err> {
+        match state {
+            "SPENT" => Ok(Self::Spent),
+            "UNSPENT" => Ok(Self::Unspent),
+            "PENDING" => Ok(Self::Pending),
+            "RESERVED" => Ok(Self::Reserved),
+            _ => Err(Error::UnknownState),
+        }
+    }
+}
+
+/// Check spendable request [NUT-07]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct CheckStateRequest {
+    /// Y's of the proofs to check
+    #[serde(rename = "Ys")]
+    #[cfg_attr(feature = "swagger", schema(value_type = Vec<String>, max_items = 1_000))]
+    pub ys: Vec<PublicKey>,
+}
+
+/// Proof state [NUT-07]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct ProofState {
+    /// Y of proof
+    #[serde(rename = "Y")]
+    #[cfg_attr(feature = "swagger", schema(value_type = String))]
+    pub y: PublicKey,
+    /// State of proof
+    pub state: State,
+    /// Witness data if it is supplied
+    pub witness: Option<String>,
+}
+
+impl From<(PublicKey, State)> for ProofState {
+    fn from(value: (PublicKey, State)) -> Self {
+        Self {
+            y: value.0,
+            state: value.1,
+            witness: None,
+        }
+    }
+}
+
+/// Check Spendable Response [NUT-07]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct CheckStateResponse {
+    /// Proof states
+    pub states: Vec<ProofState>,
+}

+ 24 - 0
crates/cdk-types/src/nuts/nut08.rs

@@ -0,0 +1,24 @@
+//! NUT-08: Lightning fee return
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/08.md>
+
+use super::nut05::{MeltBolt11Request, MeltQuoteBolt11Response};
+use crate::Amount;
+
+impl<Q> MeltBolt11Request<Q> {
+    /// Total output [`Amount`]
+    pub fn output_amount(&self) -> Option<Amount> {
+        self.outputs
+            .as_ref()
+            .and_then(|o| Amount::try_sum(o.iter().map(|proof| proof.amount)).ok())
+    }
+}
+
+impl<Q> MeltQuoteBolt11Response<Q> {
+    /// Total change [`Amount`]
+    pub fn change_amount(&self) -> Option<Amount> {
+        self.change
+            .as_ref()
+            .and_then(|o| Amount::try_sum(o.iter().map(|proof| proof.amount)).ok())
+    }
+}

+ 41 - 0
crates/cdk-types/src/nuts/nut09.rs

@@ -0,0 +1,41 @@
+//! NUT-09: Restore signatures
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/09.md>
+
+use serde::{Deserialize, Serialize};
+
+use super::nut00::{BlindSignature, BlindedMessage};
+
+/// Restore Request [NUT-09]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct RestoreRequest {
+    /// Outputs
+    pub outputs: Vec<BlindedMessage>,
+}
+
+/// Restore Response [NUT-09]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct RestoreResponse {
+    /// Outputs
+    pub outputs: Vec<BlindedMessage>,
+    /// Signatures
+    pub signatures: Vec<BlindSignature>,
+    /// Promises
+    // Temp compatibility with cashu-ts
+    pub promises: Option<Vec<BlindSignature>>,
+}
+
+mod test {
+
+    #[test]
+    fn restore_response() {
+        use super::*;
+        let rs = r#"{"outputs":[{"B_":"0204bbffa045f28ec836117a29ea0a00d77f1d692e38cf94f72a5145bfda6d8f41","amount":0,"id":"00ffd48b8f5ecf80", "witness":null},{"B_":"025f0615ccba96f810582a6885ffdb04bd57c96dbc590f5aa560447b31258988d7","amount":0,"id":"00ffd48b8f5ecf80"}],"signatures":[{"C_":"02e9701b804dc05a5294b5a580b428237a27c7ee1690a0177868016799b1761c81","amount":8,"dleq":null,"id":"00ffd48b8f5ecf80"},{"C_":"031246ee046519b15648f1b8d8ffcb8e537409c84724e148c8d6800b2e62deb795","amount":2,"dleq":null,"id":"00ffd48b8f5ecf80"}]}"#;
+
+        let res: RestoreResponse = serde_json::from_str(rs).unwrap();
+
+        println!("{:?}", res);
+    }
+}

+ 123 - 0
crates/cdk-types/src/nuts/nut10.rs

@@ -0,0 +1,123 @@
+//! NUT-10: Spending conditions
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/10.md>
+
+use std::str::FromStr;
+
+use serde::ser::SerializeTuple;
+use serde::{Deserialize, Serialize, Serializer};
+use thiserror::Error;
+
+/// NUT13 Error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Secret error
+    #[error(transparent)]
+    Secret(#[from] crate::secret::Error),
+    /// Serde Json error
+    #[error(transparent)]
+    SerdeJsonError(#[from] serde_json::Error),
+}
+
+///  NUT10 Secret Kind
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum Kind {
+    /// NUT-11 P2PK
+    P2PK,
+    /// NUT-14 HTLC
+    HTLC,
+}
+
+/// Secert Date
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct SecretData {
+    /// Unique random string
+    pub nonce: String,
+    /// Expresses the spending condition specific to each kind
+    pub data: String,
+    /// Additional data committed to and can be used for feature extensions
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub tags: Option<Vec<Vec<String>>>,
+}
+
+/// NUT10 Secret
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
+pub struct Secret {
+    ///  Kind of the spending condition
+    pub kind: Kind,
+    /// Secret Data
+    pub secret_data: SecretData,
+}
+
+impl Secret {
+    /// Create new [`Secret`]
+    pub fn new<S, V>(kind: Kind, data: S, tags: Option<V>) -> Self
+    where
+        S: Into<String>,
+        V: Into<Vec<Vec<String>>>,
+    {
+        let nonce = crate::secret::Secret::generate().to_string();
+
+        let secret_data = SecretData {
+            nonce,
+            data: data.into(),
+            tags: tags.map(|v| v.into()),
+        };
+
+        Self { kind, secret_data }
+    }
+}
+
+impl Serialize for Secret {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        // Create a tuple representing the struct fields
+        let secret_tuple = (&self.kind, &self.secret_data);
+
+        // Serialize the tuple as a JSON array
+        let mut s = serializer.serialize_tuple(2)?;
+
+        s.serialize_element(&secret_tuple.0)?;
+        s.serialize_element(&secret_tuple.1)?;
+        s.end()
+    }
+}
+
+impl TryFrom<Secret> for crate::secret::Secret {
+    type Error = Error;
+    fn try_from(secret: Secret) -> Result<crate::secret::Secret, Self::Error> {
+        Ok(crate::secret::Secret::from_str(&serde_json::to_string(
+            &secret,
+        )?)?)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::assert_eq;
+
+    use super::*;
+
+    #[test]
+    fn test_secret_serialize() {
+        let secret = Secret {
+            kind: Kind::P2PK,
+            secret_data: SecretData {
+                nonce: "5d11913ee0f92fefdc82a6764fd2457a".to_string(),
+                data: "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198"
+                    .to_string(),
+                tags: Some(vec![vec![
+                    "key".to_string(),
+                    "value1".to_string(),
+                    "value2".to_string(),
+                ]]),
+            },
+        };
+
+        let secret_str = r#"["P2PK",{"nonce":"5d11913ee0f92fefdc82a6764fd2457a","data":"026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198","tags":[["key","value1","value2"]]}]"#;
+
+        assert_eq!(serde_json::to_string(&secret).unwrap(), secret_str);
+    }
+}

+ 612 - 0
crates/cdk-types/src/nuts/nut11/mod.rs

@@ -0,0 +1,612 @@
+//! NUT-11: Pay to Public Key (P2PK)
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/11.md>
+
+use std::collections::{HashMap, HashSet};
+use std::str::FromStr;
+use std::{fmt, vec};
+
+use bitcoin::hashes::sha256::Hash as Sha256Hash;
+use serde::de::Error as DeserializerError;
+use serde::ser::SerializeSeq;
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use thiserror::Error;
+
+use super::nut01::PublicKey;
+use super::{Kind, Nut10Secret};
+use crate::secret::Secret;
+
+pub mod serde_p2pk_witness;
+
+/// Nut11 Error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Incorrect secret kind
+    #[error("Secret is not a p2pk secret")]
+    IncorrectSecretKind,
+    /// Incorrect secret kind
+    #[error("Witness is not a p2pk witness")]
+    IncorrectWitnessKind,
+    /// P2PK locktime has already passed
+    #[error("Locktime in past")]
+    LocktimeInPast,
+    /// Witness signature is not valid
+    #[error("Invalid signature")]
+    InvalidSignature,
+    /// Unknown tag in P2PK secret
+    #[error("Unknown tag P2PK secret")]
+    UnknownTag,
+    /// Unknown Sigflag
+    #[error("Unknown sigflag")]
+    UnknownSigFlag,
+    /// P2PK Spend conditions not meet
+    #[error("P2PK spend conditions are not met")]
+    SpendConditionsNotMet,
+    /// Pubkey must be in data field of P2PK
+    #[error("P2PK required in secret data")]
+    P2PKPubkeyRequired,
+    /// Unknown Kind
+    #[error("Kind not found")]
+    KindNotFound,
+    /// HTLC hash invalid
+    #[error("Invalid hash")]
+    InvalidHash,
+    /// Witness Signatures not provided
+    #[error("Witness signatures not provided")]
+    SignaturesNotProvided,
+    /// Parse Url Error
+    #[error(transparent)]
+    UrlParseError(#[from] url::ParseError),
+    /// Parse int error
+    #[error(transparent)]
+    ParseInt(#[from] std::num::ParseIntError),
+    /// From hex error
+    #[error(transparent)]
+    HexError(#[from] crate::hex::Error),
+    /// Serde Json error
+    #[error(transparent)]
+    SerdeJsonError(#[from] serde_json::Error),
+    /// Secp256k1 error
+    #[error(transparent)]
+    Secp256k1(#[from] bitcoin::secp256k1::Error),
+    /// NUT01 Error
+    #[error(transparent)]
+    NUT01(#[from] crate::nuts::nut01::Error),
+    /// Secret error
+    #[error(transparent)]
+    Secret(#[from] crate::secret::Error),
+}
+
+/// P2Pk Witness
+#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct P2PKWitness {
+    /// Signatures
+    pub signatures: Vec<String>,
+}
+
+/// Spending Conditions
+///
+/// Defined in [NUT10](https://github.com/cashubtc/nuts/blob/main/10.md)
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum SpendingConditions {
+    /// NUT11 Spending conditions
+    ///
+    /// Defined in [NUT11](https://github.com/cashubtc/nuts/blob/main/11.md)
+    P2PKConditions {
+        /// The public key of the recipient of the locked ecash
+        data: PublicKey,
+        /// Additional Optional Spending [`Conditions`]
+        conditions: Option<Conditions>,
+    },
+    /// NUT14 Spending conditions
+    ///
+    /// Dedined in [NUT14](https://github.com/cashubtc/nuts/blob/main/14.md)
+    HTLCConditions {
+        /// Hash Lock of ecash
+        data: Sha256Hash,
+        /// Additional Optional Spending [`Conditions`]
+        conditions: Option<Conditions>,
+    },
+}
+
+impl TryFrom<&Secret> for SpendingConditions {
+    type Error = Error;
+    fn try_from(secret: &Secret) -> Result<SpendingConditions, Error> {
+        let nut10_secret: Nut10Secret = secret.try_into()?;
+
+        nut10_secret.try_into()
+    }
+}
+
+impl TryFrom<Nut10Secret> for SpendingConditions {
+    type Error = Error;
+    fn try_from(secret: Nut10Secret) -> Result<SpendingConditions, Error> {
+        match secret.kind {
+            Kind::P2PK => Ok(SpendingConditions::P2PKConditions {
+                data: PublicKey::from_str(&secret.secret_data.data)?,
+                conditions: secret.secret_data.tags.and_then(|t| t.try_into().ok()),
+            }),
+            Kind::HTLC => Ok(Self::HTLCConditions {
+                data: Sha256Hash::from_str(&secret.secret_data.data)
+                    .map_err(|_| Error::InvalidHash)?,
+                conditions: secret.secret_data.tags.and_then(|t| t.try_into().ok()),
+            }),
+        }
+    }
+}
+
+impl From<SpendingConditions> for super::nut10::Secret {
+    fn from(conditions: SpendingConditions) -> super::nut10::Secret {
+        match conditions {
+            SpendingConditions::P2PKConditions { data, conditions } => {
+                super::nut10::Secret::new(Kind::P2PK, data.to_hex(), conditions)
+            }
+            SpendingConditions::HTLCConditions { data, conditions } => {
+                super::nut10::Secret::new(Kind::HTLC, data.to_string(), conditions)
+            }
+        }
+    }
+}
+
+/// P2PK and HTLC spending conditions
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
+pub struct Conditions {
+    /// Unix locktime after which refund keys can be used
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub locktime: Option<u64>,
+    /// Additional Public keys
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub pubkeys: Option<Vec<PublicKey>>,
+    /// Refund keys
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub refund_keys: Option<Vec<PublicKey>>,
+    /// Numbedr of signatures required
+    ///
+    /// Default is 1
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub num_sigs: Option<u64>,
+    /// Signature flag
+    ///
+    /// Default [`SigFlag::SigInputs`]
+    pub sig_flag: SigFlag,
+}
+
+impl From<Conditions> for Vec<Vec<String>> {
+    fn from(conditions: Conditions) -> Vec<Vec<String>> {
+        let Conditions {
+            locktime,
+            pubkeys,
+            refund_keys,
+            num_sigs,
+            sig_flag,
+        } = conditions;
+
+        let mut tags = Vec::new();
+
+        if let Some(pubkeys) = pubkeys {
+            tags.push(Tag::PubKeys(pubkeys.into_iter().collect()).as_vec());
+        }
+
+        if let Some(locktime) = locktime {
+            tags.push(Tag::LockTime(locktime).as_vec());
+        }
+
+        if let Some(num_sigs) = num_sigs {
+            tags.push(Tag::NSigs(num_sigs).as_vec());
+        }
+
+        if let Some(refund_keys) = refund_keys {
+            tags.push(Tag::Refund(refund_keys).as_vec())
+        }
+        tags.push(Tag::SigFlag(sig_flag).as_vec());
+        tags
+    }
+}
+
+impl TryFrom<Vec<Vec<String>>> for Conditions {
+    type Error = Error;
+    fn try_from(tags: Vec<Vec<String>>) -> Result<Conditions, Self::Error> {
+        let tags: HashMap<TagKind, Tag> = tags
+            .into_iter()
+            .map(|t| Tag::try_from(t).unwrap())
+            .map(|t| (t.kind(), t))
+            .collect();
+
+        let pubkeys = match tags.get(&TagKind::Pubkeys) {
+            Some(Tag::PubKeys(pubkeys)) => Some(pubkeys.clone()),
+            _ => None,
+        };
+
+        let locktime = if let Some(tag) = tags.get(&TagKind::Locktime) {
+            match tag {
+                Tag::LockTime(locktime) => Some(*locktime),
+                _ => None,
+            }
+        } else {
+            None
+        };
+
+        let refund_keys = if let Some(tag) = tags.get(&TagKind::Refund) {
+            match tag {
+                Tag::Refund(keys) => Some(keys.clone()),
+                _ => None,
+            }
+        } else {
+            None
+        };
+
+        let sig_flag = if let Some(tag) = tags.get(&TagKind::SigFlag) {
+            match tag {
+                Tag::SigFlag(sigflag) => *sigflag,
+                _ => SigFlag::SigInputs,
+            }
+        } else {
+            SigFlag::SigInputs
+        };
+
+        let num_sigs = if let Some(tag) = tags.get(&TagKind::NSigs) {
+            match tag {
+                Tag::NSigs(num_sigs) => Some(*num_sigs),
+                _ => None,
+            }
+        } else {
+            None
+        };
+
+        Ok(Conditions {
+            locktime,
+            pubkeys,
+            refund_keys,
+            num_sigs,
+            sig_flag,
+        })
+    }
+}
+
+/// P2PK and HTLC Spending condition tags
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
+#[serde(rename_all = "lowercase")]
+pub enum TagKind {
+    /// Signature flag
+    SigFlag,
+    /// Number signatures required
+    #[serde(rename = "n_sigs")]
+    NSigs,
+    /// Locktime
+    Locktime,
+    /// Refund
+    Refund,
+    /// Pubkey
+    Pubkeys,
+    /// Custom tag kind
+    Custom(String),
+}
+
+impl fmt::Display for TagKind {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::SigFlag => write!(f, "sigflag"),
+            Self::NSigs => write!(f, "n_sigs"),
+            Self::Locktime => write!(f, "locktime"),
+            Self::Refund => write!(f, "refund"),
+            Self::Pubkeys => write!(f, "pubkeys"),
+            Self::Custom(kind) => write!(f, "{}", kind),
+        }
+    }
+}
+
+impl<S> From<S> for TagKind
+where
+    S: AsRef<str>,
+{
+    fn from(tag: S) -> Self {
+        match tag.as_ref() {
+            "sigflag" => Self::SigFlag,
+            "n_sigs" => Self::NSigs,
+            "locktime" => Self::Locktime,
+            "refund" => Self::Refund,
+            "pubkeys" => Self::Pubkeys,
+            t => Self::Custom(t.to_owned()),
+        }
+    }
+}
+
+/// Signature flag
+///
+/// Defined in [NUT11](https://github.com/cashubtc/nuts/blob/main/11.md)
+#[derive(
+    Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord, Hash,
+)]
+pub enum SigFlag {
+    #[default]
+    /// Requires valid signatures on all inputs.
+    /// It is the default signature flag and will be applied even if the
+    /// `sigflag` tag is absent.
+    SigInputs,
+    /// Requires valid signatures on all inputs and on all outputs.
+    SigAll,
+}
+
+impl fmt::Display for SigFlag {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::SigAll => write!(f, "SIG_ALL"),
+            Self::SigInputs => write!(f, "SIG_INPUTS"),
+        }
+    }
+}
+
+impl FromStr for SigFlag {
+    type Err = Error;
+    fn from_str(tag: &str) -> Result<Self, Self::Err> {
+        match tag {
+            "SIG_ALL" => Ok(Self::SigAll),
+            "SIG_INPUTS" => Ok(Self::SigInputs),
+            _ => Err(Error::UnknownSigFlag),
+        }
+    }
+}
+
+/// Enforce Sigflag info
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) struct EnforceSigFlag {
+    /// Sigflag required for proofs
+    pub sig_flag: SigFlag,
+    /// Pubkeys that can sign for proofs
+    pub pubkeys: HashSet<PublicKey>,
+    /// Number of sigs required for proofs
+    pub sigs_required: u64,
+}
+
+/// Tag
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
+pub enum Tag {
+    /// Sigflag [`Tag`]
+    SigFlag(SigFlag),
+    /// Number of Sigs [`Tag`]
+    NSigs(u64),
+    /// Locktime [`Tag`]
+    LockTime(u64),
+    /// Refund [`Tag`]
+    Refund(Vec<PublicKey>),
+    /// Pubkeys [`Tag`]
+    PubKeys(Vec<PublicKey>),
+}
+
+impl<S> TryFrom<Vec<S>> for Tag
+where
+    S: AsRef<str>,
+{
+    type Error = Error;
+
+    fn try_from(tag: Vec<S>) -> Result<Self, Self::Error> {
+        let tag_kind: TagKind = match tag.first() {
+            Some(kind) => TagKind::from(kind),
+            None => return Err(Error::KindNotFound),
+        };
+
+        match tag_kind {
+            TagKind::SigFlag => Ok(Tag::SigFlag(SigFlag::from_str(tag[1].as_ref())?)),
+            TagKind::NSigs => Ok(Tag::NSigs(tag[1].as_ref().parse()?)),
+            TagKind::Locktime => Ok(Tag::LockTime(tag[1].as_ref().parse()?)),
+            TagKind::Refund => {
+                let pubkeys = tag
+                    .iter()
+                    .skip(1)
+                    .map(|p| PublicKey::from_str(p.as_ref()))
+                    .collect::<Result<Vec<PublicKey>, _>>()?;
+
+                Ok(Self::Refund(pubkeys))
+            }
+            TagKind::Pubkeys => {
+                let pubkeys = tag
+                    .iter()
+                    .skip(1)
+                    .map(|p| PublicKey::from_str(p.as_ref()))
+                    .collect::<Result<Vec<PublicKey>, _>>()?;
+
+                Ok(Self::PubKeys(pubkeys))
+            }
+            _ => Err(Error::UnknownTag),
+        }
+    }
+}
+
+impl From<Tag> for Vec<String> {
+    fn from(data: Tag) -> Self {
+        match data {
+            Tag::SigFlag(sigflag) => vec![TagKind::SigFlag.to_string(), sigflag.to_string()],
+            Tag::NSigs(num_sig) => vec![TagKind::NSigs.to_string(), num_sig.to_string()],
+            Tag::LockTime(locktime) => vec![TagKind::Locktime.to_string(), locktime.to_string()],
+            Tag::PubKeys(pubkeys) => {
+                let mut tag = vec![TagKind::Pubkeys.to_string()];
+                for pubkey in pubkeys.into_iter() {
+                    tag.push(pubkey.to_string())
+                }
+                tag
+            }
+            Tag::Refund(pubkeys) => {
+                let mut tag = vec![TagKind::Refund.to_string()];
+
+                for pubkey in pubkeys {
+                    tag.push(pubkey.to_string())
+                }
+                tag
+            }
+        }
+    }
+}
+
+impl Serialize for Tag {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let data: Vec<String> = self.as_vec();
+        let mut seq = serializer.serialize_seq(Some(data.len()))?;
+        for element in data.into_iter() {
+            seq.serialize_element(&element)?;
+        }
+        seq.end()
+    }
+}
+
+impl<'de> Deserialize<'de> for Tag {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        type Data = Vec<String>;
+        let vec: Vec<String> = Data::deserialize(deserializer)?;
+        Self::try_from(vec).map_err(DeserializerError::custom)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::str::FromStr;
+
+    use super::*;
+    use crate::nuts::Id;
+    use crate::secret::Secret;
+    use crate::{Amount, Proof, SecretKey, Witness};
+
+    #[test]
+    fn test_secret_ser() {
+        let data = PublicKey::from_str(
+            "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e",
+        )
+        .unwrap();
+
+        let conditions = Conditions {
+            locktime: Some(99999),
+            pubkeys: Some(vec![
+                PublicKey::from_str(
+                    "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
+                )
+                .unwrap(),
+                PublicKey::from_str(
+                    "023192200a0cfd3867e48eb63b03ff599c7e46c8f4e41146b2d281173ca6c50c54",
+                )
+                .unwrap(),
+            ]),
+            refund_keys: Some(vec![PublicKey::from_str(
+                "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e",
+            )
+            .unwrap()]),
+            num_sigs: Some(2),
+            sig_flag: SigFlag::SigAll,
+        };
+
+        let secret: Nut10Secret = Nut10Secret::new(Kind::P2PK, data.to_string(), Some(conditions));
+
+        let secret_str = serde_json::to_string(&secret).unwrap();
+
+        let secret_der: Nut10Secret = serde_json::from_str(&secret_str).unwrap();
+
+        assert_eq!(secret_der, secret);
+    }
+
+    #[test]
+    fn sign_proof() {
+        let secret_key =
+            SecretKey::from_str("99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37")
+                .unwrap();
+
+        let signing_key_two =
+            SecretKey::from_str("0000000000000000000000000000000000000000000000000000000000000001")
+                .unwrap();
+
+        let signing_key_three =
+            SecretKey::from_str("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f")
+                .unwrap();
+        let v_key: PublicKey = secret_key.public_key();
+        let v_key_two: PublicKey = signing_key_two.public_key();
+        let v_key_three: PublicKey = signing_key_three.public_key();
+
+        let conditions = Conditions {
+            locktime: Some(21000000000),
+            pubkeys: Some(vec![v_key_two, v_key_three]),
+            refund_keys: Some(vec![v_key]),
+            num_sigs: Some(2),
+            sig_flag: SigFlag::SigInputs,
+        };
+
+        let secret: Secret = Nut10Secret::new(Kind::P2PK, v_key.to_string(), Some(conditions))
+            .try_into()
+            .unwrap();
+
+        let mut proof = Proof {
+            keyset_id: Id::from_str("009a1f293253e41e").unwrap(),
+            amount: Amount::ZERO,
+            secret,
+            c: PublicKey::from_str(
+                "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
+            )
+            .unwrap(),
+            witness: Some(Witness::P2PKWitness(P2PKWitness { signatures: vec![] })),
+            dleq: None,
+        };
+
+        proof.sign_p2pk(secret_key).unwrap();
+        proof.sign_p2pk(signing_key_two).unwrap();
+
+        assert!(proof.verify_p2pk().is_ok());
+    }
+
+    #[test]
+    fn test_verify() {
+        // Proof with a valid signature
+        let json: &str = r#"{
+            "amount":1,
+            "secret":"[\"P2PK\",{\"nonce\":\"859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f\",\"data\":\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\",\"tags\":[[\"sigflag\",\"SIG_INPUTS\"]]}]",
+            "C":"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
+            "id":"009a1f293253e41e",
+            "witness":"{\"signatures\":[\"60f3c9b766770b46caac1d27e1ae6b77c8866ebaeba0b9489fe6a15a837eaa6fcd6eaa825499c72ac342983983fd3ba3a8a41f56677cc99ffd73da68b59e1383\"]}"
+        }"#;
+        let valid_proof: Proof = serde_json::from_str(json).unwrap();
+
+        valid_proof.verify_p2pk().unwrap();
+        assert!(valid_proof.verify_p2pk().is_ok());
+
+        // Proof with a signature that is in a different secret
+        let invalid_proof = r#"{"amount":1,"secret":"[\"P2PK\",{\"nonce\":\"859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f\",\"data\":\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\",\"tags\":[[\"sigflag\",\"SIG_INPUTS\"]]}]","C":"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904","id":"009a1f293253e41e","witness":"{\"signatures\":[\"3426df9730d365a9d18d79bed2f3e78e9172d7107c55306ac5ddd1b2d065893366cfa24ff3c874ebf1fc22360ba5888ddf6ff5dbcb9e5f2f5a1368f7afc64f15\"]}"}"#;
+
+        let invalid_proof: Proof = serde_json::from_str(invalid_proof).unwrap();
+
+        assert!(invalid_proof.verify_p2pk().is_err());
+    }
+
+    #[test]
+    fn verify_multi_sig() {
+        // Proof with 2 valid signatures to satifiy the condition
+        let valid_proof = r#"{"amount":0,"secret":"[\"P2PK\",{\"nonce\":\"0ed3fcb22c649dd7bbbdcca36e0c52d4f0187dd3b6a19efcc2bfbebb5f85b2a1\",\"data\":\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\",\"tags\":[[\"pubkeys\",\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\",\"02142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9\"],[\"n_sigs\",\"2\"],[\"sigflag\",\"SIG_INPUTS\"]]}]","C":"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904","id":"009a1f293253e41e","witness":"{\"signatures\":[\"83564aca48c668f50d022a426ce0ed19d3a9bdcffeeaee0dc1e7ea7e98e9eff1840fcc821724f623468c94f72a8b0a7280fa9ef5a54a1b130ef3055217f467b3\",\"9a72ca2d4d5075be5b511ee48dbc5e45f259bcf4a4e8bf18587f433098a9cd61ff9737dc6e8022de57c76560214c4568377792d4c2c6432886cc7050487a1f22\"]}"}"#;
+
+        let valid_proof: Proof = serde_json::from_str(valid_proof).unwrap();
+
+        assert!(valid_proof.verify_p2pk().is_ok());
+
+        // Proof with only one of the required signatures
+        let invalid_proof = r#"{"amount":0,"secret":"[\"P2PK\",{\"nonce\":\"0ed3fcb22c649dd7bbbdcca36e0c52d4f0187dd3b6a19efcc2bfbebb5f85b2a1\",\"data\":\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\",\"tags\":[[\"pubkeys\",\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\",\"02142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9\"],[\"n_sigs\",\"2\"],[\"sigflag\",\"SIG_INPUTS\"]]}]","C":"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904","id":"009a1f293253e41e","witness":"{\"signatures\":[\"83564aca48c668f50d022a426ce0ed19d3a9bdcffeeaee0dc1e7ea7e98e9eff1840fcc821724f623468c94f72a8b0a7280fa9ef5a54a1b130ef3055217f467b3\"]}"}"#;
+
+        let invalid_proof: Proof = serde_json::from_str(invalid_proof).unwrap();
+
+        // Verification should fail without the requires signatures
+        assert!(invalid_proof.verify_p2pk().is_err());
+    }
+
+    #[test]
+    fn verify_refund() {
+        let valid_proof = r#"{"amount":1,"id":"009a1f293253e41e","secret":"[\"P2PK\",{\"nonce\":\"902685f492ef3bb2ca35a47ddbba484a3365d143b9776d453947dcbf1ddf9689\",\"data\":\"026f6a2b1d709dbca78124a9f30a742985f7eddd894e72f637f7085bf69b997b9a\",\"tags\":[[\"pubkeys\",\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\",\"03142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9\"],[\"locktime\",\"21\"],[\"n_sigs\",\"2\"],[\"refund\",\"026f6a2b1d709dbca78124a9f30a742985f7eddd894e72f637f7085bf69b997b9a\"],[\"sigflag\",\"SIG_INPUTS\"]]}]","C":"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904","witness":"{\"signatures\":[\"710507b4bc202355c91ea3c147c0d0189c75e179d995e566336afd759cb342bcad9a593345f559d9b9e108ac2c9b5bd9f0b4b6a295028a98606a0a2e95eb54f7\"]}"}"#;
+
+        let valid_proof: Proof = serde_json::from_str(valid_proof).unwrap();
+        assert!(valid_proof.verify_p2pk().is_ok());
+
+        let invalid_proof = r#"{"amount":1,"id":"009a1f293253e41e","secret":"[\"P2PK\",{\"nonce\":\"64c46e5d30df27286166814b71b5d69801704f23a7ad626b05688fbdb48dcc98\",\"data\":\"026f6a2b1d709dbca78124a9f30a742985f7eddd894e72f637f7085bf69b997b9a\",\"tags\":[[\"pubkeys\",\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\",\"03142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9\"],[\"locktime\",\"21\"],[\"n_sigs\",\"2\"],[\"refund\",\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\"],[\"sigflag\",\"SIG_INPUTS\"]]}]","C":"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904","witness":"{\"signatures\":[\"f661d3dc046d636d47cb3d06586da42c498f0300373d1c2a4f417a44252cdf3809bce207c8888f934dba0d2b1671f1b8622d526840f2d5883e571b462630c1ff\"]}"}"#;
+
+        let invalid_proof: Proof = serde_json::from_str(invalid_proof).unwrap();
+
+        assert!(invalid_proof.verify_p2pk().is_err());
+    }
+}

+ 22 - 0
crates/cdk-types/src/nuts/nut11/serde_p2pk_witness.rs

@@ -0,0 +1,22 @@
+//! Serde utils for P2PK Witness
+
+use serde::{de, ser, Deserialize, Deserializer, Serializer};
+
+use super::P2PKWitness;
+
+/// Serialize [P2PKWitness] as stringified JSON
+pub fn serialize<S>(x: &P2PKWitness, s: S) -> Result<S::Ok, S::Error>
+where
+    S: Serializer,
+{
+    s.serialize_str(&serde_json::to_string(&x).map_err(ser::Error::custom)?)
+}
+
+/// Deserialize [P2PKWitness] from stringified JSON
+pub fn deserialize<'de, D>(deserializer: D) -> Result<P2PKWitness, D::Error>
+where
+    D: Deserializer<'de>,
+{
+    let s: String = String::deserialize(deserializer)?;
+    serde_json::from_str(&s).map_err(de::Error::custom)
+}

+ 63 - 0
crates/cdk-types/src/nuts/nut12.rs

@@ -0,0 +1,63 @@
+//! NUT-12: Offline ecash signature validation
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/12.md>
+
+use bitcoin::secp256k1;
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+
+use super::nut01::SecretKey;
+
+/// NUT12 Error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Missing DLEQ Proof
+    #[error("No DLEQ proof provided")]
+    MissingDleqProof,
+    /// Incomplete DLEQ Proof
+    #[error("Incomplete DLEQ proof")]
+    IncompleteDleqProof,
+    /// Invalid DLEQ Proof
+    #[error("Invalid DLEQ proof")]
+    InvalidDleqProof,
+    /// DHKE error
+    #[error(transparent)]
+    DHKE(#[from] crate::dhke::Error),
+    /// NUT01 Error
+    #[error(transparent)]
+    NUT01(#[from] crate::nuts::nut01::Error),
+    /// SECP256k1 Error
+    #[error(transparent)]
+    Secp256k1(#[from] secp256k1::Error),
+}
+
+/// Blinded Signature on Dleq
+///
+/// Defined in [NUT12](https://github.com/cashubtc/nuts/blob/main/12.md)
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct BlindSignatureDleq {
+    /// e
+    #[cfg_attr(feature = "swagger", schema(value_type = String))]
+    pub e: SecretKey,
+    /// s
+    #[cfg_attr(feature = "swagger", schema(value_type = String))]
+    pub s: SecretKey,
+}
+
+/// Proof Dleq
+///
+/// Defined in [NUT12](https://github.com/cashubtc/nuts/blob/main/12.md)
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct ProofDleq {
+    /// e
+    #[cfg_attr(feature = "swagger", schema(value_type = String))]
+    pub e: SecretKey,
+    /// s
+    #[cfg_attr(feature = "swagger", schema(value_type = String))]
+    pub s: SecretKey,
+    /// Blinding factor
+    #[cfg_attr(feature = "swagger", schema(value_type = String))]
+    pub r: SecretKey,
+}

+ 256 - 0
crates/cdk-types/src/nuts/nut13.rs

@@ -0,0 +1,256 @@
+//! NUT-13: Deterministic Secrets
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/13.md>
+
+use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
+use thiserror::Error;
+use tracing::instrument;
+
+use super::nut00::{BlindedMessage, PreMint, PreMintSecrets};
+use super::nut01::SecretKey;
+use super::nut02::Id;
+use crate::amount::SplitTarget;
+use crate::dhke::blind_message;
+use crate::secret::Secret;
+use crate::util::hex;
+use crate::{Amount, SECP256K1};
+
+/// NUT13 Error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// DHKE error
+    #[error(transparent)]
+    DHKE(#[from] crate::dhke::Error),
+    /// Amount Error
+    #[error(transparent)]
+    Amount(#[from] crate::amount::Error),
+    /// NUT00 Error
+    #[error(transparent)]
+    NUT00(#[from] crate::nuts::nut00::Error),
+    /// NUT02 Error
+    #[error(transparent)]
+    NUT02(#[from] crate::nuts::nut02::Error),
+    /// Bip32 Error
+    #[error(transparent)]
+    Bip32(#[from] bitcoin::bip32::Error),
+}
+
+impl Secret {
+    /// Create new [`Secret`] from xpriv
+    pub fn from_xpriv(xpriv: Xpriv, keyset_id: Id, counter: u32) -> Result<Self, Error> {
+        let path = derive_path_from_keyset_id(keyset_id)?
+            .child(ChildNumber::from_hardened_idx(counter)?)
+            .child(ChildNumber::from_normal_idx(0)?);
+        let derived_xpriv = xpriv.derive_priv(&SECP256K1, &path)?;
+
+        Ok(Self::new(hex::encode(
+            derived_xpriv.private_key.secret_bytes(),
+        )))
+    }
+}
+
+impl SecretKey {
+    /// Create new [`SecretKey`] from xpriv
+    pub fn from_xpriv(xpriv: Xpriv, keyset_id: Id, counter: u32) -> Result<Self, Error> {
+        let path = derive_path_from_keyset_id(keyset_id)?
+            .child(ChildNumber::from_hardened_idx(counter)?)
+            .child(ChildNumber::from_normal_idx(1)?);
+        let derived_xpriv = xpriv.derive_priv(&SECP256K1, &path)?;
+
+        Ok(Self::from(derived_xpriv.private_key))
+    }
+}
+
+impl PreMintSecrets {
+    /// Generate blinded messages from predetermined secrets and blindings
+    /// factor
+    #[instrument(skip(xpriv))]
+    pub fn from_xpriv(
+        keyset_id: Id,
+        counter: u32,
+        xpriv: Xpriv,
+        amount: Amount,
+        amount_split_target: &SplitTarget,
+    ) -> Result<Self, Error> {
+        let mut pre_mint_secrets = PreMintSecrets::new(keyset_id);
+
+        let mut counter = counter;
+
+        for amount in amount.split_targeted(amount_split_target)? {
+            let secret = Secret::from_xpriv(xpriv, keyset_id, counter)?;
+            let blinding_factor = SecretKey::from_xpriv(xpriv, keyset_id, counter)?;
+
+            let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor))?;
+
+            let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);
+
+            let pre_mint = PreMint {
+                blinded_message,
+                secret: secret.clone(),
+                r,
+                amount,
+            };
+
+            pre_mint_secrets.secrets.push(pre_mint);
+            counter += 1;
+        }
+
+        Ok(pre_mint_secrets)
+    }
+
+    /// New [`PreMintSecrets`] from xpriv with a zero amount used for change
+    pub fn from_xpriv_blank(
+        keyset_id: Id,
+        counter: u32,
+        xpriv: Xpriv,
+        amount: Amount,
+    ) -> Result<Self, Error> {
+        if amount <= Amount::ZERO {
+            return Ok(PreMintSecrets::new(keyset_id));
+        }
+        let count = ((u64::from(amount) as f64).log2().ceil() as u64).max(1);
+        let mut pre_mint_secrets = PreMintSecrets::new(keyset_id);
+
+        let mut counter = counter;
+
+        for _ in 0..count {
+            let secret = Secret::from_xpriv(xpriv, keyset_id, counter)?;
+            let blinding_factor = SecretKey::from_xpriv(xpriv, keyset_id, counter)?;
+
+            let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor))?;
+
+            let amount = Amount::ZERO;
+
+            let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);
+
+            let pre_mint = PreMint {
+                blinded_message,
+                secret: secret.clone(),
+                r,
+                amount,
+            };
+
+            pre_mint_secrets.secrets.push(pre_mint);
+            counter += 1;
+        }
+
+        Ok(pre_mint_secrets)
+    }
+
+    /// Generate blinded messages from predetermined secrets and blindings
+    /// factor
+    pub fn restore_batch(
+        keyset_id: Id,
+        xpriv: Xpriv,
+        start_count: u32,
+        end_count: u32,
+    ) -> Result<Self, Error> {
+        let mut pre_mint_secrets = PreMintSecrets::new(keyset_id);
+
+        for i in start_count..=end_count {
+            let secret = Secret::from_xpriv(xpriv, keyset_id, i)?;
+            let blinding_factor = SecretKey::from_xpriv(xpriv, keyset_id, i)?;
+
+            let (blinded, r) = blind_message(&secret.to_bytes(), Some(blinding_factor))?;
+
+            let blinded_message = BlindedMessage::new(Amount::ZERO, keyset_id, blinded);
+
+            let pre_mint = PreMint {
+                blinded_message,
+                secret: secret.clone(),
+                r,
+                amount: Amount::ZERO,
+            };
+
+            pre_mint_secrets.secrets.push(pre_mint);
+        }
+
+        Ok(pre_mint_secrets)
+    }
+}
+
+fn derive_path_from_keyset_id(id: Id) -> Result<DerivationPath, Error> {
+    let index = u32::from(id);
+
+    let keyset_child_number = ChildNumber::from_hardened_idx(index)?;
+    Ok(DerivationPath::from(vec![
+        ChildNumber::from_hardened_idx(129372)?,
+        ChildNumber::from_hardened_idx(0)?,
+        keyset_child_number,
+    ]))
+}
+
+#[cfg(test)]
+mod tests {
+    use std::str::FromStr;
+
+    use bip39::Mnemonic;
+    use bitcoin::bip32::DerivationPath;
+    use bitcoin::Network;
+
+    use super::*;
+
+    #[test]
+    fn test_secret_from_seed() {
+        let seed =
+            "half depart obvious quality work element tank gorilla view sugar picture humble";
+        let mnemonic = Mnemonic::from_str(seed).unwrap();
+        let seed: [u8; 64] = mnemonic.to_seed("");
+        let xpriv = Xpriv::new_master(Network::Bitcoin, &seed).unwrap();
+        let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
+
+        let test_secrets = [
+            "485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae",
+            "8f2b39e8e594a4056eb1e6dbb4b0c38ef13b1b2c751f64f810ec04ee35b77270",
+            "bc628c79accd2364fd31511216a0fab62afd4a18ff77a20deded7b858c9860c8",
+            "59284fd1650ea9fa17db2b3acf59ecd0f2d52ec3261dd4152785813ff27a33bf",
+            "576c23393a8b31cc8da6688d9c9a96394ec74b40fdaf1f693a6bb84284334ea0",
+        ];
+
+        for (i, test_secret) in test_secrets.iter().enumerate() {
+            let secret = Secret::from_xpriv(xpriv, keyset_id, i.try_into().unwrap()).unwrap();
+            assert_eq!(secret, Secret::from_str(test_secret).unwrap())
+        }
+    }
+    #[test]
+    fn test_r_from_seed() {
+        let seed =
+            "half depart obvious quality work element tank gorilla view sugar picture humble";
+        let mnemonic = Mnemonic::from_str(seed).unwrap();
+        let seed: [u8; 64] = mnemonic.to_seed("");
+        let xpriv = Xpriv::new_master(Network::Bitcoin, &seed).unwrap();
+        let keyset_id = Id::from_str("009a1f293253e41e").unwrap();
+
+        let test_rs = [
+            "ad00d431add9c673e843d4c2bf9a778a5f402b985b8da2d5550bf39cda41d679",
+            "967d5232515e10b81ff226ecf5a9e2e2aff92d66ebc3edf0987eb56357fd6248",
+            "b20f47bb6ae083659f3aa986bfa0435c55c6d93f687d51a01f26862d9b9a4899",
+            "fb5fca398eb0b1deb955a2988b5ac77d32956155f1c002a373535211a2dfdc29",
+            "5f09bfbfe27c439a597719321e061e2e40aad4a36768bb2bcc3de547c9644bf9",
+        ];
+
+        for (i, test_r) in test_rs.iter().enumerate() {
+            let r = SecretKey::from_xpriv(xpriv, keyset_id, i.try_into().unwrap()).unwrap();
+            assert_eq!(r, SecretKey::from_hex(test_r).unwrap())
+        }
+    }
+
+    #[test]
+    fn test_derive_path_from_keyset_id() {
+        let test_cases = [
+            ("009a1f293253e41e", "m/129372'/0'/864559728'"),
+            ("0000000000000000", "m/129372'/0'/0'"),
+            ("00ffffffffffffff", "m/129372'/0'/33554431'"),
+        ];
+
+        for (id_hex, expected_path) in test_cases {
+            let id = Id::from_str(id_hex).unwrap();
+            let path = derive_path_from_keyset_id(id).unwrap();
+            assert_eq!(
+                DerivationPath::from_str(expected_path).unwrap(),
+                path,
+                "Path derivation failed for ID {id_hex}"
+            );
+        }
+    }
+}

+ 51 - 0
crates/cdk-types/src/nuts/nut14/mod.rs

@@ -0,0 +1,51 @@
+//! NUT-14: Hashed Time Lock Contacts (HTLC)
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/14.md>
+
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+
+pub mod serde_htlc_witness;
+
+/// NUT14 Errors
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Incorrect secret kind
+    #[error("Secret is not a HTLC secret")]
+    IncorrectSecretKind,
+    /// HTLC locktime has already passed
+    #[error("Locktime in past")]
+    LocktimeInPast,
+    /// Hash Required
+    #[error("Hash required")]
+    HashRequired,
+    /// Hash is not valid
+    #[error("Hash is not valid")]
+    InvalidHash,
+    /// Preimage does not match
+    #[error("Preimage does not match")]
+    Preimage,
+    /// Witness Signatures not provided
+    #[error("Witness did not provide signatures")]
+    SignaturesNotProvided,
+    /// Secp256k1 error
+    #[error(transparent)]
+    Secp256k1(#[from] bitcoin::secp256k1::Error),
+    /// NUT11 Error
+    #[error(transparent)]
+    NUT11(#[from] super::nut11::Error),
+    #[error(transparent)]
+    /// Serde Error
+    Serde(#[from] serde_json::Error),
+}
+
+/// HTLC Witness
+#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct HTLCWitness {
+    /// Primage
+    pub preimage: String,
+    /// Signatures
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub signatures: Option<Vec<String>>,
+}

+ 22 - 0
crates/cdk-types/src/nuts/nut14/serde_htlc_witness.rs

@@ -0,0 +1,22 @@
+//! Serde helpers for HTLC Witness
+
+use serde::{de, ser, Deserialize, Deserializer, Serializer};
+
+use super::HTLCWitness;
+
+/// Serialize [HTLCWitness] as stringified JSON
+pub fn serialize<S>(x: &HTLCWitness, s: S) -> Result<S::Ok, S::Error>
+where
+    S: Serializer,
+{
+    s.serialize_str(&serde_json::to_string(&x).map_err(ser::Error::custom)?)
+}
+
+/// Deserialize [HTLCWitness] from stringified JSON
+pub fn deserialize<'de, D>(deserializer: D) -> Result<HTLCWitness, D::Error>
+where
+    D: Deserializer<'de>,
+{
+    let s: String = String::deserialize(deserializer)?;
+    serde_json::from_str(&s).map_err(de::Error::custom)
+}

+ 35 - 0
crates/cdk-types/src/nuts/nut15.rs

@@ -0,0 +1,35 @@
+//! NUT-15: Multipart payments
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/15.md>
+
+use serde::{Deserialize, Serialize};
+
+use super::{CurrencyUnit, PaymentMethod};
+use crate::Amount;
+
+/// Multi-part payment
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[serde(rename = "lowercase")]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct Mpp {
+    /// Amount
+    pub amount: Amount,
+}
+
+/// Mpp Method Settings
+#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct MppMethodSettings {
+    /// Payment Method e.g. bolt11
+    pub method: PaymentMethod,
+    /// Currency Unit e.g. sat
+    pub unit: CurrencyUnit,
+}
+
+/// Mpp Settings
+#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = nut15::Settings))]
+pub struct Settings {
+    /// Method settings
+    pub methods: Vec<MppMethodSettings>,
+}

+ 225 - 0
crates/cdk-types/src/nuts/nut17/manager.rs

@@ -0,0 +1,225 @@
+//! Specific Subscription for the cdk crate
+use std::ops::Deref;
+use std::sync::Arc;
+
+use uuid::Uuid;
+
+use super::{Notification, NotificationPayload, OnSubscription};
+use crate::cdk_database::{self, MintDatabase};
+use crate::nuts::{
+    BlindSignature, MeltQuoteBolt11Response, MeltQuoteState, MintQuoteBolt11Response,
+    MintQuoteState, ProofState,
+};
+use crate::pub_sub;
+
+/// Manager
+/// Publish–subscribe manager
+///
+/// Nut-17 implementation is system-wide and not only through the WebSocket, so
+/// it is possible for another part of the system to subscribe to events.
+pub struct PubSubManager(pub_sub::Manager<NotificationPayload<Uuid>, Notification, OnSubscription>);
+
+#[allow(clippy::default_constructed_unit_structs)]
+impl Default for PubSubManager {
+    fn default() -> Self {
+        PubSubManager(OnSubscription::default().into())
+    }
+}
+
+impl From<Arc<dyn MintDatabase<Err = cdk_database::Error> + Send + Sync>> for PubSubManager {
+    fn from(val: Arc<dyn MintDatabase<Err = cdk_database::Error> + Send + Sync>) -> Self {
+        PubSubManager(OnSubscription(Some(val)).into())
+    }
+}
+
+impl Deref for PubSubManager {
+    type Target = pub_sub::Manager<NotificationPayload<Uuid>, Notification, OnSubscription>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl PubSubManager {
+    /// Helper function to emit a ProofState status
+    pub fn proof_state<E: Into<ProofState>>(&self, event: E) {
+        self.broadcast(event.into().into());
+    }
+
+    /// Helper function to emit a MintQuoteBolt11Response status
+    pub fn mint_quote_bolt11_status<E: Into<MintQuoteBolt11Response<Uuid>>>(
+        &self,
+        quote: E,
+        new_state: MintQuoteState,
+    ) {
+        let mut event = quote.into();
+        event.state = new_state;
+
+        self.broadcast(event.into());
+    }
+
+    /// Helper function to emit a MeltQuoteBolt11Response status
+    pub fn melt_quote_status<E: Into<MeltQuoteBolt11Response<Uuid>>>(
+        &self,
+        quote: E,
+        payment_preimage: Option<String>,
+        change: Option<Vec<BlindSignature>>,
+        new_state: MeltQuoteState,
+    ) {
+        let mut quote = quote.into();
+        quote.state = new_state;
+        quote.paid = Some(new_state == MeltQuoteState::Paid);
+        quote.payment_preimage = payment_preimage;
+        quote.change = change;
+        self.broadcast(quote.into());
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use std::time::Duration;
+
+    use tokio::time::sleep;
+
+    use super::*;
+    use crate::nuts::nut17::{Kind, Params};
+    use crate::nuts::{PublicKey, State};
+
+    #[tokio::test]
+    async fn active_and_drop() {
+        let manager = PubSubManager::default();
+        let params = Params {
+            kind: Kind::ProofState,
+            filters: vec![
+                "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2".to_owned(),
+            ],
+            id: "uno".into(),
+        };
+
+        // Although the same param is used, two subscriptions are created, that
+        // is because each index is unique, thanks to `Unique`, it is the
+        // responsibility of the implementor to make sure that SubId are unique
+        // either globally or per client
+        let subscriptions = vec![
+            manager
+                .try_subscribe(params.clone())
+                .await
+                .expect("valid subscription"),
+            manager
+                .try_subscribe(params)
+                .await
+                .expect("valid subscription"),
+        ];
+        assert_eq!(2, manager.active_subscriptions());
+        drop(subscriptions);
+
+        sleep(Duration::from_millis(10)).await;
+
+        assert_eq!(0, manager.active_subscriptions());
+    }
+
+    #[tokio::test]
+    async fn broadcast() {
+        let manager = PubSubManager::default();
+        let mut subscriptions = [
+            manager
+                .try_subscribe(Params {
+                    kind: Kind::ProofState,
+                    filters: vec![
+                        "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104"
+                            .to_string(),
+                    ],
+                    id: "uno".into(),
+                })
+                .await
+                .expect("valid subscription"),
+            manager
+                .try_subscribe(Params {
+                    kind: Kind::ProofState,
+                    filters: vec![
+                        "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104"
+                            .to_string(),
+                    ],
+                    id: "dos".into(),
+                })
+                .await
+                .expect("valid subscription"),
+        ];
+
+        let event = ProofState {
+            y: PublicKey::from_hex(
+                "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104",
+            )
+            .expect("valid pk"),
+            state: State::Pending,
+            witness: None,
+        };
+
+        manager.broadcast(event.into());
+
+        sleep(Duration::from_millis(10)).await;
+
+        let (sub1, _) = subscriptions[0].try_recv().expect("valid message");
+        assert_eq!("uno", *sub1);
+
+        let (sub1, _) = subscriptions[1].try_recv().expect("valid message");
+        assert_eq!("dos", *sub1);
+
+        assert!(subscriptions[0].try_recv().is_err());
+        assert!(subscriptions[1].try_recv().is_err());
+    }
+
+    #[test]
+    fn parsing_request() {
+        let json = r#"{"kind":"proof_state","filters":["x"],"subId":"uno"}"#;
+        let params: Params = serde_json::from_str(json).expect("valid json");
+        assert_eq!(params.kind, Kind::ProofState);
+        assert_eq!(params.filters, vec!["x"]);
+        assert_eq!(*params.id, "uno");
+    }
+
+    #[tokio::test]
+    async fn json_test() {
+        let manager = PubSubManager::default();
+        let mut subscription = manager
+            .try_subscribe::<Params>(
+                serde_json::from_str(r#"{"kind":"proof_state","filters":["02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104"],"subId":"uno"}"#)
+                    .expect("valid json"),
+            )
+            .await.expect("valid subscription");
+
+        manager.broadcast(
+            ProofState {
+                y: PublicKey::from_hex(
+                    "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104",
+                )
+                .expect("valid pk"),
+                state: State::Pending,
+                witness: None,
+            }
+            .into(),
+        );
+
+        // no one is listening for this event
+        manager.broadcast(
+            ProofState {
+                y: PublicKey::from_hex(
+                    "020000000000000000000000000000000000000000000000000000000000000001",
+                )
+                .expect("valid pk"),
+                state: State::Pending,
+                witness: None,
+            }
+            .into(),
+        );
+
+        sleep(Duration::from_millis(10)).await;
+        let (sub1, msg) = subscription.try_recv().expect("valid message");
+        assert_eq!("uno", *sub1);
+        assert_eq!(
+            r#"{"Y":"02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104","state":"PENDING","witness":null}"#,
+            serde_json::to_string(&msg).expect("valid json")
+        );
+        assert!(subscription.try_recv().is_err());
+    }
+}

+ 189 - 0
crates/cdk-types/src/nuts/nut17/mod.rs

@@ -0,0 +1,189 @@
+//! Specific Subscription for the cdk crate
+use std::str::FromStr;
+
+use serde::de::DeserializeOwned;
+use serde::{Deserialize, Serialize};
+use uuid::Uuid;
+
+use super::PublicKey;
+use crate::nuts::{
+    CurrencyUnit, MeltQuoteBolt11Response, MintQuoteBolt11Response, PaymentMethod, ProofState,
+};
+use crate::pub_sub::{Index, Indexable, SubscriptionGlobalId};
+
+mod manager;
+mod on_subscription;
+pub use manager::PubSubManager;
+pub use on_subscription::OnSubscription;
+
+pub use crate::pub_sub::SubId;
+pub mod ws;
+
+/// Subscription Parameter according to the standard
+#[derive(Debug, Clone, Serialize, Eq, PartialEq, Hash, Deserialize)]
+pub struct Params {
+    /// Kind
+    pub kind: Kind,
+    /// Filters
+    pub filters: Vec<String>,
+    /// Subscription Id
+    #[serde(rename = "subId")]
+    pub id: SubId,
+}
+
+/// Check state Settings
+#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct SupportedSettings {
+    /// Supported methods
+    pub supported: Vec<SupportedMethods>,
+}
+
+/// Supported WS Methods
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct SupportedMethods {
+    /// Payment Method
+    pub method: PaymentMethod,
+    /// Unit
+    pub unit: CurrencyUnit,
+    /// Command
+    pub commands: Vec<String>,
+}
+
+impl SupportedMethods {
+    /// Create [`SupportedMethods`]
+    pub fn new(method: PaymentMethod, unit: CurrencyUnit) -> Self {
+        Self {
+            method,
+            unit,
+            commands: Vec::new(),
+        }
+    }
+}
+
+impl Default for SupportedMethods {
+    fn default() -> Self {
+        SupportedMethods {
+            method: PaymentMethod::Bolt11,
+            unit: CurrencyUnit::Sat,
+            commands: vec![
+                "bolt11_mint_quote".to_owned(),
+                "bolt11_melt_quote".to_owned(),
+                "proof_state".to_owned(),
+            ],
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(bound = "T: Serialize + DeserializeOwned")]
+#[serde(untagged)]
+/// Subscription response
+pub enum NotificationPayload<T> {
+    /// Proof State
+    ProofState(ProofState),
+    /// Melt Quote Bolt11 Response
+    MeltQuoteBolt11Response(MeltQuoteBolt11Response<T>),
+    /// Mint Quote Bolt11 Response
+    MintQuoteBolt11Response(MintQuoteBolt11Response<T>),
+}
+
+impl<T> From<ProofState> for NotificationPayload<T> {
+    fn from(proof_state: ProofState) -> NotificationPayload<T> {
+        NotificationPayload::ProofState(proof_state)
+    }
+}
+
+impl<T> From<MeltQuoteBolt11Response<T>> for NotificationPayload<T> {
+    fn from(melt_quote: MeltQuoteBolt11Response<T>) -> NotificationPayload<T> {
+        NotificationPayload::MeltQuoteBolt11Response(melt_quote)
+    }
+}
+
+impl<T> From<MintQuoteBolt11Response<T>> for NotificationPayload<T> {
+    fn from(mint_quote: MintQuoteBolt11Response<T>) -> NotificationPayload<T> {
+        NotificationPayload::MintQuoteBolt11Response(mint_quote)
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
+/// A parsed notification
+pub enum Notification {
+    /// ProofState id is a Pubkey
+    ProofState(PublicKey),
+    /// MeltQuote id is an Uuid
+    MeltQuoteBolt11(Uuid),
+    /// MintQuote id is an Uuid
+    MintQuoteBolt11(Uuid),
+}
+
+impl Indexable for NotificationPayload<Uuid> {
+    type Type = Notification;
+
+    fn to_indexes(&self) -> Vec<Index<Self::Type>> {
+        match self {
+            NotificationPayload::ProofState(proof_state) => {
+                vec![Index::from(Notification::ProofState(proof_state.y))]
+            }
+            NotificationPayload::MeltQuoteBolt11Response(melt_quote) => {
+                vec![Index::from(Notification::MeltQuoteBolt11(melt_quote.quote))]
+            }
+            NotificationPayload::MintQuoteBolt11Response(mint_quote) => {
+                vec![Index::from(Notification::MintQuoteBolt11(mint_quote.quote))]
+            }
+        }
+    }
+}
+
+/// Kind
+#[derive(Debug, Clone, Copy, Eq, Ord, PartialOrd, PartialEq, Hash, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum Kind {
+    /// Bolt 11 Melt Quote
+    Bolt11MeltQuote,
+    /// Bolt 11 Mint Quote
+    Bolt11MintQuote,
+    /// Proof State
+    ProofState,
+}
+
+impl AsRef<SubId> for Params {
+    fn as_ref(&self) -> &SubId {
+        &self.id
+    }
+}
+
+/// Parsing error
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+    #[error("Uuid Error: {0}")]
+    /// Uuid Error
+    Uuid(#[from] uuid::Error),
+
+    #[error("PublicKey Error: {0}")]
+    /// PublicKey Error
+    PublicKey(#[from] crate::nuts::nut01::Error),
+}
+
+impl TryFrom<Params> for Vec<Index<Notification>> {
+    type Error = Error;
+
+    fn try_from(val: Params) -> Result<Self, Self::Error> {
+        let sub_id: SubscriptionGlobalId = Default::default();
+        val.filters
+            .into_iter()
+            .map(|filter| {
+                let idx = match val.kind {
+                    Kind::Bolt11MeltQuote => {
+                        Notification::MeltQuoteBolt11(Uuid::from_str(&filter)?)
+                    }
+                    Kind::Bolt11MintQuote => {
+                        Notification::MintQuoteBolt11(Uuid::from_str(&filter)?)
+                    }
+                    Kind::ProofState => Notification::ProofState(PublicKey::from_str(&filter)?),
+                };
+
+                Ok(Index::from((idx, val.id.clone(), sub_id)))
+            })
+            .collect::<Result<_, _>>()
+    }
+}

+ 93 - 0
crates/cdk-types/src/nuts/nut17/on_subscription.rs

@@ -0,0 +1,93 @@
+//! On Subscription
+//!
+//! This module contains the code that is triggered when a new subscription is created.
+use std::sync::Arc;
+
+use uuid::Uuid;
+
+use super::{Notification, NotificationPayload};
+use crate::cdk_database::{self, MintDatabase};
+use crate::nuts::{MeltQuoteBolt11Response, MintQuoteBolt11Response, ProofState, PublicKey};
+use crate::pub_sub::OnNewSubscription;
+
+#[derive(Default)]
+/// Subscription Init
+///
+/// This struct triggers code when a new subscription is created.
+///
+/// It is used to send the initial state of the subscription to the client.
+pub struct OnSubscription(
+    pub(crate) Option<Arc<dyn MintDatabase<Err = cdk_database::Error> + Send + Sync>>,
+);
+
+#[async_trait::async_trait]
+impl OnNewSubscription for OnSubscription {
+    type Event = NotificationPayload<Uuid>;
+    type Index = Notification;
+
+    async fn on_new_subscription(
+        &self,
+        request: &[&Self::Index],
+    ) -> Result<Vec<Self::Event>, String> {
+        let datastore = if let Some(localstore) = self.0.as_ref() {
+            localstore
+        } else {
+            return Ok(vec![]);
+        };
+
+        let mut to_return = vec![];
+        let mut public_keys: Vec<PublicKey> = Vec::new();
+        let mut melt_queries = Vec::new();
+        let mut mint_queries = Vec::new();
+
+        for idx in request.iter() {
+            match idx {
+                Notification::ProofState(pk) => public_keys.push(*pk),
+                Notification::MeltQuoteBolt11(uuid) => {
+                    melt_queries.push(datastore.get_melt_quote(uuid))
+                }
+                Notification::MintQuoteBolt11(uuid) => {
+                    mint_queries.push(datastore.get_mint_quote(uuid))
+                }
+            }
+        }
+
+        to_return.extend(
+            futures::future::try_join_all(melt_queries)
+                .await
+                .map(|quotes| {
+                    quotes
+                        .into_iter()
+                        .filter_map(|quote| quote.map(|x| x.into()))
+                        .map(|x: MeltQuoteBolt11Response<Uuid>| x.into())
+                        .collect::<Vec<_>>()
+                })
+                .map_err(|e| e.to_string())?,
+        );
+        to_return.extend(
+            futures::future::try_join_all(mint_queries)
+                .await
+                .map(|quotes| {
+                    quotes
+                        .into_iter()
+                        .filter_map(|quote| quote.map(|x| x.into()))
+                        .map(|x: MintQuoteBolt11Response<Uuid>| x.into())
+                        .collect::<Vec<_>>()
+                })
+                .map_err(|e| e.to_string())?,
+        );
+
+        to_return.extend(
+            datastore
+                .get_proofs_states(public_keys.as_slice())
+                .await
+                .map_err(|e| e.to_string())?
+                .into_iter()
+                .enumerate()
+                .filter_map(|(idx, state)| state.map(|state| (public_keys[idx], state).into()))
+                .map(|state: ProofState| state.into()),
+        );
+
+        Ok(to_return)
+    }
+}

+ 215 - 0
crates/cdk-types/src/nuts/nut17/ws.rs

@@ -0,0 +1,215 @@
+//! Websocket types
+
+use serde::de::DeserializeOwned;
+use serde::{Deserialize, Serialize};
+use uuid::Uuid;
+
+use super::{NotificationPayload, Params, SubId};
+
+/// JSON RPC version
+pub const JSON_RPC_VERSION: &str = "2.0";
+
+/// The response to a subscription request
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct WsSubscribeResponse {
+    /// Status
+    pub status: String,
+    /// Subscription ID
+    #[serde(rename = "subId")]
+    pub sub_id: SubId,
+}
+
+/// The response to an unsubscription request
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct WsUnsubscribeResponse {
+    /// Status
+    pub status: String,
+    /// Subscription ID
+    #[serde(rename = "subId")]
+    pub sub_id: SubId,
+}
+
+/// The notification
+///
+/// This is the notification that is sent to the client when an event matches a
+/// subscription
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(bound = "T: Serialize + DeserializeOwned")]
+pub struct NotificationInner<T> {
+    /// The subscription ID
+    #[serde(rename = "subId")]
+    pub sub_id: SubId,
+
+    /// The notification payload
+    pub payload: NotificationPayload<T>,
+}
+
+impl From<NotificationInner<Uuid>> for NotificationInner<String> {
+    fn from(value: NotificationInner<Uuid>) -> Self {
+        NotificationInner {
+            sub_id: value.sub_id,
+            payload: match value.payload {
+                NotificationPayload::ProofState(pk) => NotificationPayload::ProofState(pk),
+                NotificationPayload::MeltQuoteBolt11Response(quote) => {
+                    NotificationPayload::MeltQuoteBolt11Response(quote.to_string_id())
+                }
+                NotificationPayload::MintQuoteBolt11Response(quote) => {
+                    NotificationPayload::MintQuoteBolt11Response(quote.to_string_id())
+                }
+            },
+        }
+    }
+}
+
+/// Responses from the web socket server
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum WsResponseResult {
+    /// A response to a subscription request
+    Subscribe(WsSubscribeResponse),
+    /// Unsubscribe
+    Unsubscribe(WsUnsubscribeResponse),
+}
+
+impl From<WsSubscribeResponse> for WsResponseResult {
+    fn from(response: WsSubscribeResponse) -> Self {
+        WsResponseResult::Subscribe(response)
+    }
+}
+
+impl From<WsUnsubscribeResponse> for WsResponseResult {
+    fn from(response: WsUnsubscribeResponse) -> Self {
+        WsResponseResult::Unsubscribe(response)
+    }
+}
+
+/// The request to unsubscribe
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct WsUnsubscribeRequest {
+    /// Subscription ID
+    #[serde(rename = "subId")]
+    pub sub_id: SubId,
+}
+
+/// The inner method of the websocket request
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case", tag = "method", content = "params")]
+pub enum WsMethodRequest {
+    /// Subscribe method
+    Subscribe(Params),
+    /// Unsubscribe method
+    Unsubscribe(WsUnsubscribeRequest),
+}
+
+/// Websocket request
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct WsRequest {
+    /// JSON RPC version
+    pub jsonrpc: String,
+    /// The method body
+    #[serde(flatten)]
+    pub method: WsMethodRequest,
+    /// The request ID
+    pub id: usize,
+}
+
+impl From<(WsMethodRequest, usize)> for WsRequest {
+    fn from((method, id): (WsMethodRequest, usize)) -> Self {
+        WsRequest {
+            jsonrpc: JSON_RPC_VERSION.to_owned(),
+            method,
+            id,
+        }
+    }
+}
+
+/// Notification from the server
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct WsNotification<T> {
+    /// JSON RPC version
+    pub jsonrpc: String,
+    /// The method
+    pub method: String,
+    /// The parameters
+    pub params: T,
+}
+
+/// Websocket error
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct WsErrorBody {
+    /// Error code
+    pub code: i32,
+    /// Error message
+    pub message: String,
+}
+
+/// Websocket response
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct WsResponse {
+    /// JSON RPC version
+    pub jsonrpc: String,
+    /// The result
+    pub result: WsResponseResult,
+    /// The request ID
+    pub id: usize,
+}
+
+/// WebSocket error response
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct WsErrorResponse {
+    /// JSON RPC version
+    pub jsonrpc: String,
+    /// The result
+    pub error: WsErrorBody,
+    /// The request ID
+    pub id: usize,
+}
+
+/// Message from the server to the client
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum WsMessageOrResponse {
+    /// A response to a request
+    Response(WsResponse),
+    /// An error response
+    ErrorResponse(WsErrorResponse),
+    /// A notification
+    Notification(WsNotification<NotificationInner<String>>),
+}
+
+impl From<(usize, Result<WsResponseResult, WsErrorBody>)> for WsMessageOrResponse {
+    fn from((id, result): (usize, Result<WsResponseResult, WsErrorBody>)) -> Self {
+        match result {
+            Ok(result) => WsMessageOrResponse::Response(WsResponse {
+                jsonrpc: JSON_RPC_VERSION.to_owned(),
+                result,
+                id,
+            }),
+            Err(err) => WsMessageOrResponse::ErrorResponse(WsErrorResponse {
+                jsonrpc: JSON_RPC_VERSION.to_owned(),
+                error: err,
+                id,
+            }),
+        }
+    }
+}
+
+impl From<NotificationInner<Uuid>> for WsMessageOrResponse {
+    fn from(notification: NotificationInner<Uuid>) -> Self {
+        WsMessageOrResponse::Notification(WsNotification {
+            jsonrpc: JSON_RPC_VERSION.to_owned(),
+            method: "subscribe".to_string(),
+            params: notification.into(),
+        })
+    }
+}
+
+impl From<NotificationInner<String>> for WsMessageOrResponse {
+    fn from(notification: NotificationInner<String>) -> Self {
+        WsMessageOrResponse::Notification(WsNotification {
+            jsonrpc: JSON_RPC_VERSION.to_owned(),
+            method: "subscribe".to_string(),
+            params: notification,
+        })
+    }
+}

+ 203 - 0
crates/cdk-types/src/nuts/nut18.rs

@@ -0,0 +1,203 @@
+//! NUT-18: Payment Requests
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/18.md>
+
+use std::fmt;
+use std::str::FromStr;
+
+use bitcoin::base64::engine::{general_purpose, GeneralPurpose};
+use bitcoin::base64::{alphabet, Engine};
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+
+use super::{CurrencyUnit, Proofs};
+use crate::mint_url::MintUrl;
+use crate::Amount;
+
+const PAYMENT_REQUEST_PREFIX: &str = "creqA";
+
+/// NUT18 Error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Invalid Prefix
+    #[error("Invalid Prefix")]
+    InvalidPrefix,
+    /// Ciborium error
+    #[error(transparent)]
+    CiboriumError(#[from] ciborium::de::Error<std::io::Error>),
+    /// Base64 error
+    #[error(transparent)]
+    Base64Error(#[from] bitcoin::base64::DecodeError),
+}
+
+/// Transport Type
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub enum TransportType {
+    /// Nostr
+    #[serde(rename = "nostr")]
+    Nostr,
+    /// Http post
+    #[serde(rename = "post")]
+    HttpPost,
+}
+
+impl fmt::Display for TransportType {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        use serde::ser::Error;
+        let t = serde_json::to_string(self).map_err(|e| fmt::Error::custom(e.to_string()))?;
+        write!(f, "{}", t)
+    }
+}
+
+impl FromStr for Transport {
+    type Err = serde_json::Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        serde_json::from_str(s)
+    }
+}
+
+/// Transport
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct Transport {
+    /// Type
+    #[serde(rename = "t")]
+    pub _type: TransportType,
+    /// Target
+    #[serde(rename = "a")]
+    pub target: String,
+    /// Tags
+    #[serde(rename = "g")]
+    pub tags: Option<Vec<Vec<String>>>,
+}
+
+/// Payment Request
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct PaymentRequest {
+    /// `Payment id`
+    #[serde(rename = "i")]
+    pub payment_id: Option<String>,
+    /// Amount
+    #[serde(rename = "a")]
+    pub amount: Option<Amount>,
+    /// Unit
+    #[serde(rename = "u")]
+    pub unit: Option<CurrencyUnit>,
+    /// Single use
+    #[serde(rename = "s")]
+    pub single_use: Option<bool>,
+    /// Mints
+    #[serde(rename = "m")]
+    pub mints: Option<Vec<MintUrl>>,
+    /// Description
+    #[serde(rename = "d")]
+    pub description: Option<String>,
+    /// Transport
+    #[serde(rename = "t")]
+    pub transports: Vec<Transport>,
+}
+
+impl fmt::Display for PaymentRequest {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        use serde::ser::Error;
+        let mut data = Vec::new();
+        ciborium::into_writer(self, &mut data).map_err(|e| fmt::Error::custom(e.to_string()))?;
+        let encoded = general_purpose::URL_SAFE.encode(data);
+        write!(f, "{}{}", PAYMENT_REQUEST_PREFIX, encoded)
+    }
+}
+
+impl FromStr for PaymentRequest {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let s = s
+            .strip_prefix(PAYMENT_REQUEST_PREFIX)
+            .ok_or(Error::InvalidPrefix)?;
+
+        let decode_config = general_purpose::GeneralPurposeConfig::new()
+            .with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
+        let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
+
+        Ok(ciborium::from_reader(&decoded[..])?)
+    }
+}
+
+/// Payment Request
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct PaymentRequestPayload {
+    /// Id
+    pub id: Option<String>,
+    /// Memo
+    pub memo: Option<String>,
+    /// Mint
+    pub mint: MintUrl,
+    /// Unit
+    pub unit: CurrencyUnit,
+    /// Proofs
+    pub proofs: Proofs,
+}
+
+#[cfg(test)]
+mod tests {
+    use std::str::FromStr;
+
+    use super::*;
+
+    const PAYMENT_REQUEST: &str = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF4Imh0dHBzOi8vbm9mZWVzLnRlc3RudXQuY2FzaHUuc3BhY2U=";
+
+    #[test]
+    fn test_decode_payment_req() -> anyhow::Result<()> {
+        let req = PaymentRequest::from_str(PAYMENT_REQUEST)?;
+
+        assert_eq!(&req.payment_id.unwrap(), "b7a90176");
+        assert_eq!(req.amount.unwrap(), 10.into());
+        assert_eq!(req.unit.clone().unwrap(), CurrencyUnit::Sat);
+        assert_eq!(
+            req.mints.unwrap(),
+            vec![MintUrl::from_str("https://nofees.testnut.cashu.space")?]
+        );
+        assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
+
+        let transport = req.transports.first().unwrap();
+
+        let expected_transport = Transport {_type: TransportType::Nostr, target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])};
+
+        assert_eq!(transport, &expected_transport);
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_roundtrip_payment_req() -> anyhow::Result<()> {
+        let transport = Transport {_type: TransportType::Nostr, target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), tags: Some(vec![vec!["n".to_string(), "17".to_string()]])};
+
+        let request = PaymentRequest {
+            payment_id: Some("b7a90176".to_string()),
+            amount: Some(10.into()),
+            unit: Some(CurrencyUnit::Sat),
+            single_use: None,
+            mints: Some(vec!["https://nofees.testnut.cashu.space".parse()?]),
+            description: None,
+            transports: vec![transport.clone()],
+        };
+
+        let request_str = request.to_string();
+
+        let req = PaymentRequest::from_str(&request_str)?;
+
+        assert_eq!(&req.payment_id.unwrap(), "b7a90176");
+        assert_eq!(req.amount.unwrap(), 10.into());
+        assert_eq!(req.unit.clone().unwrap(), CurrencyUnit::Sat);
+        assert_eq!(
+            req.mints.unwrap(),
+            vec![MintUrl::from_str("https://nofees.testnut.cashu.space")?]
+        );
+        assert_eq!(req.unit.unwrap(), CurrencyUnit::Sat);
+
+        let t = req.transports.first().unwrap();
+        assert_eq!(&transport, t);
+
+        Ok(())
+    }
+}

+ 55 - 0
crates/cdk-types/src/nuts/nut19.rs

@@ -0,0 +1,55 @@
+//! NUT-19: Cached Responses
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/19.md>
+
+use serde::{Deserialize, Serialize};
+
+/// Mint settings
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct Settings {
+    /// Number of seconds the responses are cached for
+    pub ttl: Option<u64>,
+    /// Cached endpoints
+    pub cached_endpoints: Vec<CachedEndpoint>,
+}
+
+/// List of the methods and paths for which caching is enabled
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct CachedEndpoint {
+    /// HTTP Method
+    pub method: Method,
+    /// Route path
+    pub path: Path,
+}
+
+impl CachedEndpoint {
+    /// Create [`CachedEndpoint`]
+    pub fn new(method: Method, path: Path) -> Self {
+        Self { method, path }
+    }
+}
+
+/// HTTP method
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[serde(rename_all = "UPPERCASE")]
+pub enum Method {
+    /// Get
+    Get,
+    /// POST
+    Post,
+}
+
+/// Route path
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum Path {
+    /// Bolt11 Mint
+    #[serde(rename = "/v1/mint/bolt11")]
+    MintBolt11,
+    /// Bolt11 Melt
+    #[serde(rename = "/v1/melt/bolt11")]
+    MeltBolt11,
+    /// Swap
+    #[serde(rename = "/v1/swap")]
+    Swap,
+}

+ 151 - 0
crates/cdk-types/src/nuts/nut20.rs

@@ -0,0 +1,151 @@
+//! Mint Quote Signatures
+
+use std::str::FromStr;
+
+use bitcoin::secp256k1::schnorr::Signature;
+use thiserror::Error;
+
+use super::{MintBolt11Request, PublicKey, SecretKey};
+
+/// Nut19 Error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Signature not provided
+    #[error("Signature not provided")]
+    SignatureMissing,
+    /// Quote signature invalid signature
+    #[error("Quote signature invalid signature")]
+    InvalidSignature,
+    /// Nut01 error
+    #[error(transparent)]
+    NUT01(#[from] crate::nuts::nut01::Error),
+}
+
+impl<Q> MintBolt11Request<Q>
+where
+    Q: ToString,
+{
+    /// Constructs the message to be signed according to NUT-20 specification.
+    ///
+    /// The message is constructed by concatenating (as UTF-8 encoded bytes):
+    /// 1. The quote ID (as UTF-8)
+    /// 2. All blinded secrets (B_0 through B_n) converted to hex strings (as UTF-8)
+    ///
+    /// Format: `quote_id || B_0 || B_1 || ... || B_n`
+    /// where each component is encoded as UTF-8 bytes
+    pub fn msg_to_sign(&self) -> Vec<u8> {
+        // Pre-calculate capacity to avoid reallocations
+        let quote_id = self.quote.to_string();
+        let capacity = quote_id.len() + (self.outputs.len() * 66);
+        let mut msg = Vec::with_capacity(capacity);
+        msg.append(&mut quote_id.clone().into_bytes()); // String.into_bytes() produces UTF-8
+        for output in &self.outputs {
+            // to_hex() creates a hex string, into_bytes() converts it to UTF-8 bytes
+            msg.append(&mut output.blinded_secret.to_hex().into_bytes());
+        }
+        msg
+    }
+
+    /// Sign [`MintBolt11Request`]
+    pub fn sign(&mut self, secret_key: SecretKey) -> Result<(), Error> {
+        let msg = self.msg_to_sign();
+
+        let signature: Signature = secret_key.sign(&msg)?;
+
+        self.signature = Some(signature.to_string());
+
+        Ok(())
+    }
+
+    /// Verify signature on [`MintBolt11Request`]
+    pub fn verify_signature(&self, pubkey: PublicKey) -> Result<(), Error> {
+        let signature = self.signature.as_ref().ok_or(Error::SignatureMissing)?;
+
+        let signature = Signature::from_str(signature).map_err(|_| Error::InvalidSignature)?;
+
+        let msg_to_sign = self.msg_to_sign();
+
+        pubkey.verify(&msg_to_sign, &signature)?;
+
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+
+    use uuid::Uuid;
+
+    use super::*;
+
+    #[test]
+    fn test_msg_to_sign() {
+        let request: MintBolt11Request<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}],"signature":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap();
+
+        // let expected_msg_to_sign = "9d745270-1405-46de-b5c5-e2762b4f5e000342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c31102be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b5302209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79";
+
+        let expected_msg_to_sign = [
+            57, 100, 55, 52, 53, 50, 55, 48, 45, 49, 52, 48, 53, 45, 52, 54, 100, 101, 45, 98, 53,
+            99, 53, 45, 101, 50, 55, 54, 50, 98, 52, 102, 53, 101, 48, 48, 48, 51, 52, 50, 101, 53,
+            98, 99, 99, 55, 55, 102, 53, 98, 50, 97, 51, 99, 50, 97, 102, 98, 52, 48, 98, 98, 53,
+            57, 49, 97, 49, 101, 50, 55, 100, 97, 56, 51, 99, 100, 100, 99, 57, 54, 56, 97, 98,
+            100, 99, 48, 101, 99, 52, 57, 48, 52, 50, 48, 49, 97, 50, 48, 49, 56, 51, 52, 48, 51,
+            50, 102, 100, 51, 99, 52, 100, 99, 52, 57, 97, 50, 56, 52, 52, 97, 56, 57, 57, 57, 56,
+            100, 53, 101, 57, 100, 53, 98, 48, 102, 48, 98, 48, 48, 100, 100, 101, 57, 51, 49, 48,
+            48, 54, 51, 97, 99, 98, 56, 97, 57, 50, 101, 50, 102, 100, 97, 102, 97, 52, 49, 50, 54,
+            100, 52, 48, 51, 51, 98, 54, 102, 100, 101, 53, 48, 98, 54, 97, 48, 100, 102, 101, 54,
+            49, 97, 100, 49, 52, 56, 102, 102, 102, 49, 54, 55, 97, 100, 57, 99, 102, 56, 51, 48,
+            56, 100, 101, 100, 53, 102, 54, 102, 54, 98, 50, 102, 101, 48, 48, 48, 97, 48, 51, 54,
+            99, 52, 54, 52, 99, 51, 49, 49, 48, 50, 98, 101, 53, 97, 53, 53, 102, 48, 51, 101, 53,
+            99, 48, 97, 97, 101, 97, 55, 55, 53, 57, 53, 100, 53, 55, 52, 98, 99, 101, 57, 50, 99,
+            54, 100, 53, 55, 97, 50, 97, 48, 102, 98, 50, 98, 53, 57, 53, 53, 99, 48, 98, 56, 55,
+            101, 52, 53, 50, 48, 101, 48, 54, 98, 53, 51, 48, 50, 50, 48, 57, 102, 99, 50, 56, 55,
+            51, 102, 50, 56, 53, 50, 49, 99, 98, 100, 100, 101, 55, 102, 55, 98, 51, 98, 98, 49,
+            53, 50, 49, 48, 48, 50, 52, 54, 51, 102, 53, 57, 55, 57, 54, 56, 54, 102, 100, 49, 53,
+            54, 102, 50, 51, 102, 101, 54, 97, 56, 97, 97, 50, 98, 55, 57,
+        ]
+        .to_vec();
+
+        let request_msg_to_sign = request.msg_to_sign();
+
+        assert_eq!(expected_msg_to_sign, request_msg_to_sign);
+    }
+
+    #[test]
+    fn test_valid_signature() {
+        let pubkey = PublicKey::from_hex(
+            "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
+        )
+        .unwrap();
+
+        let request: MintBolt11Request<Uuid> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}], "signature": "d4b386f21f7aa7172f0994ee6e4dd966539484247ea71c99b81b8e09b1bb2acbc0026a43c221fd773471dc30d6a32b04692e6837ddaccf0830a63128308e4ee0"}"#).unwrap();
+
+        assert!(request.verify_signature(pubkey).is_ok());
+    }
+
+    #[test]
+    fn test_mint_request_signature() {
+        let mut request: MintBolt11Request<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}]}"#).unwrap();
+
+        let secret =
+            SecretKey::from_hex("50d7fd7aa2b2fe4607f41f4ce6f8794fc184dd47b8cdfbe4b3d1249aa02d35aa")
+                .unwrap();
+
+        request.sign(secret.clone()).unwrap();
+
+        assert!(request.verify_signature(secret.public_key()).is_ok());
+    }
+
+    #[test]
+    fn test_invalid_signature() {
+        let pubkey = PublicKey::from_hex(
+            "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
+        )
+        .unwrap();
+
+        let request: MintBolt11Request<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}],"signature":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap();
+
+        // Signature is on a different quote id verification should fail
+        assert!(request.verify_signature(pubkey).is_err());
+    }
+}

+ 151 - 0
crates/cdk-types/src/secret.rs

@@ -0,0 +1,151 @@
+//! Secret
+
+use std::fmt;
+use std::str::FromStr;
+
+use bitcoin::secp256k1::rand::{self, RngCore};
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+
+use crate::hex;
+
+/// The secret data that allows spending ecash
+#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[serde(transparent)]
+pub struct Secret(String);
+
+/// Secret Errors
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Invalid Length
+    #[error("Invalid secret length: `{0}`")]
+    InvalidLength(u64),
+    /// Hex Error
+    #[error(transparent)]
+    Hex(#[from] hex::Error),
+    /// Serde Json error
+    #[error(transparent)]
+    SerdeJsonError(#[from] serde_json::Error),
+}
+
+impl Default for Secret {
+    fn default() -> Self {
+        Self::generate()
+    }
+}
+
+impl Secret {
+    /// Create new [`Secret`]
+    #[inline]
+    pub fn new<S>(secret: S) -> Self
+    where
+        S: Into<String>,
+    {
+        Self(secret.into())
+    }
+
+    /// Create secret value
+    /// Generate a new random secret as the recommended 32 byte hex
+    pub fn generate() -> Self {
+        let mut rng = rand::thread_rng();
+
+        let mut random_bytes = [0u8; 32];
+
+        // Generate random bytes
+        rng.fill_bytes(&mut random_bytes);
+        // The secret string is hex encoded
+        let secret = hex::encode(random_bytes);
+        Self(secret)
+    }
+
+    /// [`Secret`] as bytes
+    #[inline]
+    pub fn as_bytes(&self) -> &[u8] {
+        self.0.as_bytes()
+    }
+
+    /// [`Secret`] to bytes
+    #[inline]
+    pub fn to_bytes(&self) -> Vec<u8> {
+        self.as_bytes().to_vec()
+    }
+
+    /// Check if secret is P2PK secret
+    pub fn is_p2pk(&self) -> bool {
+        use crate::nuts::Kind;
+
+        let secret: Result<crate::nuts::nut10::Secret, serde_json::Error> =
+            serde_json::from_str(&self.0);
+
+        if let Ok(secret) = secret {
+            if secret.kind.eq(&Kind::P2PK) {
+                return true;
+            }
+        }
+
+        false
+    }
+}
+
+impl FromStr for Secret {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(Self(s.to_string()))
+    }
+}
+
+impl fmt::Display for Secret {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.0)
+    }
+}
+
+impl From<Secret> for Vec<u8> {
+    fn from(value: Secret) -> Vec<u8> {
+        value.to_bytes()
+    }
+}
+
+impl From<&Secret> for Vec<u8> {
+    fn from(value: &Secret) -> Vec<u8> {
+        value.to_bytes()
+    }
+}
+
+impl TryFrom<Secret> for crate::nuts::nut10::Secret {
+    type Error = serde_json::Error;
+
+    fn try_from(unchecked_secret: Secret) -> Result<crate::nuts::nut10::Secret, Self::Error> {
+        serde_json::from_str(&unchecked_secret.0)
+    }
+}
+
+impl TryFrom<&Secret> for crate::nuts::nut10::Secret {
+    type Error = Error;
+
+    fn try_from(unchecked_secret: &Secret) -> Result<crate::nuts::nut10::Secret, Self::Error> {
+        Ok(serde_json::from_str(&unchecked_secret.0)?)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::assert_eq;
+    use std::str::FromStr;
+
+    use super::*;
+
+    #[test]
+    fn test_secret_from_str() {
+        let secret = Secret::generate();
+
+        let secret_str = secret.to_string();
+
+        assert_eq!(hex::decode(secret_str.clone()).unwrap().len(), 32);
+
+        let secret_n = Secret::from_str(&secret_str).unwrap();
+
+        assert_eq!(secret_n, secret)
+    }
+}

+ 5 - 2
crates/cdk/Cargo.toml

@@ -19,7 +19,6 @@ wallet = ["dep:reqwest"]
 bench = []
 http_subscription = []
 
-
 [dependencies]
 async-trait = "0.1"
 anyhow = { version = "1.0.43", features = ["backtrace"] }
@@ -31,6 +30,7 @@ bitcoin = { version = "0.32.2", features = [
 ] }
 ciborium = { version = "0.2.2", default-features = false, features = ["std"] }
 cbor-diag = "0.1.12"
+cdk-types = { path = "../cdk-types" }
 lightning-invoice = { version = "0.32.0", features = ["serde", "std"] }
 once_cell = "1.19"
 regex = "1"
@@ -39,7 +39,10 @@ reqwest = { version = "0.12", default-features = false, features = [
     "rustls-tls",
     "rustls-tls-native-roots",
     "socks",
-    "zstd", "brotli", "gzip", "deflate"
+    "zstd",
+    "brotli",
+    "gzip",
+    "deflate",
 ], optional = true }
 serde = { version = "1", default-features = false, features = ["derive"] }
 serde_json = "1"

+ 17 - 6
crates/cdk/src/cdk_database/mint_memory.rs

@@ -37,18 +37,21 @@ pub struct MintMemoryDatabase {
 impl MintMemoryDatabase {
     /// Create new [`MintMemoryDatabase`]
     #[allow(clippy::too_many_arguments)]
-    pub fn new(
+    pub fn new<P: Into<Proofs>>(
         active_keysets: HashMap<CurrencyUnit, Id>,
         keysets: Vec<MintKeySetInfo>,
         mint_quotes: Vec<MintQuote>,
         melt_quotes: Vec<mint::MeltQuote>,
-        pending_proofs: Proofs,
-        spent_proofs: Proofs,
+        pending_proofs: P,
+        spent_proofs: P,
         quote_proofs: HashMap<Uuid, Vec<PublicKey>>,
         blinded_signatures: HashMap<[u8; 33], BlindSignature>,
         quote_signatures: HashMap<Uuid, Vec<BlindSignature>>,
         melt_request: Vec<(MeltBolt11Request<Uuid>, LnKey)>,
     ) -> Result<Self, Error> {
+        let pending_proofs = pending_proofs.into();
+        let spent_proofs = spent_proofs.into();
+
         let mut proofs = HashMap::new();
         let mut proof_states = HashMap::new();
 
@@ -252,7 +255,12 @@ impl MintDatabase for MintMemoryDatabase {
         Ok(melt_request.cloned())
     }
 
-    async fn add_proofs(&self, proofs: Proofs, quote_id: Option<Uuid>) -> Result<(), Self::Err> {
+    async fn add_proofs<P: Into<Proofs>>(
+        &self,
+        proofs: P,
+        quote_id: Option<Uuid>,
+    ) -> Result<(), Self::Err> {
+        let proofs = proofs.into();
         let mut db_proofs = self.proofs.write().await;
 
         let mut ys = Vec::with_capacity(proofs.capacity());
@@ -275,7 +283,10 @@ impl MintDatabase for MintMemoryDatabase {
         Ok(())
     }
 
-    async fn get_proofs_by_ys(&self, ys: &[PublicKey]) -> Result<Vec<Option<Proof>>, Self::Err> {
+    async fn get_proofs_by_ys(
+        &self,
+        ys: &[PublicKey],
+    ) -> Result<Vec<Option<cdk_types::Proof>>, Self::Err> {
         let spent_proofs = self.proofs.read().await;
 
         let mut proofs = Vec::with_capacity(ys.len());
@@ -283,7 +294,7 @@ impl MintDatabase for MintMemoryDatabase {
         for y in ys {
             let proof = spent_proofs.get(&y.to_bytes()).cloned();
 
-            proofs.push(proof);
+            proofs.push(proof.into());
         }
 
         Ok(proofs)

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

@@ -246,7 +246,12 @@ pub trait MintDatabase {
     async fn get_keyset_infos(&self) -> Result<Vec<MintKeySetInfo>, Self::Err>;
 
     /// Add spent [`Proofs`]
-    async fn add_proofs(&self, proof: Proofs, quote_id: Option<Uuid>) -> Result<(), Self::Err>;
+    async fn add_proofs<P: Into<Proofs>>(
+        &self,
+        proof: P,
+        quote_id: Option<Uuid>,
+    ) -> Result<(), Self::Err>;
+
     /// Get [`Proofs`] by ys
     async fn get_proofs_by_ys(&self, ys: &[PublicKey]) -> Result<Vec<Option<Proof>>, Self::Err>;
     /// Get ys by quote id

+ 2 - 18
crates/cdk/src/dhke.rs

@@ -16,25 +16,9 @@ use crate::secret::Secret;
 use crate::util::hex;
 use crate::SECP256K1;
 
-const DOMAIN_SEPARATOR: &[u8; 28] = b"Secp256k1_HashToCurve_Cashu_";
+use cdk_types::dhke::Error;
 
-/// NUT00 Error
-#[derive(Debug, Error)]
-pub enum Error {
-    /// Token could not be validated
-    #[error("Token not verified")]
-    TokenNotVerified,
-    /// No valid point on curve
-    #[error("No valid point found")]
-    NoValidPoint,
-    /// Secp256k1 error
-    #[error(transparent)]
-    Secp256k1(#[from] bitcoin::secp256k1::Error),
-    // TODO: Remove use anyhow
-    /// Custom Error
-    #[error("`{0}`")]
-    Custom(String),
-}
+const DOMAIN_SEPARATOR: &[u8; 28] = b"Secp256k1_HashToCurve_Cashu_";
 
 /// Deterministically maps a message to a public key point on the secp256k1
 /// curve, utilizing a domain separator to ensure uniqueness.

+ 1 - 29
crates/cdk/src/mint/mod.rs

@@ -516,35 +516,7 @@ pub struct FeeReserve {
     pub percent_fee_reserve: f32,
 }
 
-/// Mint Keyset Info
-#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
-pub struct MintKeySetInfo {
-    /// Keyset [`Id`]
-    pub id: Id,
-    /// Keyset [`CurrencyUnit`]
-    pub unit: CurrencyUnit,
-    /// Keyset active or inactive
-    /// Mint will only issue new [`BlindSignature`] on active keysets
-    pub active: bool,
-    /// Starting unix time Keyset is valid from
-    pub valid_from: u64,
-    /// When the Keyset is valid to
-    /// This is not shown to the wallet and can only be used internally
-    pub valid_to: Option<u64>,
-    /// [`DerivationPath`] keyset
-    pub derivation_path: DerivationPath,
-    /// DerivationPath index of Keyset
-    pub derivation_path_index: Option<u32>,
-    /// Max order of keyset
-    pub max_order: u8,
-    /// Input Fee ppk
-    #[serde(default = "default_fee")]
-    pub input_fee_ppk: u64,
-}
-
-fn default_fee() -> u64 {
-    0
-}
+pub use cdk_types::mint::MintKeySetInfo;
 
 impl From<MintKeySetInfo> for KeySetInfo {
     fn from(keyset_info: MintKeySetInfo) -> Self {

+ 36 - 6
crates/cdk/src/nuts/nut00/mod.rs

@@ -8,6 +8,7 @@ use std::hash::{Hash, Hasher};
 use std::str::FromStr;
 use std::string::FromUtf8Error;
 
+use cdk_types::nut00::ProofsMethods;
 use serde::{de, Deserialize, Deserializer, Serialize};
 use thiserror::Error;
 
@@ -29,13 +30,16 @@ pub use token::{Token, TokenV3, TokenV4};
 /// List of [Proof]
 pub type Proofs = Vec<Proof>;
 
-/// Utility methods for [Proofs]
-pub trait ProofsMethods {
-    /// Try to sum up the amounts of all [Proof]s
-    fn total_amount(&self) -> Result<Amount, Error>;
+impl From<cdk_types::Proofs> for Proofs {
+    fn from(proofs: cdk_types::Proofs) -> Self {
+        proofs.into_iter().map(Into::into).collect()
+    }
+}
 
-    /// Try to fetch the pubkeys of all [Proof]s
-    fn ys(&self) -> Result<Vec<PublicKey>, Error>;
+impl Into<cdk_types::Proofs> for Proofs {
+    fn into(self) -> cdk_types::Proofs {
+        self.into_iter().map(Into::into).collect()
+    }
 }
 
 impl ProofsMethods for Proofs {
@@ -250,6 +254,32 @@ pub struct Proof {
     pub dleq: Option<ProofDleq>,
 }
 
+impl From<cdk_types::Proof> for Proof {
+    fn from(proof: cdk_types::Proof) -> Self {
+        Self {
+            amount: proof.amount,
+            keyset_id: proof.keyset_id,
+            secret: proof.secret,
+            c: proof.c,
+            witness: proof.witness,
+            dleq: proof.dleq,
+        }
+    }
+}
+
+impl Into<cdk_types::Proof> for Proof {
+    fn into(self) -> cdk_types::Proof {
+        cdk_types::Proof {
+            amount: self.amount,
+            keyset_id: self.keyset_id,
+            secret: self.secret,
+            c: self.c,
+            witness: self.witness,
+            dleq: self.dleq,
+        }
+    }
+}
+
 impl Proof {
     /// Create new [`Proof`]
     pub fn new(amount: Amount, keyset_id: Id, secret: Secret, c: PublicKey) -> Self {

+ 8 - 6
crates/cdk/src/types.rs

@@ -28,13 +28,14 @@ pub struct Melted {
 
 impl Melted {
     /// Create new [`Melted`]
-    pub fn from_proofs(
+    pub fn from_proofs<P: Into<Proofs>>(
         state: MeltQuoteState,
         preimage: Option<String>,
         amount: Amount,
-        proofs: Proofs,
-        change_proofs: Option<Proofs>,
+        proofs: P,
+        change_proofs: Option<P>,
     ) -> Result<Self, Error> {
+        let proofs = proofs.into();
         let proofs_amount = proofs.total_amount()?;
         let change_amount = match &change_proofs {
             Some(change_proofs) => change_proofs.total_amount()?,
@@ -47,7 +48,7 @@ impl Melted {
         Ok(Self {
             state,
             preimage,
-            change: change_proofs,
+            change: change_proofs.map(|p| p.into()),
             amount,
             fee_paid,
         })
@@ -78,12 +79,13 @@ pub struct ProofInfo {
 
 impl ProofInfo {
     /// Create new [`ProofInfo`]
-    pub fn new(
-        proof: Proof,
+    pub fn new<P: Into<Proof>>(
+        proof: P,
         mint_url: MintUrl,
         state: State,
         unit: CurrencyUnit,
     ) -> Result<Self, Error> {
+        let proof = proof.into();
         let y = proof.y()?;
 
         let spending_condition: Option<SpendingConditions> = (&proof.secret).try_into().ok();

Some files were not shown because too many files changed in this diff