Browse Source

init commit

thesimplekid 1 year ago
commit
25c3620ecc
9 changed files with 409 additions and 0 deletions
  1. 2 0
      .gitignore
  2. 14 0
      Cargo.toml
  3. 11 0
      integration_test/Cargo.toml
  4. 36 0
      integration_test/src/main.rs
  5. 2 0
      justfile
  6. 138 0
      src/cashu_mint.rs
  7. 9 0
      src/error.rs
  8. 3 0
      src/lib.rs
  9. 194 0
      src/types.rs

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+/target
+/Cargo.lock

+ 14 - 0
Cargo.toml

@@ -0,0 +1,14 @@
+[package]
+name = "cashu-rs"
+version = "0.1.0"
+edition = "2021"
+
+[workspace]
+members = ["integration_test"]
+
+
+[dependencies]
+minreq = { version = "2.7.0", features = ["json-using-serde", "https"] }
+serde = { version = "1.0.160", features = ["derive"]}
+thiserror = "1.0.40"
+url = "2.3.1"

+ 11 - 0
integration_test/Cargo.toml

@@ -0,0 +1,11 @@
+[package]
+name = "integration_test"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+cashu-rs = { path = ".." }
+url = "2.3.1"
+tokio = { version = "1.27.0", features = ["full"] }

+ 36 - 0
integration_test/src/main.rs

@@ -0,0 +1,36 @@
+// #![deny(unused)]
+
+use std::str::FromStr;
+
+use cashu_rs::cashu_mint::CashuMint;
+use url::Url;
+
+#[tokio::main]
+async fn main() {
+    let url = Url::from_str("https://legend.lnbits.com/cashu/api/v1/SKvHRus9dmjWHhstHrsazW/keys")
+        .unwrap();
+    let mint = CashuMint::new(url);
+
+    // test_get_mint_info(&mint).await;
+
+    test_get_mint_keys(&mint).await;
+    test_get_mint_keysets(&mint).await;
+}
+
+async fn test_get_mint_info(mint: &CashuMint) {
+    let mint_info = mint.get_info().await.unwrap();
+
+    println!("{:?}", mint_info);
+}
+
+async fn test_get_mint_keys(mint: &CashuMint) {
+    let mint_keys = mint.get_keys().await.unwrap();
+
+    println!("{:?}", mint_keys);
+}
+
+async fn test_get_mint_keysets(mint: &CashuMint) {
+    let mint_keysets = mint.get_keysets().await.unwrap();
+
+    assert!(!mint_keysets.keysets.is_empty())
+}

+ 2 - 0
justfile

@@ -0,0 +1,2 @@
+test:
+    cargo r -p integration_test

+ 138 - 0
src/cashu_mint.rs

@@ -0,0 +1,138 @@
+use url::Url;
+
+use crate::{
+    error::Error,
+    types::{
+        BlindedMessage, CheckFeesRequest, CheckFeesResponse, CheckSpendableRequest,
+        CheckSpendableResponse, MeltRequest, MeltResposne, MintInfo, MintKeySets, MintKeys,
+        MintRequest, PostMintResponse, Proof, RequestMintResponse, SplitRequest, SplitResponse,
+    },
+};
+
+pub struct CashuMint {
+    url: Url,
+}
+
+impl CashuMint {
+    pub fn new(url: Url) -> Self {
+        Self { url }
+    }
+
+    /// Get Mint Keys [NUT-01]
+    pub async fn get_keys(&self) -> Result<MintKeys, Error> {
+        let url = self.url.join("keys")?;
+        Ok(minreq::get(url).send()?.json::<MintKeys>()?)
+    }
+
+    /// Get Keysets [NUT-02]
+    pub async fn get_keysets(&self) -> Result<MintKeySets, Error> {
+        let url = self.url.join("keysets")?;
+        Ok(minreq::get(url).send()?.json::<MintKeySets>()?)
+    }
+
+    /// Request Mint [NUT-03]
+    pub async fn request_mint(&self, amount: u64) -> Result<RequestMintResponse, Error> {
+        let mut url = self.url.join("mint")?;
+        url.query_pairs_mut()
+            .append_pair("amount", &amount.to_string());
+
+        Ok(minreq::get(url).send()?.json::<RequestMintResponse>()?)
+    }
+
+    /// Mint Tokens [NUT-04]
+    pub async fn mint(
+        &self,
+        blinded_messages: Vec<BlindedMessage>,
+        payment_hash: &str,
+    ) -> Result<PostMintResponse, Error> {
+        let mut url = self.url.join("mint")?;
+        url.query_pairs_mut()
+            .append_pair("payment_hash", payment_hash);
+
+        let request = MintRequest {
+            outputs: blinded_messages,
+        };
+
+        Ok(minreq::post(url)
+            .with_json(&request)?
+            .send()?
+            .json::<PostMintResponse>()?)
+    }
+
+    /// Check Max expected fee [NUT-05]
+    pub async fn check_fees(&self, invoice: &str) -> Result<CheckFeesResponse, Error> {
+        let url = self.url.join("checkfees")?;
+
+        let request = CheckFeesRequest {
+            pr: invoice.to_string(),
+        };
+
+        Ok(minreq::post(url)
+            .with_json(&request)?
+            .send()?
+            .json::<CheckFeesResponse>()?)
+    }
+
+    /// Melt [NUT-05]
+    /// [Nut-08] Lightning fee return if outputs defined
+    pub async fn melt(
+        &self,
+        proofs: Vec<Proof>,
+        invoice: &str,
+        outputs: Option<Vec<BlindedMessage>>,
+    ) -> Result<MeltResposne, Error> {
+        let url = self.url.join("melt")?;
+
+        let request = MeltRequest {
+            proofs,
+            pr: invoice.to_string(),
+            outputs,
+        };
+
+        Ok(minreq::post(url)
+            .with_json(&request)?
+            .send()?
+            .json::<MeltResposne>()?)
+    }
+
+    /// Split Token [NUT-06]
+    pub async fn split(
+        &self,
+        amount: u64,
+        proofs: Vec<Proof>,
+        outputs: Vec<BlindedMessage>,
+    ) -> Result<SplitResponse, Error> {
+        let url = self.url.join("split")?;
+
+        let request = SplitRequest {
+            amount,
+            proofs,
+            outputs,
+        };
+
+        Ok(minreq::post(url)
+            .with_json(&request)?
+            .send()?
+            .json::<SplitResponse>()?)
+    }
+
+    /// Spendable check [NUT-07]
+    pub async fn check_spendable(
+        &self,
+        proofs: Vec<Proof>,
+    ) -> Result<CheckSpendableResponse, Error> {
+        let url = self.url.join("check")?;
+        let request = CheckSpendableRequest { proofs };
+
+        Ok(minreq::post(url)
+            .with_json(&request)?
+            .send()?
+            .json::<CheckSpendableResponse>()?)
+    }
+
+    /// Get Mint Info [NUT-09]
+    pub async fn get_info(&self) -> Result<MintInfo, Error> {
+        let url = self.url.join("info")?;
+        Ok(minreq::get(url).send()?.json::<MintInfo>()?)
+    }
+}

+ 9 - 0
src/error.rs

@@ -0,0 +1,9 @@
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+    ///  Min req error
+    #[error("minreq error: {0}")]
+    MinReqError(#[from] minreq::Error),
+    /// Parse Url Error
+    #[error("minreq error: {0}")]
+    UrlParseError(#[from] url::ParseError),
+}

+ 3 - 0
src/lib.rs

@@ -0,0 +1,3 @@
+pub mod cashu_mint;
+pub mod error;
+pub mod types;

+ 194 - 0
src/types.rs

@@ -0,0 +1,194 @@
+//! Types for `cashu-rs`
+
+use std::collections::HashMap;
+
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+
+/// Blinded Message [NUT-00]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct BlindedMessage {
+    /// Amount in satoshi
+    pub amount: u64,
+    /// encrypted secret message (B_)
+    #[serde(rename = "B_")]
+    pub b: String,
+}
+
+/// Promise (BlindedMessage) [NIP-00]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct Promise {
+    pub id: String,
+    /// Amount in satoshi
+    pub amount: u64,
+    /// blinded signature (C_) on the secret message `B_` of [BlindedMessage]
+    #[serde(rename = "C_")]
+    pub c: String,
+}
+
+/// Proofs [NUT-00]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct Proof {
+    /// Amount in satoshi
+    pub amount: u64,
+    /// Secret message
+    pub secret: String,
+    /// Unblinded signature
+    #[serde(rename = "C")]
+    pub c: String,
+    /// `Keyset id`
+    pub id: Option<String>,
+    /// P2SHScript that specifies the spending condition for this Proof
+    pub script: Option<String>,
+}
+
+/// Mint Keys [NIP-01]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct MintKeys(pub HashMap<u64, String>);
+
+/// Mint Keysets [NIP-02]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct MintKeySets {
+    /// set of public keys that the mint generates
+    pub keysets: Vec<String>,
+}
+
+/// Mint request response [NUT-03]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct RequestMintResponse {
+    /// Bolt11 payment request
+    pub pr: String,
+    /// Hash of Invoice
+    pub hash: String,
+}
+
+/// Post Mint Request [NIP-04]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct MintRequest {
+    pub outputs: Vec<BlindedMessage>,
+}
+
+/// Post Mint Response [NUT-05]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct PostMintResponse {
+    pub promises: Vec<Promise>,
+}
+
+/// Check Fees Response [NUT-05]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct CheckFeesResponse {
+    /// Expected Mac Fee in satoshis
+    pub fee: u64,
+}
+
+/// Check Fees request [NUT-05]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct CheckFeesRequest {
+    /// Lighting Invoice
+    pub pr: String,
+}
+
+/// Melt Request [NUT-05]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct MeltRequest {
+    pub proofs: Vec<Proof>,
+    /// bollt11
+    pub pr: String,
+    /// Blinded Message that can be used to return change [NUT-08]
+    /// Amount feild of blindedMessages `SHOULD` be set to zero
+    pub outputs: Option<Vec<BlindedMessage>>,
+}
+
+/// Melt Response [NUT-05]
+/// Lightning fee return [NUT-08] if change is defined
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct MeltResposne {
+    pub paid: bool,
+    pub preimage: String,
+    pub change: Option<Promise>,
+}
+
+/// Split Request [NUT-06]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct SplitRequest {
+    pub amount: u64,
+    pub proofs: Vec<Proof>,
+    pub outputs: Vec<BlindedMessage>,
+}
+
+/// Split Response [NUT-06]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct SplitResponse {
+    /// Promises to keep
+    pub fst: Vec<BlindedMessage>,
+    /// Promises to send
+    pub snd: Vec<BlindedMessage>,
+}
+
+/// Check spendabale request [NUT-07]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct CheckSpendableRequest {
+    pub proofs: Vec<Proof>,
+}
+
+/// Check Spendable Response [NUT-07]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct CheckSpendableResponse {
+    /// booleans indicating whether the provided Proof is still spendable.
+    /// In same order as provided proofs
+    pub spendable: Vec<bool>,
+}
+
+/// Mint Version
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct MintVersion {
+    name: String,
+    version: String,
+}
+
+impl Serialize for MintVersion {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let combined = format!("{}/{}", self.name, self.version);
+        serializer.serialize_str(&combined)
+    }
+}
+
+impl<'de> Deserialize<'de> for MintVersion {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let combined = String::deserialize(deserializer)?;
+        let parts: Vec<&str> = combined.split(" / ").collect();
+        if parts.len() != 2 {
+            return Err(serde::de::Error::custom("Invalid input string"));
+        }
+        Ok(MintVersion {
+            name: parts[0].to_string(),
+            version: parts[1].to_string(),
+        })
+    }
+}
+
+/// Mint Info [NIP-09]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct MintInfo {
+    /// name of the mint and should be recognizable
+    pub name: String,
+    /// hex pubkey of the mint
+    pub pubkey: String,
+    /// implementation name and the version running
+    pub version: MintVersion,
+    /// short description of the mint
+    pub description: String,
+    /// long description
+    pub description_long: String,
+    /// contact methods to reach the mint operator
+    pub contact: HashMap<String, String>,
+    /// shows which NUTs the mint supports
+    pub nuts: Vec<String>,
+    /// message of the day that the wallet must display to the user
+    pub motd: String,
+}