nut06.rs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714
  1. //! NUT-06: Mint Information
  2. //!
  3. //! <https://github.com/cashubtc/nuts/blob/main/06.md>
  4. #[cfg(feature = "auth")]
  5. use std::collections::HashMap;
  6. use std::collections::HashSet;
  7. use serde::{Deserialize, Deserializer, Serialize, Serializer};
  8. use super::nut01::PublicKey;
  9. use super::nut17::SupportedMethods;
  10. use super::nut19::CachedEndpoint;
  11. use super::{nut04, nut05, nut15, nut19, MppMethodSettings};
  12. #[cfg(feature = "auth")]
  13. use super::{AuthRequired, BlindAuthSettings, ClearAuthSettings, ProtectedEndpoint};
  14. use crate::CurrencyUnit;
  15. /// Mint Version
  16. #[derive(Debug, Clone, PartialEq, Eq, Hash)]
  17. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  18. pub struct MintVersion {
  19. /// Mint Software name
  20. pub name: String,
  21. /// Mint Version
  22. pub version: String,
  23. }
  24. impl MintVersion {
  25. /// Create new [`MintVersion`]
  26. pub fn new(name: String, version: String) -> Self {
  27. Self { name, version }
  28. }
  29. }
  30. impl std::fmt::Display for MintVersion {
  31. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  32. write!(f, "{}/{}", self.name, self.version)
  33. }
  34. }
  35. impl Serialize for MintVersion {
  36. fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
  37. where
  38. S: Serializer,
  39. {
  40. let combined = format!("{}/{}", self.name, self.version);
  41. serializer.serialize_str(&combined)
  42. }
  43. }
  44. impl<'de> Deserialize<'de> for MintVersion {
  45. fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  46. where
  47. D: Deserializer<'de>,
  48. {
  49. let combined = String::deserialize(deserializer)?;
  50. let parts: Vec<&str> = combined.split('/').collect();
  51. if parts.len() != 2 {
  52. return Err(serde::de::Error::custom("Invalid input string"));
  53. }
  54. Ok(MintVersion {
  55. name: parts[0].to_string(),
  56. version: parts[1].to_string(),
  57. })
  58. }
  59. }
  60. /// Mint Info [NUT-06]
  61. #[derive(Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  62. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  63. pub struct MintInfo {
  64. /// name of the mint and should be recognizable
  65. #[serde(skip_serializing_if = "Option::is_none")]
  66. pub name: Option<String>,
  67. /// hex pubkey of the mint
  68. #[serde(skip_serializing_if = "Option::is_none")]
  69. pub pubkey: Option<PublicKey>,
  70. /// implementation name and the version running
  71. #[serde(skip_serializing_if = "Option::is_none")]
  72. pub version: Option<MintVersion>,
  73. /// short description of the mint
  74. #[serde(skip_serializing_if = "Option::is_none")]
  75. pub description: Option<String>,
  76. /// long description
  77. #[serde(skip_serializing_if = "Option::is_none")]
  78. pub description_long: Option<String>,
  79. /// Contact info
  80. #[serde(skip_serializing_if = "Option::is_none")]
  81. pub contact: Option<Vec<ContactInfo>>,
  82. /// shows which NUTs the mint supports
  83. pub nuts: Nuts,
  84. /// Mint's icon URL
  85. #[serde(skip_serializing_if = "Option::is_none")]
  86. pub icon_url: Option<String>,
  87. /// Mint's endpoint URLs
  88. #[serde(skip_serializing_if = "Option::is_none")]
  89. pub urls: Option<Vec<String>>,
  90. /// message of the day that the wallet must display to the user
  91. #[serde(skip_serializing_if = "Option::is_none")]
  92. pub motd: Option<String>,
  93. /// server unix timestamp
  94. #[serde(skip_serializing_if = "Option::is_none")]
  95. pub time: Option<u64>,
  96. /// terms of url service of the mint
  97. #[serde(skip_serializing_if = "Option::is_none")]
  98. pub tos_url: Option<String>,
  99. }
  100. impl MintInfo {
  101. /// Create new [`MintInfo`]
  102. pub fn new() -> Self {
  103. Self::default()
  104. }
  105. /// Set name
  106. pub fn name<S>(self, name: S) -> Self
  107. where
  108. S: Into<String>,
  109. {
  110. Self {
  111. name: Some(name.into()),
  112. ..self
  113. }
  114. }
  115. /// Set pubkey
  116. pub fn pubkey(self, pubkey: PublicKey) -> Self {
  117. Self {
  118. pubkey: Some(pubkey),
  119. ..self
  120. }
  121. }
  122. /// Set [`MintVersion`]
  123. pub fn version(self, mint_version: MintVersion) -> Self {
  124. Self {
  125. version: Some(mint_version),
  126. ..self
  127. }
  128. }
  129. /// Set description
  130. pub fn description<S>(self, description: S) -> Self
  131. where
  132. S: Into<String>,
  133. {
  134. Self {
  135. description: Some(description.into()),
  136. ..self
  137. }
  138. }
  139. /// Set long description
  140. pub fn long_description<S>(self, description_long: S) -> Self
  141. where
  142. S: Into<String>,
  143. {
  144. Self {
  145. description_long: Some(description_long.into()),
  146. ..self
  147. }
  148. }
  149. /// Set contact info
  150. pub fn contact_info(self, contact_info: Vec<ContactInfo>) -> Self {
  151. Self {
  152. contact: Some(contact_info),
  153. ..self
  154. }
  155. }
  156. /// Set nuts
  157. pub fn nuts(self, nuts: Nuts) -> Self {
  158. Self { nuts, ..self }
  159. }
  160. /// Set mint icon url
  161. pub fn icon_url<S>(self, icon_url: S) -> Self
  162. where
  163. S: Into<String>,
  164. {
  165. Self {
  166. icon_url: Some(icon_url.into()),
  167. ..self
  168. }
  169. }
  170. /// Set motd
  171. pub fn motd<S>(self, motd: S) -> Self
  172. where
  173. S: Into<String>,
  174. {
  175. Self {
  176. motd: Some(motd.into()),
  177. ..self
  178. }
  179. }
  180. /// Set time
  181. pub fn time<S>(self, time: S) -> Self
  182. where
  183. S: Into<u64>,
  184. {
  185. Self {
  186. time: Some(time.into()),
  187. ..self
  188. }
  189. }
  190. /// Set tos_url
  191. pub fn tos_url<S>(self, tos_url: S) -> Self
  192. where
  193. S: Into<String>,
  194. {
  195. Self {
  196. tos_url: Some(tos_url.into()),
  197. ..self
  198. }
  199. }
  200. /// Get protected endpoints
  201. #[cfg(feature = "auth")]
  202. pub fn protected_endpoints(&self) -> HashMap<ProtectedEndpoint, AuthRequired> {
  203. let mut protected_endpoints = HashMap::new();
  204. if let Some(nut21_settings) = &self.nuts.nut21 {
  205. for endpoint in nut21_settings.protected_endpoints.iter() {
  206. protected_endpoints.insert(*endpoint, AuthRequired::Clear);
  207. }
  208. }
  209. if let Some(nut22_settings) = &self.nuts.nut22 {
  210. for endpoint in nut22_settings.protected_endpoints.iter() {
  211. protected_endpoints.insert(*endpoint, AuthRequired::Blind);
  212. }
  213. }
  214. protected_endpoints
  215. }
  216. /// Get Openid discovery of the mint if it is set
  217. #[cfg(feature = "auth")]
  218. pub fn openid_discovery(&self) -> Option<String> {
  219. self.nuts
  220. .nut21
  221. .as_ref()
  222. .map(|s| s.openid_discovery.to_string())
  223. }
  224. /// Get Openid discovery of the mint if it is set
  225. #[cfg(feature = "auth")]
  226. pub fn client_id(&self) -> Option<String> {
  227. self.nuts.nut21.as_ref().map(|s| s.client_id.clone())
  228. }
  229. /// Max bat mint
  230. #[cfg(feature = "auth")]
  231. pub fn bat_max_mint(&self) -> Option<u64> {
  232. self.nuts.nut22.as_ref().map(|s| s.bat_max_mint)
  233. }
  234. /// Get all supported currency units for this mint (both mint and melt)
  235. pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
  236. let mut units = HashSet::new();
  237. units.extend(self.nuts.supported_mint_units());
  238. units.extend(self.nuts.supported_melt_units());
  239. units.into_iter().collect()
  240. }
  241. }
  242. /// Supported nuts and settings
  243. #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  244. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  245. pub struct Nuts {
  246. /// NUT04 Settings
  247. #[serde(default)]
  248. #[serde(rename = "4")]
  249. pub nut04: nut04::Settings,
  250. /// NUT05 Settings
  251. #[serde(default)]
  252. #[serde(rename = "5")]
  253. pub nut05: nut05::Settings,
  254. /// NUT07 Settings
  255. #[serde(default)]
  256. #[serde(rename = "7")]
  257. pub nut07: SupportedSettings,
  258. /// NUT08 Settings
  259. #[serde(default)]
  260. #[serde(rename = "8")]
  261. pub nut08: SupportedSettings,
  262. /// NUT09 Settings
  263. #[serde(default)]
  264. #[serde(rename = "9")]
  265. pub nut09: SupportedSettings,
  266. /// NUT10 Settings
  267. #[serde(rename = "10")]
  268. #[serde(default)]
  269. pub nut10: SupportedSettings,
  270. /// NUT11 Settings
  271. #[serde(rename = "11")]
  272. #[serde(default)]
  273. pub nut11: SupportedSettings,
  274. /// NUT12 Settings
  275. #[serde(default)]
  276. #[serde(rename = "12")]
  277. pub nut12: SupportedSettings,
  278. /// NUT14 Settings
  279. #[serde(default)]
  280. #[serde(rename = "14")]
  281. pub nut14: SupportedSettings,
  282. /// NUT15 Settings
  283. #[serde(default)]
  284. #[serde(rename = "15")]
  285. #[serde(skip_serializing_if = "nut15::Settings::is_empty")]
  286. pub nut15: nut15::Settings,
  287. /// NUT17 Settings
  288. #[serde(default)]
  289. #[serde(rename = "17")]
  290. pub nut17: super::nut17::SupportedSettings,
  291. /// NUT19 Settings
  292. #[serde(default)]
  293. #[serde(rename = "19")]
  294. pub nut19: nut19::Settings,
  295. /// NUT20 Settings
  296. #[serde(default)]
  297. #[serde(rename = "20")]
  298. pub nut20: SupportedSettings,
  299. /// NUT21 Settings
  300. #[serde(rename = "21")]
  301. #[serde(skip_serializing_if = "Option::is_none")]
  302. #[cfg(feature = "auth")]
  303. pub nut21: Option<ClearAuthSettings>,
  304. /// NUT22 Settings
  305. #[serde(rename = "22")]
  306. #[serde(skip_serializing_if = "Option::is_none")]
  307. #[cfg(feature = "auth")]
  308. pub nut22: Option<BlindAuthSettings>,
  309. }
  310. impl Nuts {
  311. /// Create new [`Nuts`]
  312. pub fn new() -> Self {
  313. Self::default()
  314. }
  315. /// Nut04 settings
  316. pub fn nut04(self, nut04_settings: nut04::Settings) -> Self {
  317. Self {
  318. nut04: nut04_settings,
  319. ..self
  320. }
  321. }
  322. /// Nut05 settings
  323. pub fn nut05(self, nut05_settings: nut05::Settings) -> Self {
  324. Self {
  325. nut05: nut05_settings,
  326. ..self
  327. }
  328. }
  329. /// Nut07 settings
  330. pub fn nut07(self, supported: bool) -> Self {
  331. Self {
  332. nut07: SupportedSettings { supported },
  333. ..self
  334. }
  335. }
  336. /// Nut08 settings
  337. pub fn nut08(self, supported: bool) -> Self {
  338. Self {
  339. nut08: SupportedSettings { supported },
  340. ..self
  341. }
  342. }
  343. /// Nut09 settings
  344. pub fn nut09(self, supported: bool) -> Self {
  345. Self {
  346. nut09: SupportedSettings { supported },
  347. ..self
  348. }
  349. }
  350. /// Nut10 settings
  351. pub fn nut10(self, supported: bool) -> Self {
  352. Self {
  353. nut10: SupportedSettings { supported },
  354. ..self
  355. }
  356. }
  357. /// Nut11 settings
  358. pub fn nut11(self, supported: bool) -> Self {
  359. Self {
  360. nut11: SupportedSettings { supported },
  361. ..self
  362. }
  363. }
  364. /// Nut12 settings
  365. pub fn nut12(self, supported: bool) -> Self {
  366. Self {
  367. nut12: SupportedSettings { supported },
  368. ..self
  369. }
  370. }
  371. /// Nut14 settings
  372. pub fn nut14(self, supported: bool) -> Self {
  373. Self {
  374. nut14: SupportedSettings { supported },
  375. ..self
  376. }
  377. }
  378. /// Nut15 settings
  379. pub fn nut15(self, mpp_settings: Vec<MppMethodSettings>) -> Self {
  380. Self {
  381. nut15: nut15::Settings {
  382. methods: mpp_settings,
  383. },
  384. ..self
  385. }
  386. }
  387. /// Nut17 settings
  388. pub fn nut17(self, supported: Vec<SupportedMethods>) -> Self {
  389. Self {
  390. nut17: super::nut17::SupportedSettings { supported },
  391. ..self
  392. }
  393. }
  394. /// Nut19 settings
  395. pub fn nut19(self, ttl: Option<u64>, cached_endpoints: Vec<CachedEndpoint>) -> Self {
  396. Self {
  397. nut19: nut19::Settings {
  398. ttl,
  399. cached_endpoints,
  400. },
  401. ..self
  402. }
  403. }
  404. /// Nut20 settings
  405. pub fn nut20(self, supported: bool) -> Self {
  406. Self {
  407. nut20: SupportedSettings { supported },
  408. ..self
  409. }
  410. }
  411. /// Units where minting is supported
  412. pub fn supported_mint_units(&self) -> Vec<&CurrencyUnit> {
  413. self.nut04
  414. .methods
  415. .iter()
  416. .map(|s| &s.unit)
  417. .collect::<HashSet<_>>()
  418. .into_iter()
  419. .collect()
  420. }
  421. /// Units where melting is supported
  422. pub fn supported_melt_units(&self) -> Vec<&CurrencyUnit> {
  423. self.nut05
  424. .methods
  425. .iter()
  426. .map(|s| &s.unit)
  427. .collect::<HashSet<_>>()
  428. .into_iter()
  429. .collect()
  430. }
  431. }
  432. /// Check state Settings
  433. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash, Serialize, Deserialize)]
  434. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  435. pub struct SupportedSettings {
  436. /// Setting supported
  437. pub supported: bool,
  438. }
  439. /// Contact Info
  440. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  441. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  442. pub struct ContactInfo {
  443. /// Contact Method i.e. nostr
  444. pub method: String,
  445. /// Contact info i.e. npub...
  446. pub info: String,
  447. }
  448. impl ContactInfo {
  449. /// Create new [`ContactInfo`]
  450. pub fn new(method: String, info: String) -> Self {
  451. Self { method, info }
  452. }
  453. }
  454. #[cfg(test)]
  455. mod tests {
  456. use super::*;
  457. use crate::nut04::MintMethodOptions;
  458. #[test]
  459. fn test_des_mint_into() {
  460. let mint_info_str = r#"{
  461. "name": "Cashu mint",
  462. "pubkey": "0296d0aa13b6a31cf0cd974249f28c7b7176d7274712c95a41c7d8066d3f29d679",
  463. "version": "Nutshell/0.15.3",
  464. "contact": [
  465. ["", ""],
  466. ["", ""]
  467. ],
  468. "nuts": {
  469. "4": {
  470. "methods": [
  471. {"method": "bolt11", "unit": "sat", "description": true},
  472. {"method": "bolt11", "unit": "usd", "description": true}
  473. ],
  474. "disabled": false
  475. },
  476. "5": {
  477. "methods": [
  478. {"method": "bolt11", "unit": "sat"},
  479. {"method": "bolt11", "unit": "usd"}
  480. ],
  481. "disabled": false
  482. },
  483. "7": {"supported": true},
  484. "8": {"supported": true},
  485. "9": {"supported": true},
  486. "10": {"supported": true},
  487. "11": {"supported": true}
  488. },
  489. "tos_url": "https://cashu.mint/tos"
  490. }"#;
  491. let _mint_info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
  492. }
  493. #[test]
  494. fn test_ser_mint_info() {
  495. /*
  496. let mint_info = serde_json::to_string(&MintInfo {
  497. name: Some("Cashu-crab".to_string()),
  498. pubkey: None,
  499. version: None,
  500. description: Some("A mint".to_string()),
  501. description_long: Some("Some longer test".to_string()),
  502. contact: None,
  503. nuts: Nuts::default(),
  504. motd: None,
  505. })
  506. .unwrap();
  507. println!("{}", mint_info);
  508. */
  509. let mint_info_str = r#"
  510. {
  511. "name": "Bob's Cashu mint",
  512. "pubkey": "0283bf290884eed3a7ca2663fc0260de2e2064d6b355ea13f98dec004b7a7ead99",
  513. "version": "Nutshell/0.15.0",
  514. "description": "The short mint description",
  515. "description_long": "A description that can be a long piece of text.",
  516. "contact": [
  517. {
  518. "method": "nostr",
  519. "info": "xxxxx"
  520. },
  521. {
  522. "method": "email",
  523. "info": "contact@me.com"
  524. }
  525. ],
  526. "motd": "Message to display to users.",
  527. "icon_url": "https://this-is-a-mint-icon-url.com/icon.png",
  528. "nuts": {
  529. "4": {
  530. "methods": [
  531. {
  532. "method": "bolt11",
  533. "unit": "sat",
  534. "min_amount": 0,
  535. "max_amount": 10000,
  536. "options": {
  537. "description": true
  538. }
  539. }
  540. ],
  541. "disabled": false
  542. },
  543. "5": {
  544. "methods": [
  545. {
  546. "method": "bolt11",
  547. "unit": "sat",
  548. "min_amount": 0,
  549. "max_amount": 10000
  550. }
  551. ],
  552. "disabled": false
  553. },
  554. "7": {"supported": true},
  555. "8": {"supported": true},
  556. "9": {"supported": true},
  557. "10": {"supported": true},
  558. "12": {"supported": true}
  559. },
  560. "tos_url": "https://cashu.mint/tos"
  561. }"#;
  562. let info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
  563. let mint_info_str = r#"
  564. {
  565. "name": "Bob's Cashu mint",
  566. "pubkey": "0283bf290884eed3a7ca2663fc0260de2e2064d6b355ea13f98dec004b7a7ead99",
  567. "version": "Nutshell/0.15.0",
  568. "description": "The short mint description",
  569. "description_long": "A description that can be a long piece of text.",
  570. "contact": [
  571. ["nostr", "xxxxx"],
  572. ["email", "contact@me.com"]
  573. ],
  574. "motd": "Message to display to users.",
  575. "icon_url": "https://this-is-a-mint-icon-url.com/icon.png",
  576. "nuts": {
  577. "4": {
  578. "methods": [
  579. {
  580. "method": "bolt11",
  581. "unit": "sat",
  582. "min_amount": 0,
  583. "max_amount": 10000,
  584. "options": {
  585. "description": true
  586. }
  587. }
  588. ],
  589. "disabled": false
  590. },
  591. "5": {
  592. "methods": [
  593. {
  594. "method": "bolt11",
  595. "unit": "sat",
  596. "min_amount": 0,
  597. "max_amount": 10000
  598. }
  599. ],
  600. "disabled": false
  601. },
  602. "7": {"supported": true},
  603. "8": {"supported": true},
  604. "9": {"supported": true},
  605. "10": {"supported": true},
  606. "12": {"supported": true}
  607. },
  608. "tos_url": "https://cashu.mint/tos"
  609. }"#;
  610. let mint_info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
  611. let t = mint_info
  612. .nuts
  613. .nut04
  614. .get_settings(&crate::CurrencyUnit::Sat, &crate::PaymentMethod::Bolt11)
  615. .unwrap();
  616. let t = t.options.unwrap();
  617. matches!(t, MintMethodOptions::Bolt11 { description: true });
  618. assert_eq!(info, mint_info);
  619. }
  620. #[test]
  621. fn test_nut15_not_serialized_when_empty() {
  622. // Test with default (empty) NUT15
  623. let mint_info = MintInfo {
  624. name: Some("Test Mint".to_string()),
  625. nuts: Nuts::default(),
  626. ..Default::default()
  627. };
  628. let json = serde_json::to_string(&mint_info).unwrap();
  629. let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
  630. // NUT15 should not be present in the nuts object when methods is empty
  631. assert!(parsed["nuts"]["15"].is_null());
  632. // Test with non-empty NUT15
  633. let mint_info_with_nut15 = MintInfo {
  634. name: Some("Test Mint".to_string()),
  635. nuts: Nuts::default().nut15(vec![MppMethodSettings {
  636. method: crate::PaymentMethod::Bolt11,
  637. unit: crate::CurrencyUnit::Sat,
  638. }]),
  639. ..Default::default()
  640. };
  641. let json = serde_json::to_string(&mint_info_with_nut15).unwrap();
  642. let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
  643. // NUT15 should be present when methods is not empty
  644. assert!(!parsed["nuts"]["15"].is_null());
  645. assert!(parsed["nuts"]["15"]["methods"].is_array());
  646. assert_eq!(parsed["nuts"]["15"]["methods"].as_array().unwrap().len(), 1);
  647. }
  648. }