diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..33922921 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,24 @@ +name: test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout Crate + uses: actions/checkout@v3 + - name: Set Toolchain + # https://github.com/dtolnay/rust-toolchain + uses: dtolnay/rust-toolchain@stable + - name: Run tests + run: | + rustup update + cargo test diff --git a/Cargo.toml b/Cargo.toml index 0bd1722d..2ed4396d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ description = "Cashu rust library" [dependencies] +base64 = "0.21.0" bitcoin = { version = "0.30.0", features=["serde"] } bitcoin_hashes = "0.12.0" hex = "0.4.3" @@ -18,6 +19,7 @@ minreq = { version = "2.7.0", features = ["json-using-serde", "https"] } rand = "0.8.5" secp256k1 = { version = "0.27.0", features = ["rand-std", "bitcoin-hashes-std"] } serde = { version = "1.0.160", features = ["derive"]} +serde_json = "1.0.96" thiserror = "1.0.40" url = "2.3.1" diff --git a/src/cashu_mint.rs b/src/cashu_mint.rs index 2b8a3a3b..067963fb 100644 --- a/src/cashu_mint.rs +++ b/src/cashu_mint.rs @@ -121,10 +121,12 @@ impl CashuMint { /// Spendable check [NUT-07] pub async fn check_spendable( &self, - proofs: Vec, + proofs: &Vec, ) -> Result { let url = self.url.join("check")?; - let request = CheckSpendableRequest { proofs }; + let request = CheckSpendableRequest { + proofs: proofs.to_owned(), + }; Ok(minreq::post(url) .with_json(&request)? diff --git a/src/cashu_wallet.rs b/src/cashu_wallet.rs new file mode 100644 index 00000000..20b9ffd3 --- /dev/null +++ b/src/cashu_wallet.rs @@ -0,0 +1,43 @@ +use bitcoin::Amount; + +use crate::{ + cashu_mint::CashuMint, + error::Error, + types::{MintKeys, Proof, ProofsStatus, RequestMintResponse}, +}; + +pub struct CashuWallet { + pub mint: CashuMint, + pub keys: MintKeys, +} + +impl CashuWallet { + pub fn new(mint: CashuMint, keys: MintKeys) -> Self { + Self { mint, keys } + } + + /// Check if a proof is spent + pub async fn check_proofs_spent(&self, proofs: Vec) -> Result { + let spendable = self.mint.check_spendable(&proofs).await?; + + 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 Mint + pub async fn request_mint(&self, amount: Amount) -> Result { + self.mint.request_mint(amount).await + } + + /// Check fee + pub async fn check_fee(&self, invoice: lightning_invoice::Invoice) -> Result { + Ok(self.mint.check_fees(invoice).await?.fee) + } +} diff --git a/src/error.rs b/src/error.rs index 9da20946..2006c5b5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,5 @@ +use std::string::FromUtf8Error; + #[derive(Debug, thiserror::Error)] pub enum Error { /// Min req error @@ -9,4 +11,16 @@ pub enum Error { /// Secp245k1 #[error("secp256k1 error: {0}")] Secpk256k1Error(#[from] secp256k1::Error), + /// Unsupported Token + #[error("Unsupported Token")] + UnsupportedToken, + /// Utf8 parse error + #[error("utf8error error: {0}")] + Utf8ParseError(#[from] FromUtf8Error), + /// Serde Json error + #[error("Serde Json error: {0}")] + SerdeJsonError(#[from] serde_json::Error), + /// Base64 error + #[error("Base64 error: {0}")] + Base64Error(#[from] base64::DecodeError), } diff --git a/src/lib.rs b/src/lib.rs index b555bae8..a846cf6f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ pub mod cashu_mint; +pub mod cashu_wallet; pub mod dhke; pub mod error; +pub mod serde_utils; pub mod types; pub mod utils; diff --git a/src/serde_utils.rs b/src/serde_utils.rs new file mode 100644 index 00000000..ec3a62bb --- /dev/null +++ b/src/serde_utils.rs @@ -0,0 +1,21 @@ +//! Utilities for serde + +pub mod serde_url { + use serde::Deserialize; + use url::Url; + + pub fn serialize(url: &Url, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(url.as_ref()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let url_string = String::deserialize(deserializer)?; + Url::parse(&url_string).map_err(serde::de::Error::custom) + } +} diff --git a/src/types.rs b/src/types.rs index 2b2815c5..b69efbb2 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,14 +1,16 @@ //! Types for `cashu-rs` -use std::collections::HashMap; +use std::{collections::HashMap, str::FromStr}; +use base64::{engine::general_purpose, Engine as _}; use bitcoin::Amount; use lightning_invoice::Invoice; use rand::Rng; use secp256k1::{PublicKey, SecretKey}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use url::Url; -use crate::{dhke::blind_message, error::Error, utils::split_amount}; +use crate::{dhke::blind_message, error::Error, serde_utils::serde_url, utils::split_amount}; /// Blinded Message [NUT-00] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -53,6 +55,28 @@ impl BlindedMessages { Ok(blinded_messages) } + + pub fn blank() -> Result { + let mut blinded_messages = BlindedMessages::default(); + + let mut rng = rand::thread_rng(); + for _i in 0..4 { + let bytes: [u8; 32] = rng.gen(); + let (blinded, r) = blind_message(&bytes, None)?; + + let blinded_message = BlindedMessage { + amount: Amount::ZERO, + b: blinded, + }; + + blinded_messages.secrets.push(bytes.to_vec()); + blinded_messages.blinded_messages.push(blinded_message); + blinded_messages.rs.push(r); + blinded_messages.amounts.push(Amount::ZERO); + } + + Ok(blinded_messages) + } } /// Promise (BlindedSignature) [NIP-00] @@ -182,11 +206,17 @@ pub struct CheckSpendableResponse { pub spendable: Vec, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProofsStatus { + pub spendable: Vec, + pub spent: Vec, +} + /// Mint Version #[derive(Debug, Clone, PartialEq, Eq)] pub struct MintVersion { - name: String, - version: String, + pub name: String, + pub version: String, } impl Serialize for MintVersion { @@ -236,3 +266,57 @@ pub struct MintInfo { /// message of the day that the wallet must display to the user pub motd: String, } + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Token { + #[serde(with = "serde_url")] + pub mint: Url, + pub proofs: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TokenData { + pub token: Vec, + pub memo: Option, +} + +impl FromStr for TokenData { + type Err = Error; + + fn from_str(s: &str) -> Result { + if !s.starts_with("cashuA") { + return Err(Error::UnsupportedToken); + } + + let s = s.replace("cashuA", ""); + let decoded = general_purpose::STANDARD.decode(s)?; + let decoded_str = String::from_utf8(decoded)?; + println!("decode: {:?}", decoded_str); + let token: TokenData = serde_json::from_str(&decoded_str)?; + Ok(token) + } +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_proof_seralize() { + let proof = "[{\"id\":\"DSAl9nvvyfva\",\"amount\":2,\"secret\":\"EhpennC9qB3iFlW8FZ_pZw\",\"C\":\"02c020067db727d586bc3183aecf97fcb800c3f4cc4759f69c626c9db5d8f5b5d4\"},{\"id\":\"DSAl9nvvyfva\",\"amount\":8,\"secret\":\"TmS6Cv0YT5PU_5ATVKnukw\",\"C\":\"02ac910bef28cbe5d7325415d5c263026f15f9b967a079ca9779ab6e5c2db133a7\"}]"; + let proof: Vec = serde_json::from_str(proof).unwrap(); + + assert_eq!(proof[0].clone().id.unwrap(), "DSAl9nvvyfva"); + } + + #[test] + fn test_token_from_str() { + let token = "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJpZCI6IkRTQWw5bnZ2eWZ2YSIsImFtb3VudCI6Miwic2VjcmV0IjoiRWhwZW5uQzlxQjNpRmxXOEZaX3BadyIsIkMiOiIwMmMwMjAwNjdkYjcyN2Q1ODZiYzMxODNhZWNmOTdmY2I4MDBjM2Y0Y2M0NzU5ZjY5YzYyNmM5ZGI1ZDhmNWI1ZDQifSx7ImlkIjoiRFNBbDludnZ5ZnZhIiwiYW1vdW50Ijo4LCJzZWNyZXQiOiJUbVM2Q3YwWVQ1UFVfNUFUVktudWt3IiwiQyI6IjAyYWM5MTBiZWYyOGNiZTVkNzMyNTQxNWQ1YzI2MzAyNmYxNWY5Yjk2N2EwNzljYTk3NzlhYjZlNWMyZGIxMzNhNyJ9XX1dLCJtZW1vIjoiVGhhbmt5b3UuIn0="; + let token = TokenData::from_str(token).unwrap(); + + assert_eq!( + token.token[0].mint, + Url::from_str("https://8333.space:3338").unwrap() + ); + assert_eq!(token.token[0].proofs[0].clone().id.unwrap(), "DSAl9nvvyfva"); + } +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 73ca9d47..1714ba85 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -38,6 +38,7 @@ async fn test_request_mint() { assert!(mint.pr.check_signature().is_ok()) } +#[ignore] #[tokio::test] async fn test_mint() { let url = Url::from_str(MINTURL).unwrap();