fix: nut18 payment request encoding/decoding (#758)

* fix: nut18 payment request encoding/decoding

* refactor: reorder nut18fns

* refactor: reorder nut18
This commit is contained in:
thesimplekid
2025-05-18 10:04:02 +01:00
committed by GitHub
parent 70944500fc
commit 3920c6f9bc
6 changed files with 530 additions and 254 deletions

View File

@@ -2,8 +2,10 @@
//!
//! <https://github.com/cashubtc/nuts/blob/main/10.md>
use std::fmt;
use std::str::FromStr;
use serde::de::{self, Deserializer, SeqAccess, Visitor};
use serde::ser::SerializeTuple;
use serde::{Deserialize, Serialize, Serializer};
use thiserror::Error;
@@ -41,7 +43,7 @@ pub struct SecretData {
}
/// NUT10 Secret
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Secret {
/// Kind of the spending condition
pub kind: Kind,
@@ -94,9 +96,52 @@ impl TryFrom<Secret> for crate::secret::Secret {
}
}
// Custom visitor for deserializing Secret
struct SecretVisitor;
impl<'de> Visitor<'de> for SecretVisitor {
type Value = Secret;
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(Secret { kind, secret_data })
}
}
impl<'de> Deserialize<'de> for Secret {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_seq(SecretVisitor)
}
}
#[cfg(test)]
mod tests {
use std::assert_eq;
use std::str::FromStr;
use super::*;
@@ -120,4 +165,69 @@ mod tests {
assert_eq!(serde_json::to_string(&secret).unwrap(), secret_str);
}
#[test]
fn test_secret_round_trip_serialization() {
// Create a Secret instance
let original_secret = Secret {
kind: Kind::P2PK,
secret_data: SecretData {
nonce: "5d11913ee0f92fefdc82a6764fd2457a".to_string(),
data: "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198"
.to_string(),
tags: None,
},
};
// Serialize the Secret to JSON string
let serialized = serde_json::to_string(&original_secret).unwrap();
// Deserialize directly back to Secret using serde
let deserialized_secret: Secret = serde_json::from_str(&serialized).unwrap();
// Verify the direct serde serialization/deserialization round trip works
assert_eq!(original_secret, deserialized_secret);
// Also verify that the conversion to crate::secret::Secret works
let cashu_secret = crate::secret::Secret::from_str(&serialized).unwrap();
let deserialized_from_cashu: Secret = TryFrom::try_from(&cashu_secret).unwrap();
assert_eq!(original_secret, deserialized_from_cashu);
}
#[test]
fn test_htlc_secret_round_trip() {
// The reference BOLT11 invoice is:
// lnbc100n1p5z3a63pp56854ytysg7e5z9fl3w5mgvrlqjfcytnjv8ff5hm5qt6gl6alxesqdqqcqzzsxqyz5vqsp5p0x0dlhn27s63j4emxnk26p7f94u0lyarnfp5yqmac9gzy4ngdss9qxpqysgqne3v0hnzt2lp0hc69xpzckk0cdcar7glvjhq60lsrfe8gejdm8c564prrnsft6ctxxyrewp4jtezrq3gxxqnfjj0f9tw2qs9y0lslmqpfu7et9
// Payment hash (typical 32 byte hash in hex format)
let payment_hash = "5c23fc3aec9d985bd5fc88ca8bceaccc52cf892715dd94b42b84f1b43350751e";
// Create a Secret instance with HTLC kind
let original_secret = Secret {
kind: Kind::HTLC,
secret_data: SecretData {
nonce: "7a9128b3f9612549f9278958337a5d7f".to_string(),
data: payment_hash.to_string(),
tags: None,
},
};
// Serialize the Secret to JSON string
let serialized = serde_json::to_string(&original_secret).unwrap();
// Validate serialized format
let expected_json = format!(
r#"["HTLC",{{"nonce":"7a9128b3f9612549f9278958337a5d7f","data":"{}"}}]"#,
payment_hash
);
assert_eq!(serialized, expected_json);
// Deserialize directly back to Secret using serde
let deserialized_secret: Secret = serde_json::from_str(&serialized).unwrap();
// Verify the direct serde serialization/deserialization round trip works
assert_eq!(original_secret, deserialized_secret);
assert_eq!(deserialized_secret.kind, Kind::HTLC);
assert_eq!(deserialized_secret.secret_data.data, payment_hash);
}
}

View File

@@ -0,0 +1,17 @@
//! Error types for NUT-18: Payment Requests
use thiserror::Error;
/// NUT18 Error
#[derive(Debug, Error)]
pub enum Error {
/// Invalid Prefix
#[error("Invalid Prefix")]
InvalidPrefix,
/// Ciborium error
#[error(transparent)]
CiboriumError(#[from] ciborium::de::Error<std::io::Error>),
/// Base64 error
#[error(transparent)]
Base64Error(#[from] bitcoin::base64::DecodeError),
}

View File

@@ -0,0 +1,11 @@
//! NUT-18 module imports
pub mod error;
pub mod payment_request;
pub mod secret;
pub mod transport;
pub use error::Error;
pub use payment_request::{PaymentRequest, PaymentRequestBuilder, PaymentRequestPayload};
pub use secret::{Nut10SecretRequest, SecretDataRequest};
pub use transport::{Transport, TransportBuilder, TransportType};

View File

@@ -8,232 +8,15 @@ use std::str::FromStr;
use bitcoin::base64::engine::{general_purpose, GeneralPurpose};
use bitcoin::base64::{alphabet, Engine};
use serde::ser::{SerializeTuple, Serializer};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use super::{CurrencyUnit, Nut10Secret, Proofs, SpendingConditions};
use super::{Error, Nut10SecretRequest, Transport};
use crate::mint_url::MintUrl;
use crate::nuts::nut10::Kind;
use crate::nuts::{CurrencyUnit, Proofs};
use crate::Amount;
const PAYMENT_REQUEST_PREFIX: &str = "creqA";
/// NUT18 Error
#[derive(Debug, Error)]
pub enum Error {
/// Invalid Prefix
#[error("Invalid Prefix")]
InvalidPrefix,
/// Ciborium error
#[error(transparent)]
CiboriumError(#[from] ciborium::de::Error<std::io::Error>),
/// Base64 error
#[error(transparent)]
Base64Error(#[from] bitcoin::base64::DecodeError),
}
/// Transport Type
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransportType {
/// Nostr
#[serde(rename = "nostr")]
Nostr,
/// Http post
#[serde(rename = "post")]
HttpPost,
}
impl fmt::Display for TransportType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use serde::ser::Error;
let t = serde_json::to_string(self).map_err(|e| fmt::Error::custom(e.to_string()))?;
write!(f, "{t}")
}
}
impl FromStr for TransportType {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"nostr" => Ok(Self::Nostr),
"post" => Ok(Self::HttpPost),
_ => Err(Error::InvalidPrefix),
}
}
}
impl FromStr for Transport {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let decode_config = general_purpose::GeneralPurposeConfig::new()
.with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
Ok(ciborium::from_reader(&decoded[..])?)
}
}
/// Transport
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct Transport {
/// Type
#[serde(rename = "t")]
pub _type: TransportType,
/// Target
#[serde(rename = "a")]
pub target: String,
/// Tags
#[serde(rename = "g")]
pub tags: Option<Vec<Vec<String>>>,
}
impl Transport {
/// Create a new TransportBuilder
pub fn builder() -> TransportBuilder {
TransportBuilder::default()
}
}
/// Builder for Transport
#[derive(Debug, Default, Clone)]
pub struct TransportBuilder {
_type: Option<TransportType>,
target: Option<String>,
tags: Option<Vec<Vec<String>>>,
}
impl TransportBuilder {
/// Set transport type
pub fn transport_type(mut self, transport_type: TransportType) -> Self {
self._type = Some(transport_type);
self
}
/// Set target
pub fn target<S: Into<String>>(mut self, target: S) -> Self {
self.target = Some(target.into());
self
}
/// Add a tag
pub fn add_tag(mut self, tag: Vec<String>) -> Self {
self.tags.get_or_insert_with(Vec::new).push(tag);
self
}
/// Set tags
pub fn tags(mut self, tags: Vec<Vec<String>>) -> Self {
self.tags = Some(tags);
self
}
/// Build the Transport
pub fn build(self) -> Result<Transport, &'static str> {
let _type = self._type.ok_or("Transport type is required")?;
let target = self.target.ok_or("Target is required")?;
Ok(Transport {
_type,
target,
tags: self.tags,
})
}
}
impl AsRef<String> for Transport {
fn as_ref(&self) -> &String {
&self.target
}
}
/// 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, Deserialize)]
pub struct Nut10SecretRequest {
/// Kind of the spending condition
pub kind: Kind,
/// Secret Data without nonce
pub secret_data: SecretDataRequest,
}
impl Nut10SecretRequest {
/// Create a new Nut10SecretRequest
pub fn new<S, V>(kind: Kind, data: S, tags: Option<V>) -> Self
where
S: Into<String>,
V: Into<Vec<Vec<String>>>,
{
let secret_data = SecretDataRequest {
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,
tags: secret.secret_data.tags,
};
Self {
kind: secret.kind,
secret_data,
}
}
}
impl From<Nut10SecretRequest> for Nut10Secret {
fn from(value: Nut10SecretRequest) -> Self {
Self::new(value.kind, value.secret_data.data, value.secret_data.tags)
}
}
impl From<SpendingConditions> for Nut10SecretRequest {
fn from(conditions: SpendingConditions) -> Self {
match conditions {
SpendingConditions::P2PKConditions { data, conditions } => {
Self::new(Kind::P2PK, data.to_hex(), conditions)
}
SpendingConditions::HTLCConditions { data, conditions } => {
Self::new(Kind::HTLC, data.to_string(), conditions)
}
}
}
}
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);
// 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()
}
}
/// Payment Request
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct PaymentRequest {
@@ -270,6 +53,38 @@ impl PaymentRequest {
}
}
impl AsRef<Option<String>> for PaymentRequest {
fn as_ref(&self) -> &Option<String> {
&self.payment_id
}
}
impl fmt::Display for PaymentRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use serde::ser::Error;
let mut data = Vec::new();
ciborium::into_writer(self, &mut data).map_err(|e| fmt::Error::custom(e.to_string()))?;
let encoded = general_purpose::URL_SAFE.encode(data);
write!(f, "{PAYMENT_REQUEST_PREFIX}{encoded}")
}
}
impl FromStr for PaymentRequest {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s
.strip_prefix(PAYMENT_REQUEST_PREFIX)
.ok_or(Error::InvalidPrefix)?;
let decode_config = general_purpose::GeneralPurposeConfig::new()
.with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
Ok(ciborium::from_reader(&decoded[..])?)
}
}
/// Builder for PaymentRequest
#[derive(Debug, Default, Clone)]
pub struct PaymentRequestBuilder {
@@ -367,38 +182,6 @@ impl PaymentRequestBuilder {
}
}
impl AsRef<Option<String>> for PaymentRequest {
fn as_ref(&self) -> &Option<String> {
&self.payment_id
}
}
impl fmt::Display for PaymentRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use serde::ser::Error;
let mut data = Vec::new();
ciborium::into_writer(self, &mut data).map_err(|e| fmt::Error::custom(e.to_string()))?;
let encoded = general_purpose::URL_SAFE.encode(data);
write!(f, "{PAYMENT_REQUEST_PREFIX}{encoded}")
}
}
impl FromStr for PaymentRequest {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s
.strip_prefix(PAYMENT_REQUEST_PREFIX)
.ok_or(Error::InvalidPrefix)?;
let decode_config = general_purpose::GeneralPurposeConfig::new()
.with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
Ok(ciborium::from_reader(&decoded[..])?)
}
}
/// Payment Request
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct PaymentRequestPayload {
@@ -418,7 +201,12 @@ pub struct PaymentRequestPayload {
mod tests {
use std::str::FromStr;
use lightning_invoice::Bolt11Invoice;
use super::*;
use crate::nuts::nut10::Kind;
use crate::nuts::SpendingConditions;
use crate::TransportType;
const PAYMENT_REQUEST: &str = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF4Imh0dHBzOi8vbm9mZWVzLnRlc3RudXQuY2FzaHUuc3BhY2U=";
@@ -536,7 +324,7 @@ mod tests {
);
// Test error case - missing required fields
let result = TransportBuilder::default().build();
let result = crate::nuts::nut18::transport::TransportBuilder::default().build();
assert!(result.is_err());
}
@@ -552,7 +340,7 @@ mod tests {
);
// Convert to a full Nut10Secret
let full_secret: Nut10Secret = secret_request.clone().into();
let full_secret: crate::nuts::Nut10Secret = secret_request.clone().into();
// Check conversion
assert_eq!(full_secret.kind, Kind::P2PK);
@@ -588,4 +376,91 @@ mod tests {
assert_eq!(payment_request.nut10, Some(secret_request));
}
#[test]
fn test_nut10_secret_request_multiple_mints() {
let mint_urls = [
"https://8333.space:3338",
"https://mint.minibits.cash/Bitcoin",
"https://antifiat.cash",
"https://mint.macadamia.cash",
]
.iter()
.map(|m| MintUrl::from_str(m).unwrap())
.collect();
let payment_request = PaymentRequestBuilder::default()
.unit(CurrencyUnit::Sat)
.amount(10)
.mints(mint_urls)
.build();
let payment_request_str = payment_request.to_string();
let r = PaymentRequest::from_str(&payment_request_str).unwrap();
assert_eq!(payment_request, r);
}
#[test]
fn test_nut10_secret_request_htlc() {
let bolt11 = "lnbc100n1p5z3a63pp56854ytysg7e5z9fl3w5mgvrlqjfcytnjv8ff5hm5qt6gl6alxesqdqqcqzzsxqyz5vqsp5p0x0dlhn27s63j4emxnk26p7f94u0lyarnfp5yqmac9gzy4ngdss9qxpqysgqne3v0hnzt2lp0hc69xpzckk0cdcar7glvjhq60lsrfe8gejdm8c564prrnsft6ctxxyrewp4jtezrq3gxxqnfjj0f9tw2qs9y0lslmqpfu7et9";
let bolt11 = Bolt11Invoice::from_str(bolt11).unwrap();
let nut10 = SpendingConditions::HTLCConditions {
data: bolt11.payment_hash().clone(),
conditions: None,
};
let payment_request = PaymentRequestBuilder::default()
.unit(CurrencyUnit::Sat)
.amount(10)
.nut10(nut10.into())
.build();
let payment_request_str = payment_request.to_string();
let r = PaymentRequest::from_str(&payment_request_str).unwrap();
assert_eq!(payment_request, r);
}
#[test]
fn test_nut10_secret_request_p2pk() {
// Use a public key for P2PK condition
let pubkey_hex = "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198";
// Create P2PK spending conditions
let nut10 = SpendingConditions::P2PKConditions {
data: crate::nuts::PublicKey::from_str(pubkey_hex).unwrap(),
conditions: None,
};
// Build payment request with P2PK condition
let payment_request = PaymentRequestBuilder::default()
.unit(CurrencyUnit::Sat)
.amount(10)
.payment_id("test-p2pk-id")
.description("P2PK locked payment")
.nut10(nut10.into())
.build();
// Convert to string representation
let payment_request_str = payment_request.to_string();
// Parse back from string
let decoded_request = PaymentRequest::from_str(&payment_request_str).unwrap();
// Verify round-trip serialization
assert_eq!(payment_request, decoded_request);
// 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);
} else {
panic!("NUT10 secret data missing in decoded payment request");
}
}
}

View File

@@ -0,0 +1,137 @@
//! 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)]
pub struct Nut10SecretRequest {
/// Kind of the spending condition
pub kind: Kind,
/// Secret Data without nonce
pub secret_data: SecretDataRequest,
}
impl Nut10SecretRequest {
/// Create a new Nut10SecretRequest
pub fn new<S, V>(kind: Kind, data: S, tags: Option<V>) -> Self
where
S: Into<String>,
V: Into<Vec<Vec<String>>>,
{
let secret_data = SecretDataRequest {
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,
tags: secret.secret_data.tags,
};
Self {
kind: secret.kind,
secret_data,
}
}
}
impl From<Nut10SecretRequest> for Nut10Secret {
fn from(value: Nut10SecretRequest) -> Self {
Self::new(value.kind, value.secret_data.data, value.secret_data.tags)
}
}
impl From<SpendingConditions> for Nut10SecretRequest {
fn from(conditions: SpendingConditions) -> Self {
match conditions {
SpendingConditions::P2PKConditions { data, conditions } => {
Self::new(Kind::P2PK, data.to_hex(), conditions)
}
SpendingConditions::HTLCConditions { data, conditions } => {
Self::new(Kind::HTLC, data.to_string(), conditions)
}
}
}
}
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);
// 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()
}
}
// 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)
}
}

View File

@@ -0,0 +1,126 @@
//! Transport types for NUT-18: Payment Requests
use std::fmt;
use std::str::FromStr;
use bitcoin::base64::engine::{general_purpose, GeneralPurpose};
use bitcoin::base64::{alphabet, Engine};
use serde::{Deserialize, Serialize};
use crate::nuts::nut18::error::Error;
/// Transport Type
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransportType {
/// Nostr
#[serde(rename = "nostr")]
Nostr,
/// Http post
#[serde(rename = "post")]
HttpPost,
}
impl fmt::Display for TransportType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use serde::ser::Error;
let t = serde_json::to_string(self).map_err(|e| fmt::Error::custom(e.to_string()))?;
write!(f, "{t}")
}
}
impl FromStr for TransportType {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"nostr" => Ok(Self::Nostr),
"post" => Ok(Self::HttpPost),
_ => Err(Error::InvalidPrefix),
}
}
}
/// Transport
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct Transport {
/// Type
#[serde(rename = "t")]
pub _type: TransportType,
/// Target
#[serde(rename = "a")]
pub target: String,
/// Tags
#[serde(rename = "g")]
pub tags: Option<Vec<Vec<String>>>,
}
impl Transport {
/// Create a new TransportBuilder
pub fn builder() -> TransportBuilder {
TransportBuilder::default()
}
}
impl FromStr for Transport {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let decode_config = general_purpose::GeneralPurposeConfig::new()
.with_decode_padding_mode(bitcoin::base64::engine::DecodePaddingMode::Indifferent);
let decoded = GeneralPurpose::new(&alphabet::URL_SAFE, decode_config).decode(s)?;
Ok(ciborium::from_reader(&decoded[..])?)
}
}
/// Builder for Transport
#[derive(Debug, Default, Clone)]
pub struct TransportBuilder {
_type: Option<TransportType>,
target: Option<String>,
tags: Option<Vec<Vec<String>>>,
}
impl TransportBuilder {
/// Set transport type
pub fn transport_type(mut self, transport_type: TransportType) -> Self {
self._type = Some(transport_type);
self
}
/// Set target
pub fn target<S: Into<String>>(mut self, target: S) -> Self {
self.target = Some(target.into());
self
}
/// Add a tag
pub fn add_tag(mut self, tag: Vec<String>) -> Self {
self.tags.get_or_insert_with(Vec::new).push(tag);
self
}
/// Set tags
pub fn tags(mut self, tags: Vec<Vec<String>>) -> Self {
self.tags = Some(tags);
self
}
/// Build the Transport
pub fn build(self) -> Result<Transport, &'static str> {
let _type = self._type.ok_or("Transport type is required")?;
let target = self.target.ok_or("Target is required")?;
Ok(Transport {
_type,
target,
tags: self.tags,
})
}
}
impl AsRef<String> for Transport {
fn as_ref(&self) -> &String {
&self.target
}
}