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