Browse Source

token from str

thesimplekid 1 year ago
parent
commit
88eb0b3322
9 changed files with 199 additions and 6 deletions
  1. 24 0
      .github/workflows/test.yml
  2. 2 0
      Cargo.toml
  3. 4 2
      src/cashu_mint.rs
  4. 43 0
      src/cashu_wallet.rs
  5. 14 0
      src/error.rs
  6. 2 0
      src/lib.rs
  7. 21 0
      src/serde_utils.rs
  8. 88 4
      src/types.rs
  9. 1 0
      tests/integration_test.rs

+ 24 - 0
.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

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

+ 4 - 2
src/cashu_mint.rs

@@ -121,10 +121,12 @@ impl CashuMint {
     /// Spendable check [NUT-07]
     pub async fn check_spendable(
         &self,
-        proofs: Vec<Proof>,
+        proofs: &Vec<Proof>,
     ) -> Result<CheckSpendableResponse, Error> {
         let url = self.url.join("check")?;
-        let request = CheckSpendableRequest { proofs };
+        let request = CheckSpendableRequest {
+            proofs: proofs.to_owned(),
+        };
 
         Ok(minreq::post(url)
             .with_json(&request)?

+ 43 - 0
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<Proof>) -> Result<ProofsStatus, Error> {
+        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<RequestMintResponse, Error> {
+        self.mint.request_mint(amount).await
+    }
+
+    /// Check fee
+    pub async fn check_fee(&self, invoice: lightning_invoice::Invoice) -> Result<Amount, Error> {
+        Ok(self.mint.check_fees(invoice).await?.fee)
+    }
+}

+ 14 - 0
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),
 }

+ 2 - 0
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;

+ 21 - 0
src/serde_utils.rs

@@ -0,0 +1,21 @@
+//! Utilities for serde
+
+pub mod serde_url {
+    use serde::Deserialize;
+    use url::Url;
+
+    pub fn serialize<S>(url: &Url, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        serializer.serialize_str(url.as_ref())
+    }
+
+    pub fn deserialize<'de, D>(deserializer: D) -> Result<Url, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let url_string = String::deserialize(deserializer)?;
+        Url::parse(&url_string).map_err(serde::de::Error::custom)
+    }
+}

+ 88 - 4
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<Self, Error> {
+        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<bool>,
 }
 
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct ProofsStatus {
+    pub spendable: Vec<Proof>,
+    pub spent: Vec<Proof>,
+}
+
 /// 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<Proof>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct TokenData {
+    pub token: Vec<Token>,
+    pub memo: Option<String>,
+}
+
+impl FromStr for TokenData {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        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<Proof> = 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");
+    }
+}

+ 1 - 0
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();