nut05.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. //! NUT-05: Melting Tokens
  2. //!
  3. //! <https://github.com/cashubtc/nuts/blob/main/05.md>
  4. use std::fmt;
  5. use std::str::FromStr;
  6. use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, Visitor};
  7. use serde::ser::{SerializeStruct, Serializer};
  8. use serde::{Deserialize, Serialize};
  9. use thiserror::Error;
  10. #[cfg(feature = "mint")]
  11. use uuid::Uuid;
  12. use super::nut00::{BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
  13. use super::ProofsMethods;
  14. use crate::Amount;
  15. /// NUT05 Error
  16. #[derive(Debug, Error)]
  17. pub enum Error {
  18. /// Unknown Quote State
  19. #[error("Unknown quote state")]
  20. UnknownState,
  21. /// Amount overflow
  22. #[error("Amount Overflow")]
  23. AmountOverflow,
  24. /// Unsupported unit
  25. #[error("Unsupported unit")]
  26. UnsupportedUnit,
  27. }
  28. /// Possible states of a quote
  29. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
  30. #[serde(rename_all = "UPPERCASE")]
  31. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = MeltQuoteState))]
  32. pub enum QuoteState {
  33. /// Quote has not been paid
  34. #[default]
  35. Unpaid,
  36. /// Quote has been paid
  37. Paid,
  38. /// Paying quote is in progress
  39. Pending,
  40. /// Unknown state
  41. Unknown,
  42. /// Failed
  43. Failed,
  44. }
  45. impl fmt::Display for QuoteState {
  46. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  47. match self {
  48. Self::Unpaid => write!(f, "UNPAID"),
  49. Self::Paid => write!(f, "PAID"),
  50. Self::Pending => write!(f, "PENDING"),
  51. Self::Unknown => write!(f, "UNKNOWN"),
  52. Self::Failed => write!(f, "FAILED"),
  53. }
  54. }
  55. }
  56. impl FromStr for QuoteState {
  57. type Err = Error;
  58. fn from_str(state: &str) -> Result<Self, Self::Err> {
  59. match state {
  60. "PENDING" => Ok(Self::Pending),
  61. "PAID" => Ok(Self::Paid),
  62. "UNPAID" => Ok(Self::Unpaid),
  63. "UNKNOWN" => Ok(Self::Unknown),
  64. "FAILED" => Ok(Self::Failed),
  65. _ => Err(Error::UnknownState),
  66. }
  67. }
  68. }
  69. /// Melt Bolt11 Request [NUT-05]
  70. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  71. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  72. #[serde(bound = "Q: Serialize + DeserializeOwned")]
  73. pub struct MeltRequest<Q> {
  74. /// Quote ID
  75. quote: Q,
  76. /// Proofs
  77. #[cfg_attr(feature = "swagger", schema(value_type = Vec<crate::Proof>))]
  78. inputs: Proofs,
  79. /// Blinded Message that can be used to return change [NUT-08]
  80. /// Amount field of BlindedMessages `SHOULD` be set to zero
  81. outputs: Option<Vec<BlindedMessage>>,
  82. }
  83. #[cfg(feature = "mint")]
  84. impl TryFrom<MeltRequest<String>> for MeltRequest<Uuid> {
  85. type Error = uuid::Error;
  86. fn try_from(value: MeltRequest<String>) -> Result<Self, Self::Error> {
  87. Ok(Self {
  88. quote: Uuid::from_str(&value.quote)?,
  89. inputs: value.inputs,
  90. outputs: value.outputs,
  91. })
  92. }
  93. }
  94. // Basic implementation without trait bounds
  95. impl<Q> MeltRequest<Q> {
  96. /// Quote Id
  97. pub fn quote_id(&self) -> &Q {
  98. &self.quote
  99. }
  100. /// Get inputs (proofs)
  101. pub fn inputs(&self) -> &Proofs {
  102. &self.inputs
  103. }
  104. /// Get mutable inputs (proofs)
  105. pub fn inputs_mut(&mut self) -> &mut Proofs {
  106. &mut self.inputs
  107. }
  108. /// Get outputs (blinded messages for change)
  109. pub fn outputs(&self) -> &Option<Vec<BlindedMessage>> {
  110. &self.outputs
  111. }
  112. }
  113. impl<Q: Serialize + DeserializeOwned> MeltRequest<Q> {
  114. /// Create new [`MeltRequest`]
  115. pub fn new(quote: Q, inputs: Proofs, outputs: Option<Vec<BlindedMessage>>) -> Self {
  116. Self {
  117. quote,
  118. inputs: inputs.without_dleqs(),
  119. outputs,
  120. }
  121. }
  122. /// Get quote
  123. pub fn quote(&self) -> &Q {
  124. &self.quote
  125. }
  126. /// Total [`Amount`] of [`Proofs`]
  127. pub fn inputs_amount(&self) -> Result<Amount, Error> {
  128. Amount::try_sum(self.inputs.iter().map(|proof| proof.amount))
  129. .map_err(|_| Error::AmountOverflow)
  130. }
  131. }
  132. /// Melt Method Settings
  133. #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
  134. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  135. pub struct MeltMethodSettings {
  136. /// Payment Method e.g. bolt11
  137. pub method: PaymentMethod,
  138. /// Currency Unit e.g. sat
  139. pub unit: CurrencyUnit,
  140. /// Min Amount
  141. pub min_amount: Option<Amount>,
  142. /// Max Amount
  143. pub max_amount: Option<Amount>,
  144. /// Options
  145. pub options: Option<MeltMethodOptions>,
  146. }
  147. impl Serialize for MeltMethodSettings {
  148. fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
  149. where
  150. S: Serializer,
  151. {
  152. let mut num_fields = 3; // method and unit are always present
  153. if self.min_amount.is_some() {
  154. num_fields += 1;
  155. }
  156. if self.max_amount.is_some() {
  157. num_fields += 1;
  158. }
  159. let mut amountless_in_top_level = false;
  160. if let Some(MeltMethodOptions::Bolt11 { amountless }) = &self.options {
  161. if *amountless {
  162. num_fields += 1;
  163. amountless_in_top_level = true;
  164. }
  165. }
  166. let mut state = serializer.serialize_struct("MeltMethodSettings", num_fields)?;
  167. state.serialize_field("method", &self.method)?;
  168. state.serialize_field("unit", &self.unit)?;
  169. if let Some(min_amount) = &self.min_amount {
  170. state.serialize_field("min_amount", min_amount)?;
  171. }
  172. if let Some(max_amount) = &self.max_amount {
  173. state.serialize_field("max_amount", max_amount)?;
  174. }
  175. // If there's an amountless flag in Bolt11 options, add it at the top level
  176. if amountless_in_top_level {
  177. state.serialize_field("amountless", &true)?;
  178. }
  179. state.end()
  180. }
  181. }
  182. struct MeltMethodSettingsVisitor;
  183. impl<'de> Visitor<'de> for MeltMethodSettingsVisitor {
  184. type Value = MeltMethodSettings;
  185. fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
  186. formatter.write_str("a MeltMethodSettings structure")
  187. }
  188. fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
  189. where
  190. M: MapAccess<'de>,
  191. {
  192. let mut method: Option<PaymentMethod> = None;
  193. let mut unit: Option<CurrencyUnit> = None;
  194. let mut min_amount: Option<Amount> = None;
  195. let mut max_amount: Option<Amount> = None;
  196. let mut amountless: Option<bool> = None;
  197. while let Some(key) = map.next_key::<String>()? {
  198. match key.as_str() {
  199. "method" => {
  200. if method.is_some() {
  201. return Err(de::Error::duplicate_field("method"));
  202. }
  203. method = Some(map.next_value()?);
  204. }
  205. "unit" => {
  206. if unit.is_some() {
  207. return Err(de::Error::duplicate_field("unit"));
  208. }
  209. unit = Some(map.next_value()?);
  210. }
  211. "min_amount" => {
  212. if min_amount.is_some() {
  213. return Err(de::Error::duplicate_field("min_amount"));
  214. }
  215. min_amount = Some(map.next_value()?);
  216. }
  217. "max_amount" => {
  218. if max_amount.is_some() {
  219. return Err(de::Error::duplicate_field("max_amount"));
  220. }
  221. max_amount = Some(map.next_value()?);
  222. }
  223. "amountless" => {
  224. if amountless.is_some() {
  225. return Err(de::Error::duplicate_field("amountless"));
  226. }
  227. amountless = Some(map.next_value()?);
  228. }
  229. "options" => {
  230. // If there are explicit options, they take precedence, except the amountless
  231. // field which we will handle specially
  232. let options: Option<MeltMethodOptions> = map.next_value()?;
  233. if let Some(MeltMethodOptions::Bolt11 {
  234. amountless: amountless_from_options,
  235. }) = options
  236. {
  237. // If we already found a top-level amountless, use that instead
  238. if amountless.is_none() {
  239. amountless = Some(amountless_from_options);
  240. }
  241. }
  242. }
  243. _ => {
  244. // Skip unknown fields
  245. let _: serde::de::IgnoredAny = map.next_value()?;
  246. }
  247. }
  248. }
  249. let method = method.ok_or_else(|| de::Error::missing_field("method"))?;
  250. let unit = unit.ok_or_else(|| de::Error::missing_field("unit"))?;
  251. // Create options based on the method and the amountless flag
  252. let options = if method == PaymentMethod::Bolt11 && amountless.is_some() {
  253. amountless.map(|amountless| MeltMethodOptions::Bolt11 { amountless })
  254. } else {
  255. None
  256. };
  257. Ok(MeltMethodSettings {
  258. method,
  259. unit,
  260. min_amount,
  261. max_amount,
  262. options,
  263. })
  264. }
  265. }
  266. impl<'de> Deserialize<'de> for MeltMethodSettings {
  267. fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  268. where
  269. D: Deserializer<'de>,
  270. {
  271. deserializer.deserialize_map(MeltMethodSettingsVisitor)
  272. }
  273. }
  274. /// Mint Method settings options
  275. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  276. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  277. #[serde(untagged)]
  278. pub enum MeltMethodOptions {
  279. /// Bolt11 Options
  280. Bolt11 {
  281. /// Mint supports paying bolt11 amountless
  282. amountless: bool,
  283. },
  284. }
  285. impl Settings {
  286. /// Create new [`Settings`]
  287. pub fn new(methods: Vec<MeltMethodSettings>, disabled: bool) -> Self {
  288. Self { methods, disabled }
  289. }
  290. /// Get [`MeltMethodSettings`] for unit method pair
  291. pub fn get_settings(
  292. &self,
  293. unit: &CurrencyUnit,
  294. method: &PaymentMethod,
  295. ) -> Option<MeltMethodSettings> {
  296. for method_settings in self.methods.iter() {
  297. if method_settings.method.eq(method) && method_settings.unit.eq(unit) {
  298. return Some(method_settings.clone());
  299. }
  300. }
  301. None
  302. }
  303. /// Remove [`MeltMethodSettings`] for unit method pair
  304. pub fn remove_settings(
  305. &mut self,
  306. unit: &CurrencyUnit,
  307. method: &PaymentMethod,
  308. ) -> Option<MeltMethodSettings> {
  309. self.methods
  310. .iter()
  311. .position(|settings| settings.method.eq(method) && settings.unit.eq(unit))
  312. .map(|index| self.methods.remove(index))
  313. }
  314. }
  315. /// Melt Settings
  316. #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
  317. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = nut05::Settings))]
  318. pub struct Settings {
  319. /// Methods to melt
  320. pub methods: Vec<MeltMethodSettings>,
  321. /// Minting disabled
  322. pub disabled: bool,
  323. }
  324. impl Settings {
  325. /// Supported nut05 methods
  326. pub fn supported_methods(&self) -> Vec<&PaymentMethod> {
  327. self.methods.iter().map(|a| &a.method).collect()
  328. }
  329. /// Supported nut05 units
  330. pub fn supported_units(&self) -> Vec<&CurrencyUnit> {
  331. self.methods.iter().map(|s| &s.unit).collect()
  332. }
  333. }
  334. #[cfg(test)]
  335. mod tests {
  336. use serde_json::{from_str, json, to_string};
  337. use super::*;
  338. #[test]
  339. fn test_melt_method_settings_top_level_amountless() {
  340. // Create JSON with top-level amountless
  341. let json_str = r#"{
  342. "method": "bolt11",
  343. "unit": "sat",
  344. "min_amount": 0,
  345. "max_amount": 10000,
  346. "amountless": true
  347. }"#;
  348. // Deserialize it
  349. let settings: MeltMethodSettings = from_str(json_str).unwrap();
  350. // Check that amountless was correctly moved to options
  351. assert_eq!(settings.method, PaymentMethod::Bolt11);
  352. assert_eq!(settings.unit, CurrencyUnit::Sat);
  353. assert_eq!(settings.min_amount, Some(Amount::from(0)));
  354. assert_eq!(settings.max_amount, Some(Amount::from(10000)));
  355. match settings.options {
  356. Some(MeltMethodOptions::Bolt11 { amountless }) => {
  357. assert!(amountless);
  358. }
  359. _ => panic!("Expected Bolt11 options with amountless = true"),
  360. }
  361. // Serialize it back
  362. let serialized = to_string(&settings).unwrap();
  363. let parsed: serde_json::Value = from_str(&serialized).unwrap();
  364. // Verify the amountless is at the top level
  365. assert_eq!(parsed["amountless"], json!(true));
  366. }
  367. #[test]
  368. fn test_both_amountless_locations() {
  369. // Create JSON with amountless in both places (top level and in options)
  370. let json_str = r#"{
  371. "method": "bolt11",
  372. "unit": "sat",
  373. "min_amount": 0,
  374. "max_amount": 10000,
  375. "amountless": true,
  376. "options": {
  377. "amountless": false
  378. }
  379. }"#;
  380. // Deserialize it - top level should take precedence
  381. let settings: MeltMethodSettings = from_str(json_str).unwrap();
  382. match settings.options {
  383. Some(MeltMethodOptions::Bolt11 { amountless }) => {
  384. assert!(amountless, "Top-level amountless should take precedence");
  385. }
  386. _ => panic!("Expected Bolt11 options with amountless = true"),
  387. }
  388. }
  389. }