mint.rs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. use std::collections::HashMap;
  2. use cdk_common::ensure_cdk;
  3. use cdk_common::wallet::{Transaction, TransactionDirection};
  4. use tracing::instrument;
  5. use super::MintQuote;
  6. use crate::amount::SplitTarget;
  7. use crate::dhke::construct_proofs;
  8. use crate::nuts::nut00::ProofsMethods;
  9. use crate::nuts::{
  10. nut12, MintBolt11Request, MintQuoteBolt11Request, MintQuoteBolt11Response, PreMintSecrets,
  11. Proofs, SecretKey, SpendingConditions, State,
  12. };
  13. use crate::types::ProofInfo;
  14. use crate::util::unix_time;
  15. use crate::wallet::MintQuoteState;
  16. use crate::{Amount, Error, Wallet};
  17. impl Wallet {
  18. /// Mint Quote
  19. /// # Synopsis
  20. /// ```rust,no_run
  21. /// use std::sync::Arc;
  22. ///
  23. /// use cdk::amount::Amount;
  24. /// use cdk_sqlite::wallet::memory;
  25. /// use cdk::nuts::CurrencyUnit;
  26. /// use cdk::wallet::Wallet;
  27. /// use rand::random;
  28. ///
  29. /// #[tokio::main]
  30. /// async fn main() -> anyhow::Result<()> {
  31. /// let seed = random::<[u8; 32]>();
  32. /// let mint_url = "https://testnut.cashu.space";
  33. /// let unit = CurrencyUnit::Sat;
  34. ///
  35. /// let localstore = memory::empty().await?;
  36. /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None)?;
  37. /// let amount = Amount::from(100);
  38. ///
  39. /// let quote = wallet.mint_quote(amount, None).await?;
  40. /// Ok(())
  41. /// }
  42. /// ```
  43. #[instrument(skip(self))]
  44. pub async fn mint_quote(
  45. &self,
  46. amount: Amount,
  47. description: Option<String>,
  48. ) -> Result<MintQuote, Error> {
  49. let mint_url = self.mint_url.clone();
  50. let unit = self.unit.clone();
  51. // If we have a description, we check that the mint supports it.
  52. if description.is_some() {
  53. let settings = self
  54. .localstore
  55. .get_mint(mint_url.clone())
  56. .await?
  57. .ok_or(Error::IncorrectMint)?
  58. .nuts
  59. .nut04
  60. .get_settings(&unit, &crate::nuts::PaymentMethod::Bolt11)
  61. .ok_or(Error::UnsupportedUnit)?;
  62. ensure_cdk!(settings.description, Error::InvoiceDescriptionUnsupported);
  63. }
  64. let secret_key = SecretKey::generate();
  65. let request = MintQuoteBolt11Request {
  66. amount,
  67. unit: unit.clone(),
  68. description,
  69. pubkey: Some(secret_key.public_key()),
  70. };
  71. let quote_res = self.client.post_mint_quote(request).await?;
  72. let quote = MintQuote {
  73. mint_url,
  74. id: quote_res.quote,
  75. amount,
  76. unit,
  77. request: quote_res.request,
  78. state: quote_res.state,
  79. expiry: quote_res.expiry.unwrap_or(0),
  80. secret_key: Some(secret_key),
  81. };
  82. self.localstore.add_mint_quote(quote.clone()).await?;
  83. Ok(quote)
  84. }
  85. /// Check mint quote status
  86. #[instrument(skip(self, quote_id))]
  87. pub async fn mint_quote_state(
  88. &self,
  89. quote_id: &str,
  90. ) -> Result<MintQuoteBolt11Response<String>, Error> {
  91. let response = self.client.get_mint_quote_status(quote_id).await?;
  92. match self.localstore.get_mint_quote(quote_id).await? {
  93. Some(quote) => {
  94. let mut quote = quote;
  95. quote.state = response.state;
  96. self.localstore.add_mint_quote(quote).await?;
  97. }
  98. None => {
  99. tracing::info!("Quote mint {} unknown", quote_id);
  100. }
  101. }
  102. Ok(response)
  103. }
  104. /// Check status of pending mint quotes
  105. #[instrument(skip(self))]
  106. pub async fn check_all_mint_quotes(&self) -> Result<Amount, Error> {
  107. let mint_quotes = self.localstore.get_mint_quotes().await?;
  108. let mut total_amount = Amount::ZERO;
  109. for mint_quote in mint_quotes {
  110. let mint_quote_response = self.mint_quote_state(&mint_quote.id).await?;
  111. if mint_quote_response.state == MintQuoteState::Paid {
  112. let proofs = self
  113. .mint(&mint_quote.id, SplitTarget::default(), None)
  114. .await?;
  115. total_amount += proofs.total_amount()?;
  116. } else if mint_quote.expiry.le(&unix_time()) {
  117. self.localstore.remove_mint_quote(&mint_quote.id).await?;
  118. }
  119. }
  120. Ok(total_amount)
  121. }
  122. /// Mint
  123. /// # Synopsis
  124. /// ```rust,no_run
  125. /// use std::sync::Arc;
  126. ///
  127. /// use anyhow::Result;
  128. /// use cdk::amount::{Amount, SplitTarget};
  129. /// use cdk_sqlite::wallet::memory;
  130. /// use cdk::nuts::nut00::ProofsMethods;
  131. /// use cdk::nuts::CurrencyUnit;
  132. /// use cdk::wallet::Wallet;
  133. /// use rand::random;
  134. ///
  135. /// #[tokio::main]
  136. /// async fn main() -> Result<()> {
  137. /// let seed = random::<[u8; 32]>();
  138. /// let mint_url = "https://testnut.cashu.space";
  139. /// let unit = CurrencyUnit::Sat;
  140. ///
  141. /// let localstore = memory::empty().await?;
  142. /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None).unwrap();
  143. /// let amount = Amount::from(100);
  144. ///
  145. /// let quote = wallet.mint_quote(amount, None).await?;
  146. /// let quote_id = quote.id;
  147. /// // To be called after quote request is paid
  148. /// let minted_proofs = wallet.mint(&quote_id, SplitTarget::default(), None).await?;
  149. /// let minted_amount = minted_proofs.total_amount()?;
  150. ///
  151. /// Ok(())
  152. /// }
  153. /// ```
  154. #[instrument(skip(self))]
  155. pub async fn mint(
  156. &self,
  157. quote_id: &str,
  158. amount_split_target: SplitTarget,
  159. spending_conditions: Option<SpendingConditions>,
  160. ) -> Result<Proofs, Error> {
  161. // Check that mint is in store of mints
  162. if self
  163. .localstore
  164. .get_mint(self.mint_url.clone())
  165. .await?
  166. .is_none()
  167. {
  168. self.get_mint_info().await?;
  169. }
  170. let quote_info = self
  171. .localstore
  172. .get_mint_quote(quote_id)
  173. .await?
  174. .ok_or(Error::UnknownQuote)?;
  175. let unix_time = unix_time();
  176. ensure_cdk!(
  177. quote_info.expiry > unix_time || quote_info.expiry == 0,
  178. Error::ExpiredQuote(quote_info.expiry, unix_time)
  179. );
  180. let active_keyset_id = self.get_active_mint_keyset().await?.id;
  181. let count = self
  182. .localstore
  183. .get_keyset_counter(&active_keyset_id)
  184. .await?;
  185. let count = count.map_or(0, |c| c + 1);
  186. let premint_secrets = match &spending_conditions {
  187. Some(spending_conditions) => PreMintSecrets::with_conditions(
  188. active_keyset_id,
  189. quote_info.amount,
  190. &amount_split_target,
  191. spending_conditions,
  192. )?,
  193. None => PreMintSecrets::from_xpriv(
  194. active_keyset_id,
  195. count,
  196. self.xpriv,
  197. quote_info.amount,
  198. &amount_split_target,
  199. )?,
  200. };
  201. let mut request = MintBolt11Request {
  202. quote: quote_id.to_string(),
  203. outputs: premint_secrets.blinded_messages(),
  204. signature: None,
  205. };
  206. if let Some(secret_key) = quote_info.secret_key {
  207. request.sign(secret_key)?;
  208. }
  209. let mint_res = self.client.post_mint(request).await?;
  210. let keys = self.get_keyset_keys(active_keyset_id).await?;
  211. // Verify the signature DLEQ is valid
  212. {
  213. for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) {
  214. let keys = self.get_keyset_keys(sig.keyset_id).await?;
  215. let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?;
  216. match sig.verify_dleq(key, premint.blinded_message.blinded_secret) {
  217. Ok(_) | Err(nut12::Error::MissingDleqProof) => (),
  218. Err(_) => return Err(Error::CouldNotVerifyDleq),
  219. }
  220. }
  221. }
  222. let proofs = construct_proofs(
  223. mint_res.signatures,
  224. premint_secrets.rs(),
  225. premint_secrets.secrets(),
  226. &keys,
  227. )?;
  228. // Remove filled quote from store
  229. self.localstore.remove_mint_quote(&quote_info.id).await?;
  230. if spending_conditions.is_none() {
  231. tracing::debug!(
  232. "Incrementing keyset {} counter by {}",
  233. active_keyset_id,
  234. proofs.len()
  235. );
  236. // Update counter for keyset
  237. self.localstore
  238. .increment_keyset_counter(&active_keyset_id, proofs.len() as u32)
  239. .await?;
  240. }
  241. let proof_infos = proofs
  242. .iter()
  243. .map(|proof| {
  244. ProofInfo::new(
  245. proof.clone(),
  246. self.mint_url.clone(),
  247. State::Unspent,
  248. quote_info.unit.clone(),
  249. )
  250. })
  251. .collect::<Result<Vec<ProofInfo>, _>>()?;
  252. // Add new proofs to store
  253. self.localstore.update_proofs(proof_infos, vec![]).await?;
  254. // Add transaction to store
  255. self.localstore
  256. .add_transaction(Transaction {
  257. mint_url: self.mint_url.clone(),
  258. direction: TransactionDirection::Incoming,
  259. amount: proofs.total_amount()?,
  260. fee: Amount::ZERO,
  261. unit: self.unit.clone(),
  262. ys: proofs.ys()?,
  263. timestamp: unix_time,
  264. memo: None,
  265. metadata: HashMap::new(),
  266. })
  267. .await?;
  268. Ok(proofs)
  269. }
  270. }