mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-06 13:45:53 +01:00
refactor Nut10SecretRequest and add tests with provided test vectors (#900)
* refactor Nut10SecretRequest and add tests with provided test vectors
This commit is contained in:
@@ -7,5 +7,5 @@ pub mod transport;
|
||||
|
||||
pub use error::Error;
|
||||
pub use payment_request::{PaymentRequest, PaymentRequestBuilder, PaymentRequestPayload};
|
||||
pub use secret::{Nut10SecretRequest, SecretDataRequest};
|
||||
pub use secret::Nut10SecretRequest;
|
||||
pub use transport::{Transport, TransportBuilder, TransportType};
|
||||
|
||||
@@ -43,6 +43,7 @@ pub struct PaymentRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub transports: Option<Vec<Transport>>,
|
||||
/// Nut10
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub nut10: Option<Nut10SecretRequest>,
|
||||
}
|
||||
|
||||
@@ -358,14 +359,8 @@ mod tests {
|
||||
|
||||
// Check round-trip conversion
|
||||
assert_eq!(converted_back.kind, secret_request.kind);
|
||||
assert_eq!(
|
||||
converted_back.secret_data.data,
|
||||
secret_request.secret_data.data
|
||||
);
|
||||
assert_eq!(
|
||||
converted_back.secret_data.tags,
|
||||
secret_request.secret_data.tags
|
||||
);
|
||||
assert_eq!(converted_back.data, secret_request.data);
|
||||
assert_eq!(converted_back.tags, secret_request.tags);
|
||||
|
||||
// Test in PaymentRequest builder
|
||||
let payment_request = PaymentRequest::builder()
|
||||
@@ -458,9 +453,231 @@ mod tests {
|
||||
// Verify the P2PK data was preserved correctly
|
||||
if let Some(nut10_secret) = decoded_request.nut10 {
|
||||
assert_eq!(nut10_secret.kind, Kind::P2PK);
|
||||
assert_eq!(nut10_secret.secret_data.data, pubkey_hex);
|
||||
assert_eq!(nut10_secret.data, pubkey_hex);
|
||||
} else {
|
||||
panic!("NUT10 secret data missing in decoded payment request");
|
||||
}
|
||||
}
|
||||
|
||||
/// Test vectors from NUT-18 specification
|
||||
/// https://github.com/cashubtc/nuts/blob/main/tests/18-tests.md
|
||||
|
||||
#[test]
|
||||
fn test_basic_payment_request() {
|
||||
// Basic payment request with required fields
|
||||
let json = r#"{
|
||||
"i": "b7a90176",
|
||||
"a": 10,
|
||||
"u": "sat",
|
||||
"m": ["https://8333.space:3338"],
|
||||
"t": [
|
||||
{
|
||||
"t": "nostr",
|
||||
"a": "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5",
|
||||
"g": [["n", "17"]]
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
|
||||
let expected_encoded = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF3aHR0cHM6Ly84MzMzLnNwYWNlOjMzMzg=";
|
||||
|
||||
// Parse the JSON into a PaymentRequest
|
||||
let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
|
||||
let payment_request_cloned = payment_request.clone();
|
||||
|
||||
// Verify the payment request fields
|
||||
assert_eq!(
|
||||
payment_request_cloned.payment_id.as_ref().unwrap(),
|
||||
"b7a90176"
|
||||
);
|
||||
assert_eq!(payment_request_cloned.amount.unwrap(), Amount::from(10));
|
||||
assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
|
||||
assert_eq!(
|
||||
payment_request_cloned.mints.unwrap(),
|
||||
vec![MintUrl::from_str("https://8333.space:3338").unwrap()]
|
||||
);
|
||||
|
||||
let transport = payment_request.transports.as_ref().unwrap();
|
||||
let transport = transport.first().unwrap();
|
||||
assert_eq!(transport._type, TransportType::Nostr);
|
||||
assert_eq!(transport.target, "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5");
|
||||
assert_eq!(
|
||||
transport.tags,
|
||||
Some(vec![vec!["n".to_string(), "17".to_string()]])
|
||||
);
|
||||
|
||||
// Test encoding - the encoded form should match the expected output
|
||||
let encoded = payment_request.to_string();
|
||||
|
||||
// For now, let's verify it can be decoded back correctly
|
||||
let decoded = PaymentRequest::from_str(&encoded).unwrap();
|
||||
assert_eq!(payment_request, decoded);
|
||||
|
||||
// Test decoding the expected encoded string
|
||||
let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
|
||||
assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "b7a90176");
|
||||
assert_eq!(decoded_from_spec.amount.unwrap(), Amount::from(10));
|
||||
assert_eq!(decoded_from_spec.unit.unwrap(), CurrencyUnit::Sat);
|
||||
assert_eq!(
|
||||
decoded_from_spec.mints.unwrap(),
|
||||
vec![MintUrl::from_str("https://8333.space:3338").unwrap()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nostr_transport_payment_request() {
|
||||
// Nostr transport payment request with multiple mints
|
||||
let json = r#"{
|
||||
"i": "f92a51b8",
|
||||
"a": 100,
|
||||
"u": "sat",
|
||||
"m": ["https://mint1.example.com", "https://mint2.example.com"],
|
||||
"t": [
|
||||
{
|
||||
"t": "nostr",
|
||||
"a": "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq28spj3",
|
||||
"g": [["n", "17"], ["n", "9735"]]
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
|
||||
let expected_encoded = "creqApWF0gaNhdGVub3N0cmFheD9ucHViMXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXEyOHNwajNhZ4KCYW5iMTeCYW5kOTczNWFpaGY5MmE1MWI4YWEYZGF1Y3NhdGFtgngZaHR0cHM6Ly9taW50MS5leGFtcGxlLmNvbXgZaHR0cHM6Ly9taW50Mi5leGFtcGxlLmNvbQ==";
|
||||
|
||||
// Parse the JSON into a PaymentRequest
|
||||
let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
|
||||
let payment_request_cloned = payment_request.clone();
|
||||
|
||||
// Verify the payment request fields
|
||||
assert_eq!(
|
||||
payment_request_cloned.payment_id.as_ref().unwrap(),
|
||||
"f92a51b8"
|
||||
);
|
||||
assert_eq!(payment_request_cloned.amount.unwrap(), Amount::from(100));
|
||||
assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
|
||||
assert_eq!(
|
||||
payment_request_cloned.mints.unwrap(),
|
||||
vec![
|
||||
MintUrl::from_str("https://mint1.example.com").unwrap(),
|
||||
MintUrl::from_str("https://mint2.example.com").unwrap()
|
||||
]
|
||||
);
|
||||
|
||||
let transport = payment_request_cloned.transports.unwrap();
|
||||
let transport = transport.first().unwrap();
|
||||
assert_eq!(transport._type, TransportType::Nostr);
|
||||
assert_eq!(
|
||||
transport.target,
|
||||
"npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq28spj3"
|
||||
);
|
||||
assert_eq!(
|
||||
transport.tags,
|
||||
Some(vec![
|
||||
vec!["n".to_string(), "17".to_string()],
|
||||
vec!["n".to_string(), "9735".to_string()]
|
||||
])
|
||||
);
|
||||
|
||||
// Test round-trip serialization
|
||||
let encoded = payment_request.to_string();
|
||||
let decoded = PaymentRequest::from_str(&encoded).unwrap();
|
||||
assert_eq!(payment_request, decoded);
|
||||
|
||||
// Test decoding the expected encoded string
|
||||
let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
|
||||
assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "f92a51b8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_minimal_payment_request() {
|
||||
// Minimal payment request with only required fields
|
||||
let json = r#"{
|
||||
"i": "7f4a2b39",
|
||||
"u": "sat",
|
||||
"m": ["https://mint.example.com"]
|
||||
}"#;
|
||||
|
||||
let expected_encoded =
|
||||
"creqAo2FpaDdmNGEyYjM5YXVjc2F0YW2BeBhodHRwczovL21pbnQuZXhhbXBsZS5jb20=";
|
||||
|
||||
// Parse the JSON into a PaymentRequest
|
||||
let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
|
||||
let payment_request_cloned = payment_request.clone();
|
||||
|
||||
// Verify the payment request fields
|
||||
assert_eq!(
|
||||
payment_request_cloned.payment_id.as_ref().unwrap(),
|
||||
"7f4a2b39"
|
||||
);
|
||||
assert_eq!(payment_request_cloned.amount, None);
|
||||
assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
|
||||
assert_eq!(
|
||||
payment_request_cloned.mints.unwrap(),
|
||||
vec![MintUrl::from_str("https://mint.example.com").unwrap()]
|
||||
);
|
||||
assert_eq!(payment_request_cloned.transports, None);
|
||||
|
||||
// Test round-trip serialization
|
||||
let encoded = payment_request.to_string();
|
||||
let decoded = PaymentRequest::from_str(&encoded).unwrap();
|
||||
assert_eq!(payment_request, decoded);
|
||||
|
||||
// Test decoding the expected encoded string
|
||||
let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
|
||||
assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "7f4a2b39");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nut10_locking_payment_request() {
|
||||
// Payment request with NUT-10 P2PK locking
|
||||
let json = r#"{
|
||||
"i": "c9e45d2a",
|
||||
"a": 500,
|
||||
"u": "sat",
|
||||
"m": ["https://mint.example.com"],
|
||||
"nut10": {
|
||||
"k": "P2PK",
|
||||
"d": "02c3b5bb27e361457c92d93d78dd73d3d53732110b2cfe8b50fbc0abc615e9c331",
|
||||
"t": [["timeout", "3600"]]
|
||||
}
|
||||
}"#;
|
||||
|
||||
let expected_encoded = "creqApWFpaGM5ZTQ1ZDJhYWEZAfRhdWNzYXRhbYF4GGh0dHBzOi8vbWludC5leGFtcGxlLmNvbWVudXQxMKNha2RQMlBLYWR4QjAyYzNiNWJiMjdlMzYxNDU3YzkyZDkzZDc4ZGQ3M2QzZDUzNzMyMTEwYjJjZmU4YjUwZmJjMGFiYzYxNWU5YzMzMWF0gYJndGltZW91dGQzNjAw";
|
||||
|
||||
// Parse the JSON into a PaymentRequest
|
||||
let payment_request: PaymentRequest = serde_json::from_str(json).unwrap();
|
||||
let payment_request_cloned = payment_request.clone();
|
||||
|
||||
// Verify the payment request fields
|
||||
assert_eq!(
|
||||
payment_request_cloned.payment_id.as_ref().unwrap(),
|
||||
"c9e45d2a"
|
||||
);
|
||||
assert_eq!(payment_request_cloned.amount.unwrap(), Amount::from(500));
|
||||
assert_eq!(payment_request_cloned.unit.unwrap(), CurrencyUnit::Sat);
|
||||
assert_eq!(
|
||||
payment_request_cloned.mints.unwrap(),
|
||||
vec![MintUrl::from_str("https://mint.example.com").unwrap()]
|
||||
);
|
||||
|
||||
// Test NUT-10 locking
|
||||
let nut10 = payment_request_cloned.nut10.unwrap();
|
||||
assert_eq!(nut10.kind, Kind::P2PK);
|
||||
assert_eq!(
|
||||
nut10.data,
|
||||
"02c3b5bb27e361457c92d93d78dd73d3d53732110b2cfe8b50fbc0abc615e9c331"
|
||||
);
|
||||
assert_eq!(
|
||||
nut10.tags,
|
||||
Some(vec![vec!["timeout".to_string(), "3600".to_string()]])
|
||||
);
|
||||
|
||||
// Test round-trip serialization
|
||||
let encoded = payment_request.to_string();
|
||||
let decoded = PaymentRequest::from_str(&encoded).unwrap();
|
||||
assert_eq!(payment_request, decoded);
|
||||
|
||||
// Test decoding the expected encoded string
|
||||
let decoded_from_spec = PaymentRequest::from_str(expected_encoded).unwrap();
|
||||
assert_eq!(decoded_from_spec.payment_id.as_ref().unwrap(), "c9e45d2a");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,21 @@
|
||||
//! Secret types for NUT-18: Payment Requests
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use serde::de::{self, Deserializer, SeqAccess, Visitor};
|
||||
use serde::ser::{SerializeTuple, Serializer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::nuts::nut10::Kind;
|
||||
use crate::nuts::{Nut10Secret, SpendingConditions};
|
||||
|
||||
/// Secret Data without nonce for payment requests
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct SecretDataRequest {
|
||||
/// 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>>>,
|
||||
}
|
||||
|
||||
/// Nut10Secret without nonce for payment requests
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Nut10SecretRequest {
|
||||
/// Kind of the spending condition
|
||||
#[serde(rename = "k")]
|
||||
pub kind: Kind,
|
||||
/// Secret Data without nonce
|
||||
pub secret_data: SecretDataRequest,
|
||||
/// Secret data
|
||||
#[serde(rename = "d")]
|
||||
pub data: String,
|
||||
/// Additional data committed to and can be used for feature extensions
|
||||
#[serde(rename = "t", skip_serializing_if = "Option::is_none")]
|
||||
pub tags: Option<Vec<Vec<String>>>,
|
||||
}
|
||||
|
||||
impl Nut10SecretRequest {
|
||||
@@ -35,32 +25,27 @@ impl Nut10SecretRequest {
|
||||
S: Into<String>,
|
||||
V: Into<Vec<Vec<String>>>,
|
||||
{
|
||||
let secret_data = SecretDataRequest {
|
||||
Self {
|
||||
kind,
|
||||
data: data.into(),
|
||||
tags: tags.map(|v| v.into()),
|
||||
};
|
||||
|
||||
Self { kind, secret_data }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Nut10Secret> for Nut10SecretRequest {
|
||||
fn from(secret: Nut10Secret) -> Self {
|
||||
let secret_data = SecretDataRequest {
|
||||
data: secret.secret_data().data().to_string(),
|
||||
tags: secret.secret_data().tags().cloned(),
|
||||
};
|
||||
|
||||
Self {
|
||||
kind: secret.kind(),
|
||||
secret_data,
|
||||
data: secret.secret_data().data().to_string(),
|
||||
tags: secret.secret_data().tags().cloned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Nut10SecretRequest> for Nut10Secret {
|
||||
fn from(value: Nut10SecretRequest) -> Self {
|
||||
Self::new(value.kind, value.secret_data.data, value.secret_data.tags)
|
||||
Self::new(value.kind, value.data, value.tags)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,61 +62,67 @@ impl From<SpendingConditions> for Nut10SecretRequest {
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Nut10SecretRequest {
|
||||
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);
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Serialize the tuple as a JSON array
|
||||
let mut s = serializer.serialize_tuple(2)?;
|
||||
#[test]
|
||||
fn test_nut10_secret_request_serialization() {
|
||||
let request = Nut10SecretRequest::new(
|
||||
Kind::P2PK,
|
||||
"026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198",
|
||||
Some(vec![vec!["key".to_string(), "value".to_string()]]),
|
||||
);
|
||||
|
||||
s.serialize_element(&secret_tuple.0)?;
|
||||
s.serialize_element(&secret_tuple.1)?;
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
|
||||
// Custom visitor for deserializing Secret
|
||||
struct SecretVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for SecretVisitor {
|
||||
type Value = Nut10SecretRequest;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a tuple with two elements: [Kind, SecretData]")
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
// Deserialize the kind (first element)
|
||||
let kind = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| de::Error::invalid_length(0, &self))?;
|
||||
|
||||
// Deserialize the secret_data (second element)
|
||||
let secret_data = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| de::Error::invalid_length(1, &self))?;
|
||||
|
||||
// Make sure there are no additional elements
|
||||
if seq.next_element::<serde::de::IgnoredAny>()?.is_some() {
|
||||
return Err(de::Error::invalid_length(3, &self));
|
||||
}
|
||||
|
||||
Ok(Nut10SecretRequest { kind, secret_data })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Nut10SecretRequest {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_seq(SecretVisitor)
|
||||
let json = serde_json::to_string(&request).unwrap();
|
||||
|
||||
// Verify json has abbreviated field names
|
||||
assert!(json.contains(r#""k":"P2PK""#));
|
||||
assert!(json.contains(r#""d":"026562"#));
|
||||
assert!(json.contains(r#""t":[["key","#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_serialization() {
|
||||
let original = Nut10SecretRequest {
|
||||
kind: Kind::P2PK,
|
||||
data: "test_data".into(),
|
||||
tags: Some(vec![vec!["key".to_string(), "value".to_string()]]),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&original).unwrap();
|
||||
let decoded: Nut10SecretRequest = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(original, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_nut10_secret() {
|
||||
let secret = Nut10Secret::new(
|
||||
Kind::P2PK,
|
||||
"test_data",
|
||||
Some(vec![vec!["key".to_string(), "value".to_string()]]),
|
||||
);
|
||||
|
||||
let request: Nut10SecretRequest = secret.clone().into();
|
||||
|
||||
assert_eq!(request.kind, secret.kind());
|
||||
assert_eq!(request.data, secret.secret_data().data());
|
||||
assert_eq!(request.tags, secret.secret_data().tags().cloned());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_nut10_secret() {
|
||||
let request = Nut10SecretRequest {
|
||||
kind: Kind::HTLC,
|
||||
data: "test_hash".into(),
|
||||
tags: None,
|
||||
};
|
||||
|
||||
let secret: Nut10Secret = request.clone().into();
|
||||
|
||||
assert_eq!(secret.kind(), request.kind);
|
||||
assert_eq!(secret.secret_data().data(), request.data);
|
||||
assert_eq!(secret.secret_data().tags(), request.tags.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user