1use std::fmt;
4use std::ops::Deref;
5use std::str::FromStr;
6
7use bitcoin::bip32::DerivationPath;
8use cashu::nuts::nut30::MeltQuoteOnchainFeeOption;
9use cashu::quote_id::QuoteId;
10use cashu::util::unix_time;
11use cashu::{
12 Bolt11Invoice, MeltOptions, MeltQuoteBolt11Response, MeltQuoteBolt12Response,
13 MeltQuoteCustomResponse, MeltQuoteOnchainResponse, MintQuoteBolt11Response,
14 MintQuoteBolt12Response, MintQuoteCustomResponse, MintQuoteOnchainResponse, PaymentMethod,
15 Proofs, State,
16};
17use lightning::offers::offer::Offer;
18use serde::{Deserialize, Serialize};
19use tracing::instrument;
20use uuid::Uuid;
21
22use crate::common::IssuerVersion;
23use crate::mint_quote::MintQuoteResponse;
24use crate::nuts::{MeltQuoteState, MintQuoteState};
25use crate::payment::PaymentIdentifier;
26use crate::{Amount, CurrencyUnit, Error, Id, KeySetInfo, PublicKey};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "lowercase")]
31pub enum OperationKind {
32 Swap,
34 Mint,
36 Melt,
38 BatchMint,
40}
41
42#[derive(Debug)]
74pub struct ProofsWithState {
75 proofs: Proofs,
76 pub state: State,
78}
79
80impl Deref for ProofsWithState {
81 type Target = Proofs;
82
83 fn deref(&self) -> &Self::Target {
84 &self.proofs
85 }
86}
87
88impl ProofsWithState {
89 pub fn new(proofs: Proofs, current_state: State) -> Self {
96 Self {
97 proofs,
98 state: current_state,
99 }
100 }
101}
102
103impl fmt::Display for OperationKind {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 match self {
106 OperationKind::Swap => write!(f, "swap"),
107 OperationKind::Mint => write!(f, "mint"),
108 OperationKind::Melt => write!(f, "melt"),
109 OperationKind::BatchMint => write!(f, "batch_mint"),
110 }
111 }
112}
113
114impl FromStr for OperationKind {
115 type Err = Error;
116 fn from_str(value: &str) -> Result<Self, Self::Err> {
117 let value = value.to_lowercase();
118 match value.as_str() {
119 "swap" => Ok(OperationKind::Swap),
120 "mint" => Ok(OperationKind::Mint),
121 "melt" => Ok(OperationKind::Melt),
122 "batch_mint" => Ok(OperationKind::BatchMint),
123 _ => Err(Error::Custom(format!("Invalid operation kind: {value}"))),
124 }
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130#[serde(rename_all = "snake_case")]
131pub enum SwapSagaState {
132 SetupComplete,
134 Signed,
136}
137
138impl fmt::Display for SwapSagaState {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 match self {
141 SwapSagaState::SetupComplete => write!(f, "setup_complete"),
142 SwapSagaState::Signed => write!(f, "signed"),
143 }
144 }
145}
146
147impl FromStr for SwapSagaState {
148 type Err = Error;
149 fn from_str(value: &str) -> Result<Self, Self::Err> {
150 let value = value.to_lowercase();
151 match value.as_str() {
152 "setup_complete" => Ok(SwapSagaState::SetupComplete),
153 "signed" => Ok(SwapSagaState::Signed),
154 _ => Err(Error::Custom(format!("Invalid swap saga state: {value}"))),
155 }
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161#[serde(rename_all = "snake_case")]
162pub enum MeltSagaState {
163 SetupComplete,
165 PaymentAttempted,
167 Finalizing,
169}
170
171impl fmt::Display for MeltSagaState {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 match self {
174 MeltSagaState::SetupComplete => write!(f, "setup_complete"),
175 MeltSagaState::PaymentAttempted => write!(f, "payment_attempted"),
176 MeltSagaState::Finalizing => write!(f, "finalizing"),
177 }
178 }
179}
180
181impl FromStr for MeltSagaState {
182 type Err = Error;
183 fn from_str(value: &str) -> Result<Self, Self::Err> {
184 let value = value.to_lowercase();
185 match value.as_str() {
186 "setup_complete" => Ok(MeltSagaState::SetupComplete),
187 "payment_attempted" => Ok(MeltSagaState::PaymentAttempted),
188 "finalizing" => Ok(MeltSagaState::Finalizing),
189 _ => Err(Error::Custom(format!("Invalid melt saga state: {}", value))),
190 }
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196#[serde(tag = "type", rename_all = "snake_case")]
197pub enum SagaStateEnum {
198 Swap(SwapSagaState),
200 Melt(MeltSagaState),
202 }
205
206impl SagaStateEnum {
207 pub fn new(operation_kind: OperationKind, s: &str) -> Result<Self, Error> {
209 match operation_kind {
210 OperationKind::Swap => Ok(SagaStateEnum::Swap(SwapSagaState::from_str(s)?)),
211 OperationKind::Melt => Ok(SagaStateEnum::Melt(MeltSagaState::from_str(s)?)),
212 OperationKind::Mint | OperationKind::BatchMint => {
213 Err(Error::Custom("Mint saga not implemented yet".to_string()))
214 }
215 }
216 }
217
218 pub fn state(&self) -> &str {
220 match self {
221 SagaStateEnum::Swap(state) => match state {
222 SwapSagaState::SetupComplete => "setup_complete",
223 SwapSagaState::Signed => "signed",
224 },
225 SagaStateEnum::Melt(state) => match state {
226 MeltSagaState::SetupComplete => "setup_complete",
227 MeltSagaState::PaymentAttempted => "payment_attempted",
228 MeltSagaState::Finalizing => "finalizing",
229 },
230 }
231 }
232}
233
234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
236pub struct Saga {
237 pub operation_id: Uuid,
239 pub operation_kind: OperationKind,
241 pub state: SagaStateEnum,
243 pub quote_id: Option<String>,
246 pub finalization_data: Option<MeltFinalizationData>,
248 pub created_at: u64,
250 pub updated_at: u64,
252}
253
254#[derive(Debug, Clone, PartialEq, Eq)]
256pub struct MeltFinalizationData {
257 pub total_spent: Amount<CurrencyUnit>,
259 pub payment_lookup_id: PaymentIdentifier,
261 pub payment_proof: Option<String>,
263}
264
265impl Serialize for MeltFinalizationData {
266 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
267 where
268 S: serde::Serializer,
269 {
270 #[derive(Serialize)]
271 struct MeltFinalizationDataSer<'a> {
272 total_spent: Amount,
273 unit: &'a CurrencyUnit,
274 payment_lookup_id: &'a PaymentIdentifier,
275 payment_proof: &'a Option<String>,
276 }
277
278 MeltFinalizationDataSer {
279 total_spent: self.total_spent.clone().into(),
280 unit: self.total_spent.unit(),
281 payment_lookup_id: &self.payment_lookup_id,
282 payment_proof: &self.payment_proof,
283 }
284 .serialize(serializer)
285 }
286}
287
288impl<'de> Deserialize<'de> for MeltFinalizationData {
289 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
290 where
291 D: serde::Deserializer<'de>,
292 {
293 #[derive(Deserialize)]
294 struct MeltFinalizationDataDe {
295 total_spent: Amount,
296 unit: CurrencyUnit,
297 payment_lookup_id: PaymentIdentifier,
298 payment_proof: Option<String>,
299 }
300
301 let data = MeltFinalizationDataDe::deserialize(deserializer)?;
302
303 Ok(Self {
304 total_spent: data.total_spent.with_unit(data.unit),
305 payment_lookup_id: data.payment_lookup_id,
306 payment_proof: data.payment_proof,
307 })
308 }
309}
310
311impl Saga {
312 pub fn new_swap(operation_id: Uuid, state: SwapSagaState) -> Self {
314 let now = unix_time();
315 Self {
316 operation_id,
317 operation_kind: OperationKind::Swap,
318 state: SagaStateEnum::Swap(state),
319 quote_id: None,
320 finalization_data: None,
321 created_at: now,
322 updated_at: now,
323 }
324 }
325
326 pub fn update_swap_state(&mut self, new_state: SwapSagaState) {
328 self.state = SagaStateEnum::Swap(new_state);
329 self.updated_at = unix_time();
330 }
331
332 pub fn new_melt(operation_id: Uuid, state: MeltSagaState, quote_id: String) -> Self {
334 let now = unix_time();
335 Self {
336 operation_id,
337 operation_kind: OperationKind::Melt,
338 state: SagaStateEnum::Melt(state),
339 quote_id: Some(quote_id),
340 finalization_data: None,
341 created_at: now,
342 updated_at: now,
343 }
344 }
345
346 pub fn update_melt_state(&mut self, new_state: MeltSagaState) {
348 self.state = SagaStateEnum::Melt(new_state);
349 self.updated_at = unix_time();
350 }
351
352 pub fn set_melt_finalization_data(&mut self, finalization_data: MeltFinalizationData) {
354 self.finalization_data = Some(finalization_data);
355 self.updated_at = unix_time();
356 }
357}
358
359#[derive(Debug)]
361pub struct Operation {
362 id: Uuid,
363 kind: OperationKind,
364 total_issued: Amount,
365 total_redeemed: Amount,
366 fee_collected: Amount,
367 complete_at: Option<u64>,
368 payment_amount: Option<Amount>,
370 payment_fee: Option<Amount>,
372 payment_method: Option<PaymentMethod>,
374}
375
376impl Operation {
377 pub fn new(
379 id: Uuid,
380 kind: OperationKind,
381 total_issued: Amount,
382 total_redeemed: Amount,
383 fee_collected: Amount,
384 complete_at: Option<u64>,
385 payment_method: Option<PaymentMethod>,
386 ) -> Self {
387 Self {
388 id,
389 kind,
390 total_issued,
391 total_redeemed,
392 fee_collected,
393 complete_at,
394 payment_amount: None,
395 payment_fee: None,
396 payment_method,
397 }
398 }
399
400 pub fn new_mint(total_issued: Amount, payment_method: PaymentMethod) -> Self {
402 Self {
403 id: Uuid::now_v7(),
404 kind: OperationKind::Mint,
405 total_issued,
406 total_redeemed: Amount::ZERO,
407 fee_collected: Amount::ZERO,
408 complete_at: None,
409 payment_amount: None,
410 payment_fee: None,
411 payment_method: Some(payment_method),
412 }
413 }
414
415 pub fn new_batch_mint(total_issued: Amount, payment_method: PaymentMethod) -> Self {
417 Self {
418 id: Uuid::now_v7(),
419 kind: OperationKind::BatchMint,
420 total_issued,
421 total_redeemed: Amount::ZERO,
422 fee_collected: Amount::ZERO,
423 complete_at: None,
424 payment_amount: None,
425 payment_fee: None,
426 payment_method: Some(payment_method),
427 }
428 }
429
430 pub fn new_melt(
434 total_redeemed: Amount,
435 fee_collected: Amount,
436 payment_method: PaymentMethod,
437 ) -> Self {
438 Self {
439 id: Uuid::now_v7(),
440 kind: OperationKind::Melt,
441 total_issued: Amount::ZERO,
442 total_redeemed,
443 fee_collected,
444 complete_at: None,
445 payment_amount: None,
446 payment_fee: None,
447 payment_method: Some(payment_method),
448 }
449 }
450
451 pub fn new_swap(total_issued: Amount, total_redeemed: Amount, fee_collected: Amount) -> Self {
453 Self {
454 id: Uuid::now_v7(),
455 kind: OperationKind::Swap,
456 total_issued,
457 total_redeemed,
458 fee_collected,
459 complete_at: None,
460 payment_amount: None,
461 payment_fee: None,
462 payment_method: None,
463 }
464 }
465
466 pub fn id(&self) -> &Uuid {
468 &self.id
469 }
470
471 pub fn kind(&self) -> OperationKind {
473 self.kind
474 }
475
476 pub fn total_issued(&self) -> Amount {
478 self.total_issued
479 }
480
481 pub fn total_redeemed(&self) -> Amount {
483 self.total_redeemed
484 }
485
486 pub fn fee_collected(&self) -> Amount {
488 self.fee_collected
489 }
490
491 pub fn completed_at(&self) -> &Option<u64> {
493 &self.complete_at
494 }
495
496 pub fn add_change(&mut self, change: Amount) {
498 self.total_issued = change;
499 }
500
501 pub fn payment_amount(&self) -> Option<Amount> {
503 self.payment_amount
504 }
505
506 pub fn payment_fee(&self) -> Option<Amount> {
508 self.payment_fee
509 }
510
511 pub fn set_payment_details(&mut self, payment_amount: Amount, payment_fee: Amount) {
513 self.payment_amount = Some(payment_amount);
514 self.payment_fee = Some(payment_fee);
515 }
516
517 pub fn payment_method(&self) -> Option<PaymentMethod> {
519 self.payment_method.clone()
520 }
521}
522
523#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
534pub struct MintQuoteChange {
535 pub payments: Option<Vec<IncomingPayment>>,
537 pub issuances: Option<Vec<Amount>>,
539}
540
541#[derive(Debug, Clone, Hash, PartialEq, Eq)]
543pub struct MintQuote {
544 pub id: QuoteId,
546 pub amount: Option<Amount<CurrencyUnit>>,
548 pub unit: CurrencyUnit,
550 pub request: String,
552 pub expiry: u64,
554 pub request_lookup_id: PaymentIdentifier,
556 pub pubkey: Option<PublicKey>,
558 pub created_time: u64,
560 amount_paid: Amount<CurrencyUnit>,
562 amount_issued: Amount<CurrencyUnit>,
564 updated_at: u64,
566 last_checked: u64,
568 pub payments: Vec<IncomingPayment>,
570 pub payment_method: PaymentMethod,
572 pub issuance: Vec<Issuance>,
574 pub extra_json: Option<serde_json::Value>,
576 changes: Option<MintQuoteChange>,
582}
583
584impl MintQuote {
585 #[allow(clippy::too_many_arguments)]
587 pub fn new(
588 id: Option<QuoteId>,
589 request: String,
590 unit: CurrencyUnit,
591 amount: Option<Amount<CurrencyUnit>>,
592 expiry: u64,
593 request_lookup_id: PaymentIdentifier,
594 pubkey: Option<PublicKey>,
595 amount_paid: Amount<CurrencyUnit>,
596 amount_issued: Amount<CurrencyUnit>,
597 payment_method: PaymentMethod,
598 created_time: u64,
599 updated_at: u64,
600 payments: Vec<IncomingPayment>,
601 issuance: Vec<Issuance>,
602 extra_json: Option<serde_json::Value>,
603 ) -> Self {
604 let id = id.unwrap_or_default();
605
606 Self {
607 id,
608 amount,
609 unit: unit.clone(),
610 request,
611 expiry,
612 request_lookup_id,
613 pubkey,
614 created_time,
615 amount_paid,
616 amount_issued,
617 updated_at,
618 last_checked: 0,
619 payment_method,
620 payments,
621 issuance,
622 extra_json,
623 changes: None,
624 }
625 }
626
627 #[instrument(skip(self))]
629 pub fn increment_amount_paid(
630 &mut self,
631 additional_amount: Amount<CurrencyUnit>,
632 ) -> Result<Amount, crate::Error> {
633 self.amount_paid = self
634 .amount_paid
635 .checked_add(&additional_amount)
636 .map_err(|_| crate::Error::AmountOverflow)?;
637 Ok(Amount::from(self.amount_paid.value()))
638 }
639
640 #[instrument(skip(self))]
642 pub fn amount_paid(&self) -> Amount<CurrencyUnit> {
643 self.amount_paid.clone()
644 }
645
646 #[instrument(skip(self))]
669 pub fn add_issuance(
670 &mut self,
671 additional_amount: Amount<CurrencyUnit>,
672 ) -> Result<Amount<CurrencyUnit>, crate::Error> {
673 let new_amount_issued = self
674 .amount_issued
675 .checked_add(&additional_amount)
676 .map_err(|_| crate::Error::AmountOverflow)?;
677
678 if new_amount_issued > self.amount_paid {
680 return Err(crate::Error::OverIssue);
681 }
682
683 self.changes
684 .get_or_insert_default()
685 .issuances
686 .get_or_insert_default()
687 .push(additional_amount.into());
688
689 self.amount_issued = new_amount_issued;
690
691 Ok(self.amount_issued.clone())
692 }
693
694 #[instrument(skip(self))]
696 pub fn amount_issued(&self) -> Amount<CurrencyUnit> {
697 self.amount_issued.clone()
698 }
699
700 pub fn updated_at(&self) -> u64 {
702 self.updated_at
703 }
704
705 pub fn set_updated_at(&mut self, updated_at: u64) {
707 self.updated_at = updated_at;
708 }
709
710 pub fn last_checked(&self) -> u64 {
712 self.last_checked
713 }
714
715 pub fn set_last_checked(&mut self, last_checked: u64) {
717 self.last_checked = last_checked;
718 }
719
720 #[instrument(skip(self))]
722 pub fn state(&self) -> MintQuoteState {
723 self.compute_quote_state()
724 }
725
726 pub fn payment_ids(&self) -> Vec<&String> {
728 self.payments.iter().map(|a| &a.payment_id).collect()
729 }
730
731 pub fn amount_mintable(&self) -> Amount<CurrencyUnit> {
738 self.amount_paid
739 .checked_sub(&self.amount_issued)
740 .unwrap_or_else(|_| Amount::new(0, self.unit.clone()))
741 }
742
743 pub fn take_changes(&mut self) -> Option<MintQuoteChange> {
753 self.changes.take()
754 }
755
756 #[instrument(skip(self))]
776 pub fn add_payment(
777 &mut self,
778 amount: Amount<CurrencyUnit>,
779 payment_id: String,
780 time: Option<u64>,
781 ) -> Result<(), crate::Error> {
782 let time = time.unwrap_or_else(unix_time);
783
784 let payment_ids = self.payment_ids();
785 if payment_ids.contains(&&payment_id) {
786 return Err(crate::Error::DuplicatePaymentId);
787 }
788
789 self.amount_paid = self
790 .amount_paid
791 .checked_add(&amount)
792 .map_err(|_| crate::Error::AmountOverflow)?;
793
794 let payment = IncomingPayment::new(amount, payment_id, time);
795
796 self.payments.push(payment.clone());
797
798 self.changes
799 .get_or_insert_default()
800 .payments
801 .get_or_insert_default()
802 .push(payment);
803
804 Ok(())
805 }
806
807 #[instrument(skip(self))]
809 fn compute_quote_state(&self) -> MintQuoteState {
810 let zero_amount = Amount::new(0, self.unit.clone());
811
812 if self.amount_paid == zero_amount && self.amount_issued == zero_amount {
813 return MintQuoteState::Unpaid;
814 }
815
816 match self.amount_paid.value().cmp(&self.amount_issued.value()) {
817 std::cmp::Ordering::Less => {
818 tracing::error!("We should not have issued more then has been paid");
819 MintQuoteState::Issued
820 }
821 std::cmp::Ordering::Equal => MintQuoteState::Issued,
822 std::cmp::Ordering::Greater => MintQuoteState::Paid,
823 }
824 }
825}
826
827#[derive(Debug, Clone, Hash, PartialEq, Eq)]
829pub struct IncomingPayment {
830 pub amount: Amount<CurrencyUnit>,
832 pub time: u64,
834 pub payment_id: String,
836}
837
838impl IncomingPayment {
839 pub fn new(amount: Amount<CurrencyUnit>, payment_id: String, time: u64) -> Self {
841 Self {
842 payment_id,
843 time,
844 amount,
845 }
846 }
847}
848
849#[derive(Debug, Clone, Hash, PartialEq, Eq)]
851pub struct Issuance {
852 pub amount: Amount<CurrencyUnit>,
854 pub time: u64,
856}
857
858impl Issuance {
859 pub fn new(amount: Amount<CurrencyUnit>, time: u64) -> Self {
861 Self { amount, time }
862 }
863}
864
865#[derive(Debug, Clone, Hash, PartialEq, Eq)]
867pub struct MeltQuote {
868 pub id: QuoteId,
870 pub unit: CurrencyUnit,
872 pub request: MeltPaymentRequest,
874 amount: Amount<CurrencyUnit>,
876 fee_reserve: Amount<CurrencyUnit>,
878 pub state: MeltQuoteState,
880 pub expiry: u64,
882 pub payment_proof: Option<String>,
884 pub request_lookup_id: Option<PaymentIdentifier>,
886 pub options: Option<MeltOptions>,
890 pub created_time: u64,
892 pub paid_time: Option<u64>,
894 pub payment_method: PaymentMethod,
896 pub extra_json: Option<serde_json::Value>,
898 pub estimated_blocks: Option<u32>,
900 fee_options: Vec<MeltQuoteOnchainFeeOption>,
910 pub selected_fee_index: Option<u32>,
912}
913
914impl MeltQuote {
915 #[allow(clippy::too_many_arguments)]
917 pub fn new(
918 id: Option<QuoteId>,
919 request: MeltPaymentRequest,
920 unit: CurrencyUnit,
921 amount: Amount<CurrencyUnit>,
922 fee_reserve: Amount<CurrencyUnit>,
923 expiry: u64,
924 request_lookup_id: Option<PaymentIdentifier>,
925 options: Option<MeltOptions>,
926 payment_method: PaymentMethod,
927 extra_json: Option<serde_json::Value>,
928 estimated_blocks: Option<u32>,
929 ) -> Self {
930 let id = id.unwrap_or_default();
931
932 let fee_options = estimated_blocks
933 .map(|estimated_blocks| {
934 vec![MeltQuoteOnchainFeeOption {
935 fee_index: 0,
936 fee_reserve: fee_reserve.clone().into(),
937 estimated_blocks,
938 }]
939 })
940 .unwrap_or_default();
941
942 Self {
943 id,
944 unit: unit.clone(),
945 request,
946 amount,
947 fee_reserve,
948 state: MeltQuoteState::Unpaid,
949 expiry,
950 payment_proof: None,
951 request_lookup_id,
952 options,
953 created_time: unix_time(),
954 paid_time: None,
955 payment_method,
956 extra_json,
957 estimated_blocks,
958 fee_options,
959 selected_fee_index: None,
960 }
961 }
962
963 #[allow(clippy::too_many_arguments)]
975 pub fn new_onchain(
976 id: Option<QuoteId>,
977 request: MeltPaymentRequest,
978 unit: CurrencyUnit,
979 amount: Amount<CurrencyUnit>,
980 expiry: u64,
981 request_lookup_id: Option<PaymentIdentifier>,
982 extra_json: Option<serde_json::Value>,
983 fee_options: Vec<MeltQuoteOnchainFeeOption>,
984 ) -> Result<Self, crate::Error> {
985 if fee_options.is_empty() {
986 return Err(crate::Error::OnchainFeeOptionsEmpty);
987 }
988
989 validate_onchain_fee_options(&fee_options)?;
990
991 let id = id.unwrap_or_default();
992
993 let initial = fee_options
997 .iter()
998 .min_by_key(|option| u64::from(option.fee_reserve))
999 .copied()
1000 .ok_or(crate::Error::OnchainFeeOptionsEmpty)?;
1001
1002 let fee_reserve = initial.fee_reserve.with_unit(unit.clone());
1003 let estimated_blocks = Some(initial.estimated_blocks);
1004
1005 Ok(Self {
1006 id,
1007 unit: unit.clone(),
1008 request,
1009 amount,
1010 fee_reserve,
1011 state: MeltQuoteState::Unpaid,
1012 expiry,
1013 payment_proof: None,
1014 request_lookup_id,
1015 options: None,
1016 created_time: unix_time(),
1017 paid_time: None,
1018 payment_method: PaymentMethod::Known(cashu::nuts::nut00::KnownMethod::Onchain),
1019 extra_json,
1020 estimated_blocks,
1021 fee_options,
1022 selected_fee_index: None,
1023 })
1024 }
1025
1026 #[inline]
1032 pub fn fee_options(&self) -> &[MeltQuoteOnchainFeeOption] {
1033 &self.fee_options
1034 }
1035
1036 #[inline]
1038 pub fn amount(&self) -> Amount<CurrencyUnit> {
1039 self.amount.clone()
1040 }
1041
1042 #[inline]
1044 pub fn fee_reserve(&self) -> Amount<CurrencyUnit> {
1045 self.fee_reserve.clone()
1046 }
1047
1048 pub fn select_onchain_fee_option(&mut self, fee_index: u32) -> Result<(), crate::Error> {
1050 let option = self
1051 .fee_options
1052 .iter()
1053 .find(|option| option.fee_index == fee_index)
1054 .copied()
1055 .ok_or(crate::Error::OnchainFeeIndexNotFound { index: fee_index })?;
1056
1057 if self
1058 .selected_fee_index
1059 .is_some_and(|selected| selected != fee_index)
1060 {
1061 return Err(crate::Error::InvalidPaymentRequest);
1062 }
1063
1064 self.fee_reserve = option.fee_reserve.with_unit(self.unit.clone());
1065 self.estimated_blocks = Some(option.estimated_blocks);
1066 self.selected_fee_index = Some(fee_index);
1067
1068 Ok(())
1069 }
1070
1071 pub fn into_response(
1077 self,
1078 change: Option<Vec<cashu::nuts::BlindSignature>>,
1079 ) -> crate::MeltQuoteResponse<QuoteId> {
1080 match self.payment_method {
1081 PaymentMethod::Known(cashu::nuts::nut00::KnownMethod::Bolt11) => {
1082 let mut response: MeltQuoteBolt11Response<QuoteId> = self.into();
1083 response.change = change;
1084 crate::MeltQuoteResponse::Bolt11(response)
1085 }
1086 PaymentMethod::Known(cashu::nuts::nut00::KnownMethod::Bolt12) => {
1087 let mut response: MeltQuoteBolt12Response<QuoteId> = self.into();
1088 response.change = change;
1089 crate::MeltQuoteResponse::Bolt12(response)
1090 }
1091 PaymentMethod::Known(cashu::nuts::nut00::KnownMethod::Onchain) => {
1092 let mut response: MeltQuoteOnchainResponse<QuoteId> = self.into();
1093 response.change = change;
1094 crate::MeltQuoteResponse::Onchain(response)
1095 }
1096 _ => {
1097 let method = self.payment_method.clone();
1098 let mut response: MeltQuoteCustomResponse<QuoteId> = self.into();
1099 response.change = change;
1100 crate::MeltQuoteResponse::Custom((method, response))
1101 }
1102 }
1103 }
1104
1105 pub fn total_needed(&self) -> Result<Amount, crate::Error> {
1107 let total = self
1108 .amount
1109 .checked_add(&self.fee_reserve)
1110 .map_err(|_| crate::Error::AmountOverflow)?;
1111 Ok(Amount::from(total.value()))
1112 }
1113
1114 #[allow(clippy::too_many_arguments)]
1116 pub fn from_db(
1117 id: QuoteId,
1118 unit: CurrencyUnit,
1119 request: MeltPaymentRequest,
1120 amount: u64,
1121 fee_reserve: u64,
1122 state: MeltQuoteState,
1123 expiry: u64,
1124 payment_proof: Option<String>,
1125 request_lookup_id: Option<PaymentIdentifier>,
1126 options: Option<MeltOptions>,
1127 created_time: u64,
1128 paid_time: Option<u64>,
1129 payment_method: PaymentMethod,
1130 extra_json: Option<serde_json::Value>,
1131 estimated_blocks: Option<u32>,
1132 fee_options: Vec<MeltQuoteOnchainFeeOption>,
1133 selected_fee_index: Option<u32>,
1134 ) -> Result<Self, crate::Error> {
1135 if payment_method == PaymentMethod::Known(cashu::nuts::nut00::KnownMethod::Onchain) {
1140 validate_onchain_fee_options(&fee_options)?;
1141 }
1142
1143 Ok(Self {
1144 id,
1145 unit: unit.clone(),
1146 request,
1147 amount: Amount::new(amount, unit.clone()),
1148 fee_reserve: Amount::new(fee_reserve, unit),
1149 state,
1150 expiry,
1151 payment_proof,
1152 request_lookup_id,
1153 options,
1154 created_time,
1155 paid_time,
1156 payment_method,
1157 extra_json,
1158 estimated_blocks,
1159 fee_options,
1160 selected_fee_index,
1161 })
1162 }
1163}
1164
1165pub fn validate_onchain_fee_options(
1174 fee_options: &[MeltQuoteOnchainFeeOption],
1175) -> Result<(), crate::Error> {
1176 if fee_options.is_empty() {
1177 return Err(crate::Error::OnchainFeeOptionsEmpty);
1178 }
1179
1180 Ok(())
1181}
1182
1183impl From<MeltQuote> for MeltQuoteOnchainResponse<QuoteId> {
1184 fn from(quote: MeltQuote) -> Self {
1185 Self {
1186 quote: quote.id.clone(),
1187 amount: quote.amount().into(),
1188 unit: quote.unit.clone(),
1189 method: PaymentMethod::Known(cashu::nuts::nut00::KnownMethod::Onchain),
1190 state: quote.state,
1191 expiry: quote.expiry,
1192 request: quote.request.to_string(),
1193 fee_options: quote.fee_options().to_vec(),
1194 selected_fee_index: quote.selected_fee_index,
1195 outpoint: quote.payment_proof.clone(),
1196 change: None,
1197 }
1198 }
1199}
1200
1201impl TryFrom<MintQuote> for MintQuoteOnchainResponse<QuoteId> {
1202 type Error = crate::error::Error;
1203 fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
1204 Ok(Self {
1205 quote: quote.id.clone(),
1206 request: quote.request.clone(),
1207 unit: quote.unit.clone(),
1208 method: PaymentMethod::Known(cashu::nuts::nut00::KnownMethod::Onchain),
1209 expiry: (quote.expiry != 0).then_some(quote.expiry),
1210 pubkey: quote.pubkey.ok_or(crate::error::Error::MissingPubkey)?,
1211 amount_paid: quote.amount_paid().into(),
1212 amount_issued: quote.amount_issued().into(),
1213 updated_at: quote.updated_at(),
1214 })
1215 }
1216}
1217
1218#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
1220pub struct MintKeySetInfo {
1221 pub id: Id,
1223 pub unit: CurrencyUnit,
1225 pub active: bool,
1228 pub valid_from: u64,
1230 pub derivation_path: DerivationPath,
1232 pub derivation_path_index: Option<u32>,
1234 pub amounts: Vec<u64>,
1236 #[serde(default = "default_fee")]
1238 pub input_fee_ppk: u64,
1239 pub final_expiry: Option<u64>,
1241 pub issuer_version: Option<IssuerVersion>,
1243}
1244
1245impl MintKeySetInfo {
1246 pub fn is_expired(&self) -> bool {
1248 self.final_expiry.is_some_and(|expiry| expiry < unix_time())
1249 }
1250}
1251
1252pub fn default_fee() -> u64 {
1254 0
1255}
1256
1257impl From<MintKeySetInfo> for KeySetInfo {
1258 fn from(keyset_info: MintKeySetInfo) -> Self {
1259 Self {
1260 id: keyset_info.id,
1261 unit: keyset_info.unit,
1262 active: keyset_info.active,
1263 input_fee_ppk: keyset_info.input_fee_ppk,
1264 final_expiry: keyset_info.final_expiry,
1265 }
1266 }
1267}
1268
1269impl From<MintQuote> for MintQuoteBolt11Response<QuoteId> {
1270 fn from(mint_quote: MintQuote) -> MintQuoteBolt11Response<QuoteId> {
1271 let amount_paid = mint_quote.amount_paid().into();
1272 let amount_issued = mint_quote.amount_issued().into();
1273 let updated_at = mint_quote.updated_at();
1274
1275 MintQuoteBolt11Response {
1276 quote: mint_quote.id.clone(),
1277 state: mint_quote.state(),
1278 request: mint_quote.request,
1279 expiry: Some(mint_quote.expiry),
1280 pubkey: mint_quote.pubkey,
1281 amount: mint_quote.amount.map(Into::into),
1282 unit: Some(mint_quote.unit),
1283 method: PaymentMethod::Known(cashu::nuts::nut00::KnownMethod::Bolt11),
1284 amount_paid,
1285 amount_issued,
1286 updated_at,
1287 }
1288 }
1289}
1290
1291impl From<MintQuote> for MintQuoteBolt11Response<String> {
1292 fn from(quote: MintQuote) -> Self {
1293 let quote: MintQuoteBolt11Response<QuoteId> = quote.into();
1294 quote.into()
1295 }
1296}
1297
1298impl TryFrom<MintQuote> for MintQuoteBolt12Response<QuoteId> {
1299 type Error = Error;
1300
1301 fn try_from(mint_quote: MintQuote) -> Result<Self, Self::Error> {
1302 let amount_paid = mint_quote.amount_paid().into();
1303 let amount_issued = mint_quote.amount_issued().into();
1304 let updated_at = mint_quote.updated_at();
1305
1306 Ok(MintQuoteBolt12Response {
1307 quote: mint_quote.id.clone(),
1308 request: mint_quote.request,
1309 expiry: (mint_quote.expiry != 0).then_some(mint_quote.expiry),
1310 amount_paid,
1311 amount_issued,
1312 pubkey: mint_quote.pubkey.ok_or(Error::PubkeyRequired)?,
1313 amount: mint_quote.amount.map(Into::into),
1314 unit: mint_quote.unit,
1315 method: PaymentMethod::Known(cashu::nuts::nut00::KnownMethod::Bolt12),
1316 updated_at,
1317 })
1318 }
1319}
1320
1321impl TryFrom<MintQuote> for MintQuoteBolt12Response<String> {
1322 type Error = Error;
1323
1324 fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
1325 let quote: MintQuoteBolt12Response<QuoteId> = quote.try_into()?;
1326 Ok(quote.into())
1327 }
1328}
1329
1330impl TryFrom<MintQuote> for MintQuoteCustomResponse<QuoteId> {
1331 type Error = Error;
1332
1333 fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
1334 let amount_paid = quote.amount_paid().into();
1335 let amount_issued = quote.amount_issued().into();
1336 let updated_at = quote.updated_at();
1337
1338 Ok(MintQuoteCustomResponse {
1339 quote: quote.id,
1340 request: quote.request,
1341 method: quote.payment_method,
1342 unit: Some(quote.unit),
1343 expiry: Some(quote.expiry),
1344 pubkey: quote.pubkey,
1345 amount: quote.amount.map(Into::into),
1346 amount_paid,
1347 amount_issued,
1348 updated_at,
1349 extra: quote.extra_json.unwrap_or_default(),
1350 })
1351 }
1352}
1353
1354impl TryFrom<MintQuote> for MintQuoteCustomResponse<String> {
1355 type Error = Error;
1356
1357 fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
1358 let quote: MintQuoteCustomResponse<QuoteId> = quote.try_into()?;
1359 Ok(quote.into())
1360 }
1361}
1362
1363impl From<MeltQuote> for crate::nuts::MeltQuoteCustomResponse<QuoteId> {
1364 fn from(melt_quote: MeltQuote) -> Self {
1365 let method = melt_quote.payment_method.clone();
1366 let request = match melt_quote.request {
1367 MeltPaymentRequest::Custom { request, .. } => Some(request),
1368 _ => None,
1369 };
1370
1371 Self {
1372 quote: melt_quote.id,
1373 method,
1374 amount: melt_quote.amount.into(),
1375 fee_reserve: Some(melt_quote.fee_reserve.into()),
1376 state: melt_quote.state,
1377 expiry: melt_quote.expiry,
1378 payment_preimage: melt_quote.payment_proof,
1379 change: None,
1380 request,
1381 unit: Some(melt_quote.unit),
1382 extra: melt_quote.extra_json.unwrap_or_default(),
1383 }
1384 }
1385}
1386
1387impl From<&MeltQuote> for MeltQuoteBolt12Response<QuoteId> {
1388 fn from(melt_quote: &MeltQuote) -> MeltQuoteBolt12Response<QuoteId> {
1389 MeltQuoteBolt12Response {
1390 quote: melt_quote.id.clone(),
1391 payment_preimage: None,
1392 change: None,
1393 state: melt_quote.state,
1394 expiry: melt_quote.expiry,
1395 amount: melt_quote.amount().into(),
1396 fee_reserve: melt_quote.fee_reserve().into(),
1397 request: None,
1398 unit: Some(melt_quote.unit.clone()),
1399 method: PaymentMethod::Known(cashu::nuts::nut00::KnownMethod::Bolt12),
1400 }
1401 }
1402}
1403
1404impl From<MeltQuote> for MeltQuoteBolt12Response<QuoteId> {
1405 fn from(melt_quote: MeltQuote) -> MeltQuoteBolt12Response<QuoteId> {
1406 MeltQuoteBolt12Response {
1407 quote: melt_quote.id.clone(),
1408 amount: melt_quote.amount().into(),
1409 fee_reserve: melt_quote.fee_reserve().into(),
1410 state: melt_quote.state,
1411 expiry: melt_quote.expiry,
1412 payment_preimage: melt_quote.payment_proof,
1413 change: None,
1414 request: Some(melt_quote.request.to_string()),
1415 unit: Some(melt_quote.unit.clone()),
1416 method: PaymentMethod::Known(cashu::nuts::nut00::KnownMethod::Bolt12),
1417 }
1418 }
1419}
1420
1421impl TryFrom<MintQuote> for MintQuoteResponse<QuoteId> {
1422 type Error = Error;
1423
1424 fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
1425 if quote.payment_method.is_bolt11() {
1426 Ok(Self::Bolt11(crate::nuts::nut23::MintQuoteBolt11Response {
1427 quote: quote.id.clone(),
1428 request: quote.request.clone(),
1429 state: quote.state(),
1430 expiry: Some(quote.expiry),
1431 amount: quote.amount.as_ref().map(|a| a.clone().into()),
1432 unit: Some(quote.unit.clone()),
1433 method: PaymentMethod::Known(cashu::nuts::nut00::KnownMethod::Bolt11),
1434 pubkey: quote.pubkey,
1435 amount_paid: quote.amount_paid().into(),
1436 amount_issued: quote.amount_issued().into(),
1437 updated_at: quote.updated_at(),
1438 }))
1439 } else if quote.payment_method.is_bolt12() {
1440 Ok(Self::Bolt12(crate::nuts::nut25::MintQuoteBolt12Response {
1441 quote: quote.id.clone(),
1442 request: quote.request.clone(),
1443 amount: quote.amount.as_ref().map(|a| a.clone().into()),
1444 unit: quote.unit.clone(),
1445 method: PaymentMethod::Known(cashu::nuts::nut00::KnownMethod::Bolt12),
1446 expiry: (quote.expiry != 0).then_some(quote.expiry),
1447 pubkey: quote.pubkey.ok_or(Error::PubkeyRequired)?,
1448 amount_paid: quote.amount_paid().into(),
1449 amount_issued: quote.amount_issued().into(),
1450 updated_at: quote.updated_at(),
1451 }))
1452 } else if quote.payment_method.is_onchain() {
1453 let onchain_response = MintQuoteOnchainResponse::try_from(quote)?;
1454 Ok(MintQuoteResponse::Onchain(onchain_response))
1455 } else {
1456 let method = quote.payment_method.clone();
1457 Ok(MintQuoteResponse::Custom {
1458 method: method.clone(),
1459 response: crate::nuts::nut04::MintQuoteCustomResponse {
1460 quote: quote.id.clone(),
1461 request: quote.request.clone(),
1462 method: method.clone(),
1463 expiry: Some(quote.expiry),
1464 amount: quote.amount.as_ref().map(|a| a.clone().into()),
1465 amount_paid: quote.amount_paid().into(),
1466 amount_issued: quote.amount_issued().into(),
1467 updated_at: quote.updated_at(),
1468 unit: Some(quote.unit.clone()),
1469 pubkey: quote.pubkey,
1470 extra: quote.extra_json.clone().unwrap_or_default(),
1471 },
1472 })
1473 }
1474 }
1475}
1476
1477impl From<MintQuoteResponse<QuoteId>> for MintQuoteResponse<String> {
1478 fn from(response: MintQuoteResponse<QuoteId>) -> Self {
1479 match response {
1480 MintQuoteResponse::Bolt11(response) => MintQuoteResponse::Bolt11(response.into()),
1481 MintQuoteResponse::Bolt12(response) => MintQuoteResponse::Bolt12(response.into()),
1482 MintQuoteResponse::Onchain(response) => MintQuoteResponse::Onchain(response.into()),
1483 MintQuoteResponse::Custom { method, response } => MintQuoteResponse::Custom {
1484 method,
1485 response: response.into(),
1486 },
1487 }
1488 }
1489}
1490
1491impl From<MintQuoteResponse<QuoteId>> for MintQuoteBolt11Response<String> {
1492 fn from(response: MintQuoteResponse<QuoteId>) -> Self {
1493 match response {
1494 MintQuoteResponse::Bolt11(bolt11_response) => MintQuoteBolt11Response {
1495 quote: bolt11_response.quote.to_string(),
1496 state: bolt11_response.state,
1497 request: bolt11_response.request,
1498 expiry: bolt11_response.expiry,
1499 pubkey: bolt11_response.pubkey,
1500 amount: bolt11_response.amount,
1501 unit: bolt11_response.unit,
1502 method: bolt11_response.method,
1503 amount_paid: bolt11_response.amount_paid,
1504 amount_issued: bolt11_response.amount_issued,
1505 updated_at: bolt11_response.updated_at,
1506 },
1507 _ => panic!("Expected Bolt11 response"),
1508 }
1509 }
1510}
1511
1512impl TryFrom<MintQuoteResponse<QuoteId>> for MintQuoteBolt11Response<QuoteId> {
1513 type Error = Error;
1514
1515 fn try_from(response: MintQuoteResponse<QuoteId>) -> Result<Self, Self::Error> {
1516 match response {
1517 MintQuoteResponse::Bolt11(r) => Ok(r),
1518 _ => Err(Error::InvalidPaymentMethod),
1519 }
1520 }
1521}
1522
1523impl TryFrom<MintQuoteResponse<QuoteId>> for MintQuoteBolt12Response<QuoteId> {
1524 type Error = Error;
1525
1526 fn try_from(response: MintQuoteResponse<QuoteId>) -> Result<Self, Self::Error> {
1527 match response {
1528 MintQuoteResponse::Bolt12(r) => Ok(r),
1529 _ => Err(Error::InvalidPaymentMethod),
1530 }
1531 }
1532}
1533
1534impl TryFrom<MintQuoteResponse<QuoteId>> for MintQuoteOnchainResponse<QuoteId> {
1535 type Error = Error;
1536
1537 fn try_from(response: MintQuoteResponse<QuoteId>) -> Result<Self, Self::Error> {
1538 match response {
1539 MintQuoteResponse::Onchain(r) => Ok(r),
1540 _ => Err(Error::InvalidPaymentMethod),
1541 }
1542 }
1543}
1544
1545impl From<&MeltQuote> for MeltQuoteBolt11Response<QuoteId> {
1546 fn from(melt_quote: &MeltQuote) -> MeltQuoteBolt11Response<QuoteId> {
1547 MeltQuoteBolt11Response {
1548 quote: melt_quote.id.clone(),
1549 payment_preimage: None,
1550 change: None,
1551 state: melt_quote.state,
1552 expiry: melt_quote.expiry,
1553 amount: melt_quote.amount().into(),
1554 fee_reserve: melt_quote.fee_reserve().into(),
1555 request: None,
1556 unit: Some(melt_quote.unit.clone()),
1557 method: melt_quote.payment_method.clone(),
1558 }
1559 }
1560}
1561
1562impl From<MeltQuote> for MeltQuoteBolt11Response<QuoteId> {
1563 fn from(melt_quote: MeltQuote) -> MeltQuoteBolt11Response<QuoteId> {
1564 MeltQuoteBolt11Response {
1565 quote: melt_quote.id.clone(),
1566 amount: melt_quote.amount().into(),
1567 fee_reserve: melt_quote.fee_reserve().into(),
1568 state: melt_quote.state,
1569 expiry: melt_quote.expiry,
1570 payment_preimage: melt_quote.payment_proof,
1571 change: None,
1572 request: Some(melt_quote.request.to_string()),
1573 unit: Some(melt_quote.unit.clone()),
1574 method: melt_quote.payment_method.clone(),
1575 }
1576 }
1577}
1578
1579#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
1581pub enum MeltPaymentRequest {
1582 Bolt11 {
1584 bolt11: Bolt11Invoice,
1586 },
1587 Bolt12 {
1589 #[serde(with = "offer_serde")]
1591 offer: Box<Offer>,
1592 },
1593 Custom {
1595 method: String,
1597 request: String,
1599 },
1600 Onchain {
1602 address: String,
1604 },
1605}
1606
1607impl std::fmt::Display for MeltPaymentRequest {
1608 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1609 match self {
1610 MeltPaymentRequest::Bolt11 { bolt11 } => write!(f, "{bolt11}"),
1611 MeltPaymentRequest::Bolt12 { offer } => write!(f, "{offer}"),
1612 MeltPaymentRequest::Custom { request, .. } => write!(f, "{request}"),
1613 MeltPaymentRequest::Onchain { address } => write!(f, "{address}"),
1614 }
1615 }
1616}
1617
1618mod offer_serde {
1619 use std::str::FromStr;
1620
1621 use serde::{self, Deserialize, Deserializer, Serializer};
1622
1623 use super::Offer;
1624
1625 pub fn serialize<S>(offer: &Offer, serializer: S) -> Result<S::Ok, S::Error>
1626 where
1627 S: Serializer,
1628 {
1629 let s = offer.to_string();
1630 serializer.serialize_str(&s)
1631 }
1632
1633 pub fn deserialize<'de, D>(deserializer: D) -> Result<Box<Offer>, D::Error>
1634 where
1635 D: Deserializer<'de>,
1636 {
1637 let s = String::deserialize(deserializer)?;
1638 Ok(Box::new(Offer::from_str(&s).map_err(|_| {
1639 serde::de::Error::custom("Invalid Bolt12 Offer")
1640 })?))
1641 }
1642}
1643
1644#[cfg(test)]
1645mod tests {
1646 use std::str::FromStr;
1647
1648 use cashu::Bolt11Invoice;
1649
1650 use super::*;
1651
1652 #[test]
1653 fn test_operation_new_mint_uses_uuid_v7() {
1654 let operation = Operation::new_mint(Amount::from(100), PaymentMethod::BOLT11);
1655
1656 assert_eq!(operation.id.get_version(), Some(uuid::Version::SortRand));
1657 }
1658
1659 #[test]
1660 fn test_melt_quote_to_custom_response_with_custom_request() {
1661 let melt_quote = MeltQuote::new(
1662 Some(QuoteId::new()),
1663 MeltPaymentRequest::Custom {
1664 method: "custom".to_string(),
1665 request: "custom_request_string".to_string(),
1666 },
1667 CurrencyUnit::Sat,
1668 Amount::new(100, CurrencyUnit::Sat),
1669 Amount::new(2, CurrencyUnit::Sat),
1670 unix_time() + 3600,
1671 None,
1672 None,
1673 PaymentMethod::Custom("custom".to_string()),
1674 Some(serde_json::json!({"extra_field": "value"})),
1675 None,
1676 );
1677
1678 let response: crate::nuts::MeltQuoteCustomResponse<QuoteId> = melt_quote.clone().into();
1679
1680 assert_eq!(response.quote, melt_quote.id);
1681 assert_eq!(response.amount, 100.into());
1682 assert_eq!(response.fee_reserve, Some(2.into()));
1683 assert_eq!(response.state, melt_quote.state);
1684 assert_eq!(response.expiry, melt_quote.expiry);
1685 assert_eq!(response.payment_preimage, melt_quote.payment_proof);
1686 assert_eq!(response.change, None);
1687 assert_eq!(response.request, Some("custom_request_string".to_string()));
1688 assert_eq!(response.unit, Some(CurrencyUnit::Sat));
1689 assert_eq!(response.extra, serde_json::json!({"extra_field": "value"}));
1690 }
1691
1692 #[test]
1693 fn test_melt_quote_to_custom_response_with_bolt11_request() {
1694 let bolt11_str = "lnbc100n1pnvpufspp5djn8hrq49r8cghwye9kqw752qjncwyfnrprhprpqk43mwcy4yfsqdq5g9kxy7fqd9h8vmmfvdjscqzzsxqyz5vqsp5uhpjt36rj75pl7jq2sshaukzfkt7uulj456s4mh7uy7l6vx7lvxs9qxpqysgqedwz08acmqwtk8g4vkwm2w78suwt2qyzz6jkkwcgrjm3r3hs6fskyhvud4fan3keru7emjm8ygqpcrwtlmhfjfmer3afs5hhwamgr4cqtactdq";
1695 let bolt11 = Bolt11Invoice::from_str(bolt11_str).unwrap();
1696
1697 let melt_quote = MeltQuote::new(
1698 Some(QuoteId::new()),
1699 MeltPaymentRequest::Bolt11 { bolt11 },
1700 CurrencyUnit::Sat,
1701 Amount::new(100, CurrencyUnit::Sat),
1702 Amount::new(2, CurrencyUnit::Sat),
1703 unix_time() + 3600,
1704 None,
1705 None,
1706 PaymentMethod::BOLT11,
1707 None,
1708 None,
1709 );
1710
1711 let response: crate::nuts::MeltQuoteCustomResponse<QuoteId> = melt_quote.clone().into();
1712
1713 assert_eq!(response.quote, melt_quote.id);
1714 assert_eq!(response.request, None);
1715 }
1716
1717 #[test]
1718 fn test_melt_quote_to_custom_response_with_bolt12_request() {
1719 use bitcoin::secp256k1::{PublicKey as Secp256k1PublicKey, Secp256k1, SecretKey};
1720 use lightning::offers::offer::OfferBuilder;
1721 let secp = Secp256k1::new();
1722 let secret_key = SecretKey::from_slice(&[0xcd; 32]).unwrap();
1723 let pubkey = Secp256k1PublicKey::from_secret_key(&secp, &secret_key);
1724 let offer = OfferBuilder::new(pubkey).build().unwrap();
1725
1726 let melt_quote = MeltQuote::new(
1727 Some(QuoteId::new()),
1728 MeltPaymentRequest::Bolt12 {
1729 offer: Box::new(offer),
1730 },
1731 CurrencyUnit::Sat,
1732 Amount::new(100, CurrencyUnit::Sat),
1733 Amount::new(2, CurrencyUnit::Sat),
1734 unix_time() + 3600,
1735 None,
1736 None,
1737 PaymentMethod::BOLT12,
1738 None,
1739 None,
1740 );
1741
1742 let response: crate::nuts::MeltQuoteCustomResponse<QuoteId> = melt_quote.clone().into();
1743
1744 assert_eq!(response.quote, melt_quote.id);
1745 assert_eq!(response.request, None);
1746 }
1747
1748 fn dummy_mint_keyset_info(final_expiry: Option<u64>) -> MintKeySetInfo {
1749 use std::str::FromStr;
1750 MintKeySetInfo {
1751 id: Id::from_str("009a1f293253e41e").unwrap(),
1752 unit: CurrencyUnit::Sat,
1753 active: true,
1754 valid_from: 0,
1755 derivation_path: "m/0'/0'/0'".parse().unwrap(),
1756 derivation_path_index: Some(0),
1757 amounts: vec![1, 2, 4, 8, 16, 32, 64, 128, 256, 512],
1758 input_fee_ppk: 0,
1759 final_expiry,
1760 issuer_version: None,
1761 }
1762 }
1763
1764 #[test]
1765 fn test_is_expired_none() {
1766 let info = dummy_mint_keyset_info(None);
1767 assert!(!info.is_expired());
1768 }
1769
1770 #[test]
1771 fn test_is_expired_far_future() {
1772 let info = dummy_mint_keyset_info(Some(unix_time() + 1_000_000));
1773 assert!(!info.is_expired());
1774 }
1775
1776 #[test]
1777 fn test_is_expired_exactly_now_is_not_expired() {
1778 let info = dummy_mint_keyset_info(Some(unix_time()));
1780 assert!(!info.is_expired());
1781 }
1782
1783 #[test]
1784 fn test_is_expired_one_second_ago() {
1785 let info = dummy_mint_keyset_info(Some(unix_time() - 1));
1786 assert!(info.is_expired());
1787 }
1788
1789 #[test]
1790 fn test_is_expired_zero() {
1791 let info = dummy_mint_keyset_info(Some(0));
1792 assert!(info.is_expired());
1793 }
1794
1795 #[test]
1796 fn test_melt_quote_into_response_onchain() {
1797 let address = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq";
1798 let mut melt_quote = MeltQuote::new(
1799 Some(QuoteId::new()),
1800 MeltPaymentRequest::Onchain {
1801 address: address.to_string(),
1802 },
1803 CurrencyUnit::Sat,
1804 Amount::new(5_000, CurrencyUnit::Sat),
1805 Amount::new(250, CurrencyUnit::Sat),
1806 unix_time() + 3600,
1807 None,
1808 None,
1809 PaymentMethod::Known(cashu::nuts::nut00::KnownMethod::Onchain),
1810 None,
1811 Some(6),
1812 );
1813
1814 melt_quote.payment_proof =
1816 Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:1".to_string());
1817 melt_quote.state = MeltQuoteState::Paid;
1818
1819 let expected_id = melt_quote.id.clone();
1820 let expected_amount: Amount = melt_quote.amount().into();
1821 let expected_fee_options = melt_quote.fee_options().to_vec();
1822 let expected_expiry = melt_quote.expiry;
1823 let expected_state = melt_quote.state;
1824 let expected_outpoint = melt_quote.payment_proof.clone();
1825
1826 let response = melt_quote.into_response(None);
1827 match response {
1828 crate::MeltQuoteResponse::Onchain(r) => {
1829 assert_eq!(r.quote, expected_id);
1830 assert_eq!(r.request, address);
1831 assert_eq!(r.amount, expected_amount);
1832 assert_eq!(r.unit, CurrencyUnit::Sat);
1833 assert_eq!(r.fee_options, expected_fee_options);
1834 assert_eq!(r.selected_fee_index, None);
1835 assert_eq!(r.state, expected_state);
1836 assert_eq!(r.expiry, expected_expiry);
1837 assert_eq!(r.outpoint, expected_outpoint);
1838 assert_eq!(r.change, None);
1839 }
1840 _ => panic!("expected MeltQuoteResponse::Onchain variant"),
1841 }
1842 }
1843
1844 #[test]
1845 fn test_mint_quote_onchain_response_converts_zero_expiry_to_none() {
1846 let pubkey = PublicKey::from_hex(
1847 "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
1848 )
1849 .unwrap();
1850 let quote_id = QuoteId::new();
1851 let now = unix_time();
1852 let mint_quote = MintQuote::new(
1853 Some(quote_id.clone()),
1854 "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh".to_string(),
1855 CurrencyUnit::Sat,
1856 None,
1857 0,
1858 PaymentIdentifier::QuoteId(quote_id.clone()),
1859 Some(pubkey),
1860 Amount::new(10_000, CurrencyUnit::Sat),
1861 Amount::new(1_000, CurrencyUnit::Sat),
1862 PaymentMethod::Known(cashu::nuts::nut00::KnownMethod::Onchain),
1863 now,
1864 now,
1865 vec![],
1866 vec![],
1867 None,
1868 );
1869
1870 let response = MintQuoteOnchainResponse::try_from(mint_quote).unwrap();
1871
1872 assert_eq!(response.quote, quote_id);
1873 assert_eq!(response.expiry, None);
1874 assert_eq!(response.pubkey, pubkey);
1875 assert_eq!(response.amount_paid, Amount::from(10_000));
1876 assert_eq!(response.amount_issued, Amount::from(1_000));
1877 }
1878
1879 fn dummy_bolt12_mint_quote(expiry: u64) -> (MintQuote, QuoteId, PublicKey) {
1880 let pubkey = PublicKey::from_hex(
1881 "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
1882 )
1883 .expect("test pubkey must parse");
1884 let quote_id = QuoteId::new();
1885 let now = unix_time();
1886 let mint_quote = MintQuote::new(
1887 Some(quote_id.clone()),
1888 "lno1testoffer".to_string(),
1889 CurrencyUnit::Sat,
1890 Some(Amount::new(10_000, CurrencyUnit::Sat)),
1891 expiry,
1892 PaymentIdentifier::QuoteId(quote_id.clone()),
1893 Some(pubkey),
1894 Amount::new(10_000, CurrencyUnit::Sat),
1895 Amount::new(1_000, CurrencyUnit::Sat),
1896 PaymentMethod::BOLT12,
1897 now,
1898 now,
1899 vec![],
1900 vec![],
1901 None,
1902 );
1903
1904 (mint_quote, quote_id, pubkey)
1905 }
1906
1907 #[test]
1908 fn test_mint_quote_bolt12_response_converts_zero_expiry_to_none() {
1909 let (mint_quote, quote_id, pubkey) = dummy_bolt12_mint_quote(0);
1910
1911 let response: MintQuoteBolt12Response<QuoteId> =
1912 MintQuoteBolt12Response::try_from(mint_quote).unwrap();
1913
1914 assert_eq!(response.quote, quote_id);
1915 assert_eq!(response.expiry, None);
1916 assert_eq!(response.pubkey, pubkey);
1917 assert_eq!(response.amount_paid, Amount::from(10_000));
1918 assert_eq!(response.amount_issued, Amount::from(1_000));
1919 }
1920
1921 #[test]
1922 fn test_mint_quote_bolt12_response_preserves_nonzero_expiry() {
1923 let expiry = unix_time() + 3600;
1924 let (mint_quote, quote_id, _) = dummy_bolt12_mint_quote(expiry);
1925
1926 let response: MintQuoteBolt12Response<QuoteId> =
1927 MintQuoteBolt12Response::try_from(mint_quote).unwrap();
1928
1929 assert_eq!(response.quote, quote_id);
1930 assert_eq!(response.expiry, Some(expiry));
1931 }
1932
1933 #[test]
1934 fn test_mint_quote_response_bolt12_converts_zero_expiry_to_none() {
1935 let (mint_quote, quote_id, pubkey) = dummy_bolt12_mint_quote(0);
1936
1937 let response = MintQuoteResponse::try_from(mint_quote).unwrap();
1938
1939 match response {
1940 MintQuoteResponse::Bolt12(response) => {
1941 assert_eq!(response.quote, quote_id);
1942 assert_eq!(response.expiry, None);
1943 assert_eq!(response.pubkey, pubkey);
1944 }
1945 _ => panic!("expected MintQuoteResponse::Bolt12 variant"),
1946 }
1947 }
1948
1949 #[test]
1950 fn test_melt_quote_into_response_onchain_includes_change() {
1951 let address = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq";
1952 let melt_quote = MeltQuote::new(
1953 Some(QuoteId::new()),
1954 MeltPaymentRequest::Onchain {
1955 address: address.to_string(),
1956 },
1957 CurrencyUnit::Sat,
1958 Amount::new(1_000, CurrencyUnit::Sat),
1959 Amount::new(10, CurrencyUnit::Sat),
1960 unix_time() + 3600,
1961 None,
1962 None,
1963 PaymentMethod::Known(cashu::nuts::nut00::KnownMethod::Onchain),
1964 None,
1965 Some(3),
1966 );
1967
1968 let response = melt_quote.into_response(Some(vec![]));
1969 match response {
1970 crate::MeltQuoteResponse::Onchain(r) => assert_eq!(r.change, Some(vec![])),
1971 _ => panic!("expected MeltQuoteResponse::Onchain variant"),
1972 }
1973 }
1974
1975 #[test]
1976 fn validate_onchain_fee_options_rejects_empty() {
1977 let err = validate_onchain_fee_options(&[]).expect_err("empty must be rejected");
1978 assert!(matches!(err, crate::Error::OnchainFeeOptionsEmpty));
1979 }
1980
1981 #[test]
1982 fn validate_onchain_fee_options_allows_duplicate_fee_index() {
1983 let options = [
1984 MeltQuoteOnchainFeeOption {
1985 fee_index: 10,
1986 fee_reserve: Amount::from(10),
1987 estimated_blocks: 3,
1988 },
1989 MeltQuoteOnchainFeeOption {
1990 fee_index: 10,
1991 fee_reserve: Amount::from(20),
1992 estimated_blocks: 6,
1993 },
1994 ];
1995 validate_onchain_fee_options(&options).expect("duplicate fee_index must be allowed");
1996 }
1997
1998 #[test]
1999 fn validate_onchain_fee_options_allows_duplicate_estimated_blocks() {
2000 let options = [
2003 MeltQuoteOnchainFeeOption {
2004 fee_index: 20,
2005 fee_reserve: Amount::from(10),
2006 estimated_blocks: 3,
2007 },
2008 MeltQuoteOnchainFeeOption {
2009 fee_index: 1,
2010 fee_reserve: Amount::from(20),
2011 estimated_blocks: 3,
2012 },
2013 ];
2014 validate_onchain_fee_options(&options).expect("duplicate blocks must be allowed");
2015 }
2016
2017 #[test]
2018 fn validate_onchain_fee_options_allows_duplicate_fee_reserve() {
2019 let options = [
2022 MeltQuoteOnchainFeeOption {
2023 fee_index: 0,
2024 fee_reserve: Amount::from(42),
2025 estimated_blocks: 1,
2026 },
2027 MeltQuoteOnchainFeeOption {
2028 fee_index: 1,
2029 fee_reserve: Amount::from(42),
2030 estimated_blocks: 6,
2031 },
2032 ];
2033 validate_onchain_fee_options(&options).expect("duplicate fee must be allowed");
2034 }
2035
2036 #[test]
2037 fn validate_onchain_fee_options_accepts_well_formed() {
2038 let options = [
2039 MeltQuoteOnchainFeeOption {
2040 fee_index: 0,
2041 fee_reserve: Amount::from(500),
2042 estimated_blocks: 1,
2043 },
2044 MeltQuoteOnchainFeeOption {
2045 fee_index: 1,
2046 fee_reserve: Amount::from(200),
2047 estimated_blocks: 6,
2048 },
2049 MeltQuoteOnchainFeeOption {
2050 fee_index: 2,
2051 fee_reserve: Amount::from(50),
2052 estimated_blocks: 144,
2053 },
2054 ];
2055 validate_onchain_fee_options(&options).expect("well-formed must validate");
2056 }
2057
2058 #[test]
2059 fn new_onchain_rejects_empty_fee_options() {
2060 let err = MeltQuote::new_onchain(
2061 None,
2062 MeltPaymentRequest::Onchain {
2063 address: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq".to_string(),
2064 },
2065 CurrencyUnit::Sat,
2066 Amount::new(1_000, CurrencyUnit::Sat),
2067 unix_time() + 3600,
2068 None,
2069 None,
2070 vec![],
2071 )
2072 .expect_err("empty fee_options must be rejected");
2073 assert!(matches!(err, crate::Error::OnchainFeeOptionsEmpty));
2074 }
2075
2076 #[test]
2077 fn new_onchain_initializes_reserve_to_cheapest_tier() {
2078 let options = vec![
2081 MeltQuoteOnchainFeeOption {
2082 fee_index: 10,
2083 fee_reserve: Amount::from(500),
2084 estimated_blocks: 1,
2085 },
2086 MeltQuoteOnchainFeeOption {
2087 fee_index: 30,
2088 fee_reserve: Amount::from(50),
2089 estimated_blocks: 144,
2090 },
2091 MeltQuoteOnchainFeeOption {
2092 fee_index: 20,
2093 fee_reserve: Amount::from(200),
2094 estimated_blocks: 6,
2095 },
2096 ];
2097 let quote = MeltQuote::new_onchain(
2098 None,
2099 MeltPaymentRequest::Onchain {
2100 address: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq".to_string(),
2101 },
2102 CurrencyUnit::Sat,
2103 Amount::new(10_000, CurrencyUnit::Sat),
2104 unix_time() + 3600,
2105 None,
2106 None,
2107 options.clone(),
2108 )
2109 .expect("well-formed quote must construct");
2110
2111 assert_eq!(quote.fee_reserve().value(), 50);
2112 assert_eq!(quote.estimated_blocks, Some(144));
2113 assert_eq!(quote.selected_fee_index, None);
2114 let returned: Vec<u32> = quote.fee_options().iter().map(|o| o.fee_index).collect();
2115 assert_eq!(returned, vec![10, 30, 20]);
2116 }
2117
2118 #[test]
2119 fn new_onchain_preserves_duplicate_backend_fee_index() {
2120 let options = vec![
2121 MeltQuoteOnchainFeeOption {
2122 fee_index: 7,
2123 fee_reserve: Amount::from(500),
2124 estimated_blocks: 1,
2125 },
2126 MeltQuoteOnchainFeeOption {
2127 fee_index: 7,
2128 fee_reserve: Amount::from(200),
2129 estimated_blocks: 6,
2130 },
2131 ];
2132 let quote = MeltQuote::new_onchain(
2133 None,
2134 MeltPaymentRequest::Onchain {
2135 address: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq".to_string(),
2136 },
2137 CurrencyUnit::Sat,
2138 Amount::new(10_000, CurrencyUnit::Sat),
2139 unix_time() + 3600,
2140 None,
2141 None,
2142 options,
2143 )
2144 .expect("duplicate backend fee_index must be preserved");
2145
2146 let returned: Vec<u32> = quote.fee_options().iter().map(|o| o.fee_index).collect();
2147 assert_eq!(returned, vec![7, 7]);
2148 }
2149
2150 #[test]
2151 fn select_onchain_fee_option_leaves_fee_options_untouched() {
2152 let options = vec![
2153 MeltQuoteOnchainFeeOption {
2154 fee_index: 1,
2155 fee_reserve: Amount::from(500),
2156 estimated_blocks: 1,
2157 },
2158 MeltQuoteOnchainFeeOption {
2159 fee_index: 2,
2160 fee_reserve: Amount::from(200),
2161 estimated_blocks: 6,
2162 },
2163 ];
2164 let mut quote = MeltQuote::new_onchain(
2165 None,
2166 MeltPaymentRequest::Onchain {
2167 address: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq".to_string(),
2168 },
2169 CurrencyUnit::Sat,
2170 Amount::new(10_000, CurrencyUnit::Sat),
2171 unix_time() + 3600,
2172 None,
2173 None,
2174 options.clone(),
2175 )
2176 .unwrap();
2177
2178 let before = quote.fee_options().to_vec();
2179 quote
2180 .select_onchain_fee_option(1)
2181 .expect("selecting a known fee_index must succeed");
2182
2183 assert_eq!(
2184 quote.fee_options(),
2185 before.as_slice(),
2186 "fee_options is fixed for the lifetime of the quote and must not \
2187 mutate on selection"
2188 );
2189 assert_eq!(quote.selected_fee_index, Some(1));
2190 assert_eq!(quote.estimated_blocks, Some(1));
2191 assert_eq!(quote.fee_reserve().value(), 500);
2192 }
2193
2194 #[test]
2195 fn select_onchain_fee_option_unknown_index_rejected() {
2196 let options = vec![MeltQuoteOnchainFeeOption {
2197 fee_index: 0,
2198 fee_reserve: Amount::from(500),
2199 estimated_blocks: 1,
2200 }];
2201 let mut quote = MeltQuote::new_onchain(
2202 None,
2203 MeltPaymentRequest::Onchain {
2204 address: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq".to_string(),
2205 },
2206 CurrencyUnit::Sat,
2207 Amount::new(10_000, CurrencyUnit::Sat),
2208 unix_time() + 3600,
2209 None,
2210 None,
2211 options,
2212 )
2213 .unwrap();
2214
2215 match quote
2216 .select_onchain_fee_option(7)
2217 .expect_err("unknown fee_index must be rejected")
2218 {
2219 crate::Error::OnchainFeeIndexNotFound { index: 7 } => {}
2220 other => panic!("unexpected error: {other:?}"),
2221 }
2222 }
2223
2224 #[test]
2225 fn from_db_preserves_duplicate_onchain_fee_options() {
2226 let options = vec![
2227 MeltQuoteOnchainFeeOption {
2228 fee_index: 0,
2229 fee_reserve: Amount::from(100),
2230 estimated_blocks: 6,
2231 },
2232 MeltQuoteOnchainFeeOption {
2233 fee_index: 0,
2234 fee_reserve: Amount::from(200),
2235 estimated_blocks: 6,
2236 },
2237 ];
2238 let quote = MeltQuote::from_db(
2239 QuoteId::new(),
2240 CurrencyUnit::Sat,
2241 MeltPaymentRequest::Onchain {
2242 address: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq".to_string(),
2243 },
2244 10_000,
2245 100,
2246 MeltQuoteState::Unpaid,
2247 unix_time() + 3600,
2248 None,
2249 None,
2250 None,
2251 unix_time(),
2252 None,
2253 PaymentMethod::Known(cashu::nuts::nut00::KnownMethod::Onchain),
2254 None,
2255 None,
2256 options,
2257 None,
2258 )
2259 .expect("duplicate onchain fee_options on reload must be preserved");
2260
2261 let returned: Vec<u32> = quote.fee_options().iter().map(|o| o.fee_index).collect();
2262 assert_eq!(returned, vec![0, 0]);
2263 }
2264
2265 #[test]
2266 fn test_custom_mint_quote_response_surfaces_extra_json() {
2267 let extra = serde_json::json!({"payment_url": "https://example.com/pay", "ref": 42});
2268 let now = unix_time();
2269 let quote = MintQuote::new(
2270 Some(QuoteId::new()),
2271 "custom://request".to_string(),
2272 CurrencyUnit::Sat,
2273 Some(Amount::new(500, CurrencyUnit::Sat)),
2274 unix_time() + 3600,
2275 PaymentIdentifier::Label("test".to_string()),
2276 None,
2277 Amount::new(0, CurrencyUnit::Sat),
2278 Amount::new(0, CurrencyUnit::Sat),
2279 PaymentMethod::Custom("custom".to_string()),
2280 now,
2281 now,
2282 Vec::new(),
2283 Vec::new(),
2284 Some(extra.clone()),
2285 );
2286
2287 let response: MintQuoteResponse<QuoteId> = quote.try_into().expect("conversion succeeds");
2288 match response {
2289 MintQuoteResponse::Custom { response, .. } => {
2290 assert_eq!(response.extra, extra);
2291 }
2292 other => panic!("expected Custom variant, got {:?}", other),
2293 }
2294 }
2295
2296 #[test]
2297 fn from_db_rejects_empty_onchain_fee_options() {
2298 let err = MeltQuote::from_db(
2299 QuoteId::new(),
2300 CurrencyUnit::Sat,
2301 MeltPaymentRequest::Onchain {
2302 address: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq".to_string(),
2303 },
2304 10_000,
2305 100,
2306 MeltQuoteState::Unpaid,
2307 unix_time() + 3600,
2308 None,
2309 None,
2310 None,
2311 unix_time(),
2312 None,
2313 PaymentMethod::Known(cashu::nuts::nut00::KnownMethod::Onchain),
2314 None,
2315 Some(6),
2316 Vec::new(),
2317 None,
2318 )
2319 .expect_err("empty onchain fee_options on reload must be rejected");
2320 assert!(matches!(err, crate::Error::OnchainFeeOptionsEmpty));
2321 }
2322}