diff --git a/README.md b/README.md index 724a2e36..77911e3b 100644 --- a/README.md +++ b/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 diff --git a/src/cashu_mint.rs b/src/cashu_mint.rs index 067963fb..264995c9 100644 --- a/src/cashu_mint.rs +++ b/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, - outputs: Vec, - ) -> Result { + pub async fn split(&self, split_request: SplitRequest) -> Result { 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::()?) + .json::()?; + + // TODO: need to handle response error + // specfically token already spent + + Ok(serde_json::from_value(res).unwrap()) } /// Spendable check [NUT-07] diff --git a/src/cashu_wallet.rs b/src/cashu_wallet.rs index 20b9ffd3..3becedfd 100644 --- a/src/cashu_wallet.rs +++ b/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) -> Result { 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 { Ok(self.mint.check_fees(invoice).await?.fee) } + + /// Receive + pub async fn receive(&self, encoded_token: &str) -> Result, 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, + ) -> Result { + 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, + }) + } } diff --git a/src/dhke.rs b/src/dhke.rs index 035363f9..56ab0246 100644 --- a/src/dhke.rs +++ b/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 { @@ -62,17 +64,32 @@ pub fn unblind_message( Ok(unblinded_key) } -/* /// Construct Proof pub fn construct_proof( promises: Vec, rs: Vec, - secrets: Vec, - keys: MintKeys, + secrets: Vec>, + keys: &MintKeys, ) -> Result, 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 { diff --git a/src/types.rs b/src/types.rs index b69efbb2..919c80b4 100644 --- a/src/types.rs +++ b/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, + pub fst: Vec, /// Promises to send - pub snd: Vec, + pub snd: Vec, } /// Check spendabale request [NUT-07] diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 1714ba85..36bb4991 100644 --- a/tests/integration_test.rs +++ b/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/"; @@ -68,6 +68,21 @@ async fn test_check_fees() { println!("{fee:?}"); } +#[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() {