فهرست منبع

Merge pull request #656 from thesimplekid/payment_request_builder

feat: payments request builder
thesimplekid 1 ماه پیش
والد
کامیت
4c447cf046
3فایلهای تغییر یافته به همراه243 افزوده شده و 3 حذف شده
  1. 1 0
      CHANGELOG.md
  2. 4 1
      crates/cashu/src/nuts/mod.rs
  3. 238 2
      crates/cashu/src/nuts/nut18.rs

+ 1 - 0
CHANGELOG.md

@@ -24,6 +24,7 @@
 - Compile-time error when no lightning backend features are enabled ([thesimplekid]).
 - Add support for sqlcipher ([benthecarman]).
 - Payment processor ([thesimplekid]).
+- Payment request builder ([thesimplekid]).
 ### Removed
 - Remove support for Memory Database in cdk ([crodas]).
 - Remove `AmountStr` ([crodas]).

+ 4 - 1
crates/cashu/src/nuts/mod.rs

@@ -54,4 +54,7 @@ pub use nut12::{BlindSignatureDleq, ProofDleq};
 pub use nut14::HTLCWitness;
 pub use nut15::{Mpp, MppMethodSettings, Settings as NUT15Settings};
 pub use nut17::NotificationPayload;
-pub use nut18::{PaymentRequest, PaymentRequestPayload, Transport};
+pub use nut18::{
+    PaymentRequest, PaymentRequestBuilder, PaymentRequestPayload, Transport, TransportBuilder,
+    TransportType,
+};

+ 238 - 2
crates/cashu/src/nuts/nut18.rs

@@ -49,11 +49,27 @@ impl fmt::Display for TransportType {
     }
 }
 
+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 = serde_json::Error;
+    type Err = Error;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        serde_json::from_str(s)
+        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[..])?)
     }
 }
 
@@ -71,6 +87,65 @@ pub struct Transport {
     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
+    }
+}
+
 /// Payment Request
 #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct PaymentRequest {
@@ -97,6 +172,106 @@ pub struct PaymentRequest {
     pub transports: Vec<Transport>,
 }
 
+impl PaymentRequest {
+    /// Create a new PaymentRequestBuilder
+    pub fn builder() -> PaymentRequestBuilder {
+        PaymentRequestBuilder::default()
+    }
+}
+
+/// Builder for PaymentRequest
+#[derive(Debug, Default, Clone)]
+pub struct PaymentRequestBuilder {
+    payment_id: Option<String>,
+    amount: Option<Amount>,
+    unit: Option<CurrencyUnit>,
+    single_use: Option<bool>,
+    mints: Option<Vec<MintUrl>>,
+    description: Option<String>,
+    transports: Vec<Transport>,
+}
+
+impl PaymentRequestBuilder {
+    /// Set payment ID
+    pub fn payment_id<S>(mut self, payment_id: S) -> Self
+    where
+        S: Into<String>,
+    {
+        self.payment_id = Some(payment_id.into());
+        self
+    }
+
+    /// Set amount
+    pub fn amount<A>(mut self, amount: A) -> Self
+    where
+        A: Into<Amount>,
+    {
+        self.amount = Some(amount.into());
+        self
+    }
+
+    /// Set unit
+    pub fn unit(mut self, unit: CurrencyUnit) -> Self {
+        self.unit = Some(unit);
+        self
+    }
+
+    /// Set single use flag
+    pub fn single_use(mut self, single_use: bool) -> Self {
+        self.single_use = Some(single_use);
+        self
+    }
+
+    /// Add a mint URL
+    pub fn add_mint(mut self, mint_url: MintUrl) -> Self {
+        self.mints.get_or_insert_with(Vec::new).push(mint_url);
+        self
+    }
+
+    /// Set mints
+    pub fn mints(mut self, mints: Vec<MintUrl>) -> Self {
+        self.mints = Some(mints);
+        self
+    }
+
+    /// Set description
+    pub fn description<S: Into<String>>(mut self, description: S) -> Self {
+        self.description = Some(description.into());
+        self
+    }
+
+    /// Add a transport
+    pub fn add_transport(mut self, transport: Transport) -> Self {
+        self.transports.push(transport);
+        self
+    }
+
+    /// Set transports
+    pub fn transports(mut self, transports: Vec<Transport>) -> Self {
+        self.transports = transports;
+        self
+    }
+
+    /// Build the PaymentRequest
+    pub fn build(self) -> PaymentRequest {
+        PaymentRequest {
+            payment_id: self.payment_id,
+            amount: self.amount,
+            unit: self.unit,
+            single_use: self.single_use,
+            mints: self.mints,
+            description: self.description,
+            transports: self.transports,
+        }
+    }
+}
+
+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;
@@ -198,4 +373,65 @@ mod tests {
         let t = req.transports.first().unwrap();
         assert_eq!(&transport, t);
     }
+
+    #[test]
+    fn test_payment_request_builder() {
+        let transport = Transport {
+            _type: TransportType::Nostr,
+            target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5".to_string(), 
+            tags: Some(vec![vec!["n".to_string(), "17".to_string()]])
+        };
+
+        let mint_url =
+            MintUrl::from_str("https://nofees.testnut.cashu.space").expect("valid mint url");
+
+        // Build a payment request using the builder pattern
+        let request = PaymentRequest::builder()
+            .payment_id("b7a90176")
+            .amount(Amount::from(10))
+            .unit(CurrencyUnit::Sat)
+            .add_mint(mint_url.clone())
+            .add_transport(transport.clone())
+            .build();
+
+        // Verify the built request
+        assert_eq!(&request.payment_id.clone().unwrap(), "b7a90176");
+        assert_eq!(request.amount.unwrap(), 10.into());
+        assert_eq!(request.unit.clone().unwrap(), CurrencyUnit::Sat);
+        assert_eq!(request.mints.clone().unwrap(), vec![mint_url]);
+
+        let t = request.transports.first().unwrap();
+        assert_eq!(&transport, t);
+
+        // Test serialization and deserialization
+        let request_str = request.to_string();
+        let req = PaymentRequest::from_str(&request_str).expect("valid payment request");
+
+        assert_eq!(req.payment_id, request.payment_id);
+        assert_eq!(req.amount, request.amount);
+        assert_eq!(req.unit, request.unit);
+    }
+
+    #[test]
+    fn test_transport_builder() {
+        // Build a transport using the builder pattern
+        let transport = Transport::builder()
+            .transport_type(TransportType::Nostr)
+            .target("nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5")
+            .add_tag(vec!["n".to_string(), "17".to_string()])
+            .build()
+            .expect("Valid transport");
+
+        // Verify the built transport
+        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 error case - missing required fields
+        let result = TransportBuilder::default().build();
+        assert!(result.is_err());
+    }
 }