thesimplekid 1 år sedan
förälder
incheckning
8aa2b42d25
6 ändrade filer med 167 tillägg och 46 borttagningar
  1. 17 18
      README.md
  2. 11 16
      src/cashu_mint.rs
  3. 89 1
      src/cashu_wallet.rs
  4. 25 8
      src/dhke.rs
  5. 9 2
      src/types.rs
  6. 16 1
      tests/integration_test.rs

+ 17 - 18
README.md

@@ -1,28 +1,27 @@
-
-
-⚠️ **Don't be reckless:** This project is in early development, it does however work with real sats! Always use amounts you don't mind loosing.
+> **Warning**
+> This project is in early development, it does however work with real sats! Always use amounts you don't mind loosing.
 
 Cashu RS is a rust library for [Cashu](https://github.com/cashubtc) wallets written in Rust.
 
-Implemented [NUTs](https://github.com/cashubtc/nuts/):
+## Implemented [NUTs](https://github.com/cashubtc/nuts/):
 
-- [x] [NUT-00](https://github.com/cashubtc/nuts/blob/main/00.md)
-- [x] [NUT-01](https://github.com/cashubtc/nuts/blob/main/01.md)
-- [x] [NUT-02](https://github.com/cashubtc/nuts/blob/main/02.md)
-- [x] [NUT-03](https://github.com/cashubtc/nuts/blob/main/03.md)
-- [x] [NUT-04](https://github.com/cashubtc/nuts/blob/main/04.md)
-- [x] [NUT-05](https://github.com/cashubtc/nuts/blob/main/05.md)
-- [x] [NUT-06](https://github.com/cashubtc/nuts/blob/main/06.md)
-- [x] [NUT-07](https://github.com/cashubtc/nuts/blob/main/07.md)
-- [x] [NUT-08](https://github.com/cashubtc/nuts/blob/main/08.md)
-- [x] [NUT-09](https://github.com/cashubtc/nuts/blob/main/09.md)
+-  [NUT-00](https://github.com/cashubtc/nuts/blob/main/00.md)
+-  [NUT-01](https://github.com/cashubtc/nuts/blob/main/01.md)
+-  [NUT-02](https://github.com/cashubtc/nuts/blob/main/02.md)
+-  [NUT-03](https://github.com/cashubtc/nuts/blob/main/03.md)
+-  [NUT-04](https://github.com/cashubtc/nuts/blob/main/04.md)
+-  [NUT-05](https://github.com/cashubtc/nuts/blob/main/05.md)
+-  [NUT-06](https://github.com/cashubtc/nuts/blob/main/06.md)
+-  [NUT-07](https://github.com/cashubtc/nuts/blob/main/07.md)
+-  [NUT-08](https://github.com/cashubtc/nuts/blob/main/08.md)
+-  [NUT-09](https://github.com/cashubtc/nuts/blob/main/09.md)
 
 
-Supported token formats:
+## Supported token formats:
 
-- [ ] v1 read
-- [ ] v2 read (deprecated)
-- [ ] v3 read/write
+- ❌ v1 read (deprecated)
+-  v2 read (deprecated)
+- ✅ [v3](https://github.com/cashubtc/nuts/blob/main/00.md#023---v3-tokens) read/write
 
 
 ## License

+ 11 - 16
src/cashu_mint.rs

@@ -1,5 +1,6 @@
 use bitcoin::Amount;
 use lightning_invoice::Invoice;
+use serde_json::Value;
 use url::Url;
 
 use crate::{
@@ -13,7 +14,7 @@ use crate::{
 };
 
 pub struct CashuMint {
-    url: Url,
+    pub url: Url,
 }
 
 impl CashuMint {
@@ -98,24 +99,18 @@ impl CashuMint {
     }
 
     /// Split Token [NUT-06]
-    pub async fn split(
-        &self,
-        amount: Amount,
-        proofs: Vec<Proof>,
-        outputs: Vec<BlindedMessage>,
-    ) -> Result<SplitResponse, Error> {
+    pub async fn split(&self, split_request: SplitRequest) -> Result<SplitResponse, Error> {
         let url = self.url.join("split")?;
 
-        let request = SplitRequest {
-            amount,
-            proofs,
-            outputs,
-        };
-
-        Ok(minreq::post(url)
-            .with_json(&request)?
+        let res = minreq::post(url)
+            .with_json(&split_request)?
             .send()?
-            .json::<SplitResponse>()?)
+            .json::<Value>()?;
+
+        // TODO: need to handle response error
+        // specfically token already spent
+
+        Ok(serde_json::from_value(res).unwrap())
     }
 
     /// Spendable check [NUT-07]

+ 89 - 1
src/cashu_wallet.rs

@@ -1,9 +1,15 @@
+use std::str::FromStr;
+
 use bitcoin::Amount;
 
 use crate::{
     cashu_mint::CashuMint,
+    dhke::construct_proof,
     error::Error,
-    types::{MintKeys, Proof, ProofsStatus, RequestMintResponse},
+    types::{
+        BlindedMessages, MintKeys, Proof, ProofsStatus, RequestMintResponse, SplitPayload,
+        SplitRequest, TokenData,
+    },
 };
 
 pub struct CashuWallet {
@@ -20,6 +26,7 @@ impl CashuWallet {
     pub async fn check_proofs_spent(&self, proofs: Vec<Proof>) -> Result<ProofsStatus, Error> {
         let spendable = self.mint.check_spendable(&proofs).await?;
 
+        // Seperate proofs in spent and unspent based on mint response
         let (spendable, spent): (Vec<_>, Vec<_>) = proofs
             .iter()
             .zip(spendable.spendable.iter())
@@ -40,4 +47,85 @@ impl CashuWallet {
     pub async fn check_fee(&self, invoice: lightning_invoice::Invoice) -> Result<Amount, Error> {
         Ok(self.mint.check_fees(invoice).await?.fee)
     }
+
+    /// Receive
+    pub async fn receive(&self, encoded_token: &str) -> Result<Vec<Proof>, Error> {
+        let token_data = TokenData::from_str(encoded_token)?;
+
+        let mut proofs = vec![];
+        for token in token_data.token {
+            if token.proofs.is_empty() {
+                continue;
+            }
+
+            let keys = if token.mint.eq(&self.mint.url) {
+                self.keys.clone()
+            } else {
+                // TODO:
+                println!("No match");
+                self.keys.clone()
+                // CashuMint::new(token.mint).get_keys().await.unwrap()
+            };
+
+            // Sum amount of all proofs
+            let amount = token
+                .proofs
+                .iter()
+                .fold(Amount::ZERO, |acc, p| acc + p.amount);
+
+            let split_payload = self
+                .create_split(Amount::ZERO, amount, token.proofs)
+                .await?;
+
+            let split_response = self.mint.split(split_payload.split_payload).await?;
+
+            // Proof to keep
+            let keep_proofs = construct_proof(
+                split_response.fst,
+                split_payload.keep_blinded_messages.rs,
+                split_payload.keep_blinded_messages.secrets,
+                &keys,
+            )?;
+
+            // Proofs to send
+            let send_proofs = construct_proof(
+                split_response.snd,
+                split_payload.send_blinded_messages.rs,
+                split_payload.send_blinded_messages.secrets,
+                &keys,
+            )?;
+
+            proofs.push(keep_proofs);
+            proofs.push(send_proofs);
+        }
+
+        Ok(proofs.iter().flatten().cloned().collect())
+    }
+
+    pub async fn create_split(
+        &self,
+        keep_amount: Amount,
+        send_amount: Amount,
+        proofs: Vec<Proof>,
+    ) -> Result<SplitPayload, Error> {
+        let keep_blinded_messages = BlindedMessages::random(keep_amount)?;
+        let send_blinded_messages = BlindedMessages::random(send_amount)?;
+
+        let outputs = {
+            let mut outputs = keep_blinded_messages.blinded_messages.clone();
+            outputs.extend(send_blinded_messages.blinded_messages.clone());
+            outputs
+        };
+        let split_payload = SplitRequest {
+            amount: send_amount,
+            proofs,
+            outputs,
+        };
+
+        Ok(SplitPayload {
+            keep_blinded_messages,
+            send_blinded_messages,
+            split_payload,
+        })
+    }
 }

+ 25 - 8
src/dhke.rs

@@ -1,14 +1,16 @@
 //! Diffie-Hellmann key exchange
 
+use std::str::FromStr;
+
 use bitcoin_hashes::sha256;
 use bitcoin_hashes::Hash;
 use secp256k1::rand::rngs::OsRng;
 use secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey};
 
 use crate::error::Error;
-// use crate::types::MintKeys;
-// use crate::types::Promise;
-// use crate::types::Proof;
+use crate::types::MintKeys;
+use crate::types::Promise;
+use crate::types::Proof;
 
 /// Hash to Curve
 pub fn hash_to_curve(secret_message: &[u8]) -> Result<PublicKey, Error> {
@@ -62,17 +64,32 @@ pub fn unblind_message(
     Ok(unblinded_key)
 }
 
-/*
 /// Construct Proof
 pub fn construct_proof(
     promises: Vec<Promise>,
     rs: Vec<SecretKey>,
-    secrets: Vec<String>,
-    keys: MintKeys,
+    secrets: Vec<Vec<u8>>,
+    keys: &MintKeys,
 ) -> Result<Vec<Proof>, Error> {
-    todo!()
+    let mut proofs = vec![];
+    for (i, promise) in promises.into_iter().enumerate() {
+        let blinded_c = PublicKey::from_str(&promise.c)?;
+        let a: PublicKey = PublicKey::from_str(keys.0.get(&promise.amount.to_sat()).unwrap())?;
+        let unblinded_signature = unblind_message(blinded_c, rs[i], a)?;
+
+        let proof = Proof {
+            id: Some(promise.id),
+            amount: promise.amount,
+            secret: hex::encode(&secrets[i]),
+            c: unblinded_signature.to_string(),
+            script: None,
+        };
+
+        proofs.push(proof);
+    }
+
+    Ok(proofs)
 }
-*/
 
 #[cfg(test)]
 mod tests {

+ 9 - 2
src/types.rs

@@ -79,6 +79,13 @@ impl BlindedMessages {
     }
 }
 
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct SplitPayload {
+    pub keep_blinded_messages: BlindedMessages,
+    pub send_blinded_messages: BlindedMessages,
+    pub split_payload: SplitRequest,
+}
+
 /// Promise (BlindedSignature) [NIP-00]
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct Promise {
@@ -187,9 +194,9 @@ pub struct SplitRequest {
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct SplitResponse {
     /// Promises to keep
-    pub fst: Vec<BlindedMessage>,
+    pub fst: Vec<Promise>,
     /// Promises to send
-    pub snd: Vec<BlindedMessage>,
+    pub snd: Vec<Promise>,
 }
 
 /// Check spendabale request [NUT-07]

+ 16 - 1
tests/integration_test.rs

@@ -6,7 +6,7 @@ use bitcoin::Amount;
 use lightning_invoice::Invoice;
 use url::Url;
 
-use cashu_rs::{cashu_mint::CashuMint, types::BlindedMessages};
+use cashu_rs::{cashu_mint::CashuMint, cashu_wallet::CashuWallet, types::BlindedMessages};
 
 const MINTURL: &str = "https://legend.lnbits.com/cashu/api/v1/SKvHRus9dmjWHhstHrsazW/";
 
@@ -70,6 +70,21 @@ async fn test_check_fees() {
 
 #[ignore]
 #[tokio::test]
+async fn test_receive() {
+    let url = Url::from_str(MINTURL).unwrap();
+    let mint = CashuMint::new(url);
+    let mint_keys = mint.get_keys().await.unwrap();
+
+    let wallet = CashuWallet::new(mint, mint_keys);
+    // FIXME: Have to manully paste an unspent token
+    let token = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6Im9DV2NkWXJyeVRrUiIsImFtb3VudCI6MiwiQyI6IjAzNmY1NTU0ZDMyZDg3MGFjMzZjMDIwOGNiMDlkZmJmZjNhN2RkZTUyNzMwOTNjYzk3ZjE2NDBkNjYyZTgyMmMyMCIsInNlY3JldCI6ImtuRlhvelpjUG5YK1l4dytIcmV3VVlXRHU2ZFVFbkY0KzRUTkRIN010V289In1dLCJtaW50IjoiaHR0cHM6Ly9sZWdlbmQubG5iaXRzLmNvbS9jYXNodS9hcGkvdjEvU0t2SFJ1czlkbWpXSGhzdEhyc2F6VyJ9XX0=";
+
+    let prom = wallet.receive(token).await.unwrap();
+    println!("{:?}", prom);
+}
+
+#[ignore]
+#[tokio::test]
 async fn test_get_mint_info() {
     let url = Url::from_str(MINTURL).unwrap();
     let mint = CashuMint::new(url);