diff --git a/.helix/languages.toml b/.helix/languages.toml new file mode 100644 index 00000000..a79ac06f --- /dev/null +++ b/.helix/languages.toml @@ -0,0 +1,4 @@ +[[language]] +name = "rust" +config = { cargo = { features = [ "blocking", "wallet" ] } } + diff --git a/Cargo.toml b/Cargo.toml index 31a8cc93..d49842a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,5 +23,6 @@ keywords = ["bitcoin", "e-cash", "cashu"] serde = { version = "1.0.160", features = ["derive"]} serde_json = "1.0.96" url = "2.3.1" +tokio = { version = "1", default-features = false } tracing = "0.1" tracing-subscriber = "0.3" \ No newline at end of file diff --git a/crates/cashu-sdk/Cargo.toml b/crates/cashu-sdk/Cargo.toml index e171ead9..fe78cb80 100644 --- a/crates/cashu-sdk/Cargo.toml +++ b/crates/cashu-sdk/Cargo.toml @@ -10,9 +10,17 @@ license.workspace = true [features] default = ["mint", "wallet"] -mint = [] +mint = ["cashu/mint"] +blocking = ["once_cell"] +wallet = ["cashu/wallet", "minreq", "once_cell"] + + # Fix: Should be minreq or gloo -wallet = ["minreq"] +# [target.'cfg(not(target_arch = "wasm32"))'.features] +# wallet = ["cashu/wallet", "minreq", "once_cell"] + +# [target.'cfg(target_arch = "wasm32")'.features] +# wallet = ["cashu/wallet", "gloo", "once_cell"] [dependencies] cashu = { path = "../cashu" } @@ -20,9 +28,16 @@ serde = { workspace = true } serde_json = { workspace = true } url = { workspace = true } tracing = { workspace = true } +futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } +once_cell = { version = "1.17", optional = true } -[target.'cfg(target_arch = "wasm32")'.dependencies] -gloo = { version = "0.9.0", features = ["net"]} [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { workspace = true, features = ["rt-multi-thread", "time", "macros", "sync"] } minreq = { version = "2.7.0", optional = true, features = ["json-using-serde", "https"] } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +tokio = { workspace = true, features = ["rt", "macros", "sync"] } +gloo = { version = "0.9.0", features = ["net"]} + + diff --git a/crates/cashu-sdk/src/client/blocking.rs b/crates/cashu-sdk/src/client/blocking.rs new file mode 100644 index 00000000..12dee6ad --- /dev/null +++ b/crates/cashu-sdk/src/client/blocking.rs @@ -0,0 +1,80 @@ +use crate::RUNTIME; + +use cashu::{ + nuts::{ + nut00::{self, wallet::BlindedMessages, BlindedMessage, Proof}, + nut01::Keys, + nut02, + nut03::RequestMintResponse, + nut04::PostMintResponse, + nut05::CheckFeesResponse, + nut06::{SplitRequest, SplitResponse}, + nut07::CheckSpendableResponse, + nut08::MeltResponse, + nut09::MintInfo, + }, + Amount, Bolt11Invoice, +}; + +use super::Error; + +#[derive(Debug, Clone)] +pub struct Client { + pub(crate) client: super::Client, +} + +impl Client { + pub fn new(mint_url: &str) -> Result { + Ok(Self { + client: super::Client::new(mint_url)?, + }) + } + + pub fn get_keys(&self) -> Result { + RUNTIME.block_on(async { self.client.get_keys().await }) + } + + pub fn get_keysets(&self) -> Result { + RUNTIME.block_on(async { self.client.get_keysets().await }) + } + + pub fn request_mint(&self, amount: Amount) -> Result { + RUNTIME.block_on(async { self.client.request_mint(amount).await }) + } + + pub fn mint( + &self, + blinded_mssages: BlindedMessages, + hash: &str, + ) -> Result { + RUNTIME.block_on(async { self.client.mint(blinded_mssages, hash).await }) + } + + pub fn check_fees(&self, invoice: Bolt11Invoice) -> Result { + RUNTIME.block_on(async { self.client.check_fees(invoice).await }) + } + + pub fn melt( + &self, + proofs: Vec, + invoice: Bolt11Invoice, + outputs: Option>, + ) -> Result { + RUNTIME.block_on(async { self.client.melt(proofs, invoice, outputs).await }) + } + + pub fn split(&self, split_request: SplitRequest) -> Result { + RUNTIME.block_on(async { self.client.split(split_request).await }) + } + + pub fn check_spendable( + &self, + proofs: &Vec, + ) -> Result { + RUNTIME.block_on(async { self.client.check_spendable(proofs).await }) + } + + pub fn get_info(&self) -> Result { + RUNTIME.block_on(async { self.client.get_info().await }) + } +} diff --git a/crates/cashu-sdk/src/client/mod.rs b/crates/cashu-sdk/src/client/mod.rs index e02cb1e9..6c281dfa 100644 --- a/crates/cashu-sdk/src/client/mod.rs +++ b/crates/cashu-sdk/src/client/mod.rs @@ -21,6 +21,9 @@ use cashu::Amount; #[cfg(target_arch = "wasm32")] use gloo::net::http::Request; +#[cfg(feature = "blocking")] +pub mod blocking; + pub use cashu::Bolt11Invoice; #[derive(Debug)] diff --git a/crates/cashu-sdk/src/lib.rs b/crates/cashu-sdk/src/lib.rs index 810d3aae..81975291 100644 --- a/crates/cashu-sdk/src/lib.rs +++ b/crates/cashu-sdk/src/lib.rs @@ -1,7 +1,28 @@ -#[cfg(feature = "wallet")] +#[cfg(feature = "blocking")] +use once_cell::sync::Lazy; +#[cfg(feature = "blocking")] +use tokio::runtime::Runtime; + +#[cfg(feature = "blocking")] +use futures_util::Future; + +// #[cfg(feature = "wallet")] pub(crate) mod client; #[cfg(feature = "mint")] pub mod mint; #[cfg(feature = "wallet")] pub mod wallet; + +pub use cashu::{self, *}; + +#[cfg(all(feature = "blocking", feature = "wallet"))] +use self::client::blocking; + +#[cfg(feature = "blocking")] +static RUNTIME: Lazy = Lazy::new(|| Runtime::new().expect("Can't start Tokio runtime")); + +#[cfg(feature = "blocking")] +pub fn block_on(future: F) -> F::Output { + RUNTIME.block_on(future) +} diff --git a/crates/cashu-sdk/src/wallet.rs b/crates/cashu-sdk/src/wallet.rs index e63df622..bfa63e25 100644 --- a/crates/cashu-sdk/src/wallet.rs +++ b/crates/cashu-sdk/src/wallet.rs @@ -16,6 +16,10 @@ use cashu::Amount; pub use cashu::Bolt11Invoice; use tracing::warn; +#[cfg(feature = "blocking")] +use crate::client::blocking::Client; + +#[cfg(not(feature = "blocking"))] use crate::client::Client; #[derive(Debug)] @@ -71,6 +75,7 @@ impl Wallet { // TODO: getter method for keys that if it cant get them try again /// Check if a proof is spent + #[cfg(not(feature = "blocking"))] pub async fn check_proofs_spent(&self, proofs: &mint::Proofs) -> Result { let spendable = self.client.check_spendable(proofs).await?; @@ -86,12 +91,37 @@ impl Wallet { }) } + /// Check if a proof is spent + #[cfg(feature = "blocking")] + pub fn check_proofs_spent(&self, proofs: &mint::Proofs) -> Result { + let spendable = self.client.check_spendable(proofs)?; + + // Separate proofs in spent and unspent based on mint response + let (spendable, spent): (Vec<_>, Vec<_>) = proofs + .iter() + .zip(spendable.spendable.iter()) + .partition(|(_, &b)| b); + + Ok(ProofsStatus { + spendable: spendable.into_iter().map(|(s, _)| s).cloned().collect(), + spent: spent.into_iter().map(|(s, _)| s).cloned().collect(), + }) + } + /// Request Token Mint + #[cfg(not(feature = "blocking"))] pub async fn request_mint(&self, amount: Amount) -> Result { Ok(self.client.request_mint(amount).await?) } + /// Request Token Mint + #[cfg(feature = "blocking")] + pub fn request_mint(&self, amount: Amount) -> Result { + Ok(self.client.request_mint(amount)?) + } + /// Mint Token + #[cfg(not(feature = "blocking"))] pub async fn mint_token(&self, amount: Amount, hash: &str) -> Result { let proofs = self.mint(amount, hash).await?; @@ -99,7 +129,17 @@ impl Wallet { Ok(token) } + /// Blocking Mint Token + #[cfg(feature = "blocking")] + pub fn mint_token(&self, amount: Amount, hash: &str) -> Result { + let proofs = self.mint(amount, hash)?; + + let token = Token::new(self.client.client.mint_url.clone(), proofs, None); + Ok(token) + } + /// Mint Proofs + #[cfg(not(feature = "blocking"))] pub async fn mint(&self, amount: Amount, hash: &str) -> Result { let blinded_messages = BlindedMessages::random(amount)?; @@ -115,12 +155,37 @@ impl Wallet { Ok(proofs) } + /// Blocking Mint Proofs + #[cfg(feature = "blocking")] + pub fn mint(&self, amount: Amount, hash: &str) -> Result { + let blinded_messages = BlindedMessages::random(amount)?; + + let mint_res = self.client.mint(blinded_messages.clone(), hash)?; + + let proofs = construct_proofs( + mint_res.promises, + blinded_messages.rs, + blinded_messages.secrets, + &self.mint_keys, + )?; + + Ok(proofs) + } + /// Check fee + #[cfg(not(feature = "blocking"))] pub async fn check_fee(&self, invoice: Bolt11Invoice) -> Result { Ok(self.client.check_fees(invoice).await?.fee) } + /// Check fee + #[cfg(feature = "blocking")] + pub fn check_fee(&self, invoice: Bolt11Invoice) -> Result { + Ok(self.client.check_fees(invoice)?.fee) + } + /// Receive + #[cfg(not(feature = "blocking"))] pub async fn receive(&self, encoded_token: &str) -> Result { let token_data = Token::from_str(encoded_token)?; @@ -160,6 +225,51 @@ impl Wallet { Ok(proofs.iter().flatten().cloned().collect()) } + /// Blocking Receive + #[cfg(feature = "blocking")] + pub fn receive(&self, encoded_token: &str) -> Result { + let token_data = Token::from_str(encoded_token)?; + + let mut proofs: Vec = vec![vec![]]; + for token in token_data.token { + if token.proofs.is_empty() { + continue; + } + + let keys = if token + .mint + .to_string() + .eq(&self.client.client.mint_url.to_string()) + { + self.mint_keys.clone() + } else { + Client::new(token.mint.as_str())?.get_keys()? + }; + + // Sum amount of all proofs + let _amount: Amount = token.proofs.iter().map(|p| p.amount).sum(); + + let split_payload = self.create_split(token.proofs)?; + + let split_response = self.client.split(split_payload.split_payload)?; + + if let Some(promises) = &split_response.promises { + // Proof to keep + let p = construct_proofs( + promises.to_owned(), + split_payload.blinded_messages.rs, + split_payload.blinded_messages.secrets, + &keys, + )?; + proofs.push(p); + } else { + warn!("Response missing promises"); + return Err(Error::Custom("Split response missing promises".to_string())); + } + } + Ok(proofs.iter().flatten().cloned().collect()) + } + /// Create Split Payload fn create_split(&self, proofs: Proofs) -> Result { let value = proofs.iter().map(|p| p.amount).sum(); @@ -217,6 +327,7 @@ impl Wallet { } /// Send + #[cfg(not(feature = "blocking"))] pub async fn send(&self, amount: Amount, proofs: Proofs) -> Result { let mut amount_available = Amount::ZERO; let mut send_proofs = SendProofs::default(); @@ -277,6 +388,69 @@ impl Wallet { }) } + /// Send + #[cfg(feature = "blocking")] + pub fn send(&self, amount: Amount, proofs: Proofs) -> Result { + let mut amount_available = Amount::ZERO; + let mut send_proofs = SendProofs::default(); + + for proof in proofs { + let proof_value = proof.amount; + if amount_available > amount { + send_proofs.change_proofs.push(proof); + } else { + send_proofs.send_proofs.push(proof); + } + amount_available += proof_value; + } + + if amount_available.lt(&amount) { + println!("Not enough funds"); + return Err(Error::InsufficantFunds); + } + + // If amount available is EQUAL to send amount no need to split + if amount_available.eq(&amount) { + return Ok(send_proofs); + } + + let _amount_to_keep = amount_available - amount; + let amount_to_send = amount; + + let split_payload = self.create_split(send_proofs.send_proofs)?; + + let split_response = self.client.split(split_payload.split_payload)?; + + // If only promises assemble proofs needed for amount + let keep_proofs; + let send_proofs; + + if let Some(promises) = split_response.promises { + let proofs = construct_proofs( + promises, + split_payload.blinded_messages.rs, + split_payload.blinded_messages.secrets, + &self.mint_keys, + )?; + + let split = amount_to_send.split(); + + keep_proofs = proofs[0..split.len()].to_vec(); + send_proofs = proofs[split.len()..].to_vec(); + } else { + return Err(Error::Custom("Invalid split response".to_string())); + } + + // println!("Send Proofs: {:#?}", send_proofs); + // println!("Keep Proofs: {:#?}", keep_proofs); + + Ok(SendProofs { + change_proofs: keep_proofs, + send_proofs, + }) + } + + #[cfg(not(feature = "blocking"))] pub async fn melt( &self, invoice: Bolt11Invoice, @@ -308,11 +482,49 @@ impl Wallet { Ok(melted) } + #[cfg(feature = "blocking")] + pub fn melt( + &self, + invoice: Bolt11Invoice, + proofs: Proofs, + fee_reserve: Amount, + ) -> Result { + let blinded = BlindedMessages::blank(fee_reserve)?; + let melt_response = self + .client + .melt(proofs, invoice, Some(blinded.blinded_messages))?; + + let change_proofs = match melt_response.change { + Some(change) => Some(construct_proofs( + change, + blinded.rs, + blinded.secrets, + &self.mint_keys, + )?), + None => None, + }; + + let melted = Melted { + paid: true, + preimage: melt_response.preimage, + change: change_proofs, + }; + + Ok(melted) + } + + #[cfg(not(feature = "blocking"))] pub fn proofs_to_token(&self, proofs: Proofs, memo: Option) -> Result { Ok(Token::new(self.client.mint_url.clone(), proofs, memo).convert_to_string()?) } + + #[cfg(feature = "blocking")] + pub fn proofs_to_token(&self, proofs: Proofs, memo: Option) -> Result { + Ok(Token::new(self.client.client.mint_url.clone(), proofs, memo).convert_to_string()?) + } } +/* #[cfg(test)] mod tests { @@ -379,3 +591,4 @@ mod tests { } } } +*/ diff --git a/crates/cashu/Cargo.toml b/crates/cashu/Cargo.toml index 0e6d5b36..11b45bb0 100644 --- a/crates/cashu/Cargo.toml +++ b/crates/cashu/Cargo.toml @@ -30,4 +30,4 @@ url = { workspace = true } regex = "1.8.4" [dev-dependencies] -tokio = {version = "1.27.0", features = ["rt", "macros"] } +# tokio = {version = "1.27.0", features = ["rt", "macros"] }