sdk: add blocking client and wallet as feature

This commit is contained in:
thesimplekid
2023-08-17 13:58:19 +01:00
parent 9ead2ea507
commit 1aedf3f2bc
8 changed files with 343 additions and 6 deletions

4
.helix/languages.toml Normal file
View File

@@ -0,0 +1,4 @@
[[language]]
name = "rust"
config = { cargo = { features = [ "blocking", "wallet" ] } }

View File

@@ -23,5 +23,6 @@ keywords = ["bitcoin", "e-cash", "cashu"]
serde = { version = "1.0.160", features = ["derive"]} serde = { version = "1.0.160", features = ["derive"]}
serde_json = "1.0.96" serde_json = "1.0.96"
url = "2.3.1" url = "2.3.1"
tokio = { version = "1", default-features = false }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"

View File

@@ -10,9 +10,17 @@ license.workspace = true
[features] [features]
default = ["mint", "wallet"] default = ["mint", "wallet"]
mint = [] mint = ["cashu/mint"]
blocking = ["once_cell"]
wallet = ["cashu/wallet", "minreq", "once_cell"]
# Fix: Should be minreq or gloo # Fix: Should be minreq or gloo
wallet = ["minreq"] # [target.'cfg(not(target_arch = "wasm32"))'.features]
# wallet = ["cashu/wallet", "minreq", "once_cell"]
# [target.'cfg(target_arch = "wasm32")'.features]
# wallet = ["cashu/wallet", "gloo", "once_cell"]
[dependencies] [dependencies]
cashu = { path = "../cashu" } cashu = { path = "../cashu" }
@@ -20,9 +28,16 @@ serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
url = { workspace = true } url = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
once_cell = { version = "1.17", optional = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
gloo = { version = "0.9.0", features = ["net"]}
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "time", "macros", "sync"] }
minreq = { version = "2.7.0", optional = true, features = ["json-using-serde", "https"] } minreq = { version = "2.7.0", optional = true, features = ["json-using-serde", "https"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
tokio = { workspace = true, features = ["rt", "macros", "sync"] }
gloo = { version = "0.9.0", features = ["net"]}

View File

@@ -0,0 +1,80 @@
use crate::RUNTIME;
use cashu::{
nuts::{
nut00::{self, wallet::BlindedMessages, BlindedMessage, Proof},
nut01::Keys,
nut02,
nut03::RequestMintResponse,
nut04::PostMintResponse,
nut05::CheckFeesResponse,
nut06::{SplitRequest, SplitResponse},
nut07::CheckSpendableResponse,
nut08::MeltResponse,
nut09::MintInfo,
},
Amount, Bolt11Invoice,
};
use super::Error;
#[derive(Debug, Clone)]
pub struct Client {
pub(crate) client: super::Client,
}
impl Client {
pub fn new(mint_url: &str) -> Result<Self, Error> {
Ok(Self {
client: super::Client::new(mint_url)?,
})
}
pub fn get_keys(&self) -> Result<Keys, Error> {
RUNTIME.block_on(async { self.client.get_keys().await })
}
pub fn get_keysets(&self) -> Result<nut02::Response, Error> {
RUNTIME.block_on(async { self.client.get_keysets().await })
}
pub fn request_mint(&self, amount: Amount) -> Result<RequestMintResponse, Error> {
RUNTIME.block_on(async { self.client.request_mint(amount).await })
}
pub fn mint(
&self,
blinded_mssages: BlindedMessages,
hash: &str,
) -> Result<PostMintResponse, Error> {
RUNTIME.block_on(async { self.client.mint(blinded_mssages, hash).await })
}
pub fn check_fees(&self, invoice: Bolt11Invoice) -> Result<CheckFeesResponse, Error> {
RUNTIME.block_on(async { self.client.check_fees(invoice).await })
}
pub fn melt(
&self,
proofs: Vec<Proof>,
invoice: Bolt11Invoice,
outputs: Option<Vec<BlindedMessage>>,
) -> Result<MeltResponse, Error> {
RUNTIME.block_on(async { self.client.melt(proofs, invoice, outputs).await })
}
pub fn split(&self, split_request: SplitRequest) -> Result<SplitResponse, Error> {
RUNTIME.block_on(async { self.client.split(split_request).await })
}
pub fn check_spendable(
&self,
proofs: &Vec<nut00::mint::Proof>,
) -> Result<CheckSpendableResponse, Error> {
RUNTIME.block_on(async { self.client.check_spendable(proofs).await })
}
pub fn get_info(&self) -> Result<MintInfo, Error> {
RUNTIME.block_on(async { self.client.get_info().await })
}
}

View File

@@ -21,6 +21,9 @@ use cashu::Amount;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
use gloo::net::http::Request; use gloo::net::http::Request;
#[cfg(feature = "blocking")]
pub mod blocking;
pub use cashu::Bolt11Invoice; pub use cashu::Bolt11Invoice;
#[derive(Debug)] #[derive(Debug)]

View File

@@ -1,7 +1,28 @@
#[cfg(feature = "wallet")] #[cfg(feature = "blocking")]
use once_cell::sync::Lazy;
#[cfg(feature = "blocking")]
use tokio::runtime::Runtime;
#[cfg(feature = "blocking")]
use futures_util::Future;
// #[cfg(feature = "wallet")]
pub(crate) mod client; pub(crate) mod client;
#[cfg(feature = "mint")] #[cfg(feature = "mint")]
pub mod mint; pub mod mint;
#[cfg(feature = "wallet")] #[cfg(feature = "wallet")]
pub mod wallet; pub mod wallet;
pub use cashu::{self, *};
#[cfg(all(feature = "blocking", feature = "wallet"))]
use self::client::blocking;
#[cfg(feature = "blocking")]
static RUNTIME: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("Can't start Tokio runtime"));
#[cfg(feature = "blocking")]
pub fn block_on<F: Future>(future: F) -> F::Output {
RUNTIME.block_on(future)
}

View File

@@ -16,6 +16,10 @@ use cashu::Amount;
pub use cashu::Bolt11Invoice; pub use cashu::Bolt11Invoice;
use tracing::warn; use tracing::warn;
#[cfg(feature = "blocking")]
use crate::client::blocking::Client;
#[cfg(not(feature = "blocking"))]
use crate::client::Client; use crate::client::Client;
#[derive(Debug)] #[derive(Debug)]
@@ -71,6 +75,7 @@ impl Wallet {
// TODO: getter method for keys that if it cant get them try again // TODO: getter method for keys that if it cant get them try again
/// Check if a proof is spent /// Check if a proof is spent
#[cfg(not(feature = "blocking"))]
pub async fn check_proofs_spent(&self, proofs: &mint::Proofs) -> Result<ProofsStatus, Error> { pub async fn check_proofs_spent(&self, proofs: &mint::Proofs) -> Result<ProofsStatus, Error> {
let spendable = self.client.check_spendable(proofs).await?; let spendable = self.client.check_spendable(proofs).await?;
@@ -86,12 +91,37 @@ impl Wallet {
}) })
} }
/// Check if a proof is spent
#[cfg(feature = "blocking")]
pub fn check_proofs_spent(&self, proofs: &mint::Proofs) -> Result<ProofsStatus, Error> {
let spendable = self.client.check_spendable(proofs)?;
// Separate proofs in spent and unspent based on mint response
let (spendable, spent): (Vec<_>, Vec<_>) = proofs
.iter()
.zip(spendable.spendable.iter())
.partition(|(_, &b)| b);
Ok(ProofsStatus {
spendable: spendable.into_iter().map(|(s, _)| s).cloned().collect(),
spent: spent.into_iter().map(|(s, _)| s).cloned().collect(),
})
}
/// Request Token Mint /// Request Token Mint
#[cfg(not(feature = "blocking"))]
pub async fn request_mint(&self, amount: Amount) -> Result<RequestMintResponse, Error> { pub async fn request_mint(&self, amount: Amount) -> Result<RequestMintResponse, Error> {
Ok(self.client.request_mint(amount).await?) Ok(self.client.request_mint(amount).await?)
} }
/// Request Token Mint
#[cfg(feature = "blocking")]
pub fn request_mint(&self, amount: Amount) -> Result<RequestMintResponse, Error> {
Ok(self.client.request_mint(amount)?)
}
/// Mint Token /// Mint Token
#[cfg(not(feature = "blocking"))]
pub async fn mint_token(&self, amount: Amount, hash: &str) -> Result<Token, Error> { pub async fn mint_token(&self, amount: Amount, hash: &str) -> Result<Token, Error> {
let proofs = self.mint(amount, hash).await?; let proofs = self.mint(amount, hash).await?;
@@ -99,7 +129,17 @@ impl Wallet {
Ok(token) Ok(token)
} }
/// Blocking Mint Token
#[cfg(feature = "blocking")]
pub fn mint_token(&self, amount: Amount, hash: &str) -> Result<Token, Error> {
let proofs = self.mint(amount, hash)?;
let token = Token::new(self.client.client.mint_url.clone(), proofs, None);
Ok(token)
}
/// Mint Proofs /// Mint Proofs
#[cfg(not(feature = "blocking"))]
pub async fn mint(&self, amount: Amount, hash: &str) -> Result<Proofs, Error> { pub async fn mint(&self, amount: Amount, hash: &str) -> Result<Proofs, Error> {
let blinded_messages = BlindedMessages::random(amount)?; let blinded_messages = BlindedMessages::random(amount)?;
@@ -115,12 +155,37 @@ impl Wallet {
Ok(proofs) Ok(proofs)
} }
/// Blocking Mint Proofs
#[cfg(feature = "blocking")]
pub 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)?;
let proofs = construct_proofs(
mint_res.promises,
blinded_messages.rs,
blinded_messages.secrets,
&self.mint_keys,
)?;
Ok(proofs)
}
/// Check fee /// Check fee
#[cfg(not(feature = "blocking"))]
pub async fn check_fee(&self, invoice: Bolt11Invoice) -> Result<Amount, Error> { pub async fn check_fee(&self, invoice: Bolt11Invoice) -> Result<Amount, Error> {
Ok(self.client.check_fees(invoice).await?.fee) Ok(self.client.check_fees(invoice).await?.fee)
} }
/// Check fee
#[cfg(feature = "blocking")]
pub fn check_fee(&self, invoice: Bolt11Invoice) -> Result<Amount, Error> {
Ok(self.client.check_fees(invoice)?.fee)
}
/// Receive /// Receive
#[cfg(not(feature = "blocking"))]
pub async fn receive(&self, encoded_token: &str) -> Result<Proofs, Error> { pub async fn receive(&self, encoded_token: &str) -> Result<Proofs, Error> {
let token_data = Token::from_str(encoded_token)?; let token_data = Token::from_str(encoded_token)?;
@@ -160,6 +225,51 @@ impl Wallet {
Ok(proofs.iter().flatten().cloned().collect()) Ok(proofs.iter().flatten().cloned().collect())
} }
/// Blocking Receive
#[cfg(feature = "blocking")]
pub fn receive(&self, encoded_token: &str) -> Result<Proofs, Error> {
let token_data = Token::from_str(encoded_token)?;
let mut proofs: Vec<Proofs> = vec![vec![]];
for token in token_data.token {
if token.proofs.is_empty() {
continue;
}
let keys = if token
.mint
.to_string()
.eq(&self.client.client.mint_url.to_string())
{
self.mint_keys.clone()
} else {
Client::new(token.mint.as_str())?.get_keys()?
};
// Sum amount of all proofs
let _amount: Amount = token.proofs.iter().map(|p| p.amount).sum();
let split_payload = self.create_split(token.proofs)?;
let split_response = self.client.split(split_payload.split_payload)?;
if let Some(promises) = &split_response.promises {
// Proof to keep
let p = construct_proofs(
promises.to_owned(),
split_payload.blinded_messages.rs,
split_payload.blinded_messages.secrets,
&keys,
)?;
proofs.push(p);
} else {
warn!("Response missing promises");
return Err(Error::Custom("Split response missing promises".to_string()));
}
}
Ok(proofs.iter().flatten().cloned().collect())
}
/// Create Split Payload /// Create Split Payload
fn create_split(&self, proofs: Proofs) -> Result<SplitPayload, Error> { fn create_split(&self, proofs: Proofs) -> Result<SplitPayload, Error> {
let value = proofs.iter().map(|p| p.amount).sum(); let value = proofs.iter().map(|p| p.amount).sum();
@@ -217,6 +327,7 @@ impl Wallet {
} }
/// Send /// Send
#[cfg(not(feature = "blocking"))]
pub async fn send(&self, amount: Amount, proofs: Proofs) -> Result<SendProofs, Error> { pub async fn send(&self, amount: Amount, proofs: Proofs) -> Result<SendProofs, Error> {
let mut amount_available = Amount::ZERO; let mut amount_available = Amount::ZERO;
let mut send_proofs = SendProofs::default(); let mut send_proofs = SendProofs::default();
@@ -277,6 +388,69 @@ impl Wallet {
}) })
} }
/// Send
#[cfg(feature = "blocking")]
pub fn send(&self, amount: Amount, proofs: Proofs) -> Result<SendProofs, Error> {
let mut amount_available = Amount::ZERO;
let mut send_proofs = SendProofs::default();
for proof in proofs {
let proof_value = proof.amount;
if amount_available > amount {
send_proofs.change_proofs.push(proof);
} else {
send_proofs.send_proofs.push(proof);
}
amount_available += proof_value;
}
if amount_available.lt(&amount) {
println!("Not enough funds");
return Err(Error::InsufficantFunds);
}
// If amount available is EQUAL to send amount no need to split
if amount_available.eq(&amount) {
return Ok(send_proofs);
}
let _amount_to_keep = amount_available - amount;
let amount_to_send = amount;
let split_payload = self.create_split(send_proofs.send_proofs)?;
let split_response = self.client.split(split_payload.split_payload)?;
// If only promises assemble proofs needed for amount
let keep_proofs;
let send_proofs;
if let Some(promises) = split_response.promises {
let proofs = construct_proofs(
promises,
split_payload.blinded_messages.rs,
split_payload.blinded_messages.secrets,
&self.mint_keys,
)?;
let split = amount_to_send.split();
keep_proofs = proofs[0..split.len()].to_vec();
send_proofs = proofs[split.len()..].to_vec();
} else {
return Err(Error::Custom("Invalid split response".to_string()));
}
// println!("Send Proofs: {:#?}", send_proofs);
// println!("Keep Proofs: {:#?}", keep_proofs);
Ok(SendProofs {
change_proofs: keep_proofs,
send_proofs,
})
}
#[cfg(not(feature = "blocking"))]
pub async fn melt( pub async fn melt(
&self, &self,
invoice: Bolt11Invoice, invoice: Bolt11Invoice,
@@ -308,11 +482,49 @@ impl Wallet {
Ok(melted) Ok(melted)
} }
#[cfg(feature = "blocking")]
pub fn melt(
&self,
invoice: Bolt11Invoice,
proofs: Proofs,
fee_reserve: Amount,
) -> Result<Melted, Error> {
let blinded = BlindedMessages::blank(fee_reserve)?;
let melt_response = self
.client
.melt(proofs, invoice, Some(blinded.blinded_messages))?;
let change_proofs = match melt_response.change {
Some(change) => Some(construct_proofs(
change,
blinded.rs,
blinded.secrets,
&self.mint_keys,
)?),
None => None,
};
let melted = Melted {
paid: true,
preimage: melt_response.preimage,
change: change_proofs,
};
Ok(melted)
}
#[cfg(not(feature = "blocking"))]
pub fn proofs_to_token(&self, proofs: Proofs, memo: Option<String>) -> Result<String, Error> { 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()?) Ok(Token::new(self.client.mint_url.clone(), proofs, memo).convert_to_string()?)
} }
#[cfg(feature = "blocking")]
pub fn proofs_to_token(&self, proofs: Proofs, memo: Option<String>) -> Result<String, Error> {
Ok(Token::new(self.client.client.mint_url.clone(), proofs, memo).convert_to_string()?)
}
} }
/*
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@@ -379,3 +591,4 @@ mod tests {
} }
} }
} }
*/

View File

@@ -30,4 +30,4 @@ url = { workspace = true }
regex = "1.8.4" regex = "1.8.4"
[dev-dependencies] [dev-dependencies]
tokio = {version = "1.27.0", features = ["rt", "macros"] } # tokio = {version = "1.27.0", features = ["rt", "macros"] }