Skip to main content

cdk_ffi/types/
payment_request.rs

1//! Payment Request FFI types (NUT-18)
2
3use std::sync::Arc;
4
5use serde::{Deserialize, Serialize};
6
7use super::amount::{Amount, CurrencyUnit};
8use super::mint::MintUrl;
9use super::proof::Proof;
10use crate::error::FfiError;
11
12/// Transport type for payment request delivery
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
14pub enum TransportType {
15    /// Nostr transport (privacy-preserving)
16    Nostr,
17    /// HTTP POST transport
18    HttpPost,
19}
20
21impl From<cdk::nuts::TransportType> for TransportType {
22    fn from(t: cdk::nuts::TransportType) -> Self {
23        match t {
24            cdk::nuts::TransportType::Nostr => Self::Nostr,
25            cdk::nuts::TransportType::HttpPost => Self::HttpPost,
26        }
27    }
28}
29
30impl From<TransportType> for cdk::nuts::TransportType {
31    fn from(t: TransportType) -> Self {
32        match t {
33            TransportType::Nostr => cdk::nuts::TransportType::Nostr,
34            TransportType::HttpPost => cdk::nuts::TransportType::HttpPost,
35        }
36    }
37}
38
39/// Transport for payment request delivery
40#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
41pub struct Transport {
42    /// Transport type
43    pub transport_type: TransportType,
44    /// Target (e.g., nprofile for Nostr, URL for HTTP)
45    pub target: String,
46    /// Tags
47    pub tags: Vec<Vec<String>>,
48}
49
50impl From<cdk::nuts::Transport> for Transport {
51    fn from(t: cdk::nuts::Transport) -> Self {
52        Self {
53            transport_type: t._type.into(),
54            target: t.target,
55            tags: t.tags,
56        }
57    }
58}
59
60impl From<Transport> for cdk::nuts::Transport {
61    fn from(t: Transport) -> Self {
62        Self {
63            _type: t.transport_type.into(),
64            target: t.target,
65            tags: t.tags,
66        }
67    }
68}
69
70/// NUT-18 Payment Request
71///
72/// A payment request that can be shared to request Cashu tokens.
73/// Encoded as a string with the `creqA` prefix.
74#[derive(Debug, uniffi::Object)]
75pub struct PaymentRequest {
76    inner: cdk::nuts::PaymentRequest,
77}
78
79impl PaymentRequest {
80    /// Get inner reference
81    pub(crate) fn inner(&self) -> &cdk::nuts::PaymentRequest {
82        &self.inner
83    }
84
85    /// Create from the inner CDK type
86    pub(crate) fn from_inner(inner: cdk::nuts::PaymentRequest) -> Self {
87        Self { inner }
88    }
89}
90
91#[uniffi::export]
92impl PaymentRequest {
93    /// Parse a payment request from its encoded string representation
94    #[uniffi::constructor]
95    pub fn from_string(encoded: String) -> Result<Arc<Self>, FfiError> {
96        use std::str::FromStr;
97        let inner = cdk::nuts::PaymentRequest::from_str(&encoded).map_err(FfiError::internal)?;
98        Ok(Arc::new(Self { inner }))
99    }
100
101    /// Encode the payment request to a string
102    pub fn to_string_encoded(&self) -> String {
103        self.inner.to_string()
104    }
105
106    /// Encode the payment request to a NUT-26 bech32m string (creqB prefix)
107    pub fn to_bech32_string(&self) -> Result<String, FfiError> {
108        self.inner.to_bech32_string().map_err(FfiError::internal)
109    }
110
111    /// Convert this payment request to a BIP 321 `bitcoin:` URI string.
112    ///
113    /// The cashu payment request is encoded as a NUT-26 bech32m `CREQB1...`
114    /// string in the `creq=` query parameter. Optionally include a BOLT11
115    /// invoice (`lightning=`) and/or BOLT12 offer (`lno=`) as fallback
116    /// payment methods for wallets that don't support cashu.
117    ///
118    /// ```text
119    /// val request = PaymentRequest.fromString("CREQB1...")
120    /// val uri = request.toBip321(
121    ///     bolt11 = "lnbc100n1p...",
122    ///     bolt12 = "lno1qgsq..."
123    /// )
124    /// // => "bitcoin:?creq=CREQB1...&lightning=lnbc100n1p...&lno=lno1qgsq..."
125    /// ```
126    pub fn to_bip321(
127        &self,
128        bolt11: Option<String>,
129        bolt12: Option<String>,
130    ) -> Result<String, FfiError> {
131        use cdk::wallet::bip321::PaymentRequestBip321Ext;
132        let builder = self.inner.to_bip321().map_err(FfiError::from)?;
133        let builder = crate::bip321::apply_optional_lightning_methods(builder, bolt11, bolt12);
134        Ok(builder.to_string())
135    }
136
137    /// Get the payment ID
138    pub fn payment_id(&self) -> Option<String> {
139        self.inner.payment_id.clone()
140    }
141
142    /// Get the requested amount
143    pub fn amount(&self) -> Option<Amount> {
144        self.inner.amount.map(|a| a.into())
145    }
146
147    /// Get the currency unit
148    pub fn unit(&self) -> Option<CurrencyUnit> {
149        self.inner.unit.clone().map(|u| u.into())
150    }
151
152    /// Get whether this is a single-use request
153    pub fn single_use(&self) -> Option<bool> {
154        self.inner.single_use
155    }
156
157    /// Get the list of acceptable mint URLs
158    pub fn mints(&self) -> Vec<String> {
159        self.inner.mints.iter().map(|m| m.to_string()).collect()
160    }
161
162    /// Get the description
163    pub fn description(&self) -> Option<String> {
164        self.inner.description.clone()
165    }
166
167    /// Get the transports for delivering the payment
168    pub fn transports(&self) -> Vec<Transport> {
169        self.inner
170            .transports
171            .iter()
172            .cloned()
173            .map(|t| t.into())
174            .collect()
175    }
176}
177
178/// Parameters for creating a NUT-18 payment request
179#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
180pub struct CreateRequestParams {
181    /// Optional amount to request (in smallest unit for the currency)
182    pub amount: Option<u64>,
183    /// Currency unit (e.g., "sat", "msat", "usd")
184    pub unit: String,
185    /// Optional description for the request
186    pub description: Option<String>,
187    /// Optional public keys for P2PK spending conditions (hex-encoded)
188    pub pubkeys: Option<Vec<String>>,
189    /// Required number of signatures for multisig (defaults to 1)
190    pub num_sigs: u64,
191    /// Optional HTLC hash (hex-encoded SHA-256)
192    pub hash: Option<String>,
193    /// Optional HTLC preimage (alternative to hash)
194    pub preimage: Option<String>,
195    /// Transport type: "nostr", "http", or "none"
196    pub transport: String,
197    /// HTTP URL for HTTP transport (required if transport is "http")
198    pub http_url: Option<String>,
199    /// Nostr relay URLs (required if transport is "nostr")
200    pub nostr_relays: Option<Vec<String>>,
201    /// Optional list of mint URLs the receiver trusts. If not provided, the wallet's current mints for the requested unit will be used.
202    pub mints: Option<Vec<String>>,
203}
204
205impl Default for CreateRequestParams {
206    fn default() -> Self {
207        Self {
208            amount: None,
209            unit: "sat".to_string(),
210            description: None,
211            pubkeys: None,
212            num_sigs: 1,
213            hash: None,
214            preimage: None,
215            transport: "none".to_string(),
216            http_url: None,
217            nostr_relays: None,
218            mints: None,
219        }
220    }
221}
222
223impl From<CreateRequestParams> for cdk::wallet::payment_request::CreateRequestParams {
224    fn from(params: CreateRequestParams) -> Self {
225        Self {
226            amount: params.amount,
227            unit: params.unit,
228            description: params.description,
229            pubkeys: params.pubkeys,
230            num_sigs: params.num_sigs,
231            hash: params.hash,
232            preimage: params.preimage,
233            transport: params.transport,
234            http_url: params.http_url,
235            nostr_relays: params.nostr_relays,
236            mints: params.mints,
237        }
238    }
239}
240
241impl From<cdk::wallet::payment_request::CreateRequestParams> for CreateRequestParams {
242    fn from(params: cdk::wallet::payment_request::CreateRequestParams) -> Self {
243        Self {
244            amount: params.amount,
245            unit: params.unit,
246            description: params.description,
247            pubkeys: params.pubkeys,
248            num_sigs: params.num_sigs,
249            hash: params.hash,
250            preimage: params.preimage,
251            transport: params.transport,
252            http_url: params.http_url,
253            nostr_relays: params.nostr_relays,
254            mints: params.mints,
255        }
256    }
257}
258
259/// Decode a payment request from its encoded string representation
260#[uniffi::export]
261pub fn decode_payment_request(encoded: String) -> Result<Arc<PaymentRequest>, FfiError> {
262    PaymentRequest::from_string(encoded)
263}
264
265/// Encode CreateRequestParams to JSON string
266#[uniffi::export]
267pub fn encode_create_request_params(params: CreateRequestParams) -> Result<String, FfiError> {
268    Ok(serde_json::to_string(&params)?)
269}
270
271/// Decode CreateRequestParams from JSON string
272#[uniffi::export]
273pub fn decode_create_request_params(json: String) -> Result<CreateRequestParams, FfiError> {
274    Ok(serde_json::from_str(&json)?)
275}
276
277/// Information needed to wait for an incoming Nostr payment
278///
279/// Returned by `create_request` when the transport is `nostr`. Pass this to
280/// `wait_for_nostr_payment` to connect, subscribe, and receive the incoming
281/// payment on the specified relays.
282#[derive(uniffi::Object)]
283pub struct NostrWaitInfo {
284    inner: cdk::wallet::payment_request::NostrWaitInfo,
285}
286
287impl NostrWaitInfo {
288    /// Get inner reference
289    #[allow(dead_code)]
290    pub(crate) fn inner(&self) -> &cdk::wallet::payment_request::NostrWaitInfo {
291        &self.inner
292    }
293}
294
295#[uniffi::export]
296impl NostrWaitInfo {
297    /// Get the Nostr relays to connect to
298    pub fn relays(&self) -> Vec<String> {
299        self.inner.relays.clone()
300    }
301
302    /// Get the recipient public key as a hex string
303    pub fn pubkey(&self) -> String {
304        self.inner.pubkey.to_hex()
305    }
306}
307
308/// Result of creating a payment request
309///
310/// Contains the payment request and optionally the Nostr wait info
311/// if the transport was set to "nostr".
312#[derive(uniffi::Record)]
313pub struct CreateRequestResult {
314    /// The payment request to share with the payer
315    pub payment_request: Arc<PaymentRequest>,
316    /// Nostr wait info (present when transport is "nostr")
317    pub nostr_wait_info: Option<Arc<NostrWaitInfo>>,
318}
319
320/// Payment Request Payload
321///
322/// Sent over Nostr or other transports.
323#[derive(uniffi::Object)]
324pub struct PaymentRequestPayload {
325    inner: cdk::nuts::PaymentRequestPayload,
326}
327
328#[uniffi::export]
329impl PaymentRequestPayload {
330    /// Decode PaymentRequestPayload from JSON string
331    #[uniffi::constructor]
332    pub fn from_string(json: String) -> Result<Arc<PaymentRequestPayload>, FfiError> {
333        let inner: cdk::nuts::PaymentRequestPayload = serde_json::from_str(&json)?;
334        Ok(Arc::new(PaymentRequestPayload { inner }))
335    }
336
337    /// Get the ID
338    pub fn id(&self) -> Option<String> {
339        self.inner.id.clone()
340    }
341
342    /// Get the memo
343    pub fn memo(&self) -> Option<String> {
344        self.inner.memo.clone()
345    }
346
347    /// Get the mint URL
348    pub fn mint(&self) -> MintUrl {
349        self.inner.mint.clone().into()
350    }
351
352    /// Get the currency unit
353    pub fn unit(&self) -> CurrencyUnit {
354        self.inner.unit.clone().into()
355    }
356
357    /// Get the proofs
358    pub fn proofs(&self) -> Vec<Proof> {
359        self.inner.proofs.iter().map(|p| p.clone().into()).collect()
360    }
361}
362
363impl core::fmt::Display for PaymentRequestPayload {
364    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
365        write!(
366            f,
367            "{}",
368            serde_json::to_string(&self.inner).map_err(|_| core::fmt::Error)?
369        )
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[test]
378    fn test_payment_request_payload() {
379        use std::str::FromStr;
380        // Create a sample payload using inner types
381        let mint_url = cdk::mint_url::MintUrl::from_str("https://mint.example.com").unwrap();
382        let unit = cdk::nuts::CurrencyUnit::Sat;
383        let proofs = vec![];
384
385        let inner = cdk::nuts::PaymentRequestPayload {
386            id: Some("test-id".to_string()),
387            memo: Some("test-memo".to_string()),
388            mint: mint_url.clone(),
389            unit: unit.clone(),
390            proofs: proofs.clone(),
391        };
392
393        let payload = PaymentRequestPayload { inner };
394
395        assert_eq!(payload.id(), Some("test-id".to_string()));
396        assert_eq!(payload.memo(), Some("test-memo".to_string()));
397        assert_eq!(payload.mint().url, "https://mint.example.com");
398        assert!(matches!(payload.unit(), CurrencyUnit::Sat));
399        assert!(payload.proofs().is_empty());
400    }
401
402    #[test]
403    fn test_payment_request_payload_json() {
404        use std::str::FromStr;
405        let mint_url = cdk::mint_url::MintUrl::from_str("https://mint.example.com").unwrap();
406        let unit = cdk::nuts::CurrencyUnit::Sat;
407
408        let inner = cdk::nuts::PaymentRequestPayload {
409            id: Some("test-id".to_string()),
410            memo: Some("test-memo".to_string()),
411            mint: mint_url,
412            unit,
413            proofs: vec![],
414        };
415
416        let payload = PaymentRequestPayload { inner };
417
418        let json = payload.to_string();
419        let decoded = PaymentRequestPayload::from_string(json).unwrap();
420
421        assert_eq!(decoded.id(), payload.id());
422        assert_eq!(decoded.memo(), payload.memo());
423        assert_eq!(decoded.mint().url, payload.mint().url);
424    }
425
426    const PAYMENT_REQUEST: &str = "creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF4Imh0dHBzOi8vbm9mZWVzLnRlc3RudXQuY2FzaHUuc3BhY2U=";
427
428    #[test]
429    fn test_decode_payment_request() {
430        let req = PaymentRequest::from_string(PAYMENT_REQUEST.to_string()).unwrap();
431
432        assert_eq!(req.payment_id().unwrap(), "b7a90176");
433        assert_eq!(req.amount().unwrap().value, 10);
434        assert!(matches!(req.unit().unwrap(), CurrencyUnit::Sat));
435
436        let mints = req.mints();
437        assert_eq!(mints.len(), 1);
438        assert_eq!(mints[0], "https://nofees.testnut.cashu.space");
439
440        let transports = req.transports();
441        assert_eq!(transports.len(), 1);
442        assert!(matches!(transports[0].transport_type, TransportType::Nostr));
443    }
444
445    #[test]
446    fn test_roundtrip_payment_request() {
447        let req = PaymentRequest::from_string(PAYMENT_REQUEST.to_string()).unwrap();
448        let encoded = req.to_string_encoded();
449        let decoded = PaymentRequest::from_string(encoded).unwrap();
450
451        assert_eq!(req.payment_id(), decoded.payment_id());
452        assert_eq!(
453            req.amount().map(|a| a.value),
454            decoded.amount().map(|a| a.value)
455        );
456    }
457
458    #[test]
459    fn test_to_bech32_string() {
460        let req = PaymentRequest::from_string(PAYMENT_REQUEST.to_string()).unwrap();
461        let bech32 = req.to_bech32_string().unwrap();
462
463        // NUT-26 bech32m strings use the CREQB prefix (uppercase for QR compat)
464        assert!(
465            bech32.starts_with("CREQB1"),
466            "Expected bech32 string to start with CREQB1, got: {}",
467            &bech32[..10.min(bech32.len())]
468        );
469
470        // Round-trip: decode the bech32m string and verify fields match
471        let decoded = PaymentRequest::from_string(bech32).unwrap();
472        assert_eq!(req.payment_id(), decoded.payment_id());
473        assert_eq!(
474            req.amount().map(|a| a.value),
475            decoded.amount().map(|a| a.value)
476        );
477        assert_eq!(req.mints(), decoded.mints());
478        assert_eq!(req.single_use(), decoded.single_use());
479        assert_eq!(req.description(), decoded.description());
480    }
481
482    #[test]
483    fn test_transport_conversion() {
484        let ffi_transport = Transport {
485            transport_type: TransportType::Nostr,
486            target: "nprofile1...".to_string(),
487            tags: vec![vec!["n".to_string(), "17".to_string()]],
488        };
489
490        let cdk_transport: cdk::nuts::Transport = ffi_transport.clone().into();
491        let back: Transport = cdk_transport.into();
492
493        assert_eq!(ffi_transport.transport_type, back.transport_type);
494        assert_eq!(ffi_transport.target, back.target);
495        assert_eq!(ffi_transport.tags, back.tags);
496    }
497
498    #[test]
499    fn test_create_request_params_default() {
500        let params = CreateRequestParams::default();
501
502        assert_eq!(params.unit, "sat");
503        assert_eq!(params.num_sigs, 1);
504        assert_eq!(params.transport, "none");
505        assert!(params.amount.is_none());
506    }
507
508    #[test]
509    fn test_create_request_params_serialization() {
510        let params = CreateRequestParams {
511            amount: Some(100),
512            unit: "sat".to_string(),
513            description: Some("Test payment".to_string()),
514            transport: "http".to_string(),
515            http_url: Some("https://example.com/callback".to_string()),
516            ..Default::default()
517        };
518
519        let json = encode_create_request_params(params.clone()).unwrap();
520        let decoded = decode_create_request_params(json).unwrap();
521
522        assert_eq!(params.amount, decoded.amount);
523        assert_eq!(params.unit, decoded.unit);
524        assert_eq!(params.description, decoded.description);
525    }
526}