mod.rs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. use cdk_common::mint::MintQuote;
  2. use cdk_common::payment::{
  3. Bolt11IncomingPaymentOptions, Bolt11Settings, Bolt12IncomingPaymentOptions,
  4. IncomingPaymentOptions, WaitPaymentResponse,
  5. };
  6. use cdk_common::util::unix_time;
  7. use cdk_common::{
  8. database, ensure_cdk, Amount, CurrencyUnit, Error, MintQuoteBolt11Request,
  9. MintQuoteBolt11Response, MintQuoteBolt12Request, MintQuoteBolt12Response, MintQuoteState,
  10. MintRequest, MintResponse, NotificationPayload, PaymentMethod, PublicKey,
  11. };
  12. use tracing::instrument;
  13. use uuid::Uuid;
  14. use crate::mint::Verification;
  15. use crate::Mint;
  16. #[cfg(feature = "auth")]
  17. mod auth;
  18. /// Request for creating a mint quote
  19. ///
  20. /// This enum represents the different types of payment requests that can be used
  21. /// to create a mint quote.
  22. #[derive(Debug, Clone, PartialEq, Eq)]
  23. pub enum MintQuoteRequest {
  24. /// Lightning Network BOLT11 invoice request
  25. Bolt11(MintQuoteBolt11Request),
  26. /// Lightning Network BOLT12 offer request
  27. Bolt12(MintQuoteBolt12Request),
  28. }
  29. impl From<MintQuoteBolt11Request> for MintQuoteRequest {
  30. fn from(request: MintQuoteBolt11Request) -> Self {
  31. MintQuoteRequest::Bolt11(request)
  32. }
  33. }
  34. impl From<MintQuoteBolt12Request> for MintQuoteRequest {
  35. fn from(request: MintQuoteBolt12Request) -> Self {
  36. MintQuoteRequest::Bolt12(request)
  37. }
  38. }
  39. /// Response for a mint quote request
  40. ///
  41. /// This enum represents the different types of payment responses that can be returned
  42. /// when creating a mint quote.
  43. #[derive(Debug, Clone, PartialEq, Eq)]
  44. pub enum MintQuoteResponse {
  45. /// Lightning Network BOLT11 invoice response
  46. Bolt11(MintQuoteBolt11Response<Uuid>),
  47. /// Lightning Network BOLT12 offer response
  48. Bolt12(MintQuoteBolt12Response<Uuid>),
  49. }
  50. impl TryFrom<MintQuoteResponse> for MintQuoteBolt11Response<Uuid> {
  51. type Error = Error;
  52. fn try_from(response: MintQuoteResponse) -> Result<Self, Self::Error> {
  53. match response {
  54. MintQuoteResponse::Bolt11(bolt11_response) => Ok(bolt11_response),
  55. _ => Err(Error::InvalidPaymentMethod),
  56. }
  57. }
  58. }
  59. impl TryFrom<MintQuoteResponse> for MintQuoteBolt12Response<Uuid> {
  60. type Error = Error;
  61. fn try_from(response: MintQuoteResponse) -> Result<Self, Self::Error> {
  62. match response {
  63. MintQuoteResponse::Bolt12(bolt12_response) => Ok(bolt12_response),
  64. _ => Err(Error::InvalidPaymentMethod),
  65. }
  66. }
  67. }
  68. impl TryFrom<MintQuote> for MintQuoteResponse {
  69. type Error = Error;
  70. fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
  71. match quote.payment_method {
  72. PaymentMethod::Bolt11 => {
  73. let bolt11_response: MintQuoteBolt11Response<Uuid> = quote.into();
  74. Ok(MintQuoteResponse::Bolt11(bolt11_response))
  75. }
  76. PaymentMethod::Bolt12 => {
  77. let bolt12_response = MintQuoteBolt12Response::try_from(quote)?;
  78. Ok(MintQuoteResponse::Bolt12(bolt12_response))
  79. }
  80. PaymentMethod::Custom(_) => Err(Error::InvalidPaymentMethod),
  81. }
  82. }
  83. }
  84. impl From<MintQuoteResponse> for MintQuoteBolt11Response<String> {
  85. fn from(response: MintQuoteResponse) -> Self {
  86. match response {
  87. MintQuoteResponse::Bolt11(bolt11_response) => MintQuoteBolt11Response {
  88. quote: bolt11_response.quote.to_string(),
  89. state: bolt11_response.state,
  90. request: bolt11_response.request,
  91. expiry: bolt11_response.expiry,
  92. pubkey: bolt11_response.pubkey,
  93. amount: bolt11_response.amount,
  94. unit: bolt11_response.unit,
  95. },
  96. _ => panic!("Expected Bolt11 response"),
  97. }
  98. }
  99. }
  100. impl Mint {
  101. /// Validates that a mint request meets all requirements
  102. ///
  103. /// Checks that:
  104. /// - Minting is enabled for the requested payment method
  105. /// - The currency unit is supported
  106. /// - The amount (if provided) is within the allowed range for the payment method
  107. ///
  108. /// # Arguments
  109. /// * `amount` - Optional amount to validate
  110. /// * `unit` - Currency unit for the request
  111. /// * `payment_method` - Payment method (Bolt11, Bolt12, etc.)
  112. ///
  113. /// # Returns
  114. /// * `Ok(())` if the request is acceptable
  115. /// * `Error` if any validation fails
  116. pub async fn check_mint_request_acceptable(
  117. &self,
  118. amount: Option<Amount>,
  119. unit: &CurrencyUnit,
  120. payment_method: &PaymentMethod,
  121. ) -> Result<(), Error> {
  122. let mint_info = self.localstore.get_mint_info().await?;
  123. let nut04 = &mint_info.nuts.nut04;
  124. ensure_cdk!(!nut04.disabled, Error::MintingDisabled);
  125. let disabled = nut04.disabled;
  126. ensure_cdk!(!disabled, Error::MintingDisabled);
  127. let settings = nut04
  128. .get_settings(unit, payment_method)
  129. .ok_or(Error::UnsupportedUnit)?;
  130. let min_amount = settings.min_amount;
  131. let max_amount = settings.max_amount;
  132. // Check amount limits if an amount is provided
  133. if let Some(amount) = amount {
  134. let is_above_max = max_amount.is_some_and(|max_amount| amount > max_amount);
  135. let is_below_min = min_amount.is_some_and(|min_amount| amount < min_amount);
  136. let is_out_of_range = is_above_max || is_below_min;
  137. ensure_cdk!(
  138. !is_out_of_range,
  139. Error::AmountOutofLimitRange(
  140. min_amount.unwrap_or_default(),
  141. max_amount.unwrap_or_default(),
  142. amount,
  143. )
  144. );
  145. }
  146. Ok(())
  147. }
  148. /// Creates a new mint quote for the specified payment request
  149. ///
  150. /// Handles both Bolt11 and Bolt12 payment requests by:
  151. /// 1. Validating the request parameters
  152. /// 2. Creating an appropriate payment request via the payment processor
  153. /// 3. Storing the quote in the database
  154. /// 4. Broadcasting a notification about the new quote
  155. ///
  156. /// # Arguments
  157. /// * `mint_quote_request` - The request containing payment details
  158. ///
  159. /// # Returns
  160. /// * `MintQuoteResponse` - Response with payment details if successful
  161. /// * `Error` - If the request is invalid or payment creation fails
  162. #[instrument(skip_all)]
  163. pub async fn get_mint_quote(
  164. &self,
  165. mint_quote_request: MintQuoteRequest,
  166. ) -> Result<MintQuoteResponse, Error> {
  167. let unit: CurrencyUnit;
  168. let amount;
  169. let pubkey;
  170. let payment_method;
  171. let create_invoice_response = match mint_quote_request {
  172. MintQuoteRequest::Bolt11(bolt11_request) => {
  173. unit = bolt11_request.unit;
  174. amount = Some(bolt11_request.amount);
  175. pubkey = bolt11_request.pubkey;
  176. payment_method = PaymentMethod::Bolt11;
  177. self.check_mint_request_acceptable(
  178. Some(bolt11_request.amount),
  179. &unit,
  180. &payment_method,
  181. )
  182. .await?;
  183. let ln = self.get_payment_processor(unit.clone(), PaymentMethod::Bolt11)?;
  184. let mint_ttl = self.localstore.get_quote_ttl().await?.mint_ttl;
  185. let quote_expiry = unix_time() + mint_ttl;
  186. let settings = ln.get_settings().await?;
  187. let settings: Bolt11Settings = serde_json::from_value(settings)?;
  188. let description = bolt11_request.description;
  189. if description.is_some() && !settings.invoice_description {
  190. tracing::error!("Backend does not support invoice description");
  191. return Err(Error::InvoiceDescriptionUnsupported);
  192. }
  193. let bolt11_options = Bolt11IncomingPaymentOptions {
  194. description,
  195. amount: bolt11_request.amount,
  196. unix_expiry: Some(quote_expiry),
  197. };
  198. let incoming_options = IncomingPaymentOptions::Bolt11(bolt11_options);
  199. ln.create_incoming_payment_request(&unit, incoming_options)
  200. .await
  201. .map_err(|err| {
  202. tracing::error!("Could not create invoice: {}", err);
  203. Error::InvalidPaymentRequest
  204. })?
  205. }
  206. MintQuoteRequest::Bolt12(bolt12_request) => {
  207. unit = bolt12_request.unit;
  208. amount = bolt12_request.amount;
  209. pubkey = Some(bolt12_request.pubkey);
  210. payment_method = PaymentMethod::Bolt12;
  211. self.check_mint_request_acceptable(amount, &unit, &payment_method)
  212. .await?;
  213. let ln = self.get_payment_processor(unit.clone(), payment_method.clone())?;
  214. let description = bolt12_request.description;
  215. let mint_ttl = self.localstore.get_quote_ttl().await?.mint_ttl;
  216. let expiry = unix_time() + mint_ttl;
  217. let bolt12_options = Bolt12IncomingPaymentOptions {
  218. description,
  219. amount,
  220. unix_expiry: Some(expiry),
  221. };
  222. let incoming_options = IncomingPaymentOptions::Bolt12(Box::new(bolt12_options));
  223. ln.create_incoming_payment_request(&unit, incoming_options)
  224. .await
  225. .map_err(|err| {
  226. tracing::error!("Could not create invoice: {}", err);
  227. Error::InvalidPaymentRequest
  228. })?
  229. }
  230. };
  231. let quote = MintQuote::new(
  232. None,
  233. create_invoice_response.request.to_string(),
  234. unit.clone(),
  235. amount,
  236. create_invoice_response.expiry.unwrap_or(0),
  237. create_invoice_response.request_lookup_id.clone(),
  238. pubkey,
  239. Amount::ZERO,
  240. Amount::ZERO,
  241. payment_method.clone(),
  242. unix_time(),
  243. vec![],
  244. vec![],
  245. );
  246. tracing::debug!(
  247. "New {} mint quote {} for {:?} {} with request id {:?}",
  248. payment_method,
  249. quote.id,
  250. amount,
  251. unit,
  252. create_invoice_response.request_lookup_id,
  253. );
  254. let mut tx = self.localstore.begin_transaction().await?;
  255. tx.add_mint_quote(quote.clone()).await?;
  256. tx.commit().await?;
  257. match payment_method {
  258. PaymentMethod::Bolt11 => {
  259. let res: MintQuoteBolt11Response<Uuid> = quote.clone().into();
  260. self.pubsub_manager
  261. .broadcast(NotificationPayload::MintQuoteBolt11Response(res));
  262. }
  263. PaymentMethod::Bolt12 => {
  264. let res: MintQuoteBolt12Response<Uuid> = quote.clone().try_into()?;
  265. self.pubsub_manager
  266. .broadcast(NotificationPayload::MintQuoteBolt12Response(res));
  267. }
  268. PaymentMethod::Custom(_) => {}
  269. }
  270. quote.try_into()
  271. }
  272. /// Retrieves all mint quotes from the database
  273. ///
  274. /// # Returns
  275. /// * `Vec<MintQuote>` - List of all mint quotes
  276. /// * `Error` if database access fails
  277. #[instrument(skip_all)]
  278. pub async fn mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
  279. let quotes = self.localstore.get_mint_quotes().await?;
  280. Ok(quotes)
  281. }
  282. /// Removes a mint quote from the database
  283. ///
  284. /// # Arguments
  285. /// * `quote_id` - The UUID of the quote to remove
  286. ///
  287. /// # Returns
  288. /// * `Ok(())` if removal was successful
  289. /// * `Error` if the quote doesn't exist or removal fails
  290. #[instrument(skip_all)]
  291. pub async fn remove_mint_quote(&self, quote_id: &Uuid) -> Result<(), Error> {
  292. let mut tx = self.localstore.begin_transaction().await?;
  293. tx.remove_mint_quote(quote_id).await?;
  294. tx.commit().await?;
  295. Ok(())
  296. }
  297. /// Marks a mint quote as paid based on the payment request ID
  298. ///
  299. /// Looks up the mint quote by the payment request ID and marks it as paid
  300. /// if found.
  301. ///
  302. /// # Arguments
  303. /// * `wait_payment_response` - Payment response containing payment details
  304. ///
  305. /// # Returns
  306. /// * `Ok(())` if the quote was found and updated
  307. /// * `Error` if the update fails
  308. #[instrument(skip_all)]
  309. pub async fn pay_mint_quote_for_request_id(
  310. &self,
  311. wait_payment_response: WaitPaymentResponse,
  312. ) -> Result<(), Error> {
  313. if wait_payment_response.payment_amount == Amount::ZERO {
  314. tracing::warn!(
  315. "Received payment response with 0 amount with payment id {}.",
  316. wait_payment_response.payment_id
  317. );
  318. return Err(Error::AmountUndefined);
  319. }
  320. let mut tx = self.localstore.begin_transaction().await?;
  321. if let Ok(Some(mint_quote)) = tx
  322. .get_mint_quote_by_request_lookup_id(&wait_payment_response.payment_identifier)
  323. .await
  324. {
  325. self.pay_mint_quote(&mut tx, &mint_quote, wait_payment_response)
  326. .await?;
  327. } else {
  328. tracing::warn!(
  329. "Could not get request for request lookup id {:?}.",
  330. wait_payment_response.payment_identifier
  331. );
  332. }
  333. tx.commit().await?;
  334. Ok(())
  335. }
  336. /// Marks a specific mint quote as paid
  337. ///
  338. /// Updates the mint quote with payment information and broadcasts
  339. /// a notification about the payment status change.
  340. ///
  341. /// # Arguments
  342. /// * `mint_quote` - The mint quote to mark as paid
  343. /// * `wait_payment_response` - Payment response containing payment details
  344. ///
  345. /// # Returns
  346. /// * `Ok(())` if the update was successful
  347. /// * `Error` if the update fails
  348. #[instrument(skip_all)]
  349. pub async fn pay_mint_quote(
  350. &self,
  351. tx: &mut Box<dyn database::MintTransaction<'_, database::Error> + Send + Sync + '_>,
  352. mint_quote: &MintQuote,
  353. wait_payment_response: WaitPaymentResponse,
  354. ) -> Result<(), Error> {
  355. tracing::debug!(
  356. "Received payment notification of {} for mint quote {} with payment id {}",
  357. wait_payment_response.payment_amount,
  358. mint_quote.id,
  359. wait_payment_response.payment_id
  360. );
  361. let quote_state = mint_quote.state();
  362. if !mint_quote
  363. .payment_ids()
  364. .contains(&&wait_payment_response.payment_id)
  365. {
  366. if mint_quote.payment_method == PaymentMethod::Bolt11
  367. && (quote_state == MintQuoteState::Issued || quote_state == MintQuoteState::Paid)
  368. {
  369. tracing::info!("Received payment notification for already seen payment.");
  370. } else {
  371. tx.increment_mint_quote_amount_paid(
  372. &mint_quote.id,
  373. wait_payment_response.payment_amount,
  374. wait_payment_response.payment_id,
  375. )
  376. .await?;
  377. self.pubsub_manager
  378. .mint_quote_bolt11_status(mint_quote.clone(), MintQuoteState::Paid);
  379. }
  380. } else {
  381. tracing::info!("Received payment notification for already seen payment.");
  382. }
  383. Ok(())
  384. }
  385. /// Checks the status of a mint quote and updates it if necessary
  386. ///
  387. /// If the quote is unpaid, this will check if payment has been received.
  388. /// Returns the current state of the quote.
  389. ///
  390. /// # Arguments
  391. /// * `quote_id` - The UUID of the quote to check
  392. ///
  393. /// # Returns
  394. /// * `MintQuoteResponse` - The current state of the quote
  395. /// * `Error` if the quote doesn't exist or checking fails
  396. #[instrument(skip(self))]
  397. pub async fn check_mint_quote(&self, quote_id: &Uuid) -> Result<MintQuoteResponse, Error> {
  398. let mut quote = self
  399. .localstore
  400. .get_mint_quote(quote_id)
  401. .await?
  402. .ok_or(Error::UnknownQuote)?;
  403. self.check_mint_quote_paid(&mut quote).await?;
  404. quote.try_into()
  405. }
  406. /// Processes a mint request to issue new tokens
  407. ///
  408. /// This function:
  409. /// 1. Verifies the mint quote exists and is paid
  410. /// 2. Validates the request signature if a pubkey was provided
  411. /// 3. Verifies the outputs match the expected amount
  412. /// 4. Signs the blinded messages
  413. /// 5. Updates the quote status
  414. /// 6. Broadcasts a notification about the status change
  415. ///
  416. /// # Arguments
  417. /// * `mint_request` - The mint request containing blinded outputs to sign
  418. ///
  419. /// # Returns
  420. /// * `MintBolt11Response` - Response containing blind signatures
  421. /// * `Error` if validation fails or signing fails
  422. #[instrument(skip_all)]
  423. pub async fn process_mint_request(
  424. &self,
  425. mint_request: MintRequest<Uuid>,
  426. ) -> Result<MintResponse, Error> {
  427. let mut mint_quote = self
  428. .localstore
  429. .get_mint_quote(&mint_request.quote)
  430. .await?
  431. .ok_or(Error::UnknownQuote)?;
  432. self.check_mint_quote_paid(&mut mint_quote).await?;
  433. // get the blind signatures before having starting the db transaction, if there are any
  434. // rollbacks this blind_signatures will be lost, and the signature is stateless. It is not a
  435. // good idea to call an external service (which is really a trait, it could be anything
  436. // anywhere) while keeping a database transaction on-going
  437. let blind_signatures = self.blind_sign(mint_request.outputs.clone()).await?;
  438. let mut tx = self.localstore.begin_transaction().await?;
  439. let mint_quote = tx
  440. .get_mint_quote(&mint_request.quote)
  441. .await?
  442. .ok_or(Error::UnknownQuote)?;
  443. match mint_quote.state() {
  444. MintQuoteState::Unpaid => {
  445. return Err(Error::UnpaidQuote);
  446. }
  447. MintQuoteState::Issued => {
  448. if mint_quote.payment_method == PaymentMethod::Bolt12
  449. && mint_quote.amount_paid() > mint_quote.amount_issued()
  450. {
  451. tracing::warn!("Mint quote should state should have been set to issued upon new payment. Something isn't right. Stopping mint");
  452. }
  453. return Err(Error::IssuedQuote);
  454. }
  455. MintQuoteState::Paid => (),
  456. }
  457. if mint_quote.payment_method == PaymentMethod::Bolt12 && mint_quote.pubkey.is_none() {
  458. tracing::warn!("Bolt12 mint quote created without pubkey");
  459. return Err(Error::SignatureMissingOrInvalid);
  460. }
  461. let mint_amount = match mint_quote.payment_method {
  462. PaymentMethod::Bolt11 => mint_quote.amount.ok_or(Error::AmountUndefined)?,
  463. PaymentMethod::Bolt12 => {
  464. if mint_quote.amount_issued() > mint_quote.amount_paid() {
  465. tracing::error!(
  466. "Quote state should not be issued if issued {} is > paid {}.",
  467. mint_quote.amount_issued(),
  468. mint_quote.amount_paid()
  469. );
  470. return Err(Error::UnpaidQuote);
  471. }
  472. mint_quote.amount_paid() - mint_quote.amount_issued()
  473. }
  474. _ => return Err(Error::UnsupportedPaymentMethod),
  475. };
  476. // If the there is a public key provoided in mint quote request
  477. // verify the signature is provided for the mint request
  478. if let Some(pubkey) = mint_quote.pubkey {
  479. mint_request.verify_signature(pubkey)?;
  480. }
  481. let Verification { amount, unit } =
  482. match self.verify_outputs(&mut tx, &mint_request.outputs).await {
  483. Ok(verification) => verification,
  484. Err(err) => {
  485. tracing::debug!("Could not verify mint outputs");
  486. return Err(err);
  487. }
  488. };
  489. // We check the total value of blinded messages == mint quote
  490. if amount != mint_amount {
  491. return Err(Error::TransactionUnbalanced(
  492. mint_amount.into(),
  493. mint_request.total_amount()?.into(),
  494. 0,
  495. ));
  496. }
  497. let unit = unit.ok_or(Error::UnsupportedUnit).unwrap();
  498. ensure_cdk!(unit == mint_quote.unit, Error::UnsupportedUnit);
  499. tx.add_blind_signatures(
  500. &mint_request
  501. .outputs
  502. .iter()
  503. .map(|p| p.blinded_secret)
  504. .collect::<Vec<PublicKey>>(),
  505. &blind_signatures,
  506. Some(mint_request.quote),
  507. )
  508. .await?;
  509. tx.increment_mint_quote_amount_issued(&mint_request.quote, mint_request.total_amount()?)
  510. .await?;
  511. tx.commit().await?;
  512. self.pubsub_manager
  513. .mint_quote_bolt11_status(mint_quote, MintQuoteState::Issued);
  514. Ok(MintResponse {
  515. signatures: blind_signatures,
  516. })
  517. }
  518. }