1#[cfg(feature = "auth")]
6use std::collections::HashMap;
7
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10use super::nut01::PublicKey;
11use super::nut17::SupportedMethods;
12use super::nut19::CachedEndpoint;
13use super::{nut04, nut05, nut15, nut19, MppMethodSettings};
14#[cfg(feature = "auth")]
15use super::{AuthRequired, BlindAuthSettings, ClearAuthSettings, ProtectedEndpoint};
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
20pub struct MintVersion {
21 pub name: String,
23 pub version: String,
25}
26
27impl MintVersion {
28 pub fn new(name: String, version: String) -> Self {
30 Self { name, version }
31 }
32}
33
34impl std::fmt::Display for MintVersion {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 write!(f, "{}/{}", self.name, self.version)
37 }
38}
39
40impl Serialize for MintVersion {
41 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
42 where
43 S: Serializer,
44 {
45 let combined = format!("{}/{}", self.name, self.version);
46 serializer.serialize_str(&combined)
47 }
48}
49
50impl<'de> Deserialize<'de> for MintVersion {
51 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
52 where
53 D: Deserializer<'de>,
54 {
55 let combined = String::deserialize(deserializer)?;
56 let parts: Vec<&str> = combined.split('/').collect();
57 if parts.len() != 2 {
58 return Err(serde::de::Error::custom("Invalid input string"));
59 }
60 Ok(MintVersion {
61 name: parts[0].to_string(),
62 version: parts[1].to_string(),
63 })
64 }
65}
66
67#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
69#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
70pub struct MintInfo {
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub name: Option<String>,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub pubkey: Option<PublicKey>,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub version: Option<MintVersion>,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub description: Option<String>,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub description_long: Option<String>,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub contact: Option<Vec<ContactInfo>>,
89 pub nuts: Nuts,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub icon_url: Option<String>,
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub urls: Option<Vec<String>>,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub motd: Option<String>,
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub time: Option<u64>,
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub tos_url: Option<String>,
106}
107
108impl MintInfo {
109 pub fn new() -> Self {
111 Self::default()
112 }
113
114 pub fn name<S>(self, name: S) -> Self
116 where
117 S: Into<String>,
118 {
119 Self {
120 name: Some(name.into()),
121 ..self
122 }
123 }
124
125 pub fn pubkey(self, pubkey: PublicKey) -> Self {
127 Self {
128 pubkey: Some(pubkey),
129 ..self
130 }
131 }
132
133 pub fn version(self, mint_version: MintVersion) -> Self {
135 Self {
136 version: Some(mint_version),
137 ..self
138 }
139 }
140
141 pub fn description<S>(self, description: S) -> Self
143 where
144 S: Into<String>,
145 {
146 Self {
147 description: Some(description.into()),
148 ..self
149 }
150 }
151
152 pub fn long_description<S>(self, description_long: S) -> Self
154 where
155 S: Into<String>,
156 {
157 Self {
158 description_long: Some(description_long.into()),
159 ..self
160 }
161 }
162
163 pub fn contact_info(self, contact_info: Vec<ContactInfo>) -> Self {
165 Self {
166 contact: Some(contact_info),
167 ..self
168 }
169 }
170
171 pub fn nuts(self, nuts: Nuts) -> Self {
173 Self { nuts, ..self }
174 }
175
176 pub fn icon_url<S>(self, icon_url: S) -> Self
178 where
179 S: Into<String>,
180 {
181 Self {
182 icon_url: Some(icon_url.into()),
183 ..self
184 }
185 }
186
187 pub fn motd<S>(self, motd: S) -> Self
189 where
190 S: Into<String>,
191 {
192 Self {
193 motd: Some(motd.into()),
194 ..self
195 }
196 }
197
198 pub fn time<S>(self, time: S) -> Self
200 where
201 S: Into<u64>,
202 {
203 Self {
204 time: Some(time.into()),
205 ..self
206 }
207 }
208
209 pub fn tos_url<S>(self, tos_url: S) -> Self
211 where
212 S: Into<String>,
213 {
214 Self {
215 tos_url: Some(tos_url.into()),
216 ..self
217 }
218 }
219
220 #[cfg(feature = "auth")]
222 pub fn protected_endpoints(&self) -> HashMap<ProtectedEndpoint, AuthRequired> {
223 let mut protected_endpoints = HashMap::new();
224
225 if let Some(nut21_settings) = &self.nuts.nut21 {
226 for endpoint in nut21_settings.protected_endpoints.iter() {
227 protected_endpoints.insert(*endpoint, AuthRequired::Clear);
228 }
229 }
230
231 if let Some(nut22_settings) = &self.nuts.nut22 {
232 for endpoint in nut22_settings.protected_endpoints.iter() {
233 protected_endpoints.insert(*endpoint, AuthRequired::Blind);
234 }
235 }
236 protected_endpoints
237 }
238
239 #[cfg(feature = "auth")]
241 pub fn openid_discovery(&self) -> Option<String> {
242 self.nuts
243 .nut21
244 .as_ref()
245 .map(|s| s.openid_discovery.to_string())
246 }
247
248 #[cfg(feature = "auth")]
250 pub fn client_id(&self) -> Option<String> {
251 self.nuts.nut21.as_ref().map(|s| s.client_id.clone())
252 }
253
254 #[cfg(feature = "auth")]
256 pub fn bat_max_mint(&self) -> Option<u64> {
257 self.nuts.nut22.as_ref().map(|s| s.bat_max_mint)
258 }
259}
260
261#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
263#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
264pub struct Nuts {
265 #[serde(default)]
267 #[serde(rename = "4")]
268 pub nut04: nut04::Settings,
269 #[serde(default)]
271 #[serde(rename = "5")]
272 pub nut05: nut05::Settings,
273 #[serde(default)]
275 #[serde(rename = "7")]
276 pub nut07: SupportedSettings,
277 #[serde(default)]
279 #[serde(rename = "8")]
280 pub nut08: SupportedSettings,
281 #[serde(default)]
283 #[serde(rename = "9")]
284 pub nut09: SupportedSettings,
285 #[serde(rename = "10")]
287 #[serde(default)]
288 pub nut10: SupportedSettings,
289 #[serde(rename = "11")]
291 #[serde(default)]
292 pub nut11: SupportedSettings,
293 #[serde(default)]
295 #[serde(rename = "12")]
296 pub nut12: SupportedSettings,
297 #[serde(default)]
299 #[serde(rename = "14")]
300 pub nut14: SupportedSettings,
301 #[serde(default)]
303 #[serde(rename = "15")]
304 pub nut15: nut15::Settings,
305 #[serde(default)]
307 #[serde(rename = "17")]
308 pub nut17: super::nut17::SupportedSettings,
309 #[serde(default)]
311 #[serde(rename = "19")]
312 pub nut19: nut19::Settings,
313 #[serde(default)]
315 #[serde(rename = "20")]
316 pub nut20: SupportedSettings,
317 #[serde(rename = "21")]
319 #[serde(skip_serializing_if = "Option::is_none")]
320 #[cfg(feature = "auth")]
321 pub nut21: Option<ClearAuthSettings>,
322 #[serde(rename = "22")]
324 #[serde(skip_serializing_if = "Option::is_none")]
325 #[cfg(feature = "auth")]
326 pub nut22: Option<BlindAuthSettings>,
327}
328
329impl Nuts {
330 pub fn new() -> Self {
332 Self::default()
333 }
334
335 pub fn nut04(self, nut04_settings: nut04::Settings) -> Self {
337 Self {
338 nut04: nut04_settings,
339 ..self
340 }
341 }
342
343 pub fn nut05(self, nut05_settings: nut05::Settings) -> Self {
345 Self {
346 nut05: nut05_settings,
347 ..self
348 }
349 }
350
351 pub fn nut07(self, supported: bool) -> Self {
353 Self {
354 nut07: SupportedSettings { supported },
355 ..self
356 }
357 }
358
359 pub fn nut08(self, supported: bool) -> Self {
361 Self {
362 nut08: SupportedSettings { supported },
363 ..self
364 }
365 }
366
367 pub fn nut09(self, supported: bool) -> Self {
369 Self {
370 nut09: SupportedSettings { supported },
371 ..self
372 }
373 }
374
375 pub fn nut10(self, supported: bool) -> Self {
377 Self {
378 nut10: SupportedSettings { supported },
379 ..self
380 }
381 }
382
383 pub fn nut11(self, supported: bool) -> Self {
385 Self {
386 nut11: SupportedSettings { supported },
387 ..self
388 }
389 }
390
391 pub fn nut12(self, supported: bool) -> Self {
393 Self {
394 nut12: SupportedSettings { supported },
395 ..self
396 }
397 }
398
399 pub fn nut14(self, supported: bool) -> Self {
401 Self {
402 nut14: SupportedSettings { supported },
403 ..self
404 }
405 }
406
407 pub fn nut15(self, mpp_settings: Vec<MppMethodSettings>) -> Self {
409 Self {
410 nut15: nut15::Settings {
411 methods: mpp_settings,
412 },
413 ..self
414 }
415 }
416
417 pub fn nut17(self, supported: Vec<SupportedMethods>) -> Self {
419 Self {
420 nut17: super::nut17::SupportedSettings { supported },
421 ..self
422 }
423 }
424
425 pub fn nut19(self, ttl: Option<u64>, cached_endpoints: Vec<CachedEndpoint>) -> Self {
427 Self {
428 nut19: nut19::Settings {
429 ttl,
430 cached_endpoints,
431 },
432 ..self
433 }
434 }
435
436 pub fn nut20(self, supported: bool) -> Self {
438 Self {
439 nut20: SupportedSettings { supported },
440 ..self
441 }
442 }
443}
444
445#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)]
447#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
448pub struct SupportedSettings {
449 supported: bool,
450}
451
452#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
454#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
455pub struct ContactInfo {
456 pub method: String,
458 pub info: String,
460}
461
462impl ContactInfo {
463 pub fn new(method: String, info: String) -> Self {
465 Self { method, info }
466 }
467}
468
469#[cfg(test)]
470mod tests {
471
472 use super::*;
473
474 #[test]
475 fn test_des_mint_into() {
476 let mint_info_str = r#"{
477"name": "Cashu mint",
478"pubkey": "0296d0aa13b6a31cf0cd974249f28c7b7176d7274712c95a41c7d8066d3f29d679",
479"version": "Nutshell/0.15.3",
480"contact": [
481 ["", ""],
482 ["", ""]
483 ],
484 "nuts": {
485 "4": {
486 "methods": [
487 {"method": "bolt11", "unit": "sat", "description": true},
488 {"method": "bolt11", "unit": "usd", "description": true}
489 ],
490 "disabled": false
491 },
492 "5": {
493 "methods": [
494 {"method": "bolt11", "unit": "sat"},
495 {"method": "bolt11", "unit": "usd"}
496 ],
497 "disabled": false
498 },
499 "7": {"supported": true},
500 "8": {"supported": true},
501 "9": {"supported": true},
502 "10": {"supported": true},
503 "11": {"supported": true}
504 },
505"tos_url": "https://cashu.mint/tos"
506}"#;
507
508 let _mint_info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
509 }
510
511 #[test]
512 fn test_ser_mint_info() {
513 let mint_info_str = r#"
529{
530 "name": "Bob's Cashu mint",
531 "pubkey": "0283bf290884eed3a7ca2663fc0260de2e2064d6b355ea13f98dec004b7a7ead99",
532 "version": "Nutshell/0.15.0",
533 "description": "The short mint description",
534 "description_long": "A description that can be a long piece of text.",
535 "contact": [
536 {
537 "method": "nostr",
538 "info": "xxxxx"
539 },
540 {
541 "method": "email",
542 "info": "contact@me.com"
543 }
544 ],
545 "motd": "Message to display to users.",
546 "icon_url": "https://this-is-a-mint-icon-url.com/icon.png",
547 "nuts": {
548 "4": {
549 "methods": [
550 {
551 "method": "bolt11",
552 "unit": "sat",
553 "min_amount": 0,
554 "max_amount": 10000,
555 "description": true
556 }
557 ],
558 "disabled": false
559 },
560 "5": {
561 "methods": [
562 {
563 "method": "bolt11",
564 "unit": "sat",
565 "min_amount": 0,
566 "max_amount": 10000
567 }
568 ],
569 "disabled": false
570 },
571 "7": {"supported": true},
572 "8": {"supported": true},
573 "9": {"supported": true},
574 "10": {"supported": true},
575 "12": {"supported": true}
576 },
577 "tos_url": "https://cashu.mint/tos"
578}"#;
579 let info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
580 let mint_info_str = r#"
581{
582 "name": "Bob's Cashu mint",
583 "pubkey": "0283bf290884eed3a7ca2663fc0260de2e2064d6b355ea13f98dec004b7a7ead99",
584 "version": "Nutshell/0.15.0",
585 "description": "The short mint description",
586 "description_long": "A description that can be a long piece of text.",
587 "contact": [
588 ["nostr", "xxxxx"],
589 ["email", "contact@me.com"]
590 ],
591 "motd": "Message to display to users.",
592 "icon_url": "https://this-is-a-mint-icon-url.com/icon.png",
593 "nuts": {
594 "4": {
595 "methods": [
596 {
597 "method": "bolt11",
598 "unit": "sat",
599 "min_amount": 0,
600 "max_amount": 10000,
601 "description": true
602 }
603 ],
604 "disabled": false
605 },
606 "5": {
607 "methods": [
608 {
609 "method": "bolt11",
610 "unit": "sat",
611 "min_amount": 0,
612 "max_amount": 10000
613 }
614 ],
615 "disabled": false
616 },
617 "7": {"supported": true},
618 "8": {"supported": true},
619 "9": {"supported": true},
620 "10": {"supported": true},
621 "12": {"supported": true}
622 },
623 "tos_url": "https://cashu.mint/tos"
624}"#;
625 let mint_info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
626
627 assert_eq!(info, mint_info);
628 }
629}