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;
16use crate::nut23::QuoteState;
17#[cfg(feature = "mint")]
18use crate::quote_id::QuoteId;
19#[cfg(feature = "mint")]
20use crate::quote_id::QuoteIdError;
21use crate::util::serde_helpers::deserialize_empty_string_as_none;
22use crate::{Amount, PublicKey};
23
24#[derive(Debug, Error)]
26pub enum Error {
27 #[error("Unknown Quote State")]
29 UnknownState,
30 #[error("Amount overflow")]
32 AmountOverflow,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
38#[serde(bound = "Q: Serialize + DeserializeOwned")]
39pub struct MintRequest<Q> {
40 #[cfg_attr(feature = "swagger", schema(max_length = 1_000))]
42 pub quote: Q,
43 #[cfg_attr(feature = "swagger", schema(max_items = 1_000))]
45 pub outputs: Vec<BlindedMessage>,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub signature: Option<String>,
49}
50
51#[cfg(feature = "mint")]
52impl TryFrom<MintRequest<String>> for MintRequest<QuoteId> {
53 type Error = QuoteIdError;
54
55 fn try_from(value: MintRequest<String>) -> Result<Self, Self::Error> {
56 Ok(Self {
57 quote: QuoteId::from_str(&value.quote)?,
58 outputs: value.outputs,
59 signature: value.signature,
60 })
61 }
62}
63
64impl<Q> MintRequest<Q> {
65 pub fn total_amount(&self) -> Result<Amount, Error> {
67 Amount::try_sum(
68 self.outputs
69 .iter()
70 .map(|BlindedMessage { amount, .. }| *amount),
71 )
72 .map_err(|_| Error::AmountOverflow)
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
79pub struct MintResponse {
80 pub signatures: Vec<BlindSignature>,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Hash)]
86#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
87pub struct MintMethodSettings {
88 pub method: PaymentMethod,
90 pub unit: CurrencyUnit,
92 pub min_amount: Option<Amount>,
94 pub max_amount: Option<Amount>,
96 pub options: Option<MintMethodOptions>,
98}
99
100impl Serialize for MintMethodSettings {
101 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
102 where
103 S: Serializer,
104 {
105 let mut num_fields = 3; if self.min_amount.is_some() {
107 num_fields += 1;
108 }
109 if self.max_amount.is_some() {
110 num_fields += 1;
111 }
112
113 let mut description_in_top_level = false;
114 if let Some(MintMethodOptions::Bolt11 { description }) = &self.options {
115 if *description {
116 num_fields += 1;
117 description_in_top_level = true;
118 }
119 }
120
121 let mut state = serializer.serialize_struct("MintMethodSettings", num_fields)?;
122
123 state.serialize_field("method", &self.method)?;
124 state.serialize_field("unit", &self.unit)?;
125
126 if let Some(min_amount) = &self.min_amount {
127 state.serialize_field("min_amount", min_amount)?;
128 }
129
130 if let Some(max_amount) = &self.max_amount {
131 state.serialize_field("max_amount", max_amount)?;
132 }
133
134 if description_in_top_level {
136 state.serialize_field("description", &true)?;
137 }
138
139 state.end()
140 }
141}
142
143struct MintMethodSettingsVisitor;
144
145impl<'de> Visitor<'de> for MintMethodSettingsVisitor {
146 type Value = MintMethodSettings;
147
148 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
149 formatter.write_str("a MintMethodSettings structure")
150 }
151
152 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
153 where
154 M: MapAccess<'de>,
155 {
156 let mut method: Option<PaymentMethod> = None;
157 let mut unit: Option<CurrencyUnit> = None;
158 let mut min_amount: Option<Amount> = None;
159 let mut max_amount: Option<Amount> = None;
160 let mut description: Option<bool> = None;
161
162 while let Some(key) = map.next_key::<String>()? {
163 match key.as_str() {
164 "method" => {
165 if method.is_some() {
166 return Err(de::Error::duplicate_field("method"));
167 }
168 method = Some(map.next_value()?);
169 }
170 "unit" => {
171 if unit.is_some() {
172 return Err(de::Error::duplicate_field("unit"));
173 }
174 unit = Some(map.next_value()?);
175 }
176 "min_amount" => {
177 if min_amount.is_some() {
178 return Err(de::Error::duplicate_field("min_amount"));
179 }
180 min_amount = Some(map.next_value()?);
181 }
182 "max_amount" => {
183 if max_amount.is_some() {
184 return Err(de::Error::duplicate_field("max_amount"));
185 }
186 max_amount = Some(map.next_value()?);
187 }
188 "description" => {
189 if description.is_some() {
190 return Err(de::Error::duplicate_field("description"));
191 }
192 description = Some(map.next_value()?);
193 }
194 "options" => {
195 let options: Option<MintMethodOptions> = map.next_value()?;
198
199 if let Some(MintMethodOptions::Bolt11 {
200 description: desc_from_options,
201 }) = options
202 {
203 if description.is_none() {
205 description = Some(desc_from_options);
206 }
207 }
208 }
209 _ => {
210 let _: serde::de::IgnoredAny = map.next_value()?;
212 }
213 }
214 }
215
216 let method = method.ok_or_else(|| de::Error::missing_field("method"))?;
217 let unit = unit.ok_or_else(|| de::Error::missing_field("unit"))?;
218
219 let options = if method == PaymentMethod::Known(KnownMethod::Bolt11) {
221 description.map(|description| MintMethodOptions::Bolt11 { description })
222 } else {
223 None
224 };
225
226 Ok(MintMethodSettings {
227 method,
228 unit,
229 min_amount,
230 max_amount,
231 options,
232 })
233 }
234}
235
236impl<'de> Deserialize<'de> for MintMethodSettings {
237 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
238 where
239 D: Deserializer<'de>,
240 {
241 deserializer.deserialize_map(MintMethodSettingsVisitor)
242 }
243}
244
245#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
247#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
248#[serde(untagged)]
249pub enum MintMethodOptions {
250 Bolt11 {
252 description: bool,
254 },
255 Custom {},
257}
258
259#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
261#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = nut04::Settings))]
262pub struct Settings {
263 pub methods: Vec<MintMethodSettings>,
265 pub disabled: bool,
267}
268
269impl Settings {
270 pub fn new(methods: Vec<MintMethodSettings>, disabled: bool) -> Self {
272 Self { methods, disabled }
273 }
274
275 pub fn get_settings(
277 &self,
278 unit: &CurrencyUnit,
279 method: &PaymentMethod,
280 ) -> Option<MintMethodSettings> {
281 for method_settings in self.methods.iter() {
282 if method_settings.method.eq(method) && method_settings.unit.eq(unit) {
283 return Some(method_settings.clone());
284 }
285 }
286
287 None
288 }
289
290 pub fn remove_settings(
292 &mut self,
293 unit: &CurrencyUnit,
294 method: &PaymentMethod,
295 ) -> Option<MintMethodSettings> {
296 self.methods
297 .iter()
298 .position(|settings| &settings.method == method && &settings.unit == unit)
299 .map(|index| self.methods.remove(index))
300 }
301
302 pub fn supported_methods(&self) -> Vec<&PaymentMethod> {
304 self.methods.iter().map(|a| &a.method).collect()
305 }
306
307 pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
309 self.methods.iter().map(|s| &s.unit).collect()
310 }
311}
312
313#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
321#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
322pub struct MintQuoteCustomRequest {
323 pub amount: Amount,
325 pub unit: CurrencyUnit,
327 #[serde(skip_serializing_if = "Option::is_none")]
329 pub description: Option<String>,
330 #[serde(skip_serializing_if = "Option::is_none")]
332 pub pubkey: Option<PublicKey>,
333 #[serde(flatten, default, skip_serializing_if = "serde_json::Value::is_null")]
340 #[cfg_attr(feature = "swagger", schema(value_type = Object, additional_properties = true))]
341 pub extra: serde_json::Value,
342}
343
344#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
368#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
369#[serde(bound = "Q: Serialize + for<'a> Deserialize<'a>")]
370pub struct MintQuoteCustomResponse<Q> {
371 pub quote: Q,
373 pub request: String,
375 pub amount: Option<Amount>,
377 pub unit: Option<CurrencyUnit>,
379 pub state: QuoteState,
381 pub expiry: Option<u64>,
383 #[serde(
385 default,
386 skip_serializing_if = "Option::is_none",
387 deserialize_with = "deserialize_empty_string_as_none"
388 )]
389 pub pubkey: Option<PublicKey>,
390 #[serde(flatten, default, skip_serializing_if = "serde_json::Value::is_null")]
395 #[cfg_attr(feature = "swagger", schema(value_type = Object, additional_properties = true))]
396 pub extra: serde_json::Value,
397}
398
399#[cfg(feature = "mint")]
400impl<Q: ToString> MintQuoteCustomResponse<Q> {
401 pub fn to_string_id(&self) -> MintQuoteCustomResponse<String> {
403 MintQuoteCustomResponse {
404 quote: self.quote.to_string(),
405 request: self.request.clone(),
406 amount: self.amount,
407 state: self.state,
408 unit: self.unit.clone(),
409 expiry: self.expiry,
410 pubkey: self.pubkey,
411 extra: self.extra.clone(),
412 }
413 }
414}
415
416#[cfg(feature = "mint")]
417impl From<MintQuoteCustomResponse<QuoteId>> for MintQuoteCustomResponse<String> {
418 fn from(value: MintQuoteCustomResponse<QuoteId>) -> Self {
419 Self {
420 quote: value.quote.to_string(),
421 request: value.request,
422 amount: value.amount,
423 unit: value.unit,
424 expiry: value.expiry,
425 state: value.state,
426 pubkey: value.pubkey,
427 extra: value.extra,
428 }
429 }
430}
431#[cfg(test)]
432mod tests {
433 use serde_json::{from_str, json, to_string};
434
435 use super::*;
436 use crate::nut00::KnownMethod;
437
438 #[test]
439 fn test_mint_method_settings_top_level_description() {
440 let json_str = r#"{
442 "method": "bolt11",
443 "unit": "sat",
444 "min_amount": 0,
445 "max_amount": 10000,
446 "description": true
447 }"#;
448
449 let settings: MintMethodSettings = from_str(json_str).unwrap();
451
452 assert_eq!(settings.method, PaymentMethod::Known(KnownMethod::Bolt11));
454 assert_eq!(settings.unit, CurrencyUnit::Sat);
455 assert_eq!(settings.min_amount, Some(Amount::from(0)));
456 assert_eq!(settings.max_amount, Some(Amount::from(10000)));
457
458 match settings.options {
459 Some(MintMethodOptions::Bolt11 { description }) => {
460 assert!(description);
461 }
462 _ => panic!("Expected Bolt11 options with description = true"),
463 }
464
465 let serialized = to_string(&settings).unwrap();
467 let parsed: serde_json::Value = from_str(&serialized).unwrap();
468
469 assert_eq!(parsed["description"], json!(true));
471 }
472
473 #[test]
474 fn test_both_description_locations() {
475 let json_str = r#"{
477 "method": "bolt11",
478 "unit": "sat",
479 "min_amount": 0,
480 "max_amount": 10000,
481 "description": true,
482 "options": {
483 "description": false
484 }
485 }"#;
486
487 let settings: MintMethodSettings = from_str(json_str).unwrap();
489
490 match settings.options {
491 Some(MintMethodOptions::Bolt11 { description }) => {
492 assert!(description, "Top-level description should take precedence");
493 }
494 _ => panic!("Expected Bolt11 options with description = true"),
495 }
496 }
497}