issue_bolt12.rs 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. use std::collections::HashMap;
  2. use cdk_common::nut04::MintMethodOptions;
  3. use cdk_common::nut25::MintQuoteBolt12Request;
  4. use cdk_common::wallet::{Transaction, TransactionDirection};
  5. use cdk_common::{Proofs, SecretKey};
  6. use tracing::instrument;
  7. use crate::amount::SplitTarget;
  8. use crate::dhke::construct_proofs;
  9. use crate::nuts::nut00::ProofsMethods;
  10. use crate::nuts::{
  11. nut12, MintQuoteBolt12Response, MintRequest, PaymentMethod, PreMintSecrets, SpendingConditions,
  12. State,
  13. };
  14. use crate::types::ProofInfo;
  15. use crate::util::unix_time;
  16. use crate::wallet::MintQuote;
  17. use crate::{Amount, Error, Wallet};
  18. impl Wallet {
  19. /// Mint Bolt12
  20. #[instrument(skip(self))]
  21. pub async fn mint_bolt12_quote(
  22. &self,
  23. amount: Option<Amount>,
  24. description: Option<String>,
  25. ) -> Result<MintQuote, Error> {
  26. let mint_info = self.load_mint_info().await?;
  27. let mint_url = self.mint_url.clone();
  28. let unit = &self.unit;
  29. // If we have a description, we check that the mint supports it.
  30. if description.is_some() {
  31. let mint_method_settings = mint_info
  32. .nuts
  33. .nut04
  34. .get_settings(unit, &crate::nuts::PaymentMethod::Bolt12)
  35. .ok_or(Error::UnsupportedUnit)?;
  36. match mint_method_settings.options {
  37. Some(MintMethodOptions::Bolt11 { description }) if description => (),
  38. _ => return Err(Error::InvoiceDescriptionUnsupported),
  39. }
  40. }
  41. let secret_key = SecretKey::generate();
  42. let mint_request = MintQuoteBolt12Request {
  43. amount,
  44. unit: self.unit.clone(),
  45. description,
  46. pubkey: secret_key.public_key(),
  47. };
  48. let quote_res = self.client.post_mint_bolt12_quote(mint_request).await?;
  49. let quote = MintQuote::new(
  50. quote_res.quote,
  51. mint_url,
  52. PaymentMethod::Bolt12,
  53. amount,
  54. unit.clone(),
  55. quote_res.request,
  56. quote_res.expiry.unwrap_or(0),
  57. Some(secret_key),
  58. );
  59. let mut tx = self.localstore.begin_db_transaction().await?;
  60. tx.add_mint_quote(quote.clone()).await?;
  61. tx.commit().await?;
  62. Ok(quote)
  63. }
  64. /// Mint bolt12
  65. #[instrument(skip(self))]
  66. pub async fn mint_bolt12(
  67. &self,
  68. quote_id: &str,
  69. amount: Option<Amount>,
  70. amount_split_target: SplitTarget,
  71. spending_conditions: Option<SpendingConditions>,
  72. ) -> Result<Proofs, Error> {
  73. let active_keyset_id = self.fetch_active_keyset().await?.id;
  74. let fee_and_amounts = self
  75. .get_keyset_fees_and_amounts_by_id(active_keyset_id)
  76. .await?;
  77. let mut tx = self.localstore.begin_db_transaction().await?;
  78. let quote_info = tx.get_mint_quote(quote_id).await?;
  79. let quote_info = if let Some(quote) = quote_info {
  80. if quote.expiry.le(&unix_time()) && quote.expiry.ne(&0) {
  81. tracing::info!("Attempting to mint expired quote.");
  82. }
  83. quote.clone()
  84. } else {
  85. return Err(Error::UnknownQuote);
  86. };
  87. let (mut tx, quote_info, amount) = match amount {
  88. Some(amount) => (tx, quote_info, amount),
  89. None => {
  90. // If an amount it not supplied with check the status of the quote
  91. // The mint will tell us how much can be minted
  92. tx.commit().await?;
  93. let state = self.mint_bolt12_quote_state(quote_id).await?;
  94. let mut tx = self.localstore.begin_db_transaction().await?;
  95. let quote_info = tx
  96. .get_mint_quote(quote_id)
  97. .await?
  98. .ok_or(Error::UnknownQuote)?;
  99. (tx, quote_info, state.amount_paid - state.amount_issued)
  100. }
  101. };
  102. if amount == Amount::ZERO {
  103. tracing::error!("Cannot mint zero amount.");
  104. return Err(Error::UnpaidQuote);
  105. }
  106. let split_target = match amount_split_target {
  107. SplitTarget::None => {
  108. self.determine_split_target_values(&mut tx, amount, &fee_and_amounts)
  109. .await?
  110. }
  111. s => s,
  112. };
  113. let premint_secrets = match &spending_conditions {
  114. Some(spending_conditions) => PreMintSecrets::with_conditions(
  115. active_keyset_id,
  116. amount,
  117. &split_target,
  118. spending_conditions,
  119. &fee_and_amounts,
  120. )?,
  121. None => {
  122. let amount_split = amount.split_targeted(&split_target, &fee_and_amounts)?;
  123. let num_secrets = amount_split.len() as u32;
  124. tracing::debug!(
  125. "Incrementing keyset {} counter by {}",
  126. active_keyset_id,
  127. num_secrets
  128. );
  129. // Atomically get the counter range we need
  130. let new_counter = tx
  131. .increment_keyset_counter(&active_keyset_id, num_secrets)
  132. .await?;
  133. let count = new_counter - num_secrets;
  134. PreMintSecrets::from_seed(
  135. active_keyset_id,
  136. count,
  137. &self.seed,
  138. amount,
  139. &split_target,
  140. &fee_and_amounts,
  141. )?
  142. }
  143. };
  144. let mut request = MintRequest {
  145. quote: quote_id.to_string(),
  146. outputs: premint_secrets.blinded_messages(),
  147. signature: None,
  148. };
  149. if let Some(secret_key) = quote_info.secret_key.clone() {
  150. request.sign(secret_key)?;
  151. } else {
  152. tracing::error!("Signature is required for bolt12.");
  153. return Err(Error::SignatureMissingOrInvalid);
  154. }
  155. tx.commit().await?;
  156. let mint_res = self.client.post_mint(request).await?;
  157. let mut tx = self.localstore.begin_db_transaction().await?;
  158. let keys = self.load_keyset_keys(active_keyset_id).await?;
  159. // Verify the signature DLEQ is valid
  160. {
  161. for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) {
  162. let keys = self.load_keyset_keys(sig.keyset_id).await?;
  163. let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?;
  164. match sig.verify_dleq(key, premint.blinded_message.blinded_secret) {
  165. Ok(_) | Err(nut12::Error::MissingDleqProof) => (),
  166. Err(_) => return Err(Error::CouldNotVerifyDleq),
  167. }
  168. }
  169. }
  170. let proofs = construct_proofs(
  171. mint_res.signatures,
  172. premint_secrets.rs(),
  173. premint_secrets.secrets(),
  174. &keys,
  175. )?;
  176. // Remove filled quote from store
  177. let mut quote_info = tx
  178. .get_mint_quote(quote_id)
  179. .await?
  180. .ok_or(Error::UnpaidQuote)?;
  181. quote_info.amount_issued += proofs.total_amount()?;
  182. tx.add_mint_quote(quote_info.clone()).await?;
  183. let proof_infos = proofs
  184. .iter()
  185. .map(|proof| {
  186. ProofInfo::new(
  187. proof.clone(),
  188. self.mint_url.clone(),
  189. State::Unspent,
  190. quote_info.unit.clone(),
  191. )
  192. })
  193. .collect::<Result<Vec<ProofInfo>, _>>()?;
  194. // Add new proofs to store
  195. tx.update_proofs(proof_infos, vec![]).await?;
  196. // Add transaction to store
  197. tx.add_transaction(Transaction {
  198. mint_url: self.mint_url.clone(),
  199. direction: TransactionDirection::Incoming,
  200. amount: proofs.total_amount()?,
  201. fee: Amount::ZERO,
  202. unit: self.unit.clone(),
  203. ys: proofs.ys()?,
  204. timestamp: unix_time(),
  205. memo: None,
  206. metadata: HashMap::new(),
  207. quote_id: Some(quote_id.to_string()),
  208. payment_request: Some(quote_info.request),
  209. payment_proof: None,
  210. })
  211. .await?;
  212. tx.commit().await?;
  213. Ok(proofs)
  214. }
  215. /// Check mint quote status
  216. #[instrument(skip(self, quote_id))]
  217. pub async fn mint_bolt12_quote_state(
  218. &self,
  219. quote_id: &str,
  220. ) -> Result<MintQuoteBolt12Response<String>, Error> {
  221. let response = self.client.get_mint_quote_bolt12_status(quote_id).await?;
  222. let mut tx = self.localstore.begin_db_transaction().await?;
  223. match tx.get_mint_quote(quote_id).await? {
  224. Some(quote) => {
  225. let mut quote = quote;
  226. quote.amount_issued = response.amount_issued;
  227. quote.amount_paid = response.amount_paid;
  228. tx.add_mint_quote(quote).await?;
  229. }
  230. None => {
  231. tracing::info!("Quote mint {} unknown", quote_id);
  232. }
  233. }
  234. tx.commit().await?;
  235. Ok(response)
  236. }
  237. }