Skip to main content

cdk_common/
common.rs

1//! Types
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::Error;
6use crate::nuts::nut00::ProofsMethods;
7use crate::nuts::{CurrencyUnit, MeltQuoteState, PaymentMethod, Proofs};
8// Re-export ProofInfo from wallet module for backwards compatibility
9#[cfg(feature = "wallet")]
10pub use crate::wallet::ProofInfo;
11use crate::Amount;
12
13/// Result of a finalized melt operation
14#[derive(Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
15pub struct FinalizedMelt {
16    /// Quote ID
17    quote_id: String,
18    /// State of quote
19    state: MeltQuoteState,
20    /// Payment proof (e.g., Lightning preimage)
21    payment_proof: Option<String>,
22    /// Melt change
23    change: Option<Proofs>,
24    /// Melt amount
25    amount: Amount,
26    /// Fee paid
27    fee_paid: Amount,
28}
29
30impl FinalizedMelt {
31    /// Create new [`FinalizedMelt`]
32    pub fn new(
33        quote_id: String,
34        state: MeltQuoteState,
35        payment_proof: Option<String>,
36        amount: Amount,
37        fee_paid: Amount,
38        change: Option<Proofs>,
39    ) -> Self {
40        Self {
41            quote_id,
42            state,
43            payment_proof,
44            change,
45            amount,
46            fee_paid,
47        }
48    }
49
50    /// Create new [`FinalizedMelt`] calculating fee from proofs
51    pub fn from_proofs(
52        quote_id: String,
53        state: MeltQuoteState,
54        payment_proof: Option<String>,
55        quote_amount: Amount,
56        proofs: Proofs,
57        change_proofs: Option<Proofs>,
58    ) -> Result<Self, Error> {
59        let proofs_amount = proofs.total_amount()?;
60        let change_amount = match &change_proofs {
61            Some(change_proofs) => change_proofs.total_amount()?,
62            None => Amount::ZERO,
63        };
64
65        tracing::info!(
66            "Proofs amount: {} Amount: {} Change: {}",
67            proofs_amount,
68            quote_amount,
69            change_amount
70        );
71
72        let fee_paid = proofs_amount
73            .checked_sub(
74                quote_amount
75                    .checked_add(change_amount)
76                    .ok_or(Error::AmountOverflow)?,
77            )
78            .ok_or(Error::AmountOverflow)?;
79
80        Ok(Self {
81            quote_id,
82            state,
83            payment_proof,
84            change: change_proofs,
85            amount: quote_amount,
86            fee_paid,
87        })
88    }
89
90    /// Get the quote ID
91    #[inline]
92    pub fn quote_id(&self) -> &str {
93        &self.quote_id
94    }
95
96    /// Get the state of the melt
97    #[inline]
98    pub fn state(&self) -> MeltQuoteState {
99        self.state
100    }
101
102    /// Get the payment proof (e.g., Lightning preimage)
103    #[inline]
104    pub fn payment_proof(&self) -> Option<&str> {
105        self.payment_proof.as_deref()
106    }
107
108    /// Get the change proofs
109    #[inline]
110    pub fn change(&self) -> Option<&Proofs> {
111        self.change.as_ref()
112    }
113
114    /// Consume self and return the change proofs
115    #[inline]
116    pub fn into_change(self) -> Option<Proofs> {
117        self.change
118    }
119
120    /// Get the amount melted
121    #[inline]
122    pub fn amount(&self) -> Amount {
123        self.amount
124    }
125
126    /// Get the fee paid
127    #[inline]
128    pub fn fee_paid(&self) -> Amount {
129        self.fee_paid
130    }
131
132    /// Total amount melted (amount + fee)
133    ///
134    /// # Panics
135    ///
136    /// Panics if the sum of `amount` and `fee_paid` overflows. This should not
137    /// happen as the fee is validated when calculated.
138    #[inline]
139    pub fn total_amount(&self) -> Amount {
140        self.amount
141            .checked_add(self.fee_paid)
142            .expect("We check when calc fee paid")
143    }
144}
145
146impl std::fmt::Debug for FinalizedMelt {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        f.debug_struct("FinalizedMelt")
149            .field("quote_id", &self.quote_id)
150            .field("state", &self.state)
151            .field("amount", &self.amount)
152            .field("fee_paid", &self.fee_paid)
153            .finish()
154    }
155}
156
157/// Key used in hashmap of ln backends to identify what unit and payment method
158/// it is for
159#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
160pub struct PaymentProcessorKey {
161    /// Unit of Payment backend
162    pub unit: CurrencyUnit,
163    /// Method of payment backend
164    pub method: PaymentMethod,
165}
166
167impl PaymentProcessorKey {
168    /// Create new [`PaymentProcessorKey`]
169    pub fn new(unit: CurrencyUnit, method: PaymentMethod) -> Self {
170        Self { unit, method }
171    }
172}
173
174/// Seconds quotes are valid
175#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
176pub struct QuoteTTL {
177    /// Seconds mint quote is valid
178    pub mint_ttl: u64,
179    /// Seconds melt quote is valid
180    pub melt_ttl: u64,
181}
182
183impl QuoteTTL {
184    /// Create new [`QuoteTTL`]
185    pub fn new(mint_ttl: u64, melt_ttl: u64) -> QuoteTTL {
186        Self { mint_ttl, melt_ttl }
187    }
188}
189
190impl Default for QuoteTTL {
191    fn default() -> Self {
192        Self {
193            mint_ttl: 60 * 60, // 1 hour
194            melt_ttl: 60,      // 1 minute
195        }
196    }
197}
198
199/// Mint Fee Reserve
200#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
201pub struct FeeReserve {
202    /// Absolute expected min fee
203    pub min_fee_reserve: Amount,
204    /// Percentage expected fee
205    pub percent_fee_reserve: f32,
206}
207
208/// CDK Version
209#[derive(Debug, Clone, Hash, PartialEq, Eq)]
210pub struct IssuerVersion {
211    /// Implementation name (e.g., "cdk", "nutshell")
212    pub implementation: String,
213    /// Major version
214    pub major: u16,
215    /// Minor version
216    pub minor: u16,
217    /// Patch version
218    pub patch: u16,
219}
220
221impl IssuerVersion {
222    /// Create new [`IssuerVersion`]
223    pub fn new(implementation: String, major: u16, minor: u16, patch: u16) -> Self {
224        Self {
225            implementation,
226            major,
227            minor,
228            patch,
229        }
230    }
231}
232
233impl std::fmt::Display for IssuerVersion {
234    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235        write!(
236            f,
237            "{}/{}.{}.{}",
238            self.implementation, self.major, self.minor, self.patch
239        )
240    }
241}
242
243impl PartialOrd for IssuerVersion {
244    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
245        if self.implementation != other.implementation {
246            return None;
247        }
248
249        match self.major.cmp(&other.major) {
250            std::cmp::Ordering::Equal => match self.minor.cmp(&other.minor) {
251                std::cmp::Ordering::Equal => Some(self.patch.cmp(&other.patch)),
252                other => Some(other),
253            },
254            other => Some(other),
255        }
256    }
257}
258
259impl std::str::FromStr for IssuerVersion {
260    type Err = Error;
261
262    fn from_str(s: &str) -> Result<Self, Self::Err> {
263        let (implementation, version_str) = s
264            .split_once('/')
265            .ok_or(Error::Custom(format!("Invalid version string: {}", s)))?;
266        let implementation = implementation.to_string();
267
268        let parts: Vec<&str> = version_str.splitn(3, '.').collect();
269        if parts.len() != 3 {
270            return Err(Error::Custom(format!("Invalid version string: {}", s)));
271        }
272
273        let major = parts[0]
274            .parse()
275            .map_err(|_| Error::Custom(format!("Invalid major version: {}", parts[0])))?;
276        let minor = parts[1]
277            .parse()
278            .map_err(|_| Error::Custom(format!("Invalid minor version: {}", parts[1])))?;
279
280        // Handle patch version with optional suffixes like -rc1
281        let patch_str = parts[2];
282        let patch_end = patch_str
283            .find(|c: char| !c.is_numeric())
284            .unwrap_or(patch_str.len());
285        let patch = patch_str[..patch_end]
286            .parse()
287            .map_err(|_| Error::Custom(format!("Invalid patch version: {}", parts[2])))?;
288
289        Ok(Self {
290            implementation,
291            major,
292            minor,
293            patch,
294        })
295    }
296}
297
298impl Serialize for IssuerVersion {
299    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
300    where
301        S: serde::Serializer,
302    {
303        serializer.serialize_str(&self.to_string())
304    }
305}
306
307impl<'de> Deserialize<'de> for IssuerVersion {
308    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
309    where
310        D: serde::Deserializer<'de>,
311    {
312        let s = String::deserialize(deserializer)?;
313        std::str::FromStr::from_str(&s).map_err(serde::de::Error::custom)
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use std::str::FromStr;
320
321    use super::FinalizedMelt;
322    use crate::nuts::{Id, Proof, PublicKey};
323    use crate::secret::Secret;
324    use crate::Amount;
325
326    #[test]
327    fn test_finalized_melt() {
328        let keyset_id = Id::from_str("00deadbeef123456").unwrap();
329        let proof = Proof::new(
330            Amount::from(64),
331            keyset_id,
332            Secret::generate(),
333            PublicKey::from_hex(
334                "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
335            )
336            .unwrap(),
337        );
338        let finalized = FinalizedMelt::from_proofs(
339            "test_quote_id".to_string(),
340            super::MeltQuoteState::Paid,
341            Some("preimage".to_string()),
342            Amount::from(64),
343            vec![proof.clone()],
344            None,
345        )
346        .unwrap();
347        assert_eq!(finalized.quote_id(), "test_quote_id");
348        assert_eq!(finalized.amount(), Amount::from(64));
349        assert_eq!(finalized.fee_paid(), Amount::ZERO);
350        assert_eq!(finalized.total_amount(), Amount::from(64));
351    }
352
353    #[test]
354    fn test_finalized_melt_with_change() {
355        let keyset_id = Id::from_str("00deadbeef123456").unwrap();
356        let proof = Proof::new(
357            Amount::from(64),
358            keyset_id,
359            Secret::generate(),
360            PublicKey::from_hex(
361                "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
362            )
363            .unwrap(),
364        );
365        let change_proof = Proof::new(
366            Amount::from(32),
367            keyset_id,
368            Secret::generate(),
369            PublicKey::from_hex(
370                "03deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
371            )
372            .unwrap(),
373        );
374        let finalized = FinalizedMelt::from_proofs(
375            "test_quote_id".to_string(),
376            super::MeltQuoteState::Paid,
377            Some("preimage".to_string()),
378            Amount::from(31),
379            vec![proof.clone()],
380            Some(vec![change_proof.clone()]),
381        )
382        .unwrap();
383        assert_eq!(finalized.quote_id(), "test_quote_id");
384        assert_eq!(finalized.amount(), Amount::from(31));
385        assert_eq!(finalized.fee_paid(), Amount::from(1));
386        assert_eq!(finalized.total_amount(), Amount::from(32));
387    }
388
389    use super::IssuerVersion;
390
391    #[test]
392    fn test_version_parsing() {
393        // Test explicit cdk format
394        let v = IssuerVersion::from_str("cdk/1.2.3").unwrap();
395        assert_eq!(v.implementation, "cdk");
396        assert_eq!(v.major, 1);
397        assert_eq!(v.minor, 2);
398        assert_eq!(v.patch, 3);
399        assert_eq!(v.to_string(), "cdk/1.2.3");
400
401        // Test nutshell format
402        let v = IssuerVersion::from_str("nutshell/0.16.0").unwrap();
403        assert_eq!(v.implementation, "nutshell");
404        assert_eq!(v.major, 0);
405        assert_eq!(v.minor, 16);
406        assert_eq!(v.patch, 0);
407        assert_eq!(v.to_string(), "nutshell/0.16.0");
408    }
409
410    #[test]
411    fn test_version_ordering() {
412        let v1 = IssuerVersion::from_str("cdk/0.1.0").unwrap();
413        let v2 = IssuerVersion::from_str("cdk/0.1.1").unwrap();
414        let v3 = IssuerVersion::from_str("cdk/0.2.0").unwrap();
415        let v4 = IssuerVersion::from_str("cdk/1.0.0").unwrap();
416
417        assert!(v1 < v2);
418        assert!(v2 < v3);
419        assert!(v3 < v4);
420        assert!(v1 < v4);
421
422        // Test mixed implementations
423        let v_nutshell = IssuerVersion::from_str("nutshell/0.1.0").unwrap();
424        assert_eq!(v1.partial_cmp(&v_nutshell), None);
425        assert!(!(v1 < v_nutshell));
426        assert!(!(v1 > v_nutshell));
427        assert!(!(v1 == v_nutshell));
428    }
429
430    #[test]
431    fn test_version_serialization() {
432        let v = IssuerVersion::from_str("cdk/0.14.2").unwrap();
433        let json = serde_json::to_string(&v).unwrap();
434        assert_eq!(json, "\"cdk/0.14.2\"");
435
436        let v_deserialized: IssuerVersion = serde_json::from_str(&json).unwrap();
437        assert_eq!(v, v_deserialized);
438    }
439
440    #[test]
441    fn test_cdk_version_parsing_with_suffix() {
442        let version_str = "cdk/0.15.0-rc1";
443        let version = IssuerVersion::from_str(version_str).unwrap();
444        assert_eq!(version.implementation, "cdk");
445        assert_eq!(version.major, 0);
446        assert_eq!(version.minor, 15);
447        assert_eq!(version.patch, 0);
448    }
449
450    #[test]
451    fn test_cdk_version_parsing_standard() {
452        let version_str = "cdk/0.15.0";
453        let version = IssuerVersion::from_str(version_str).unwrap();
454        assert_eq!(version.implementation, "cdk");
455        assert_eq!(version.major, 0);
456        assert_eq!(version.minor, 15);
457        assert_eq!(version.patch, 0);
458    }
459
460    #[test]
461    fn test_cdk_version_parsing_complex_suffix() {
462        let version_str = "cdk/0.15.0-beta.1+build123";
463        let version = IssuerVersion::from_str(version_str).unwrap();
464        assert_eq!(version.implementation, "cdk");
465        assert_eq!(version.major, 0);
466        assert_eq!(version.minor, 15);
467        assert_eq!(version.patch, 0);
468    }
469
470    #[test]
471    fn test_cdk_version_parsing_invalid() {
472        // Missing prefix
473        let version_str = "0.15.0";
474        assert!(IssuerVersion::from_str(version_str).is_err());
475
476        // Invalid version format
477        let version_str = "cdk/0.15";
478        assert!(IssuerVersion::from_str(version_str).is_err());
479
480        let version_str = "cdk/0.15.a";
481        assert!(IssuerVersion::from_str(version_str).is_err());
482    }
483
484    #[test]
485    fn test_cdk_version_parsing_with_implementation() {
486        let version_str = "nutshell/0.16.2";
487        let version = IssuerVersion::from_str(version_str).unwrap();
488        assert_eq!(version.implementation, "nutshell");
489        assert_eq!(version.major, 0);
490        assert_eq!(version.minor, 16);
491        assert_eq!(version.patch, 2);
492    }
493
494    #[test]
495    fn test_cdk_version_comparison_different_implementations() {
496        let v1 = IssuerVersion::from_str("cdk/0.15.0").unwrap();
497        let v2 = IssuerVersion::from_str("nutshell/0.15.0").unwrap();
498
499        assert_eq!(v1.partial_cmp(&v2), None);
500    }
501}