1use std::convert::Infallible;
4use std::pin::Pin;
5
6use async_trait::async_trait;
7use cashu::util::hex;
8use cashu::{Bolt11Invoice, MeltOptions};
9#[cfg(feature = "prometheus")]
10use cdk_prometheus::METRICS;
11use futures::Stream;
12use lightning::offers::offer::Offer;
13use lightning_invoice::ParseOrSemanticError;
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use thiserror::Error;
17
18use crate::mint::{MeltPaymentRequest, MeltQuote};
19use crate::nuts::nut_onchain::MeltQuoteOnchainFeeOption;
20use crate::nuts::{CurrencyUnit, MeltQuoteState};
21use crate::{Amount, QuoteId};
22
23#[derive(Debug, Error)]
25pub enum Error {
26 #[error("Invoice already paid")]
28 InvoiceAlreadyPaid,
29 #[error("Invoice pay is pending")]
31 InvoicePaymentPending,
32 #[error("Unsupported unit")]
34 UnsupportedUnit,
35 #[error("Unsupported payment option")]
37 UnsupportedPaymentOption,
38 #[error("Payment state is unknown")]
40 UnknownPaymentState,
41 #[error("Amount is not what is expected")]
43 AmountMismatch,
44 #[error("Invalid expiry")]
46 InvalidExpiry,
47 #[error(transparent)]
49 Lightning(Box<dyn std::error::Error + Send + Sync>),
50 #[error(transparent)]
52 Onchain(Box<dyn std::error::Error + Send + Sync>),
53 #[error(transparent)]
55 Serde(#[from] serde_json::Error),
56 #[error(transparent)]
58 Anyhow(#[from] anyhow::Error),
59 #[error(transparent)]
61 Parse(#[from] ParseOrSemanticError),
62 #[error(transparent)]
64 Amount(#[from] crate::amount::Error),
65 #[error(transparent)]
67 NUT04(#[from] crate::nuts::nut04::Error),
68 #[error(transparent)]
70 NUT05(#[from] crate::nuts::nut05::Error),
71 #[error(transparent)]
73 NUT23(#[from] crate::nuts::nut23::Error),
74 #[error("Hex error")]
76 Hex(#[from] hex::Error),
77 #[error("Invalid hash")]
79 InvalidHash,
80 #[error("`{0}`")]
82 Custom(String),
83}
84
85impl From<Infallible> for Error {
86 fn from(_: Infallible) -> Self {
87 unreachable!("Infallible cannot be constructed")
88 }
89}
90
91#[derive(Clone, Hash, PartialEq, Eq, Deserialize, Serialize)]
93#[serde(tag = "type", content = "value")]
94pub enum PaymentIdentifier {
95 Label(String),
97 OfferId(String),
99 PaymentHash([u8; 32]),
101 Bolt12PaymentHash([u8; 32]),
103 PaymentId([u8; 32]),
105 CustomId(String),
107 QuoteId(QuoteId),
109}
110
111impl PaymentIdentifier {
112 pub fn new(kind: &str, identifier: &str) -> Result<Self, Error> {
114 match kind.to_lowercase().as_str() {
115 "label" => Ok(Self::Label(identifier.to_string())),
116 "offer_id" => Ok(Self::OfferId(identifier.to_string())),
117 "payment_hash" => Ok(Self::PaymentHash(
118 hex::decode(identifier)?
119 .try_into()
120 .map_err(|_| Error::InvalidHash)?,
121 )),
122 "bolt12_payment_hash" => Ok(Self::Bolt12PaymentHash(
123 hex::decode(identifier)?
124 .try_into()
125 .map_err(|_| Error::InvalidHash)?,
126 )),
127 "custom" => Ok(Self::CustomId(identifier.to_string())),
128 "payment_id" => Ok(Self::PaymentId(
129 hex::decode(identifier)?
130 .try_into()
131 .map_err(|_| Error::InvalidHash)?,
132 )),
133 "quote_id" => {
134 Ok(Self::QuoteId(identifier.parse().map_err(|_| {
135 Error::Custom("Invalid QuoteId".to_string())
136 })?))
137 }
138 _ => Err(Error::UnsupportedPaymentOption),
139 }
140 }
141
142 pub fn kind(&self) -> String {
144 match self {
145 Self::Label(_) => "label".to_string(),
146 Self::OfferId(_) => "offer_id".to_string(),
147 Self::PaymentHash(_) => "payment_hash".to_string(),
148 Self::Bolt12PaymentHash(_) => "bolt12_payment_hash".to_string(),
149 Self::PaymentId(_) => "payment_id".to_string(),
150 Self::CustomId(_) => "custom".to_string(),
151 Self::QuoteId(_) => "quote_id".to_string(),
152 }
153 }
154}
155
156impl std::fmt::Display for PaymentIdentifier {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 match self {
159 Self::Label(l) => write!(f, "{l}"),
160 Self::OfferId(o) => write!(f, "{o}"),
161 Self::PaymentHash(h) => write!(f, "{}", hex::encode(h)),
162 Self::Bolt12PaymentHash(h) => write!(f, "{}", hex::encode(h)),
163 Self::PaymentId(h) => write!(f, "{}", hex::encode(h)),
164 Self::CustomId(c) => write!(f, "{c}"),
165 Self::QuoteId(q) => write!(f, "{q}"),
166 }
167 }
168}
169
170impl std::fmt::Debug for PaymentIdentifier {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 match self {
173 PaymentIdentifier::PaymentHash(h) => write!(f, "PaymentHash({})", hex::encode(h)),
174 PaymentIdentifier::Bolt12PaymentHash(h) => {
175 write!(f, "Bolt12PaymentHash({})", hex::encode(h))
176 }
177 PaymentIdentifier::PaymentId(h) => write!(f, "PaymentId({})", hex::encode(h)),
178 PaymentIdentifier::Label(s) => write!(f, "Label({})", s),
179 PaymentIdentifier::OfferId(s) => write!(f, "OfferId({})", s),
180 PaymentIdentifier::CustomId(s) => write!(f, "CustomId({})", s),
181 PaymentIdentifier::QuoteId(q) => write!(f, "QuoteId({})", q),
182 }
183 }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, Hash)]
188pub struct Bolt11IncomingPaymentOptions {
189 pub description: Option<String>,
191 pub amount: Amount<CurrencyUnit>,
193 pub unix_expiry: Option<u64>,
195}
196
197impl Default for Bolt11IncomingPaymentOptions {
198 fn default() -> Self {
199 Self {
200 description: None,
201 amount: Amount::new(0, CurrencyUnit::Sat),
202 unix_expiry: None,
203 }
204 }
205}
206
207#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
209pub struct Bolt12IncomingPaymentOptions {
210 pub description: Option<String>,
212 pub amount: Option<Amount<CurrencyUnit>>,
214 pub unix_expiry: Option<u64>,
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, Hash)]
220pub struct CustomIncomingPaymentOptions {
221 pub method: String,
223 pub description: Option<String>,
225 pub amount: Amount<CurrencyUnit>,
227 pub unix_expiry: Option<u64>,
229 pub extra_json: Option<String>,
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Hash)]
238pub struct OnchainIncomingPaymentOptions {
239 pub quote_id: QuoteId,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Hash)]
245pub enum IncomingPaymentOptions {
246 Bolt11(Bolt11IncomingPaymentOptions),
248 Bolt12(Box<Bolt12IncomingPaymentOptions>),
250 Custom(Box<CustomIncomingPaymentOptions>),
252 Onchain(OnchainIncomingPaymentOptions),
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Hash)]
258pub struct Bolt11OutgoingPaymentOptions {
259 pub bolt11: Bolt11Invoice,
261 pub max_fee_amount: Option<Amount<CurrencyUnit>>,
263 pub timeout_secs: Option<u64>,
265 pub melt_options: Option<MeltOptions>,
267 pub quote_id: QuoteId,
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Hash)]
276pub struct Bolt12OutgoingPaymentOptions {
277 pub offer: Offer,
279 pub max_fee_amount: Option<Amount<CurrencyUnit>>,
281 pub timeout_secs: Option<u64>,
283 pub melt_options: Option<MeltOptions>,
285 pub quote_id: QuoteId,
287}
288
289#[derive(Debug, Clone, PartialEq, Eq, Hash)]
291pub struct CustomOutgoingPaymentOptions {
292 pub method: String,
294 pub request: String,
296 pub max_fee_amount: Option<Amount<CurrencyUnit>>,
298 pub timeout_secs: Option<u64>,
300 pub melt_options: Option<MeltOptions>,
302 pub extra_json: Option<String>,
307 pub quote_id: QuoteId,
313}
314
315#[derive(Debug, Clone, PartialEq, Eq, Hash)]
317pub struct OnchainOutgoingPaymentOptions {
318 pub address: String,
320 pub amount: Amount<CurrencyUnit>,
322 pub max_fee_amount: Option<Amount<CurrencyUnit>>,
324 pub quote_id: QuoteId,
337 pub fee_index: Option<u32>,
339 pub metadata: Option<String>,
341}
342
343#[derive(Debug, Clone, PartialEq, Eq, Hash)]
345pub enum OutgoingPaymentOptions {
346 Bolt11(Box<Bolt11OutgoingPaymentOptions>),
348 Bolt12(Box<Bolt12OutgoingPaymentOptions>),
350 Custom(Box<CustomOutgoingPaymentOptions>),
352 Onchain(Box<OnchainOutgoingPaymentOptions>),
354}
355
356impl OutgoingPaymentOptions {
357 pub fn from_melt_quote_with_fee(
359 melt_quote: MeltQuote,
360 ) -> Result<OutgoingPaymentOptions, Error> {
361 let fee_reserve = melt_quote.fee_reserve();
362 let quote_id = melt_quote.id.clone();
363 match &melt_quote.request {
364 MeltPaymentRequest::Bolt11 { bolt11 } => Ok(OutgoingPaymentOptions::Bolt11(Box::new(
365 Bolt11OutgoingPaymentOptions {
366 max_fee_amount: Some(fee_reserve),
367 timeout_secs: None,
368 bolt11: bolt11.clone(),
369 melt_options: melt_quote.options,
370 quote_id,
371 },
372 ))),
373 MeltPaymentRequest::Bolt12 { offer } => {
374 let melt_options = match melt_quote.options {
375 Some(MeltOptions::Mpp { mpp: _ }) => return Err(Error::UnsupportedUnit),
376 Some(options) => Some(options),
377 _ => None,
378 };
379
380 Ok(OutgoingPaymentOptions::Bolt12(Box::new(
381 Bolt12OutgoingPaymentOptions {
382 max_fee_amount: Some(fee_reserve),
383 timeout_secs: None,
384 offer: *offer.clone(),
385 melt_options,
386 quote_id,
387 },
388 )))
389 }
390 MeltPaymentRequest::Custom { method, request } => Ok(OutgoingPaymentOptions::Custom(
391 Box::new(CustomOutgoingPaymentOptions {
392 method: method.to_string(),
393 request: request.to_string(),
394 max_fee_amount: Some(fee_reserve),
395 timeout_secs: None,
396 melt_options: melt_quote.options,
397 extra_json: None,
398 quote_id,
399 }),
400 )),
401 MeltPaymentRequest::Onchain { address } => Ok(OutgoingPaymentOptions::Onchain(
402 Box::new(OnchainOutgoingPaymentOptions {
403 address: address.clone(),
404 amount: melt_quote.amount(),
405 max_fee_amount: Some(fee_reserve),
406 quote_id: melt_quote.id,
407 fee_index: melt_quote.selected_fee_index,
408 metadata: None,
409 }),
410 )),
411 }
412 }
413}
414
415#[async_trait]
417pub trait MintPayment {
418 type Err: Into<Error> + From<Error>;
420
421 async fn start(&self) -> Result<(), Self::Err> {
424 Ok(())
426 }
427
428 async fn stop(&self) -> Result<(), Self::Err> {
431 Ok(())
433 }
434
435 async fn get_settings(&self) -> Result<SettingsResponse, Self::Err>;
437
438 async fn create_incoming_payment_request(
440 &self,
441 options: IncomingPaymentOptions,
442 ) -> Result<CreateIncomingPaymentResponse, Self::Err>;
443
444 async fn get_payment_quote(
447 &self,
448 unit: &CurrencyUnit,
449 options: OutgoingPaymentOptions,
450 ) -> Result<PaymentQuoteResponse, Self::Err>;
451
452 async fn make_payment(
454 &self,
455 unit: &CurrencyUnit,
456 options: OutgoingPaymentOptions,
457 ) -> Result<MakePaymentResponse, Self::Err>;
458
459 async fn wait_payment_event(
462 &self,
463 ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err>;
464
465 fn is_payment_event_stream_active(&self) -> bool;
467
468 fn cancel_payment_event_stream(&self);
470
471 async fn check_incoming_payment_status(
473 &self,
474 payment_identifier: &PaymentIdentifier,
475 ) -> Result<Vec<WaitPaymentResponse>, Self::Err>;
476
477 async fn check_outgoing_payment(
479 &self,
480 payment_identifier: &PaymentIdentifier,
481 ) -> Result<MakePaymentResponse, Self::Err>;
482}
483
484#[derive(Debug, Clone, Hash)]
486pub enum Event {
487 PaymentReceived(WaitPaymentResponse),
489 PaymentSuccessful {
491 quote_id: QuoteId,
493 details: MakePaymentResponse,
495 },
496 PaymentFailed {
498 quote_id: QuoteId,
500 reason: String,
502 },
503}
504
505#[derive(Debug, Clone, Hash)]
507pub struct WaitPaymentResponse {
508 pub payment_identifier: PaymentIdentifier,
511 pub payment_amount: Amount<CurrencyUnit>,
513 pub payment_id: String,
516}
517
518impl WaitPaymentResponse {
519 pub fn unit(&self) -> &CurrencyUnit {
521 self.payment_amount.unit()
522 }
523}
524
525#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
527pub struct CreateIncomingPaymentResponse {
528 pub request_lookup_id: PaymentIdentifier,
530 pub request: String,
532 pub expiry: Option<u64>,
534 #[serde(flatten, default)]
539 pub extra_json: Option<serde_json::Value>,
540}
541
542#[derive(Debug, Clone, Hash, PartialEq, Eq)]
544pub struct MakePaymentResponse {
545 pub payment_lookup_id: PaymentIdentifier,
553 pub payment_proof: Option<String>,
555 pub status: MeltQuoteState,
557 pub total_spent: Amount<CurrencyUnit>,
560}
561
562impl MakePaymentResponse {
563 pub fn unit(&self) -> &CurrencyUnit {
565 self.total_spent.unit()
566 }
567}
568
569#[derive(Debug, Clone, Hash, PartialEq, Eq)]
571pub struct PaymentQuoteResponse {
572 pub request_lookup_id: Option<PaymentIdentifier>,
581 pub amount: Amount<CurrencyUnit>,
583 pub fee: Amount<CurrencyUnit>,
585 pub state: MeltQuoteState,
587 pub extra_json: Option<serde_json::Value>,
589 pub estimated_blocks: Option<u32>,
594 pub fee_options: Option<Vec<MeltQuoteOnchainFeeOption>>,
606}
607
608impl PaymentQuoteResponse {
609 pub fn unit(&self) -> &CurrencyUnit {
611 self.amount.unit()
612 }
613}
614
615#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
617pub struct Bolt11Settings {
618 pub mpp: bool,
620 pub amountless: bool,
622 pub invoice_description: bool,
624}
625
626#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
628pub struct Bolt12Settings {
629 pub amountless: bool,
631}
632
633#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
635pub struct OnchainSettings {
636 pub confirmations: u32,
638 pub min_receive_amount_sat: u64,
640 pub min_send_amount_sat: u64,
642}
643
644#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
647pub struct SettingsResponse {
648 pub unit: String,
650 pub bolt11: Option<Bolt11Settings>,
652 pub bolt12: Option<Bolt12Settings>,
654 pub onchain: Option<OnchainSettings>,
656 #[serde(default)]
658 pub custom: std::collections::HashMap<String, String>,
659}
660
661impl From<SettingsResponse> for Value {
662 fn from(value: SettingsResponse) -> Self {
663 serde_json::to_value(value).unwrap_or(Value::Null)
664 }
665}
666
667impl TryFrom<Value> for SettingsResponse {
668 type Error = crate::error::Error;
669
670 fn try_from(value: Value) -> Result<Self, Self::Error> {
671 serde_json::from_value(value).map_err(|err| err.into())
672 }
673}
674
675#[derive(Debug, Clone)]
681#[cfg(feature = "prometheus")]
682pub struct MetricsMintPayment<T> {
683 inner: T,
684}
685#[cfg(feature = "prometheus")]
686impl<T> MetricsMintPayment<T>
687where
688 T: MintPayment,
689{
690 pub fn new(inner: T) -> Self {
692 Self { inner }
693 }
694
695 pub fn inner(&self) -> &T {
697 &self.inner
698 }
699
700 pub fn into_inner(self) -> T {
702 self.inner
703 }
704}
705
706#[async_trait]
707#[cfg(feature = "prometheus")]
708impl<T> MintPayment for MetricsMintPayment<T>
709where
710 T: MintPayment + Send + Sync,
711{
712 type Err = T::Err;
713
714 async fn start(&self) -> Result<(), Self::Err> {
715 let start = std::time::Instant::now();
716 METRICS.inc_in_flight_requests("start");
717
718 let result = self.inner.start().await;
719
720 let duration = start.elapsed().as_secs_f64();
721 METRICS.record_mint_operation_histogram("start", result.is_ok(), duration);
722 METRICS.dec_in_flight_requests("start");
723
724 result
725 }
726
727 async fn stop(&self) -> Result<(), Self::Err> {
728 let start = std::time::Instant::now();
729 METRICS.inc_in_flight_requests("stop");
730
731 let result = self.inner.stop().await;
732
733 let duration = start.elapsed().as_secs_f64();
734 METRICS.record_mint_operation_histogram("stop", result.is_ok(), duration);
735 METRICS.dec_in_flight_requests("stop");
736
737 result
738 }
739 async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
740 let start = std::time::Instant::now();
741 METRICS.inc_in_flight_requests("get_settings");
742
743 let result = self.inner.get_settings().await;
744
745 let duration = start.elapsed().as_secs_f64();
746 METRICS.record_mint_operation_histogram("get_settings", result.is_ok(), duration);
747 METRICS.dec_in_flight_requests("get_settings");
748
749 result
750 }
751
752 async fn create_incoming_payment_request(
753 &self,
754 options: IncomingPaymentOptions,
755 ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
756 let start = std::time::Instant::now();
757 METRICS.inc_in_flight_requests("create_incoming_payment_request");
758
759 let result = self.inner.create_incoming_payment_request(options).await;
760
761 let duration = start.elapsed().as_secs_f64();
762 METRICS.record_mint_operation_histogram(
763 "create_incoming_payment_request",
764 result.is_ok(),
765 duration,
766 );
767 METRICS.dec_in_flight_requests("create_incoming_payment_request");
768
769 result
770 }
771
772 async fn get_payment_quote(
773 &self,
774 unit: &CurrencyUnit,
775 options: OutgoingPaymentOptions,
776 ) -> Result<PaymentQuoteResponse, Self::Err> {
777 let start = std::time::Instant::now();
778 METRICS.inc_in_flight_requests("get_payment_quote");
779
780 let result = self.inner.get_payment_quote(unit, options).await;
781
782 let duration = start.elapsed().as_secs_f64();
783 let success = result.is_ok();
784
785 if let Ok(ref quote) = result {
786 let amount: f64 = quote.amount.value() as f64;
787 let fee: f64 = quote.fee.value() as f64;
788 METRICS.record_lightning_payment(amount, fee);
789 }
790
791 METRICS.record_mint_operation_histogram("get_payment_quote", success, duration);
792 METRICS.dec_in_flight_requests("get_payment_quote");
793
794 result
795 }
796 async fn wait_payment_event(
797 &self,
798 ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
799 let start = std::time::Instant::now();
800 METRICS.inc_in_flight_requests("wait_payment_event");
801
802 let result = self.inner.wait_payment_event().await;
803
804 let duration = start.elapsed().as_secs_f64();
805 let success = result.is_ok();
806
807 METRICS.record_mint_operation_histogram("wait_payment_event", success, duration);
808 METRICS.dec_in_flight_requests("wait_payment_event");
809
810 result
811 }
812
813 async fn make_payment(
814 &self,
815 unit: &CurrencyUnit,
816 options: OutgoingPaymentOptions,
817 ) -> Result<MakePaymentResponse, Self::Err> {
818 let start = std::time::Instant::now();
819 METRICS.inc_in_flight_requests("make_payment");
820
821 let result = self.inner.make_payment(unit, options).await;
822
823 let duration = start.elapsed().as_secs_f64();
824 let success = result.is_ok();
825
826 METRICS.record_mint_operation_histogram("make_payment", success, duration);
827 METRICS.dec_in_flight_requests("make_payment");
828
829 result
830 }
831
832 fn is_payment_event_stream_active(&self) -> bool {
833 self.inner.is_payment_event_stream_active()
834 }
835
836 fn cancel_payment_event_stream(&self) {
837 self.inner.cancel_payment_event_stream()
838 }
839
840 async fn check_incoming_payment_status(
841 &self,
842 payment_identifier: &PaymentIdentifier,
843 ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
844 let start = std::time::Instant::now();
845 METRICS.inc_in_flight_requests("check_incoming_payment_status");
846
847 let result = self
848 .inner
849 .check_incoming_payment_status(payment_identifier)
850 .await;
851
852 let duration = start.elapsed().as_secs_f64();
853 METRICS.record_mint_operation_histogram(
854 "check_incoming_payment_status",
855 result.is_ok(),
856 duration,
857 );
858 METRICS.dec_in_flight_requests("check_incoming_payment_status");
859
860 result
861 }
862
863 async fn check_outgoing_payment(
864 &self,
865 payment_identifier: &PaymentIdentifier,
866 ) -> Result<MakePaymentResponse, Self::Err> {
867 let start = std::time::Instant::now();
868 METRICS.inc_in_flight_requests("check_outgoing_payment");
869
870 let result = self.inner.check_outgoing_payment(payment_identifier).await;
871
872 let duration = start.elapsed().as_secs_f64();
873 let success = result.is_ok();
874
875 METRICS.record_mint_operation_histogram("check_outgoing_payment", success, duration);
876 METRICS.dec_in_flight_requests("check_outgoing_payment");
877
878 result
879 }
880}
881
882pub type DynMintPayment = std::sync::Arc<dyn MintPayment<Err = Error> + Send + Sync>;
884
885#[cfg(test)]
886mod tests {
887 use std::str::FromStr;
888
889 use super::*;
890 use crate::QuoteId;
891
892 #[test]
893 fn test_payment_identifier_quote_id_roundtrip() {
894 let quote_id = QuoteId::new_uuid();
895 let identifier = PaymentIdentifier::QuoteId(quote_id.clone());
896
897 let kind = identifier.kind();
898 assert_eq!(kind, "quote_id");
899
900 let display = identifier.to_string();
901 assert_eq!(display, quote_id.to_string());
902
903 let debug = format!("{:?}", identifier);
904 assert_eq!(debug, format!("QuoteId({})", quote_id));
905
906 let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
907 assert_eq!(parsed, identifier);
908 }
909
910 #[test]
911 fn test_payment_identifier_quote_id_base64_roundtrip() {
912 let quote_id_str = "SGVsbG8gV29ybGQh"; let identifier = PaymentIdentifier::QuoteId(QuoteId::from_str(quote_id_str).unwrap());
914
915 let kind = identifier.kind();
916 assert_eq!(kind, "quote_id");
917
918 let display = identifier.to_string();
919 assert_eq!(display, quote_id_str);
920
921 let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
922 assert_eq!(parsed, identifier);
923 }
924
925 #[test]
926 fn test_payment_identifier_unsupported_kind() {
927 let result = PaymentIdentifier::new("unsupported_kind", "123");
928 assert!(matches!(result, Err(Error::UnsupportedPaymentOption)));
929 }
930
931 #[test]
932 fn test_payment_identifier_invalid_quote_id() {
933 let result = PaymentIdentifier::new("quote_id", "invalid!@#quote");
935 assert!(matches!(result, Err(Error::Custom(_))));
936 }
937
938 #[test]
939 fn test_payment_identifier_invalid_hash() {
940 let result_hex = PaymentIdentifier::new("payment_hash", "not_hex!");
942 assert!(matches!(result_hex, Err(Error::Hex(_))));
943
944 let result_len = PaymentIdentifier::new("payment_hash", "00");
946 assert!(matches!(result_len, Err(Error::InvalidHash)));
947
948 let result_bolt12 = PaymentIdentifier::new("bolt12_payment_hash", "00");
950 assert!(matches!(result_bolt12, Err(Error::InvalidHash)));
951 }
952}
953
954#[test]
955fn test_payment_identifier_hash_variants_roundtrip() {
956 let dummy_hash = [1u8; 32];
957 let hex_encoded = hex::encode(dummy_hash);
958
959 let bolt12_identifier = PaymentIdentifier::Bolt12PaymentHash(dummy_hash);
961
962 let kind = bolt12_identifier.kind();
963 assert_eq!(kind, "bolt12_payment_hash");
964
965 let display = bolt12_identifier.to_string();
966 assert_eq!(display, hex_encoded);
967
968 let debug = format!("{:?}", bolt12_identifier);
969 assert_eq!(debug, format!("Bolt12PaymentHash({})", hex_encoded));
970
971 let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
972 assert_eq!(parsed, bolt12_identifier);
973
974 let dummy_hash_2 = [2u8; 32];
976 let hex_encoded_2 = hex::encode(dummy_hash_2);
977 let payment_id_identifier = PaymentIdentifier::PaymentId(dummy_hash_2);
978
979 let kind = payment_id_identifier.kind();
980 assert_eq!(kind, "payment_id");
981
982 let display = payment_id_identifier.to_string();
983 assert_eq!(display, hex_encoded_2);
984
985 let debug = format!("{:?}", payment_id_identifier);
986 assert_eq!(debug, format!("PaymentId({})", hex_encoded_2));
987
988 let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
989 assert_eq!(parsed, payment_id_identifier);
990}