1use bitcoin::bip32::DerivationPath;
4use cashu::quote_id::QuoteId;
5use cashu::util::unix_time;
6use cashu::{
7 Bolt11Invoice, MeltOptions, MeltQuoteBolt11Response, MintQuoteBolt11Response,
8 MintQuoteBolt12Response, PaymentMethod,
9};
10use lightning::offers::offer::Offer;
11use serde::{Deserialize, Serialize};
12use tracing::instrument;
13use uuid::Uuid;
14
15use crate::nuts::{MeltQuoteState, MintQuoteState};
16use crate::payment::PaymentIdentifier;
17use crate::{Amount, CurrencyUnit, Id, KeySetInfo, PublicKey};
18
19#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
21pub struct MintQuote {
22 pub id: QuoteId,
24 pub amount: Option<Amount>,
26 pub unit: CurrencyUnit,
28 pub request: String,
30 pub expiry: u64,
32 pub request_lookup_id: PaymentIdentifier,
34 pub pubkey: Option<PublicKey>,
36 #[serde(default)]
38 pub created_time: u64,
39 #[serde(default)]
41 amount_paid: Amount,
42 #[serde(default)]
44 amount_issued: Amount,
45 #[serde(default)]
47 pub payments: Vec<IncomingPayment>,
48 #[serde(default)]
50 pub payment_method: PaymentMethod,
51 #[serde(default)]
53 pub issuance: Vec<Issuance>,
54}
55
56impl MintQuote {
57 #[allow(clippy::too_many_arguments)]
59 pub fn new(
60 id: Option<QuoteId>,
61 request: String,
62 unit: CurrencyUnit,
63 amount: Option<Amount>,
64 expiry: u64,
65 request_lookup_id: PaymentIdentifier,
66 pubkey: Option<PublicKey>,
67 amount_paid: Amount,
68 amount_issued: Amount,
69 payment_method: PaymentMethod,
70 created_time: u64,
71 payments: Vec<IncomingPayment>,
72 issuance: Vec<Issuance>,
73 ) -> Self {
74 let id = id.unwrap_or_else(QuoteId::new_uuid);
75
76 Self {
77 id,
78 amount,
79 unit,
80 request,
81 expiry,
82 request_lookup_id,
83 pubkey,
84 created_time,
85 amount_paid,
86 amount_issued,
87 payment_method,
88 payments,
89 issuance,
90 }
91 }
92
93 #[instrument(skip(self))]
95 pub fn increment_amount_paid(
96 &mut self,
97 additional_amount: Amount,
98 ) -> Result<Amount, crate::Error> {
99 self.amount_paid = self
100 .amount_paid
101 .checked_add(additional_amount)
102 .ok_or(crate::Error::AmountOverflow)?;
103 Ok(self.amount_paid)
104 }
105
106 #[instrument(skip(self))]
108 pub fn amount_paid(&self) -> Amount {
109 self.amount_paid
110 }
111
112 #[instrument(skip(self))]
114 pub fn increment_amount_issued(
115 &mut self,
116 additional_amount: Amount,
117 ) -> Result<Amount, crate::Error> {
118 self.amount_issued = self
119 .amount_issued
120 .checked_add(additional_amount)
121 .ok_or(crate::Error::AmountOverflow)?;
122 Ok(self.amount_issued)
123 }
124
125 #[instrument(skip(self))]
127 pub fn amount_issued(&self) -> Amount {
128 self.amount_issued
129 }
130
131 #[instrument(skip(self))]
133 pub fn state(&self) -> MintQuoteState {
134 self.compute_quote_state()
135 }
136
137 pub fn payment_ids(&self) -> Vec<&String> {
139 self.payments.iter().map(|a| &a.payment_id).collect()
140 }
141
142 pub fn amount_mintable(&self) -> Amount {
149 self.amount_paid - self.amount_issued
150 }
151
152 #[instrument(skip(self))]
156 pub fn add_payment(
157 &mut self,
158 amount: Amount,
159 payment_id: String,
160 time: u64,
161 ) -> Result<(), crate::Error> {
162 let payment_ids = self.payment_ids();
163 if payment_ids.contains(&&payment_id) {
164 return Err(crate::Error::DuplicatePaymentId);
165 }
166
167 let payment = IncomingPayment::new(amount, payment_id, time);
168
169 self.payments.push(payment);
170 Ok(())
171 }
172
173 #[instrument(skip(self))]
175 fn compute_quote_state(&self) -> MintQuoteState {
176 if self.amount_paid == Amount::ZERO && self.amount_issued == Amount::ZERO {
177 return MintQuoteState::Unpaid;
178 }
179
180 match self.amount_paid.cmp(&self.amount_issued) {
181 std::cmp::Ordering::Less => {
182 tracing::error!("We should not have issued more then has been paid");
185 MintQuoteState::Issued
186 }
187 std::cmp::Ordering::Equal => {
188 MintQuoteState::Issued
192 }
193 std::cmp::Ordering::Greater => {
194 MintQuoteState::Paid
197 }
198 }
199 }
200}
201
202#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
204pub struct IncomingPayment {
205 pub amount: Amount,
207 pub time: u64,
209 pub payment_id: String,
211}
212
213impl IncomingPayment {
214 pub fn new(amount: Amount, payment_id: String, time: u64) -> Self {
216 Self {
217 payment_id,
218 time,
219 amount,
220 }
221 }
222}
223
224#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
226pub struct Issuance {
227 pub amount: Amount,
229 pub time: u64,
231}
232
233impl Issuance {
234 pub fn new(amount: Amount, time: u64) -> Self {
236 Self { amount, time }
237 }
238}
239
240#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
242pub struct MeltQuote {
243 pub id: QuoteId,
245 pub unit: CurrencyUnit,
247 pub amount: Amount,
249 pub request: MeltPaymentRequest,
251 pub fee_reserve: Amount,
253 pub state: MeltQuoteState,
255 pub expiry: u64,
257 pub payment_preimage: Option<String>,
259 pub request_lookup_id: Option<PaymentIdentifier>,
261 pub options: Option<MeltOptions>,
265 #[serde(default)]
267 pub created_time: u64,
268 pub paid_time: Option<u64>,
270 #[serde(default)]
272 pub payment_method: PaymentMethod,
273}
274
275impl MeltQuote {
276 #[allow(clippy::too_many_arguments)]
278 pub fn new(
279 request: MeltPaymentRequest,
280 unit: CurrencyUnit,
281 amount: Amount,
282 fee_reserve: Amount,
283 expiry: u64,
284 request_lookup_id: Option<PaymentIdentifier>,
285 options: Option<MeltOptions>,
286 payment_method: PaymentMethod,
287 ) -> Self {
288 let id = Uuid::new_v4();
289
290 Self {
291 id: QuoteId::UUID(id),
292 amount,
293 unit,
294 request,
295 fee_reserve,
296 state: MeltQuoteState::Unpaid,
297 expiry,
298 payment_preimage: None,
299 request_lookup_id,
300 options,
301 created_time: unix_time(),
302 paid_time: None,
303 payment_method,
304 }
305 }
306}
307
308#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
310pub struct MintKeySetInfo {
311 pub id: Id,
313 pub unit: CurrencyUnit,
315 pub active: bool,
318 pub valid_from: u64,
320 pub derivation_path: DerivationPath,
322 pub derivation_path_index: Option<u32>,
324 pub max_order: u8,
326 pub amounts: Vec<u64>,
328 #[serde(default = "default_fee")]
330 pub input_fee_ppk: u64,
331 pub final_expiry: Option<u64>,
333}
334
335pub fn default_fee() -> u64 {
337 0
338}
339
340impl From<MintKeySetInfo> for KeySetInfo {
341 fn from(keyset_info: MintKeySetInfo) -> Self {
342 Self {
343 id: keyset_info.id,
344 unit: keyset_info.unit,
345 active: keyset_info.active,
346 input_fee_ppk: keyset_info.input_fee_ppk,
347 final_expiry: keyset_info.final_expiry,
348 }
349 }
350}
351
352impl From<MintQuote> for MintQuoteBolt11Response<QuoteId> {
353 fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt11Response<QuoteId> {
354 MintQuoteBolt11Response {
355 quote: mint_quote.id.clone(),
356 state: mint_quote.state(),
357 request: mint_quote.request,
358 expiry: Some(mint_quote.expiry),
359 pubkey: mint_quote.pubkey,
360 amount: mint_quote.amount,
361 unit: Some(mint_quote.unit.clone()),
362 }
363 }
364}
365
366impl From<MintQuote> for MintQuoteBolt11Response<String> {
367 fn from(quote: MintQuote) -> Self {
368 let quote: MintQuoteBolt11Response<QuoteId> = quote.into();
369
370 quote.into()
371 }
372}
373
374impl TryFrom<crate::mint::MintQuote> for MintQuoteBolt12Response<QuoteId> {
375 type Error = crate::Error;
376
377 fn try_from(mint_quote: crate::mint::MintQuote) -> Result<Self, Self::Error> {
378 Ok(MintQuoteBolt12Response {
379 quote: mint_quote.id.clone(),
380 request: mint_quote.request,
381 expiry: Some(mint_quote.expiry),
382 amount_paid: mint_quote.amount_paid,
383 amount_issued: mint_quote.amount_issued,
384 pubkey: mint_quote.pubkey.ok_or(crate::Error::PubkeyRequired)?,
385 amount: mint_quote.amount,
386 unit: mint_quote.unit,
387 })
388 }
389}
390
391impl TryFrom<MintQuote> for MintQuoteBolt12Response<String> {
392 type Error = crate::Error;
393
394 fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
395 let quote: MintQuoteBolt12Response<QuoteId> = quote.try_into()?;
396
397 Ok(quote.into())
398 }
399}
400
401impl From<&MeltQuote> for MeltQuoteBolt11Response<QuoteId> {
402 fn from(melt_quote: &MeltQuote) -> MeltQuoteBolt11Response<QuoteId> {
403 MeltQuoteBolt11Response {
404 quote: melt_quote.id.clone(),
405 payment_preimage: None,
406 change: None,
407 state: melt_quote.state,
408 paid: Some(melt_quote.state == MeltQuoteState::Paid),
409 expiry: melt_quote.expiry,
410 amount: melt_quote.amount,
411 fee_reserve: melt_quote.fee_reserve,
412 request: None,
413 unit: Some(melt_quote.unit.clone()),
414 }
415 }
416}
417
418impl From<MeltQuote> for MeltQuoteBolt11Response<QuoteId> {
419 fn from(melt_quote: MeltQuote) -> MeltQuoteBolt11Response<QuoteId> {
420 let paid = melt_quote.state == MeltQuoteState::Paid;
421 MeltQuoteBolt11Response {
422 quote: melt_quote.id.clone(),
423 amount: melt_quote.amount,
424 fee_reserve: melt_quote.fee_reserve,
425 paid: Some(paid),
426 state: melt_quote.state,
427 expiry: melt_quote.expiry,
428 payment_preimage: melt_quote.payment_preimage,
429 change: None,
430 request: Some(melt_quote.request.to_string()),
431 unit: Some(melt_quote.unit.clone()),
432 }
433 }
434}
435
436#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
438pub enum MeltPaymentRequest {
439 Bolt11 {
441 bolt11: Bolt11Invoice,
443 },
444 Bolt12 {
446 #[serde(with = "offer_serde")]
448 offer: Box<Offer>,
449 },
450}
451
452impl std::fmt::Display for MeltPaymentRequest {
453 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
454 match self {
455 MeltPaymentRequest::Bolt11 { bolt11 } => write!(f, "{bolt11}"),
456 MeltPaymentRequest::Bolt12 { offer } => write!(f, "{offer}"),
457 }
458 }
459}
460
461mod offer_serde {
462 use std::str::FromStr;
463
464 use serde::{self, Deserialize, Deserializer, Serializer};
465
466 use super::Offer;
467
468 pub fn serialize<S>(offer: &Offer, serializer: S) -> Result<S::Ok, S::Error>
469 where
470 S: Serializer,
471 {
472 let s = offer.to_string();
473 serializer.serialize_str(&s)
474 }
475
476 pub fn deserialize<'de, D>(deserializer: D) -> Result<Box<Offer>, D::Error>
477 where
478 D: Deserializer<'de>,
479 {
480 let s = String::deserialize(deserializer)?;
481 Ok(Box::new(Offer::from_str(&s).map_err(|_| {
482 serde::de::Error::custom("Invalid Bolt12 Offer")
483 })?))
484 }
485}