nut04.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. //! NUT-04: Mint Tokens via Bolt11
  2. //!
  3. //! <https://github.com/cashubtc/nuts/blob/main/04.md>
  4. use std::fmt;
  5. #[cfg(feature = "mint")]
  6. use std::str::FromStr;
  7. use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, Visitor};
  8. use serde::ser::{SerializeStruct, Serializer};
  9. use serde::{Deserialize, Serialize};
  10. use thiserror::Error;
  11. use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod};
  12. #[cfg(feature = "mint")]
  13. use crate::quote_id::QuoteId;
  14. #[cfg(feature = "mint")]
  15. use crate::quote_id::QuoteIdError;
  16. use crate::Amount;
  17. /// NUT04 Error
  18. #[derive(Debug, Error)]
  19. pub enum Error {
  20. /// Unknown Quote State
  21. #[error("Unknown Quote State")]
  22. UnknownState,
  23. /// Amount overflow
  24. #[error("Amount overflow")]
  25. AmountOverflow,
  26. }
  27. /// Mint request [NUT-04]
  28. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  29. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  30. #[serde(bound = "Q: Serialize + DeserializeOwned")]
  31. pub struct MintRequest<Q> {
  32. /// Quote id
  33. #[cfg_attr(feature = "swagger", schema(max_length = 1_000))]
  34. pub quote: Q,
  35. /// Outputs
  36. #[cfg_attr(feature = "swagger", schema(max_items = 1_000))]
  37. pub outputs: Vec<BlindedMessage>,
  38. /// Signature
  39. #[serde(skip_serializing_if = "Option::is_none")]
  40. pub signature: Option<String>,
  41. }
  42. #[cfg(feature = "mint")]
  43. impl TryFrom<MintRequest<String>> for MintRequest<QuoteId> {
  44. type Error = QuoteIdError;
  45. fn try_from(value: MintRequest<String>) -> Result<Self, Self::Error> {
  46. Ok(Self {
  47. quote: QuoteId::from_str(&value.quote)?,
  48. outputs: value.outputs,
  49. signature: value.signature,
  50. })
  51. }
  52. }
  53. impl<Q> MintRequest<Q> {
  54. /// Total [`Amount`] of outputs
  55. pub fn total_amount(&self) -> Result<Amount, Error> {
  56. Amount::try_sum(
  57. self.outputs
  58. .iter()
  59. .map(|BlindedMessage { amount, .. }| *amount),
  60. )
  61. .map_err(|_| Error::AmountOverflow)
  62. }
  63. }
  64. /// Mint response [NUT-04]
  65. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  66. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  67. pub struct MintResponse {
  68. /// Blinded Signatures
  69. pub signatures: Vec<BlindSignature>,
  70. }
  71. /// Mint Method Settings
  72. #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
  73. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  74. pub struct MintMethodSettings {
  75. /// Payment Method e.g. bolt11
  76. pub method: PaymentMethod,
  77. /// Currency Unit e.g. sat
  78. pub unit: CurrencyUnit,
  79. /// Min Amount
  80. pub min_amount: Option<Amount>,
  81. /// Max Amount
  82. pub max_amount: Option<Amount>,
  83. /// Options
  84. pub options: Option<MintMethodOptions>,
  85. }
  86. impl Serialize for MintMethodSettings {
  87. fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
  88. where
  89. S: Serializer,
  90. {
  91. let mut num_fields = 3; // method and unit are always present
  92. if self.min_amount.is_some() {
  93. num_fields += 1;
  94. }
  95. if self.max_amount.is_some() {
  96. num_fields += 1;
  97. }
  98. let mut description_in_top_level = false;
  99. if let Some(MintMethodOptions::Bolt11 { description }) = &self.options {
  100. if *description {
  101. num_fields += 1;
  102. description_in_top_level = true;
  103. }
  104. }
  105. let mut state = serializer.serialize_struct("MintMethodSettings", num_fields)?;
  106. state.serialize_field("method", &self.method)?;
  107. state.serialize_field("unit", &self.unit)?;
  108. if let Some(min_amount) = &self.min_amount {
  109. state.serialize_field("min_amount", min_amount)?;
  110. }
  111. if let Some(max_amount) = &self.max_amount {
  112. state.serialize_field("max_amount", max_amount)?;
  113. }
  114. // If there's a description flag in Bolt11 options, add it at the top level
  115. if description_in_top_level {
  116. state.serialize_field("description", &true)?;
  117. }
  118. state.end()
  119. }
  120. }
  121. struct MintMethodSettingsVisitor;
  122. impl<'de> Visitor<'de> for MintMethodSettingsVisitor {
  123. type Value = MintMethodSettings;
  124. fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
  125. formatter.write_str("a MintMethodSettings structure")
  126. }
  127. fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
  128. where
  129. M: MapAccess<'de>,
  130. {
  131. let mut method: Option<PaymentMethod> = None;
  132. let mut unit: Option<CurrencyUnit> = None;
  133. let mut min_amount: Option<Amount> = None;
  134. let mut max_amount: Option<Amount> = None;
  135. let mut description: Option<bool> = None;
  136. while let Some(key) = map.next_key::<String>()? {
  137. match key.as_str() {
  138. "method" => {
  139. if method.is_some() {
  140. return Err(de::Error::duplicate_field("method"));
  141. }
  142. method = Some(map.next_value()?);
  143. }
  144. "unit" => {
  145. if unit.is_some() {
  146. return Err(de::Error::duplicate_field("unit"));
  147. }
  148. unit = Some(map.next_value()?);
  149. }
  150. "min_amount" => {
  151. if min_amount.is_some() {
  152. return Err(de::Error::duplicate_field("min_amount"));
  153. }
  154. min_amount = Some(map.next_value()?);
  155. }
  156. "max_amount" => {
  157. if max_amount.is_some() {
  158. return Err(de::Error::duplicate_field("max_amount"));
  159. }
  160. max_amount = Some(map.next_value()?);
  161. }
  162. "description" => {
  163. if description.is_some() {
  164. return Err(de::Error::duplicate_field("description"));
  165. }
  166. description = Some(map.next_value()?);
  167. }
  168. "options" => {
  169. // If there are explicit options, they take precedence, except the description
  170. // field which we will handle specially
  171. let options: Option<MintMethodOptions> = map.next_value()?;
  172. if let Some(MintMethodOptions::Bolt11 {
  173. description: desc_from_options,
  174. }) = options
  175. {
  176. // If we already found a top-level description, use that instead
  177. if description.is_none() {
  178. description = Some(desc_from_options);
  179. }
  180. }
  181. }
  182. _ => {
  183. // Skip unknown fields
  184. let _: serde::de::IgnoredAny = map.next_value()?;
  185. }
  186. }
  187. }
  188. let method = method.ok_or_else(|| de::Error::missing_field("method"))?;
  189. let unit = unit.ok_or_else(|| de::Error::missing_field("unit"))?;
  190. // Create options based on the method and the description flag
  191. let options = if method == PaymentMethod::Bolt11 {
  192. description.map(|description| MintMethodOptions::Bolt11 { description })
  193. } else {
  194. None
  195. };
  196. Ok(MintMethodSettings {
  197. method,
  198. unit,
  199. min_amount,
  200. max_amount,
  201. options,
  202. })
  203. }
  204. }
  205. impl<'de> Deserialize<'de> for MintMethodSettings {
  206. fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  207. where
  208. D: Deserializer<'de>,
  209. {
  210. deserializer.deserialize_map(MintMethodSettingsVisitor)
  211. }
  212. }
  213. /// Mint Method settings options
  214. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  215. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  216. #[serde(untagged)]
  217. pub enum MintMethodOptions {
  218. /// Bolt11 Options
  219. Bolt11 {
  220. /// Mint supports setting bolt11 description
  221. description: bool,
  222. },
  223. }
  224. /// Mint Settings
  225. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
  226. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = nut04::Settings))]
  227. pub struct Settings {
  228. /// Methods to mint
  229. pub methods: Vec<MintMethodSettings>,
  230. /// Minting disabled
  231. pub disabled: bool,
  232. }
  233. impl Settings {
  234. /// Create new [`Settings`]
  235. pub fn new(methods: Vec<MintMethodSettings>, disabled: bool) -> Self {
  236. Self { methods, disabled }
  237. }
  238. /// Get [`MintMethodSettings`] for unit method pair
  239. pub fn get_settings(
  240. &self,
  241. unit: &CurrencyUnit,
  242. method: &PaymentMethod,
  243. ) -> Option<MintMethodSettings> {
  244. for method_settings in self.methods.iter() {
  245. if method_settings.method.eq(method) && method_settings.unit.eq(unit) {
  246. return Some(method_settings.clone());
  247. }
  248. }
  249. None
  250. }
  251. /// Remove [`MintMethodSettings`] for unit method pair
  252. pub fn remove_settings(
  253. &mut self,
  254. unit: &CurrencyUnit,
  255. method: &PaymentMethod,
  256. ) -> Option<MintMethodSettings> {
  257. self.methods
  258. .iter()
  259. .position(|settings| &settings.method == method && &settings.unit == unit)
  260. .map(|index| self.methods.remove(index))
  261. }
  262. /// Supported nut04 methods
  263. pub fn supported_methods(&self) -> Vec<&PaymentMethod> {
  264. self.methods.iter().map(|a| &a.method).collect()
  265. }
  266. /// Supported nut04 units
  267. pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
  268. self.methods.iter().map(|s| &s.unit).collect()
  269. }
  270. }
  271. #[cfg(test)]
  272. mod tests {
  273. use serde_json::{from_str, json, to_string};
  274. use super::*;
  275. #[test]
  276. fn test_mint_method_settings_top_level_description() {
  277. // Create JSON with top-level description
  278. let json_str = r#"{
  279. "method": "bolt11",
  280. "unit": "sat",
  281. "min_amount": 0,
  282. "max_amount": 10000,
  283. "description": true
  284. }"#;
  285. // Deserialize it
  286. let settings: MintMethodSettings = from_str(json_str).unwrap();
  287. // Check that description was correctly moved to options
  288. assert_eq!(settings.method, PaymentMethod::Bolt11);
  289. assert_eq!(settings.unit, CurrencyUnit::Sat);
  290. assert_eq!(settings.min_amount, Some(Amount::from(0)));
  291. assert_eq!(settings.max_amount, Some(Amount::from(10000)));
  292. match settings.options {
  293. Some(MintMethodOptions::Bolt11 { description }) => {
  294. assert!(description);
  295. }
  296. _ => panic!("Expected Bolt11 options with description = true"),
  297. }
  298. // Serialize it back
  299. let serialized = to_string(&settings).unwrap();
  300. let parsed: serde_json::Value = from_str(&serialized).unwrap();
  301. // Verify the description is at the top level
  302. assert_eq!(parsed["description"], json!(true));
  303. }
  304. #[test]
  305. fn test_both_description_locations() {
  306. // Create JSON with description in both places (top level and in options)
  307. let json_str = r#"{
  308. "method": "bolt11",
  309. "unit": "sat",
  310. "min_amount": 0,
  311. "max_amount": 10000,
  312. "description": true,
  313. "options": {
  314. "description": false
  315. }
  316. }"#;
  317. // Deserialize it - top level should take precedence
  318. let settings: MintMethodSettings = from_str(json_str).unwrap();
  319. match settings.options {
  320. Some(MintMethodOptions::Bolt11 { description }) => {
  321. assert!(description, "Top-level description should take precedence");
  322. }
  323. _ => panic!("Expected Bolt11 options with description = true"),
  324. }
  325. }
  326. }