issue_bolt11.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. use std::collections::HashMap;
  2. use cdk_common::nut04::MintMethodOptions;
  3. use cdk_common::wallet::{MintQuote, Transaction, TransactionDirection};
  4. use cdk_common::PaymentMethod;
  5. use tracing::instrument;
  6. use crate::amount::SplitTarget;
  7. use crate::dhke::construct_proofs;
  8. use crate::nuts::nut00::ProofsMethods;
  9. use crate::nuts::{
  10. nut12, MintQuoteBolt11Request, MintQuoteBolt11Response, MintRequest, PreMintSecrets, Proofs,
  11. 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::nuts::CurrencyUnit;
  25. /// use cdk::wallet::Wallet;
  26. /// use cdk_sqlite::wallet::memory;
  27. /// use rand::random;
  28. ///
  29. /// #[tokio::main]
  30. /// async fn main() -> anyhow::Result<()> {
  31. /// let seed = random::<[u8; 64]>();
  32. /// let mint_url = "https://fake.thesimplekid.dev";
  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_info = self.load_mint_info().await?;
  50. let mint_url = self.mint_url.clone();
  51. let unit = self.unit.clone();
  52. // If we have a description, we check that the mint supports it.
  53. if description.is_some() {
  54. let settings = mint_info
  55. .nuts
  56. .nut04
  57. .get_settings(&unit, &crate::nuts::PaymentMethod::Bolt11)
  58. .ok_or(Error::UnsupportedUnit)?;
  59. match settings.options {
  60. Some(MintMethodOptions::Bolt11 { description }) if description => (),
  61. _ => return Err(Error::InvoiceDescriptionUnsupported),
  62. }
  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::new(
  73. quote_res.quote,
  74. mint_url,
  75. PaymentMethod::Bolt11,
  76. Some(amount),
  77. unit,
  78. quote_res.request,
  79. quote_res.expiry.unwrap_or(0),
  80. Some(secret_key),
  81. );
  82. let mut tx = self.localstore.begin_db_transaction().await?;
  83. tx.add_mint_quote(quote.clone()).await?;
  84. tx.commit().await?;
  85. Ok(quote)
  86. }
  87. /// Check mint quote status
  88. #[instrument(skip(self, quote_id))]
  89. pub async fn mint_quote_state(
  90. &self,
  91. quote_id: &str,
  92. ) -> Result<MintQuoteBolt11Response<String>, Error> {
  93. let response = self.client.get_mint_quote_status(quote_id).await?;
  94. let mut tx = self.localstore.begin_db_transaction().await?;
  95. match tx.get_mint_quote(quote_id).await? {
  96. Some(quote) => {
  97. let mut quote = quote;
  98. quote.state = response.state;
  99. tx.add_mint_quote(quote).await?;
  100. }
  101. None => {
  102. tracing::info!("Quote mint {} unknown", quote_id);
  103. }
  104. }
  105. tx.commit().await?;
  106. Ok(response)
  107. }
  108. /// Check status of pending mint quotes
  109. #[instrument(skip(self))]
  110. pub async fn check_all_mint_quotes(&self) -> Result<Amount, Error> {
  111. let mint_quotes = self.localstore.get_unissued_mint_quotes().await?;
  112. let mut total_amount = Amount::ZERO;
  113. for mint_quote in mint_quotes {
  114. match mint_quote.payment_method {
  115. PaymentMethod::Bolt11 => {
  116. let mint_quote_response = self.mint_quote_state(&mint_quote.id).await?;
  117. if mint_quote_response.state == MintQuoteState::Paid {
  118. let proofs = self
  119. .mint(&mint_quote.id, SplitTarget::default(), None)
  120. .await?;
  121. total_amount += proofs.total_amount()?;
  122. }
  123. }
  124. PaymentMethod::Bolt12 => {
  125. let mint_quote_response = self.mint_bolt12_quote_state(&mint_quote.id).await?;
  126. if mint_quote_response.amount_paid > mint_quote_response.amount_issued {
  127. let proofs = self
  128. .mint_bolt12(&mint_quote.id, None, SplitTarget::default(), None)
  129. .await?;
  130. total_amount += proofs.total_amount()?;
  131. }
  132. }
  133. PaymentMethod::Custom(_) => {
  134. tracing::warn!("We cannot check unknown types");
  135. }
  136. }
  137. }
  138. Ok(total_amount)
  139. }
  140. /// Get active mint quotes
  141. /// Returns mint quotes that are not expired and not yet issued.
  142. #[instrument(skip(self))]
  143. pub async fn get_active_mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
  144. let mut mint_quotes = self.localstore.get_mint_quotes().await?;
  145. let unix_time = unix_time();
  146. mint_quotes.retain(|quote| {
  147. quote.mint_url == self.mint_url
  148. && quote.state != MintQuoteState::Issued
  149. && quote.expiry > unix_time
  150. });
  151. Ok(mint_quotes)
  152. }
  153. /// Get unissued mint quotes
  154. /// Returns bolt11 quotes where nothing has been issued yet (amount_issued = 0) and all bolt12 quotes.
  155. /// Includes unpaid bolt11 quotes to allow checking with the mint if they've been paid (wallet state may be outdated).
  156. /// Filters out quotes from other mints. Does not filter by expiry time to allow
  157. /// checking with the mint if expired quotes can still be minted.
  158. #[instrument(skip(self))]
  159. pub async fn get_unissued_mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
  160. let mut pending_quotes = self.localstore.get_unissued_mint_quotes().await?;
  161. pending_quotes.retain(|quote| quote.mint_url == self.mint_url);
  162. Ok(pending_quotes)
  163. }
  164. /// Mint
  165. /// # Synopsis
  166. /// ```rust,no_run
  167. /// use std::sync::Arc;
  168. ///
  169. /// use anyhow::Result;
  170. /// use cdk::amount::{Amount, SplitTarget};
  171. /// use cdk::nuts::nut00::ProofsMethods;
  172. /// use cdk::nuts::CurrencyUnit;
  173. /// use cdk::wallet::Wallet;
  174. /// use cdk_sqlite::wallet::memory;
  175. /// use rand::random;
  176. ///
  177. /// #[tokio::main]
  178. /// async fn main() -> Result<()> {
  179. /// let seed = random::<[u8; 64]>();
  180. /// let mint_url = "https://fake.thesimplekid.dev";
  181. /// let unit = CurrencyUnit::Sat;
  182. ///
  183. /// let localstore = memory::empty().await?;
  184. /// let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), seed, None).unwrap();
  185. /// let amount = Amount::from(100);
  186. ///
  187. /// let quote = wallet.mint_quote(amount, None).await?;
  188. /// let quote_id = quote.id;
  189. /// // To be called after quote request is paid
  190. /// let minted_proofs = wallet.mint(&quote_id, SplitTarget::default(), None).await?;
  191. /// let minted_amount = minted_proofs.total_amount()?;
  192. ///
  193. /// Ok(())
  194. /// }
  195. /// ```
  196. #[instrument(skip(self))]
  197. pub async fn mint(
  198. &self,
  199. quote_id: &str,
  200. amount_split_target: SplitTarget,
  201. spending_conditions: Option<SpendingConditions>,
  202. ) -> Result<Proofs, Error> {
  203. let active_keyset_id = self.fetch_active_keyset().await?.id;
  204. let fee_and_amounts = self
  205. .get_keyset_fees_and_amounts_by_id(active_keyset_id)
  206. .await?;
  207. let mut tx = self.localstore.begin_db_transaction().await?;
  208. let quote_info = tx
  209. .get_mint_quote(quote_id)
  210. .await?
  211. .ok_or(Error::UnknownQuote)?;
  212. if quote_info.payment_method != PaymentMethod::Bolt11 {
  213. return Err(Error::UnsupportedPaymentMethod);
  214. }
  215. let amount_mintable = quote_info.amount_mintable();
  216. if amount_mintable == Amount::ZERO {
  217. tracing::debug!("Amount mintable 0.");
  218. return Err(Error::AmountUndefined);
  219. }
  220. let unix_time = unix_time();
  221. if quote_info.expiry > unix_time {
  222. tracing::warn!("Attempting to mint with expired quote.");
  223. }
  224. let split_target = match amount_split_target {
  225. SplitTarget::None => {
  226. self.determine_split_target_values(&mut tx, amount_mintable, &fee_and_amounts)
  227. .await?
  228. }
  229. s => s,
  230. };
  231. let premint_secrets = match &spending_conditions {
  232. Some(spending_conditions) => PreMintSecrets::with_conditions(
  233. active_keyset_id,
  234. amount_mintable,
  235. &split_target,
  236. spending_conditions,
  237. &fee_and_amounts,
  238. )?,
  239. None => {
  240. let amount_split =
  241. amount_mintable.split_targeted(&split_target, &fee_and_amounts)?;
  242. let num_secrets = amount_split.len() as u32;
  243. tracing::debug!(
  244. "Incrementing keyset {} counter by {}",
  245. active_keyset_id,
  246. num_secrets
  247. );
  248. // Atomically get the counter range we need
  249. let new_counter = tx
  250. .increment_keyset_counter(&active_keyset_id, num_secrets)
  251. .await?;
  252. let count = new_counter - num_secrets;
  253. PreMintSecrets::from_seed(
  254. active_keyset_id,
  255. count,
  256. &self.seed,
  257. amount_mintable,
  258. &split_target,
  259. &fee_and_amounts,
  260. )?
  261. }
  262. };
  263. let mut request = MintRequest {
  264. quote: quote_id.to_string(),
  265. outputs: premint_secrets.blinded_messages(),
  266. signature: None,
  267. };
  268. if let Some(secret_key) = &quote_info.secret_key {
  269. request.sign(secret_key.clone())?;
  270. }
  271. tx.commit().await?;
  272. let mint_res = self.client.post_mint(request).await?;
  273. let keys = self.load_keyset_keys(active_keyset_id).await?;
  274. // Verify the signature DLEQ is valid
  275. {
  276. for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) {
  277. let keys = self.load_keyset_keys(sig.keyset_id).await?;
  278. let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?;
  279. match sig.verify_dleq(key, premint.blinded_message.blinded_secret) {
  280. Ok(_) | Err(nut12::Error::MissingDleqProof) => (),
  281. Err(_) => return Err(Error::CouldNotVerifyDleq),
  282. }
  283. }
  284. }
  285. let proofs = construct_proofs(
  286. mint_res.signatures,
  287. premint_secrets.rs(),
  288. premint_secrets.secrets(),
  289. &keys,
  290. )?;
  291. // Start new transaction for post-mint operations
  292. let mut tx = self.localstore.begin_db_transaction().await?;
  293. // Remove filled quote from store
  294. tx.remove_mint_quote(&quote_info.id).await?;
  295. let proof_infos = proofs
  296. .iter()
  297. .map(|proof| {
  298. ProofInfo::new(
  299. proof.clone(),
  300. self.mint_url.clone(),
  301. State::Unspent,
  302. quote_info.unit.clone(),
  303. )
  304. })
  305. .collect::<Result<Vec<ProofInfo>, _>>()?;
  306. // Add new proofs to store
  307. tx.update_proofs(proof_infos, vec![]).await?;
  308. // Add transaction to store
  309. tx.add_transaction(Transaction {
  310. mint_url: self.mint_url.clone(),
  311. direction: TransactionDirection::Incoming,
  312. amount: proofs.total_amount()?,
  313. fee: Amount::ZERO,
  314. unit: self.unit.clone(),
  315. ys: proofs.ys()?,
  316. timestamp: unix_time,
  317. memo: None,
  318. metadata: HashMap::new(),
  319. quote_id: Some(quote_id.to_string()),
  320. payment_request: Some(quote_info.request),
  321. payment_proof: None,
  322. payment_method: Some(quote_info.payment_method),
  323. })
  324. .await?;
  325. tx.commit().await?;
  326. Ok(proofs)
  327. }
  328. }