1use std::convert::Infallible;
4use std::fmt;
5use std::pin::Pin;
6
7use async_trait::async_trait;
8use cashu::util::hex;
9use cashu::{Bolt11Invoice, MeltOptions};
10#[cfg(feature = "prometheus")]
11use cdk_prometheus::MintMetricGuard;
12use futures::Stream;
13use lightning::offers::offer::Offer;
14use lightning_invoice::ParseOrSemanticError;
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17use thiserror::Error;
18
19use crate::mint::{MeltPaymentRequest, MeltQuote};
20use crate::nuts::nut30::MeltQuoteOnchainFeeOption;
21use crate::nuts::{CurrencyUnit, MeltQuoteState, PublicKey};
22use crate::{Amount, QuoteId};
23
24#[derive(Debug, Error)]
26pub enum Error {
27 #[error("Invoice already paid")]
29 InvoiceAlreadyPaid,
30 #[error("Invoice pay is pending")]
32 InvoicePaymentPending,
33 #[error("Unsupported unit")]
35 UnsupportedUnit,
36 #[error("Unsupported payment option")]
38 UnsupportedPaymentOption,
39 #[error("Payment state is unknown")]
41 UnknownPaymentState,
42 #[error("Amount is not what is expected")]
44 AmountMismatch,
45 #[error("Invalid expiry")]
47 InvalidExpiry,
48 #[error(transparent)]
50 Backend(Box<dyn std::error::Error + Send + Sync>),
51 #[error(transparent)]
53 Onchain(Box<dyn std::error::Error + Send + Sync>),
54 #[error(transparent)]
56 Serde(#[from] serde_json::Error),
57 #[error(transparent)]
59 Anyhow(#[from] anyhow::Error),
60 #[error(transparent)]
62 Parse(#[from] ParseOrSemanticError),
63 #[error(transparent)]
65 Amount(#[from] crate::amount::Error),
66 #[error(transparent)]
68 NUT04(#[from] crate::nuts::nut04::Error),
69 #[error(transparent)]
71 NUT05(#[from] crate::nuts::nut05::Error),
72 #[error(transparent)]
74 NUT23(#[from] crate::nuts::nut23::Error),
75 #[error("Hex error")]
77 Hex(#[from] hex::Error),
78 #[error("Invalid hash")]
80 InvalidHash,
81 #[error("`{0}`")]
83 Custom(String),
84}
85
86impl From<Infallible> for Error {
87 fn from(_: Infallible) -> Self {
88 unreachable!("Infallible cannot be constructed")
89 }
90}
91
92#[derive(Clone, Hash, PartialEq, Eq, Deserialize, Serialize)]
94#[serde(tag = "type", content = "value")]
95pub enum PaymentIdentifier {
96 Label(String),
98 OfferId(String),
100 PaymentHash([u8; 32]),
102 Bolt12PaymentHash([u8; 32]),
104 PaymentId([u8; 32]),
106 CustomId(String),
108 QuoteId(QuoteId),
110}
111
112impl PaymentIdentifier {
113 pub fn new(kind: &str, identifier: &str) -> Result<Self, Error> {
115 match kind.to_lowercase().as_str() {
116 "label" => Ok(Self::Label(identifier.to_string())),
117 "offer_id" => Ok(Self::OfferId(identifier.to_string())),
118 "payment_hash" => Ok(Self::PaymentHash(
119 hex::decode(identifier)?
120 .try_into()
121 .map_err(|_| Error::InvalidHash)?,
122 )),
123 "bolt12_payment_hash" => Ok(Self::Bolt12PaymentHash(
124 hex::decode(identifier)?
125 .try_into()
126 .map_err(|_| Error::InvalidHash)?,
127 )),
128 "custom" => Ok(Self::CustomId(identifier.to_string())),
129 "payment_id" => Ok(Self::PaymentId(
130 hex::decode(identifier)?
131 .try_into()
132 .map_err(|_| Error::InvalidHash)?,
133 )),
134 "quote_id" => {
135 Ok(Self::QuoteId(identifier.parse().map_err(|_| {
136 Error::Custom("Invalid QuoteId".to_string())
137 })?))
138 }
139 _ => Err(Error::UnsupportedPaymentOption),
140 }
141 }
142
143 pub fn kind(&self) -> String {
145 match self {
146 Self::Label(_) => "label".to_string(),
147 Self::OfferId(_) => "offer_id".to_string(),
148 Self::PaymentHash(_) => "payment_hash".to_string(),
149 Self::Bolt12PaymentHash(_) => "bolt12_payment_hash".to_string(),
150 Self::PaymentId(_) => "payment_id".to_string(),
151 Self::CustomId(_) => "custom".to_string(),
152 Self::QuoteId(_) => "quote_id".to_string(),
153 }
154 }
155}
156
157impl std::fmt::Display for PaymentIdentifier {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 match self {
160 Self::Label(l) => write!(f, "{l}"),
161 Self::OfferId(o) => write!(f, "{o}"),
162 Self::PaymentHash(h) => write!(f, "{}", hex::encode(h)),
163 Self::Bolt12PaymentHash(h) => write!(f, "{}", hex::encode(h)),
164 Self::PaymentId(h) => write!(f, "{}", hex::encode(h)),
165 Self::CustomId(c) => write!(f, "{c}"),
166 Self::QuoteId(q) => write!(f, "{q}"),
167 }
168 }
169}
170
171impl std::fmt::Debug for PaymentIdentifier {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 match self {
174 PaymentIdentifier::PaymentHash(h) => write!(f, "PaymentHash({})", hex::encode(h)),
175 PaymentIdentifier::Bolt12PaymentHash(h) => {
176 write!(f, "Bolt12PaymentHash({})", hex::encode(h))
177 }
178 PaymentIdentifier::PaymentId(h) => write!(f, "PaymentId({})", hex::encode(h)),
179 PaymentIdentifier::Label(s) => write!(f, "Label({})", s),
180 PaymentIdentifier::OfferId(s) => write!(f, "OfferId({})", s),
181 PaymentIdentifier::CustomId(s) => write!(f, "CustomId({})", s),
182 PaymentIdentifier::QuoteId(q) => write!(f, "QuoteId({})", q),
183 }
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Hash)]
189pub struct Bolt11IncomingPaymentOptions {
190 pub description: Option<String>,
192 pub amount: Amount<CurrencyUnit>,
194 pub unix_expiry: Option<u64>,
196}
197
198impl Default for Bolt11IncomingPaymentOptions {
199 fn default() -> Self {
200 Self {
201 description: None,
202 amount: Amount::new(0, CurrencyUnit::Sat),
203 unix_expiry: None,
204 }
205 }
206}
207
208#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
210pub struct Bolt12IncomingPaymentOptions {
211 pub description: Option<String>,
213 pub amount: Option<Amount<CurrencyUnit>>,
215 pub unix_expiry: Option<u64>,
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Hash)]
221pub struct CustomIncomingPaymentOptions {
222 pub method: String,
224 pub description: Option<String>,
226 pub amount: Option<Amount<CurrencyUnit>>,
228 pub unix_expiry: Option<u64>,
230 pub extra_json: Option<String>,
235 pub quote_id: QuoteId,
242 pub pubkey: Option<PublicKey>,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Hash)]
253pub struct OnchainIncomingPaymentOptions {
254 pub quote_id: QuoteId,
256}
257
258#[derive(Debug, Clone, PartialEq, Eq, Hash)]
260pub enum IncomingPaymentOptions {
261 Bolt11(Bolt11IncomingPaymentOptions),
263 Bolt12(Box<Bolt12IncomingPaymentOptions>),
265 Custom(Box<CustomIncomingPaymentOptions>),
267 Onchain(OnchainIncomingPaymentOptions),
269}
270
271#[derive(Debug, Clone, PartialEq, Eq, Hash)]
273pub struct Bolt11OutgoingPaymentOptions {
274 pub bolt11: Bolt11Invoice,
276 pub max_fee_amount: Option<Amount<CurrencyUnit>>,
278 pub timeout_secs: Option<u64>,
280 pub melt_options: Option<MeltOptions>,
282 pub quote_id: QuoteId,
287}
288
289#[derive(Debug, Clone, PartialEq, Eq, Hash)]
291pub struct Bolt12OutgoingPaymentOptions {
292 pub offer: Offer,
294 pub max_fee_amount: Option<Amount<CurrencyUnit>>,
296 pub timeout_secs: Option<u64>,
298 pub melt_options: Option<MeltOptions>,
300 pub quote_id: QuoteId,
302}
303
304#[derive(Debug, Clone, PartialEq, Eq, Hash)]
306pub struct CustomOutgoingPaymentOptions {
307 pub method: String,
309 pub request: String,
311 pub amount: Option<Amount<CurrencyUnit>>,
313 pub max_fee_amount: Option<Amount<CurrencyUnit>>,
315 pub timeout_secs: Option<u64>,
317 pub melt_options: Option<MeltOptions>,
319 pub extra_json: Option<String>,
324 pub quote_id: QuoteId,
330}
331
332#[derive(Debug, Clone, PartialEq, Eq, Hash)]
334pub struct OnchainOutgoingPaymentOptions {
335 pub address: String,
337 pub amount: Amount<CurrencyUnit>,
339 pub max_fee_amount: Option<Amount<CurrencyUnit>>,
341 pub quote_id: QuoteId,
354 pub fee_index: Option<u32>,
356 pub metadata: Option<String>,
358}
359
360#[derive(Debug, Clone, PartialEq, Eq, Hash)]
362pub enum OutgoingPaymentOptions {
363 Bolt11(Box<Bolt11OutgoingPaymentOptions>),
365 Bolt12(Box<Bolt12OutgoingPaymentOptions>),
367 Custom(Box<CustomOutgoingPaymentOptions>),
369 Onchain(Box<OnchainOutgoingPaymentOptions>),
371}
372
373impl OutgoingPaymentOptions {
374 pub fn from_melt_quote_with_fee(
376 melt_quote: MeltQuote,
377 ) -> Result<OutgoingPaymentOptions, Error> {
378 let fee_reserve = melt_quote.fee_reserve();
379 let quote_id = melt_quote.id.clone();
380 match &melt_quote.request {
381 MeltPaymentRequest::Bolt11 { bolt11 } => Ok(OutgoingPaymentOptions::Bolt11(Box::new(
382 Bolt11OutgoingPaymentOptions {
383 max_fee_amount: Some(fee_reserve),
384 timeout_secs: None,
385 bolt11: bolt11.clone(),
386 melt_options: melt_quote.options,
387 quote_id,
388 },
389 ))),
390 MeltPaymentRequest::Bolt12 { offer } => {
391 let melt_options = match melt_quote.options {
392 Some(MeltOptions::Mpp { mpp: _ }) => return Err(Error::UnsupportedUnit),
393 Some(options) => Some(options),
394 _ => None,
395 };
396
397 Ok(OutgoingPaymentOptions::Bolt12(Box::new(
398 Bolt12OutgoingPaymentOptions {
399 max_fee_amount: Some(fee_reserve),
400 timeout_secs: None,
401 offer: *offer.clone(),
402 melt_options,
403 quote_id,
404 },
405 )))
406 }
407 MeltPaymentRequest::Custom { method, request } => Ok(OutgoingPaymentOptions::Custom(
408 Box::new(CustomOutgoingPaymentOptions {
409 method: method.to_string(),
410 request: request.to_string(),
411 amount: None,
413 max_fee_amount: Some(fee_reserve),
414 timeout_secs: None,
415 melt_options: melt_quote.options,
416 extra_json: melt_quote
417 .extra_json
418 .as_ref()
419 .map(serde_json::Value::to_string),
420 quote_id,
421 }),
422 )),
423 MeltPaymentRequest::Onchain { address } => Ok(OutgoingPaymentOptions::Onchain(
424 Box::new(OnchainOutgoingPaymentOptions {
425 address: address.clone(),
426 amount: melt_quote.amount(),
427 max_fee_amount: Some(fee_reserve),
428 quote_id: melt_quote.id,
429 fee_index: melt_quote.selected_fee_index,
430 metadata: None,
431 }),
432 )),
433 }
434 }
435}
436
437#[async_trait]
439pub trait MintPayment {
440 type Err: Into<Error> + From<Error>;
442
443 async fn start(&self) -> Result<(), Self::Err> {
446 Ok(())
448 }
449
450 async fn stop(&self) -> Result<(), Self::Err> {
453 Ok(())
455 }
456
457 async fn get_settings(&self) -> Result<SettingsResponse, Self::Err>;
459
460 async fn create_incoming_payment_request(
462 &self,
463 options: IncomingPaymentOptions,
464 ) -> Result<CreateIncomingPaymentResponse, Self::Err>;
465
466 async fn get_payment_quote(
469 &self,
470 unit: &CurrencyUnit,
471 options: OutgoingPaymentOptions,
472 ) -> Result<PaymentQuoteResponse, Self::Err>;
473
474 async fn make_payment(
476 &self,
477 unit: &CurrencyUnit,
478 options: OutgoingPaymentOptions,
479 ) -> Result<MakePaymentResponse, Self::Err>;
480
481 async fn wait_payment_event(
484 &self,
485 ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err>;
486
487 fn is_payment_event_stream_active(&self) -> bool;
489
490 fn cancel_payment_event_stream(&self);
492
493 async fn check_incoming_payment_status(
495 &self,
496 payment_identifier: &PaymentIdentifier,
497 ) -> Result<Vec<WaitPaymentResponse>, Self::Err>;
498
499 async fn check_outgoing_payment(
508 &self,
509 payment_identifier: &PaymentIdentifier,
510 ) -> Result<MakePaymentResponse, Self::Err>;
511}
512
513#[derive(Debug, Clone, Hash)]
515pub enum Event {
516 PaymentReceived(WaitPaymentResponse),
518 PaymentSuccessful {
520 quote_id: QuoteId,
522 details: MakePaymentResponse,
524 },
525 PaymentFailed {
527 quote_id: QuoteId,
529 reason: String,
531 },
532}
533
534#[derive(Debug, Clone, Hash)]
536pub struct WaitPaymentResponse {
537 pub payment_identifier: PaymentIdentifier,
540 pub payment_amount: Amount<CurrencyUnit>,
542 pub payment_id: String,
545}
546
547impl WaitPaymentResponse {
548 pub fn unit(&self) -> &CurrencyUnit {
550 self.payment_amount.unit()
551 }
552}
553
554#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
556pub struct CreateIncomingPaymentResponse {
557 pub request_lookup_id: PaymentIdentifier,
559 pub request: String,
561 pub expiry: Option<u64>,
563 #[serde(flatten, default)]
568 pub extra_json: Option<serde_json::Value>,
569}
570
571#[derive(Clone, Hash, PartialEq, Eq)]
573pub struct MakePaymentResponse {
574 pub payment_lookup_id: PaymentIdentifier,
582 pub payment_proof: Option<String>,
584 pub status: MeltQuoteState,
592 pub total_spent: Amount<CurrencyUnit>,
595}
596
597impl fmt::Debug for MakePaymentResponse {
598 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
599 f.debug_struct("MakePaymentResponse")
600 .field("payment_lookup_id", &self.payment_lookup_id)
601 .field(
602 "payment_proof",
603 &self.payment_proof.as_ref().map(|_| "[REDACTED]"),
604 )
605 .field("status", &self.status)
606 .field("total_spent", &self.total_spent)
607 .finish()
608 }
609}
610
611impl MakePaymentResponse {
612 pub fn unit(&self) -> &CurrencyUnit {
614 self.total_spent.unit()
615 }
616}
617
618#[derive(Debug, Clone, Hash, PartialEq, Eq)]
620pub struct PaymentQuoteResponse {
621 pub request_lookup_id: Option<PaymentIdentifier>,
630 pub amount: Amount<CurrencyUnit>,
632 pub fee: Amount<CurrencyUnit>,
634 pub state: MeltQuoteState,
636 pub extra_json: Option<serde_json::Value>,
638 pub estimated_blocks: Option<u32>,
643 pub fee_options: Option<Vec<MeltQuoteOnchainFeeOption>>,
655}
656
657impl PaymentQuoteResponse {
658 pub fn unit(&self) -> &CurrencyUnit {
660 self.amount.unit()
661 }
662}
663
664#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
666pub struct Bolt11Settings {
667 pub mpp: bool,
669 pub amountless: bool,
671 pub invoice_description: bool,
673}
674
675#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
677pub struct Bolt12Settings {
678 pub amountless: bool,
680 pub invoice_description: bool,
682}
683
684#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
686pub struct OnchainSettings {
687 pub confirmations: u32,
689 pub min_receive_amount_sat: u64,
691 pub min_send_amount_sat: u64,
693}
694
695#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
698pub struct SettingsResponse {
699 pub unit: String,
701 pub bolt11: Option<Bolt11Settings>,
703 pub bolt12: Option<Bolt12Settings>,
705 pub onchain: Option<OnchainSettings>,
707 #[serde(default)]
709 pub custom: std::collections::HashMap<String, String>,
710}
711
712impl From<SettingsResponse> for Value {
713 fn from(value: SettingsResponse) -> Self {
714 serde_json::to_value(value).unwrap_or(Value::Null)
715 }
716}
717
718impl TryFrom<Value> for SettingsResponse {
719 type Error = crate::error::Error;
720
721 fn try_from(value: Value) -> Result<Self, Self::Error> {
722 serde_json::from_value(value).map_err(|err| err.into())
723 }
724}
725
726#[derive(Debug, Clone)]
732#[cfg(feature = "prometheus")]
733pub struct MetricsMintPayment<T> {
734 inner: T,
735}
736#[cfg(feature = "prometheus")]
737impl<T> MetricsMintPayment<T>
738where
739 T: MintPayment,
740{
741 pub fn new(inner: T) -> Self {
743 Self { inner }
744 }
745
746 pub fn inner(&self) -> &T {
748 &self.inner
749 }
750
751 pub fn into_inner(self) -> T {
753 self.inner
754 }
755}
756
757#[async_trait]
758#[cfg(feature = "prometheus")]
759impl<T> MintPayment for MetricsMintPayment<T>
760where
761 T: MintPayment + Send + Sync,
762{
763 type Err = T::Err;
764
765 async fn start(&self) -> Result<(), Self::Err> {
766 let metrics = MintMetricGuard::new("start");
767
768 let result = self.inner.start().await;
769
770 metrics.record(result.is_ok());
771
772 result
773 }
774
775 async fn stop(&self) -> Result<(), Self::Err> {
776 let metrics = MintMetricGuard::new("stop");
777
778 let result = self.inner.stop().await;
779
780 metrics.record(result.is_ok());
781
782 result
783 }
784 async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
785 let metrics = MintMetricGuard::new("get_settings");
786
787 let result = self.inner.get_settings().await;
788
789 metrics.record(result.is_ok());
790
791 result
792 }
793
794 async fn create_incoming_payment_request(
795 &self,
796 options: IncomingPaymentOptions,
797 ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
798 let metrics = MintMetricGuard::new("create_incoming_payment_request");
799
800 let result = self.inner.create_incoming_payment_request(options).await;
801
802 metrics.record(result.is_ok());
803
804 result
805 }
806
807 async fn get_payment_quote(
808 &self,
809 unit: &CurrencyUnit,
810 options: OutgoingPaymentOptions,
811 ) -> Result<PaymentQuoteResponse, Self::Err> {
812 let metrics = MintMetricGuard::new("get_payment_quote");
813
814 let result = self.inner.get_payment_quote(unit, options).await;
815
816 metrics.record(result.is_ok());
817
818 result
819 }
820 async fn wait_payment_event(
821 &self,
822 ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
823 let metrics = MintMetricGuard::new("wait_payment_event");
824
825 let result = self.inner.wait_payment_event().await;
826
827 let success = result.is_ok();
828
829 metrics.record(success);
830
831 result
832 }
833
834 async fn make_payment(
835 &self,
836 unit: &CurrencyUnit,
837 options: OutgoingPaymentOptions,
838 ) -> Result<MakePaymentResponse, Self::Err> {
839 let metrics = MintMetricGuard::new("make_payment");
840
841 let result = self.inner.make_payment(unit, options).await;
842
843 let success = result.is_ok();
844
845 metrics.record(success);
846
847 result
848 }
849
850 fn is_payment_event_stream_active(&self) -> bool {
851 self.inner.is_payment_event_stream_active()
852 }
853
854 fn cancel_payment_event_stream(&self) {
855 self.inner.cancel_payment_event_stream()
856 }
857
858 async fn check_incoming_payment_status(
859 &self,
860 payment_identifier: &PaymentIdentifier,
861 ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
862 let metrics = MintMetricGuard::new("check_incoming_payment_status");
863
864 let result = self
865 .inner
866 .check_incoming_payment_status(payment_identifier)
867 .await;
868
869 metrics.record(result.is_ok());
870
871 result
872 }
873
874 async fn check_outgoing_payment(
875 &self,
876 payment_identifier: &PaymentIdentifier,
877 ) -> Result<MakePaymentResponse, Self::Err> {
878 let metrics = MintMetricGuard::new("check_outgoing_payment");
879
880 let result = self.inner.check_outgoing_payment(payment_identifier).await;
881
882 let success = result.is_ok();
883
884 metrics.record(success);
885
886 result
887 }
888}
889
890pub type DynMintPayment = std::sync::Arc<dyn MintPayment<Err = Error> + Send + Sync>;
892
893#[cfg(test)]
894mod tests {
895 use std::str::FromStr;
896
897 use super::*;
898 use crate::QuoteId;
899
900 #[test]
901 fn make_payment_response_debug_redacts_payment_proof() {
902 let secret = "backend-payment-preimage-secret";
903 let response = MakePaymentResponse {
904 payment_lookup_id: PaymentIdentifier::CustomId("public-lookup-id".to_string()),
905 payment_proof: Some(secret.to_string()),
906 status: MeltQuoteState::Paid,
907 total_spent: Amount::new(10, CurrencyUnit::Sat),
908 };
909
910 let debug = format!("{response:?}");
911
912 assert!(debug.contains("public-lookup-id"));
913 assert!(debug.contains("[REDACTED]"));
914 assert!(!debug.contains(secret));
915 }
916
917 #[test]
918 fn test_payment_identifier_quote_id_roundtrip() {
919 let quote_id = QuoteId::new();
920 let identifier = PaymentIdentifier::QuoteId(quote_id.clone());
921
922 let kind = identifier.kind();
923 assert_eq!(kind, "quote_id");
924
925 let display = identifier.to_string();
926 assert_eq!(display, quote_id.to_string());
927
928 let debug = format!("{:?}", identifier);
929 assert_eq!(debug, format!("QuoteId({})", quote_id));
930
931 let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
932 assert_eq!(parsed, identifier);
933 }
934
935 #[test]
936 fn test_payment_identifier_quote_id_base64_roundtrip() {
937 let quote_id_str = "SGVsbG8gV29ybGQh"; let identifier = PaymentIdentifier::QuoteId(QuoteId::from_str(quote_id_str).unwrap());
939
940 let kind = identifier.kind();
941 assert_eq!(kind, "quote_id");
942
943 let display = identifier.to_string();
944 assert_eq!(display, quote_id_str);
945
946 let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
947 assert_eq!(parsed, identifier);
948 }
949
950 #[test]
951 fn test_payment_identifier_unsupported_kind() {
952 let result = PaymentIdentifier::new("unsupported_kind", "123");
953 assert!(matches!(result, Err(Error::UnsupportedPaymentOption)));
954 }
955
956 #[test]
957 fn test_payment_identifier_invalid_quote_id() {
958 let result = PaymentIdentifier::new("quote_id", "invalid!@#quote");
960 assert!(matches!(result, Err(Error::Custom(_))));
961 }
962
963 #[test]
964 fn test_payment_identifier_invalid_hash() {
965 let result_hex = PaymentIdentifier::new("payment_hash", "not_hex!");
967 assert!(matches!(result_hex, Err(Error::Hex(_))));
968
969 let result_len = PaymentIdentifier::new("payment_hash", "00");
971 assert!(matches!(result_len, Err(Error::InvalidHash)));
972
973 let result_bolt12 = PaymentIdentifier::new("bolt12_payment_hash", "00");
975 assert!(matches!(result_bolt12, Err(Error::InvalidHash)));
976 }
977}
978
979#[test]
980fn test_payment_identifier_hash_variants_roundtrip() {
981 let dummy_hash = [1u8; 32];
982 let hex_encoded = hex::encode(dummy_hash);
983
984 let bolt12_identifier = PaymentIdentifier::Bolt12PaymentHash(dummy_hash);
986
987 let kind = bolt12_identifier.kind();
988 assert_eq!(kind, "bolt12_payment_hash");
989
990 let display = bolt12_identifier.to_string();
991 assert_eq!(display, hex_encoded);
992
993 let debug = format!("{:?}", bolt12_identifier);
994 assert_eq!(debug, format!("Bolt12PaymentHash({})", hex_encoded));
995
996 let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
997 assert_eq!(parsed, bolt12_identifier);
998
999 let dummy_hash_2 = [2u8; 32];
1001 let hex_encoded_2 = hex::encode(dummy_hash_2);
1002 let payment_id_identifier = PaymentIdentifier::PaymentId(dummy_hash_2);
1003
1004 let kind = payment_id_identifier.kind();
1005 assert_eq!(kind, "payment_id");
1006
1007 let display = payment_id_identifier.to_string();
1008 assert_eq!(display, hex_encoded_2);
1009
1010 let debug = format!("{:?}", payment_id_identifier);
1011 assert_eq!(debug, format!("PaymentId({})", hex_encoded_2));
1012
1013 let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
1014 assert_eq!(parsed, payment_id_identifier);
1015}