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