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(
501 &self,
502 payment_identifier: &PaymentIdentifier,
503 ) -> Result<MakePaymentResponse, Self::Err>;
504}
505
506#[derive(Debug, Clone, Hash)]
508pub enum Event {
509 PaymentReceived(WaitPaymentResponse),
511 PaymentSuccessful {
513 quote_id: QuoteId,
515 details: MakePaymentResponse,
517 },
518 PaymentFailed {
520 quote_id: QuoteId,
522 reason: String,
524 },
525}
526
527#[derive(Debug, Clone, Hash)]
529pub struct WaitPaymentResponse {
530 pub payment_identifier: PaymentIdentifier,
533 pub payment_amount: Amount<CurrencyUnit>,
535 pub payment_id: String,
538}
539
540impl WaitPaymentResponse {
541 pub fn unit(&self) -> &CurrencyUnit {
543 self.payment_amount.unit()
544 }
545}
546
547#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
549pub struct CreateIncomingPaymentResponse {
550 pub request_lookup_id: PaymentIdentifier,
552 pub request: String,
554 pub expiry: Option<u64>,
556 #[serde(flatten, default)]
561 pub extra_json: Option<serde_json::Value>,
562}
563
564#[derive(Clone, Hash, PartialEq, Eq)]
566pub struct MakePaymentResponse {
567 pub payment_lookup_id: PaymentIdentifier,
575 pub payment_proof: Option<String>,
577 pub status: MeltQuoteState,
585 pub total_spent: Amount<CurrencyUnit>,
588}
589
590impl fmt::Debug for MakePaymentResponse {
591 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
592 f.debug_struct("MakePaymentResponse")
593 .field("payment_lookup_id", &self.payment_lookup_id)
594 .field(
595 "payment_proof",
596 &self.payment_proof.as_ref().map(|_| "[REDACTED]"),
597 )
598 .field("status", &self.status)
599 .field("total_spent", &self.total_spent)
600 .finish()
601 }
602}
603
604impl MakePaymentResponse {
605 pub fn unit(&self) -> &CurrencyUnit {
607 self.total_spent.unit()
608 }
609}
610
611#[derive(Debug, Clone, Hash, PartialEq, Eq)]
613pub struct PaymentQuoteResponse {
614 pub request_lookup_id: Option<PaymentIdentifier>,
623 pub amount: Amount<CurrencyUnit>,
625 pub fee: Amount<CurrencyUnit>,
627 pub state: MeltQuoteState,
629 pub extra_json: Option<serde_json::Value>,
631 pub estimated_blocks: Option<u32>,
636 pub fee_options: Option<Vec<MeltQuoteOnchainFeeOption>>,
648}
649
650impl PaymentQuoteResponse {
651 pub fn unit(&self) -> &CurrencyUnit {
653 self.amount.unit()
654 }
655}
656
657#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
659pub struct Bolt11Settings {
660 pub mpp: bool,
662 pub amountless: bool,
664 pub invoice_description: bool,
666}
667
668#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
670pub struct Bolt12Settings {
671 pub amountless: bool,
673}
674
675#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
677pub struct OnchainSettings {
678 pub confirmations: u32,
680 pub min_receive_amount_sat: u64,
682 pub min_send_amount_sat: u64,
684}
685
686#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
689pub struct SettingsResponse {
690 pub unit: String,
692 pub bolt11: Option<Bolt11Settings>,
694 pub bolt12: Option<Bolt12Settings>,
696 pub onchain: Option<OnchainSettings>,
698 #[serde(default)]
700 pub custom: std::collections::HashMap<String, String>,
701}
702
703impl From<SettingsResponse> for Value {
704 fn from(value: SettingsResponse) -> Self {
705 serde_json::to_value(value).unwrap_or(Value::Null)
706 }
707}
708
709impl TryFrom<Value> for SettingsResponse {
710 type Error = crate::error::Error;
711
712 fn try_from(value: Value) -> Result<Self, Self::Error> {
713 serde_json::from_value(value).map_err(|err| err.into())
714 }
715}
716
717#[derive(Debug, Clone)]
723#[cfg(feature = "prometheus")]
724pub struct MetricsMintPayment<T> {
725 inner: T,
726}
727#[cfg(feature = "prometheus")]
728impl<T> MetricsMintPayment<T>
729where
730 T: MintPayment,
731{
732 pub fn new(inner: T) -> Self {
734 Self { inner }
735 }
736
737 pub fn inner(&self) -> &T {
739 &self.inner
740 }
741
742 pub fn into_inner(self) -> T {
744 self.inner
745 }
746}
747
748#[async_trait]
749#[cfg(feature = "prometheus")]
750impl<T> MintPayment for MetricsMintPayment<T>
751where
752 T: MintPayment + Send + Sync,
753{
754 type Err = T::Err;
755
756 async fn start(&self) -> Result<(), Self::Err> {
757 let metrics = MintMetricGuard::new("start");
758
759 let result = self.inner.start().await;
760
761 metrics.record(result.is_ok());
762
763 result
764 }
765
766 async fn stop(&self) -> Result<(), Self::Err> {
767 let metrics = MintMetricGuard::new("stop");
768
769 let result = self.inner.stop().await;
770
771 metrics.record(result.is_ok());
772
773 result
774 }
775 async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
776 let metrics = MintMetricGuard::new("get_settings");
777
778 let result = self.inner.get_settings().await;
779
780 metrics.record(result.is_ok());
781
782 result
783 }
784
785 async fn create_incoming_payment_request(
786 &self,
787 options: IncomingPaymentOptions,
788 ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
789 let metrics = MintMetricGuard::new("create_incoming_payment_request");
790
791 let result = self.inner.create_incoming_payment_request(options).await;
792
793 metrics.record(result.is_ok());
794
795 result
796 }
797
798 async fn get_payment_quote(
799 &self,
800 unit: &CurrencyUnit,
801 options: OutgoingPaymentOptions,
802 ) -> Result<PaymentQuoteResponse, Self::Err> {
803 let metrics = MintMetricGuard::new("get_payment_quote");
804
805 let result = self.inner.get_payment_quote(unit, options).await;
806
807 metrics.record(result.is_ok());
808
809 result
810 }
811 async fn wait_payment_event(
812 &self,
813 ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
814 let metrics = MintMetricGuard::new("wait_payment_event");
815
816 let result = self.inner.wait_payment_event().await;
817
818 let success = result.is_ok();
819
820 metrics.record(success);
821
822 result
823 }
824
825 async fn make_payment(
826 &self,
827 unit: &CurrencyUnit,
828 options: OutgoingPaymentOptions,
829 ) -> Result<MakePaymentResponse, Self::Err> {
830 let metrics = MintMetricGuard::new("make_payment");
831
832 let result = self.inner.make_payment(unit, options).await;
833
834 let success = result.is_ok();
835
836 metrics.record(success);
837
838 result
839 }
840
841 fn is_payment_event_stream_active(&self) -> bool {
842 self.inner.is_payment_event_stream_active()
843 }
844
845 fn cancel_payment_event_stream(&self) {
846 self.inner.cancel_payment_event_stream()
847 }
848
849 async fn check_incoming_payment_status(
850 &self,
851 payment_identifier: &PaymentIdentifier,
852 ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
853 let metrics = MintMetricGuard::new("check_incoming_payment_status");
854
855 let result = self
856 .inner
857 .check_incoming_payment_status(payment_identifier)
858 .await;
859
860 metrics.record(result.is_ok());
861
862 result
863 }
864
865 async fn check_outgoing_payment(
866 &self,
867 payment_identifier: &PaymentIdentifier,
868 ) -> Result<MakePaymentResponse, Self::Err> {
869 let metrics = MintMetricGuard::new("check_outgoing_payment");
870
871 let result = self.inner.check_outgoing_payment(payment_identifier).await;
872
873 let success = result.is_ok();
874
875 metrics.record(success);
876
877 result
878 }
879}
880
881pub type DynMintPayment = std::sync::Arc<dyn MintPayment<Err = Error> + Send + Sync>;
883
884#[cfg(test)]
885mod tests {
886 use std::str::FromStr;
887
888 use super::*;
889 use crate::QuoteId;
890
891 #[test]
892 fn make_payment_response_debug_redacts_payment_proof() {
893 let secret = "backend-payment-preimage-secret";
894 let response = MakePaymentResponse {
895 payment_lookup_id: PaymentIdentifier::CustomId("public-lookup-id".to_string()),
896 payment_proof: Some(secret.to_string()),
897 status: MeltQuoteState::Paid,
898 total_spent: Amount::new(10, CurrencyUnit::Sat),
899 };
900
901 let debug = format!("{response:?}");
902
903 assert!(debug.contains("public-lookup-id"));
904 assert!(debug.contains("[REDACTED]"));
905 assert!(!debug.contains(secret));
906 }
907
908 #[test]
909 fn test_payment_identifier_quote_id_roundtrip() {
910 let quote_id = QuoteId::new();
911 let identifier = PaymentIdentifier::QuoteId(quote_id.clone());
912
913 let kind = identifier.kind();
914 assert_eq!(kind, "quote_id");
915
916 let display = identifier.to_string();
917 assert_eq!(display, quote_id.to_string());
918
919 let debug = format!("{:?}", identifier);
920 assert_eq!(debug, format!("QuoteId({})", quote_id));
921
922 let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
923 assert_eq!(parsed, identifier);
924 }
925
926 #[test]
927 fn test_payment_identifier_quote_id_base64_roundtrip() {
928 let quote_id_str = "SGVsbG8gV29ybGQh"; let identifier = PaymentIdentifier::QuoteId(QuoteId::from_str(quote_id_str).unwrap());
930
931 let kind = identifier.kind();
932 assert_eq!(kind, "quote_id");
933
934 let display = identifier.to_string();
935 assert_eq!(display, quote_id_str);
936
937 let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
938 assert_eq!(parsed, identifier);
939 }
940
941 #[test]
942 fn test_payment_identifier_unsupported_kind() {
943 let result = PaymentIdentifier::new("unsupported_kind", "123");
944 assert!(matches!(result, Err(Error::UnsupportedPaymentOption)));
945 }
946
947 #[test]
948 fn test_payment_identifier_invalid_quote_id() {
949 let result = PaymentIdentifier::new("quote_id", "invalid!@#quote");
951 assert!(matches!(result, Err(Error::Custom(_))));
952 }
953
954 #[test]
955 fn test_payment_identifier_invalid_hash() {
956 let result_hex = PaymentIdentifier::new("payment_hash", "not_hex!");
958 assert!(matches!(result_hex, Err(Error::Hex(_))));
959
960 let result_len = PaymentIdentifier::new("payment_hash", "00");
962 assert!(matches!(result_len, Err(Error::InvalidHash)));
963
964 let result_bolt12 = PaymentIdentifier::new("bolt12_payment_hash", "00");
966 assert!(matches!(result_bolt12, Err(Error::InvalidHash)));
967 }
968}
969
970#[test]
971fn test_payment_identifier_hash_variants_roundtrip() {
972 let dummy_hash = [1u8; 32];
973 let hex_encoded = hex::encode(dummy_hash);
974
975 let bolt12_identifier = PaymentIdentifier::Bolt12PaymentHash(dummy_hash);
977
978 let kind = bolt12_identifier.kind();
979 assert_eq!(kind, "bolt12_payment_hash");
980
981 let display = bolt12_identifier.to_string();
982 assert_eq!(display, hex_encoded);
983
984 let debug = format!("{:?}", bolt12_identifier);
985 assert_eq!(debug, format!("Bolt12PaymentHash({})", hex_encoded));
986
987 let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
988 assert_eq!(parsed, bolt12_identifier);
989
990 let dummy_hash_2 = [2u8; 32];
992 let hex_encoded_2 = hex::encode(dummy_hash_2);
993 let payment_id_identifier = PaymentIdentifier::PaymentId(dummy_hash_2);
994
995 let kind = payment_id_identifier.kind();
996 assert_eq!(kind, "payment_id");
997
998 let display = payment_id_identifier.to_string();
999 assert_eq!(display, hex_encoded_2);
1000
1001 let debug = format!("{:?}", payment_id_identifier);
1002 assert_eq!(debug, format!("PaymentId({})", hex_encoded_2));
1003
1004 let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
1005 assert_eq!(parsed, payment_id_identifier);
1006}