1use std::fmt;
4use std::str::FromStr;
5
6use bitcoin::bip32::DerivationPath;
7use cashu::quote_id::QuoteId;
8use cashu::util::unix_time;
9use cashu::{
10 Bolt11Invoice, MeltOptions, MeltQuoteBolt11Response, MintQuoteBolt11Response,
11 MintQuoteBolt12Response, PaymentMethod,
12};
13use lightning::offers::offer::Offer;
14use serde::{Deserialize, Serialize};
15use tracing::instrument;
16use uuid::Uuid;
17
18use crate::nuts::{MeltQuoteState, MintQuoteState};
19use crate::payment::PaymentIdentifier;
20use crate::{Amount, CurrencyUnit, Error, Id, KeySetInfo, PublicKey};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum OperationKind {
26 Swap,
28 Mint,
30 Melt,
32}
33
34impl fmt::Display for OperationKind {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 OperationKind::Swap => write!(f, "swap"),
38 OperationKind::Mint => write!(f, "mint"),
39 OperationKind::Melt => write!(f, "melt"),
40 }
41 }
42}
43
44impl FromStr for OperationKind {
45 type Err = Error;
46 fn from_str(value: &str) -> Result<Self, Self::Err> {
47 let value = value.to_lowercase();
48 match value.as_str() {
49 "swap" => Ok(OperationKind::Swap),
50 "mint" => Ok(OperationKind::Mint),
51 "melt" => Ok(OperationKind::Melt),
52 _ => Err(Error::Custom(format!("Invalid operation kind: {value}"))),
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum SwapSagaState {
61 SetupComplete,
63 Signed,
65}
66
67impl fmt::Display for SwapSagaState {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 match self {
70 SwapSagaState::SetupComplete => write!(f, "setup_complete"),
71 SwapSagaState::Signed => write!(f, "signed"),
72 }
73 }
74}
75
76impl FromStr for SwapSagaState {
77 type Err = Error;
78 fn from_str(value: &str) -> Result<Self, Self::Err> {
79 let value = value.to_lowercase();
80 match value.as_str() {
81 "setup_complete" => Ok(SwapSagaState::SetupComplete),
82 "signed" => Ok(SwapSagaState::Signed),
83 _ => Err(Error::Custom(format!("Invalid swap saga state: {value}"))),
84 }
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum MeltSagaState {
92 SetupComplete,
94 PaymentAttempted,
96}
97
98impl fmt::Display for MeltSagaState {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 match self {
101 MeltSagaState::SetupComplete => write!(f, "setup_complete"),
102 MeltSagaState::PaymentAttempted => write!(f, "payment_attempted"),
103 }
104 }
105}
106
107impl FromStr for MeltSagaState {
108 type Err = Error;
109 fn from_str(value: &str) -> Result<Self, Self::Err> {
110 let value = value.to_lowercase();
111 match value.as_str() {
112 "setup_complete" => Ok(MeltSagaState::SetupComplete),
113 "payment_attempted" => Ok(MeltSagaState::PaymentAttempted),
114 _ => Err(Error::Custom(format!("Invalid melt saga state: {}", value))),
115 }
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121#[serde(tag = "type", rename_all = "snake_case")]
122pub enum SagaStateEnum {
123 Swap(SwapSagaState),
125 Melt(MeltSagaState),
127 }
130
131impl SagaStateEnum {
132 pub fn new(operation_kind: OperationKind, s: &str) -> Result<Self, Error> {
134 match operation_kind {
135 OperationKind::Swap => Ok(SagaStateEnum::Swap(SwapSagaState::from_str(s)?)),
136 OperationKind::Melt => Ok(SagaStateEnum::Melt(MeltSagaState::from_str(s)?)),
137 OperationKind::Mint => Err(Error::Custom("Mint saga not implemented yet".to_string())),
138 }
139 }
140
141 pub fn state(&self) -> &str {
143 match self {
144 SagaStateEnum::Swap(state) => match state {
145 SwapSagaState::SetupComplete => "setup_complete",
146 SwapSagaState::Signed => "signed",
147 },
148 SagaStateEnum::Melt(state) => match state {
149 MeltSagaState::SetupComplete => "setup_complete",
150 MeltSagaState::PaymentAttempted => "payment_attempted",
151 },
152 }
153 }
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158pub struct Saga {
159 pub operation_id: Uuid,
161 pub operation_kind: OperationKind,
163 pub state: SagaStateEnum,
165 pub blinded_secrets: Vec<PublicKey>,
167 pub input_ys: Vec<PublicKey>,
169 pub quote_id: Option<String>,
172 pub created_at: u64,
174 pub updated_at: u64,
176}
177
178impl Saga {
179 pub fn new_swap(
181 operation_id: Uuid,
182 state: SwapSagaState,
183 blinded_secrets: Vec<PublicKey>,
184 input_ys: Vec<PublicKey>,
185 ) -> Self {
186 let now = unix_time();
187 Self {
188 operation_id,
189 operation_kind: OperationKind::Swap,
190 state: SagaStateEnum::Swap(state),
191 blinded_secrets,
192 input_ys,
193 quote_id: None,
194 created_at: now,
195 updated_at: now,
196 }
197 }
198
199 pub fn update_swap_state(&mut self, new_state: SwapSagaState) {
201 self.state = SagaStateEnum::Swap(new_state);
202 self.updated_at = unix_time();
203 }
204
205 pub fn new_melt(
207 operation_id: Uuid,
208 state: MeltSagaState,
209 input_ys: Vec<PublicKey>,
210 blinded_secrets: Vec<PublicKey>,
211 quote_id: String,
212 ) -> Self {
213 let now = unix_time();
214 Self {
215 operation_id,
216 operation_kind: OperationKind::Melt,
217 state: SagaStateEnum::Melt(state),
218 blinded_secrets,
219 input_ys,
220 quote_id: Some(quote_id),
221 created_at: now,
222 updated_at: now,
223 }
224 }
225
226 pub fn update_melt_state(&mut self, new_state: MeltSagaState) {
228 self.state = SagaStateEnum::Melt(new_state);
229 self.updated_at = unix_time();
230 }
231}
232
233pub enum Operation {
235 Mint(Uuid),
237 Melt(Uuid),
239 Swap(Uuid),
241}
242
243impl Operation {
244 pub fn new_mint() -> Self {
246 Self::Mint(Uuid::new_v4())
247 }
248 pub fn new_melt() -> Self {
250 Self::Melt(Uuid::new_v4())
251 }
252 pub fn new_swap() -> Self {
254 Self::Swap(Uuid::new_v4())
255 }
256
257 pub fn id(&self) -> &Uuid {
259 match self {
260 Operation::Mint(id) => id,
261 Operation::Melt(id) => id,
262 Operation::Swap(id) => id,
263 }
264 }
265
266 pub fn kind(&self) -> &str {
268 match self {
269 Operation::Mint(_) => "mint",
270 Operation::Melt(_) => "melt",
271 Operation::Swap(_) => "swap",
272 }
273 }
274
275 pub fn from_kind_and_id(kind: &str, id: &str) -> Result<Self, Error> {
277 let uuid = Uuid::parse_str(id)?;
278 match kind {
279 "mint" => Ok(Self::Mint(uuid)),
280 "melt" => Ok(Self::Melt(uuid)),
281 "swap" => Ok(Self::Swap(uuid)),
282 _ => Err(Error::Custom(format!("Invalid operation kind: {kind}"))),
283 }
284 }
285}
286
287#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
289pub struct MintQuote {
290 pub id: QuoteId,
292 pub amount: Option<Amount>,
294 pub unit: CurrencyUnit,
296 pub request: String,
298 pub expiry: u64,
300 pub request_lookup_id: PaymentIdentifier,
302 pub pubkey: Option<PublicKey>,
304 #[serde(default)]
306 pub created_time: u64,
307 #[serde(default)]
309 amount_paid: Amount,
310 #[serde(default)]
312 amount_issued: Amount,
313 #[serde(default)]
315 pub payments: Vec<IncomingPayment>,
316 #[serde(default)]
318 pub payment_method: PaymentMethod,
319 #[serde(default)]
321 pub issuance: Vec<Issuance>,
322}
323
324impl MintQuote {
325 #[allow(clippy::too_many_arguments)]
327 pub fn new(
328 id: Option<QuoteId>,
329 request: String,
330 unit: CurrencyUnit,
331 amount: Option<Amount>,
332 expiry: u64,
333 request_lookup_id: PaymentIdentifier,
334 pubkey: Option<PublicKey>,
335 amount_paid: Amount,
336 amount_issued: Amount,
337 payment_method: PaymentMethod,
338 created_time: u64,
339 payments: Vec<IncomingPayment>,
340 issuance: Vec<Issuance>,
341 ) -> Self {
342 let id = id.unwrap_or_else(QuoteId::new_uuid);
343
344 Self {
345 id,
346 amount,
347 unit,
348 request,
349 expiry,
350 request_lookup_id,
351 pubkey,
352 created_time,
353 amount_paid,
354 amount_issued,
355 payment_method,
356 payments,
357 issuance,
358 }
359 }
360
361 #[instrument(skip(self))]
363 pub fn increment_amount_paid(
364 &mut self,
365 additional_amount: Amount,
366 ) -> Result<Amount, crate::Error> {
367 self.amount_paid = self
368 .amount_paid
369 .checked_add(additional_amount)
370 .ok_or(crate::Error::AmountOverflow)?;
371 Ok(self.amount_paid)
372 }
373
374 #[instrument(skip(self))]
376 pub fn amount_paid(&self) -> Amount {
377 self.amount_paid
378 }
379
380 #[instrument(skip(self))]
382 pub fn increment_amount_issued(
383 &mut self,
384 additional_amount: Amount,
385 ) -> Result<Amount, crate::Error> {
386 self.amount_issued = self
387 .amount_issued
388 .checked_add(additional_amount)
389 .ok_or(crate::Error::AmountOverflow)?;
390 Ok(self.amount_issued)
391 }
392
393 #[instrument(skip(self))]
395 pub fn amount_issued(&self) -> Amount {
396 self.amount_issued
397 }
398
399 #[instrument(skip(self))]
401 pub fn state(&self) -> MintQuoteState {
402 self.compute_quote_state()
403 }
404
405 pub fn payment_ids(&self) -> Vec<&String> {
407 self.payments.iter().map(|a| &a.payment_id).collect()
408 }
409
410 pub fn amount_mintable(&self) -> Amount {
417 self.amount_paid - self.amount_issued
418 }
419
420 #[instrument(skip(self))]
424 pub fn add_payment(
425 &mut self,
426 amount: Amount,
427 payment_id: String,
428 time: u64,
429 ) -> Result<(), crate::Error> {
430 let payment_ids = self.payment_ids();
431 if payment_ids.contains(&&payment_id) {
432 return Err(crate::Error::DuplicatePaymentId);
433 }
434
435 let payment = IncomingPayment::new(amount, payment_id, time);
436
437 self.payments.push(payment);
438 Ok(())
439 }
440
441 #[instrument(skip(self))]
443 fn compute_quote_state(&self) -> MintQuoteState {
444 if self.amount_paid == Amount::ZERO && self.amount_issued == Amount::ZERO {
445 return MintQuoteState::Unpaid;
446 }
447
448 match self.amount_paid.cmp(&self.amount_issued) {
449 std::cmp::Ordering::Less => {
450 tracing::error!("We should not have issued more then has been paid");
453 MintQuoteState::Issued
454 }
455 std::cmp::Ordering::Equal => {
456 MintQuoteState::Issued
460 }
461 std::cmp::Ordering::Greater => {
462 MintQuoteState::Paid
465 }
466 }
467 }
468}
469
470#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
472pub struct IncomingPayment {
473 pub amount: Amount,
475 pub time: u64,
477 pub payment_id: String,
479}
480
481impl IncomingPayment {
482 pub fn new(amount: Amount, payment_id: String, time: u64) -> Self {
484 Self {
485 payment_id,
486 time,
487 amount,
488 }
489 }
490}
491
492#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
494pub struct Issuance {
495 pub amount: Amount,
497 pub time: u64,
499}
500
501impl Issuance {
502 pub fn new(amount: Amount, time: u64) -> Self {
504 Self { amount, time }
505 }
506}
507
508#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
510pub struct MeltQuote {
511 pub id: QuoteId,
513 pub unit: CurrencyUnit,
515 pub amount: Amount,
517 pub request: MeltPaymentRequest,
519 pub fee_reserve: Amount,
521 pub state: MeltQuoteState,
523 pub expiry: u64,
525 pub payment_preimage: Option<String>,
527 pub request_lookup_id: Option<PaymentIdentifier>,
529 pub options: Option<MeltOptions>,
533 #[serde(default)]
535 pub created_time: u64,
536 pub paid_time: Option<u64>,
538 #[serde(default)]
540 pub payment_method: PaymentMethod,
541}
542
543impl MeltQuote {
544 #[allow(clippy::too_many_arguments)]
546 pub fn new(
547 request: MeltPaymentRequest,
548 unit: CurrencyUnit,
549 amount: Amount,
550 fee_reserve: Amount,
551 expiry: u64,
552 request_lookup_id: Option<PaymentIdentifier>,
553 options: Option<MeltOptions>,
554 payment_method: PaymentMethod,
555 ) -> Self {
556 let id = Uuid::new_v4();
557
558 Self {
559 id: QuoteId::UUID(id),
560 amount,
561 unit,
562 request,
563 fee_reserve,
564 state: MeltQuoteState::Unpaid,
565 expiry,
566 payment_preimage: None,
567 request_lookup_id,
568 options,
569 created_time: unix_time(),
570 paid_time: None,
571 payment_method,
572 }
573 }
574}
575
576#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
578pub struct MintKeySetInfo {
579 pub id: Id,
581 pub unit: CurrencyUnit,
583 pub active: bool,
586 pub valid_from: u64,
588 pub derivation_path: DerivationPath,
590 pub derivation_path_index: Option<u32>,
592 pub max_order: u8,
594 pub amounts: Vec<u64>,
596 #[serde(default = "default_fee")]
598 pub input_fee_ppk: u64,
599 pub final_expiry: Option<u64>,
601}
602
603pub fn default_fee() -> u64 {
605 0
606}
607
608impl From<MintKeySetInfo> for KeySetInfo {
609 fn from(keyset_info: MintKeySetInfo) -> Self {
610 Self {
611 id: keyset_info.id,
612 unit: keyset_info.unit,
613 active: keyset_info.active,
614 input_fee_ppk: keyset_info.input_fee_ppk,
615 final_expiry: keyset_info.final_expiry,
616 }
617 }
618}
619
620impl From<MintQuote> for MintQuoteBolt11Response<QuoteId> {
621 fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt11Response<QuoteId> {
622 MintQuoteBolt11Response {
623 quote: mint_quote.id.clone(),
624 state: mint_quote.state(),
625 request: mint_quote.request,
626 expiry: Some(mint_quote.expiry),
627 pubkey: mint_quote.pubkey,
628 amount: mint_quote.amount,
629 unit: Some(mint_quote.unit.clone()),
630 }
631 }
632}
633
634impl From<MintQuote> for MintQuoteBolt11Response<String> {
635 fn from(quote: MintQuote) -> Self {
636 let quote: MintQuoteBolt11Response<QuoteId> = quote.into();
637
638 quote.into()
639 }
640}
641
642impl TryFrom<crate::mint::MintQuote> for MintQuoteBolt12Response<QuoteId> {
643 type Error = crate::Error;
644
645 fn try_from(mint_quote: crate::mint::MintQuote) -> Result<Self, Self::Error> {
646 Ok(MintQuoteBolt12Response {
647 quote: mint_quote.id.clone(),
648 request: mint_quote.request,
649 expiry: Some(mint_quote.expiry),
650 amount_paid: mint_quote.amount_paid,
651 amount_issued: mint_quote.amount_issued,
652 pubkey: mint_quote.pubkey.ok_or(crate::Error::PubkeyRequired)?,
653 amount: mint_quote.amount,
654 unit: mint_quote.unit,
655 })
656 }
657}
658
659impl TryFrom<MintQuote> for MintQuoteBolt12Response<String> {
660 type Error = crate::Error;
661
662 fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
663 let quote: MintQuoteBolt12Response<QuoteId> = quote.try_into()?;
664
665 Ok(quote.into())
666 }
667}
668
669impl From<&MeltQuote> for MeltQuoteBolt11Response<QuoteId> {
670 fn from(melt_quote: &MeltQuote) -> MeltQuoteBolt11Response<QuoteId> {
671 MeltQuoteBolt11Response {
672 quote: melt_quote.id.clone(),
673 payment_preimage: None,
674 change: None,
675 state: melt_quote.state,
676 paid: Some(melt_quote.state == MeltQuoteState::Paid),
677 expiry: melt_quote.expiry,
678 amount: melt_quote.amount,
679 fee_reserve: melt_quote.fee_reserve,
680 request: None,
681 unit: Some(melt_quote.unit.clone()),
682 }
683 }
684}
685
686impl From<MeltQuote> for MeltQuoteBolt11Response<QuoteId> {
687 fn from(melt_quote: MeltQuote) -> MeltQuoteBolt11Response<QuoteId> {
688 let paid = melt_quote.state == MeltQuoteState::Paid;
689 MeltQuoteBolt11Response {
690 quote: melt_quote.id.clone(),
691 amount: melt_quote.amount,
692 fee_reserve: melt_quote.fee_reserve,
693 paid: Some(paid),
694 state: melt_quote.state,
695 expiry: melt_quote.expiry,
696 payment_preimage: melt_quote.payment_preimage,
697 change: None,
698 request: Some(melt_quote.request.to_string()),
699 unit: Some(melt_quote.unit.clone()),
700 }
701 }
702}
703
704#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
706pub enum MeltPaymentRequest {
707 Bolt11 {
709 bolt11: Bolt11Invoice,
711 },
712 Bolt12 {
714 #[serde(with = "offer_serde")]
716 offer: Box<Offer>,
717 },
718}
719
720impl std::fmt::Display for MeltPaymentRequest {
721 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
722 match self {
723 MeltPaymentRequest::Bolt11 { bolt11 } => write!(f, "{bolt11}"),
724 MeltPaymentRequest::Bolt12 { offer } => write!(f, "{offer}"),
725 }
726 }
727}
728
729mod offer_serde {
730 use std::str::FromStr;
731
732 use serde::{self, Deserialize, Deserializer, Serializer};
733
734 use super::Offer;
735
736 pub fn serialize<S>(offer: &Offer, serializer: S) -> Result<S::Ok, S::Error>
737 where
738 S: Serializer,
739 {
740 let s = offer.to_string();
741 serializer.serialize_str(&s)
742 }
743
744 pub fn deserialize<'de, D>(deserializer: D) -> Result<Box<Offer>, D::Error>
745 where
746 D: Deserializer<'de>,
747 {
748 let s = String::deserialize(deserializer)?;
749 Ok(Box::new(Offer::from_str(&s).map_err(|_| {
750 serde::de::Error::custom("Invalid Bolt12 Offer")
751 })?))
752 }
753}