1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, uniffi::Enum)]
14pub enum TransportType {
15 Nostr,
17 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#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
41pub struct Transport {
42 pub transport_type: TransportType,
44 pub target: String,
46 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#[derive(Debug, uniffi::Object)]
75pub struct PaymentRequest {
76 inner: cdk::nuts::PaymentRequest,
77}
78
79impl PaymentRequest {
80 pub(crate) fn inner(&self) -> &cdk::nuts::PaymentRequest {
82 &self.inner
83 }
84
85 pub(crate) fn from_inner(inner: cdk::nuts::PaymentRequest) -> Self {
87 Self { inner }
88 }
89}
90
91#[uniffi::export]
92impl PaymentRequest {
93 #[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 pub fn to_string_encoded(&self) -> String {
103 self.inner.to_string()
104 }
105
106 pub fn to_bech32_string(&self) -> Result<String, FfiError> {
108 self.inner.to_bech32_string().map_err(FfiError::internal)
109 }
110
111 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 pub fn payment_id(&self) -> Option<String> {
139 self.inner.payment_id.clone()
140 }
141
142 pub fn amount(&self) -> Option<Amount> {
144 self.inner.amount.map(|a| a.into())
145 }
146
147 pub fn unit(&self) -> Option<CurrencyUnit> {
149 self.inner.unit.clone().map(|u| u.into())
150 }
151
152 pub fn single_use(&self) -> Option<bool> {
154 self.inner.single_use
155 }
156
157 pub fn mints(&self) -> Vec<String> {
159 self.inner.mints.iter().map(|m| m.to_string()).collect()
160 }
161
162 pub fn description(&self) -> Option<String> {
164 self.inner.description.clone()
165 }
166
167 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#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)]
180pub struct CreateRequestParams {
181 pub amount: Option<u64>,
183 pub unit: String,
185 pub description: Option<String>,
187 pub pubkeys: Option<Vec<String>>,
189 pub num_sigs: u64,
191 pub hash: Option<String>,
193 pub preimage: Option<String>,
195 pub transport: String,
197 pub http_url: Option<String>,
199 pub nostr_relays: Option<Vec<String>>,
201 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#[uniffi::export]
261pub fn decode_payment_request(encoded: String) -> Result<Arc<PaymentRequest>, FfiError> {
262 PaymentRequest::from_string(encoded)
263}
264
265#[uniffi::export]
267pub fn encode_create_request_params(params: CreateRequestParams) -> Result<String, FfiError> {
268 Ok(serde_json::to_string(¶ms)?)
269}
270
271#[uniffi::export]
273pub fn decode_create_request_params(json: String) -> Result<CreateRequestParams, FfiError> {
274 Ok(serde_json::from_str(&json)?)
275}
276
277#[derive(uniffi::Object)]
283pub struct NostrWaitInfo {
284 inner: cdk::wallet::payment_request::NostrWaitInfo,
285}
286
287impl NostrWaitInfo {
288 #[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 pub fn relays(&self) -> Vec<String> {
299 self.inner.relays.clone()
300 }
301
302 pub fn pubkey(&self) -> String {
304 self.inner.pubkey.to_hex()
305 }
306}
307
308#[derive(uniffi::Record)]
313pub struct CreateRequestResult {
314 pub payment_request: Arc<PaymentRequest>,
316 pub nostr_wait_info: Option<Arc<NostrWaitInfo>>,
318}
319
320#[derive(uniffi::Object)]
324pub struct PaymentRequestPayload {
325 inner: cdk::nuts::PaymentRequestPayload,
326}
327
328#[uniffi::export]
329impl PaymentRequestPayload {
330 #[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 pub fn id(&self) -> Option<String> {
339 self.inner.id.clone()
340 }
341
342 pub fn memo(&self) -> Option<String> {
344 self.inner.memo.clone()
345 }
346
347 pub fn mint(&self) -> MintUrl {
349 self.inner.mint.clone().into()
350 }
351
352 pub fn unit(&self) -> CurrencyUnit {
354 self.inner.unit.clone().into()
355 }
356
357 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 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 assert!(
465 bech32.starts_with("CREQB1"),
466 "Expected bech32 string to start with CREQB1, got: {}",
467 &bech32[..10.min(bech32.len())]
468 );
469
470 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}