nut05.rs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  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::DeserializeOwned;
  7. use serde::{Deserialize, Deserializer, Serialize};
  8. use serde_json::Value;
  9. use thiserror::Error;
  10. #[cfg(feature = "mint")]
  11. use uuid::Uuid;
  12. use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
  13. use super::nut15::Mpp;
  14. use super::ProofsMethods;
  15. use crate::nuts::MeltQuoteState;
  16. use crate::{Amount, Bolt11Invoice};
  17. /// NUT05 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. /// Invalid Amount
  27. #[error("Invalid Request")]
  28. InvalidAmountRequest,
  29. /// Unsupported unit
  30. #[error("Unsupported unit")]
  31. UnsupportedUnit,
  32. }
  33. /// Melt quote request [NUT-05]
  34. #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
  35. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  36. pub struct MeltQuoteBolt11Request {
  37. /// Bolt11 invoice to be paid
  38. #[cfg_attr(feature = "swagger", schema(value_type = String))]
  39. pub request: Bolt11Invoice,
  40. /// Unit wallet would like to pay with
  41. pub unit: CurrencyUnit,
  42. /// Payment Options
  43. pub options: Option<MeltOptions>,
  44. }
  45. /// Melt Options
  46. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
  47. #[serde(untagged)]
  48. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  49. pub enum MeltOptions {
  50. /// Mpp Options
  51. Mpp {
  52. /// MPP
  53. mpp: Mpp,
  54. },
  55. /// Amountless options
  56. Amountless {
  57. /// Amountless
  58. amountless: Amountless,
  59. },
  60. }
  61. impl MeltOptions {
  62. /// Create new [`MeltOptions::Mpp`]
  63. pub fn new_mpp<A>(amount: A) -> Self
  64. where
  65. A: Into<Amount>,
  66. {
  67. Self::Mpp {
  68. mpp: Mpp {
  69. amount: amount.into(),
  70. },
  71. }
  72. }
  73. /// Create new [`MeltOptions::Amountless`]
  74. pub fn new_amountless<A>(amount_msat: A) -> Self
  75. where
  76. A: Into<Amount>,
  77. {
  78. Self::Amountless {
  79. amountless: Amountless {
  80. amount_msat: amount_msat.into(),
  81. },
  82. }
  83. }
  84. /// Payment amount
  85. pub fn amount_msat(&self) -> Amount {
  86. match self {
  87. Self::Mpp { mpp } => mpp.amount,
  88. Self::Amountless { amountless } => amountless.amount_msat,
  89. }
  90. }
  91. }
  92. /// Amountless payment
  93. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
  94. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  95. pub struct Amountless {
  96. /// Amount to pay in msat
  97. pub amount_msat: Amount,
  98. }
  99. impl MeltQuoteBolt11Request {
  100. /// Amount from [`MeltQuoteBolt11Request`]
  101. ///
  102. /// Amount can either be defined in the bolt11 invoice,
  103. /// in the request for an amountless bolt11 or in MPP option.
  104. pub fn amount_msat(&self) -> Result<Amount, Error> {
  105. let MeltQuoteBolt11Request {
  106. request,
  107. unit: _,
  108. options,
  109. ..
  110. } = self;
  111. match options {
  112. None => Ok(request
  113. .amount_milli_satoshis()
  114. .ok_or(Error::InvalidAmountRequest)?
  115. .into()),
  116. Some(MeltOptions::Mpp { mpp }) => Ok(mpp.amount),
  117. Some(MeltOptions::Amountless { amountless }) => {
  118. let amount = amountless.amount_msat;
  119. if let Some(amount_msat) = request.amount_milli_satoshis() {
  120. if amount != amount_msat.into() {
  121. return Err(Error::InvalidAmountRequest);
  122. }
  123. }
  124. Ok(amount)
  125. }
  126. }
  127. }
  128. }
  129. /// Possible states of a quote
  130. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
  131. #[serde(rename_all = "UPPERCASE")]
  132. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = MeltQuoteState))]
  133. pub enum QuoteState {
  134. /// Quote has not been paid
  135. #[default]
  136. Unpaid,
  137. /// Quote has been paid
  138. Paid,
  139. /// Paying quote is in progress
  140. Pending,
  141. /// Unknown state
  142. Unknown,
  143. /// Failed
  144. Failed,
  145. }
  146. impl fmt::Display for QuoteState {
  147. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  148. match self {
  149. Self::Unpaid => write!(f, "UNPAID"),
  150. Self::Paid => write!(f, "PAID"),
  151. Self::Pending => write!(f, "PENDING"),
  152. Self::Unknown => write!(f, "UNKNOWN"),
  153. Self::Failed => write!(f, "FAILED"),
  154. }
  155. }
  156. }
  157. impl FromStr for QuoteState {
  158. type Err = Error;
  159. fn from_str(state: &str) -> Result<Self, Self::Err> {
  160. match state {
  161. "PENDING" => Ok(Self::Pending),
  162. "PAID" => Ok(Self::Paid),
  163. "UNPAID" => Ok(Self::Unpaid),
  164. "UNKNOWN" => Ok(Self::Unknown),
  165. "FAILED" => Ok(Self::Failed),
  166. _ => Err(Error::UnknownState),
  167. }
  168. }
  169. }
  170. /// Melt quote response [NUT-05]
  171. #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
  172. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  173. #[serde(bound = "Q: Serialize")]
  174. pub struct MeltQuoteBolt11Response<Q> {
  175. /// Quote Id
  176. pub quote: Q,
  177. /// The amount that needs to be provided
  178. pub amount: Amount,
  179. /// The fee reserve that is required
  180. pub fee_reserve: Amount,
  181. /// Whether the request haas be paid
  182. // TODO: To be deprecated
  183. /// Deprecated
  184. pub paid: Option<bool>,
  185. /// Quote State
  186. pub state: MeltQuoteState,
  187. /// Unix timestamp until the quote is valid
  188. pub expiry: u64,
  189. /// Payment preimage
  190. #[serde(skip_serializing_if = "Option::is_none")]
  191. pub payment_preimage: Option<String>,
  192. /// Change
  193. #[serde(skip_serializing_if = "Option::is_none")]
  194. pub change: Option<Vec<BlindSignature>>,
  195. /// Payment request to fulfill
  196. // REVIEW: This is now required in the spec, we should remove the option once all mints update
  197. #[serde(skip_serializing_if = "Option::is_none")]
  198. pub request: Option<String>,
  199. /// Unit
  200. // REVIEW: This is now required in the spec, we should remove the option once all mints update
  201. #[serde(skip_serializing_if = "Option::is_none")]
  202. pub unit: Option<CurrencyUnit>,
  203. }
  204. impl<Q: ToString> MeltQuoteBolt11Response<Q> {
  205. /// Convert a `MeltQuoteBolt11Response` with type Q (generic/unknown) to a
  206. /// `MeltQuoteBolt11Response` with `String`
  207. pub fn to_string_id(self) -> MeltQuoteBolt11Response<String> {
  208. MeltQuoteBolt11Response {
  209. quote: self.quote.to_string(),
  210. amount: self.amount,
  211. fee_reserve: self.fee_reserve,
  212. paid: self.paid,
  213. state: self.state,
  214. expiry: self.expiry,
  215. payment_preimage: self.payment_preimage,
  216. change: self.change,
  217. request: self.request,
  218. unit: self.unit,
  219. }
  220. }
  221. }
  222. #[cfg(feature = "mint")]
  223. impl From<MeltQuoteBolt11Response<Uuid>> for MeltQuoteBolt11Response<String> {
  224. fn from(value: MeltQuoteBolt11Response<Uuid>) -> Self {
  225. Self {
  226. quote: value.quote.to_string(),
  227. amount: value.amount,
  228. fee_reserve: value.fee_reserve,
  229. paid: value.paid,
  230. state: value.state,
  231. expiry: value.expiry,
  232. payment_preimage: value.payment_preimage,
  233. change: value.change,
  234. request: value.request,
  235. unit: value.unit,
  236. }
  237. }
  238. }
  239. // A custom deserializer is needed until all mints
  240. // update some will return without the required state.
  241. impl<'de, Q: DeserializeOwned> Deserialize<'de> for MeltQuoteBolt11Response<Q> {
  242. fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  243. where
  244. D: Deserializer<'de>,
  245. {
  246. let value = Value::deserialize(deserializer)?;
  247. let quote: Q = serde_json::from_value(
  248. value
  249. .get("quote")
  250. .ok_or(serde::de::Error::missing_field("quote"))?
  251. .clone(),
  252. )
  253. .map_err(|_| serde::de::Error::custom("Invalid quote if string"))?;
  254. let amount = value
  255. .get("amount")
  256. .ok_or(serde::de::Error::missing_field("amount"))?
  257. .as_u64()
  258. .ok_or(serde::de::Error::missing_field("amount"))?;
  259. let amount = Amount::from(amount);
  260. let fee_reserve = value
  261. .get("fee_reserve")
  262. .ok_or(serde::de::Error::missing_field("fee_reserve"))?
  263. .as_u64()
  264. .ok_or(serde::de::Error::missing_field("fee_reserve"))?;
  265. let fee_reserve = Amount::from(fee_reserve);
  266. let paid: Option<bool> = value.get("paid").and_then(|p| p.as_bool());
  267. let state: Option<String> = value
  268. .get("state")
  269. .and_then(|s| serde_json::from_value(s.clone()).ok());
  270. let (state, paid) = match (state, paid) {
  271. (None, None) => return Err(serde::de::Error::custom("State or paid must be defined")),
  272. (Some(state), _) => {
  273. let state: QuoteState = QuoteState::from_str(&state)
  274. .map_err(|_| serde::de::Error::custom("Unknown state"))?;
  275. let paid = state == QuoteState::Paid;
  276. (state, paid)
  277. }
  278. (None, Some(paid)) => {
  279. let state = if paid {
  280. QuoteState::Paid
  281. } else {
  282. QuoteState::Unpaid
  283. };
  284. (state, paid)
  285. }
  286. };
  287. let expiry = value
  288. .get("expiry")
  289. .ok_or(serde::de::Error::missing_field("expiry"))?
  290. .as_u64()
  291. .ok_or(serde::de::Error::missing_field("expiry"))?;
  292. let payment_preimage: Option<String> = value
  293. .get("payment_preimage")
  294. .and_then(|p| serde_json::from_value(p.clone()).ok());
  295. let change: Option<Vec<BlindSignature>> = value
  296. .get("change")
  297. .and_then(|b| serde_json::from_value(b.clone()).ok());
  298. let request: Option<String> = value
  299. .get("request")
  300. .and_then(|r| serde_json::from_value(r.clone()).ok());
  301. let unit: Option<CurrencyUnit> = value
  302. .get("unit")
  303. .and_then(|u| serde_json::from_value(u.clone()).ok());
  304. Ok(Self {
  305. quote,
  306. amount,
  307. fee_reserve,
  308. paid: Some(paid),
  309. state,
  310. expiry,
  311. payment_preimage,
  312. change,
  313. request,
  314. unit,
  315. })
  316. }
  317. }
  318. /// Melt Bolt11 Request [NUT-05]
  319. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  320. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  321. #[serde(bound = "Q: Serialize + DeserializeOwned")]
  322. pub struct MeltBolt11Request<Q> {
  323. /// Quote ID
  324. quote: Q,
  325. /// Proofs
  326. #[cfg_attr(feature = "swagger", schema(value_type = Vec<crate::Proof>))]
  327. inputs: Proofs,
  328. /// Blinded Message that can be used to return change [NUT-08]
  329. /// Amount field of BlindedMessages `SHOULD` be set to zero
  330. outputs: Option<Vec<BlindedMessage>>,
  331. }
  332. #[cfg(feature = "mint")]
  333. impl TryFrom<MeltBolt11Request<String>> for MeltBolt11Request<Uuid> {
  334. type Error = uuid::Error;
  335. fn try_from(value: MeltBolt11Request<String>) -> Result<Self, Self::Error> {
  336. Ok(Self {
  337. quote: Uuid::from_str(&value.quote)?,
  338. inputs: value.inputs,
  339. outputs: value.outputs,
  340. })
  341. }
  342. }
  343. // Basic implementation without trait bounds
  344. impl<Q> MeltBolt11Request<Q> {
  345. /// Get inputs (proofs)
  346. pub fn inputs(&self) -> &Proofs {
  347. &self.inputs
  348. }
  349. /// Get outputs (blinded messages for change)
  350. pub fn outputs(&self) -> &Option<Vec<BlindedMessage>> {
  351. &self.outputs
  352. }
  353. }
  354. impl<Q: Serialize + DeserializeOwned> MeltBolt11Request<Q> {
  355. /// Create new [`MeltBolt11Request`]
  356. pub fn new(quote: Q, inputs: Proofs, outputs: Option<Vec<BlindedMessage>>) -> Self {
  357. Self {
  358. quote,
  359. inputs: inputs.without_dleqs(),
  360. outputs,
  361. }
  362. }
  363. /// Get quote
  364. pub fn quote(&self) -> &Q {
  365. &self.quote
  366. }
  367. /// Total [`Amount`] of [`Proofs`]
  368. pub fn proofs_amount(&self) -> Result<Amount, Error> {
  369. Amount::try_sum(self.inputs.iter().map(|proof| proof.amount))
  370. .map_err(|_| Error::AmountOverflow)
  371. }
  372. }
  373. /// Melt Method Settings
  374. #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  375. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  376. pub struct MeltMethodSettings {
  377. /// Payment Method e.g. bolt11
  378. pub method: PaymentMethod,
  379. /// Currency Unit e.g. sat
  380. pub unit: CurrencyUnit,
  381. /// Min Amount
  382. #[serde(skip_serializing_if = "Option::is_none")]
  383. pub min_amount: Option<Amount>,
  384. /// Max Amount
  385. #[serde(skip_serializing_if = "Option::is_none")]
  386. pub max_amount: Option<Amount>,
  387. /// Amountless
  388. #[serde(default)]
  389. pub amountless: bool,
  390. }
  391. impl Settings {
  392. /// Create new [`Settings`]
  393. pub fn new(methods: Vec<MeltMethodSettings>, disabled: bool) -> Self {
  394. Self { methods, disabled }
  395. }
  396. /// Get [`MeltMethodSettings`] for unit method pair
  397. pub fn get_settings(
  398. &self,
  399. unit: &CurrencyUnit,
  400. method: &PaymentMethod,
  401. ) -> Option<MeltMethodSettings> {
  402. for method_settings in self.methods.iter() {
  403. if method_settings.method.eq(method) && method_settings.unit.eq(unit) {
  404. return Some(method_settings.clone());
  405. }
  406. }
  407. None
  408. }
  409. /// Remove [`MeltMethodSettings`] for unit method pair
  410. pub fn remove_settings(
  411. &mut self,
  412. unit: &CurrencyUnit,
  413. method: &PaymentMethod,
  414. ) -> Option<MeltMethodSettings> {
  415. self.methods
  416. .iter()
  417. .position(|settings| settings.method.eq(method) && settings.unit.eq(unit))
  418. .map(|index| self.methods.remove(index))
  419. }
  420. }
  421. /// Melt Settings
  422. #[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
  423. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = nut05::Settings))]
  424. pub struct Settings {
  425. /// Methods to melt
  426. pub methods: Vec<MeltMethodSettings>,
  427. /// Minting disabled
  428. pub disabled: bool,
  429. }