mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-26 17:14:59 +01:00
init commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
/Cargo.lock
|
||||
14
Cargo.toml
Normal file
14
Cargo.toml
Normal 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"
|
||||
11
integration_test/Cargo.toml
Normal file
11
integration_test/Cargo.toml
Normal 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"] }
|
||||
36
integration_test/src/main.rs
Normal file
36
integration_test/src/main.rs
Normal 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())
|
||||
}
|
||||
138
src/cashu_mint.rs
Normal file
138
src/cashu_mint.rs
Normal 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
9
src/error.rs
Normal 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
3
src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod cashu_mint;
|
||||
pub mod error;
|
||||
pub mod types;
|
||||
194
src/types.rs
Normal file
194
src/types.rs
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user