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