1use std::fmt;
6#[cfg(feature = "mint")]
7use std::str::FromStr;
8
9use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, Visitor};
10use serde::ser::{SerializeStruct, Serializer};
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod};
15use crate::nut00::KnownMethod;
16#[cfg(feature = "mint")]
17use crate::quote_id::QuoteId;
18#[cfg(feature = "mint")]
19use crate::quote_id::QuoteIdError;
20use crate::util::serde_helpers::deserialize_empty_string_as_none;
21use crate::{Amount, PublicKey};
22
23#[derive(Debug, Error)]
25pub enum Error {
26 #[error("Unknown Quote State")]
28 UnknownState,
29 #[error("Amount overflow")]
31 AmountOverflow,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(bound = "Q: Serialize + DeserializeOwned")]
37pub struct MintRequest<Q> {
38 pub quote: Q,
40 pub outputs: Vec<BlindedMessage>,
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub signature: Option<String>,
45}
46
47#[cfg(feature = "mint")]
48impl TryFrom<MintRequest<String>> for MintRequest<QuoteId> {
49 type Error = QuoteIdError;
50
51 fn try_from(value: MintRequest<String>) -> Result<Self, Self::Error> {
52 Ok(Self {
53 quote: QuoteId::from_str(&value.quote)?,
54 outputs: value.outputs,
55 signature: value.signature,
56 })
57 }
58}
59
60impl<Q> MintRequest<Q> {
61 pub fn total_amount(&self) -> Result<Amount, Error> {
63 Amount::try_sum(
64 self.outputs
65 .iter()
66 .map(|BlindedMessage { amount, .. }| *amount),
67 )
68 .map_err(|_| Error::AmountOverflow)
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct MintResponse {
75 pub signatures: Vec<BlindSignature>,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Hash)]
81pub struct MintMethodSettings {
82 pub method: PaymentMethod,
84 pub unit: CurrencyUnit,
86 pub min_amount: Option<Amount>,
88 pub max_amount: Option<Amount>,
90 pub options: Option<MintMethodOptions>,
92}
93
94impl Serialize for MintMethodSettings {
95 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
96 where
97 S: Serializer,
98 {
99 let mut num_fields = 2; if self.min_amount.is_some() {
101 num_fields += 1;
102 }
103 if self.max_amount.is_some() {
104 num_fields += 1;
105 }
106
107 let mut description_in_top_level = false;
108 let mut onchain_confirmations: Option<u32> = None;
109
110 match &self.options {
111 Some(MintMethodOptions::Bolt11 { description }) if *description => {
112 num_fields += 1;
113 description_in_top_level = true;
114 }
115 Some(MintMethodOptions::Onchain { confirmations }) => {
116 onchain_confirmations = Some(*confirmations);
117 num_fields += 1; }
119 _ => {}
120 }
121
122 let mut state = serializer.serialize_struct("MintMethodSettings", num_fields)?;
123
124 state.serialize_field("method", &self.method)?;
125 state.serialize_field("unit", &self.unit)?;
126
127 if let Some(min_amount) = &self.min_amount {
128 state.serialize_field("min_amount", min_amount)?;
129 }
130
131 if let Some(max_amount) = &self.max_amount {
132 state.serialize_field("max_amount", max_amount)?;
133 }
134
135 if description_in_top_level {
137 state.serialize_field("description", &true)?;
138 }
139
140 if let Some(confirmations) = onchain_confirmations {
142 #[derive(Serialize)]
143 struct OnchainOptions {
144 confirmations: u32,
145 }
146 state.serialize_field("options", &OnchainOptions { confirmations })?;
147 }
148
149 state.end()
150 }
151}
152
153struct MintMethodSettingsVisitor;
154
155impl<'de> Visitor<'de> for MintMethodSettingsVisitor {
156 type Value = MintMethodSettings;
157
158 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
159 formatter.write_str("a MintMethodSettings structure")
160 }
161
162 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
163 where
164 M: MapAccess<'de>,
165 {
166 let mut method: Option<PaymentMethod> = None;
167 let mut unit: Option<CurrencyUnit> = None;
168 let mut min_amount: Option<Amount> = None;
169 let mut max_amount: Option<Amount> = None;
170 let mut description: Option<bool> = None;
171 let mut confirmations: Option<u32> = None;
172
173 while let Some(key) = map.next_key::<String>()? {
174 match key.as_str() {
175 "method" => {
176 if method.is_some() {
177 return Err(de::Error::duplicate_field("method"));
178 }
179 method = Some(map.next_value()?);
180 }
181 "unit" => {
182 if unit.is_some() {
183 return Err(de::Error::duplicate_field("unit"));
184 }
185 unit = Some(map.next_value()?);
186 }
187 "min_amount" => {
188 if min_amount.is_some() {
189 return Err(de::Error::duplicate_field("min_amount"));
190 }
191 min_amount = Some(map.next_value()?);
192 }
193 "max_amount" => {
194 if max_amount.is_some() {
195 return Err(de::Error::duplicate_field("max_amount"));
196 }
197 max_amount = Some(map.next_value()?);
198 }
199 "description" => {
200 if description.is_some() {
201 return Err(de::Error::duplicate_field("description"));
202 }
203 description = Some(map.next_value()?);
204 }
205 "confirmations" => {
206 return Err(de::Error::unknown_field("confirmations", &["options"]));
207 }
208 "options" => {
209 let options: Option<MintMethodOptions> = map.next_value()?;
212
213 if let Some(MintMethodOptions::Bolt11 {
214 description: desc_from_options,
215 }) = options
216 {
217 if description.is_none() {
219 description = Some(desc_from_options);
220 }
221 }
222
223 if let Some(MintMethodOptions::Onchain {
224 confirmations: conf_from_options,
225 }) = options
226 {
227 confirmations = Some(conf_from_options);
228 }
229 }
230 _ => {
231 let _: serde::de::IgnoredAny = map.next_value()?;
233 }
234 }
235 }
236
237 let method = method.ok_or_else(|| de::Error::missing_field("method"))?;
238 let unit = unit.ok_or_else(|| de::Error::missing_field("unit"))?;
239
240 let options = if method == PaymentMethod::Known(KnownMethod::Bolt11) {
242 description.map(|desc| MintMethodOptions::Bolt11 { description: desc })
243 } else if method == PaymentMethod::Known(KnownMethod::Onchain) {
244 confirmations.map(|conf| MintMethodOptions::Onchain {
245 confirmations: conf,
246 })
247 } else {
248 None
249 };
250
251 Ok(MintMethodSettings {
252 method,
253 unit,
254 min_amount,
255 max_amount,
256 options,
257 })
258 }
259}
260
261impl<'de> Deserialize<'de> for MintMethodSettings {
262 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
263 where
264 D: Deserializer<'de>,
265 {
266 deserializer.deserialize_map(MintMethodSettingsVisitor)
267 }
268}
269
270#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
272#[serde(untagged)]
273pub enum MintMethodOptions {
274 Bolt11 {
276 description: bool,
278 },
279 Onchain {
281 confirmations: u32,
283 },
284 Custom {},
286}
287
288#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
290pub struct Settings {
291 pub methods: Vec<MintMethodSettings>,
293 pub disabled: bool,
295}
296
297impl Settings {
298 pub fn new(methods: Vec<MintMethodSettings>, disabled: bool) -> Self {
300 Self { methods, disabled }
301 }
302
303 pub fn get_settings(
305 &self,
306 unit: &CurrencyUnit,
307 method: &PaymentMethod,
308 ) -> Option<MintMethodSettings> {
309 for method_settings in self.methods.iter() {
310 if method_settings.method.eq(method) && method_settings.unit.eq(unit) {
311 return Some(method_settings.clone());
312 }
313 }
314
315 None
316 }
317
318 pub fn remove_settings(
320 &mut self,
321 unit: &CurrencyUnit,
322 method: &PaymentMethod,
323 ) -> Option<MintMethodSettings> {
324 self.methods
325 .iter()
326 .position(|settings| &settings.method == method && &settings.unit == unit)
327 .map(|index| self.methods.remove(index))
328 }
329
330 pub fn supported_methods(&self) -> Vec<&PaymentMethod> {
332 self.methods.iter().map(|a| &a.method).collect()
333 }
334
335 pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
337 self.methods.iter().map(|s| &s.unit).collect()
338 }
339}
340
341#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
349pub struct MintQuoteCustomRequest {
350 pub amount: Amount,
352 pub unit: CurrencyUnit,
354 #[serde(skip_serializing_if = "Option::is_none")]
356 pub description: Option<String>,
357 #[serde(skip_serializing_if = "Option::is_none")]
359 pub pubkey: Option<PublicKey>,
360 #[serde(flatten, default, skip_serializing_if = "serde_json::Value::is_null")]
367 pub extra: serde_json::Value,
368}
369
370#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
395#[serde(bound = "Q: Serialize + for<'a> Deserialize<'a>")]
396pub struct MintQuoteCustomResponse<Q> {
397 pub quote: Q,
399 pub request: String,
401 pub amount: Option<Amount>,
403 pub amount_paid: Amount,
405 pub amount_issued: Amount,
407 pub unit: Option<CurrencyUnit>,
409 pub expiry: Option<u64>,
411 #[serde(
413 default,
414 skip_serializing_if = "Option::is_none",
415 deserialize_with = "deserialize_empty_string_as_none"
416 )]
417 pub pubkey: Option<PublicKey>,
418 #[serde(flatten, default, skip_serializing_if = "serde_json::Value::is_null")]
423 pub extra: serde_json::Value,
424}
425
426#[cfg(feature = "mint")]
427impl<Q: ToString> MintQuoteCustomResponse<Q> {
428 pub fn to_string_id(&self) -> MintQuoteCustomResponse<String> {
430 MintQuoteCustomResponse {
431 quote: self.quote.to_string(),
432 request: self.request.clone(),
433 amount: self.amount,
434 amount_paid: self.amount_paid,
435 amount_issued: self.amount_issued,
436 unit: self.unit.clone(),
437 expiry: self.expiry,
438 pubkey: self.pubkey,
439 extra: self.extra.clone(),
440 }
441 }
442}
443
444#[cfg(feature = "mint")]
445impl From<MintQuoteCustomResponse<QuoteId>> for MintQuoteCustomResponse<String> {
446 fn from(value: MintQuoteCustomResponse<QuoteId>) -> Self {
447 Self {
448 quote: value.quote.to_string(),
449 request: value.request,
450 amount: value.amount,
451 amount_paid: value.amount_paid,
452 amount_issued: value.amount_issued,
453 unit: value.unit,
454 expiry: value.expiry,
455 pubkey: value.pubkey,
456 extra: value.extra,
457 }
458 }
459}
460#[cfg(test)]
461mod tests {
462 use std::fmt;
463
464 use serde::ser::{Impossible, SerializeStruct, Serializer};
465 use serde_json::{from_str, json, to_string};
466
467 use super::*;
468 use crate::nut00::KnownMethod;
469
470 #[derive(Debug)]
471 struct FieldCountError(String);
472
473 impl fmt::Display for FieldCountError {
474 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
475 f.write_str(&self.0)
476 }
477 }
478
479 impl std::error::Error for FieldCountError {}
480
481 impl serde::ser::Error for FieldCountError {
482 fn custom<T>(msg: T) -> Self
483 where
484 T: fmt::Display,
485 {
486 Self(msg.to_string())
487 }
488 }
489
490 struct FieldCountSerializer;
491
492 struct FieldCountStruct {
493 declared: usize,
494 actual: usize,
495 }
496
497 impl Serializer for FieldCountSerializer {
498 type Ok = ();
499 type Error = FieldCountError;
500 type SerializeSeq = Impossible<Self::Ok, Self::Error>;
501 type SerializeTuple = Impossible<Self::Ok, Self::Error>;
502 type SerializeTupleStruct = Impossible<Self::Ok, Self::Error>;
503 type SerializeTupleVariant = Impossible<Self::Ok, Self::Error>;
504 type SerializeMap = Impossible<Self::Ok, Self::Error>;
505 type SerializeStruct = FieldCountStruct;
506 type SerializeStructVariant = Impossible<Self::Ok, Self::Error>;
507
508 fn serialize_bool(self, _v: bool) -> Result<Self::Ok, Self::Error> {
509 Err(FieldCountError("unsupported bool".to_string()))
510 }
511
512 fn serialize_i8(self, _v: i8) -> Result<Self::Ok, Self::Error> {
513 Err(FieldCountError("unsupported i8".to_string()))
514 }
515
516 fn serialize_i16(self, _v: i16) -> Result<Self::Ok, Self::Error> {
517 Err(FieldCountError("unsupported i16".to_string()))
518 }
519
520 fn serialize_i32(self, _v: i32) -> Result<Self::Ok, Self::Error> {
521 Err(FieldCountError("unsupported i32".to_string()))
522 }
523
524 fn serialize_i64(self, _v: i64) -> Result<Self::Ok, Self::Error> {
525 Err(FieldCountError("unsupported i64".to_string()))
526 }
527
528 fn serialize_u8(self, _v: u8) -> Result<Self::Ok, Self::Error> {
529 Err(FieldCountError("unsupported u8".to_string()))
530 }
531
532 fn serialize_u16(self, _v: u16) -> Result<Self::Ok, Self::Error> {
533 Err(FieldCountError("unsupported u16".to_string()))
534 }
535
536 fn serialize_u32(self, _v: u32) -> Result<Self::Ok, Self::Error> {
537 Err(FieldCountError("unsupported u32".to_string()))
538 }
539
540 fn serialize_u64(self, _v: u64) -> Result<Self::Ok, Self::Error> {
541 Err(FieldCountError("unsupported u64".to_string()))
542 }
543
544 fn serialize_f32(self, _v: f32) -> Result<Self::Ok, Self::Error> {
545 Err(FieldCountError("unsupported f32".to_string()))
546 }
547
548 fn serialize_f64(self, _v: f64) -> Result<Self::Ok, Self::Error> {
549 Err(FieldCountError("unsupported f64".to_string()))
550 }
551
552 fn serialize_char(self, _v: char) -> Result<Self::Ok, Self::Error> {
553 Err(FieldCountError("unsupported char".to_string()))
554 }
555
556 fn serialize_str(self, _v: &str) -> Result<Self::Ok, Self::Error> {
557 Err(FieldCountError("unsupported str".to_string()))
558 }
559
560 fn serialize_bytes(self, _v: &[u8]) -> Result<Self::Ok, Self::Error> {
561 Err(FieldCountError("unsupported bytes".to_string()))
562 }
563
564 fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
565 Err(FieldCountError("unsupported none".to_string()))
566 }
567
568 fn serialize_some<T>(self, _value: &T) -> Result<Self::Ok, Self::Error>
569 where
570 T: ?Sized + Serialize,
571 {
572 Err(FieldCountError("unsupported some".to_string()))
573 }
574
575 fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
576 Err(FieldCountError("unsupported unit".to_string()))
577 }
578
579 fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
580 Err(FieldCountError("unsupported unit struct".to_string()))
581 }
582
583 fn serialize_unit_variant(
584 self,
585 _name: &'static str,
586 _variant_index: u32,
587 _variant: &'static str,
588 ) -> Result<Self::Ok, Self::Error> {
589 Err(FieldCountError("unsupported unit variant".to_string()))
590 }
591
592 fn serialize_newtype_struct<T>(
593 self,
594 _name: &'static str,
595 _value: &T,
596 ) -> Result<Self::Ok, Self::Error>
597 where
598 T: ?Sized + Serialize,
599 {
600 Err(FieldCountError("unsupported newtype struct".to_string()))
601 }
602
603 fn serialize_newtype_variant<T>(
604 self,
605 _name: &'static str,
606 _variant_index: u32,
607 _variant: &'static str,
608 _value: &T,
609 ) -> Result<Self::Ok, Self::Error>
610 where
611 T: ?Sized + Serialize,
612 {
613 Err(FieldCountError("unsupported newtype variant".to_string()))
614 }
615
616 fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
617 Err(FieldCountError("unsupported seq".to_string()))
618 }
619
620 fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
621 Err(FieldCountError("unsupported tuple".to_string()))
622 }
623
624 fn serialize_tuple_struct(
625 self,
626 _name: &'static str,
627 _len: usize,
628 ) -> Result<Self::SerializeTupleStruct, Self::Error> {
629 Err(FieldCountError("unsupported tuple struct".to_string()))
630 }
631
632 fn serialize_tuple_variant(
633 self,
634 _name: &'static str,
635 _variant_index: u32,
636 _variant: &'static str,
637 _len: usize,
638 ) -> Result<Self::SerializeTupleVariant, Self::Error> {
639 Err(FieldCountError("unsupported tuple variant".to_string()))
640 }
641
642 fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
643 Err(FieldCountError("unsupported map".to_string()))
644 }
645
646 fn serialize_struct(
647 self,
648 _name: &'static str,
649 len: usize,
650 ) -> Result<Self::SerializeStruct, Self::Error> {
651 Ok(FieldCountStruct {
652 declared: len,
653 actual: 0,
654 })
655 }
656
657 fn serialize_struct_variant(
658 self,
659 _name: &'static str,
660 _variant_index: u32,
661 _variant: &'static str,
662 _len: usize,
663 ) -> Result<Self::SerializeStructVariant, Self::Error> {
664 Err(FieldCountError("unsupported struct variant".to_string()))
665 }
666 }
667
668 impl SerializeStruct for FieldCountStruct {
669 type Ok = ();
670 type Error = FieldCountError;
671
672 fn serialize_field<T>(&mut self, _key: &'static str, _value: &T) -> Result<(), Self::Error>
673 where
674 T: ?Sized + Serialize,
675 {
676 self.actual += 1;
677 Ok(())
678 }
679
680 fn end(self) -> Result<Self::Ok, Self::Error> {
681 if self.actual == self.declared {
682 Ok(())
683 } else {
684 Err(FieldCountError(format!(
685 "declared {} fields but serialized {}",
686 self.declared, self.actual
687 )))
688 }
689 }
690 }
691
692 fn assert_mint_method_settings_field_count(settings: &MintMethodSettings) {
693 settings.serialize(FieldCountSerializer).unwrap();
694 }
695
696 #[test]
697 fn test_mint_request_total_amount() {
698 let request: MintRequest<String> = from_str(
699 r#"{
700 "quote": "quote-id",
701 "outputs": [
702 {
703 "amount": 2,
704 "id": "00bfa73302d12ffd",
705 "B_": "038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39"
706 },
707 {
708 "amount": 4,
709 "id": "00bfa73302d12ffd",
710 "B_": "02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd"
711 }
712 ]
713 }"#,
714 )
715 .unwrap();
716
717 assert_eq!(request.total_amount().unwrap(), Amount::from(6));
718 }
719
720 #[test]
721 fn test_mint_method_settings_serialize_field_count() {
722 assert_mint_method_settings_field_count(&MintMethodSettings {
723 method: PaymentMethod::Known(KnownMethod::Bolt11),
724 unit: CurrencyUnit::Sat,
725 min_amount: Some(Amount::from(1)),
726 max_amount: Some(Amount::from(1000)),
727 options: Some(MintMethodOptions::Bolt11 { description: true }),
728 });
729
730 assert_mint_method_settings_field_count(&MintMethodSettings {
731 method: PaymentMethod::Known(KnownMethod::Bolt11),
732 unit: CurrencyUnit::Sat,
733 min_amount: Some(Amount::from(1)),
734 max_amount: None,
735 options: None,
736 });
737
738 assert_mint_method_settings_field_count(&MintMethodSettings {
739 method: PaymentMethod::Known(KnownMethod::Bolt11),
740 unit: CurrencyUnit::Sat,
741 min_amount: None,
742 max_amount: Some(Amount::from(1000)),
743 options: None,
744 });
745
746 assert_mint_method_settings_field_count(&MintMethodSettings {
747 method: PaymentMethod::Known(KnownMethod::Bolt11),
748 unit: CurrencyUnit::Sat,
749 min_amount: None,
750 max_amount: None,
751 options: Some(MintMethodOptions::Bolt11 { description: true }),
752 });
753
754 assert_mint_method_settings_field_count(&MintMethodSettings {
755 method: PaymentMethod::Known(KnownMethod::Onchain),
756 unit: CurrencyUnit::Sat,
757 min_amount: None,
758 max_amount: None,
759 options: Some(MintMethodOptions::Onchain { confirmations: 3 }),
760 });
761 }
762
763 #[test]
764 fn test_mint_method_settings_top_level_description() {
765 let json_str = r#"{
767 "method": "bolt11",
768 "unit": "sat",
769 "min_amount": 0,
770 "max_amount": 10000,
771 "description": true
772 }"#;
773
774 let settings: MintMethodSettings = from_str(json_str).unwrap();
776
777 assert_eq!(settings.method, PaymentMethod::Known(KnownMethod::Bolt11));
779 assert_eq!(settings.unit, CurrencyUnit::Sat);
780 assert_eq!(settings.min_amount, Some(Amount::from(0)));
781 assert_eq!(settings.max_amount, Some(Amount::from(10000)));
782
783 match settings.options {
784 Some(MintMethodOptions::Bolt11 { description }) => {
785 assert!(description);
786 }
787 _ => panic!("Expected Bolt11 options with description = true"),
788 }
789
790 let serialized = to_string(&settings).unwrap();
792 let parsed: serde_json::Value = from_str(&serialized).unwrap();
793
794 assert_eq!(parsed["description"], json!(true));
796 }
797
798 #[test]
799 fn test_mint_method_settings_does_not_serialize_false_description() {
800 let settings = MintMethodSettings {
801 method: PaymentMethod::Known(KnownMethod::Bolt11),
802 unit: CurrencyUnit::Sat,
803 min_amount: None,
804 max_amount: None,
805 options: Some(MintMethodOptions::Bolt11 { description: false }),
806 };
807
808 let serialized = to_string(&settings).unwrap();
809 let parsed: serde_json::Value = from_str(&serialized).unwrap();
810
811 assert_eq!(parsed["method"], json!("bolt11"));
812 assert!(parsed.get("description").is_none());
813 assert!(parsed.get("options").is_none());
814 }
815
816 #[test]
817 fn test_both_description_locations() {
818 let json_str = r#"{
820 "method": "bolt11",
821 "unit": "sat",
822 "min_amount": 0,
823 "max_amount": 10000,
824 "description": true,
825 "options": {
826 "description": false
827 }
828 }"#;
829
830 let settings: MintMethodSettings = from_str(json_str).unwrap();
832
833 match settings.options {
834 Some(MintMethodOptions::Bolt11 { description }) => {
835 assert!(description, "Top-level description should take precedence");
836 }
837 _ => panic!("Expected Bolt11 options with description = true"),
838 }
839 }
840
841 #[test]
842 fn custom_mint_quote_response_has_no_typed_state() {
843 let response = MintQuoteCustomResponse {
844 quote: "abc123".to_string(),
845 request: "paypal://pay?id=123".to_string(),
846 amount: Some(Amount::from(1000)),
847 amount_paid: Amount::ZERO,
848 amount_issued: Amount::ZERO,
849 unit: Some(CurrencyUnit::Sat),
850 expiry: Some(9999999),
851 pubkey: None,
852 extra: serde_json::Value::Null,
853 };
854
855 let serialized = to_string(&response).unwrap();
856 let parsed: serde_json::Value = from_str(&serialized).unwrap();
857
858 assert!(parsed.get("state").is_none());
859 assert_eq!(parsed["amount_paid"], json!(0));
860 assert_eq!(parsed["amount_issued"], json!(0));
861 }
862
863 #[test]
864 fn custom_mint_quote_response_flattens_extra_on_wire() {
865 let response = MintQuoteCustomResponse {
866 quote: "q1".to_string(),
867 request: "custom://pay".to_string(),
868 amount: Some(Amount::from(100)),
869 amount_paid: Amount::ZERO,
870 amount_issued: Amount::ZERO,
871 unit: Some(CurrencyUnit::Sat),
872 expiry: Some(9999),
873 pubkey: None,
874 extra: json!({"payment_url": "https://example.com", "ref": 42}),
875 };
876
877 let serialized = to_string(&response).expect("serializes");
878 let parsed: serde_json::Value = from_str(&serialized).expect("parses");
879
880 assert_eq!(parsed["payment_url"], json!("https://example.com"));
881 assert_eq!(parsed["ref"], json!(42));
882 assert!(
883 parsed.get("extra").is_none(),
884 "extra must be flattened, not nested under an 'extra' key"
885 );
886 }
887
888 #[test]
889 fn test_onchain_settings_nested_options_round_trip() {
890 let json_str = r#"{
892 "method": "onchain",
893 "unit": "sat",
894 "min_amount": 1000,
895 "max_amount": 1000000,
896 "options": {
897 "confirmations": 3
898 }
899 }"#;
900
901 let settings: MintMethodSettings = from_str(json_str).unwrap();
902
903 assert_eq!(settings.method, PaymentMethod::Known(KnownMethod::Onchain));
904 assert_eq!(settings.unit, CurrencyUnit::Sat);
905 assert_eq!(settings.min_amount, Some(Amount::from(1000)));
906 assert_eq!(settings.max_amount, Some(Amount::from(1000000)));
907
908 match settings.options {
909 Some(MintMethodOptions::Onchain { confirmations }) => {
910 assert_eq!(confirmations, 3);
911 }
912 _ => panic!("Expected Onchain options with confirmations = 3"),
913 }
914
915 let serialized = to_string(&settings).unwrap();
917 let parsed: serde_json::Value = from_str(&serialized).unwrap();
918
919 assert_eq!(parsed["method"], json!("onchain"));
920 assert_eq!(parsed["options"]["confirmations"], json!(3));
921 assert!(parsed.get("confirmations").is_none());
923 }
924
925 #[test]
926 fn test_onchain_settings_top_level_confirmations_rejected() {
927 let json_str = r#"{
928 "method": "onchain",
929 "unit": "sat",
930 "confirmations": 6
931 }"#;
932
933 let err = from_str::<MintMethodSettings>(json_str).unwrap_err();
934 assert!(err.to_string().contains("unknown field"));
935 }
936
937 #[test]
938 fn test_onchain_settings_top_level_and_nested_rejected() {
939 let json_str = r#"{
940 "method": "onchain",
941 "unit": "sat",
942 "confirmations": 6,
943 "options": {
944 "confirmations": 3
945 }
946 }"#;
947
948 let err = from_str::<MintMethodSettings>(json_str).unwrap_err();
949 assert!(err.to_string().contains("unknown field"));
950 }
951
952 #[test]
953 fn test_mint_method_settings_visitor_reports_expected_type() {
954 let err = from_str::<MintMethodSettings>("[]").unwrap_err();
955
956 assert!(err.to_string().contains("a MintMethodSettings structure"));
957 }
958
959 #[test]
960 fn test_get_settings_requires_exact_method_and_unit_match() {
961 let bolt11_msat = MintMethodSettings {
962 method: PaymentMethod::Known(KnownMethod::Bolt11),
963 unit: CurrencyUnit::Msat,
964 min_amount: Some(Amount::from(1)),
965 max_amount: None,
966 options: None,
967 };
968 let bolt12_sat = MintMethodSettings {
969 method: PaymentMethod::Known(KnownMethod::Bolt12),
970 unit: CurrencyUnit::Sat,
971 min_amount: Some(Amount::from(2)),
972 max_amount: None,
973 options: None,
974 };
975 let bolt11_sat = MintMethodSettings {
976 method: PaymentMethod::Known(KnownMethod::Bolt11),
977 unit: CurrencyUnit::Sat,
978 min_amount: Some(Amount::from(3)),
979 max_amount: None,
980 options: None,
981 };
982 let settings = Settings::new(
983 vec![bolt11_msat.clone(), bolt12_sat.clone(), bolt11_sat.clone()],
984 false,
985 );
986
987 assert_eq!(
988 settings.get_settings(&CurrencyUnit::Sat, &PaymentMethod::BOLT11),
989 Some(bolt11_sat)
990 );
991 assert_eq!(
992 settings.get_settings(
993 &CurrencyUnit::Msat,
994 &PaymentMethod::Known(KnownMethod::Bolt12)
995 ),
996 None
997 );
998 }
999
1000 #[test]
1001 fn test_supported_methods_and_units_preserve_configured_values() {
1002 let settings = Settings::new(
1003 vec![
1004 MintMethodSettings {
1005 method: PaymentMethod::Known(KnownMethod::Bolt11),
1006 unit: CurrencyUnit::Msat,
1007 min_amount: Some(Amount::from(1)),
1008 max_amount: None,
1009 options: None,
1010 },
1011 MintMethodSettings {
1012 method: PaymentMethod::Known(KnownMethod::Onchain),
1013 unit: CurrencyUnit::Eur,
1014 min_amount: None,
1015 max_amount: Some(Amount::from(100)),
1016 options: Some(MintMethodOptions::Onchain { confirmations: 3 }),
1017 },
1018 ],
1019 false,
1020 );
1021
1022 let methods = settings.supported_methods();
1023 assert_eq!(methods.len(), 2);
1024 assert_eq!(methods[0], &PaymentMethod::Known(KnownMethod::Bolt11));
1025 assert_eq!(methods[1], &PaymentMethod::Known(KnownMethod::Onchain));
1026
1027 let units = settings.supported_units();
1028 assert_eq!(units.len(), 2);
1029 assert_eq!(units[0], &CurrencyUnit::Msat);
1030 assert_eq!(units[1], &CurrencyUnit::Eur);
1031 }
1032
1033 #[test]
1034 fn test_remove_settings_requires_exact_method_and_unit_match() {
1035 let bolt11_msat = MintMethodSettings {
1036 method: PaymentMethod::Known(KnownMethod::Bolt11),
1037 unit: CurrencyUnit::Msat,
1038 min_amount: Some(Amount::from(1)),
1039 max_amount: None,
1040 options: None,
1041 };
1042 let bolt12_sat = MintMethodSettings {
1043 method: PaymentMethod::Known(KnownMethod::Bolt12),
1044 unit: CurrencyUnit::Sat,
1045 min_amount: Some(Amount::from(2)),
1046 max_amount: None,
1047 options: None,
1048 };
1049 let bolt11_sat = MintMethodSettings {
1050 method: PaymentMethod::Known(KnownMethod::Bolt11),
1051 unit: CurrencyUnit::Sat,
1052 min_amount: Some(Amount::from(3)),
1053 max_amount: None,
1054 options: None,
1055 };
1056 let mut settings = Settings::new(
1057 vec![bolt11_msat.clone(), bolt12_sat.clone(), bolt11_sat.clone()],
1058 false,
1059 );
1060
1061 assert_eq!(
1062 settings.remove_settings(&CurrencyUnit::Sat, &PaymentMethod::BOLT11),
1063 Some(bolt11_sat.clone())
1064 );
1065 assert_eq!(settings.methods, vec![bolt11_msat, bolt12_sat]);
1066 assert_eq!(
1067 settings.remove_settings(&CurrencyUnit::Sat, &PaymentMethod::BOLT11),
1068 None
1069 );
1070 }
1071}