commit 25c3620ecc92cca12e89d5f8d61f44af37fbaee3 Author: thesimplekid Date: Sun Apr 23 00:32:40 2023 -0400 init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4fffb2f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..8e0cff6d --- /dev/null +++ b/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" diff --git a/integration_test/Cargo.toml b/integration_test/Cargo.toml new file mode 100644 index 00000000..cfadb6fe --- /dev/null +++ b/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"] } \ No newline at end of file diff --git a/integration_test/src/main.rs b/integration_test/src/main.rs new file mode 100644 index 00000000..54c63b1b --- /dev/null +++ b/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()) +} diff --git a/justfile b/justfile new file mode 100644 index 00000000..b1bf1cfc --- /dev/null +++ b/justfile @@ -0,0 +1,2 @@ +test: + cargo r -p integration_test diff --git a/src/cashu_mint.rs b/src/cashu_mint.rs new file mode 100644 index 00000000..1cbc1008 --- /dev/null +++ b/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 { + let url = self.url.join("keys")?; + Ok(minreq::get(url).send()?.json::()?) + } + + /// Get Keysets [NUT-02] + pub async fn get_keysets(&self) -> Result { + let url = self.url.join("keysets")?; + Ok(minreq::get(url).send()?.json::()?) + } + + /// Request Mint [NUT-03] + pub async fn request_mint(&self, amount: u64) -> Result { + let mut url = self.url.join("mint")?; + url.query_pairs_mut() + .append_pair("amount", &amount.to_string()); + + Ok(minreq::get(url).send()?.json::()?) + } + + /// Mint Tokens [NUT-04] + pub async fn mint( + &self, + blinded_messages: Vec, + payment_hash: &str, + ) -> Result { + 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::()?) + } + + /// Check Max expected fee [NUT-05] + pub async fn check_fees(&self, invoice: &str) -> Result { + let url = self.url.join("checkfees")?; + + let request = CheckFeesRequest { + pr: invoice.to_string(), + }; + + Ok(minreq::post(url) + .with_json(&request)? + .send()? + .json::()?) + } + + /// Melt [NUT-05] + /// [Nut-08] Lightning fee return if outputs defined + pub async fn melt( + &self, + proofs: Vec, + invoice: &str, + outputs: Option>, + ) -> Result { + let url = self.url.join("melt")?; + + let request = MeltRequest { + proofs, + pr: invoice.to_string(), + outputs, + }; + + Ok(minreq::post(url) + .with_json(&request)? + .send()? + .json::()?) + } + + /// Split Token [NUT-06] + pub async fn split( + &self, + amount: u64, + proofs: Vec, + outputs: Vec, + ) -> Result { + let url = self.url.join("split")?; + + let request = SplitRequest { + amount, + proofs, + outputs, + }; + + Ok(minreq::post(url) + .with_json(&request)? + .send()? + .json::()?) + } + + /// Spendable check [NUT-07] + pub async fn check_spendable( + &self, + proofs: Vec, + ) -> Result { + let url = self.url.join("check")?; + let request = CheckSpendableRequest { proofs }; + + Ok(minreq::post(url) + .with_json(&request)? + .send()? + .json::()?) + } + + /// Get Mint Info [NUT-09] + pub async fn get_info(&self) -> Result { + let url = self.url.join("info")?; + Ok(minreq::get(url).send()?.json::()?) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 00000000..73504ebe --- /dev/null +++ b/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), +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..7fe6efb2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,3 @@ +pub mod cashu_mint; +pub mod error; +pub mod types; diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 00000000..bc5d37d0 --- /dev/null +++ b/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, + /// P2SHScript that specifies the spending condition for this Proof + pub script: Option, +} + +/// Mint Keys [NIP-01] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MintKeys(pub HashMap); + +/// 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, +} + +/// 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, +} + +/// Post Mint Response [NUT-05] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PostMintResponse { + pub promises: Vec, +} + +/// 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, + /// 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>, +} + +/// 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, +} + +/// Split Request [NUT-06] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SplitRequest { + pub amount: u64, + pub proofs: Vec, + pub outputs: Vec, +} + +/// Split Response [NUT-06] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SplitResponse { + /// Promises to keep + pub fst: Vec, + /// Promises to send + pub snd: Vec, +} + +/// Check spendabale request [NUT-07] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CheckSpendableRequest { + pub proofs: Vec, +} + +/// 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, +} + +/// Mint Version +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MintVersion { + name: String, + version: String, +} + +impl Serialize for MintVersion { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let combined = format!("{}/{}", self.name, self.version); + serializer.serialize_str(&combined) + } +} + +impl<'de> Deserialize<'de> for MintVersion { + fn deserialize(deserializer: D) -> Result + 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, + /// shows which NUTs the mint supports + pub nuts: Vec, + /// message of the day that the wallet must display to the user + pub motd: String, +}