thesimplekid hace 10 meses
padre
commit
db14c11714

+ 1 - 0
README.md

@@ -26,6 +26,7 @@ CDK is a collection of rust crates for [Cashu](https://github.com/cashubtc) wall
 - :heavy_check_mark: [NUT-11](https://github.com/cashubtc/nuts/blob/main/11.md)
 - :heavy_check_mark: [NUT-12](https://github.com/cashubtc/nuts/blob/main/12.md)
 - :heavy_check_mark: [NUT-13](https://github.com/cashubtc/nuts/blob/main/13.md)
+- :heavy_check_mark: [NUT-14](https://github.com/cashubtc/nuts/blob/main/14.md)
 
 
 ## License

+ 0 - 1
crates/cdk/src/client.rs

@@ -234,7 +234,6 @@ impl HttpClient {
         let value = res.json::<Value>().await?;
         let response: Result<SwapResponse, serde_json::Error> =
             serde_json::from_value(value.clone());
-
         match response {
             Ok(res) => Ok(res),
             Err(_) => Err(ErrorResponse::from_json(&value.to_string())?.into()),

+ 33 - 5
crates/cdk/src/mint.rs

@@ -9,6 +9,7 @@ use thiserror::Error;
 use tokio::sync::RwLock;
 use tracing::{debug, error, info};
 
+use self::nut11::enforce_sig_flag;
 use crate::cdk_database::{self, MintDatabase};
 use crate::dhke::{hash_to_curve, sign_message, verify_message};
 use crate::error::ErrorResponse;
@@ -49,6 +50,8 @@ pub enum Error {
     NUT11(#[from] crate::nuts::nut11::Error),
     #[error(transparent)]
     Nut12(#[from] crate::nuts::nut12::Error),
+    #[error(transparent)]
+    Nut14(#[from] crate::nuts::nut14::Error),
     /// Database Error
     #[error(transparent)]
     Database(#[from] crate::cdk_database::Error),
@@ -439,6 +442,15 @@ impl Mint {
             return Err(Error::MultipleUnits);
         }
 
+        let (sig_flag, pubkeys) = enforce_sig_flag(swap_request.inputs.clone());
+
+        if sig_flag.eq(&SigFlag::SigAll) {
+            let pubkeys = pubkeys.into_iter().collect();
+            for blinded_messaage in &swap_request.outputs {
+                blinded_messaage.verify_p2pk(&pubkeys, 1)?;
+            }
+        }
+
         for proof in swap_request.inputs {
             self.localstore.add_spent_proof(proof).await?;
         }
@@ -461,11 +473,18 @@ impl Mint {
         if let Ok(secret) =
             <&crate::secret::Secret as TryInto<crate::nuts::nut10::Secret>>::try_into(&proof.secret)
         {
-            // Verify if p2pk
-            if secret.kind.eq(&Kind::P2PK) {
-                proof.verify_p2pk()?;
-            } else {
-                return Err(Error::UnknownSecretKind);
+            // Checks and verifes known secret kinds.
+            // If it is an unknown secret kind it will be treated as a normal secret.
+            // Spending conditions will **not** be check. It is up to the wallet to ensure
+            // only supported secret kinds are used as there is no way for the mint to enforce
+            // only signing supported secrets as they are blinded at that point.
+            match secret.kind {
+                Kind::P2PK => {
+                    proof.verify_p2pk()?;
+                }
+                Kind::HTLC => {
+                    proof.verify_htlc()?;
+                }
             }
         }
 
@@ -552,6 +571,15 @@ impl Mint {
         }
 
         if let Some(outputs) = &melt_request.outputs {
+            let (sig_flag, pubkeys) = enforce_sig_flag(melt_request.inputs.clone());
+
+            if sig_flag.eq(&SigFlag::SigAll) {
+                let pubkeys = pubkeys.into_iter().collect();
+                for blinded_messaage in outputs {
+                    blinded_messaage.verify_p2pk(&pubkeys, 1)?;
+                }
+            }
+
             let output_keysets_ids: HashSet<Id> = outputs.iter().map(|b| b.keyset_id).collect();
             for id in output_keysets_ids {
                 let keyset = self

+ 2 - 1
crates/cdk/src/nuts/mod.rs

@@ -13,6 +13,7 @@ pub mod nut11;
 pub mod nut12;
 #[cfg(feature = "nut13")]
 pub mod nut13;
+pub mod nut14;
 
 pub use nut00::{
     BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, PreMint, PreMintSecrets, Proof,
@@ -35,5 +36,5 @@ pub use nut06::{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::{P2PKConditions, SigFlag, Signatures, SigningKey, VerifyingKey};
+pub use nut11::{Conditions, P2PKWitness, SigFlag, SigningKey, SpendingConditions, VerifyingKey};
 pub use nut12::{BlindSignatureDleq, ProofDleq};

+ 49 - 16
crates/cdk/src/nuts/nut00.rs

@@ -14,11 +14,14 @@ use serde::{Deserialize, Deserializer, Serialize};
 use thiserror::Error;
 use url::Url;
 
+use super::nut10;
+use super::nut11::SpendingConditions;
 use crate::dhke::blind_message;
 use crate::nuts::nut01::{PublicKey, SecretKey};
-use crate::nuts::nut11::{witness_deserialize, witness_serialize, Signatures};
+use crate::nuts::nut11::{serde_p2pk_witness, P2PKWitness};
 use crate::nuts::nut12::BlindSignatureDleq;
-use crate::nuts::{Id, P2PKConditions, ProofDleq};
+use crate::nuts::nut14::{serde_htlc_witness, HTLCWitness};
+use crate::nuts::{Id, ProofDleq};
 use crate::secret::Secret;
 use crate::url::UncheckedUrl;
 use crate::Amount;
@@ -77,11 +80,8 @@ pub struct BlindedMessage {
     /// Witness
     ///
     /// <https://github.com/cashubtc/nuts/blob/main/11.md>
-    #[serde(default)]
     #[serde(skip_serializing_if = "Option::is_none")]
-    //#[serde(serialize_with = "witness_serialize")]
-    //#[serde(deserialize_with = "witness_deserialize")]
-    pub witness: Option<Signatures>,
+    pub witness: Option<Witness>,
 }
 
 impl BlindedMessage {
@@ -97,9 +97,9 @@ impl BlindedMessage {
     }
 
     /// Add witness
-    pub fn witness(mut self, witness: Signatures) -> Self {
+    #[inline]
+    pub fn witness(&mut self, witness: Witness) {
         self.witness = Some(witness);
-        self
     }
 }
 
@@ -123,11 +123,44 @@ pub struct BlindSignature {
     /// DLEQ Proof
     ///
     /// <https://github.com/cashubtc/nuts/blob/main/12.md>
-    #[serde(default)]
     #[serde(skip_serializing_if = "Option::is_none")]
     pub dleq: Option<BlindSignatureDleq>,
 }
 
+/// Witness
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum Witness {
+    /// P2PK Witness
+    #[serde(with = "serde_p2pk_witness")]
+    P2PKWitness(P2PKWitness),
+    /// HTLC Witness
+    #[serde(with = "serde_htlc_witness")]
+    HTLCWitness(HTLCWitness),
+}
+
+impl Witness {
+    pub fn add_signatures(&mut self, signatues: Vec<String>) {
+        match self {
+            Self::P2PKWitness(p2pk_witness) => p2pk_witness.signatures.extend(signatues),
+            Self::HTLCWitness(htlc_witness) => {
+                htlc_witness.signatures = htlc_witness.signatures.clone().map(|sigs| {
+                    let mut sigs = sigs;
+                    sigs.extend(signatues);
+                    sigs
+                });
+            }
+        }
+    }
+
+    pub fn signatures(&self) -> Option<Vec<String>> {
+        match self {
+            Self::P2PKWitness(witness) => Some(witness.signatures.clone()),
+            Self::HTLCWitness(witness) => witness.signatures.clone(),
+        }
+    }
+}
+
 /// Proofs
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct Proof {
@@ -142,12 +175,10 @@ pub struct Proof {
     #[serde(rename = "C")]
     pub c: PublicKey,
     /// Witness
-    #[serde(default)]
     #[serde(skip_serializing_if = "Option::is_none")]
-    #[serde(serialize_with = "witness_serialize")]
-    #[serde(deserialize_with = "witness_deserialize")]
-    pub witness: Option<Signatures>,
+    pub witness: Option<Witness>,
     /// DLEQ Proof
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub dleq: Option<ProofDleq>,
 }
 
@@ -381,17 +412,19 @@ impl PreMintSecrets {
         Ok(PreMintSecrets { secrets: output })
     }
 
-    pub fn with_p2pk_conditions(
+    pub fn with_conditions(
         keyset_id: Id,
         amount: Amount,
-        conditions: P2PKConditions,
+        conditions: SpendingConditions,
     ) -> Result<Self, Error> {
         let amount_split = amount.split();
 
         let mut output = Vec::with_capacity(amount_split.len());
 
         for amount in amount_split {
-            let secret: Secret = conditions.clone().try_into()?;
+            let secret: nut10::Secret = conditions.clone().into();
+
+            let secret: Secret = secret.try_into()?;
             let (blinded, r) = blind_message(&secret.to_bytes(), None)?;
 
             let blinded_message = BlindedMessage::new(amount, keyset_id, blinded);

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

@@ -67,20 +67,22 @@ pub struct MintBolt11Response {
 }
 
 /// Mint Method Settings
-#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub struct MintMethodSettings {
     /// Payment Method e.g. bolt11
     method: PaymentMethod,
     /// Currency Unit e.g. sat
     unit: CurrencyUnit,
     /// Min Amount
-    min_amount: Amount,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    min_amount: Option<Amount>,
     /// Max Amount
-    max_amount: Amount,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    max_amount: Option<Amount>,
 }
 
 /// Mint Settings
-#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub struct Settings {
     methods: Vec<MintMethodSettings>,
     disabled: bool,

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

@@ -74,20 +74,23 @@ pub struct MeltBolt11Response {
 }
 
 /// Melt Method Settings
-#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub struct MeltMethodSettings {
     /// Payment Method e.g. bolt11
     method: PaymentMethod,
     /// Currency Unit e.g. sat
     unit: CurrencyUnit,
     /// Min Amount
-    min_amount: Amount,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    min_amount: Option<Amount>,
     /// Max Amount
-    max_amount: Amount,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    max_amount: Option<Amount>,
 }
 
 /// Melt Settings
-#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub struct Settings {
     methods: Vec<MeltMethodSettings>,
+    disabled: bool,
 }

+ 34 - 13
crates/cdk/src/nuts/nut06.rs

@@ -5,10 +5,10 @@
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
 
 use super::nut01::PublicKey;
-use super::{nut04, nut05, nut07, nut08};
+use super::{nut04, nut05};
 
 /// Mint Version
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct MintVersion {
     pub name: String,
     pub version: String,
@@ -42,7 +42,7 @@ impl<'de> Deserialize<'de> for MintVersion {
 }
 
 /// Mint Info [NIP-09]
-#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub struct MintInfo {
     /// name of the mint and should be recognizable
     #[serde(skip_serializing_if = "Option::is_none")]
@@ -69,7 +69,8 @@ pub struct MintInfo {
     pub motd: Option<String>,
 }
 
-#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
+/// Supported nuts and settings
+#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub struct Nuts {
     #[serde(default)]
     #[serde(rename = "4")]
@@ -79,21 +80,40 @@ pub struct Nuts {
     pub nut05: nut05::Settings,
     #[serde(default)]
     #[serde(rename = "7")]
-    pub nut07: nut07::Settings,
+    pub nut07: SupportedSettings,
     #[serde(default)]
     #[serde(rename = "8")]
-    pub nut08: nut08::Settings,
-    // TODO: Change to nut settings
+    pub nut08: SupportedSettings,
     #[serde(default)]
     #[serde(rename = "9")]
-    pub nut09: nut07::Settings,
-    // TODO: Change to nut settings
+    pub nut09: SupportedSettings,
+    #[serde(rename = "10")]
     #[serde(default)]
-    pub nut10: nut07::Settings,
-    // TODO: Change to nut settings
+    pub nut10: SupportedSettings,
+    #[serde(rename = "11")]
+    #[serde(default)]
+    pub nut11: SupportedSettings,
     #[serde(default)]
     #[serde(rename = "12")]
-    pub nut12: nut07::Settings,
+    pub nut12: SupportedSettings,
+    #[serde(default)]
+    #[serde(rename = "13")]
+    pub nut13: SupportedSettings,
+    #[serde(default)]
+    #[serde(rename = "14")]
+    pub nut14: SupportedSettings,
+}
+
+/// Check state Settings
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct SupportedSettings {
+    supported: bool,
+}
+
+impl Default for SupportedSettings {
+    fn default() -> Self {
+        Self { supported: true }
+    }
 }
 
 #[cfg(test)]
@@ -150,7 +170,8 @@ mod tests {
         "min_amount": 0,
         "max_amount": 10000
         }
-      ]
+      ],
+      "disabled": false
     },
     "7": {"supported": true},
     "8": {"supported": true},

+ 0 - 6
crates/cdk/src/nuts/nut07.rs

@@ -39,9 +39,3 @@ pub struct ProofState {
 pub struct CheckStateResponse {
     pub states: Vec<ProofState>,
 }
-
-/// Spendable Settings
-#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct Settings {
-    supported: bool,
-}

+ 0 - 8
crates/cdk/src/nuts/nut08.rs

@@ -2,8 +2,6 @@
 //!
 //! <https://github.com/cashubtc/nuts/blob/main/08.md>
 
-use serde::{Deserialize, Serialize};
-
 use super::nut05::{MeltBolt11Request, MeltBolt11Response};
 use crate::Amount;
 
@@ -22,9 +20,3 @@ impl MeltBolt11Response {
             .map(|c| c.iter().map(|b| b.amount).sum())
     }
 }
-
-/// Melt Settings
-#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct Settings {
-    supported: bool,
-}

+ 11 - 7
crates/cdk/src/nuts/nut10.rs

@@ -2,21 +2,23 @@
 //!
 //! <https://github.com/cashubtc/nuts/blob/main/10.md>
 
-use core::str::FromStr;
+use std::str::FromStr;
 
 use serde::ser::SerializeTuple;
 use serde::{Deserialize, Serialize, Serializer};
 
 use crate::error::Error;
 
-#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
+///  NUT10 Secret Kind
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub enum Kind {
     /// NUT-11 P2PK
-    #[default]
     P2PK,
+    /// NUT-14 HTLC
+    HTLC,
 }
 
-#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub struct SecretData {
     /// Unique random string
     pub nonce: String,
@@ -27,23 +29,25 @@ pub struct SecretData {
     pub tags: Vec<Vec<String>>,
 }
 
-#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
+#[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 {
-    pub fn new<S>(kind: Kind, data: S, tags: Vec<Vec<String>>) -> Self
+    pub fn new<S, V>(kind: Kind, data: S, tags: 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: tags.into(),
         };
 
         Self { kind, secret_data }

+ 255 - 153
crates/cdk/src/nuts/nut11.rs → crates/cdk/src/nuts/nut11/mod.rs

@@ -2,7 +2,7 @@
 //!
 //! <https://github.com/cashubtc/nuts/blob/main/11.md>
 
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
 use std::fmt;
 use std::ops::Deref;
 use std::str::FromStr;
@@ -15,21 +15,26 @@ use bitcoin::secp256k1::{
 };
 use serde::de::Error as DeserializerError;
 use serde::ser::SerializeSeq;
-use serde::{de, ser, Deserialize, Deserializer, Serialize, Serializer};
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
 use thiserror::Error;
 
+use super::nut00::Witness;
 use super::nut01::PublicKey;
-use super::nut10::{Secret, SecretData};
-use super::{Proof, SecretKey};
+use super::{Kind, Nut10Secret, Proof, Proofs, SecretKey};
 use crate::nuts::nut00::BlindedMessage;
 use crate::util::{hex, unix_time};
 use crate::SECP256K1;
 
+pub mod serde_p2pk_witness;
+
 #[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,
@@ -39,14 +44,24 @@ pub enum Error {
     /// 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),
@@ -70,66 +85,73 @@ pub enum Error {
     Secret(#[from] crate::secret::Error),
 }
 
+/// P2Pk Witness
 #[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub struct Signatures {
-    #[serde(default)]
-    #[serde(skip_serializing_if = "Vec::is_empty")]
+pub struct P2PKWitness {
     pub signatures: Vec<String>,
 }
 
-impl Signatures {
+impl P2PKWitness {
     #[inline]
     pub fn is_empty(&self) -> bool {
         self.signatures.is_empty()
     }
 }
 
-/// Serialize [Signatures] as stringified JSON
-pub fn witness_serialize<S>(x: &Option<Signatures>, s: S) -> Result<S::Ok, S::Error>
-where
-    S: Serializer,
-{
-    s.serialize_str(&serde_json::to_string(&x).map_err(ser::Error::custom)?)
-}
+impl Proof {
+    /// Sign [Proof]
+    pub fn sign_p2pk(&mut self, secret_key: SigningKey) -> Result<(), Error> {
+        let msg: Vec<u8> = self.secret.to_bytes();
+        let signature: Signature = secret_key.sign(&msg)?;
 
-/// Serialize [Signatures] from stringified JSON
-pub fn witness_deserialize<'de, D>(deserializer: D) -> Result<Option<Signatures>, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    let s: String = String::deserialize(deserializer)?;
-    serde_json::from_str(&s).map_err(de::Error::custom)
-}
+        let signatures = vec![signature.to_string()];
 
-impl Proof {
-    pub fn verify_p2pk(&self) -> Result<(), Error> {
-        if !self.secret.is_p2pk() {
-            return Err(Error::IncorrectSecretKind);
-        }
+        match self.witness.as_mut() {
+            Some(witness) => {
+                witness.add_signatures(signatures);
+            }
+            None => {
+                let mut p2pk_witness = Witness::P2PKWitness(P2PKWitness::default());
+                p2pk_witness.add_signatures(signatures);
+                self.witness = Some(p2pk_witness);
+            }
+        };
 
-        let secret: Secret = self.secret.clone().try_into()?;
-        let spending_conditions: P2PKConditions = secret.clone().try_into()?;
+        Ok(())
+    }
+
+    /// Verify P2PK signature on [Proof]
+    pub fn verify_p2pk(&self) -> Result<(), Error> {
+        let secret: Nut10Secret = self.secret.clone().try_into()?;
+        let spending_conditions: Conditions = secret.secret_data.tags.try_into()?;
         let msg: &[u8] = self.secret.as_bytes();
 
         let mut valid_sigs = 0;
 
-        if let Some(witness) = &self.witness {
-            for signature in witness.signatures.iter() {
-                let mut pubkeys = spending_conditions.pubkeys.clone();
+        let witness_signatures = match &self.witness {
+            Some(witness) => witness.signatures(),
+            None => None,
+        };
 
-                pubkeys.push(VerifyingKey::from_str(&secret.secret_data.data)?);
+        let witness_signatures = witness_signatures.ok_or(Error::SignaturesNotProvided)?;
 
-                for v in &spending_conditions.pubkeys {
-                    let sig = Signature::from_str(signature)?;
+        let mut pubkeys = spending_conditions.pubkeys.clone().unwrap_or_default();
 
-                    if v.verify(msg, &sig).is_ok() {
-                        valid_sigs += 1;
-                    } else {
-                        tracing::debug!(
-                            "Could not verify signature: {sig} on message: {}",
-                            self.secret.to_string()
-                        )
-                    }
+        if secret.kind.eq(&Kind::P2PK) {
+            pubkeys.push(VerifyingKey::from_str(&secret.secret_data.data)?);
+        }
+
+        for signature in witness_signatures.iter() {
+            for v in &pubkeys {
+                let sig = Signature::from_str(signature)?;
+
+                if v.verify(msg, &sig).is_ok() {
+                    valid_sigs += 1;
+                } else {
+                    tracing::debug!(
+                        "Could not verify signature: {sig} on message: {}",
+                        self.secret.to_string()
+                    )
                 }
             }
         }
@@ -144,16 +166,13 @@ impl Proof {
         ) {
             // If lock time has passed check if refund witness signature is valid
             if locktime.lt(&unix_time()) {
-                if let Some(signatures) = &self.witness {
-                    for s in &signatures.signatures {
-                        for v in &refund_keys {
-                            let sig =
-                                Signature::from_str(s).map_err(|_| Error::InvalidSignature)?;
-
-                            // As long as there is one valid refund signature it can be spent
-                            if v.verify(msg, &sig).is_ok() {
-                                return Ok(());
-                            }
+                for s in witness_signatures.iter() {
+                    for v in &refund_keys {
+                        let sig = Signature::from_str(s).map_err(|_| Error::InvalidSignature)?;
+
+                        // As long as there is one valid refund signature it can be spent
+                        if v.verify(msg, &sig).is_ok() {
+                            return Ok(());
                         }
                     }
                 }
@@ -162,35 +181,46 @@ impl Proof {
 
         Err(Error::SpendConditionsNotMet)
     }
+}
 
-    pub fn sign_p2pk(&mut self, secret_key: SigningKey) -> Result<(), Error> {
-        let msg: Vec<u8> = self.secret.to_bytes();
-        let signature: Signature = secret_key.sign(&msg)?;
-
-        self.witness
-            .as_mut()
-            .unwrap_or(&mut Signatures::default())
-            .signatures
-            .push(signature.to_string());
+/// Returns count of valid signatures
+pub fn valid_signatures(msg: &[u8], pubkeys: &[VerifyingKey], signatures: &[Signature]) -> u64 {
+    let mut count = 0;
 
-        Ok(())
+    for pubkey in pubkeys {
+        for signature in signatures {
+            if pubkey.verify(msg, signature).is_ok() {
+                count += 1;
+            }
+        }
     }
+
+    count
 }
 
 impl BlindedMessage {
+    /// Sign [BlindedMessage]
     pub fn sign_p2pk(&mut self, secret_key: SigningKey) -> Result<(), Error> {
         let msg: [u8; 33] = self.blinded_secret.to_bytes();
         let signature: Signature = secret_key.sign(&msg)?;
 
-        self.witness
-            .as_mut()
-            .unwrap_or(&mut Signatures::default())
-            .signatures
-            .push(signature.to_string());
+        let signatures = vec![signature.to_string()];
+
+        match self.witness.as_mut() {
+            Some(witness) => {
+                witness.add_signatures(signatures);
+            }
+            None => {
+                let mut p2pk_witness = Witness::P2PKWitness(P2PKWitness::default());
+                p2pk_witness.add_signatures(signatures);
+                self.witness = Some(p2pk_witness);
+            }
+        };
 
         Ok(())
     }
 
+    /// Verify P2PK conditions on [BlindedMessage]
     pub fn verify_p2pk(
         &self,
         pubkeys: &Vec<VerifyingKey>,
@@ -198,7 +228,11 @@ impl BlindedMessage {
     ) -> Result<(), Error> {
         let mut valid_sigs = 0;
         if let Some(witness) = &self.witness {
-            for signature in &witness.signatures {
+            for signature in witness
+                .signatures()
+                .ok_or(Error::SignaturesNotProvided)?
+                .iter()
+            {
                 for v in pubkeys {
                     let msg = &self.blinded_secret.to_bytes();
                     let sig = Signature::from_str(signature)?;
@@ -223,11 +257,87 @@ impl BlindedMessage {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-pub struct P2PKConditions {
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum SpendingConditions {
+    /// NUT11 Spending conditions
+    P2PKConditions {
+        data: VerifyingKey,
+        conditions: Conditions,
+    },
+    /// NUT14 Spending conditions
+    HTLCConditions {
+        data: Sha256Hash,
+        conditions: Conditions,
+    },
+}
+
+impl SpendingConditions {
+    /// New HTLC [SpendingConditions]
+    pub fn new_htlc(preimage: String, conditions: Conditions) -> Result<Self, Error> {
+        let htlc = Sha256Hash::hash(&hex::decode(preimage)?);
+
+        Ok(Self::HTLCConditions {
+            data: htlc,
+            conditions,
+        })
+    }
+
+    /// New P2PK [SpendingConditions]
+    pub fn new_p2pk(pubkey: VerifyingKey, conditions: Conditions) -> Self {
+        Self::P2PKConditions {
+            data: pubkey,
+            conditions,
+        }
+    }
+
+    /// Kind of [SpendingConditions]
+    pub fn kind(&self) -> Kind {
+        match self {
+            Self::P2PKConditions { .. } => Kind::P2PK,
+            Self::HTLCConditions { .. } => Kind::HTLC,
+        }
+    }
+}
+
+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: VerifyingKey::from_str(&secret.secret_data.data)?,
+                conditions: secret.secret_data.tags.try_into()?,
+            }),
+            Kind::HTLC => Ok(Self::HTLCConditions {
+                data: Sha256Hash::from_str(&secret.secret_data.data)
+                    .map_err(|_| Error::InvalidHash)?,
+                conditions: secret.secret_data.tags.try_into()?,
+            }),
+        }
+    }
+}
+
+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_normalized_public_key().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, Serialize, Deserialize)]
+pub struct Conditions {
     #[serde(skip_serializing_if = "Option::is_none")]
     pub locktime: Option<u64>,
-    pub pubkeys: Vec<VerifyingKey>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub pubkeys: Option<Vec<VerifyingKey>>,
     #[serde(skip_serializing_if = "Option::is_none")]
     pub refund_keys: Option<Vec<VerifyingKey>>,
     #[serde(skip_serializing_if = "Option::is_none")]
@@ -235,10 +345,10 @@ pub struct P2PKConditions {
     pub sig_flag: SigFlag,
 }
 
-impl P2PKConditions {
+impl Conditions {
     pub fn new(
         locktime: Option<u64>,
-        pubkeys: Vec<VerifyingKey>,
+        pubkeys: Option<Vec<VerifyingKey>>,
         refund_keys: Option<Vec<VerifyingKey>>,
         num_sigs: Option<u64>,
         sig_flag: Option<SigFlag>,
@@ -258,11 +368,9 @@ impl P2PKConditions {
         })
     }
 }
-
-impl TryFrom<P2PKConditions> for Secret {
-    type Error = Error;
-    fn try_from(conditions: P2PKConditions) -> Result<Secret, Self::Error> {
-        let P2PKConditions {
+impl From<Conditions> for Vec<Vec<String>> {
+    fn from(conditions: Conditions) -> Vec<Vec<String>> {
+        let Conditions {
             locktime,
             pubkeys,
             refund_keys,
@@ -270,17 +378,10 @@ impl TryFrom<P2PKConditions> for Secret {
             sig_flag,
         } = conditions;
 
-        let data = match pubkeys.first() {
-            Some(data) => data.to_string(),
-            None => return Err(Error::P2PKPubkeyRequired),
-        };
-
-        let data = data.to_string();
-
         let mut tags = Vec::new();
 
-        if pubkeys.len().gt(&1) {
-            tags.push(Tag::PubKeys(pubkeys.into_iter().skip(1).collect()).as_vec());
+        if let Some(pubkeys) = pubkeys {
+            tags.push(Tag::PubKeys(pubkeys.into_iter().collect()).as_vec());
         }
 
         if let Some(locktime) = locktime {
@@ -295,48 +396,23 @@ impl TryFrom<P2PKConditions> for Secret {
             tags.push(Tag::Refund(refund_keys).as_vec())
         }
         tags.push(Tag::SigFlag(sig_flag).as_vec());
-
-        Ok(Secret {
-            kind: super::nut10::Kind::P2PK,
-            secret_data: SecretData {
-                nonce: crate::secret::Secret::default().to_string(),
-                data,
-                tags,
-            },
-        })
+        tags
     }
 }
 
-impl TryFrom<P2PKConditions> for crate::secret::Secret {
+impl TryFrom<Vec<Vec<String>>> for Conditions {
     type Error = Error;
-    fn try_from(conditions: P2PKConditions) -> Result<crate::secret::Secret, Self::Error> {
-        let secret: Secret = conditions.try_into()?;
-
-        secret.try_into().map_err(|_| Error::IncorrectSecretKind)
-    }
-}
-
-impl TryFrom<Secret> for P2PKConditions {
-    type Error = Error;
-    fn try_from(secret: Secret) -> Result<P2PKConditions, Self::Error> {
-        let tags: HashMap<TagKind, Tag> = secret
-            .clone()
-            .secret_data
-            .tags
+    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 mut pubkeys: Vec<VerifyingKey> = vec![];
-
-        if let Some(Tag::PubKeys(keys)) = tags.get(&TagKind::Pubkeys) {
-            let mut keys = keys.clone();
-            pubkeys.append(&mut keys);
-        }
-
-        let data_pubkey = VerifyingKey::from_str(&secret.secret_data.data)?;
-        pubkeys.push(data_pubkey);
+        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 {
@@ -374,7 +450,7 @@ impl TryFrom<Secret> for P2PKConditions {
             None
         };
 
-        Ok(P2PKConditions {
+        Ok(Conditions {
             locktime,
             pubkeys,
             refund_keys,
@@ -384,7 +460,8 @@ impl TryFrom<Secret> for P2PKConditions {
     }
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
+// 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
@@ -436,7 +513,6 @@ pub enum SigFlag {
     #[default]
     SigInputs,
     SigAll,
-    Custom(String),
 }
 
 impl fmt::Display for SigFlag {
@@ -444,25 +520,48 @@ impl fmt::Display for SigFlag {
         match self {
             Self::SigAll => write!(f, "SIG_ALL"),
             Self::SigInputs => write!(f, "SIG_INPUTS"),
-            Self::Custom(flag) => write!(f, "{}", flag),
         }
     }
 }
 
-impl<S> From<S> for SigFlag
-where
-    S: AsRef<str>,
-{
-    fn from(tag: S) -> Self {
-        match tag.as_ref() {
-            "SIG_ALL" => Self::SigAll,
-            "SIG_INPUTS" => Self::SigInputs,
-            tag => Self::Custom(tag.to_string()),
+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),
+        }
+    }
+}
+
+pub fn enforce_sig_flag(proofs: Proofs) -> (SigFlag, HashSet<VerifyingKey>) {
+    let mut sig_flag = SigFlag::SigInputs;
+    let mut pubkeys = HashSet::new();
+    for proof in proofs {
+        if let Ok(secret) = Nut10Secret::try_from(proof.secret) {
+            if secret.kind.eq(&Kind::P2PK) {
+                if let Ok(verifying_key) = VerifyingKey::from_str(&secret.secret_data.data) {
+                    pubkeys.insert(verifying_key);
+                }
+            }
+
+            if let Ok(conditions) = Conditions::try_from(secret.secret_data.tags) {
+                if conditions.sig_flag.eq(&SigFlag::SigAll) {
+                    sig_flag = SigFlag::SigAll;
+                }
+
+                if let Some(pubs) = conditions.pubkeys {
+                    pubkeys.extend(pubs);
+                }
+            }
         }
     }
+
+    (sig_flag, pubkeys)
 }
 
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
 pub enum Tag {
     SigFlag(SigFlag),
     NSigs(u64),
@@ -501,7 +600,7 @@ where
         };
 
         match tag_kind {
-            TagKind::SigFlag => Ok(Tag::SigFlag(SigFlag::from(tag[1].as_ref()))),
+            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 => {
@@ -535,7 +634,6 @@ impl From<Tag> for Vec<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() {
                     let pubkey: PublicKey = pubkey.to_normalized_public_key();
                     tag.push(pubkey.to_string())
@@ -579,7 +677,7 @@ impl<'de> Deserialize<'de> for Tag {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 #[serde(transparent)]
 pub struct VerifyingKey(XOnlyPublicKey);
 
@@ -706,17 +804,19 @@ mod tests {
 
     use super::*;
     use crate::nuts::Id;
+    use crate::secret::Secret;
     use crate::Amount;
 
     #[test]
     fn test_secret_ser() {
-        let conditions = P2PKConditions {
+        let data = VerifyingKey::from_str(
+            "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e",
+        )
+        .unwrap();
+
+        let conditions = Conditions {
             locktime: Some(99999),
-            pubkeys: vec![
-                VerifyingKey::from_str(
-                    "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e",
-                )
-                .unwrap(),
+            pubkeys: Some(vec![
                 VerifyingKey::from_str(
                     "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
                 )
@@ -725,7 +825,7 @@ mod tests {
                     "023192200a0cfd3867e48eb63b03ff599c7e46c8f4e41146b2d281173ca6c50c54",
                 )
                 .unwrap(),
-            ],
+            ]),
             refund_keys: Some(vec![VerifyingKey::from_str(
                 "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e",
             )
@@ -734,11 +834,11 @@ mod tests {
             sig_flag: SigFlag::SigAll,
         };
 
-        let secret: Secret = conditions.try_into().unwrap();
+        let secret: Nut10Secret = Nut10Secret::new(Kind::P2PK, data.to_string(), conditions);
 
         let secret_str = serde_json::to_string(&secret).unwrap();
 
-        let secret_der: Secret = serde_json::from_str(&secret_str).unwrap();
+        let secret_der: Nut10Secret = serde_json::from_str(&secret_str).unwrap();
 
         assert_eq!(secret_der, secret);
     }
@@ -763,15 +863,17 @@ mod tests {
         let v_key_two: VerifyingKey = signing_key_two.verifying_key();
         let v_key_three: VerifyingKey = signing_key_three.verifying_key();
 
-        let conditions = P2PKConditions {
+        let conditions = Conditions {
             locktime: Some(21),
-            pubkeys: vec![v_key.clone(), v_key_two, v_key_three],
-            refund_keys: Some(vec![v_key]),
+            pubkeys: Some(vec![v_key_two, v_key_three]),
+            refund_keys: Some(vec![v_key.clone()]),
             num_sigs: Some(2),
             sig_flag: SigFlag::SigInputs,
         };
 
-        let secret: super::Secret = conditions.try_into().unwrap();
+        let secret: Secret = Nut10Secret::new(Kind::P2PK, v_key.to_string(), conditions)
+            .try_into()
+            .unwrap();
 
         let mut proof = Proof {
             keyset_id: Id::from_str("009a1f293253e41e").unwrap(),
@@ -781,7 +883,7 @@ mod tests {
                 "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
             )
             .unwrap(),
-            witness: Some(Signatures { signatures: vec![] }),
+            witness: Some(Witness::P2PKWitness(P2PKWitness { signatures: vec![] })),
             dleq: None,
         };
 

+ 22 - 0
crates/cdk/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)
+}

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

@@ -0,0 +1,130 @@
+//! NUT-14: Hashed Time Lock Contacts (HTLC)
+//!
+//! <https://github.com/cashubtc/nuts/blob/main/14.md>
+
+use std::str::FromStr;
+
+use bitcoin::hashes::sha256::Hash as Sha256Hash;
+use bitcoin::hashes::Hash;
+use bitcoin::secp256k1::schnorr::Signature;
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+
+use super::nut00::Witness;
+use super::nut10::Secret;
+use super::nut11::valid_signatures;
+use super::{Conditions, Proof};
+use crate::util::unix_time;
+
+pub mod serde_htlc_witness;
+
+#[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,
+    /// NUT11 Error
+    #[error(transparent)]
+    NUT11(#[from] super::nut11::Error),
+    #[error(transparent)]
+    Serde(#[from] serde_json::Error),
+}
+
+#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub struct HTLCWitness {
+    pub preimage: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub signatures: Option<Vec<String>>,
+}
+
+impl Proof {
+    /// Verify HTLC
+    pub fn verify_htlc(&self) -> Result<(), Error> {
+        let secret: Secret = self.secret.clone().try_into()?;
+        let conditions: Conditions = secret.secret_data.tags.try_into()?;
+
+        // Check locktime
+        if let Some(locktime) = conditions.locktime {
+            // If locktime is in passed and no refund keys provided anyone can spend
+            if locktime.lt(&unix_time()) && conditions.refund_keys.is_none() {
+                return Ok(());
+            }
+
+            // If refund keys are provided verify p2pk signatures
+            if let (Some(refund_key), Some(signatures)) = (conditions.refund_keys, &self.witness) {
+                let signatures: Vec<Signature> = signatures
+                    .signatures()
+                    .ok_or(Error::SignaturesNotProvided)?
+                    .iter()
+                    .flat_map(|s| Signature::from_str(s))
+                    .collect();
+
+                // If secret includes refund keys check that there is a valid signature
+                if valid_signatures(self.secret.as_bytes(), &refund_key, &signatures).ge(&1) {
+                    return Ok(());
+                }
+            }
+        }
+
+        if secret.kind.ne(&super::Kind::HTLC) {
+            return Err(Error::IncorrectSecretKind);
+        }
+
+        let htlc_witness = match &self.witness {
+            Some(Witness::HTLCWitness(witness)) => witness,
+            _ => return Err(Error::IncorrectSecretKind),
+        };
+
+        let hash_lock =
+            Sha256Hash::from_str(&secret.secret_data.data).map_err(|_| Error::InvalidHash)?;
+
+        let preimage_hash = Sha256Hash::hash(htlc_witness.preimage.as_bytes());
+
+        if hash_lock.ne(&preimage_hash) {
+            return Err(Error::Preimage);
+        }
+
+        // If pubkeys are present check there is a valid signature
+        if let Some(pubkey) = conditions.pubkeys {
+            let req_sigs = conditions.num_sigs.unwrap_or(1);
+            let signatures = htlc_witness
+                .signatures
+                .as_ref()
+                .ok_or(Error::SignaturesNotProvided)?;
+
+            let signatures: Vec<Signature> = signatures
+                .iter()
+                .flat_map(|s| Signature::from_str(s))
+                .collect();
+
+            if valid_signatures(self.secret.as_bytes(), &pubkey, &signatures).lt(&req_sigs) {
+                return Err(Error::IncorrectSecretKind);
+            }
+        }
+
+        Ok(())
+    }
+
+    #[inline]
+    pub fn add_preimage(&mut self, preimage: String) {
+        self.witness = Some(Witness::HTLCWitness(HTLCWitness {
+            preimage,
+            signatures: None,
+        }))
+    }
+}

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

@@ -0,0 +1,20 @@
+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)
+}

+ 190 - 243
crates/cdk/src/wallet.rs

@@ -6,21 +6,22 @@ use std::str::FromStr;
 use std::sync::Arc;
 
 use bip39::Mnemonic;
+use bitcoin::hashes::sha256::Hash as Sha256Hash;
+use bitcoin::hashes::Hash;
 use thiserror::Error;
-use tracing::{debug, warn};
 
 use crate::cdk_database::wallet_memory::WalletMemoryDatabase;
 use crate::cdk_database::{self, WalletDatabase};
 use crate::client::HttpClient;
 use crate::dhke::{construct_proofs, hash_to_curve, unblind_message};
 use crate::nuts::{
-    nut12, BlindSignature, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, P2PKConditions,
+    nut12, BlindSignature, Conditions, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, Kind, MintInfo,
     PreMintSecrets, PreSwap, Proof, ProofState, Proofs, PublicKey, RestoreRequest, SigFlag,
-    SigningKey, State, SwapRequest, Token,
+    SigningKey, SpendingConditions, State, SwapRequest, Token, VerifyingKey,
 };
 use crate::types::{MeltQuote, Melted, MintQuote};
 use crate::url::UncheckedUrl;
-use crate::util::unix_time;
+use crate::util::{hex, unix_time};
 use crate::{Amount, Bolt11Invoice};
 
 #[derive(Debug, Error)]
@@ -42,6 +43,8 @@ pub enum Error {
     P2PKConditionsNotMet(String),
     #[error("Invalid Spending Conditions: `{0}`")]
     InvalidSpendConditions(String),
+    #[error("Preimage not provided")]
+    PreimageNotProvided,
     #[error("Unknown Key")]
     UnknownKey,
     #[error(transparent)]
@@ -61,6 +64,8 @@ pub enum Error {
     /// Database Error
     #[error(transparent)]
     Database(#[from] crate::cdk_database::Error),
+    #[error(transparent)]
+    Serde(#[from] serde_json::Error),
     #[error("`{0}`")]
     Custom(String),
 }
@@ -136,7 +141,7 @@ impl Wallet {
         {
             Ok(mint_info) => Some(mint_info),
             Err(err) => {
-                warn!("Could not get mint info {}", err);
+                tracing::warn!("Could not get mint info {}", err);
                 None
             }
         };
@@ -410,87 +415,6 @@ impl Wallet {
         Ok(minted_amount)
     }
 
-    /// Receive
-    pub async fn receive(&mut self, encoded_token: &str) -> Result<(), Error> {
-        let token_data = Token::from_str(encoded_token)?;
-
-        let unit = token_data.unit.unwrap_or_default();
-
-        // Verify the signature DLEQ is valid
-        // Verify that all proofs in the token have a valid DLEQ proof if one is supplied
-        {
-            for mint_proof in &token_data.token {
-                let mint_url = &mint_proof.mint;
-                let proofs = &mint_proof.proofs;
-
-                for proof in proofs {
-                    let keys = self.get_keyset_keys(mint_url, proof.keyset_id).await?;
-                    let key = keys.amount_key(proof.amount).ok_or(Error::UnknownKey)?;
-                    match proof.verify_dleq(key) {
-                        Ok(_) | Err(nut12::Error::MissingDleqProof) => continue,
-                        Err(_) => return Err(Error::CouldNotVerifyDleq),
-                    }
-                }
-            }
-        }
-
-        let mut proofs: HashMap<UncheckedUrl, Proofs> = HashMap::new();
-        for token in token_data.token {
-            if token.proofs.is_empty() {
-                continue;
-            }
-
-            let active_keyset_id = self.active_mint_keyset(&token.mint, &unit).await?;
-
-            // TODO: if none fetch keyset for mint
-
-            let keys = if let Some(keys) = self.localstore.get_keys(&active_keyset_id).await? {
-                keys
-            } else {
-                self.get_keyset_keys(&token.mint, active_keyset_id).await?;
-                self.localstore.get_keys(&active_keyset_id).await?.unwrap()
-            };
-
-            // Sum amount of all proofs
-            let amount: Amount = token.proofs.iter().map(|p| p.amount).sum();
-
-            let pre_swap = self
-                .create_swap(&token.mint, &unit, Some(amount), token.proofs)
-                .await?;
-
-            let swap_response = self
-                .client
-                .post_swap(token.mint.clone().try_into()?, pre_swap.swap_request)
-                .await?;
-
-            // Proof to keep
-            let p = construct_proofs(
-                swap_response.signatures,
-                pre_swap.pre_mint_secrets.rs(),
-                pre_swap.pre_mint_secrets.secrets(),
-                &keys,
-            )?;
-
-            #[cfg(feature = "nut13")]
-            if self.mnemonic.is_some() {
-                self.localstore
-                    .increment_keyset_counter(&active_keyset_id, p.len() as u64)
-                    .await?;
-            }
-
-            let mint_proofs = proofs.entry(token.mint).or_default();
-
-            mint_proofs.extend(p);
-        }
-
-        for (mint, p) in proofs {
-            self.add_mint(mint.clone()).await?;
-            self.localstore.add_proofs(mint, p).await?;
-        }
-
-        Ok(())
-    }
-
     /// Create Swap Payload
     async fn create_swap(
         &mut self,
@@ -498,6 +422,7 @@ impl Wallet {
         unit: &CurrencyUnit,
         amount: Option<Amount>,
         proofs: Proofs,
+        spending_conditions: Option<SpendingConditions>,
     ) -> Result<PreSwap, Error> {
         let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?;
 
@@ -505,59 +430,112 @@ impl Wallet {
         let proofs_total = proofs.iter().map(|p| p.amount).sum();
 
         let desired_amount = amount.unwrap_or(proofs_total);
-
-        let mut counter = None;
+        let change_amount = proofs_total - desired_amount;
 
         let mut desired_messages;
+        let change_messages;
 
         #[cfg(not(feature = "nut13"))]
         {
-            desired_messages = PreMintSecrets::random(active_keyset_id, desired_amount)?;
+            (desired_messages, change_messages) = match spendig_conditions {
+                Some(conditions) => (
+                    PreMintSecrets::with_conditions(active_keyset_id, desired_amount, conditions)?,
+                    PreMintSecrets::random(active_keyset_id, change_amount),
+                ),
+                None => (
+                    PreMintSecrets::random(active_keyset_id, proofs_total)?,
+                    PreMintSecrets::default(),
+                ),
+            };
         }
 
         #[cfg(feature = "nut13")]
         {
-            desired_messages = if let Some(mnemonic) = &self.mnemonic {
-                let count = self
-                    .localstore
-                    .get_keyset_counter(&active_keyset_id)
-                    .await?;
-
-                let count = if let Some(count) = count {
-                    count + 1
-                } else {
-                    0
-                };
-
-                let premint_secrets = PreMintSecrets::from_seed(
-                    active_keyset_id,
-                    count,
-                    mnemonic,
-                    desired_amount,
-                    false,
-                )?;
-
-                counter = Some(count + premint_secrets.len() as u64);
-
-                premint_secrets
-            } else {
-                PreMintSecrets::random(active_keyset_id, desired_amount)?
+            (desired_messages, change_messages) = match &self.mnemonic {
+                Some(mnemonic) => match spending_conditions {
+                    Some(conditions) => {
+                        let count = self
+                            .localstore
+                            .get_keyset_counter(&active_keyset_id)
+                            .await?;
+
+                        let count = if let Some(count) = count {
+                            count + 1
+                        } else {
+                            0
+                        };
+
+                        let change_premint_secrets = PreMintSecrets::from_seed(
+                            active_keyset_id,
+                            count,
+                            mnemonic,
+                            change_amount,
+                            false,
+                        )?;
+
+                        (
+                            PreMintSecrets::with_conditions(
+                                active_keyset_id,
+                                desired_amount,
+                                conditions,
+                            )?,
+                            change_premint_secrets,
+                        )
+                    }
+                    None => {
+                        let count = self
+                            .localstore
+                            .get_keyset_counter(&active_keyset_id)
+                            .await?;
+
+                        let count = if let Some(count) = count {
+                            count + 1
+                        } else {
+                            0
+                        };
+
+                        let premint_secrets = PreMintSecrets::from_seed(
+                            active_keyset_id,
+                            count,
+                            mnemonic,
+                            desired_amount,
+                            false,
+                        )?;
+
+                        let count = count + premint_secrets.len() as u64;
+
+                        let change_premint_secrets = PreMintSecrets::from_seed(
+                            active_keyset_id,
+                            count,
+                            mnemonic,
+                            change_amount,
+                            false,
+                        )?;
+
+                        (premint_secrets, change_premint_secrets)
+                    }
+                },
+                None => match spending_conditions {
+                    Some(conditions) => (
+                        PreMintSecrets::with_conditions(
+                            active_keyset_id,
+                            desired_amount,
+                            conditions,
+                        )?,
+                        PreMintSecrets::random(active_keyset_id, change_amount)?,
+                    ),
+                    None => (
+                        PreMintSecrets::random(active_keyset_id, desired_amount)?,
+                        PreMintSecrets::random(active_keyset_id, change_amount)?,
+                    ),
+                },
             };
         }
 
-        if let Some(amt) = amount {
-            let change_amount = proofs_total - amt;
-
-            let change_messages = if let (Some(count), Some(mnemonic)) = (counter, &self.mnemonic) {
-                PreMintSecrets::from_seed(active_keyset_id, count, mnemonic, change_amount, false)?
-            } else {
-                PreMintSecrets::random(active_keyset_id, change_amount)?
-            };
-            // Combine the BlindedMessages totoalling the desired amount with change
-            desired_messages.combine(change_messages);
-            // Sort the premint secrets to avoid finger printing
-            desired_messages.sort_secrets();
-        };
+        // Combine the BlindedMessages totoalling the desired amount with change
+        desired_messages.combine(change_messages);
+        // Sort the premint secrets to avoid finger printing
+        desired_messages.sort_secrets();
 
         let swap_request = SwapRequest::new(proofs, desired_messages.blinded_messages());
 
@@ -633,11 +611,20 @@ impl Wallet {
         mint_url: &UncheckedUrl,
         unit: &CurrencyUnit,
         amount: Amount,
+        conditions: Option<SpendingConditions>,
     ) -> Result<Proofs, Error> {
-        let proofs = self.select_proofs(mint_url.clone(), unit, amount).await?;
+        let input_proofs = self.select_proofs(mint_url.clone(), unit, amount).await?;
+
+        let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?;
 
         let pre_swap = self
-            .create_swap(mint_url, unit, Some(amount), proofs.clone())
+            .create_swap(
+                mint_url,
+                unit,
+                Some(amount),
+                input_proofs.clone(),
+                conditions,
+            )
             .await?;
 
         let swap_response = self
@@ -655,12 +642,10 @@ impl Wallet {
             &self.active_keys(mint_url, unit).await?.unwrap(),
         )?;
 
-        let active_keyset = self.active_mint_keyset(mint_url, unit).await?;
-
         #[cfg(feature = "nut13")]
         if self.mnemonic.is_some() {
             self.localstore
-                .increment_keyset_counter(&active_keyset, post_swap_proofs.len() as u64)
+                .increment_keyset_counter(&active_keyset_id, post_swap_proofs.len() as u64)
                 .await?;
         }
 
@@ -677,18 +662,19 @@ impl Wallet {
         let send_amount: Amount = send_proofs.iter().map(|p| p.amount).sum();
 
         if send_amount.ne(&amount) {
-            warn!(
+            tracing::warn!(
                 "Send amount proofs is {:?} expected {:?}",
-                send_amount, amount
+                send_amount,
+                amount
             );
         }
 
         self.localstore
-            .remove_proofs(mint_url.clone(), &proofs)
+            .remove_proofs(mint_url.clone(), &input_proofs)
             .await?;
 
         self.localstore
-            .add_pending_proofs(mint_url.clone(), proofs)
+            .add_pending_proofs(mint_url.clone(), input_proofs)
             .await?;
         self.localstore
             .add_pending_proofs(mint_url.clone(), send_proofs.clone())
@@ -874,7 +860,7 @@ impl Wallet {
         };
 
         if let Some(change_proofs) = change_proofs {
-            debug!(
+            tracing::debug!(
                 "Change amount returned from melt: {}",
                 change_proofs.iter().map(|p| p.amount).sum::<Amount>()
             );
@@ -901,89 +887,13 @@ impl Wallet {
         Ok(melted)
     }
 
-    /// Create P2PK locked proofs
-    /// Uses a swap to swap proofs for locked p2pk conditions
-    pub async fn send_p2pk(
-        &mut self,
-        mint_url: &UncheckedUrl,
-        unit: &CurrencyUnit,
-        amount: Amount,
-        conditions: P2PKConditions,
-    ) -> Result<Proofs, Error> {
-        let input_proofs = self.select_proofs(mint_url.clone(), unit, amount).await?;
-        let active_keyset_id = self.active_mint_keyset(mint_url, unit).await?;
-
-        let input_amount: Amount = input_proofs.iter().map(|p| p.amount).sum();
-        let change_amount = input_amount - amount;
-
-        let send_premint_secrets =
-            PreMintSecrets::with_p2pk_conditions(active_keyset_id, amount, conditions)?;
-
-        let change_premint_secrets = PreMintSecrets::random(active_keyset_id, change_amount)?;
-        let mut pre_mint_secrets = send_premint_secrets;
-        pre_mint_secrets.combine(change_premint_secrets);
-
-        let swap_request =
-            SwapRequest::new(input_proofs.clone(), pre_mint_secrets.blinded_messages());
-
-        let pre_swap = PreSwap {
-            pre_mint_secrets,
-            swap_request,
-        };
-
-        let swap_response = self
-            .client
-            .post_swap(mint_url.clone().try_into()?, pre_swap.swap_request)
-            .await?;
-
-        let post_swap_proofs = construct_proofs(
-            swap_response.signatures,
-            pre_swap.pre_mint_secrets.rs(),
-            pre_swap.pre_mint_secrets.secrets(),
-            &self.active_keys(mint_url, unit).await?.unwrap(),
-        )?;
-
-        let mut send_proofs = vec![];
-        let mut change_proofs = vec![];
-
-        for proof in post_swap_proofs {
-            let conditions: Result<crate::nuts::nut10::Secret, _> = (&proof.secret).try_into();
-            if conditions.is_ok() {
-                send_proofs.push(proof);
-            } else {
-                change_proofs.push(proof);
-            }
-        }
-
-        self.localstore
-            .remove_proofs(mint_url.clone(), &input_proofs)
-            .await?;
-
-        self.localstore
-            .add_pending_proofs(mint_url.clone(), input_proofs)
-            .await?;
-        self.localstore
-            .add_pending_proofs(mint_url.clone(), send_proofs.clone())
-            .await?;
-        self.localstore
-            .add_proofs(mint_url.clone(), change_proofs.clone())
-            .await?;
-
-        Ok(send_proofs)
-    }
-
-    /// Receive p2pk
-    pub async fn receive_p2pk(
+    /// Receive
+    pub async fn receive(
         &mut self,
         encoded_token: &str,
-        signing_keys: Vec<SigningKey>,
+        signing_keys: Option<Vec<SigningKey>>,
+        preimages: Option<Vec<String>>,
     ) -> Result<(), Error> {
-        let signing_key = signing_keys[0].clone();
-        let pubkey_secret_key: HashMap<String, SigningKey> = signing_keys
-            .into_iter()
-            .map(|s| (s.public_key().to_string(), s))
-            .collect();
-
         let token_data = Token::from_str(encoded_token)?;
 
         let unit = token_data.unit.unwrap_or_default();
@@ -1005,7 +915,27 @@ impl Wallet {
 
             let mut proofs = token.proofs;
 
-            let mut sig_flag = None;
+            let mut sig_flag = SigFlag::SigInputs;
+
+            let pubkey_secret_key = match &signing_keys {
+                Some(signing_keys) => signing_keys
+                    .iter()
+                    .map(|s| (s.verifying_key().to_string(), s))
+                    .collect(),
+                None => HashMap::new(),
+            };
+
+            // Map hash of preimage to preimage
+            let hashed_to_preimage = match preimages {
+                Some(ref preimages) => preimages
+                    .iter()
+                    .flat_map(|p| match hex::decode(p) {
+                        Ok(hex_bytes) => Some((Sha256Hash::hash(&hex_bytes).to_string(), p)),
+                        Err(_) => None,
+                    })
+                    .collect(),
+                None => HashMap::new(),
+            };
 
             for proof in &mut proofs {
                 // Verify that proof DLEQ is valid
@@ -1020,29 +950,45 @@ impl Wallet {
                         proof.secret.clone(),
                     )
                 {
-                    let conditions: Result<P2PKConditions, _> = secret.try_into();
+                    let conditions: Result<Conditions, _> = secret.secret_data.tags.try_into();
                     if let Ok(conditions) = conditions {
-                        let pubkeys = conditions.pubkeys;
+                        let mut pubkeys = conditions.pubkeys.unwrap_or_default();
 
+                        match secret.kind {
+                            Kind::P2PK => {
+                                let data_key = VerifyingKey::from_str(&secret.secret_data.data)?;
+
+                                pubkeys.push(data_key);
+                            }
+                            Kind::HTLC => {
+                                let hashed_preimage = &secret.secret_data.data;
+                                let preimage = hashed_to_preimage
+                                    .get(hashed_preimage)
+                                    .ok_or(Error::PreimageNotProvided)?;
+                                proof.add_preimage(preimage.to_string());
+                            }
+                        }
                         for pubkey in pubkeys {
                             if let Some(signing) = pubkey_secret_key.get(&pubkey.to_string()) {
-                                proof.sign_p2pk(signing.clone())?;
+                                proof.sign_p2pk(signing.to_owned().clone())?;
                             }
                         }
 
-                        sig_flag = Some(conditions.sig_flag);
+                        if conditions.sig_flag.eq(&SigFlag::SigAll) {
+                            sig_flag = SigFlag::SigAll;
+                        }
                     }
                 }
             }
 
             let mut pre_swap = self
-                .create_swap(&token.mint, &unit, Some(amount), proofs)
+                .create_swap(&token.mint, &unit, Some(amount), proofs, None)
                 .await?;
 
-            if let Some(sigflag) = sig_flag {
-                if sigflag.eq(&SigFlag::SigAll) {
-                    for blinded_message in &mut pre_swap.swap_request.outputs {
-                        blinded_message.sign_p2pk(signing_key.clone()).unwrap();
+            if sig_flag.eq(&SigFlag::SigAll) {
+                for blinded_message in &mut pre_swap.swap_request.outputs {
+                    for signing_key in pubkey_secret_key.values() {
+                        blinded_message.sign_p2pk(signing_key.to_owned().clone())?
                     }
                 }
             }
@@ -1112,7 +1058,7 @@ impl Wallet {
                     start_counter + 100,
                 )?;
 
-                debug!(
+                tracing::debug!(
                     "Attempting to restore counter {}-{} for mint {} keyset {}",
                     start_counter,
                     start_counter + 100,
@@ -1158,7 +1104,7 @@ impl Wallet {
                     &keys,
                 )?;
 
-                debug!("Restored {} proofs", proofs.len());
+                tracing::debug!("Restored {} proofs", proofs.len());
 
                 #[cfg(feature = "nut13")]
                 self.localstore
@@ -1196,12 +1142,12 @@ impl Wallet {
     pub fn verify_token_p2pk(
         &self,
         token: &Token,
-        spending_conditions: P2PKConditions,
+        spending_conditions: Conditions,
     ) -> Result<(), Error> {
         use crate::nuts::nut10;
 
         if spending_conditions.refund_keys.is_some() && spending_conditions.locktime.is_none() {
-            warn!(
+            tracing::warn!(
                 "Invalid spending conditions set: Locktime must be set if refund keys are allowed"
             );
             return Err(Error::InvalidSpendConditions(
@@ -1211,14 +1157,15 @@ impl Wallet {
 
         for mint_proof in &token.token {
             for proof in &mint_proof.proofs {
-                let secret: nut10::Secret = (&proof.secret).try_into().unwrap();
+                let secret: nut10::Secret = (&proof.secret).try_into()?;
 
-                let proof_conditions: P2PKConditions = secret.try_into().unwrap();
+                let proof_conditions: Conditions = secret.secret_data.tags.try_into()?;
 
                 if spending_conditions.num_sigs.ne(&proof_conditions.num_sigs) {
-                    debug!(
+                    tracing::debug!(
                         "Spending condition requires: {:?} sigs proof secret specifies: {:?}",
-                        spending_conditions.num_sigs, proof_conditions.num_sigs
+                        spending_conditions.num_sigs,
+                        proof_conditions.num_sigs
                     );
 
                     return Err(Error::P2PKConditionsNotMet(
@@ -1226,17 +1173,17 @@ impl Wallet {
                     ));
                 }
 
+                let spending_condition_pubkeys =
+                    spending_conditions.pubkeys.clone().unwrap_or_default();
+                let proof_pubkeys = proof_conditions.pubkeys.unwrap_or_default();
+
                 // Check the Proof has the required pubkeys
-                if proof_conditions
-                    .pubkeys
-                    .len()
-                    .ne(&spending_conditions.pubkeys.len())
-                    || !proof_conditions
-                        .pubkeys
+                if proof_pubkeys.len().ne(&spending_condition_pubkeys.len())
+                    || !proof_pubkeys
                         .iter()
-                        .all(|pubkey| spending_conditions.pubkeys.contains(pubkey))
+                        .all(|pubkey| spending_condition_pubkeys.contains(pubkey))
                 {
-                    debug!("Proof did not included Publickeys meeting condition");
+                    tracing::debug!("Proof did not included Publickeys meeting condition");
                     return Err(Error::P2PKConditionsNotMet(
                         "Pubkeys in proof not allowed by spending condition".to_string(),
                     ));