refactor: split into cashu and cashu-sdk

This commit is contained in:
thesimplekid
2023-08-16 22:51:00 +01:00
parent fd200e7de8
commit 3aeb6b6ab4
29 changed files with 446 additions and 506 deletions

View File

@@ -1,47 +1,27 @@
[package]
name = "cashu-crab"
version = "0.4.1-ALPHA"
edition = "2021"
authors = ["thesimplekid"]
[workspace]
members = [
"crates/cashu",
"crates/cashu-sdk"
]
[workspace.package]
license = "BSD-3-Clause"
homepage = "https://github.com/thesimplekid/cashu-crab"
repository = "https://github.com/thesimplekid/cashu-crab.git"
[workspace.metadata]
authors = ["thesimplekid"]
edition = "2021"
description = "cashu-crab"
readme = "README.md"
documentation = "https://docs.rs/crate/cashu-crab"
repository = "https://github.com/thesimplekid/cashu-crab"
description = "Cashu rust wallet and mint library"
# exclude = ["integration_test"]
repository = "https://github.com/thesimplekid/cashu-rs-mint"
license-file = "LICENSE"
keywords = ["bitcoin", "e-cash", "cashu"]
#[workspace]
#members = ["bindings"]
[features]
default = ["mint", "wallet"]
mint = []
wallet = ["minreq"]
[dependencies]
base64 = "0.21.0"
bitcoin = { version = "0.30.0", features=["serde", "rand", "no-std"] }
bitcoin_hashes = "0.12.0"
hex = "0.4.3"
k256 = { version = "0.13.1", features=["arithmetic"] }
lightning-invoice = { version = "0.24.0", features=["serde"] }
rand = "0.8.5"
getrandom = { version = "0.2", features = ["js"] }
[workspace.dependencies]
serde = { version = "1.0.160", features = ["derive"]}
serde_json = "1.0.96"
url = "2.3.1"
regex = "1.8.4"
log = "0.4.19"
[workspace.dependencies]
uniffi = "0.24"
[target.'cfg(target_arch = "wasm32")'.dependencies]
gloo = { version = "0.9.0", features = ["net"]}
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
minreq = { version = "2.7.0", optional = true, features = ["json-using-serde", "https"] }
[dev-dependencies]
tokio = {version = "1.27.0", features = ["rt", "macros"] }
tracing = "0.1"
tracing-subscriber = "0.3"

21
LICENSES/nostr-MIT Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022-2023 Yuki Kishimoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,28 @@
[package]
name = "cashu-sdk"
version = "0.1.0"
edition = "2021"
authors = ["thesimplekid"]
homepage.workspace = true
repository.workspace = true
license.workspace = true
[features]
default = ["mint", "wallet"]
mint = []
# Fix: Should be minreq or gloo
wallet = ["minreq"]
[dependencies]
cashu = { path = "../cashu" }
serde = { workspace = true }
serde_json = { workspace = true }
url = { workspace = true }
tracing = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
gloo = { version = "0.9.0", features = ["net"]}
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
minreq = { version = "2.7.0", optional = true, features = ["json-using-serde", "https"] }

View File

@@ -1,24 +1,27 @@
//! Client to connet to mint
use std::fmt;
use gloo::net::http::Request;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use url::Url;
use crate::nuts::nut00::{wallet::BlindedMessages, BlindedMessage, Proof};
use crate::nuts::nut01::Keys;
use crate::nuts::nut03::RequestMintResponse;
use crate::nuts::nut04::{MintRequest, PostMintResponse};
use crate::nuts::nut05::{CheckFeesRequest, CheckFeesResponse};
use crate::nuts::nut06::{SplitRequest, SplitResponse};
use crate::nuts::nut07::{CheckSpendableRequest, CheckSpendableResponse};
use crate::nuts::nut08::{MeltRequest, MeltResponse};
use crate::nuts::nut09::MintInfo;
use crate::nuts::*;
use crate::utils;
use crate::Amount;
pub use crate::Bolt11Invoice;
use cashu::nuts::nut00::{wallet::BlindedMessages, BlindedMessage, Proof};
use cashu::nuts::nut01::Keys;
use cashu::nuts::nut03::RequestMintResponse;
use cashu::nuts::nut04::{MintRequest, PostMintResponse};
use cashu::nuts::nut05::{CheckFeesRequest, CheckFeesResponse};
use cashu::nuts::nut06::{SplitRequest, SplitResponse};
use cashu::nuts::nut07::{CheckSpendableRequest, CheckSpendableResponse};
use cashu::nuts::nut08::{MeltRequest, MeltResponse};
use cashu::nuts::nut09::MintInfo;
use cashu::nuts::*;
use cashu::utils;
use cashu::Amount;
#[cfg(target_arch = "wasm32")]
use gloo::net::http::Request;
pub use cashu::Bolt11Invoice;
#[derive(Debug)]
pub enum Error {
@@ -28,7 +31,10 @@ pub enum Error {
UrlParseError(url::ParseError),
/// Serde Json error
SerdeJsonError(serde_json::Error),
/// Gloo error
/// Min req error
#[cfg(not(target_arch = "wasm32"))]
MinReqError(minreq::Error),
#[cfg(target_arch = "wasm32")]
GlooError(String),
/// Custom Error
Custom(String),
@@ -46,6 +52,13 @@ impl From<serde_json::Error> for Error {
}
}
#[cfg(not(target_arch = "wasm32"))]
impl From<minreq::Error> for Error {
fn from(err: minreq::Error) -> Error {
Error::MinReqError(err)
}
}
impl std::error::Error for Error {}
impl fmt::Display for Error {
@@ -61,6 +74,9 @@ impl fmt::Display for Error {
}
Error::UrlParseError(err) => write!(f, "{}", err),
Error::SerdeJsonError(err) => write!(f, "{}", err),
#[cfg(not(target_arch = "wasm32"))]
Error::MinReqError(err) => write!(f, "{}", err),
#[cfg(target_arch = "wasm32")]
Error::GlooError(err) => write!(f, "{}", err),
Error::Custom(message) => write!(f, "{}", message),
}
@@ -106,6 +122,38 @@ impl Client {
}
/// Get Mint Keys [NUT-01]
#[cfg(not(target_arch = "wasm32"))]
pub async fn get_keys(&self) -> Result<Keys, Error> {
let url = self.mint_url.join("keys")?;
let keys = minreq::get(url).send()?.json::<Value>()?;
let keys: Keys = serde_json::from_str(&keys.to_string())?;
/*
let keys: BTreeMap<u64, String> = match serde_json::from_value(keys.clone()) {
Ok(keys) => keys,
Err(_err) => {
return Err(Error::CustomError(format!(
"url: {}, {}",
url,
serde_json::to_string(&keys)?
)))
}
};
let mint_keys: BTreeMap<u64, PublicKey> = keys
.into_iter()
.filter_map(|(k, v)| {
let key = hex::decode(v).ok()?;
let public_key = PublicKey::from_sec1_bytes(&key).ok()?;
Some((k, public_key))
})
.collect();
*/
Ok(keys)
}
/// Get Mint Keys [NUT-01]
#[cfg(target_arch = "wasm32")]
pub async fn get_keys(&self) -> Result<Keys, Error> {
let url = self.mint_url.join("keys")?;
let keys = Request::get(url.as_str())
@@ -142,6 +190,22 @@ impl Client {
}
/// Get Keysets [NUT-02]
#[cfg(not(target_arch = "wasm32"))]
pub async fn get_keysets(&self) -> Result<nut02::Response, Error> {
let url = self.mint_url.join("keysets")?;
let res = minreq::get(url).send()?.json::<Value>()?;
let response: Result<nut02::Response, serde_json::Error> =
serde_json::from_value(res.clone());
match response {
Ok(res) => Ok(res),
Err(_) => Err(Error::from_json(&res.to_string())?),
}
}
/// Get Keysets [NUT-02]
#[cfg(target_arch = "wasm32")]
pub async fn get_keysets(&self) -> Result<nut02::Response, Error> {
let url = self.mint_url.join("keysets")?;
let res = Request::get(url.as_str())
@@ -162,6 +226,25 @@ impl Client {
}
/// Request Mint [NUT-03]
#[cfg(not(target_arch = "wasm32"))]
pub async fn request_mint(&self, amount: Amount) -> Result<RequestMintResponse, Error> {
let mut url = self.mint_url.join("mint")?;
url.query_pairs_mut()
.append_pair("amount", &amount.to_sat().to_string());
let res = minreq::get(url).send()?.json::<Value>()?;
let response: Result<RequestMintResponse, serde_json::Error> =
serde_json::from_value(res.clone());
match response {
Ok(res) => Ok(res),
Err(_) => Err(Error::from_json(&res.to_string())?),
}
}
/// Request Mint [NUT-03]
#[cfg(target_arch = "wasm32")]
pub async fn request_mint(&self, amount: Amount) -> Result<RequestMintResponse, Error> {
let mut url = self.mint_url.join("mint")?;
url.query_pairs_mut()
@@ -185,6 +268,35 @@ impl Client {
}
/// Mint Tokens [NUT-04]
#[cfg(not(target_arch = "wasm32"))]
pub async fn mint(
&self,
blinded_messages: BlindedMessages,
hash: &str,
) -> Result<PostMintResponse, Error> {
let mut url = self.mint_url.join("mint")?;
url.query_pairs_mut().append_pair("hash", hash);
let request = MintRequest {
outputs: blinded_messages.blinded_messages,
};
let res = minreq::post(url)
.with_json(&request)?
.send()?
.json::<Value>()?;
let response: Result<PostMintResponse, serde_json::Error> =
serde_json::from_value(res.clone());
match response {
Ok(res) => Ok(res),
Err(_) => Err(Error::from_json(&res.to_string())?),
}
}
/// Mint Tokens [NUT-04]
#[cfg(target_arch = "wasm32")]
pub async fn mint(
&self,
blinded_messages: BlindedMessages,
@@ -217,6 +329,28 @@ impl Client {
}
/// Check Max expected fee [NUT-05]
#[cfg(not(target_arch = "wasm32"))]
pub async fn check_fees(&self, invoice: Bolt11Invoice) -> Result<CheckFeesResponse, Error> {
let url = self.mint_url.join("checkfees")?;
let request = CheckFeesRequest { pr: invoice };
let res = minreq::post(url)
.with_json(&request)?
.send()?
.json::<Value>()?;
let response: Result<CheckFeesResponse, serde_json::Error> =
serde_json::from_value(res.clone());
match response {
Ok(res) => Ok(res),
Err(_) => Err(Error::from_json(&res.to_string())?),
}
}
/// Check Max expected fee [NUT-05]
#[cfg(target_arch = "wasm32")]
pub async fn check_fees(&self, invoice: Bolt11Invoice) -> Result<CheckFeesResponse, Error> {
let url = self.mint_url.join("checkfees")?;
@@ -243,6 +377,38 @@ impl Client {
/// Melt [NUT-05]
/// [Nut-08] Lightning fee return if outputs defined
#[cfg(not(target_arch = "wasm32"))]
pub async fn melt(
&self,
proofs: Vec<Proof>,
invoice: Bolt11Invoice,
outputs: Option<Vec<BlindedMessage>>,
) -> Result<MeltResponse, Error> {
let url = self.mint_url.join("melt")?;
let request = MeltRequest {
proofs,
pr: invoice,
outputs,
};
let value = minreq::post(url)
.with_json(&request)?
.send()?
.json::<Value>()?;
let response: Result<MeltResponse, serde_json::Error> =
serde_json::from_value(value.clone());
match response {
Ok(res) => Ok(res),
Err(_) => Err(Error::from_json(&value.to_string())?),
}
}
/// Melt [NUT-05]
/// [Nut-08] Lightning fee return if outputs defined
#[cfg(target_arch = "wasm32")]
pub async fn melt(
&self,
proofs: Vec<Proof>,
@@ -277,6 +443,26 @@ impl Client {
}
/// Split Token [NUT-06]
#[cfg(not(target_arch = "wasm32"))]
pub async fn split(&self, split_request: SplitRequest) -> Result<SplitResponse, Error> {
let url = self.mint_url.join("split")?;
let res = minreq::post(url)
.with_json(&split_request)?
.send()?
.json::<Value>()?;
let response: Result<SplitResponse, serde_json::Error> =
serde_json::from_value(res.clone());
match response {
Ok(res) => Ok(res),
Err(_) => Err(Error::from_json(&res.to_string())?),
}
}
/// Split Token [NUT-06]
#[cfg(target_arch = "wasm32")]
pub async fn split(&self, split_request: SplitRequest) -> Result<SplitResponse, Error> {
let url = self.mint_url.join("split")?;
@@ -300,6 +486,32 @@ impl Client {
}
/// Spendable check [NUT-07]
#[cfg(not(target_arch = "wasm32"))]
pub async fn check_spendable(
&self,
proofs: &Vec<nut00::mint::Proof>,
) -> Result<CheckSpendableResponse, Error> {
let url = self.mint_url.join("check")?;
let request = CheckSpendableRequest {
proofs: proofs.to_owned(),
};
let res = minreq::post(url)
.with_json(&request)?
.send()?
.json::<Value>()?;
let response: Result<CheckSpendableResponse, serde_json::Error> =
serde_json::from_value(res.clone());
match response {
Ok(res) => Ok(res),
Err(_) => Err(Error::from_json(&res.to_string())?),
}
}
/// Spendable check [NUT-07]
#[cfg(target_arch = "wasm32")]
pub async fn check_spendable(
&self,
proofs: &Vec<nut00::mint::Proof>,
@@ -329,6 +541,21 @@ impl Client {
}
/// Get Mint Info [NUT-09]
#[cfg(not(target_arch = "wasm32"))]
pub async fn get_info(&self) -> Result<MintInfo, Error> {
let url = self.mint_url.join("info")?;
let res = minreq::get(url).send()?.json::<Value>()?;
let response: Result<MintInfo, serde_json::Error> = serde_json::from_value(res.clone());
match response {
Ok(res) => Ok(res),
Err(_) => Err(Error::from_json(&res.to_string())?),
}
}
/// Get Mint Info [NUT-09]
#[cfg(target_arch = "wasm32")]
pub async fn get_info(&self) -> Result<MintInfo, Error> {
let url = self.mint_url.join("info")?;
let res = Request::get(url.as_str())

View File

@@ -0,0 +1,7 @@
#[cfg(feature = "wallet")]
pub(crate) mod client;
#[cfg(feature = "mint")]
pub mod mint;
#[cfg(feature = "wallet")]
pub mod wallet;

View File

@@ -1,19 +1,19 @@
use std::collections::{HashMap, HashSet};
use crate::dhke::sign_message;
use crate::dhke::verify_message;
use crate::error::mint::Error;
use crate::nuts::nut00::BlindedMessage;
use crate::nuts::nut00::BlindedSignature;
use crate::nuts::nut00::Proof;
use crate::nuts::nut06::SplitRequest;
use crate::nuts::nut06::SplitResponse;
use crate::nuts::nut07::CheckSpendableRequest;
use crate::nuts::nut07::CheckSpendableResponse;
use crate::nuts::nut08::MeltRequest;
use crate::nuts::nut08::MeltResponse;
use crate::nuts::*;
use crate::Amount;
use cashu::dhke::sign_message;
use cashu::dhke::verify_message;
use cashu::error::mint::Error;
use cashu::nuts::nut00::BlindedMessage;
use cashu::nuts::nut00::BlindedSignature;
use cashu::nuts::nut00::Proof;
use cashu::nuts::nut06::SplitRequest;
use cashu::nuts::nut06::SplitResponse;
use cashu::nuts::nut07::CheckSpendableRequest;
use cashu::nuts::nut07::CheckSpendableResponse;
use cashu::nuts::nut08::MeltRequest;
use cashu::nuts::nut08::MeltResponse;
use cashu::nuts::*;
use cashu::Amount;
pub struct Mint {
// pub pubkey: PublicKey,

View File

@@ -1,29 +1,57 @@
//! Cashu Wallet
use std::error::Error as StdError;
use std::fmt;
use std::str::FromStr;
use log::warn;
use crate::dhke::unblind_message;
use crate::nuts::nut00::{
use cashu::dhke::construct_proofs;
use cashu::dhke::unblind_message;
use cashu::nuts::nut00::{
mint, wallet::BlindedMessages, wallet::Token, BlindedSignature, Proof, Proofs,
};
use crate::nuts::nut01::Keys;
use crate::nuts::nut03::RequestMintResponse;
use crate::nuts::nut06::{SplitPayload, SplitRequest};
use crate::types::{Melted, ProofsStatus, SendProofs};
use crate::Amount;
pub use crate::Bolt11Invoice;
use crate::{
dhke::construct_proofs,
error::{self, wallet::Error},
};
use cashu::nuts::nut01::Keys;
use cashu::nuts::nut03::RequestMintResponse;
use cashu::nuts::nut06::{SplitPayload, SplitRequest};
use cashu::types::{Melted, ProofsStatus, SendProofs};
use cashu::Amount;
pub use cashu::Bolt11Invoice;
use tracing::warn;
#[cfg(target_arch = "wasm32")]
use crate::wasm_client::Client;
#[cfg(not(target_arch = "wasm32"))]
use crate::client::Client;
#[derive(Debug)]
pub enum Error {
/// Insufficaint Funds
InsufficantFunds,
CashuError(cashu::error::wallet::Error),
ClientError(crate::client::Error),
CustomError(String),
}
impl StdError for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::InsufficantFunds => write!(f, "Insufficant Funds"),
Error::CashuError(err) => write!(f, "{}", err),
Error::ClientError(err) => write!(f, "{}", err),
Error::CustomError(err) => write!(f, "{}", err),
}
}
}
impl From<cashu::error::wallet::Error> for Error {
fn from(err: cashu::error::wallet::Error) -> Self {
Self::CashuError(err)
}
}
impl From<crate::client::Error> for Error {
fn from(err: crate::client::Error) -> Error {
Error::ClientError(err)
}
}
#[derive(Clone, Debug)]
pub struct Wallet {
pub client: Client,
@@ -43,10 +71,7 @@ impl Wallet {
// TODO: getter method for keys that if it cant get them try again
/// Check if a proof is spent
pub async fn check_proofs_spent(
&self,
proofs: &mint::Proofs,
) -> Result<ProofsStatus, error::wallet::Error> {
pub async fn check_proofs_spent(&self, proofs: &mint::Proofs) -> Result<ProofsStatus, Error> {
let spendable = self.client.check_spendable(proofs).await?;
// Separate proofs in spent and unspent based on mint response
@@ -62,19 +87,12 @@ impl Wallet {
}
/// Request Token Mint
pub async fn request_mint(
&self,
amount: Amount,
) -> Result<RequestMintResponse, error::wallet::Error> {
pub async fn request_mint(&self, amount: Amount) -> Result<RequestMintResponse, Error> {
Ok(self.client.request_mint(amount).await?)
}
/// Mint Token
pub async fn mint_token(
&self,
amount: Amount,
hash: &str,
) -> Result<Token, error::wallet::Error> {
pub async fn mint_token(&self, amount: Amount, hash: &str) -> Result<Token, Error> {
let proofs = self.mint(amount, hash).await?;
let token = Token::new(self.client.mint_url.clone(), proofs, None);
@@ -82,7 +100,7 @@ impl Wallet {
}
/// Mint Proofs
pub async fn mint(&self, amount: Amount, hash: &str) -> Result<Proofs, error::wallet::Error> {
pub async fn mint(&self, amount: Amount, hash: &str) -> Result<Proofs, Error> {
let blinded_messages = BlindedMessages::random(amount)?;
let mint_res = self.client.mint(blinded_messages.clone(), hash).await?;
@@ -98,12 +116,12 @@ impl Wallet {
}
/// Check fee
pub async fn check_fee(&self, invoice: Bolt11Invoice) -> Result<Amount, error::wallet::Error> {
pub async fn check_fee(&self, invoice: Bolt11Invoice) -> Result<Amount, Error> {
Ok(self.client.check_fees(invoice).await?.fee)
}
/// Receive
pub async fn receive(&self, encoded_token: &str) -> Result<Proofs, error::wallet::Error> {
pub async fn receive(&self, encoded_token: &str) -> Result<Proofs, Error> {
let token_data = Token::from_str(encoded_token)?;
let mut proofs: Vec<Proofs> = vec![vec![]];
@@ -119,7 +137,7 @@ impl Wallet {
};
// Sum amount of all proofs
let amount: Amount = token.proofs.iter().map(|p| p.amount).sum();
let _amount: Amount = token.proofs.iter().map(|p| p.amount).sum();
let split_payload = self.create_split(token.proofs)?;
@@ -145,7 +163,7 @@ impl Wallet {
}
/// Create Split Payload
fn create_split(&self, proofs: Proofs) -> Result<SplitPayload, error::wallet::Error> {
fn create_split(&self, proofs: Proofs) -> Result<SplitPayload, Error> {
let value = proofs.iter().map(|p| p.amount).sum();
let blinded_messages = BlindedMessages::random(value)?;
@@ -166,7 +184,7 @@ impl Wallet {
&self,
blinded_messages: BlindedMessages,
promises: Vec<BlindedSignature>,
) -> Result<Proofs, error::wallet::Error> {
) -> Result<Proofs, Error> {
let BlindedMessages {
blinded_messages: _,
secrets,
@@ -201,11 +219,7 @@ impl Wallet {
}
/// Send
pub async fn send(
&self,
amount: Amount,
proofs: Proofs,
) -> Result<SendProofs, error::wallet::Error> {
pub async fn send(&self, amount: Amount, proofs: Proofs) -> Result<SendProofs, Error> {
let mut amount_available = Amount::ZERO;
let mut send_proofs = SendProofs::default();
@@ -221,7 +235,7 @@ impl Wallet {
if amount_available.lt(&amount) {
println!("Not enough funds");
return Err(error::wallet::Error::InsufficantFunds);
return Err(Error::InsufficantFunds);
}
// If amount available is EQUAL to send amount no need to split
@@ -229,7 +243,7 @@ impl Wallet {
return Ok(send_proofs);
}
let amount_to_keep = amount_available - amount;
let _amount_to_keep = amount_available - amount;
let amount_to_send = amount;
let split_payload = self.create_split(send_proofs.send_proofs)?;
@@ -270,7 +284,7 @@ impl Wallet {
invoice: Bolt11Invoice,
proofs: Proofs,
fee_reserve: Amount,
) -> Result<Melted, error::wallet::Error> {
) -> Result<Melted, Error> {
let blinded = BlindedMessages::blank(fee_reserve)?;
let melt_response = self
.client
@@ -296,12 +310,8 @@ impl Wallet {
Ok(melted)
}
pub fn proofs_to_token(
&self,
proofs: Proofs,
memo: Option<String>,
) -> Result<String, error::wallet::Error> {
Token::new(self.client.mint_url.clone(), proofs, memo).convert_to_string()
pub fn proofs_to_token(&self, proofs: Proofs, memo: Option<String>) -> Result<String, Error> {
Ok(Token::new(self.client.mint_url.clone(), proofs, memo).convert_to_string()?)
}
}
@@ -314,7 +324,7 @@ mod tests {
use crate::client::Client;
use crate::mint::Mint;
use crate::nuts::nut04;
use cashu::nuts::nut04;
#[test]
fn test_wallet() {

35
crates/cashu/Cargo.toml Normal file
View File

@@ -0,0 +1,35 @@
[package]
name = "cashu"
version = "0.4.1-ALPHA"
edition = "2021"
authors = ["thesimplekid"]
readme = "README.md"
homepage.workspace = true
repository.workspace = true
license.workspace = true
description = "Cashu rust wallet and mint library"
[features]
default = ["mint", "wallet"]
mint = []
wallet = []
[dependencies]
base64 = "0.21.0"
bitcoin = { version = "0.30.0", features=["serde", "rand", "no-std"] }
bitcoin_hashes = "0.12.0"
hex = "0.4.3"
k256 = { version = "0.13.1", features=["arithmetic"] }
lightning-invoice = { version = "0.24.0", features=["serde"] }
rand = "0.8.5"
getrandom = { version = "0.2", features = ["js"] }
serde = { workspace = true }
serde_json = { workspace = true }
url = { workspace = true }
regex = "1.8.4"
log = "0.4.19"
[dev-dependencies]
tokio = {version = "1.27.0", features = ["rt", "macros"] }

View File

@@ -80,10 +80,6 @@ pub mod wallet {
#[derive(Debug)]
pub enum Error {
#[cfg(not(target_arch = "wasm32"))]
CrabMintError(crate::client::Error),
#[cfg(target_arch = "wasm32")]
CrabMintError(crate::wasm_client::Error),
/// Serde Json error
SerdeJsonError(serde_json::Error),
/// From elliptic curve
@@ -104,7 +100,6 @@ pub mod wallet {
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::CrabMintError(err) => write!(f, "{}", err),
Error::CustomError(err) => write!(f, "{}", err),
Error::InsufficantFunds => write!(f, "Insufficant Funds"),
Error::Utf8ParseError(err) => write!(f, "{}", err),
@@ -116,20 +111,6 @@ pub mod wallet {
}
}
#[cfg(not(target_arch = "wasm32"))]
impl From<crate::client::Error> for Error {
fn from(err: crate::client::Error) -> Error {
Error::CrabMintError(err)
}
}
#[cfg(target_arch = "wasm32")]
impl From<crate::wasm_client::Error> for Error {
fn from(err: crate::wasm_client::Error) -> Error {
Error::CrabMintError(err)
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Error {
Error::SerdeJsonError(err)

15
crates/cashu/src/lib.rs Normal file
View File

@@ -0,0 +1,15 @@
pub mod amount;
#[cfg(feature = "wallet")]
pub mod dhke;
pub mod error;
pub mod nuts;
pub mod serde_utils;
pub mod types;
pub mod utils;
pub use amount::Amount;
pub use bitcoin::hashes::sha256::Hash as Sha256;
pub use lightning_invoice;
pub use lightning_invoice::Bolt11Invoice;
pub type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;

View File

@@ -249,7 +249,7 @@ mod tests {
use super::*;
#[test]
fn test_proof_seralize() {
fn test_proof_serialize() {
let proof = "[{\"id\":\"DSAl9nvvyfva\",\"amount\":2,\"secret\":\"EhpennC9qB3iFlW8FZ_pZw\",\"C\":\"02c020067db727d586bc3183aecf97fcb800c3f4cc4759f69c626c9db5d8f5b5d4\"},{\"id\":\"DSAl9nvvyfva\",\"amount\":8,\"secret\":\"TmS6Cv0YT5PU_5ATVKnukw\",\"C\":\"02ac910bef28cbe5d7325415d5c263026f15f9b967a079ca9779ab6e5c2db133a7\"}]";
let proof: Proofs = serde_json::from_str(proof).unwrap();

View File

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

View File

@@ -1,332 +0,0 @@
//! Client to connet to mint
use std::fmt;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use url::Url;
use crate::nuts::nut00::{wallet::BlindedMessages, BlindedMessage, Proof};
use crate::nuts::nut01::Keys;
use crate::nuts::nut03::RequestMintResponse;
use crate::nuts::nut04::{MintRequest, PostMintResponse};
use crate::nuts::nut05::{CheckFeesRequest, CheckFeesResponse};
use crate::nuts::nut06::{SplitRequest, SplitResponse};
use crate::nuts::nut07::{CheckSpendableRequest, CheckSpendableResponse};
use crate::nuts::nut08::{MeltRequest, MeltResponse};
use crate::nuts::nut09::MintInfo;
use crate::nuts::*;
use crate::utils;
use crate::Amount;
pub use crate::Bolt11Invoice;
#[derive(Debug)]
pub enum Error {
InvoiceNotPaid,
LightingWalletNotResponding(Option<String>),
/// Parse Url Error
UrlParseError(url::ParseError),
/// Serde Json error
SerdeJsonError(serde_json::Error),
/// Min req error
MinReqError(minreq::Error),
/// Custom Error
Custom(String),
}
impl From<url::ParseError> for Error {
fn from(err: url::ParseError) -> Error {
Error::UrlParseError(err)
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Error {
Error::SerdeJsonError(err)
}
}
impl From<minreq::Error> for Error {
fn from(err: minreq::Error) -> Error {
Error::MinReqError(err)
}
}
impl std::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::InvoiceNotPaid => write!(f, "Invoice not paid"),
Error::LightingWalletNotResponding(mint) => {
write!(
f,
"Lightning Wallet not responding: {}",
mint.clone().unwrap_or("".to_string())
)
}
Error::UrlParseError(err) => write!(f, "{}", err),
Error::SerdeJsonError(err) => write!(f, "{}", err),
Error::MinReqError(err) => write!(f, "{}", err),
Error::Custom(message) => write!(f, "{}", message),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MintErrorResponse {
code: u32,
error: String,
}
impl Error {
pub fn from_json(json: &str) -> Result<Self, Error> {
let mint_res: MintErrorResponse = serde_json::from_str(json)?;
let mint_error = match mint_res.error {
error if error.starts_with("Lightning invoice not paid yet.") => Error::InvoiceNotPaid,
error if error.starts_with("Lightning wallet not responding") => {
let mint = utils::extract_url_from_error(&error);
Error::LightingWalletNotResponding(mint)
}
error => Error::Custom(error),
};
Ok(mint_error)
}
}
#[derive(Debug, Clone)]
pub struct Client {
pub mint_url: Url,
}
impl Client {
pub fn new(mint_url: &str) -> Result<Self, Error> {
// HACK
let mut mint_url = String::from(mint_url);
if !mint_url.ends_with('/') {
mint_url.push('/');
}
let mint_url = Url::parse(&mint_url)?;
Ok(Self { mint_url })
}
/// Get Mint Keys [NUT-01]
pub async fn get_keys(&self) -> Result<Keys, Error> {
let url = self.mint_url.join("keys")?;
let keys = minreq::get(url).send()?.json::<Value>()?;
let keys: Keys = serde_json::from_str(&keys.to_string())?;
/*
let keys: BTreeMap<u64, String> = match serde_json::from_value(keys.clone()) {
Ok(keys) => keys,
Err(_err) => {
return Err(Error::CustomError(format!(
"url: {}, {}",
url,
serde_json::to_string(&keys)?
)))
}
};
let mint_keys: BTreeMap<u64, PublicKey> = keys
.into_iter()
.filter_map(|(k, v)| {
let key = hex::decode(v).ok()?;
let public_key = PublicKey::from_sec1_bytes(&key).ok()?;
Some((k, public_key))
})
.collect();
*/
Ok(keys)
}
/// Get Keysets [NUT-02]
pub async fn get_keysets(&self) -> Result<nut02::Response, Error> {
let url = self.mint_url.join("keysets")?;
let res = minreq::get(url).send()?.json::<Value>()?;
let response: Result<nut02::Response, serde_json::Error> =
serde_json::from_value(res.clone());
match response {
Ok(res) => Ok(res),
Err(_) => Err(Error::from_json(&res.to_string())?),
}
}
/// Request Mint [NUT-03]
pub async fn request_mint(&self, amount: Amount) -> Result<RequestMintResponse, Error> {
let mut url = self.mint_url.join("mint")?;
url.query_pairs_mut()
.append_pair("amount", &amount.to_sat().to_string());
let res = minreq::get(url).send()?.json::<Value>()?;
let response: Result<RequestMintResponse, serde_json::Error> =
serde_json::from_value(res.clone());
match response {
Ok(res) => Ok(res),
Err(_) => Err(Error::from_json(&res.to_string())?),
}
}
/// Mint Tokens [NUT-04]
pub async fn mint(
&self,
blinded_messages: BlindedMessages,
hash: &str,
) -> Result<PostMintResponse, Error> {
let mut url = self.mint_url.join("mint")?;
url.query_pairs_mut().append_pair("hash", hash);
let request = MintRequest {
outputs: blinded_messages.blinded_messages,
};
let res = minreq::post(url)
.with_json(&request)?
.send()?
.json::<Value>()?;
let response: Result<PostMintResponse, serde_json::Error> =
serde_json::from_value(res.clone());
match response {
Ok(res) => Ok(res),
Err(_) => Err(Error::from_json(&res.to_string())?),
}
}
/// Check Max expected fee [NUT-05]
pub async fn check_fees(&self, invoice: Bolt11Invoice) -> Result<CheckFeesResponse, Error> {
let url = self.mint_url.join("checkfees")?;
let request = CheckFeesRequest { pr: invoice };
let res = minreq::post(url)
.with_json(&request)?
.send()?
.json::<Value>()?;
let response: Result<CheckFeesResponse, serde_json::Error> =
serde_json::from_value(res.clone());
match response {
Ok(res) => Ok(res),
Err(_) => Err(Error::from_json(&res.to_string())?),
}
}
/// Melt [NUT-05]
/// [Nut-08] Lightning fee return if outputs defined
pub async fn melt(
&self,
proofs: Vec<Proof>,
invoice: Bolt11Invoice,
outputs: Option<Vec<BlindedMessage>>,
) -> Result<MeltResponse, Error> {
let url = self.mint_url.join("melt")?;
let request = MeltRequest {
proofs,
pr: invoice,
outputs,
};
let value = minreq::post(url)
.with_json(&request)?
.send()?
.json::<Value>()?;
let response: Result<MeltResponse, serde_json::Error> =
serde_json::from_value(value.clone());
match response {
Ok(res) => Ok(res),
Err(_) => Err(Error::from_json(&value.to_string())?),
}
}
/// Split Token [NUT-06]
pub async fn split(&self, split_request: SplitRequest) -> Result<SplitResponse, Error> {
let url = self.mint_url.join("split")?;
let res = minreq::post(url)
.with_json(&split_request)?
.send()?
.json::<Value>()?;
let response: Result<SplitResponse, serde_json::Error> =
serde_json::from_value(res.clone());
match response {
Ok(res) => Ok(res),
Err(_) => Err(Error::from_json(&res.to_string())?),
}
}
/// Spendable check [NUT-07]
pub async fn check_spendable(
&self,
proofs: &Vec<nut00::mint::Proof>,
) -> Result<CheckSpendableResponse, Error> {
let url = self.mint_url.join("check")?;
let request = CheckSpendableRequest {
proofs: proofs.to_owned(),
};
let res = minreq::post(url)
.with_json(&request)?
.send()?
.json::<Value>()?;
let response: Result<CheckSpendableResponse, serde_json::Error> =
serde_json::from_value(res.clone());
match response {
Ok(res) => Ok(res),
Err(_) => Err(Error::from_json(&res.to_string())?),
}
}
/// Get Mint Info [NUT-09]
pub async fn get_info(&self) -> Result<MintInfo, Error> {
let url = self.mint_url.join("info")?;
let res = minreq::get(url).send()?.json::<Value>()?;
let response: Result<MintInfo, serde_json::Error> = serde_json::from_value(res.clone());
match response {
Ok(res) => Ok(res),
Err(_) => Err(Error::from_json(&res.to_string())?),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decode_error() {
let err = r#"{"code":0,"error":"Lightning invoice not paid yet."}"#;
let error = Error::from_json(err).unwrap();
match error {
Error::InvoiceNotPaid => {}
_ => panic!("Wrong error"),
}
let err = r#"{"code": 0, "error": "Lightning wallet not responding: Failed to connect to https://legend.lnbits.com due to: All connection attempts failed"}"#;
let error = Error::from_json(err).unwrap();
match error {
Error::LightingWalletNotResponding(mint) => {
assert_eq!(mint, Some("https://legend.lnbits.com".to_string()));
}
_ => panic!("Wrong error"),
}
}
}

View File

@@ -1,30 +0,0 @@
pub mod amount;
#[cfg(feature = "wallet")]
pub mod dhke;
pub mod error;
#[cfg(feature = "mint")]
pub mod mint;
pub mod nuts;
pub mod serde_utils;
pub mod types;
pub mod utils;
#[cfg(feature = "wallet")]
pub mod wallet;
#[cfg(all(feature = "wallet", not(target_arch = "wasm32")))]
pub mod client;
#[cfg(all(feature = "wallet", target_arch = "wasm32"))]
pub mod wasm_client;
#[cfg(all(feature = "wallet", target_arch = "wasm32"))]
pub use wasm_client::Client;
#[cfg(all(feature = "wallet", not(target_arch = "wasm32")))]
pub use client::Client;
pub use amount::Amount;
pub use bitcoin::hashes::sha256::Hash as Sha256;
pub use lightning_invoice;
pub use lightning_invoice::Bolt11Invoice;
pub type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;