nut23.rs 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. //! Bolt11
  2. use std::fmt;
  3. use std::str::FromStr;
  4. use lightning_invoice::Bolt11Invoice;
  5. use serde::de::DeserializeOwned;
  6. use serde::{Deserialize, Serialize};
  7. use thiserror::Error;
  8. use super::{BlindSignature, CurrencyUnit, MeltQuoteState, Mpp, PublicKey};
  9. #[cfg(feature = "mint")]
  10. use crate::quote_id::QuoteId;
  11. use crate::util::serde_helpers::deserialize_empty_string_as_none;
  12. use crate::Amount;
  13. /// NUT023 Error
  14. #[derive(Debug, Error)]
  15. pub enum Error {
  16. /// Unknown Quote State
  17. #[error("Unknown Quote State")]
  18. UnknownState,
  19. /// Amount overflow
  20. #[error("Amount overflow")]
  21. AmountOverflow,
  22. /// Invalid Amount
  23. #[error("Invalid Request")]
  24. InvalidAmountRequest,
  25. }
  26. /// Mint quote request [NUT-04]
  27. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  28. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  29. pub struct MintQuoteBolt11Request {
  30. /// Amount
  31. pub amount: Amount,
  32. /// Unit wallet would like to pay with
  33. pub unit: CurrencyUnit,
  34. /// Memo to create the invoice with
  35. #[serde(skip_serializing_if = "Option::is_none")]
  36. pub description: Option<String>,
  37. /// NUT-19 Pubkey
  38. #[serde(skip_serializing_if = "Option::is_none")]
  39. pub pubkey: Option<PublicKey>,
  40. }
  41. /// Possible states of a quote
  42. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
  43. #[serde(rename_all = "UPPERCASE")]
  44. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = MintQuoteState))]
  45. pub enum QuoteState {
  46. /// Quote has not been paid
  47. #[default]
  48. Unpaid,
  49. /// Quote has been paid and wallet can mint
  50. Paid,
  51. /// ecash issued for quote
  52. Issued,
  53. }
  54. impl fmt::Display for QuoteState {
  55. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  56. match self {
  57. Self::Unpaid => write!(f, "UNPAID"),
  58. Self::Paid => write!(f, "PAID"),
  59. Self::Issued => write!(f, "ISSUED"),
  60. }
  61. }
  62. }
  63. impl FromStr for QuoteState {
  64. type Err = Error;
  65. fn from_str(state: &str) -> Result<Self, Self::Err> {
  66. match state {
  67. "PAID" => Ok(Self::Paid),
  68. "UNPAID" => Ok(Self::Unpaid),
  69. "ISSUED" => Ok(Self::Issued),
  70. _ => Err(Error::UnknownState),
  71. }
  72. }
  73. }
  74. /// Mint quote response [NUT-04]
  75. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  76. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  77. #[serde(bound = "Q: Serialize + DeserializeOwned")]
  78. pub struct MintQuoteBolt11Response<Q> {
  79. /// Quote Id
  80. pub quote: Q,
  81. /// Payment request to fulfil
  82. pub request: String,
  83. /// Amount
  84. // REVIEW: This is now required in the spec, we should remove the option once all mints update
  85. pub amount: Option<Amount>,
  86. /// Unit
  87. // REVIEW: This is now required in the spec, we should remove the option once all mints update
  88. pub unit: Option<CurrencyUnit>,
  89. /// Quote State
  90. pub state: QuoteState,
  91. /// Unix timestamp until the quote is valid
  92. pub expiry: Option<u64>,
  93. /// NUT-19 Pubkey
  94. #[serde(
  95. default,
  96. skip_serializing_if = "Option::is_none",
  97. deserialize_with = "deserialize_empty_string_as_none"
  98. )]
  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<QuoteId>> for MintQuoteBolt11Response<String> {
  117. fn from(value: MintQuoteBolt11Response<QuoteId>) -> 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. /// BOLT11 melt quote request [NUT-23]
  130. #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
  131. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  132. pub struct MeltQuoteBolt11Request {
  133. /// Bolt11 invoice to be paid
  134. #[cfg_attr(feature = "swagger", schema(value_type = String))]
  135. pub request: Bolt11Invoice,
  136. /// Unit wallet would like to pay with
  137. pub unit: CurrencyUnit,
  138. /// Payment Options
  139. pub options: Option<MeltOptions>,
  140. }
  141. /// Melt Options
  142. #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
  143. #[serde(untagged)]
  144. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  145. pub enum MeltOptions {
  146. /// Mpp Options
  147. Mpp {
  148. /// MPP
  149. mpp: Mpp,
  150. },
  151. /// Amountless options
  152. Amountless {
  153. /// Amountless
  154. amountless: Amountless,
  155. },
  156. }
  157. impl MeltOptions {
  158. /// Create new [`MeltOptions::Mpp`]
  159. pub fn new_mpp<A>(amount: A) -> Self
  160. where
  161. A: Into<Amount>,
  162. {
  163. Self::Mpp {
  164. mpp: Mpp {
  165. amount: amount.into(),
  166. },
  167. }
  168. }
  169. /// Create new [`MeltOptions::Amountless`]
  170. pub fn new_amountless<A>(amount_msat: A) -> Self
  171. where
  172. A: Into<Amount>,
  173. {
  174. Self::Amountless {
  175. amountless: Amountless {
  176. amount_msat: amount_msat.into(),
  177. },
  178. }
  179. }
  180. /// Payment amount
  181. pub fn amount_msat(&self) -> Amount {
  182. match self {
  183. Self::Mpp { mpp } => mpp.amount,
  184. Self::Amountless { amountless } => amountless.amount_msat,
  185. }
  186. }
  187. }
  188. /// Amountless payment
  189. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
  190. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  191. pub struct Amountless {
  192. /// Amount to pay in msat
  193. pub amount_msat: Amount,
  194. }
  195. impl MeltQuoteBolt11Request {
  196. /// Amount from [`MeltQuoteBolt11Request`]
  197. ///
  198. /// Amount can either be defined in the bolt11 invoice,
  199. /// in the request for an amountless bolt11 or in MPP option.
  200. pub fn amount_msat(&self) -> Result<Amount, Error> {
  201. let MeltQuoteBolt11Request {
  202. request, options, ..
  203. } = self;
  204. match options {
  205. None => Ok(request
  206. .amount_milli_satoshis()
  207. .ok_or(Error::InvalidAmountRequest)?
  208. .into()),
  209. Some(MeltOptions::Mpp { mpp }) => Ok(mpp.amount),
  210. Some(MeltOptions::Amountless { amountless }) => {
  211. let amount = amountless.amount_msat;
  212. if let Some(amount_msat) = request.amount_milli_satoshis() {
  213. if amount != amount_msat.into() {
  214. return Err(Error::InvalidAmountRequest);
  215. }
  216. }
  217. Ok(amount)
  218. }
  219. }
  220. }
  221. }
  222. /// Melt quote response [NUT-05]
  223. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
  224. #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
  225. #[serde(bound = "Q: Serialize + DeserializeOwned")]
  226. pub struct MeltQuoteBolt11Response<Q> {
  227. /// Quote Id
  228. pub quote: Q,
  229. /// The amount that needs to be provided
  230. pub amount: Amount,
  231. /// The fee reserve that is required
  232. pub fee_reserve: Amount,
  233. /// Quote State
  234. pub state: MeltQuoteState,
  235. /// Unix timestamp until the quote is valid
  236. pub expiry: u64,
  237. /// Payment preimage
  238. #[serde(skip_serializing_if = "Option::is_none")]
  239. pub payment_preimage: Option<String>,
  240. /// Change
  241. #[serde(skip_serializing_if = "Option::is_none")]
  242. pub change: Option<Vec<BlindSignature>>,
  243. /// Payment request to fulfill
  244. // REVIEW: This is now required in the spec, we should remove the option once all mints update
  245. #[serde(skip_serializing_if = "Option::is_none")]
  246. pub request: Option<String>,
  247. /// Unit
  248. // REVIEW: This is now required in the spec, we should remove the option once all mints update
  249. #[serde(skip_serializing_if = "Option::is_none")]
  250. pub unit: Option<CurrencyUnit>,
  251. }
  252. impl<Q: ToString> MeltQuoteBolt11Response<Q> {
  253. /// Convert a `MeltQuoteBolt11Response` with type Q (generic/unknown) to a
  254. /// `MeltQuoteBolt11Response` with `String`
  255. pub fn to_string_id(self) -> MeltQuoteBolt11Response<String> {
  256. MeltQuoteBolt11Response {
  257. quote: self.quote.to_string(),
  258. amount: self.amount,
  259. fee_reserve: self.fee_reserve,
  260. state: self.state,
  261. expiry: self.expiry,
  262. payment_preimage: self.payment_preimage,
  263. change: self.change,
  264. request: self.request,
  265. unit: self.unit,
  266. }
  267. }
  268. }
  269. #[cfg(feature = "mint")]
  270. impl From<MeltQuoteBolt11Response<QuoteId>> for MeltQuoteBolt11Response<String> {
  271. fn from(value: MeltQuoteBolt11Response<QuoteId>) -> Self {
  272. Self {
  273. quote: value.quote.to_string(),
  274. amount: value.amount,
  275. fee_reserve: value.fee_reserve,
  276. state: value.state,
  277. expiry: value.expiry,
  278. payment_preimage: value.payment_preimage,
  279. change: value.change,
  280. request: value.request,
  281. unit: value.unit,
  282. }
  283. }
  284. }