Kaynağa Gözat

Keysets V2 (#702)

---------
Co-authored-by: thesimplekid <tsk@thesimplekid.com>
lollerfirst 3 hafta önce
ebeveyn
işleme
c61fd3830a

+ 84 - 0
crates/cashu/src/nuts/nut00/mod.rs

@@ -12,6 +12,7 @@ use std::string::FromUtf8Error;
 use serde::{de, Deserialize, Deserializer, Serialize};
 use thiserror::Error;
 
+use super::nut02::ShortKeysetId;
 #[cfg(feature = "wallet")]
 use super::nut10;
 #[cfg(feature = "wallet")]
@@ -183,6 +184,9 @@ pub enum Error {
     /// NUT11 error
     #[error(transparent)]
     NUT11(#[from] crate::nuts::nut11::Error),
+    /// Short keyset id -> id error
+    #[error(transparent)]
+    NUT02(#[from] crate::nuts::nut02::Error),
 }
 
 /// Blinded Message (also called `output`)
@@ -434,6 +438,12 @@ impl ProofV4 {
     }
 }
 
+impl Hash for ProofV4 {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.secret.hash(state);
+    }
+}
+
 impl From<Proof> for ProofV4 {
     fn from(proof: Proof) -> ProofV4 {
         let Proof {
@@ -454,6 +464,80 @@ impl From<Proof> for ProofV4 {
     }
 }
 
+impl From<ProofV3> for ProofV4 {
+    fn from(proof: ProofV3) -> Self {
+        Self {
+            amount: proof.amount,
+            secret: proof.secret,
+            c: proof.c,
+            witness: proof.witness,
+            dleq: proof.dleq,
+        }
+    }
+}
+
+/// Proof v3 with short keyset id
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct ProofV3 {
+    /// Amount
+    pub amount: Amount,
+    /// Short keyset id
+    #[serde(rename = "id")]
+    pub keyset_id: ShortKeysetId,
+    /// Secret message
+    pub secret: Secret,
+    /// Unblinded signature
+    #[serde(rename = "C")]
+    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 ProofV3 {
+    /// [`ProofV3`] 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 ProofV3 {
+    fn from(proof: Proof) -> ProofV3 {
+        let Proof {
+            amount,
+            keyset_id,
+            secret,
+            c,
+            witness,
+            dleq,
+        } = proof;
+        ProofV3 {
+            amount,
+            secret,
+            c,
+            witness,
+            dleq,
+            keyset_id: keyset_id.into(),
+        }
+    }
+}
+
+impl Hash for ProofV3 {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.secret.hash(state);
+    }
+}
+
 fn serialize_v4_pubkey<S>(key: &PublicKey, serializer: S) -> Result<S::Ok, S::Error>
 where
     S: serde::Serializer,

+ 186 - 44
crates/cashu/src/nuts/nut00/token.rs

@@ -10,11 +10,11 @@ use bitcoin::base64::engine::{general_purpose, GeneralPurpose};
 use bitcoin::base64::{alphabet, Engine as _};
 use serde::{Deserialize, Serialize};
 
-use super::{Error, Proof, ProofV4, Proofs};
+use super::{Error, Proof, ProofV3, ProofV4, Proofs};
 use crate::mint_url::MintUrl;
-use crate::nuts::nut00::ProofsMethods;
+use crate::nut02::ShortKeysetId;
 use crate::nuts::{CurrencyUnit, Id};
-use crate::{ensure_cdk, Amount};
+use crate::{ensure_cdk, Amount, KeySetInfo};
 
 /// Token Enum
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -66,10 +66,10 @@ impl Token {
     }
 
     /// Proofs in [`Token`]
-    pub fn proofs(&self) -> Proofs {
+    pub fn proofs(&self, mint_keysets: &[KeySetInfo]) -> Result<Proofs, Error> {
         match self {
-            Self::TokenV3(token) => token.proofs(),
-            Self::TokenV4(token) => token.proofs(),
+            Self::TokenV3(token) => token.proofs(mint_keysets),
+            Self::TokenV4(token) => token.proofs(mint_keysets),
         }
     }
 
@@ -181,8 +181,8 @@ impl TryFrom<&Vec<u8>> for Token {
 pub struct TokenV3Token {
     /// Url of mint
     pub mint: MintUrl,
-    /// [`Proofs`]
-    pub proofs: Proofs,
+    /// [`Vec<ProofV3>`]
+    pub proofs: Vec<ProofV3>,
 }
 
 impl TokenV3Token {
@@ -190,7 +190,7 @@ impl TokenV3Token {
     pub fn new(mint_url: MintUrl, proofs: Proofs) -> Self {
         Self {
             mint: mint_url,
-            proofs,
+            proofs: proofs.into_iter().map(ProofV3::from).collect(),
         }
     }
 }
@@ -226,17 +226,21 @@ impl TokenV3 {
     }
 
     /// Proofs
-    pub fn proofs(&self) -> Proofs {
-        self.token
-            .iter()
-            .flat_map(|token| token.proofs.clone())
-            .collect()
+    pub fn proofs(&self, mint_keysets: &[KeySetInfo]) -> Result<Proofs, Error> {
+        let mut proofs: Proofs = vec![];
+        for t in self.token.iter() {
+            for p in t.proofs.iter() {
+                let long_id = Id::from_short_keyset_id(&p.keyset_id, mint_keysets)?;
+                proofs.push(p.into_proof(&long_id));
+            }
+        }
+        Ok(proofs)
     }
 
     /// Value - errors if duplicate proofs are found
     #[inline]
     pub fn value(&self) -> Result<Amount, Error> {
-        let proofs = self.proofs();
+        let proofs: Vec<ProofV3> = self.token.iter().flat_map(|t| t.proofs.clone()).collect();
         let unique_count = proofs
             .iter()
             .collect::<std::collections::HashSet<_>>()
@@ -247,7 +251,12 @@ impl TokenV3 {
             return Err(Error::DuplicateProofs);
         }
 
-        proofs.total_amount()
+        Ok(Amount::try_sum(
+            self.token
+                .iter()
+                .map(|t| Amount::try_sum(t.proofs.iter().map(|p| p.amount)))
+                .collect::<Result<Vec<Amount>, _>>()?,
+        )?)
     }
 
     /// Memo
@@ -306,10 +315,27 @@ impl fmt::Display for TokenV3 {
 
 impl From<TokenV4> for TokenV3 {
     fn from(token: TokenV4) -> Self {
-        let proofs = token.proofs();
+        let proofs: Vec<ProofV3> = token
+            .token
+            .into_iter()
+            .flat_map(|token| {
+                token.proofs.into_iter().map(move |p| ProofV3 {
+                    amount: p.amount,
+                    keyset_id: token.keyset_id.clone(),
+                    secret: p.secret,
+                    c: p.c,
+                    witness: p.witness,
+                    dleq: p.dleq,
+                })
+            })
+            .collect();
 
+        let token_v3_token = TokenV3Token {
+            mint: token.mint_url,
+            proofs,
+        };
         TokenV3 {
-            token: vec![TokenV3Token::new(token.mint_url, proofs)],
+            token: vec![token_v3_token],
             memo: token.memo,
             unit: Some(token.unit),
         }
@@ -335,17 +361,19 @@ pub struct TokenV4 {
 
 impl TokenV4 {
     /// Proofs from token
-    pub fn proofs(&self) -> Proofs {
-        self.token
-            .iter()
-            .flat_map(|token| token.proofs.iter().map(|p| p.into_proof(&token.keyset_id)))
-            .collect()
+    pub fn proofs(&self, mint_keysets: &[KeySetInfo]) -> Result<Proofs, Error> {
+        let mut proofs: Proofs = vec![];
+        for t in self.token.iter() {
+            let long_id = Id::from_short_keyset_id(&t.keyset_id, mint_keysets)?;
+            proofs.extend(t.proofs.iter().map(|p| p.into_proof(&long_id)));
+        }
+        Ok(proofs)
     }
 
     /// Value - errors if duplicate proofs are found
     #[inline]
     pub fn value(&self) -> Result<Amount, Error> {
-        let proofs = self.proofs();
+        let proofs: Vec<ProofV4> = self.token.iter().flat_map(|t| t.proofs.clone()).collect();
         let unique_count = proofs
             .iter()
             .collect::<std::collections::HashSet<_>>()
@@ -356,7 +384,12 @@ impl TokenV4 {
             return Err(Error::DuplicateProofs);
         }
 
-        proofs.total_amount()
+        Ok(Amount::try_sum(
+            self.token
+                .iter()
+                .map(|t| Amount::try_sum(t.proofs.iter().map(|p| p.amount)))
+                .collect::<Result<Vec<Amount>, _>>()?,
+        )?)
     }
 
     /// Memo
@@ -421,23 +454,29 @@ impl TryFrom<&Vec<u8>> for TokenV4 {
 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();
+        let proofs: Vec<ProofV3> = token.token.into_iter().flat_map(|t| t.proofs).collect();
 
         ensure_cdk!(mint_urls.len() == 1, Error::UnsupportedToken);
 
         let mint_url = mint_urls.first().ok_or(Error::UnsupportedToken)?;
 
         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))
+            .fold(
+                HashMap::<ShortKeysetId, Vec<ProofV4>>::new(),
+                |mut acc, val| {
+                    acc.entry(val.keyset_id.clone())
+                        .and_modify(|p: &mut Vec<ProofV4>| p.push(val.clone().into()))
+                        .or_insert(vec![val.clone().into()]);
+                    acc
+                },
+            )
+            .into_iter()
+            .map(|(id, proofs)| TokenV4Token {
+                keyset_id: id,
+                proofs,
+            })
             .collect();
 
         Ok(TokenV4 {
@@ -458,32 +497,34 @@ pub struct TokenV4Token {
         serialize_with = "serialize_v4_keyset_id",
         deserialize_with = "deserialize_v4_keyset_id"
     )]
-    pub keyset_id: Id,
+    pub keyset_id: ShortKeysetId,
     /// Proofs
     #[serde(rename = "p")]
     pub proofs: Vec<ProofV4>,
 }
 
-fn serialize_v4_keyset_id<S>(keyset_id: &Id, serializer: S) -> Result<S::Ok, S::Error>
+fn serialize_v4_keyset_id<S>(keyset_id: &ShortKeysetId, 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>
+fn deserialize_v4_keyset_id<'de, D>(deserializer: D) -> Result<ShortKeysetId, D::Error>
 where
     D: serde::Deserializer<'de>,
 {
     let bytes = Vec::<u8>::deserialize(deserializer)?;
-    Id::from_bytes(&bytes).map_err(serde::de::Error::custom)
+    ShortKeysetId::from_bytes(&bytes).map_err(serde::de::Error::custom)
 }
 
 impl TokenV4Token {
     /// Create new [`TokenV4Token`]
     pub fn new(keyset_id: Id, proofs: Proofs) -> Self {
+        // Create a short keyset id from id
+        let short_id = ShortKeysetId::from(keyset_id);
         Self {
-            keyset_id,
+            keyset_id: short_id,
             proofs: proofs.into_iter().map(|p| p.into()).collect(),
         }
     }
@@ -493,7 +534,10 @@ impl TokenV4Token {
 mod tests {
     use std::str::FromStr;
 
+    use bip39::rand::{self, RngCore};
+
     use super::*;
+    use crate::dhke::hash_to_curve;
     use crate::mint_url::MintUrl;
     use crate::secret::Secret;
     use crate::util::hex;
@@ -522,7 +566,7 @@ mod tests {
         );
         assert_eq!(
             token.token[0].keyset_id,
-            Id::from_str("00ad268c4d1f5826").unwrap()
+            ShortKeysetId::from_str("00ad268c4d1f5826").unwrap()
         );
 
         let encoded = &token.to_string();
@@ -546,12 +590,13 @@ mod tests {
 
         match token {
             Token::TokenV4(token) => {
-                let tokens: Vec<Id> = token.token.iter().map(|t| t.keyset_id).collect();
+                let tokens: Vec<ShortKeysetId> =
+                    token.token.iter().map(|t| t.keyset_id.clone()).collect();
 
                 assert_eq!(tokens.len(), 2);
 
-                assert!(tokens.contains(&Id::from_str("00ffd48b8f5ecf80").unwrap()));
-                assert!(tokens.contains(&Id::from_str("00ad268c4d1f5826").unwrap()));
+                assert!(tokens.contains(&ShortKeysetId::from_str("00ffd48b8f5ecf80").unwrap()));
+                assert!(tokens.contains(&ShortKeysetId::from_str("00ad268c4d1f5826").unwrap()));
 
                 let mint_url = token.mint_url;
 
@@ -584,7 +629,7 @@ mod tests {
         );
         assert_eq!(
             token.token[0].proofs[0].clone().keyset_id,
-            Id::from_str("009a1f293253e41e").unwrap()
+            ShortKeysetId::from_str("009a1f293253e41e").unwrap()
         );
         assert_eq!(token.unit.clone().unwrap(), CurrencyUnit::Sat);
 
@@ -684,4 +729,101 @@ mod tests {
         assert!(result.is_ok());
         assert_eq!(result.unwrap(), Amount::from(20));
     }
+
+    #[test]
+    fn test_token_from_proofs_with_idv2_round_trip() {
+        let mint_url = MintUrl::from_str("https://example.com").unwrap();
+
+        let keysets_info: Vec<KeySetInfo> = (0..10)
+            .map(|_| {
+                let mut bytes: [u8; 33] = [0u8; 33];
+                bytes[0] = 1u8;
+                rand::thread_rng().fill_bytes(&mut bytes[1..]);
+                let id = Id::from_bytes(&bytes).unwrap();
+                KeySetInfo {
+                    id,
+                    unit: CurrencyUnit::Sat,
+                    active: true,
+                    input_fee_ppk: 0,
+                    final_expiry: None,
+                }
+            })
+            .collect();
+
+        let chosen_keyset_id = keysets_info[0].id;
+        // Make up a bunch of fake proofs
+        let proofs = (0..5)
+            .map(|_| {
+                let mut c_preimage: [u8; 33] = [0u8; 33];
+                c_preimage[0] = 1u8;
+                rand::thread_rng().fill_bytes(&mut c_preimage[1..]);
+                Proof::new(
+                    Amount::from(1),
+                    chosen_keyset_id,
+                    Secret::generate(),
+                    hash_to_curve(&c_preimage).unwrap(),
+                )
+            })
+            .collect();
+
+        let token = Token::new(mint_url.clone(), proofs, None, CurrencyUnit::Sat);
+        let token_str = token.to_string();
+
+        let token1 = Token::from_str(&token_str);
+        assert!(token1.is_ok());
+
+        let proofs1 = token1.unwrap().proofs(&keysets_info);
+        assert!(proofs1.is_ok());
+
+        //println!("{:?}", proofs1);
+    }
+
+    #[test]
+    fn test_token_proofs_with_unknown_short_keyset_id() {
+        let mint_url = MintUrl::from_str("https://example.com").unwrap();
+
+        let keysets_info: Vec<KeySetInfo> = (0..10)
+            .map(|_| {
+                let mut bytes: [u8; 33] = [0u8; 33];
+                bytes[0] = 1u8;
+                rand::thread_rng().fill_bytes(&mut bytes[1..]);
+                let id = Id::from_bytes(&bytes).unwrap();
+                KeySetInfo {
+                    id,
+                    unit: CurrencyUnit::Sat,
+                    active: true,
+                    input_fee_ppk: 0,
+                    final_expiry: None,
+                }
+            })
+            .collect();
+
+        let chosen_keyset_id =
+            Id::from_str("01c352c0b47d42edb764bddf8c53d77b85f057157d92084d9d05e876251ecd8422")
+                .unwrap();
+
+        // Make up a bunch of fake proofs
+        let proofs = (0..5)
+            .map(|_| {
+                let mut c_preimage: [u8; 33] = [0u8; 33];
+                c_preimage[0] = 1u8;
+                rand::thread_rng().fill_bytes(&mut c_preimage[1..]);
+                Proof::new(
+                    Amount::from(1),
+                    chosen_keyset_id,
+                    Secret::generate(),
+                    hash_to_curve(&c_preimage).unwrap(),
+                )
+            })
+            .collect();
+
+        let token = Token::new(mint_url.clone(), proofs, None, CurrencyUnit::Sat);
+        let token_str = token.to_string();
+
+        let token1 = Token::from_str(&token_str);
+        assert!(token1.is_ok());
+
+        let proofs1 = token1.unwrap().proofs(&keysets_info);
+        assert!(proofs1.is_err());
+    }
 }

+ 387 - 65
crates/cashu/src/nuts/nut02.rs

@@ -42,6 +42,12 @@ pub enum Error {
     /// Keyset id does not match
     #[error("Keyset id incorrect")]
     IncorrectKeysetId,
+    /// Short keyset id does not match any of the provided IDv2s
+    #[error("Short keyset id does not match any of the provided IDv2s")]
+    UnknownShortKeysetId,
+    /// Short keyset id is ill-formed
+    #[error("Short keyset id is ill-formed")]
+    MalformedShortKeysetId,
     /// Slice Error
     #[error(transparent)]
     Slice(#[from] TryFromSliceError),
@@ -51,8 +57,10 @@ pub enum Error {
 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub enum KeySetVersion {
-    /// Current Version 00
+    /// Version 00
     Version00,
+    /// Version 01
+    Version01,
 }
 
 impl KeySetVersion {
@@ -60,6 +68,7 @@ impl KeySetVersion {
     pub fn to_byte(&self) -> u8 {
         match self {
             Self::Version00 => 0,
+            Self::Version01 => 1,
         }
     }
 
@@ -67,6 +76,7 @@ impl KeySetVersion {
     pub fn from_byte(byte: &u8) -> Result<Self, Error> {
         match byte {
             0 => Ok(Self::Version00),
+            1 => Ok(Self::Version01),
             _ => Err(Error::UnknownVersion),
         }
     }
@@ -76,6 +86,27 @@ impl fmt::Display for KeySetVersion {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
             KeySetVersion::Version00 => f.write_str("00"),
+            KeySetVersion::Version01 => f.write_str("01"),
+        }
+    }
+}
+
+/// Keyset ID bytes
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub enum IdBytes {
+    /// Bytes for v1
+    V1([u8; 7]),
+    /// Bytes for v2
+    V2([u8; 32]),
+}
+
+impl IdBytes {
+    /// Convert [`IdBytes`] to [`Vec<u8>`]
+    pub fn to_vec(&self) -> Vec<u8> {
+        match self {
+            IdBytes::V1(bytes) => bytes.to_vec(),
+            IdBytes::V2(bytes) => bytes.to_vec(),
         }
     }
 }
@@ -89,12 +120,14 @@ impl fmt::Display for KeySetVersion {
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub struct Id {
     version: KeySetVersion,
-    id: [u8; Self::BYTELEN],
+    id: IdBytes,
 }
 
 impl Id {
-    const STRLEN: usize = 14;
-    const BYTELEN: usize = 7;
+    const STRLEN_V1: usize = 14;
+    const BYTELEN_V1: usize = 7;
+    const STRLEN_V2: usize = 64;
+    const BYTELEN_V2: usize = 32;
 
     /// [`Id`] to bytes
     pub fn to_bytes(&self) -> Vec<u8> {
@@ -103,18 +136,122 @@ impl Id {
 
     /// [`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()?,
-        })
+        let version = KeySetVersion::from_byte(&bytes[0])?;
+        let id = match version {
+            KeySetVersion::Version00 => IdBytes::V1(bytes[1..].try_into()?),
+            KeySetVersion::Version01 => IdBytes::V2(bytes[1..].try_into()?),
+        };
+        Ok(Self { version, id })
+    }
+
+    /// Get the version of the keyset
+    pub fn get_version(&self) -> KeySetVersion {
+        self.version
+    }
+
+    /// *** V2 KEYSET ***
+    /// create [`Id`] v2 from keys, unit and (optionally) expiry
+    /// 1 - sort public keys by their amount in ascending order
+    /// 2 - concatenate all public keys to one byte array
+    /// 3 - concatenate the lowercase unit string to the byte array (e.g. "unit:sat")
+    /// 4 - If a final expiration is specified, convert it into a radix-10 string and concatenate it (e.g "final_expiry:1896187313")
+    /// 5 - HASH_SHA256 the concatenated byte array and take the first 31 bytes
+    /// 6 - prefix it with a keyset ID version byte
+    pub fn v2_from_data(map: &Keys, unit: &CurrencyUnit, expiry: Option<u64>) -> Self {
+        let mut keys: Vec<(&Amount, &super::PublicKey)> = map.iter().collect();
+        keys.sort_by_key(|(amt, _v)| *amt);
+
+        let mut pubkeys_concat: Vec<u8> = keys
+            .iter()
+            .map(|(_, pubkey)| pubkey.to_bytes())
+            .collect::<Vec<[u8; 33]>>()
+            .concat();
+
+        // Add the unit
+        pubkeys_concat.extend(b"unit:");
+        pubkeys_concat.extend(unit.to_string().to_lowercase().as_bytes());
+
+        // Add the expiration
+        if let Some(expiry) = expiry {
+            pubkeys_concat.extend(b"final_expiry:");
+            pubkeys_concat.extend(expiry.to_string().as_bytes());
+        }
+
+        let hash = Sha256::hash(&pubkeys_concat);
+        let hex_of_hash = hex::encode(hash.to_byte_array());
+
+        Self {
+            version: KeySetVersion::Version01,
+            id: IdBytes::V2(
+                hex::decode(&hex_of_hash[0..Self::STRLEN_V2])
+                    .expect("Keys hash could not be hex decoded")
+                    .try_into()
+                    .expect("Invalid length of hex id"),
+            ),
+        }
+    }
+
+    /// *** V1 VERSION ***
+    /// 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
+    pub fn v1_from_keys(map: &Keys) -> Self {
+        let mut keys: Vec<(&Amount, &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: IdBytes::V1(
+                hex::decode(&hex_of_hash[0..Self::STRLEN_V1])
+                    .expect("Keys hash could not be hex decoded")
+                    .try_into()
+                    .expect("Invalid length of hex id"),
+            ),
+        }
     }
 
-    /// [`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
+    /// Selects the correct IDv2 from a list of keysets and the given short-id
+    /// or returns the short-id in the case of v1.
+    pub fn from_short_keyset_id(
+        short_id: &ShortKeysetId,
+        keysets_info: &[KeySetInfo],
+    ) -> Result<Self, Error> {
+        // Check prefix length
+        if short_id.prefix.len() < Self::BYTELEN_V1 || short_id.prefix.len() > Self::BYTELEN_V2 {
+            return Err(Error::MalformedShortKeysetId);
+        }
+
+        match short_id.version {
+            KeySetVersion::Version00 => {
+                let mut idbytes: [u8; Self::BYTELEN_V1] = [0u8; Self::BYTELEN_V1];
+                idbytes.copy_from_slice(&short_id.prefix[..Self::BYTELEN_V1]);
+                Ok(Self {
+                    version: short_id.version,
+                    id: IdBytes::V1(idbytes),
+                })
+            }
+            KeySetVersion::Version01 => {
+                // We return the first match or error
+                for keyset_info in keysets_info.iter() {
+                    if keyset_info.id.id.to_vec()[..short_id.prefix.len()] == short_id.prefix {
+                        return Ok(keyset_info.id);
+                    }
+                }
+                Err(Error::UnknownShortKeysetId)
+            }
+        }
     }
 }
 
@@ -122,7 +259,9 @@ impl Id {
 // 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 id_bytes = value.to_bytes();
+        let mut hex_bytes: [u8; 8] = [0; 8];
+        hex_bytes.copy_from_slice(&id_bytes[..8]);
 
         let int = u64::from_be_bytes(hex_bytes);
 
@@ -132,13 +271,21 @@ impl From<Id> for 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)))
+        let hex_id = match self.id {
+            IdBytes::V1(id) => hex::encode(id),
+            IdBytes::V2(id) => hex::encode(id),
+        };
+        f.write_str(&format!("{}{}", self.version, hex_id))
     }
 }
 
 impl fmt::Debug for Id {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        f.write_str(&format!("{}{}", self.version, hex::encode(self.id)))
+        let hex_id = match self.id {
+            IdBytes::V1(id) => hex::encode(id),
+            IdBytes::V2(id) => hex::encode(id),
+        };
+        f.write_str(&format!("{}{}", self.version, hex_id))
     }
 }
 
@@ -146,14 +293,26 @@ impl TryFrom<String> for Id {
     type Error = Error;
 
     fn try_from(s: String) -> Result<Self, Self::Error> {
-        ensure_cdk!(s.len() == 16, Error::Length);
-
-        Ok(Self {
-            version: KeySetVersion::from_byte(&hex::decode(&s[..2])?[0])?,
-            id: hex::decode(&s[2..])?
-                .try_into()
-                .map_err(|_| Error::Length)?,
-        })
+        ensure_cdk!(
+            s.len() == Self::STRLEN_V1 + 2 || s.len() == Self::STRLEN_V2 + 2,
+            Error::Length
+        );
+
+        let version: KeySetVersion = KeySetVersion::from_byte(&hex::decode(&s[..2])?[0])?;
+        let id = match version {
+            KeySetVersion::Version00 => IdBytes::V1(
+                hex::decode(&s[2..])?
+                    .try_into()
+                    .map_err(|_| Error::Length)?,
+            ),
+            KeySetVersion::Version01 => IdBytes::V2(
+                hex::decode(&s[2..])?
+                    .try_into()
+                    .map_err(|_| Error::Length)?,
+            ),
+        };
+
+        Ok(Self { version, id })
     }
 }
 
@@ -171,33 +330,88 @@ impl From<Id> for 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<(&Amount, &super::PublicKey)> = map.iter().collect();
-        keys.sort_by_key(|(amt, _v)| *amt);
+/// Improper prefix of the keyset ID. In case of v1, this is the whole ID.
+/// In case of v2, this is the 8-byte prefix
+#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+#[serde(into = "String", try_from = "String")]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct ShortKeysetId {
+    /// The version of the short keyset
+    version: KeySetVersion,
+    /// The improper prefix of the keyset ID bytes
+    prefix: Vec<u8>,
+}
 
-        let pubkeys_concat: Vec<u8> = keys
-            .iter()
-            .map(|(_, pubkey)| pubkey.to_bytes())
-            .collect::<Vec<[u8; 33]>>()
-            .concat();
+impl ShortKeysetId {
+    /// [`ShortKeysetId`] to bytes
+    pub fn to_bytes(&self) -> Vec<u8> {
+        [vec![self.version.to_byte()], self.prefix.clone()].concat()
+    }
 
-        let hash = Sha256::hash(&pubkeys_concat);
-        let hex_of_hash = hex::encode(hash.to_byte_array());
+    /// [`ShortKeysetId`] from bytes
+    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
+        let version = KeySetVersion::from_byte(&bytes[0])?;
+        let prefix = bytes[1..].to_vec();
+        Ok(Self { version, prefix })
+    }
+}
 
-        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"),
-        }
+impl From<Id> for ShortKeysetId {
+    fn from(id: Id) -> Self {
+        let version = id.version;
+        let prefix: Vec<u8> = match id.version {
+            KeySetVersion::Version00 => match id.id {
+                IdBytes::V1(idbytes) => Vec::from(&idbytes),
+                _ => panic!("Unexpected IdBytes length"),
+            },
+            KeySetVersion::Version01 => match id.id {
+                IdBytes::V2(idbytes) => Vec::from(&idbytes[..7]),
+                _ => panic!("Unexpected IdBytes length"),
+            },
+        };
+
+        Self { version, prefix }
+    }
+}
+
+impl fmt::Display for ShortKeysetId {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let hex_id = hex::encode(&self.prefix);
+        f.write_str(&format!("{}{}", self.version, hex_id))
+    }
+}
+
+impl fmt::Debug for ShortKeysetId {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let hex_id = hex::encode(&self.prefix);
+        f.write_str(&format!("{}{}", self.version, hex_id))
+    }
+}
+
+impl TryFrom<String> for ShortKeysetId {
+    type Error = Error;
+
+    fn try_from(s: String) -> Result<Self, Self::Error> {
+        ensure_cdk!(s.len() == 16, Error::Length);
+
+        let version: KeySetVersion = KeySetVersion::from_byte(&hex::decode(&s[..2])?[0])?;
+        let prefix = hex::decode(&s[2..])?;
+
+        Ok(Self { version, prefix })
+    }
+}
+
+impl FromStr for ShortKeysetId {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Self::try_from(s.to_string())
+    }
+}
+
+impl From<ShortKeysetId> for String {
+    fn from(value: ShortKeysetId) -> Self {
+        value.to_string()
     }
 }
 
@@ -223,14 +437,26 @@ pub struct KeySet {
     pub unit: CurrencyUnit,
     /// Keyset [`Keys`]
     pub keys: Keys,
+    /// Expiry
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub final_expiry: Option<u64>,
 }
 
 impl KeySet {
-    /// Verify the keyset is matches keys
+    /// Verify the keyset id matches keys
     pub fn verify_id(&self) -> Result<(), Error> {
-        let keys_id: Id = (&self.keys).into();
+        match self.id.version {
+            KeySetVersion::Version00 => {
+                let keys_id: Id = Id::v1_from_keys(&self.keys);
+
+                ensure_cdk!(keys_id == self.id, Error::IncorrectKeysetId);
+            }
+            KeySetVersion::Version01 => {
+                let keys_id: Id = Id::v2_from_data(&self.keys, &self.unit, self.final_expiry);
 
-        ensure_cdk!(keys_id == self.id, Error::IncorrectKeysetId);
+                ensure_cdk!(keys_id == self.id, Error::IncorrectKeysetId);
+            }
+        }
 
         Ok(())
     }
@@ -243,6 +469,7 @@ impl From<MintKeySet> for KeySet {
             id: keyset.id,
             unit: keyset.unit,
             keys: Keys::from(keyset.keys),
+            final_expiry: keyset.final_expiry,
         }
     }
 }
@@ -265,6 +492,9 @@ pub struct KeySetInfo {
         default = "default_input_fee_ppk"
     )]
     pub input_fee_ppk: u64,
+    /// Expiry of the keyset
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub final_expiry: Option<u64>,
 }
 
 fn deserialize_input_fee_ppk<'de, D>(deserializer: D) -> Result<u64, D::Error>
@@ -290,6 +520,9 @@ pub struct MintKeySet {
     pub unit: CurrencyUnit,
     /// Keyset [`MintKeys`]
     pub keys: MintKeys,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    /// Expiry [`Option<u64>`]
+    pub final_expiry: Option<u64>,
 }
 
 #[cfg(feature = "mint")]
@@ -300,6 +533,8 @@ impl MintKeySet {
         xpriv: Xpriv,
         unit: CurrencyUnit,
         max_order: u8,
+        final_expiry: Option<u64>,
+        version: KeySetVersion,
     ) -> Self {
         let mut map = BTreeMap::new();
         for i in 0..max_order {
@@ -322,10 +557,15 @@ impl MintKeySet {
         }
 
         let keys = MintKeys::new(map);
+        let id = match version {
+            KeySetVersion::Version00 => Id::v1_from_keys(&keys.clone().into()),
+            KeySetVersion::Version01 => Id::v2_from_data(&keys.clone().into(), &unit, final_expiry),
+        };
         Self {
-            id: (&keys).into(),
+            id,
             unit,
             keys,
+            final_expiry,
         }
     }
 
@@ -336,6 +576,8 @@ impl MintKeySet {
         max_order: u8,
         currency_unit: CurrencyUnit,
         derivation_path: DerivationPath,
+        final_expiry: Option<u64>,
+        version: KeySetVersion,
     ) -> Self {
         let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, seed).expect("RNG busted");
         Self::generate(
@@ -345,6 +587,8 @@ impl MintKeySet {
                 .expect("RNG busted"),
             currency_unit,
             max_order,
+            final_expiry,
+            version,
         )
     }
 
@@ -355,6 +599,8 @@ impl MintKeySet {
         max_order: u8,
         currency_unit: CurrencyUnit,
         derivation_path: DerivationPath,
+        final_expiry: Option<u64>,
+        version: KeySetVersion,
     ) -> Self {
         Self::generate(
             secp,
@@ -363,6 +609,8 @@ impl MintKeySet {
                 .expect("RNG busted"),
             currency_unit,
             max_order,
+            final_expiry,
+            version,
         )
     }
 }
@@ -371,8 +619,10 @@ impl MintKeySet {
 impl From<MintKeySet> for Id {
     fn from(keyset: MintKeySet) -> Id {
         let keys: super::KeySet = keyset.into();
-
-        Id::from(&keys.keys)
+        match keys.id.version {
+            KeySetVersion::Version00 => Id::v1_from_keys(&keys.keys),
+            KeySetVersion::Version01 => Id::v2_from_data(&keys.keys, &keys.unit, keys.final_expiry),
+        }
     }
 }
 
@@ -381,7 +631,7 @@ impl From<&MintKeys> for Id {
     fn from(map: &MintKeys) -> Self {
         let keys: super::Keys = map.clone().into();
 
-        Id::from(&keys)
+        Id::v1_from_keys(&keys)
     }
 }
 
@@ -391,10 +641,11 @@ mod test {
 
     use bitcoin::secp256k1::rand::{self, RngCore};
 
-    use super::{KeySetInfo, Keys, KeysetResponse};
+    use super::{KeySetInfo, KeySetVersion, Keys, KeysetResponse, ShortKeysetId};
     use crate::nuts::nut02::{Error, Id};
     use crate::nuts::KeysResponse;
     use crate::util::hex;
+    use crate::CurrencyUnit;
 
     const SHORT_KEYSET_ID: &str = "00456a94ab4e1c46";
     const SHORT_KEYSET: &str = r#"
@@ -482,18 +733,44 @@ mod test {
 
         let keys: Keys = serde_json::from_str(SHORT_KEYSET).unwrap();
 
-        let id: Id = (&keys).into();
+        let id: Id = Id::v1_from_keys(&keys);
 
         assert_eq!(id, Id::from_str(SHORT_KEYSET_ID).unwrap());
 
         let keys: Keys = serde_json::from_str(KEYSET).unwrap();
 
-        let id: Id = (&keys).into();
+        let id: Id = Id::v1_from_keys(&keys);
 
         assert_eq!(id, Id::from_str(KEYSET_ID).unwrap());
     }
 
     #[test]
+    fn test_v2_deserialization_and_id_generation() {
+        let unit: CurrencyUnit = CurrencyUnit::from_str("sat").unwrap();
+        let expiry: u64 = 2059210353; // +10 years from now
+
+        let keys: Keys = serde_json::from_str(SHORT_KEYSET).unwrap();
+        let id_from_str =
+            Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
+                .unwrap();
+        let id = Id::v2_from_data(&keys, &unit, Some(expiry));
+        assert_eq!(id, id_from_str);
+
+        let keys: Keys = serde_json::from_str(KEYSET).unwrap();
+        let id_from_str =
+            Id::from_str("0125bc634e270ad7e937af5b957f8396bb627d73f6e1fd2ffe4294c26b57daf9e0")
+                .unwrap();
+        let id = Id::v2_from_data(&keys, &unit, Some(expiry));
+        assert_eq!(id, id_from_str);
+
+        let id = Id::v2_from_data(&keys, &unit, None);
+        let id_from_str =
+            Id::from_str("016d72f27c8d22808ad66d1959b3dab83af17e2510db7ffd57d2365d9eec3ced75")
+                .unwrap();
+        assert_eq!(id, id_from_str);
+    }
+
+    #[test]
     fn test_deserialization_keyset_info() {
         let h = r#"{"id":"009a1f293253e41e","unit":"sat","active":true}"#;
 
@@ -520,6 +797,15 @@ mod test {
     }
 
     #[test]
+    fn test_v2_to_int() {
+        let id = Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
+            .unwrap();
+
+        let id_int = u32::from(id);
+        assert_eq!(2113471806, id_int);
+    }
+
+    #[test]
     fn test_id_from_invalid_byte_length() {
         let three_bytes = [0x01, 0x02, 0x03];
         let result = Id::from_bytes(&three_bytes);
@@ -548,16 +834,28 @@ mod test {
         assert_eq!(keys_response.keysets.len(), 2);
     }
 
-    fn generate_random_id() -> Id {
-        let mut rand_bytes = vec![0u8; 8];
-        rand::thread_rng().fill_bytes(&mut rand_bytes[1..]);
-        Id::from_bytes(&rand_bytes)
-            .unwrap_or_else(|e| panic!("Failed to create Id from {}: {e}", hex::encode(rand_bytes)))
+    fn generate_random_id(version: KeySetVersion) -> Id {
+        match version {
+            KeySetVersion::Version00 => {
+                let mut rand_bytes = vec![0u8; 8];
+                rand::thread_rng().fill_bytes(&mut rand_bytes[1..]);
+                Id::from_bytes(&rand_bytes).unwrap_or_else(|e| {
+                    panic!("Failed to create Id from {}: {e}", hex::encode(rand_bytes))
+                })
+            }
+            KeySetVersion::Version01 => {
+                let mut rand_bytes = vec![1u8; 33];
+                rand::thread_rng().fill_bytes(&mut rand_bytes[1..]);
+                Id::from_bytes(&rand_bytes).unwrap_or_else(|e| {
+                    panic!("Failed to create Id from {}: {e}", hex::encode(rand_bytes))
+                })
+            }
+        }
     }
 
     #[test]
     fn test_id_serialization() {
-        let id = generate_random_id();
+        let id = generate_random_id(KeySetVersion::Version00);
         let id_str = id.to_string();
 
         assert!(id_str.chars().all(|c| c.is_ascii_hexdigit()));
@@ -566,6 +864,16 @@ mod test {
     }
 
     #[test]
+    fn test_id_v2_serialization() {
+        let id = generate_random_id(KeySetVersion::Version01);
+        let id_str = id.to_string();
+
+        assert!(id_str.chars().all(|c| c.is_ascii_hexdigit()));
+        assert_eq!(66, id_str.len());
+        assert_eq!(id_str.to_lowercase(), id_str);
+    }
+
+    #[test]
     fn test_id_deserialization() {
         let id_from_short_str = Id::from_str("00123");
         assert!(matches!(id_from_short_str, Err(Error::Length)));
@@ -579,4 +887,18 @@ mod test {
         let id_from_uppercase = Id::from_str(&SHORT_KEYSET_ID.to_uppercase());
         assert!(id_from_uppercase.is_ok());
     }
+
+    #[test]
+    fn test_short_keyset_id_from_id() {
+        let idv1 = Id::from_str("009a1f293253e41e").unwrap();
+        let idv2 =
+            Id::from_str("01adc013fa9d85171586660abab27579888611659d357bc86bc09cb26eee8bc035")
+                .unwrap();
+
+        let short_id_1: ShortKeysetId = idv1.into();
+        let short_id_2: ShortKeysetId = idv2.into();
+
+        assert!(short_id_1.to_string() == "009a1f293253e41e");
+        assert!(short_id_2.to_string() == "01adc013fa9d8517");
+    }
 }

+ 13 - 1
crates/cdk-cli/src/sub_commands/pay_request.rs

@@ -91,7 +91,19 @@ pub async fn pay_request(
             },
         )
         .await?;
-    let proofs = matching_wallet.send(prepared_send, None).await?.proofs();
+
+    let token = matching_wallet.send(prepared_send, None).await?;
+
+    // We need the keysets information to properly convert from token proof to proof
+    let keysets_info = match matching_wallet
+        .localstore
+        .get_mint_keysets(token.mint_url()?)
+        .await?
+    {
+        Some(keysets_info) => keysets_info,
+        None => matching_wallet.get_mint_keysets().await?, // Hit the keysets endpoint if we don't have the keysets for this Mint
+    };
+    let proofs = token.proofs(&keysets_info)?;
 
     if let Some(transport) = transport {
         let payload = PaymentRequestPayload {

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

@@ -19,7 +19,7 @@ async fn setup_keyset<E: Debug, DB: Database<E> + KeysDatabase<Err = E>>(db: &DB
         unit: CurrencyUnit::Sat,
         active: true,
         valid_from: 0,
-        valid_to: None,
+        final_expiry: None,
         derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
         derivation_path_index: Some(0),
         max_order: 32,

+ 2 - 1
crates/cdk-common/src/database/wallet.rs

@@ -4,6 +4,7 @@ use std::collections::HashMap;
 use std::fmt::Debug;
 
 use async_trait::async_trait;
+use cashu::KeySet;
 
 use super::Error;
 use crate::common::ProofInfo;
@@ -72,7 +73,7 @@ pub trait Database: Debug {
     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>;
+    async fn add_keys(&self, keyset: KeySet) -> Result<(), Self::Err>;
     /// Get [`Keys`] from storage
     async fn get_keys(&self, id: &Id) -> Result<Option<Keys>, Self::Err>;
     /// Remove [`Keys`] from storage

+ 3 - 3
crates/cdk-common/src/mint.rs

@@ -139,9 +139,6 @@ pub struct MintKeySetInfo {
     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
@@ -151,6 +148,8 @@ pub struct MintKeySetInfo {
     /// Input Fee ppk
     #[serde(default = "default_fee")]
     pub input_fee_ppk: u64,
+    /// Final expiry
+    pub final_expiry: Option<u64>,
 }
 
 /// Default fee
@@ -165,6 +164,7 @@ impl From<MintKeySetInfo> for KeySetInfo {
             unit: keyset_info.unit,
             active: keyset_info.active,
             input_fee_ppk: keyset_info.input_fee_ppk,
+            final_expiry: keyset_info.final_expiry,
         }
     }
 }

+ 20 - 16
crates/cdk-integration-tests/tests/integration_tests_pure.rs

@@ -77,10 +77,11 @@ async fn test_swap_to_send() {
         )
         .await
         .expect("Failed to send token");
+    let keysets_info = wallet_alice.get_mint_keysets().await.unwrap();
+    let token_proofs = token.proofs(&keysets_info).unwrap();
     assert_eq!(
         Amount::from(40),
-        token
-            .proofs()
+        token_proofs
             .total_amount()
             .expect("Failed to get total amount")
     );
@@ -92,7 +93,7 @@ async fn test_swap_to_send() {
             .expect("Failed to get balance")
     );
     assert_eq!(
-        HashSet::<_, RandomState>::from_iter(token.proofs().ys().expect("Failed to get ys")),
+        HashSet::<_, RandomState>::from_iter(token_proofs.ys().expect("Failed to get ys")),
         HashSet::from_iter(
             wallet_alice
                 .get_pending_spent_proofs()
@@ -103,7 +104,8 @@ async fn test_swap_to_send() {
         )
     );
 
-    let transaction_id = TransactionId::from_proofs(token.proofs()).expect("Failed to get tx id");
+    let transaction_id =
+        TransactionId::from_proofs(token_proofs.clone()).expect("Failed to get tx id");
 
     let transaction = wallet_alice
         .get_transaction(transaction_id)
@@ -115,7 +117,7 @@ async fn test_swap_to_send() {
     assert_eq!(Amount::from(40), transaction.amount);
     assert_eq!(Amount::from(0), transaction.fee);
     assert_eq!(CurrencyUnit::Sat, transaction.unit);
-    assert_eq!(token.proofs().ys().unwrap(), transaction.ys);
+    assert_eq!(token_proofs.ys().unwrap(), transaction.ys);
 
     // Alice sends cashu, Carol receives
     let wallet_carol = create_test_wallet_for_mint(mint_bob.clone())
@@ -123,7 +125,7 @@ async fn test_swap_to_send() {
         .expect("Failed to create Carol's wallet");
     let received_amount = wallet_carol
         .receive_proofs(
-            token.proofs(),
+            token_proofs.clone(),
             ReceiveOptions::default(),
             token.memo().clone(),
         )
@@ -149,7 +151,7 @@ async fn test_swap_to_send() {
     assert_eq!(Amount::from(40), transaction.amount);
     assert_eq!(Amount::from(0), transaction.fee);
     assert_eq!(CurrencyUnit::Sat, transaction.unit);
-    assert_eq!(token.proofs().ys().unwrap(), transaction.ys);
+    assert_eq!(token_proofs.ys().unwrap(), transaction.ys);
     assert_eq!(token.memo().clone(), transaction.memo);
 }
 
@@ -237,8 +239,8 @@ async fn test_mint_double_spend() {
         .await
         .expect("Could not get proofs");
 
-    let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys;
-    let keyset_id = Id::from(&keys);
+    let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
+    let keyset_id = keys.id;
 
     let preswap = PreMintSecrets::random(
         keyset_id,
@@ -294,8 +296,8 @@ async fn test_attempt_to_swap_by_overflowing() {
 
     let amount = 2_u64.pow(63);
 
-    let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys;
-    let keyset_id = Id::from(&keys);
+    let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
+    let keyset_id = keys.id;
 
     let pre_mint_amount =
         PreMintSecrets::random(keyset_id, amount.into(), &SplitTarget::default()).unwrap();
@@ -532,7 +534,7 @@ async fn test_swap_overpay_underpay_fee() {
         .expect("Could not get proofs");
 
     let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys;
-    let keyset_id = Id::from(&keys);
+    let keyset_id = Id::v1_from_keys(&keys);
 
     let preswap = PreMintSecrets::random(keyset_id, 9998.into(), &SplitTarget::default()).unwrap();
 
@@ -597,8 +599,8 @@ async fn test_mint_enforce_fee() {
         .await
         .expect("Could not get proofs");
 
-    let keys = mint_bob.pubkeys().keysets.first().unwrap().clone().keys;
-    let keyset_id = Id::from(&keys);
+    let keys = mint_bob.pubkeys().keysets.first().unwrap().clone();
+    let keyset_id = keys.id;
 
     let five_proofs: Vec<_> = proofs.drain(..5).collect();
 
@@ -884,6 +886,8 @@ async fn test_concurrent_double_spend_melt() {
 }
 
 async fn get_keyset_id(mint: &Mint) -> Id {
-    let keys = mint.pubkeys().keysets.first().unwrap().clone().keys;
-    Id::from(&keys)
+    let keys = mint.pubkeys().keysets.first().unwrap().clone();
+    keys.verify_id()
+        .expect("Keyset ID generation is successful");
+    keys.id
 }

+ 9 - 4
crates/cdk-redb/src/wallet/mod.rs

@@ -13,7 +13,8 @@ use cdk_common::mint_url::MintUrl;
 use cdk_common::util::unix_time;
 use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId};
 use cdk_common::{
-    database, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions, State,
+    database, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions,
+    State,
 };
 use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition};
 use tracing::instrument;
@@ -493,15 +494,19 @@ impl WalletDatabase for WalletRedbDatabase {
     }
 
     #[instrument(skip_all)]
-    async fn add_keys(&self, keys: Keys) -> Result<(), Self::Err> {
+    async fn add_keys(&self, keyset: KeySet) -> Result<(), Self::Err> {
         let write_txn = self.db.begin_write().map_err(Error::from)?;
 
+        keyset.verify_id()?;
+
         {
             let mut table = write_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
             table
                 .insert(
-                    Id::from(&keys).to_string().as_str(),
-                    serde_json::to_string(&keys).map_err(Error::from)?.as_str(),
+                    keyset.id.to_string().as_str(),
+                    serde_json::to_string(&keyset.keys)
+                        .map_err(Error::from)?
+                        .as_str(),
                 )
                 .map_err(Error::from)?;
         }

+ 5 - 2
crates/cdk-rexie/src/wallet.rs

@@ -547,7 +547,10 @@ impl WalletDatabase for WalletRexieDatabase {
         Ok(())
     }
 
-    async fn add_keys(&self, keys: Keys) -> Result<(), Self::Err> {
+    async fn add_keys(&self, keyset: KeySet) -> Result<(), Self::Err> {
+        // Verify ID by recomputing id
+        keyset.verify_id()?;
+
         let rexie = self.db.lock().await;
 
         let transaction = rexie
@@ -556,7 +559,7 @@ impl WalletDatabase for WalletRexieDatabase {
 
         let keys_store = transaction.store(MINT_KEYS).map_err(Error::from)?;
 
-        let keyset_id = serde_wasm_bindgen::to_value(&Id::from(&keys)).map_err(Error::from)?;
+        let keyset_id = serde_wasm_bindgen::to_value(&keyset.id).map_err(Error::from)?;
         let keys = serde_wasm_bindgen::to_value(&keys).map_err(Error::from)?;
 
         keys_store

+ 10 - 1
crates/cdk-signatory/src/common.rs

@@ -68,6 +68,8 @@ pub async fn init_keysets(
                         highest_index_keyset.max_order,
                         highest_index_keyset.unit.clone(),
                         highest_index_keyset.derivation_path.clone(),
+                        highest_index_keyset.final_expiry,
+                        cdk_common::nut02::KeySetVersion::Version00,
                     );
                     active_keysets.insert(id, keyset);
                     let mut keyset_info = highest_index_keyset;
@@ -97,6 +99,8 @@ pub async fn init_keysets(
                         unit.clone(),
                         *max_order,
                         *input_fee_ppk,
+                        // TODO: add Mint settings for a final expiry of newly generated keysets
+                        None,
                     );
 
                     let id = keyset_info.id;
@@ -114,6 +118,7 @@ pub async fn init_keysets(
 
 /// Generate new [`MintKeySetInfo`] from path
 #[tracing::instrument(skip_all)]
+#[allow(clippy::too_many_arguments)]
 pub fn create_new_keyset<C: secp256k1::Signing>(
     secp: &secp256k1::Secp256k1<C>,
     xpriv: Xpriv,
@@ -122,6 +127,7 @@ pub fn create_new_keyset<C: secp256k1::Signing>(
     unit: CurrencyUnit,
     max_order: u8,
     input_fee_ppk: u64,
+    final_expiry: Option<u64>,
 ) -> (MintKeySet, MintKeySetInfo) {
     let keyset = MintKeySet::generate(
         secp,
@@ -130,13 +136,16 @@ pub fn create_new_keyset<C: secp256k1::Signing>(
             .expect("RNG busted"),
         unit,
         max_order,
+        final_expiry,
+        // TODO: change this to Version01 to generate keysets v2
+        cdk_common::nut02::KeySetVersion::Version00,
     );
     let keyset_info = MintKeySetInfo {
         id: keyset.id,
         unit: keyset.unit.clone(),
         active: true,
         valid_from: unix_time(),
-        valid_to: None,
+        final_expiry: keyset.final_expiry,
         derivation_path,
         derivation_path_index,
         max_order,

+ 10 - 0
crates/cdk-signatory/src/db_signatory.rs

@@ -72,6 +72,8 @@ impl DbSignatory {
                     unit.clone(),
                     max_order,
                     fee,
+                    // TODO: add and connect settings for this
+                    None,
                 );
 
                 let id = keyset_info.id;
@@ -130,6 +132,8 @@ impl DbSignatory {
             keyset_info.max_order,
             keyset_info.unit.clone(),
             keyset_info.derivation_path.clone(),
+            keyset_info.final_expiry,
+            keyset_info.id.get_version(),
         )
     }
 }
@@ -236,6 +240,8 @@ impl Signatory for DbSignatory {
             args.unit.clone(),
             args.max_order,
             args.input_fee_ppk,
+            // TODO: add and connect settings for this
+            None,
         );
         let id = info.id;
         self.localstore.add_keyset_info(info.clone()).await?;
@@ -266,6 +272,8 @@ mod test {
             2,
             CurrencyUnit::Sat,
             derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(),
+            None,
+            cdk_common::nut02::KeySetVersion::Version00,
         );
 
         assert_eq!(keyset.unit, CurrencyUnit::Sat);
@@ -310,6 +318,8 @@ mod test {
             2,
             CurrencyUnit::Sat,
             derivation_path_from_unit(CurrencyUnit::Sat, 0).unwrap(),
+            None,
+            cdk_common::nut02::KeySetVersion::Version00,
         );
 
         assert_eq!(keyset.unit, CurrencyUnit::Sat);

+ 5 - 0
crates/cdk-signatory/src/proto/convert.rs

@@ -60,6 +60,7 @@ impl TryInto<crate::signatory::SignatoryKeySet> for KeySet {
                     .map(|(amount, pk)| PublicKey::from_slice(&pk).map(|pk| (amount.into(), pk)))
                     .collect::<Result<BTreeMap<Amount, _>, _>>()?,
             ),
+            final_expiry: self.final_expiry,
         })
     }
 }
@@ -78,6 +79,7 @@ impl From<crate::signatory::SignatoryKeySet> for KeySet {
                     .map(|(key, value)| ((*key).into(), value.to_bytes().to_vec()))
                     .collect(),
             }),
+            final_expiry: keyset.final_expiry,
         }
     }
 }
@@ -393,6 +395,7 @@ impl TryInto<cdk_common::KeySet> for KeySet {
                     .map(|(k, v)| cdk_common::PublicKey::from_slice(&v).map(|pk| (k.into(), pk)))
                     .collect::<Result<BTreeMap<cdk_common::Amount, cdk_common::PublicKey>, _>>()?,
             ),
+            final_expiry: self.final_expiry,
         })
     }
 }
@@ -433,6 +436,7 @@ impl From<cdk_common::KeySetInfo> for KeySet {
             active: value.active,
             input_fee_ppk: value.input_fee_ppk,
             keys: Default::default(),
+            final_expiry: value.final_expiry,
         }
     }
 }
@@ -450,6 +454,7 @@ impl TryInto<cdk_common::KeySetInfo> for KeySet {
                 .map_err(|_| cdk_common::Error::Custom("Invalid unit encoding".to_owned()))?,
             active: self.active,
             input_fee_ppk: self.input_fee_ppk,
+            final_expiry: self.final_expiry,
         })
     }
 }

+ 1 - 0
crates/cdk-signatory/src/proto/signatory.proto

@@ -62,6 +62,7 @@ message KeySet {
   bool active = 3;
   uint64 input_fee_ppk = 4;
   Keys keys = 5;
+  optional uint64 final_expiry = 6;
 }
 
 message Keys {

+ 5 - 1
crates/cdk-signatory/src/signatory.rs

@@ -73,6 +73,8 @@ pub struct SignatoryKeySet {
     pub keys: Keys,
     /// Information about the fee per public key
     pub input_fee_ppk: u64,
+    /// Final expiry of the keyset (unix timestamp in the future)
+    pub final_expiry: Option<u64>,
 }
 
 impl From<&SignatoryKeySet> for KeySet {
@@ -87,6 +89,7 @@ impl From<SignatoryKeySet> for KeySet {
             id: val.id,
             unit: val.unit,
             keys: val.keys,
+            final_expiry: val.final_expiry,
         }
     }
 }
@@ -107,7 +110,7 @@ impl From<SignatoryKeySet> for MintKeySetInfo {
             derivation_path: Default::default(),
             derivation_path_index: Default::default(),
             max_order: 0,
-            valid_to: None,
+            final_expiry: val.final_expiry,
             valid_from: 0,
         }
     }
@@ -121,6 +124,7 @@ impl From<&(MintKeySetInfo, MintKeySet)> for SignatoryKeySet {
             active: info.active,
             input_fee_ppk: info.input_fee_ppk,
             keys: key.keys.clone().into(),
+            final_expiry: key.final_expiry,
         }
     }
 }

+ 1 - 1
crates/cdk-sqlite/src/mint/auth/mod.rs

@@ -121,7 +121,7 @@ impl MintAuthDatabase for MintSqliteAuthDatabase {
         .bind(":unit", keyset.unit.to_string())
         .bind(":active", keyset.active)
         .bind(":valid_from", keyset.valid_from as i64)
-        .bind(":valid_to", keyset.valid_to.map(|v| v as i64))
+        .bind(":valid_to", keyset.final_expiry.map(|v| v as i64))
         .bind(":derivation_path", keyset.derivation_path.to_string())
         .bind(":max_order", keyset.max_order)
         .bind(":derivation_path_index", keyset.derivation_path_index)

+ 4 - 4
crates/cdk-sqlite/src/mint/mod.rs

@@ -214,7 +214,7 @@ impl MintKeysDatabase for MintSqliteDatabase {
         .bind(":unit", keyset.unit.to_string())
         .bind(":active", keyset.active)
         .bind(":valid_from", keyset.valid_from as i64)
-        .bind(":valid_to", keyset.valid_to.map(|v| v as i64))
+        .bind(":valid_to", keyset.final_expiry.map(|v| v as i64))
         .bind(":derivation_path", keyset.derivation_path.to_string())
         .bind(":max_order", keyset.max_order)
         .bind(":input_fee_ppk", keyset.input_fee_ppk as i64)
@@ -1134,11 +1134,11 @@ fn sqlite_row_to_keyset_info(row: Vec<Column>) -> Result<MintKeySetInfo, Error>
         unit: column_as_string!(unit, CurrencyUnit::from_str),
         active: matches!(active, Column::Integer(1)),
         valid_from: column_as_number!(valid_from),
-        valid_to: column_as_nullable_number!(valid_to),
         derivation_path: column_as_string!(derivation_path, DerivationPath::from_str),
         derivation_path_index: column_as_nullable_number!(derivation_path_index),
         max_order: column_as_number!(max_order),
         input_fee_ppk: column_as_number!(row_keyset_ppk),
+        final_expiry: column_as_nullable_number!(valid_to),
     })
 }
 
@@ -1319,11 +1319,11 @@ mod tests {
             unit: CurrencyUnit::Sat,
             active: true,
             valid_from: 0,
-            valid_to: None,
             derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
             derivation_path_index: Some(0),
             max_order: 32,
             input_fee_ppk: 0,
+            final_expiry: None,
         };
         db.add_keyset_info(keyset_info).await.unwrap();
 
@@ -1387,11 +1387,11 @@ mod tests {
             unit: CurrencyUnit::Sat,
             active: true,
             valid_from: 0,
-            valid_to: None,
             derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
             derivation_path_index: Some(0),
             max_order: 32,
             input_fee_ppk: 0,
+            final_expiry: None,
         };
         db.add_keyset_info(keyset_info).await.unwrap();
 

+ 1 - 0
crates/cdk-sqlite/src/wallet/migrations.rs

@@ -16,4 +16,5 @@ pub static MIGRATIONS: &[(&str, &str)] = &[
     ("20250314082116_allow_pending_spent.sql", include_str!(r#"./migrations/20250314082116_allow_pending_spent.sql"#)),
     ("20250323152040_wallet_dleq_proofs.sql", include_str!(r#"./migrations/20250323152040_wallet_dleq_proofs.sql"#)),
     ("20250401120000_add_transactions_table.sql", include_str!(r#"./migrations/20250401120000_add_transactions_table.sql"#)),
+    ("20250616144830_add_keyset_expiry.sql", include_str!(r#"./migrations/20250616144830_add_keyset_expiry.sql"#)),
 ];

+ 1 - 0
crates/cdk-sqlite/src/wallet/migrations/20250616144830_add_keyset_expiry.sql

@@ -0,0 +1 @@
+ALTER TABLE keyset ADD COLUMN final_expiry INTEGER DEFAULT NULL;

+ 24 - 11
crates/cdk-sqlite/src/wallet/mod.rs

@@ -14,8 +14,8 @@ use cdk_common::nuts::{MeltQuoteState, MintQuoteState};
 use cdk_common::secret::Secret;
 use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId};
 use cdk_common::{
-    database, Amount, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proof, ProofDleq, PublicKey,
-    SecretKey, SpendingConditions, State,
+    database, Amount, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, MintInfo, Proof, ProofDleq,
+    PublicKey, SecretKey, SpendingConditions, State,
 };
 use error::Error;
 use tracing::instrument;
@@ -294,14 +294,15 @@ ON CONFLICT(mint_url) DO UPDATE SET
             Statement::new(
                 r#"
     INSERT INTO keyset
-    (mint_url, id, unit, active, input_fee_ppk)
+    (mint_url, id, unit, active, input_fee_ppk, final_expiry)
     VALUES
-    (:mint_url, :id, :unit, :active, :input_fee_ppk)
+    (:mint_url, :id, :unit, :active, :input_fee_ppk, :final_expiry)
     ON CONFLICT(id) DO UPDATE SET
         mint_url = excluded.mint_url,
         unit = excluded.unit,
         active = excluded.active,
-        input_fee_ppk = excluded.input_fee_ppk;
+        input_fee_ppk = excluded.input_fee_ppk,
+        final_expiry = excluded.final_expiry;
     "#,
             )
             .bind(":mint_url", mint_url.to_string())
@@ -309,6 +310,7 @@ ON CONFLICT(mint_url) DO UPDATE SET
             .bind(":unit", keyset.unit.to_string())
             .bind(":active", keyset.active)
             .bind(":input_fee_ppk", keyset.input_fee_ppk as i64)
+            .bind(":final_expiry", keyset.final_expiry.map(|v| v as i64))
             .execute(&conn)
             .map_err(Error::Sqlite)?;
         }
@@ -327,7 +329,8 @@ ON CONFLICT(mint_url) DO UPDATE SET
                 id,
                 unit,
                 active,
-                input_fee_ppk
+                input_fee_ppk,
+                final_expiry
             FROM
                 keyset
             WHERE mint_url = :mint_url
@@ -354,7 +357,8 @@ ON CONFLICT(mint_url) DO UPDATE SET
                 id,
                 unit,
                 active,
-                input_fee_ppk
+                input_fee_ppk,
+                final_expiry
             FROM
                 keyset
             WHERE id = :id
@@ -528,7 +532,10 @@ ON CONFLICT(id) DO UPDATE SET
     }
 
     #[instrument(skip_all)]
-    async fn add_keys(&self, keys: Keys) -> Result<(), Self::Err> {
+    async fn add_keys(&self, keyset: KeySet) -> Result<(), Self::Err> {
+        // Recompute ID for verification
+        keyset.verify_id()?;
+
         Statement::new(
             r#"
             INSERT INTO key
@@ -539,8 +546,11 @@ ON CONFLICT(id) DO UPDATE SET
                 keys = excluded.keys
         "#,
         )
-        .bind(":id", Id::from(&keys).to_string())
-        .bind(":keys", serde_json::to_string(&keys).map_err(Error::from)?)
+        .bind(":id", keyset.id.to_string())
+        .bind(
+            ":keys",
+            serde_json::to_string(&keyset.keys).map_err(Error::from)?,
+        )
         .execute(&self.pool.get().map_err(Error::Pool)?)
         .map_err(Error::Sqlite)?;
 
@@ -909,13 +919,15 @@ fn sqlite_row_to_mint_info(row: Vec<Column>) -> Result<MintInfo, Error> {
     })
 }
 
+#[instrument(skip_all)]
 fn sqlite_row_to_keyset(row: Vec<Column>) -> Result<KeySetInfo, Error> {
     unpack_into!(
         let (
             id,
             unit,
             active,
-            input_fee_ppk
+            input_fee_ppk,
+            final_expiry
         ) = row
     );
 
@@ -924,6 +936,7 @@ fn sqlite_row_to_keyset(row: Vec<Column>) -> Result<KeySetInfo, Error> {
         unit: column_as_string!(unit, CurrencyUnit::from_str),
         active: matches!(active, Column::Integer(1)),
         input_fee_ppk: column_as_nullable_number!(input_fee_ppk).unwrap_or_default(),
+        final_expiry: column_as_nullable_number!(final_expiry),
     })
 }
 

+ 1 - 0
crates/cdk/src/mint/keysets/auth.rs

@@ -39,6 +39,7 @@ impl Mint {
                             unit: key.unit.clone(),
                             active: key.active,
                             input_fee_ppk: key.input_fee_ppk,
+                            final_expiry: key.final_expiry,
                         })
                     } else {
                         None

+ 1 - 0
crates/cdk/src/mint/keysets/mod.rs

@@ -53,6 +53,7 @@ impl Mint {
                     unit: k.unit.clone(),
                     active: k.active,
                     input_fee_ppk: k.input_fee_ppk,
+                    final_expiry: k.final_expiry,
                 })
                 .collect(),
         }

+ 1 - 1
crates/cdk/src/wallet/auth/auth_wallet.rs

@@ -180,7 +180,7 @@ impl AuthWallet {
 
             keys.verify_id()?;
 
-            self.localstore.add_keys(keys.keys.clone()).await?;
+            self.localstore.add_keys(keys.clone()).await?;
 
             keys.keys
         };

+ 18 - 2
crates/cdk/src/wallet/keysets.rs

@@ -19,7 +19,7 @@ impl Wallet {
 
             keys.verify_id()?;
 
-            self.localstore.add_keys(keys.keys.clone()).await?;
+            self.localstore.add_keys(keys.clone()).await?;
 
             keys.keys
         };
@@ -27,7 +27,23 @@ impl Wallet {
         Ok(keys)
     }
 
-    /// Get keysets for mint
+    /// Get keysets from DB or fetch them
+    ///
+    /// Checks the database for keysets and queries the Mint if
+    /// it can't find any.
+    #[instrument(skip(self))]
+    pub async fn load_mint_keysets(&self) -> Result<Vec<KeySetInfo>, Error> {
+        match self
+            .localstore
+            .get_mint_keysets(self.mint_url.clone())
+            .await?
+        {
+            Some(keysets_info) => Ok(keysets_info),
+            None => self.get_mint_keysets().await, // Hit the keysets endpoint if we don't have the keysets for this Mint
+        }
+    }
+
+    /// Get keysets for wallet's mint
     ///
     /// Queries mint for all keysets
     #[instrument(skip(self))]

+ 7 - 3
crates/cdk/src/wallet/mod.rs

@@ -474,7 +474,7 @@ impl Wallet {
     /// Can be used to allow a wallet to accept payments offline while reducing
     /// the risk of claiming back to the limits let by the spending_conditions
     #[instrument(skip(self, token))]
-    pub fn verify_token_p2pk(
+    pub async fn verify_token_p2pk(
         &self,
         token: &Token,
         spending_conditions: SpendingConditions,
@@ -526,8 +526,10 @@ impl Wallet {
                 token.mint_url()?
             )));
         }
+        // We need the keysets information to properly convert from token proof to proof
+        let keysets_info = self.load_mint_keysets().await?;
+        let proofs = token.proofs(&keysets_info)?;
 
-        let proofs = token.proofs();
         for proof in proofs {
             let secret: nut10::Secret = (&proof.secret).try_into()?;
 
@@ -620,7 +622,9 @@ impl Wallet {
         //     )));
         // }
 
-        let proofs = token.proofs();
+        // We need the keysets information to properly convert from token proof to proof
+        let keysets_info = self.load_mint_keysets().await?;
+        let proofs = token.proofs(&keysets_info)?;
         for proof in proofs {
             let mint_pubkey = match keys_cache.get(&proof.keyset_id) {
                 Some(keys) => keys.amount_key(proof.amount),

+ 17 - 7
crates/cdk/src/wallet/multi_mint_wallet.rs

@@ -271,12 +271,6 @@ impl MultiMintWallet {
         let token_data = Token::from_str(encoded_token)?;
         let unit = token_data.unit().unwrap_or_default();
 
-        let proofs = token_data.proofs();
-
-        let mut amount_received = Amount::ZERO;
-
-        let mut mint_errors = None;
-
         let mint_url = token_data.mint_url()?;
 
         // Check that all mints in tokes have wallets
@@ -291,6 +285,22 @@ impl MultiMintWallet {
             .get(&wallet_key)
             .ok_or(Error::UnknownWallet(wallet_key.clone()))?;
 
+        // We need the keysets information to properly convert from token proof to proof
+        let keysets_info = match self
+            .localstore
+            .get_mint_keysets(token_data.mint_url()?)
+            .await?
+        {
+            Some(keysets_info) => keysets_info,
+            // Hit the keysets endpoint if we don't have the keysets for this Mint
+            None => wallet.get_mint_keysets().await?,
+        };
+        let proofs = token_data.proofs(&keysets_info)?;
+
+        let mut amount_received = Amount::ZERO;
+
+        let mut mint_errors = None;
+
         match wallet
             .receive_proofs(proofs, opts, token_data.memo().clone())
             .await
@@ -356,7 +366,7 @@ impl MultiMintWallet {
             .get(wallet_key)
             .ok_or(Error::UnknownWallet(wallet_key.clone()))?;
 
-        wallet.verify_token_p2pk(token, conditions)
+        wallet.verify_token_p2pk(token, conditions).await
     }
 
     /// Verifys all proofs in token have valid dleq proof

+ 2 - 1
crates/cdk/src/wallet/receive.rs

@@ -219,7 +219,8 @@ impl Wallet {
 
         ensure_cdk!(unit == self.unit, Error::UnsupportedUnit);
 
-        let proofs = token.proofs();
+        let keysets_info = self.load_mint_keysets().await?;
+        let proofs = token.proofs(&keysets_info)?;
 
         if let Token::TokenV3(token) = &token {
             ensure_cdk!(!token.is_multi_mint(), Error::MultiMintTokenNotSupported);