Quellcode durchsuchen

sdk: add blocking client and wallet as feature

thesimplekid vor 1 Jahr
Ursprung
Commit
1aedf3f2bc

+ 4 - 0
.helix/languages.toml

@@ -0,0 +1,4 @@
+[[language]]
+name = "rust"
+config = { cargo = { features = [ "blocking", "wallet" ] } }
+

+ 1 - 0
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"

+ 19 - 4
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"]}
+
+

+ 80 - 0
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<Self, Error> {
+        Ok(Self {
+            client: super::Client::new(mint_url)?,
+        })
+    }
+
+    pub fn get_keys(&self) -> Result<Keys, Error> {
+        RUNTIME.block_on(async { self.client.get_keys().await })
+    }
+
+    pub fn get_keysets(&self) -> Result<nut02::Response, Error> {
+        RUNTIME.block_on(async { self.client.get_keysets().await })
+    }
+
+    pub fn request_mint(&self, amount: Amount) -> Result<RequestMintResponse, Error> {
+        RUNTIME.block_on(async { self.client.request_mint(amount).await })
+    }
+
+    pub fn mint(
+        &self,
+        blinded_mssages: BlindedMessages,
+        hash: &str,
+    ) -> Result<PostMintResponse, Error> {
+        RUNTIME.block_on(async { self.client.mint(blinded_mssages, hash).await })
+    }
+
+    pub fn check_fees(&self, invoice: Bolt11Invoice) -> Result<CheckFeesResponse, Error> {
+        RUNTIME.block_on(async { self.client.check_fees(invoice).await })
+    }
+
+    pub fn melt(
+        &self,
+        proofs: Vec<Proof>,
+        invoice: Bolt11Invoice,
+        outputs: Option<Vec<BlindedMessage>>,
+    ) -> Result<MeltResponse, Error> {
+        RUNTIME.block_on(async { self.client.melt(proofs, invoice, outputs).await })
+    }
+
+    pub fn split(&self, split_request: SplitRequest) -> Result<SplitResponse, Error> {
+        RUNTIME.block_on(async { self.client.split(split_request).await })
+    }
+
+    pub fn check_spendable(
+        &self,
+        proofs: &Vec<nut00::mint::Proof>,
+    ) -> Result<CheckSpendableResponse, Error> {
+        RUNTIME.block_on(async { self.client.check_spendable(proofs).await })
+    }
+
+    pub fn get_info(&self) -> Result<MintInfo, Error> {
+        RUNTIME.block_on(async { self.client.get_info().await })
+    }
+}

+ 3 - 0
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)]

+ 22 - 1
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<Runtime> = Lazy::new(|| Runtime::new().expect("Can't start Tokio runtime"));
+
+#[cfg(feature = "blocking")]
+pub fn block_on<F: Future>(future: F) -> F::Output {
+    RUNTIME.block_on(future)
+}

+ 213 - 0
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<ProofsStatus, Error> {
         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<ProofsStatus, Error> {
+        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<RequestMintResponse, Error> {
         Ok(self.client.request_mint(amount).await?)
     }
 
+    /// Request Token Mint
+    #[cfg(feature = "blocking")]
+    pub fn request_mint(&self, amount: Amount) -> Result<RequestMintResponse, Error> {
+        Ok(self.client.request_mint(amount)?)
+    }
+
     /// Mint Token
+    #[cfg(not(feature = "blocking"))]
     pub async fn mint_token(&self, amount: Amount, hash: &str) -> Result<Token, Error> {
         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<Token, Error> {
+        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<Proofs, Error> {
         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<Proofs, Error> {
+        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<Amount, Error> {
         Ok(self.client.check_fees(invoice).await?.fee)
     }
 
+    /// Check fee
+    #[cfg(feature = "blocking")]
+    pub fn check_fee(&self, invoice: Bolt11Invoice) -> Result<Amount, Error> {
+        Ok(self.client.check_fees(invoice)?.fee)
+    }
+
     /// Receive
+    #[cfg(not(feature = "blocking"))]
     pub async fn receive(&self, encoded_token: &str) -> Result<Proofs, Error> {
         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<Proofs, Error> {
+        let token_data = Token::from_str(encoded_token)?;
+
+        let mut proofs: Vec<Proofs> = 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<SplitPayload, Error> {
         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<SendProofs, Error> {
         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<SendProofs, Error> {
+        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<Melted, Error> {
+        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<String>) -> Result<String, Error> {
         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<String>) -> Result<String, Error> {
+        Ok(Token::new(self.client.client.mint_url.clone(), proofs, memo).convert_to_string()?)
+    }
 }
 
+/*
 #[cfg(test)]
 mod tests {
 
@@ -379,3 +591,4 @@ mod tests {
         }
     }
 }
+*/

+ 1 - 1
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"] }