nut06.rs 17 KB

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