Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nut10 and Nut11 #37

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/cashu-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ nut09 = ["cashu/nut09"]


[dependencies]
cashu = { path = "../cashu" }
cashu = { path = "../cashu", default-features = false, features = ["nut08"] }
serde = { workspace = true }
serde_json = { workspace = true }
url = { workspace = true }
Expand Down
4 changes: 2 additions & 2 deletions crates/cashu-sdk/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

use async_trait::async_trait;
use cashu::nuts::nut00::wallet::BlindedMessages;
use cashu::nuts::nut00::{BlindedMessage, Proof};
use cashu::nuts::nut01::Keys;
use cashu::nuts::nut03::RequestMintResponse;
use cashu::nuts::nut04::PostMintResponse;
Expand All @@ -13,7 +12,8 @@ use cashu::nuts::nut07::CheckSpendableResponse;
use cashu::nuts::nut08::MeltResponse;
#[cfg(feature = "nut09")]
use cashu::nuts::MintInfo;
use cashu::nuts::*;
use cashu::nuts::{Proof, *, *};
use cashu::url::UncheckedUrl;
use cashu::{utils, Amount};
use serde::{Deserialize, Serialize};
use thiserror::Error;
Expand Down
3 changes: 2 additions & 1 deletion crates/cashu/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ description = "Cashu rust wallet and mint library"
default = ["mint", "wallet", "all-nuts"]
mint = []
wallet = []
all-nuts = ["nut07", "nut08", "nut09"]
all-nuts = ["nut07", "nut08", "nut09", "nut10"]
nut07 = []
nut08 = []
nut09 = []
nut10 = []


[dependencies]
Expand Down
1 change: 0 additions & 1 deletion crates/cashu/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
pub mod amount;
#[cfg(feature = "wallet")]
pub mod dhke;
pub mod error;
pub mod nuts;
Expand Down
9 changes: 9 additions & 0 deletions crates/cashu/src/nuts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,13 @@ pub use nut08::{MeltRequest, MeltResponse};
#[cfg(feature = "nut09")]
pub use nut09::MintInfo;

#[cfg(feature = "nut12")]
pub mod nut12;

#[cfg(feature = "nut12")]
pub use nut12::{BlindedSignature, DleqProof, Proof};
#[cfg(feature = "nut10")]
pub mod nut10;

/// List of proofs
pub type Proofs = Vec<Proof>;
3 changes: 1 addition & 2 deletions crates/cashu/src/nuts/nut06.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
// https://github.com/cashubtc/nuts/blob/main/06.md
use serde::{Deserialize, Serialize};

use super::nut00::BlindedSignature;
#[cfg(feature = "wallet")]
use crate::nuts::BlindedMessages;
use crate::nuts::{BlindedMessage, Proofs};
use crate::nuts::{BlindedMessage, BlindedSignature, Proofs};
use crate::Amount;

#[cfg(feature = "wallet")]
Expand Down
2 changes: 1 addition & 1 deletion crates/cashu/src/nuts/nut08.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
use lightning_invoice::Bolt11Invoice;
use serde::{Deserialize, Serialize};

use super::{BlindedMessage, BlindedSignature, Proofs};
use crate::error::Error;
use crate::nuts::{BlindedMessage, BlindedSignature, Proofs};
use crate::Amount;

/// Melt Request [NUT-08]
Expand Down
111 changes: 111 additions & 0 deletions crates/cashu/src/nuts/nut10.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use serde::ser::SerializeTuple;
use serde::{Deserialize, Serialize, Serializer};

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum Kind {
/// NUT-11 P2PK
#[default]
P2PK,
}

#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq, Serialize)]
pub struct SecretData {
/// Unique random string
pub nonce: String,
/// Expresses the spending condition specific to each kind
pub data: String,
/// Additional data committed to and can be used for feature extensions
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<Vec<String>>>,
}

#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
pub struct Secret {
/// Kind of the spending condition
pub kind: Kind,
pub secret_data: SecretData,
}

impl Serialize for Secret {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Create a tuple representing the struct fields
let secret_tuple = (&self.kind, &self.secret_data);

// Serialize the tuple as a JSON array
let mut s = serializer.serialize_tuple(2)?;

s.serialize_element(&secret_tuple.0)?;
s.serialize_element(&secret_tuple.1)?;
s.end()
}
}

#[cfg(test)]
mod tests {
use std::assert_eq;

use super::*;

#[test]
fn test_secret_deserialize() {
let secret_str = r#"[
"P2PK",
{
"nonce": "5d11913ee0f92fefdc82a6764fd2457a",
"data": "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198",
"tags": [["key", "value1", "value2"]]
}
]"#
.to_string();

let secret_ser: Secret = serde_json::from_str(&secret_str).unwrap();
let secret = Secret {
kind: Kind::P2PK,
secret_data: SecretData {
nonce: "5d11913ee0f92fefdc82a6764fd2457a".to_string(),
data: "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198"
.to_string(),
tags: Some(vec![vec![
"key".to_string(),
"value1".to_string(),
"value2".to_string(),
]]),
},
};

assert_eq!(secret, secret_ser);
}

#[test]
fn test_secret_serialize() {
let secret = Secret {
kind: Kind::P2PK,
secret_data: SecretData {
nonce: "5d11913ee0f92fefdc82a6764fd2457a".to_string(),
data: "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198"
.to_string(),
tags: Some(vec![vec![
"key".to_string(),
"value1".to_string(),
"value2".to_string(),
]]),
},
};

let secret_str = r#"["P2PK",{"nonce":"5d11913ee0f92fefdc82a6764fd2457a","data":"026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198","tags":[["key","value1","value2"]]}]"#;

assert_eq!(serde_json::to_string(&secret).unwrap(), secret_str);
}

#[test]
fn test_secret_roundtrip() {
let secret_str = r#"["P2PK",{"nonce":"5d11913ee0f92fefdc82a6764fd2457a","data":"026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198","tags":[["key","value1","value2"]]}]"#;

let secret_ser: Secret = serde_json::from_str(secret_str).unwrap();

assert_eq!(serde_json::to_string(&secret_ser).unwrap(), secret_str)
}
}
79 changes: 79 additions & 0 deletions crates/cashu/src/nuts/nut11.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//! Pay to Public Key (P2PK)
// https://github.com/cashubtc/nuts/blob/main/11.md

use serde::{Deserialize, Serialize};

use super::nut01::PublicKey;
use super::nut02::Id;
use super::nut10::Secret;
use crate::Amount;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Signatures {
signatures: Vec<String>,
}

/// Proofs [NUT-11]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Proof {
/// Amount in satoshi
pub amount: Amount,
/// NUT-10 Secret
pub secret: Secret,
/// Unblinded signature
#[serde(rename = "C")]
pub c: PublicKey,
/// `Keyset id`
pub id: Option<Id>,
/// Witness
pub witness: Vec<Signatures>,
}

#[cfg(test)]
mod tests {

use std::assert_eq;

use super::*;
use crate::nuts::nut10::{Kind, SecretData};

#[test]
fn test_proof_serialize() {
let proof = r#"[{"id":"DSAl9nvvyfva","amount":8,"C":"02ac910bef28cbe5d7325415d5c263026f15f9b967a079ca9779ab6e5c2db133a7","secret":["P2PK",{"nonce":"5d11913ee0f92fefdc82a6764fd2457a","data":"026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198"}],"witness":{"signatures":["c43d0090be59340a6364dc1340876211f2173d6a21c391115adf097adb6ea0a3ddbe7fd81b4677281decc77be09c0359faa77416025130e487f8b9169eb0c609"]}}"#;

let proof: Proof = serde_json::from_str(proof).unwrap();

assert_eq!(
proof.clone().id.unwrap(),
Id::try_from_base64("DSAl9nvvyfva").unwrap()
);
}

#[test]
fn test_proof_serualize() {
let secret = Secret {
kind: Kind::P2PK,
secret_data: SecretData {
nonce: "5d11913ee0f92fefdc82a6764fd2457a".to_string(),
data: "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198"
.to_string(),
tags: None,
},
};

let proof = Proof {
amount: Amount::from_sat(8),
secret,
c: PublicKey::from_hex("02ac910bef28cbe5d7325415d5c263026f15f9b967a079ca9779ab6e5c2db133a7".to_string()).unwrap(),
id: Some(Id::try_from_base64("DSAl9nvvyfva").unwrap()),
witness: vec![Signatures {
signatures: vec!["c43d0090be59340a6364dc1340876211f2173d6a21c391115adf097adb6ea0a3ddbe7fd81b4677281decc77be09c0359faa77416025130e487f8b9169eb0c609".to_string()]
}]

};

let proof_str = r#"{"amount":8,"secret":["P2PK",{"nonce":"5d11913ee0f92fefdc82a6764fd2457a","data":"026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198"}],"C":"02ac910bef28cbe5d7325415d5c263026f15f9b967a079ca9779ab6e5c2db133a7","id":"DSAl9nvvyfva","witness":[{"signatures":["c43d0090be59340a6364dc1340876211f2173d6a21c391115adf097adb6ea0a3ddbe7fd81b4677281decc77be09c0359faa77416025130e487f8b9169eb0c609"]}]}"#;

assert_eq!(serde_json::to_string(&proof).unwrap(), proof_str);
}
}
26 changes: 26 additions & 0 deletions crates/cashu/src/nuts/nut12.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//! NUT-12: Offline ecash signature validation
// TODO: link to nut

use serde::{Deserialize, Serialize};

use super::nut01::PublicKey;
use super::nut02::Id;
use crate::Amount;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DleqProof {
e: String,
s: String,
r: Option<String>,
}

/// Promise (BlindedSignature) [NUT-12] with DLEQ proof
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlindedSignature {
pub id: Id,
pub amount: Amount,
/// blinded signature (C_) on the secret message `B_` of [BlindedMessage]
#[serde(rename = "C_")]
pub c: PublicKey,
pub dleq: Option<DleqProof>,
}