cdk_common/
common.rs

1//! Types
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::Error;
6use crate::mint_url::MintUrl;
7use crate::nuts::nut00::ProofsMethods;
8use crate::nuts::{
9    CurrencyUnit, MeltQuoteState, PaymentMethod, Proof, Proofs, PublicKey, SpendingConditions,
10    State,
11};
12use crate::Amount;
13
14/// Melt response with proofs
15#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
16pub struct Melted {
17    /// State of quote
18    pub state: MeltQuoteState,
19    /// Preimage of melt payment
20    pub preimage: Option<String>,
21    /// Melt change
22    pub change: Option<Proofs>,
23    /// Melt amount
24    pub amount: Amount,
25    /// Fee paid
26    pub fee_paid: Amount,
27}
28
29impl Melted {
30    /// Create new [`Melted`]
31    pub fn from_proofs(
32        state: MeltQuoteState,
33        preimage: Option<String>,
34        quote_amount: Amount,
35        proofs: Proofs,
36        change_proofs: Option<Proofs>,
37    ) -> Result<Self, Error> {
38        let proofs_amount = proofs.total_amount()?;
39        let change_amount = match &change_proofs {
40            Some(change_proofs) => change_proofs.total_amount()?,
41            None => Amount::ZERO,
42        };
43
44        tracing::info!(
45            "Proofs amount: {} Amount: {} Change: {}",
46            proofs_amount,
47            quote_amount,
48            change_amount
49        );
50
51        let fee_paid = proofs_amount
52            .checked_sub(quote_amount + change_amount)
53            .ok_or(Error::AmountOverflow)?;
54
55        Ok(Self {
56            state,
57            preimage,
58            change: change_proofs,
59            amount: quote_amount,
60            fee_paid,
61        })
62    }
63
64    /// Total amount melted
65    pub fn total_amount(&self) -> Amount {
66        self.amount + self.fee_paid
67    }
68}
69
70/// Prooinfo
71#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
72pub struct ProofInfo {
73    /// Proof
74    pub proof: Proof,
75    /// y
76    pub y: PublicKey,
77    /// Mint Url
78    pub mint_url: MintUrl,
79    /// Proof State
80    pub state: State,
81    /// Proof Spending Conditions
82    pub spending_condition: Option<SpendingConditions>,
83    /// Unit
84    pub unit: CurrencyUnit,
85}
86
87impl ProofInfo {
88    /// Create new [`ProofInfo`]
89    pub fn new(
90        proof: Proof,
91        mint_url: MintUrl,
92        state: State,
93        unit: CurrencyUnit,
94    ) -> Result<Self, Error> {
95        let y = proof.y()?;
96
97        let spending_condition: Option<SpendingConditions> = (&proof.secret).try_into().ok();
98
99        Ok(Self {
100            proof,
101            y,
102            mint_url,
103            state,
104            spending_condition,
105            unit,
106        })
107    }
108
109    /// Check if [`Proof`] matches conditions
110    pub fn matches_conditions(
111        &self,
112        mint_url: &Option<MintUrl>,
113        unit: &Option<CurrencyUnit>,
114        state: &Option<Vec<State>>,
115        spending_conditions: &Option<Vec<SpendingConditions>>,
116    ) -> bool {
117        if let Some(mint_url) = mint_url {
118            if mint_url.ne(&self.mint_url) {
119                return false;
120            }
121        }
122
123        if let Some(unit) = unit {
124            if unit.ne(&self.unit) {
125                return false;
126            }
127        }
128
129        if let Some(state) = state {
130            if !state.contains(&self.state) {
131                return false;
132            }
133        }
134
135        if let Some(spending_conditions) = spending_conditions {
136            match &self.spending_condition {
137                None => {
138                    if !spending_conditions.is_empty() {
139                        return false;
140                    }
141                }
142                Some(s) => {
143                    if !spending_conditions.contains(s) {
144                        return false;
145                    }
146                }
147            }
148        }
149
150        true
151    }
152}
153
154/// Key used in hashmap of ln backends to identify what unit and payment method
155/// it is for
156#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
157pub struct PaymentProcessorKey {
158    /// Unit of Payment backend
159    pub unit: CurrencyUnit,
160    /// Method of payment backend
161    pub method: PaymentMethod,
162}
163
164impl PaymentProcessorKey {
165    /// Create new [`PaymentProcessorKey`]
166    pub fn new(unit: CurrencyUnit, method: PaymentMethod) -> Self {
167        Self { unit, method }
168    }
169}
170
171/// Seconds quotes are valid
172#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
173pub struct QuoteTTL {
174    /// Seconds mint quote is valid
175    pub mint_ttl: u64,
176    /// Seconds melt quote is valid
177    pub melt_ttl: u64,
178}
179
180impl QuoteTTL {
181    /// Create new [`QuoteTTL`]
182    pub fn new(mint_ttl: u64, melt_ttl: u64) -> QuoteTTL {
183        Self { mint_ttl, melt_ttl }
184    }
185}
186
187impl Default for QuoteTTL {
188    fn default() -> Self {
189        Self {
190            mint_ttl: 60 * 60, // 1 hour
191            melt_ttl: 60,      // 1 minute
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use std::str::FromStr;
199
200    use cashu::SecretKey;
201
202    use super::{Melted, ProofInfo};
203    use crate::mint_url::MintUrl;
204    use crate::nuts::{CurrencyUnit, Id, Proof, PublicKey, SpendingConditions, State};
205    use crate::secret::Secret;
206    use crate::Amount;
207
208    #[test]
209    fn test_melted() {
210        let keyset_id = Id::from_str("00deadbeef123456").unwrap();
211        let proof = Proof::new(
212            Amount::from(64),
213            keyset_id,
214            Secret::generate(),
215            PublicKey::from_hex(
216                "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
217            )
218            .unwrap(),
219        );
220        let melted = Melted::from_proofs(
221            super::MeltQuoteState::Paid,
222            Some("preimage".to_string()),
223            Amount::from(64),
224            vec![proof.clone()],
225            None,
226        )
227        .unwrap();
228        assert_eq!(melted.amount, Amount::from(64));
229        assert_eq!(melted.fee_paid, Amount::ZERO);
230        assert_eq!(melted.total_amount(), Amount::from(64));
231    }
232
233    #[test]
234    fn test_melted_with_change() {
235        let keyset_id = Id::from_str("00deadbeef123456").unwrap();
236        let proof = Proof::new(
237            Amount::from(64),
238            keyset_id,
239            Secret::generate(),
240            PublicKey::from_hex(
241                "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
242            )
243            .unwrap(),
244        );
245        let change_proof = Proof::new(
246            Amount::from(32),
247            keyset_id,
248            Secret::generate(),
249            PublicKey::from_hex(
250                "03deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
251            )
252            .unwrap(),
253        );
254        let melted = Melted::from_proofs(
255            super::MeltQuoteState::Paid,
256            Some("preimage".to_string()),
257            Amount::from(31),
258            vec![proof.clone()],
259            Some(vec![change_proof.clone()]),
260        )
261        .unwrap();
262        assert_eq!(melted.amount, Amount::from(31));
263        assert_eq!(melted.fee_paid, Amount::from(1));
264        assert_eq!(melted.total_amount(), Amount::from(32));
265    }
266
267    #[test]
268    fn test_matches_conditions() {
269        let keyset_id = Id::from_str("00deadbeef123456").unwrap();
270        let proof = Proof::new(
271            Amount::from(64),
272            keyset_id,
273            Secret::new("test_secret"),
274            PublicKey::from_hex(
275                "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
276            )
277            .unwrap(),
278        );
279
280        let mint_url = MintUrl::from_str("https://example.com").unwrap();
281        let proof_info =
282            ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
283
284        // Test matching mint_url
285        assert!(proof_info.matches_conditions(&Some(mint_url.clone()), &None, &None, &None));
286        assert!(!proof_info.matches_conditions(
287            &Some(MintUrl::from_str("https://different.com").unwrap()),
288            &None,
289            &None,
290            &None
291        ));
292
293        // Test matching unit
294        assert!(proof_info.matches_conditions(&None, &Some(CurrencyUnit::Sat), &None, &None));
295        assert!(!proof_info.matches_conditions(&None, &Some(CurrencyUnit::Msat), &None, &None));
296
297        // Test matching state
298        assert!(proof_info.matches_conditions(&None, &None, &Some(vec![State::Unspent]), &None));
299        assert!(proof_info.matches_conditions(
300            &None,
301            &None,
302            &Some(vec![State::Unspent, State::Spent]),
303            &None
304        ));
305        assert!(!proof_info.matches_conditions(&None, &None, &Some(vec![State::Spent]), &None));
306
307        // Test with no conditions (should match)
308        assert!(proof_info.matches_conditions(&None, &None, &None, &None));
309
310        // Test with multiple conditions
311        assert!(proof_info.matches_conditions(
312            &Some(mint_url),
313            &Some(CurrencyUnit::Sat),
314            &Some(vec![State::Unspent]),
315            &None
316        ));
317    }
318
319    #[test]
320    fn test_matches_conditions_with_spending_conditions() {
321        // This test would need to be expanded with actual SpendingConditions
322        // implementation, but we can test the basic case where no spending
323        // conditions are present
324
325        let keyset_id = Id::from_str("00deadbeef123456").unwrap();
326        let proof = Proof::new(
327            Amount::from(64),
328            keyset_id,
329            Secret::new("test_secret"),
330            PublicKey::from_hex(
331                "02deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
332            )
333            .unwrap(),
334        );
335
336        let mint_url = MintUrl::from_str("https://example.com").unwrap();
337        let proof_info =
338            ProofInfo::new(proof, mint_url, State::Unspent, CurrencyUnit::Sat).unwrap();
339
340        // Test with empty spending conditions (should match when proof has none)
341        assert!(proof_info.matches_conditions(&None, &None, &None, &Some(vec![])));
342
343        // Test with non-empty spending conditions (should not match when proof has none)
344        let dummy_condition = SpendingConditions::P2PKConditions {
345            data: SecretKey::generate().public_key(),
346            conditions: None,
347        };
348        assert!(!proof_info.matches_conditions(&None, &None, &None, &Some(vec![dummy_condition])));
349    }
350}
351
352/// Mint Fee Reserve
353#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
354pub struct FeeReserve {
355    /// Absolute expected min fee
356    pub min_fee_reserve: Amount,
357    /// Percentage expected fee
358    pub percent_fee_reserve: f32,
359}