This commit is contained in:
thesimplekid
2023-04-23 21:58:56 -04:00
parent 88eb0b3322
commit 8aa2b42d25
6 changed files with 167 additions and 46 deletions

View File

@@ -1,28 +1,27 @@
⚠️ **Don't be reckless:** This project is in early development, it does however work with real sats! Always use amounts you don't mind loosing.
> **Warning**
> This project is in early development, it does however work with real sats! Always use amounts you don't mind loosing.
Cashu RS is a rust library for [Cashu](https://github.com/cashubtc) wallets written in Rust.
Implemented [NUTs](https://github.com/cashubtc/nuts/):
## Implemented [NUTs](https://github.com/cashubtc/nuts/):
- [x] [NUT-00](https://github.com/cashubtc/nuts/blob/main/00.md)
- [x] [NUT-01](https://github.com/cashubtc/nuts/blob/main/01.md)
- [x] [NUT-02](https://github.com/cashubtc/nuts/blob/main/02.md)
- [x] [NUT-03](https://github.com/cashubtc/nuts/blob/main/03.md)
- [x] [NUT-04](https://github.com/cashubtc/nuts/blob/main/04.md)
- [x] [NUT-05](https://github.com/cashubtc/nuts/blob/main/05.md)
- [x] [NUT-06](https://github.com/cashubtc/nuts/blob/main/06.md)
- [x] [NUT-07](https://github.com/cashubtc/nuts/blob/main/07.md)
- [x] [NUT-08](https://github.com/cashubtc/nuts/blob/main/08.md)
- [x] [NUT-09](https://github.com/cashubtc/nuts/blob/main/09.md)
- [NUT-00](https://github.com/cashubtc/nuts/blob/main/00.md)
- [NUT-01](https://github.com/cashubtc/nuts/blob/main/01.md)
- [NUT-02](https://github.com/cashubtc/nuts/blob/main/02.md)
- [NUT-03](https://github.com/cashubtc/nuts/blob/main/03.md)
- [NUT-04](https://github.com/cashubtc/nuts/blob/main/04.md)
- [NUT-05](https://github.com/cashubtc/nuts/blob/main/05.md)
- [NUT-06](https://github.com/cashubtc/nuts/blob/main/06.md)
- [NUT-07](https://github.com/cashubtc/nuts/blob/main/07.md)
- [NUT-08](https://github.com/cashubtc/nuts/blob/main/08.md)
- [NUT-09](https://github.com/cashubtc/nuts/blob/main/09.md)
Supported token formats:
## Supported token formats:
- [ ] v1 read
- [ ] v2 read (deprecated)
- [ ] v3 read/write
- v1 read (deprecated)
- v2 read (deprecated)
- ✅ [v3](https://github.com/cashubtc/nuts/blob/main/00.md#023---v3-tokens) read/write
## License

View File

@@ -1,5 +1,6 @@
use bitcoin::Amount;
use lightning_invoice::Invoice;
use serde_json::Value;
use url::Url;
use crate::{
@@ -13,7 +14,7 @@ use crate::{
};
pub struct CashuMint {
url: Url,
pub url: Url,
}
impl CashuMint {
@@ -98,24 +99,18 @@ impl CashuMint {
}
/// Split Token [NUT-06]
pub async fn split(
&self,
amount: Amount,
proofs: Vec<Proof>,
outputs: Vec<BlindedMessage>,
) -> Result<SplitResponse, Error> {
pub async fn split(&self, split_request: SplitRequest) -> Result<SplitResponse, Error> {
let url = self.url.join("split")?;
let request = SplitRequest {
amount,
proofs,
outputs,
};
Ok(minreq::post(url)
.with_json(&request)?
let res = minreq::post(url)
.with_json(&split_request)?
.send()?
.json::<SplitResponse>()?)
.json::<Value>()?;
// TODO: need to handle response error
// specfically token already spent
Ok(serde_json::from_value(res).unwrap())
}
/// Spendable check [NUT-07]

View File

@@ -1,9 +1,15 @@
use std::str::FromStr;
use bitcoin::Amount;
use crate::{
cashu_mint::CashuMint,
dhke::construct_proof,
error::Error,
types::{MintKeys, Proof, ProofsStatus, RequestMintResponse},
types::{
BlindedMessages, MintKeys, Proof, ProofsStatus, RequestMintResponse, SplitPayload,
SplitRequest, TokenData,
},
};
pub struct CashuWallet {
@@ -20,6 +26,7 @@ impl CashuWallet {
pub async fn check_proofs_spent(&self, proofs: Vec<Proof>) -> Result<ProofsStatus, Error> {
let spendable = self.mint.check_spendable(&proofs).await?;
// Seperate proofs in spent and unspent based on mint response
let (spendable, spent): (Vec<_>, Vec<_>) = proofs
.iter()
.zip(spendable.spendable.iter())
@@ -40,4 +47,85 @@ impl CashuWallet {
pub async fn check_fee(&self, invoice: lightning_invoice::Invoice) -> Result<Amount, Error> {
Ok(self.mint.check_fees(invoice).await?.fee)
}
/// Receive
pub async fn receive(&self, encoded_token: &str) -> Result<Vec<Proof>, Error> {
let token_data = TokenData::from_str(encoded_token)?;
let mut proofs = vec![];
for token in token_data.token {
if token.proofs.is_empty() {
continue;
}
let keys = if token.mint.eq(&self.mint.url) {
self.keys.clone()
} else {
// TODO:
println!("No match");
self.keys.clone()
// CashuMint::new(token.mint).get_keys().await.unwrap()
};
// Sum amount of all proofs
let amount = token
.proofs
.iter()
.fold(Amount::ZERO, |acc, p| acc + p.amount);
let split_payload = self
.create_split(Amount::ZERO, amount, token.proofs)
.await?;
let split_response = self.mint.split(split_payload.split_payload).await?;
// Proof to keep
let keep_proofs = construct_proof(
split_response.fst,
split_payload.keep_blinded_messages.rs,
split_payload.keep_blinded_messages.secrets,
&keys,
)?;
// Proofs to send
let send_proofs = construct_proof(
split_response.snd,
split_payload.send_blinded_messages.rs,
split_payload.send_blinded_messages.secrets,
&keys,
)?;
proofs.push(keep_proofs);
proofs.push(send_proofs);
}
Ok(proofs.iter().flatten().cloned().collect())
}
pub async fn create_split(
&self,
keep_amount: Amount,
send_amount: Amount,
proofs: Vec<Proof>,
) -> Result<SplitPayload, Error> {
let keep_blinded_messages = BlindedMessages::random(keep_amount)?;
let send_blinded_messages = BlindedMessages::random(send_amount)?;
let outputs = {
let mut outputs = keep_blinded_messages.blinded_messages.clone();
outputs.extend(send_blinded_messages.blinded_messages.clone());
outputs
};
let split_payload = SplitRequest {
amount: send_amount,
proofs,
outputs,
};
Ok(SplitPayload {
keep_blinded_messages,
send_blinded_messages,
split_payload,
})
}
}

View File

@@ -1,14 +1,16 @@
//! Diffie-Hellmann key exchange
use std::str::FromStr;
use bitcoin_hashes::sha256;
use bitcoin_hashes::Hash;
use secp256k1::rand::rngs::OsRng;
use secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey};
use crate::error::Error;
// use crate::types::MintKeys;
// use crate::types::Promise;
// use crate::types::Proof;
use crate::types::MintKeys;
use crate::types::Promise;
use crate::types::Proof;
/// Hash to Curve
pub fn hash_to_curve(secret_message: &[u8]) -> Result<PublicKey, Error> {
@@ -62,17 +64,32 @@ pub fn unblind_message(
Ok(unblinded_key)
}
/*
/// Construct Proof
pub fn construct_proof(
promises: Vec<Promise>,
rs: Vec<SecretKey>,
secrets: Vec<String>,
keys: MintKeys,
secrets: Vec<Vec<u8>>,
keys: &MintKeys,
) -> Result<Vec<Proof>, Error> {
todo!()
let mut proofs = vec![];
for (i, promise) in promises.into_iter().enumerate() {
let blinded_c = PublicKey::from_str(&promise.c)?;
let a: PublicKey = PublicKey::from_str(keys.0.get(&promise.amount.to_sat()).unwrap())?;
let unblinded_signature = unblind_message(blinded_c, rs[i], a)?;
let proof = Proof {
id: Some(promise.id),
amount: promise.amount,
secret: hex::encode(&secrets[i]),
c: unblinded_signature.to_string(),
script: None,
};
proofs.push(proof);
}
Ok(proofs)
}
*/
#[cfg(test)]
mod tests {

View File

@@ -79,6 +79,13 @@ impl BlindedMessages {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SplitPayload {
pub keep_blinded_messages: BlindedMessages,
pub send_blinded_messages: BlindedMessages,
pub split_payload: SplitRequest,
}
/// Promise (BlindedSignature) [NIP-00]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Promise {
@@ -187,9 +194,9 @@ pub struct SplitRequest {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SplitResponse {
/// Promises to keep
pub fst: Vec<BlindedMessage>,
pub fst: Vec<Promise>,
/// Promises to send
pub snd: Vec<BlindedMessage>,
pub snd: Vec<Promise>,
}
/// Check spendabale request [NUT-07]

View File

@@ -6,7 +6,7 @@ use bitcoin::Amount;
use lightning_invoice::Invoice;
use url::Url;
use cashu_rs::{cashu_mint::CashuMint, types::BlindedMessages};
use cashu_rs::{cashu_mint::CashuMint, cashu_wallet::CashuWallet, types::BlindedMessages};
const MINTURL: &str = "https://legend.lnbits.com/cashu/api/v1/SKvHRus9dmjWHhstHrsazW/";
@@ -68,6 +68,21 @@ async fn test_check_fees() {
println!("{fee:?}");
}
#[ignore]
#[tokio::test]
async fn test_receive() {
let url = Url::from_str(MINTURL).unwrap();
let mint = CashuMint::new(url);
let mint_keys = mint.get_keys().await.unwrap();
let wallet = CashuWallet::new(mint, mint_keys);
// FIXME: Have to manully paste an unspent token
let token = "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6Im9DV2NkWXJyeVRrUiIsImFtb3VudCI6MiwiQyI6IjAzNmY1NTU0ZDMyZDg3MGFjMzZjMDIwOGNiMDlkZmJmZjNhN2RkZTUyNzMwOTNjYzk3ZjE2NDBkNjYyZTgyMmMyMCIsInNlY3JldCI6ImtuRlhvelpjUG5YK1l4dytIcmV3VVlXRHU2ZFVFbkY0KzRUTkRIN010V289In1dLCJtaW50IjoiaHR0cHM6Ly9sZWdlbmQubG5iaXRzLmNvbS9jYXNodS9hcGkvdjEvU0t2SFJ1czlkbWpXSGhzdEhyc2F6VyJ9XX0=";
let prom = wallet.receive(token).await.unwrap();
println!("{:?}", prom);
}
#[ignore]
#[tokio::test]
async fn test_get_mint_info() {