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 method_name: Option<String>,
91 pub min_amount: Option<Amount>,
93 pub max_amount: Option<Amount>,
95 pub options: Option<MintMethodOptions>,
97}
98
99impl MintMethodSettings {
100 pub fn method_name(&self) -> String {
106 self.method_name
107 .clone()
108 .unwrap_or_else(|| self.method.derived_method_name())
109 }
110}
111
112impl Serialize for MintMethodSettings {
113 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
114 where
115 S: Serializer,
116 {
117 let mut num_fields = 2; if self.min_amount.is_some() {
119 num_fields += 1;
120 }
121 if self.max_amount.is_some() {
122 num_fields += 1;
123 }
124 if self.method_name.is_some() {
125 num_fields += 1;
126 }
127
128 let mut nested_description: Option<bool> = None;
129 let mut onchain_confirmations: Option<u32> = None;
130
131 match &self.options {
132 Some(MintMethodOptions::Bolt11 { description })
135 | Some(MintMethodOptions::Bolt12 { description }) => {
136 nested_description = Some(*description);
137 num_fields += 1; }
139 Some(MintMethodOptions::Onchain { confirmations }) => {
140 onchain_confirmations = Some(*confirmations);
141 num_fields += 1; }
143 _ => {}
144 }
145
146 let mut state = serializer.serialize_struct("MintMethodSettings", num_fields)?;
147
148 state.serialize_field("method", &self.method)?;
149 state.serialize_field("unit", &self.unit)?;
150
151 if let Some(method_name) = &self.method_name {
152 state.serialize_field("method_name", method_name)?;
153 }
154
155 if let Some(min_amount) = &self.min_amount {
156 state.serialize_field("min_amount", min_amount)?;
157 }
158
159 if let Some(max_amount) = &self.max_amount {
160 state.serialize_field("max_amount", max_amount)?;
161 }
162
163 if let Some(confirmations) = onchain_confirmations {
165 #[derive(Serialize)]
166 struct OnchainOptions {
167 confirmations: u32,
168 }
169 state.serialize_field("options", &OnchainOptions { confirmations })?;
170 }
171
172 if let Some(description) = nested_description {
174 #[derive(Serialize)]
175 struct DescriptionOptions {
176 description: bool,
177 }
178 state.serialize_field("options", &DescriptionOptions { description })?;
179 }
180
181 state.end()
182 }
183}
184
185struct MintMethodSettingsVisitor;
186
187impl<'de> Visitor<'de> for MintMethodSettingsVisitor {
188 type Value = MintMethodSettings;
189
190 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
191 formatter.write_str("a MintMethodSettings structure")
192 }
193
194 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
195 where
196 M: MapAccess<'de>,
197 {
198 let mut method: Option<PaymentMethod> = None;
199 let mut unit: Option<CurrencyUnit> = None;
200 let mut method_name: Option<String> = None;
201 let mut min_amount: Option<Amount> = None;
202 let mut max_amount: Option<Amount> = None;
203 let mut description: Option<bool> = None;
204 let mut confirmations: Option<u32> = None;
205
206 while let Some(key) = map.next_key::<String>()? {
207 match key.as_str() {
208 "method" => {
209 if method.is_some() {
210 return Err(de::Error::duplicate_field("method"));
211 }
212 method = Some(map.next_value()?);
213 }
214 "unit" => {
215 if unit.is_some() {
216 return Err(de::Error::duplicate_field("unit"));
217 }
218 unit = Some(map.next_value()?);
219 }
220 "method_name" => {
221 if method_name.is_some() {
222 return Err(de::Error::duplicate_field("method_name"));
223 }
224 method_name = map.next_value()?;
225 }
226 "min_amount" => {
227 if min_amount.is_some() {
228 return Err(de::Error::duplicate_field("min_amount"));
229 }
230 min_amount = Some(map.next_value()?);
231 }
232 "max_amount" => {
233 if max_amount.is_some() {
234 return Err(de::Error::duplicate_field("max_amount"));
235 }
236 max_amount = Some(map.next_value()?);
237 }
238 "description" => {
239 if description.is_some() {
240 return Err(de::Error::duplicate_field("description"));
241 }
242 description = Some(map.next_value()?);
243 }
244 "confirmations" => {
245 return Err(de::Error::unknown_field("confirmations", &["options"]));
246 }
247 "options" => {
248 let options: Option<MintMethodOptions> = map.next_value()?;
251
252 if let Some(MintMethodOptions::Bolt11 {
253 description: desc_from_options,
254 }) = options
255 {
256 if description.is_none() {
258 description = Some(desc_from_options);
259 }
260 }
261
262 if let Some(MintMethodOptions::Onchain {
263 confirmations: conf_from_options,
264 }) = options
265 {
266 confirmations = Some(conf_from_options);
267 }
268 }
269 _ => {
270 let _: serde::de::IgnoredAny = map.next_value()?;
272 }
273 }
274 }
275
276 let method = method.ok_or_else(|| de::Error::missing_field("method"))?;
277 let unit = unit.ok_or_else(|| de::Error::missing_field("unit"))?;
278
279 let options = if method == PaymentMethod::Known(KnownMethod::Bolt11) {
284 description.map(|desc| MintMethodOptions::Bolt11 { description: desc })
285 } else if method == PaymentMethod::Known(KnownMethod::Bolt12) {
286 description.map(|desc| MintMethodOptions::Bolt12 { description: desc })
287 } else if method == PaymentMethod::Known(KnownMethod::Onchain) {
288 confirmations.map(|conf| MintMethodOptions::Onchain {
289 confirmations: conf,
290 })
291 } else {
292 None
293 };
294
295 Ok(MintMethodSettings {
296 method,
297 unit,
298 method_name,
299 min_amount,
300 max_amount,
301 options,
302 })
303 }
304}
305
306impl<'de> Deserialize<'de> for MintMethodSettings {
307 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
308 where
309 D: Deserializer<'de>,
310 {
311 deserializer.deserialize_map(MintMethodSettingsVisitor)
312 }
313}
314
315#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
317#[serde(untagged)]
318pub enum MintMethodOptions {
319 Bolt11 {
321 description: bool,
323 },
324 Bolt12 {
326 description: bool,
328 },
329 Onchain {
331 confirmations: u32,
333 },
334 Custom {},
336}
337
338#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
340pub struct Settings {
341 pub methods: Vec<MintMethodSettings>,
343 pub disabled: bool,
345}
346
347impl Settings {
348 pub fn new(methods: Vec<MintMethodSettings>, disabled: bool) -> Self {
350 Self { methods, disabled }
351 }
352
353 pub fn get_settings(
355 &self,
356 unit: &CurrencyUnit,
357 method: &PaymentMethod,
358 ) -> Option<MintMethodSettings> {
359 for method_settings in self.methods.iter() {
360 if method_settings.method.eq(method) && method_settings.unit.eq(unit) {
361 return Some(method_settings.clone());
362 }
363 }
364
365 None
366 }
367
368 pub fn remove_settings(
370 &mut self,
371 unit: &CurrencyUnit,
372 method: &PaymentMethod,
373 ) -> Option<MintMethodSettings> {
374 self.methods
375 .iter()
376 .position(|settings| &settings.method == method && &settings.unit == unit)
377 .map(|index| self.methods.remove(index))
378 }
379
380 pub fn supported_methods(&self) -> Vec<&PaymentMethod> {
382 self.methods.iter().map(|a| &a.method).collect()
383 }
384
385 pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
387 self.methods.iter().map(|s| &s.unit).collect()
388 }
389}
390
391#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
399pub struct MintQuoteCustomRequest {
400 #[serde(default, skip_serializing_if = "Option::is_none")]
405 pub amount: Option<Amount>,
406 pub unit: CurrencyUnit,
408 #[serde(skip_serializing_if = "Option::is_none")]
410 pub description: Option<String>,
411 #[serde(skip_serializing_if = "Option::is_none")]
413 pub pubkey: Option<PublicKey>,
414 #[serde(flatten, default, skip_serializing_if = "serde_json::Value::is_null")]
421 pub extra: serde_json::Value,
422}
423
424#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
450#[serde(bound = "Q: Serialize + for<'a> Deserialize<'a>")]
451pub struct MintQuoteCustomResponse<Q> {
452 pub quote: Q,
454 pub request: String,
456 pub method: PaymentMethod,
458 pub amount: Option<Amount>,
460 pub amount_paid: Amount,
462 pub amount_issued: Amount,
464 #[serde(default)]
466 pub updated_at: u64,
467 pub unit: Option<CurrencyUnit>,
469 pub expiry: Option<u64>,
471 #[serde(
473 default,
474 skip_serializing_if = "Option::is_none",
475 deserialize_with = "deserialize_empty_string_as_none"
476 )]
477 pub pubkey: Option<PublicKey>,
478 #[serde(flatten, default, skip_serializing_if = "serde_json::Value::is_null")]
483 pub extra: serde_json::Value,
484}
485
486#[cfg(feature = "mint")]
487impl<Q: ToString> MintQuoteCustomResponse<Q> {
488 pub fn to_string_id(&self) -> MintQuoteCustomResponse<String> {
490 MintQuoteCustomResponse {
491 quote: self.quote.to_string(),
492 request: self.request.clone(),
493 method: self.method.clone(),
494 amount: self.amount,
495 amount_paid: self.amount_paid,
496 amount_issued: self.amount_issued,
497 updated_at: self.updated_at,
498 unit: self.unit.clone(),
499 expiry: self.expiry,
500 pubkey: self.pubkey,
501 extra: self.extra.clone(),
502 }
503 }
504}
505
506#[cfg(feature = "mint")]
507impl From<MintQuoteCustomResponse<QuoteId>> for MintQuoteCustomResponse<String> {
508 fn from(value: MintQuoteCustomResponse<QuoteId>) -> Self {
509 Self {
510 quote: value.quote.to_string(),
511 request: value.request,
512 method: value.method,
513 amount: value.amount,
514 amount_paid: value.amount_paid,
515 amount_issued: value.amount_issued,
516 updated_at: value.updated_at,
517 unit: value.unit,
518 expiry: value.expiry,
519 pubkey: value.pubkey,
520 extra: value.extra,
521 }
522 }
523}
524#[cfg(test)]
525mod tests {
526 use std::fmt;
527
528 use serde::ser::{Impossible, SerializeStruct, Serializer};
529 use serde_json::{from_str, json, to_string};
530
531 use super::*;
532 use crate::nut00::KnownMethod;
533
534 #[derive(Debug)]
535 struct FieldCountError(String);
536
537 impl fmt::Display for FieldCountError {
538 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
539 f.write_str(&self.0)
540 }
541 }
542
543 impl std::error::Error for FieldCountError {}
544
545 impl serde::ser::Error for FieldCountError {
546 fn custom<T>(msg: T) -> Self
547 where
548 T: fmt::Display,
549 {
550 Self(msg.to_string())
551 }
552 }
553
554 struct FieldCountSerializer;
555
556 struct FieldCountStruct {
557 declared: usize,
558 actual: usize,
559 }
560
561 impl Serializer for FieldCountSerializer {
562 type Ok = ();
563 type Error = FieldCountError;
564 type SerializeSeq = Impossible<Self::Ok, Self::Error>;
565 type SerializeTuple = Impossible<Self::Ok, Self::Error>;
566 type SerializeTupleStruct = Impossible<Self::Ok, Self::Error>;
567 type SerializeTupleVariant = Impossible<Self::Ok, Self::Error>;
568 type SerializeMap = Impossible<Self::Ok, Self::Error>;
569 type SerializeStruct = FieldCountStruct;
570 type SerializeStructVariant = Impossible<Self::Ok, Self::Error>;
571
572 fn serialize_bool(self, _v: bool) -> Result<Self::Ok, Self::Error> {
573 Err(FieldCountError("unsupported bool".to_string()))
574 }
575
576 fn serialize_i8(self, _v: i8) -> Result<Self::Ok, Self::Error> {
577 Err(FieldCountError("unsupported i8".to_string()))
578 }
579
580 fn serialize_i16(self, _v: i16) -> Result<Self::Ok, Self::Error> {
581 Err(FieldCountError("unsupported i16".to_string()))
582 }
583
584 fn serialize_i32(self, _v: i32) -> Result<Self::Ok, Self::Error> {
585 Err(FieldCountError("unsupported i32".to_string()))
586 }
587
588 fn serialize_i64(self, _v: i64) -> Result<Self::Ok, Self::Error> {
589 Err(FieldCountError("unsupported i64".to_string()))
590 }
591
592 fn serialize_u8(self, _v: u8) -> Result<Self::Ok, Self::Error> {
593 Err(FieldCountError("unsupported u8".to_string()))
594 }
595
596 fn serialize_u16(self, _v: u16) -> Result<Self::Ok, Self::Error> {
597 Err(FieldCountError("unsupported u16".to_string()))
598 }
599
600 fn serialize_u32(self, _v: u32) -> Result<Self::Ok, Self::Error> {
601 Err(FieldCountError("unsupported u32".to_string()))
602 }
603
604 fn serialize_u64(self, _v: u64) -> Result<Self::Ok, Self::Error> {
605 Err(FieldCountError("unsupported u64".to_string()))
606 }
607
608 fn serialize_f32(self, _v: f32) -> Result<Self::Ok, Self::Error> {
609 Err(FieldCountError("unsupported f32".to_string()))
610 }
611
612 fn serialize_f64(self, _v: f64) -> Result<Self::Ok, Self::Error> {
613 Err(FieldCountError("unsupported f64".to_string()))
614 }
615
616 fn serialize_char(self, _v: char) -> Result<Self::Ok, Self::Error> {
617 Err(FieldCountError("unsupported char".to_string()))
618 }
619
620 fn serialize_str(self, _v: &str) -> Result<Self::Ok, Self::Error> {
621 Err(FieldCountError("unsupported str".to_string()))
622 }
623
624 fn serialize_bytes(self, _v: &[u8]) -> Result<Self::Ok, Self::Error> {
625 Err(FieldCountError("unsupported bytes".to_string()))
626 }
627
628 fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
629 Err(FieldCountError("unsupported none".to_string()))
630 }
631
632 fn serialize_some<T>(self, _value: &T) -> Result<Self::Ok, Self::Error>
633 where
634 T: ?Sized + Serialize,
635 {
636 Err(FieldCountError("unsupported some".to_string()))
637 }
638
639 fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
640 Err(FieldCountError("unsupported unit".to_string()))
641 }
642
643 fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
644 Err(FieldCountError("unsupported unit struct".to_string()))
645 }
646
647 fn serialize_unit_variant(
648 self,
649 _name: &'static str,
650 _variant_index: u32,
651 _variant: &'static str,
652 ) -> Result<Self::Ok, Self::Error> {
653 Err(FieldCountError("unsupported unit variant".to_string()))
654 }
655
656 fn serialize_newtype_struct<T>(
657 self,
658 _name: &'static str,
659 _value: &T,
660 ) -> Result<Self::Ok, Self::Error>
661 where
662 T: ?Sized + Serialize,
663 {
664 Err(FieldCountError("unsupported newtype struct".to_string()))
665 }
666
667 fn serialize_newtype_variant<T>(
668 self,
669 _name: &'static str,
670 _variant_index: u32,
671 _variant: &'static str,
672 _value: &T,
673 ) -> Result<Self::Ok, Self::Error>
674 where
675 T: ?Sized + Serialize,
676 {
677 Err(FieldCountError("unsupported newtype variant".to_string()))
678 }
679
680 fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
681 Err(FieldCountError("unsupported seq".to_string()))
682 }
683
684 fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
685 Err(FieldCountError("unsupported tuple".to_string()))
686 }
687
688 fn serialize_tuple_struct(
689 self,
690 _name: &'static str,
691 _len: usize,
692 ) -> Result<Self::SerializeTupleStruct, Self::Error> {
693 Err(FieldCountError("unsupported tuple struct".to_string()))
694 }
695
696 fn serialize_tuple_variant(
697 self,
698 _name: &'static str,
699 _variant_index: u32,
700 _variant: &'static str,
701 _len: usize,
702 ) -> Result<Self::SerializeTupleVariant, Self::Error> {
703 Err(FieldCountError("unsupported tuple variant".to_string()))
704 }
705
706 fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
707 Err(FieldCountError("unsupported map".to_string()))
708 }
709
710 fn serialize_struct(
711 self,
712 _name: &'static str,
713 len: usize,
714 ) -> Result<Self::SerializeStruct, Self::Error> {
715 Ok(FieldCountStruct {
716 declared: len,
717 actual: 0,
718 })
719 }
720
721 fn serialize_struct_variant(
722 self,
723 _name: &'static str,
724 _variant_index: u32,
725 _variant: &'static str,
726 _len: usize,
727 ) -> Result<Self::SerializeStructVariant, Self::Error> {
728 Err(FieldCountError("unsupported struct variant".to_string()))
729 }
730 }
731
732 impl SerializeStruct for FieldCountStruct {
733 type Ok = ();
734 type Error = FieldCountError;
735
736 fn serialize_field<T>(&mut self, _key: &'static str, _value: &T) -> Result<(), Self::Error>
737 where
738 T: ?Sized + Serialize,
739 {
740 self.actual += 1;
741 Ok(())
742 }
743
744 fn end(self) -> Result<Self::Ok, Self::Error> {
745 if self.actual == self.declared {
746 Ok(())
747 } else {
748 Err(FieldCountError(format!(
749 "declared {} fields but serialized {}",
750 self.declared, self.actual
751 )))
752 }
753 }
754 }
755
756 fn assert_mint_method_settings_field_count(settings: &MintMethodSettings) {
757 settings.serialize(FieldCountSerializer).unwrap();
758 }
759
760 #[test]
761 fn test_mint_request_total_amount() {
762 let request: MintRequest<String> = from_str(
763 r#"{
764 "quote": "quote-id",
765 "outputs": [
766 {
767 "amount": 2,
768 "id": "00bfa73302d12ffd",
769 "B_": "038ec853d65ae1b79b5cdbc2774150b2cb288d6d26e12958a16fb33c32d9a86c39"
770 },
771 {
772 "amount": 4,
773 "id": "00bfa73302d12ffd",
774 "B_": "02c97ee3d1db41cf0a3ddb601724be8711a032950811bf326f8219c50c4808d3cd"
775 }
776 ]
777 }"#,
778 )
779 .unwrap();
780
781 assert_eq!(request.total_amount().unwrap(), Amount::from(6));
782 }
783
784 #[test]
785 fn test_mint_method_settings_serialize_field_count() {
786 assert_mint_method_settings_field_count(&MintMethodSettings {
787 method: PaymentMethod::Known(KnownMethod::Bolt11),
788 unit: CurrencyUnit::Sat,
789 method_name: None,
790 min_amount: Some(Amount::from(1)),
791 max_amount: Some(Amount::from(1000)),
792 options: Some(MintMethodOptions::Bolt11 { description: true }),
793 });
794
795 assert_mint_method_settings_field_count(&MintMethodSettings {
796 method: PaymentMethod::Known(KnownMethod::Bolt11),
797 unit: CurrencyUnit::Sat,
798 method_name: None,
799 min_amount: Some(Amount::from(1)),
800 max_amount: None,
801 options: None,
802 });
803
804 assert_mint_method_settings_field_count(&MintMethodSettings {
805 method: PaymentMethod::Known(KnownMethod::Bolt11),
806 unit: CurrencyUnit::Sat,
807 method_name: None,
808 min_amount: None,
809 max_amount: Some(Amount::from(1000)),
810 options: None,
811 });
812
813 assert_mint_method_settings_field_count(&MintMethodSettings {
814 method: PaymentMethod::Known(KnownMethod::Bolt11),
815 unit: CurrencyUnit::Sat,
816 method_name: None,
817 min_amount: None,
818 max_amount: None,
819 options: Some(MintMethodOptions::Bolt11 { description: true }),
820 });
821
822 assert_mint_method_settings_field_count(&MintMethodSettings {
823 method: PaymentMethod::Known(KnownMethod::Onchain),
824 unit: CurrencyUnit::Sat,
825 method_name: None,
826 min_amount: None,
827 max_amount: None,
828 options: Some(MintMethodOptions::Onchain { confirmations: 3 }),
829 });
830
831 assert_mint_method_settings_field_count(&MintMethodSettings {
832 method: PaymentMethod::Known(KnownMethod::Bolt11),
833 unit: CurrencyUnit::Sat,
834 method_name: Some("Lightning".to_string()),
835 min_amount: None,
836 max_amount: None,
837 options: None,
838 });
839 }
840
841 #[test]
842 fn test_mint_method_settings_top_level_description() {
843 let json_str = r#"{
845 "method": "bolt11",
846 "unit": "sat",
847 "min_amount": 0,
848 "max_amount": 10000,
849 "description": true
850 }"#;
851
852 let settings: MintMethodSettings = from_str(json_str).unwrap();
854
855 assert_eq!(settings.method, PaymentMethod::Known(KnownMethod::Bolt11));
857 assert_eq!(settings.unit, CurrencyUnit::Sat);
858 assert_eq!(settings.method_name, None);
859 assert_eq!(settings.min_amount, Some(Amount::from(0)));
860 assert_eq!(settings.max_amount, Some(Amount::from(10000)));
861
862 match settings.options {
863 Some(MintMethodOptions::Bolt11 { description }) => {
864 assert!(description);
865 }
866 _ => panic!("Expected Bolt11 options with description = true"),
867 }
868
869 let serialized = to_string(&settings).unwrap();
871 let parsed: serde_json::Value = from_str(&serialized).unwrap();
872
873 assert_eq!(parsed["options"]["description"], json!(true));
874 assert!(parsed.get("description").is_none());
875 }
876
877 #[test]
878 fn test_mint_method_settings_serializes_false_description_in_options() {
879 let settings = MintMethodSettings {
880 method: PaymentMethod::Known(KnownMethod::Bolt11),
881 unit: CurrencyUnit::Sat,
882 method_name: None,
883 min_amount: None,
884 max_amount: None,
885 options: Some(MintMethodOptions::Bolt11 { description: false }),
886 };
887
888 let serialized = to_string(&settings).unwrap();
889 let parsed: serde_json::Value = from_str(&serialized).unwrap();
890
891 assert_eq!(parsed["method"], json!("bolt11"));
892 assert!(parsed.get("description").is_none());
893 assert_eq!(parsed["options"]["description"], json!(false));
894 }
895
896 #[test]
897 fn test_bolt11_settings_nested_options_round_trip() {
898 let json_str = r#"{
899 "method": "bolt11",
900 "unit": "sat",
901 "min_amount": 0,
902 "max_amount": 10000,
903 "options": {
904 "description": true
905 }
906 }"#;
907
908 let settings: MintMethodSettings = from_str(json_str).unwrap();
909
910 match settings.options {
911 Some(MintMethodOptions::Bolt11 { description }) => {
912 assert!(description);
913 }
914 _ => panic!("Expected Bolt11 options with description = true"),
915 }
916
917 let serialized = to_string(&settings).unwrap();
918 let parsed: serde_json::Value = from_str(&serialized).unwrap();
919
920 assert_eq!(parsed["options"]["description"], json!(true));
921 assert!(parsed.get("description").is_none());
922 }
923
924 #[test]
925 fn test_both_description_locations() {
926 let json_str = r#"{
928 "method": "bolt11",
929 "unit": "sat",
930 "min_amount": 0,
931 "max_amount": 10000,
932 "description": true,
933 "options": {
934 "description": false
935 }
936 }"#;
937
938 let settings: MintMethodSettings = from_str(json_str).unwrap();
940
941 match settings.options {
942 Some(MintMethodOptions::Bolt11 { description }) => {
943 assert!(description, "Top-level description should take precedence");
944 }
945 _ => panic!("Expected Bolt11 options with description = true"),
946 }
947 }
948
949 #[test]
950 fn test_mint_method_settings_method_name_round_trip() {
951 let json_str = r#"{
952 "method": "bolt11",
953 "unit": "sat",
954 "method_name": "Lightning",
955 "min_amount": 0,
956 "max_amount": 10000
957 }"#;
958
959 let settings: MintMethodSettings = from_str(json_str).unwrap();
960
961 assert_eq!(settings.method_name, Some("Lightning".to_string()));
962 assert_eq!(settings.method_name(), "Lightning");
963
964 let serialized = to_string(&settings).unwrap();
965 let parsed: serde_json::Value = from_str(&serialized).unwrap();
966
967 assert_eq!(parsed["method_name"], json!("Lightning"));
968 }
969
970 #[test]
971 fn test_mint_method_settings_null_method_name_deserializes_as_none() {
972 let json_str = r#"{
973 "method": "bolt11",
974 "unit": "sat",
975 "method_name": null
976 }"#;
977
978 let settings: MintMethodSettings = from_str(json_str).unwrap();
979
980 assert_eq!(settings.method_name, None);
981 assert_eq!(settings.method_name(), "Bolt11");
982
983 let serialized = to_string(&settings).unwrap();
984 let parsed: serde_json::Value = from_str(&serialized).unwrap();
985
986 assert!(parsed.get("method_name").is_none());
987 }
988
989 #[test]
990 fn test_mint_method_settings_omitted_method_name_uses_derived_name() {
991 let json_str = r#"{
992 "method": "apple-pay",
993 "unit": "usd"
994 }"#;
995
996 let settings: MintMethodSettings = from_str(json_str).unwrap();
997
998 assert_eq!(settings.method_name, None);
999 assert_eq!(settings.method_name(), "Apple Pay");
1000 }
1001
1002 #[test]
1003 fn custom_mint_quote_response_has_no_typed_state() {
1004 let response = MintQuoteCustomResponse {
1005 quote: "abc123".to_string(),
1006 request: "paypal://pay?id=123".to_string(),
1007 method: PaymentMethod::Custom("paypal".to_string()),
1008 amount: Some(Amount::from(1000)),
1009 amount_paid: Amount::ZERO,
1010 amount_issued: Amount::ZERO,
1011 updated_at: 0,
1012 unit: Some(CurrencyUnit::Sat),
1013 expiry: Some(9999999),
1014 pubkey: None,
1015 extra: serde_json::Value::Null,
1016 };
1017
1018 let serialized = to_string(&response).unwrap();
1019 let parsed: serde_json::Value = from_str(&serialized).unwrap();
1020
1021 assert!(parsed.get("state").is_none());
1022 assert_eq!(parsed["method"], json!("paypal"));
1023 assert_eq!(parsed["amount_paid"], json!(0));
1024 assert_eq!(parsed["amount_issued"], json!(0));
1025 }
1026
1027 #[test]
1028 fn custom_mint_quote_response_flattens_extra_on_wire() {
1029 let response = MintQuoteCustomResponse {
1030 quote: "q1".to_string(),
1031 request: "custom://pay".to_string(),
1032 method: PaymentMethod::Custom("custom".to_string()),
1033 amount: Some(Amount::from(100)),
1034 amount_paid: Amount::ZERO,
1035 amount_issued: Amount::ZERO,
1036 updated_at: 0,
1037 unit: Some(CurrencyUnit::Sat),
1038 expiry: Some(9999),
1039 pubkey: None,
1040 extra: json!({"payment_url": "https://example.com", "ref": 42}),
1041 };
1042
1043 let serialized = to_string(&response).expect("serializes");
1044 let parsed: serde_json::Value = from_str(&serialized).expect("parses");
1045
1046 assert_eq!(parsed["payment_url"], json!("https://example.com"));
1047 assert_eq!(parsed["ref"], json!(42));
1048 assert!(
1049 parsed.get("extra").is_none(),
1050 "extra must be flattened, not nested under an 'extra' key"
1051 );
1052 }
1053
1054 #[test]
1055 fn test_onchain_settings_nested_options_round_trip() {
1056 let json_str = r#"{
1058 "method": "onchain",
1059 "unit": "sat",
1060 "min_amount": 1000,
1061 "max_amount": 1000000,
1062 "options": {
1063 "confirmations": 3
1064 }
1065 }"#;
1066
1067 let settings: MintMethodSettings = from_str(json_str).unwrap();
1068
1069 assert_eq!(settings.method, PaymentMethod::Known(KnownMethod::Onchain));
1070 assert_eq!(settings.unit, CurrencyUnit::Sat);
1071 assert_eq!(settings.min_amount, Some(Amount::from(1000)));
1072 assert_eq!(settings.max_amount, Some(Amount::from(1000000)));
1073
1074 match settings.options {
1075 Some(MintMethodOptions::Onchain { confirmations }) => {
1076 assert_eq!(confirmations, 3);
1077 }
1078 _ => panic!("Expected Onchain options with confirmations = 3"),
1079 }
1080
1081 let serialized = to_string(&settings).unwrap();
1083 let parsed: serde_json::Value = from_str(&serialized).unwrap();
1084
1085 assert_eq!(parsed["method"], json!("onchain"));
1086 assert_eq!(parsed["options"]["confirmations"], json!(3));
1087 assert!(parsed.get("confirmations").is_none());
1089 }
1090
1091 #[test]
1092 fn test_onchain_settings_top_level_confirmations_rejected() {
1093 let json_str = r#"{
1094 "method": "onchain",
1095 "unit": "sat",
1096 "confirmations": 6
1097 }"#;
1098
1099 let err = from_str::<MintMethodSettings>(json_str).unwrap_err();
1100 assert!(err.to_string().contains("unknown field"));
1101 }
1102
1103 #[test]
1104 fn test_onchain_settings_top_level_and_nested_rejected() {
1105 let json_str = r#"{
1106 "method": "onchain",
1107 "unit": "sat",
1108 "confirmations": 6,
1109 "options": {
1110 "confirmations": 3
1111 }
1112 }"#;
1113
1114 let err = from_str::<MintMethodSettings>(json_str).unwrap_err();
1115 assert!(err.to_string().contains("unknown field"));
1116 }
1117
1118 #[test]
1119 fn test_bolt12_settings_nested_options_round_trip() {
1120 let json_str = r#"{
1122 "method": "bolt12",
1123 "unit": "sat",
1124 "min_amount": 0,
1125 "max_amount": 10000,
1126 "options": {
1127 "description": true
1128 }
1129 }"#;
1130
1131 let settings: MintMethodSettings = from_str(json_str).unwrap();
1132
1133 assert_eq!(settings.method, PaymentMethod::Known(KnownMethod::Bolt12));
1134 assert_eq!(settings.unit, CurrencyUnit::Sat);
1135
1136 match settings.options {
1137 Some(MintMethodOptions::Bolt12 { description }) => {
1138 assert!(description);
1139 }
1140 _ => panic!("Expected Bolt12 options with description = true"),
1141 }
1142
1143 let serialized = to_string(&settings).unwrap();
1145 let parsed: serde_json::Value = from_str(&serialized).unwrap();
1146
1147 assert_eq!(parsed["method"], json!("bolt12"));
1148 assert_eq!(parsed["options"]["description"], json!(true));
1149 assert!(parsed.get("description").is_none());
1151 }
1152
1153 #[test]
1154 fn test_bolt12_settings_top_level_description_accepted() {
1155 let json_str = r#"{
1158 "method": "bolt12",
1159 "unit": "sat",
1160 "description": true
1161 }"#;
1162
1163 let settings: MintMethodSettings = from_str(json_str).unwrap();
1164
1165 assert_eq!(settings.method, PaymentMethod::Known(KnownMethod::Bolt12));
1166 match settings.options {
1167 Some(MintMethodOptions::Bolt12 { description }) => {
1168 assert!(description);
1169 }
1170 _ => panic!("Expected Bolt12 options with description = true"),
1171 }
1172 }
1173
1174 #[test]
1175 fn test_bolt12_settings_no_options_when_description_absent() {
1176 let json_str = r#"{
1177 "method": "bolt12",
1178 "unit": "sat"
1179 }"#;
1180
1181 let settings: MintMethodSettings = from_str(json_str).unwrap();
1182
1183 assert_eq!(settings.method, PaymentMethod::Known(KnownMethod::Bolt12));
1184 assert_eq!(settings.options, None);
1185 }
1186
1187 #[test]
1188 fn test_mint_method_settings_visitor_reports_expected_type() {
1189 let err = from_str::<MintMethodSettings>("[]").unwrap_err();
1190
1191 assert!(err.to_string().contains("a MintMethodSettings structure"));
1192 }
1193
1194 #[test]
1195 fn test_get_settings_requires_exact_method_and_unit_match() {
1196 let bolt11_msat = MintMethodSettings {
1197 method: PaymentMethod::Known(KnownMethod::Bolt11),
1198 unit: CurrencyUnit::Msat,
1199 method_name: None,
1200 min_amount: Some(Amount::from(1)),
1201 max_amount: None,
1202 options: None,
1203 };
1204 let bolt12_sat = MintMethodSettings {
1205 method: PaymentMethod::Known(KnownMethod::Bolt12),
1206 unit: CurrencyUnit::Sat,
1207 method_name: None,
1208 min_amount: Some(Amount::from(2)),
1209 max_amount: None,
1210 options: None,
1211 };
1212 let bolt11_sat = MintMethodSettings {
1213 method: PaymentMethod::Known(KnownMethod::Bolt11),
1214 unit: CurrencyUnit::Sat,
1215 method_name: None,
1216 min_amount: Some(Amount::from(3)),
1217 max_amount: None,
1218 options: None,
1219 };
1220 let settings = Settings::new(
1221 vec![bolt11_msat.clone(), bolt12_sat.clone(), bolt11_sat.clone()],
1222 false,
1223 );
1224
1225 assert_eq!(
1226 settings.get_settings(&CurrencyUnit::Sat, &PaymentMethod::BOLT11),
1227 Some(bolt11_sat)
1228 );
1229 assert_eq!(
1230 settings.get_settings(
1231 &CurrencyUnit::Msat,
1232 &PaymentMethod::Known(KnownMethod::Bolt12)
1233 ),
1234 None
1235 );
1236 }
1237
1238 #[test]
1239 fn test_supported_methods_and_units_preserve_configured_values() {
1240 let settings = Settings::new(
1241 vec![
1242 MintMethodSettings {
1243 method: PaymentMethod::Known(KnownMethod::Bolt11),
1244 unit: CurrencyUnit::Msat,
1245 method_name: None,
1246 min_amount: Some(Amount::from(1)),
1247 max_amount: None,
1248 options: None,
1249 },
1250 MintMethodSettings {
1251 method: PaymentMethod::Known(KnownMethod::Onchain),
1252 unit: CurrencyUnit::Eur,
1253 method_name: None,
1254 min_amount: None,
1255 max_amount: Some(Amount::from(100)),
1256 options: Some(MintMethodOptions::Onchain { confirmations: 3 }),
1257 },
1258 ],
1259 false,
1260 );
1261
1262 let methods = settings.supported_methods();
1263 assert_eq!(methods.len(), 2);
1264 assert_eq!(methods[0], &PaymentMethod::Known(KnownMethod::Bolt11));
1265 assert_eq!(methods[1], &PaymentMethod::Known(KnownMethod::Onchain));
1266
1267 let units = settings.supported_units();
1268 assert_eq!(units.len(), 2);
1269 assert_eq!(units[0], &CurrencyUnit::Msat);
1270 assert_eq!(units[1], &CurrencyUnit::Eur);
1271 }
1272
1273 #[test]
1274 fn test_remove_settings_requires_exact_method_and_unit_match() {
1275 let bolt11_msat = MintMethodSettings {
1276 method: PaymentMethod::Known(KnownMethod::Bolt11),
1277 unit: CurrencyUnit::Msat,
1278 method_name: None,
1279 min_amount: Some(Amount::from(1)),
1280 max_amount: None,
1281 options: None,
1282 };
1283 let bolt12_sat = MintMethodSettings {
1284 method: PaymentMethod::Known(KnownMethod::Bolt12),
1285 unit: CurrencyUnit::Sat,
1286 method_name: None,
1287 min_amount: Some(Amount::from(2)),
1288 max_amount: None,
1289 options: None,
1290 };
1291 let bolt11_sat = MintMethodSettings {
1292 method: PaymentMethod::Known(KnownMethod::Bolt11),
1293 unit: CurrencyUnit::Sat,
1294 method_name: None,
1295 min_amount: Some(Amount::from(3)),
1296 max_amount: None,
1297 options: None,
1298 };
1299 let mut settings = Settings::new(
1300 vec![bolt11_msat.clone(), bolt12_sat.clone(), bolt11_sat.clone()],
1301 false,
1302 );
1303
1304 assert_eq!(
1305 settings.remove_settings(&CurrencyUnit::Sat, &PaymentMethod::BOLT11),
1306 Some(bolt11_sat.clone())
1307 );
1308 assert_eq!(settings.methods, vec![bolt11_msat, bolt12_sat]);
1309 assert_eq!(
1310 settings.remove_settings(&CurrencyUnit::Sat, &PaymentMethod::BOLT11),
1311 None
1312 );
1313 }
1314}