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};
15#[cfg(feature = "mint")]
16use crate::quote_id::QuoteId;
17#[cfg(feature = "mint")]
18use crate::quote_id::QuoteIdError;
19use crate::Amount;
20
21#[derive(Debug, Error)]
23pub enum Error {
24 #[error("Unknown Quote State")]
26 UnknownState,
27 #[error("Amount overflow")]
29 AmountOverflow,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
35#[serde(bound = "Q: Serialize + DeserializeOwned")]
36pub struct MintRequest<Q> {
37 #[cfg_attr(feature = "swagger", schema(max_length = 1_000))]
39 pub quote: Q,
40 #[cfg_attr(feature = "swagger", schema(max_items = 1_000))]
42 pub outputs: Vec<BlindedMessage>,
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub signature: Option<String>,
46}
47
48#[cfg(feature = "mint")]
49impl TryFrom<MintRequest<String>> for MintRequest<QuoteId> {
50 type Error = QuoteIdError;
51
52 fn try_from(value: MintRequest<String>) -> Result<Self, Self::Error> {
53 Ok(Self {
54 quote: QuoteId::from_str(&value.quote)?,
55 outputs: value.outputs,
56 signature: value.signature,
57 })
58 }
59}
60
61impl<Q> MintRequest<Q> {
62 pub fn total_amount(&self) -> Result<Amount, Error> {
64 Amount::try_sum(
65 self.outputs
66 .iter()
67 .map(|BlindedMessage { amount, .. }| *amount),
68 )
69 .map_err(|_| Error::AmountOverflow)
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
76pub struct MintResponse {
77 pub signatures: Vec<BlindSignature>,
79}
80
81#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
83#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
84pub struct MintMethodSettings {
85 pub method: PaymentMethod,
87 pub unit: CurrencyUnit,
89 pub min_amount: Option<Amount>,
91 pub max_amount: Option<Amount>,
93 pub options: Option<MintMethodOptions>,
95}
96
97impl Serialize for MintMethodSettings {
98 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
99 where
100 S: Serializer,
101 {
102 let mut num_fields = 3; if self.min_amount.is_some() {
104 num_fields += 1;
105 }
106 if self.max_amount.is_some() {
107 num_fields += 1;
108 }
109
110 let mut description_in_top_level = false;
111 if let Some(MintMethodOptions::Bolt11 { description }) = &self.options {
112 if *description {
113 num_fields += 1;
114 description_in_top_level = true;
115 }
116 }
117
118 let mut state = serializer.serialize_struct("MintMethodSettings", num_fields)?;
119
120 state.serialize_field("method", &self.method)?;
121 state.serialize_field("unit", &self.unit)?;
122
123 if let Some(min_amount) = &self.min_amount {
124 state.serialize_field("min_amount", min_amount)?;
125 }
126
127 if let Some(max_amount) = &self.max_amount {
128 state.serialize_field("max_amount", max_amount)?;
129 }
130
131 if description_in_top_level {
133 state.serialize_field("description", &true)?;
134 }
135
136 state.end()
137 }
138}
139
140struct MintMethodSettingsVisitor;
141
142impl<'de> Visitor<'de> for MintMethodSettingsVisitor {
143 type Value = MintMethodSettings;
144
145 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
146 formatter.write_str("a MintMethodSettings structure")
147 }
148
149 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
150 where
151 M: MapAccess<'de>,
152 {
153 let mut method: Option<PaymentMethod> = None;
154 let mut unit: Option<CurrencyUnit> = None;
155 let mut min_amount: Option<Amount> = None;
156 let mut max_amount: Option<Amount> = None;
157 let mut description: Option<bool> = None;
158
159 while let Some(key) = map.next_key::<String>()? {
160 match key.as_str() {
161 "method" => {
162 if method.is_some() {
163 return Err(de::Error::duplicate_field("method"));
164 }
165 method = Some(map.next_value()?);
166 }
167 "unit" => {
168 if unit.is_some() {
169 return Err(de::Error::duplicate_field("unit"));
170 }
171 unit = Some(map.next_value()?);
172 }
173 "min_amount" => {
174 if min_amount.is_some() {
175 return Err(de::Error::duplicate_field("min_amount"));
176 }
177 min_amount = Some(map.next_value()?);
178 }
179 "max_amount" => {
180 if max_amount.is_some() {
181 return Err(de::Error::duplicate_field("max_amount"));
182 }
183 max_amount = Some(map.next_value()?);
184 }
185 "description" => {
186 if description.is_some() {
187 return Err(de::Error::duplicate_field("description"));
188 }
189 description = Some(map.next_value()?);
190 }
191 "options" => {
192 let options: Option<MintMethodOptions> = map.next_value()?;
195
196 if let Some(MintMethodOptions::Bolt11 {
197 description: desc_from_options,
198 }) = options
199 {
200 if description.is_none() {
202 description = Some(desc_from_options);
203 }
204 }
205 }
206 _ => {
207 let _: serde::de::IgnoredAny = map.next_value()?;
209 }
210 }
211 }
212
213 let method = method.ok_or_else(|| de::Error::missing_field("method"))?;
214 let unit = unit.ok_or_else(|| de::Error::missing_field("unit"))?;
215
216 let options = if method == PaymentMethod::Bolt11 {
218 description.map(|description| MintMethodOptions::Bolt11 { description })
219 } else {
220 None
221 };
222
223 Ok(MintMethodSettings {
224 method,
225 unit,
226 min_amount,
227 max_amount,
228 options,
229 })
230 }
231}
232
233impl<'de> Deserialize<'de> for MintMethodSettings {
234 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
235 where
236 D: Deserializer<'de>,
237 {
238 deserializer.deserialize_map(MintMethodSettingsVisitor)
239 }
240}
241
242#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
244#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
245#[serde(untagged)]
246pub enum MintMethodOptions {
247 Bolt11 {
249 description: bool,
251 },
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
256#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = nut04::Settings))]
257pub struct Settings {
258 pub methods: Vec<MintMethodSettings>,
260 pub disabled: bool,
262}
263
264impl Settings {
265 pub fn new(methods: Vec<MintMethodSettings>, disabled: bool) -> Self {
267 Self { methods, disabled }
268 }
269
270 pub fn get_settings(
272 &self,
273 unit: &CurrencyUnit,
274 method: &PaymentMethod,
275 ) -> Option<MintMethodSettings> {
276 for method_settings in self.methods.iter() {
277 if method_settings.method.eq(method) && method_settings.unit.eq(unit) {
278 return Some(method_settings.clone());
279 }
280 }
281
282 None
283 }
284
285 pub fn remove_settings(
287 &mut self,
288 unit: &CurrencyUnit,
289 method: &PaymentMethod,
290 ) -> Option<MintMethodSettings> {
291 self.methods
292 .iter()
293 .position(|settings| &settings.method == method && &settings.unit == unit)
294 .map(|index| self.methods.remove(index))
295 }
296
297 pub fn supported_methods(&self) -> Vec<&PaymentMethod> {
299 self.methods.iter().map(|a| &a.method).collect()
300 }
301
302 pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
304 self.methods.iter().map(|s| &s.unit).collect()
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use serde_json::{from_str, json, to_string};
311
312 use super::*;
313
314 #[test]
315 fn test_mint_method_settings_top_level_description() {
316 let json_str = r#"{
318 "method": "bolt11",
319 "unit": "sat",
320 "min_amount": 0,
321 "max_amount": 10000,
322 "description": true
323 }"#;
324
325 let settings: MintMethodSettings = from_str(json_str).unwrap();
327
328 assert_eq!(settings.method, PaymentMethod::Bolt11);
330 assert_eq!(settings.unit, CurrencyUnit::Sat);
331 assert_eq!(settings.min_amount, Some(Amount::from(0)));
332 assert_eq!(settings.max_amount, Some(Amount::from(10000)));
333
334 match settings.options {
335 Some(MintMethodOptions::Bolt11 { description }) => {
336 assert!(description);
337 }
338 _ => panic!("Expected Bolt11 options with description = true"),
339 }
340
341 let serialized = to_string(&settings).unwrap();
343 let parsed: serde_json::Value = from_str(&serialized).unwrap();
344
345 assert_eq!(parsed["description"], json!(true));
347 }
348
349 #[test]
350 fn test_both_description_locations() {
351 let json_str = r#"{
353 "method": "bolt11",
354 "unit": "sat",
355 "min_amount": 0,
356 "max_amount": 10000,
357 "description": true,
358 "options": {
359 "description": false
360 }
361 }"#;
362
363 let settings: MintMethodSettings = from_str(json_str).unwrap();
365
366 match settings.options {
367 Some(MintMethodOptions::Bolt11 { description }) => {
368 assert!(description, "Top-level description should take precedence");
369 }
370 _ => panic!("Expected Bolt11 options with description = true"),
371 }
372 }
373}