init commit

This commit is contained in:
thesimplekid
2023-04-23 00:32:40 -04:00
commit 25c3620ecc
9 changed files with 409 additions and 0 deletions

2
.gitignore vendored Normal file
View File

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

14
Cargo.toml Normal file
View File

@@ -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"

View File

@@ -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"] }

View File

@@ -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
justfile Normal file
View File

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

138
src/cashu_mint.rs Normal file
View File

@@ -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
src/error.rs Normal file
View File

@@ -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
src/lib.rs Normal file
View File

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

194
src/types.rs Normal file
View File

@@ -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,
}