diff --git a/crates/cashu-sdk/src/client/gloo_client.rs b/crates/cashu-sdk/src/client/gloo_client.rs index 5a0b23af..eebb4ca7 100644 --- a/crates/cashu-sdk/src/client/gloo_client.rs +++ b/crates/cashu-sdk/src/client/gloo_client.rs @@ -7,6 +7,7 @@ use cashu::nuts::{ }; #[cfg(feature = "nut07")] use cashu::nuts::{CheckSpendableRequest, CheckSpendableResponse}; +use cashu::Amount; use gloo::net::http::Request; use serde_json::Value; use url::Url; @@ -54,6 +55,35 @@ impl Client for HttpClient { } } + /// Mint Quote [NUT-04] + async fn post_mint_quote( + &self, + mint_url: Url, + amount: Amount, + unit: CurrencyUnit, + ) -> Result { + let url = join_url(mint_url, &["v1", "quote", "bolt11"])?; + + let request = MintQuoteBolt11Request { amount, unit }; + let res = Request::post(url.as_str()) + .json(&request) + .map_err(|err| Error::Gloo(err.to_string()))? + .send() + .await + .map_err(|err| Error::Gloo(err.to_string()))? + .json::() + .await + .map_err(|err| Error::Gloo(err.to_string()))?; + + let response: Result = + serde_json::from_value(res.clone()); + + match response { + Ok(res) => Ok(res), + Err(_) => Err(Error::from_json(&res.to_string())?), + } + } + /// Mint Tokens [NUT-04] async fn post_mint( &self, @@ -61,7 +91,7 @@ impl Client for HttpClient { quote: &str, premint_secrets: PreMintSecrets, ) -> Result { - let url = join_url(mint_url, &["mint"])?; + let url = join_url(mint_url, &["v1", "mint"])?; let request = MintBolt11Request { quote: quote.to_string(), diff --git a/crates/cashu-sdk/src/client/minreq_client.rs b/crates/cashu-sdk/src/client/minreq_client.rs index bf0572a2..e361b6d1 100644 --- a/crates/cashu-sdk/src/client/minreq_client.rs +++ b/crates/cashu-sdk/src/client/minreq_client.rs @@ -9,7 +9,9 @@ use cashu::nuts::{ }; #[cfg(feature = "nut07")] use cashu::nuts::{CheckSpendableRequest, CheckSpendableResponse}; +use cashu::Amount; use serde_json::Value; +use tracing::warn; use url::Url; use super::join_url; @@ -43,6 +45,34 @@ impl Client for HttpClient { } } + /// Mint Quote [NUT-04] + async fn post_mint_quote( + &self, + mint_url: Url, + amount: Amount, + unit: CurrencyUnit, + ) -> Result { + let url = join_url(mint_url, &["v1", "quote", "bolt11"])?; + + let request = MintQuoteBolt11Request { amount, unit }; + + let res = minreq::post(url) + .with_json(&request)? + .send()? + .json::()?; + + let response: Result = + serde_json::from_value(res.clone()); + + match response { + Ok(res) => Ok(res), + Err(_) => { + warn!("Bolt11 Mint Quote Unexpected response: {}", res); + Err(Error::from_json(&res.to_string())?) + } + } + } + /// Mint Tokens [NUT-04] async fn post_mint( &self, diff --git a/crates/cashu-sdk/src/client/mod.rs b/crates/cashu-sdk/src/client/mod.rs index 44c2ed3a..05a25b91 100644 --- a/crates/cashu-sdk/src/client/mod.rs +++ b/crates/cashu-sdk/src/client/mod.rs @@ -6,10 +6,10 @@ use cashu::nuts::nut00; #[cfg(feature = "nut07")] use cashu::nuts::CheckSpendableResponse; use cashu::nuts::{ - BlindedMessage, Keys, KeysetResponse, MeltBolt11Response, MintBolt11Response, MintInfo, - PreMintSecrets, Proof, SwapRequest, SwapResponse, + BlindedMessage, CurrencyUnit, Keys, KeysetResponse, MeltBolt11Response, MintBolt11Response, + MintInfo, MintQuoteBolt11Response, PreMintSecrets, Proof, SwapRequest, SwapResponse, }; -use cashu::utils; +use cashu::{utils, Amount}; use serde::{Deserialize, Serialize}; use thiserror::Error; use url::Url; @@ -87,7 +87,13 @@ pub trait Client { async fn get_mint_keysets(&self, mint_url: Url) -> Result; - // TODO: Hash should have a type + async fn post_mint_quote( + &self, + mint_url: Url, + amount: Amount, + unit: CurrencyUnit, + ) -> Result; + async fn post_mint( &self, mint_url: Url, diff --git a/crates/cashu-sdk/src/wallet.rs b/crates/cashu-sdk/src/wallet.rs index c8327527..602903f7 100644 --- a/crates/cashu-sdk/src/wallet.rs +++ b/crates/cashu-sdk/src/wallet.rs @@ -1,4 +1,5 @@ //! Cashu Wallet +use std::collections::HashMap; use std::str::FromStr; use cashu::dhke::{construct_proofs, unblind_message}; @@ -10,7 +11,7 @@ use cashu::nuts::{ }; #[cfg(feature = "nut07")] use cashu::types::ProofsStatus; -use cashu::types::{Melted, SendProofs}; +use cashu::types::{Melted, MintQuoteInfo, SendProofs}; use cashu::url::UncheckedUrl; use cashu::Amount; pub use cashu::Bolt11Invoice; @@ -39,16 +40,23 @@ pub enum Error { pub struct Wallet { pub client: C, pub mint_url: UncheckedUrl, + pub quotes: HashMap, pub mint_keys: Keys, pub balance: Amount, } impl Wallet { - pub fn new(client: C, mint_url: UncheckedUrl, mint_keys: Keys) -> Self { + pub fn new( + client: C, + mint_url: UncheckedUrl, + quotes: Vec, + mint_keys: Keys, + ) -> Self { Self { client, mint_url, mint_keys, + quotes: quotes.into_iter().map(|q| (q.id.clone(), q)).collect(), balance: Amount::ZERO, } } @@ -80,25 +88,56 @@ impl Wallet { spent: spent.into_iter().map(|(s, _)| s).cloned().collect(), }) } + /* + // TODO: Need to use the unit, check keyset is of the same unit of attempting to + // mint + pub async fn mint_token( + &self, + amount: Amount, + quote: &str, + memo: Option, + unit: Option, + ) -> Result { + let proofs = self.mint(amount, unit.clone().unwrap()).await?; - // TODO: Need to use the unit, check keyset is of the same unit of attempting to - // mint - pub async fn mint_token( - &self, + let token = Token::new(self.mint_url.clone(), proofs, memo, unit); + Ok(token?) + } + + */ + + pub async fn mint_quote( + &mut self, amount: Amount, - quote: &str, - memo: Option, - unit: Option, - ) -> Result { - let proofs = self.mint(amount, quote).await?; + unit: CurrencyUnit, + ) -> Result { + let quote_res = self + .client + .post_mint_quote(self.mint_url.clone().try_into()?, amount, unit.clone()) + .await?; - let token = Token::new(self.mint_url.clone(), proofs, memo, unit); - Ok(token?) + let quote = MintQuoteInfo { + id: quote_res.quote.clone(), + amount, + unit, + request: Some(Bolt11Invoice::from_str("e_res.request).unwrap()), + paid: quote_res.paid, + expiry: quote_res.expiry, + }; + + self.quotes.insert(quote_res.quote.clone(), quote.clone()); + + Ok(quote) } /// Mint Proofs - pub async fn mint(&self, amount: Amount, quote: &str) -> Result { - let premint_secrets = PreMintSecrets::random((&self.mint_keys).into(), amount)?; + pub async fn mint(&self, quote: &str) -> Result { + let quote_info = self + .quotes + .get(quote) + .ok_or(Error::Custom("Unknown quote".to_string()))?; + + let premint_secrets = PreMintSecrets::random((&self.mint_keys).into(), quote_info.amount)?; let mint_res = self .client @@ -271,6 +310,7 @@ impl Wallet { }) } + /// Melt pub async fn melt( &self, quote: String, diff --git a/crates/cashu/src/types.rs b/crates/cashu/src/types.rs index 403ff74f..041ce6aa 100644 --- a/crates/cashu/src/types.rs +++ b/crates/cashu/src/types.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::nuts::{CurrencyUnit, Proofs}; -use crate::Bolt11Invoice; +use crate::{Amount, Bolt11Invoice}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ProofsStatus { @@ -34,6 +34,17 @@ pub enum InvoiceStatus { InFlight, } +/// Mint Quote Info +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct MintQuoteInfo { + pub id: String, + pub amount: Amount, + pub unit: CurrencyUnit, + pub request: Option, + pub paid: bool, + pub expiry: u64, +} + /// Quote #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Quote {