nut04.rs 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. //! NUT-04: Mint Tokens via Bolt11
  2. //!
  3. //! <https://github.com/cashubtc/nuts/blob/main/04.md>
  4. use std::fmt;
  5. use std::str::FromStr;
  6. use serde::de::DeserializeOwned;
  7. use serde::{Deserialize, Serialize};
  8. use thiserror::Error;
  9. #[cfg(feature = "mint")]
  10. use uuid::Uuid;
  11. use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod};
  12. use super::{MintQuoteState, PublicKey};
  13. use crate::Amount;
  14. /// NUT04 Error
  15. #[derive(Debug, Error)]
  16. pub enum Error {
  17. /// Unknown Quote State
  18. #[error("Unknown Quote State")]
  19. UnknownState,
  20. /// Amount overflow
  21. #[error("Amount overflow")]
  22. AmountOverflow,
  23. }
  24. /// Mint quote request [NUT-04]
  25. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  26. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  27. pub struct MintQuoteBolt11Request {
  28. /// Amount
  29. pub amount: Amount,
  30. /// Unit wallet would like to pay with
  31. pub unit: CurrencyUnit,
  32. /// Memo to create the invoice with
  33. #[serde(skip_serializing_if = "Option::is_none")]
  34. pub description: Option<String>,
  35. /// NUT-19 Pubkey
  36. #[serde(skip_serializing_if = "Option::is_none")]
  37. pub pubkey: Option<PublicKey>,
  38. }
  39. /// Possible states of a quote
  40. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
  41. #[serde(rename_all = "UPPERCASE")]
  42. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = MintQuoteState))]
  43. pub enum QuoteState {
  44. /// Quote has not been paid
  45. #[default]
  46. Unpaid,
  47. /// Quote has been paid and wallet can mint
  48. Paid,
  49. /// Minting is in progress
  50. /// **Note:** This state is to be used internally but is not part of the
  51. /// nut.
  52. Pending,
  53. /// ecash issued for quote
  54. Issued,
  55. }
  56. impl fmt::Display for QuoteState {
  57. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  58. match self {
  59. Self::Unpaid => write!(f, "UNPAID"),
  60. Self::Paid => write!(f, "PAID"),
  61. Self::Pending => write!(f, "PENDING"),
  62. Self::Issued => write!(f, "ISSUED"),
  63. }
  64. }
  65. }
  66. impl FromStr for QuoteState {
  67. type Err = Error;
  68. fn from_str(state: &str) -> Result<Self, Self::Err> {
  69. match state {
  70. "PENDING" => Ok(Self::Pending),
  71. "PAID" => Ok(Self::Paid),
  72. "UNPAID" => Ok(Self::Unpaid),
  73. "ISSUED" => Ok(Self::Issued),
  74. _ => Err(Error::UnknownState),
  75. }
  76. }
  77. }
  78. /// Mint quote response [NUT-04]
  79. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  80. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  81. #[serde(bound = "Q: Serialize + DeserializeOwned")]
  82. pub struct MintQuoteBolt11Response<Q> {
  83. /// Quote Id
  84. pub quote: Q,
  85. /// Payment request to fulfil
  86. pub request: String,
  87. /// Amount
  88. // REVIEW: This is now required in the spec, we should remove the option once all mints update
  89. pub amount: Option<Amount>,
  90. /// Unit
  91. // REVIEW: This is now required in the spec, we should remove the option once all mints update
  92. pub unit: Option<CurrencyUnit>,
  93. /// Quote State
  94. pub state: MintQuoteState,
  95. /// Unix timestamp until the quote is valid
  96. pub expiry: Option<u64>,
  97. /// NUT-19 Pubkey
  98. #[serde(skip_serializing_if = "Option::is_none")]
  99. pub pubkey: Option<PublicKey>,
  100. }
  101. impl<Q: ToString> MintQuoteBolt11Response<Q> {
  102. /// Convert the MintQuote with a quote type Q to a String
  103. pub fn to_string_id(&self) -> MintQuoteBolt11Response<String> {
  104. MintQuoteBolt11Response {
  105. quote: self.quote.to_string(),
  106. request: self.request.clone(),
  107. state: self.state,
  108. expiry: self.expiry,
  109. pubkey: self.pubkey,
  110. amount: self.amount,
  111. unit: self.unit.clone(),
  112. }
  113. }
  114. }
  115. #[cfg(feature = "mint")]
  116. impl From<MintQuoteBolt11Response<Uuid>> for MintQuoteBolt11Response<String> {
  117. fn from(value: MintQuoteBolt11Response<Uuid>) -> Self {
  118. Self {
  119. quote: value.quote.to_string(),
  120. request: value.request,
  121. state: value.state,
  122. expiry: value.expiry,
  123. pubkey: value.pubkey,
  124. amount: value.amount,
  125. unit: value.unit.clone(),
  126. }
  127. }
  128. }
  129. /// Mint request [NUT-04]
  130. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  131. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  132. #[serde(bound = "Q: Serialize + DeserializeOwned")]
  133. pub struct MintBolt11Request<Q> {
  134. /// Quote id
  135. #[cfg_attr(feature = "swagger", schema(max_length = 1_000))]
  136. pub quote: Q,
  137. /// Outputs
  138. #[cfg_attr(feature = "swagger", schema(max_items = 1_000))]
  139. pub outputs: Vec<BlindedMessage>,
  140. /// Signature
  141. #[serde(skip_serializing_if = "Option::is_none")]
  142. pub signature: Option<String>,
  143. }
  144. #[cfg(feature = "mint")]
  145. impl TryFrom<MintBolt11Request<String>> for MintBolt11Request<Uuid> {
  146. type Error = uuid::Error;
  147. fn try_from(value: MintBolt11Request<String>) -> Result<Self, Self::Error> {
  148. Ok(Self {
  149. quote: Uuid::from_str(&value.quote)?,
  150. outputs: value.outputs,
  151. signature: value.signature,
  152. })
  153. }
  154. }
  155. impl<Q> MintBolt11Request<Q> {
  156. /// Total [`Amount`] of outputs
  157. pub fn total_amount(&self) -> Result<Amount, Error> {
  158. Amount::try_sum(
  159. self.outputs
  160. .iter()
  161. .map(|BlindedMessage { amount, .. }| *amount),
  162. )
  163. .map_err(|_| Error::AmountOverflow)
  164. }
  165. }
  166. /// Mint response [NUT-04]
  167. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  168. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  169. pub struct MintBolt11Response {
  170. /// Blinded Signatures
  171. pub signatures: Vec<BlindSignature>,
  172. }
  173. /// Mint Method Settings
  174. #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  175. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  176. pub struct MintMethodSettings {
  177. /// Payment Method e.g. bolt11
  178. pub method: PaymentMethod,
  179. /// Currency Unit e.g. sat
  180. pub unit: CurrencyUnit,
  181. /// Min Amount
  182. #[serde(skip_serializing_if = "Option::is_none")]
  183. pub min_amount: Option<Amount>,
  184. /// Max Amount
  185. #[serde(skip_serializing_if = "Option::is_none")]
  186. pub max_amount: Option<Amount>,
  187. /// Quote Description
  188. #[serde(default)]
  189. pub description: bool,
  190. }
  191. /// Mint Settings
  192. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
  193. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = nut04::Settings))]
  194. pub struct Settings {
  195. /// Methods to mint
  196. pub methods: Vec<MintMethodSettings>,
  197. /// Minting disabled
  198. pub disabled: bool,
  199. }
  200. impl Settings {
  201. /// Create new [`Settings`]
  202. pub fn new(methods: Vec<MintMethodSettings>, disabled: bool) -> Self {
  203. Self { methods, disabled }
  204. }
  205. /// Get [`MintMethodSettings`] for unit method pair
  206. pub fn get_settings(
  207. &self,
  208. unit: &CurrencyUnit,
  209. method: &PaymentMethod,
  210. ) -> Option<MintMethodSettings> {
  211. for method_settings in self.methods.iter() {
  212. if method_settings.method.eq(method) && method_settings.unit.eq(unit) {
  213. return Some(method_settings.clone());
  214. }
  215. }
  216. None
  217. }
  218. /// Remove [`MintMethodSettings`] for unit method pair
  219. pub fn remove_settings(
  220. &mut self,
  221. unit: &CurrencyUnit,
  222. method: &PaymentMethod,
  223. ) -> Option<MintMethodSettings> {
  224. self.methods
  225. .iter()
  226. .position(|settings| &settings.method == method && &settings.unit == unit)
  227. .map(|index| self.methods.remove(index))
  228. }
  229. }