cdk_common/
mint.rs

1//! Mint types
2
3use std::fmt;
4use std::str::FromStr;
5
6use bitcoin::bip32::DerivationPath;
7use cashu::quote_id::QuoteId;
8use cashu::util::unix_time;
9use cashu::{
10    Bolt11Invoice, MeltOptions, MeltQuoteBolt11Response, MintQuoteBolt11Response,
11    MintQuoteBolt12Response, PaymentMethod,
12};
13use lightning::offers::offer::Offer;
14use serde::{Deserialize, Serialize};
15use tracing::instrument;
16use uuid::Uuid;
17
18use crate::nuts::{MeltQuoteState, MintQuoteState};
19use crate::payment::PaymentIdentifier;
20use crate::{Amount, CurrencyUnit, Error, Id, KeySetInfo, PublicKey};
21
22/// Operation kind for saga persistence
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum OperationKind {
26    /// Swap operation
27    Swap,
28    /// Mint operation
29    Mint,
30    /// Melt operation
31    Melt,
32}
33
34impl fmt::Display for OperationKind {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        match self {
37            OperationKind::Swap => write!(f, "swap"),
38            OperationKind::Mint => write!(f, "mint"),
39            OperationKind::Melt => write!(f, "melt"),
40        }
41    }
42}
43
44impl FromStr for OperationKind {
45    type Err = Error;
46    fn from_str(value: &str) -> Result<Self, Self::Err> {
47        let value = value.to_lowercase();
48        match value.as_str() {
49            "swap" => Ok(OperationKind::Swap),
50            "mint" => Ok(OperationKind::Mint),
51            "melt" => Ok(OperationKind::Melt),
52            _ => Err(Error::Custom(format!("Invalid operation kind: {value}"))),
53        }
54    }
55}
56
57/// States specific to swap saga
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum SwapSagaState {
61    /// Swap setup complete (proofs added, blinded messages added)
62    SetupComplete,
63    /// Outputs signed (signatures generated but not persisted)
64    Signed,
65}
66
67impl fmt::Display for SwapSagaState {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        match self {
70            SwapSagaState::SetupComplete => write!(f, "setup_complete"),
71            SwapSagaState::Signed => write!(f, "signed"),
72        }
73    }
74}
75
76impl FromStr for SwapSagaState {
77    type Err = Error;
78    fn from_str(value: &str) -> Result<Self, Self::Err> {
79        let value = value.to_lowercase();
80        match value.as_str() {
81            "setup_complete" => Ok(SwapSagaState::SetupComplete),
82            "signed" => Ok(SwapSagaState::Signed),
83            _ => Err(Error::Custom(format!("Invalid swap saga state: {value}"))),
84        }
85    }
86}
87
88/// States specific to melt saga
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum MeltSagaState {
92    /// Setup complete (proofs reserved, quote verified)
93    SetupComplete,
94    /// Payment attempted to Lightning network (may or may not have succeeded)
95    PaymentAttempted,
96}
97
98impl fmt::Display for MeltSagaState {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        match self {
101            MeltSagaState::SetupComplete => write!(f, "setup_complete"),
102            MeltSagaState::PaymentAttempted => write!(f, "payment_attempted"),
103        }
104    }
105}
106
107impl FromStr for MeltSagaState {
108    type Err = Error;
109    fn from_str(value: &str) -> Result<Self, Self::Err> {
110        let value = value.to_lowercase();
111        match value.as_str() {
112            "setup_complete" => Ok(MeltSagaState::SetupComplete),
113            "payment_attempted" => Ok(MeltSagaState::PaymentAttempted),
114            _ => Err(Error::Custom(format!("Invalid melt saga state: {}", value))),
115        }
116    }
117}
118
119/// Saga state for different operation types
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121#[serde(tag = "type", rename_all = "snake_case")]
122pub enum SagaStateEnum {
123    /// Swap saga states
124    Swap(SwapSagaState),
125    /// Melt saga states
126    Melt(MeltSagaState),
127    // Future: Mint saga states
128    // Mint(MintSagaState),
129}
130
131impl SagaStateEnum {
132    /// Create from string given operation kind
133    pub fn new(operation_kind: OperationKind, s: &str) -> Result<Self, Error> {
134        match operation_kind {
135            OperationKind::Swap => Ok(SagaStateEnum::Swap(SwapSagaState::from_str(s)?)),
136            OperationKind::Melt => Ok(SagaStateEnum::Melt(MeltSagaState::from_str(s)?)),
137            OperationKind::Mint => Err(Error::Custom("Mint saga not implemented yet".to_string())),
138        }
139    }
140
141    /// Get string representation of the state
142    pub fn state(&self) -> &str {
143        match self {
144            SagaStateEnum::Swap(state) => match state {
145                SwapSagaState::SetupComplete => "setup_complete",
146                SwapSagaState::Signed => "signed",
147            },
148            SagaStateEnum::Melt(state) => match state {
149                MeltSagaState::SetupComplete => "setup_complete",
150                MeltSagaState::PaymentAttempted => "payment_attempted",
151            },
152        }
153    }
154}
155
156/// Persisted saga for recovery
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158pub struct Saga {
159    /// Operation ID (correlation key)
160    pub operation_id: Uuid,
161    /// Operation kind (swap, mint, melt)
162    pub operation_kind: OperationKind,
163    /// Current saga state (operation-specific)
164    pub state: SagaStateEnum,
165    /// Blinded secrets (B values) from output blinded messages
166    pub blinded_secrets: Vec<PublicKey>,
167    /// Y values (public keys) from input proofs
168    pub input_ys: Vec<PublicKey>,
169    /// Quote ID for melt operations (used for payment status lookup during recovery)
170    /// None for swap operations
171    pub quote_id: Option<String>,
172    /// Unix timestamp when saga was created
173    pub created_at: u64,
174    /// Unix timestamp when saga was last updated
175    pub updated_at: u64,
176}
177
178impl Saga {
179    /// Create new swap saga
180    pub fn new_swap(
181        operation_id: Uuid,
182        state: SwapSagaState,
183        blinded_secrets: Vec<PublicKey>,
184        input_ys: Vec<PublicKey>,
185    ) -> Self {
186        let now = unix_time();
187        Self {
188            operation_id,
189            operation_kind: OperationKind::Swap,
190            state: SagaStateEnum::Swap(state),
191            blinded_secrets,
192            input_ys,
193            quote_id: None,
194            created_at: now,
195            updated_at: now,
196        }
197    }
198
199    /// Update swap saga state
200    pub fn update_swap_state(&mut self, new_state: SwapSagaState) {
201        self.state = SagaStateEnum::Swap(new_state);
202        self.updated_at = unix_time();
203    }
204
205    /// Create new melt saga
206    pub fn new_melt(
207        operation_id: Uuid,
208        state: MeltSagaState,
209        input_ys: Vec<PublicKey>,
210        blinded_secrets: Vec<PublicKey>,
211        quote_id: String,
212    ) -> Self {
213        let now = unix_time();
214        Self {
215            operation_id,
216            operation_kind: OperationKind::Melt,
217            state: SagaStateEnum::Melt(state),
218            blinded_secrets,
219            input_ys,
220            quote_id: Some(quote_id),
221            created_at: now,
222            updated_at: now,
223        }
224    }
225
226    /// Update melt saga state
227    pub fn update_melt_state(&mut self, new_state: MeltSagaState) {
228        self.state = SagaStateEnum::Melt(new_state);
229        self.updated_at = unix_time();
230    }
231}
232
233/// Operation
234pub enum Operation {
235    /// Mint
236    Mint(Uuid),
237    /// Melt
238    Melt(Uuid),
239    /// Swap
240    Swap(Uuid),
241}
242
243impl Operation {
244    /// Mint
245    pub fn new_mint() -> Self {
246        Self::Mint(Uuid::new_v4())
247    }
248    /// Melt
249    pub fn new_melt() -> Self {
250        Self::Melt(Uuid::new_v4())
251    }
252    /// Swap
253    pub fn new_swap() -> Self {
254        Self::Swap(Uuid::new_v4())
255    }
256
257    /// Operation id
258    pub fn id(&self) -> &Uuid {
259        match self {
260            Operation::Mint(id) => id,
261            Operation::Melt(id) => id,
262            Operation::Swap(id) => id,
263        }
264    }
265
266    /// Operation kind
267    pub fn kind(&self) -> &str {
268        match self {
269            Operation::Mint(_) => "mint",
270            Operation::Melt(_) => "melt",
271            Operation::Swap(_) => "swap",
272        }
273    }
274
275    /// From kind and i
276    pub fn from_kind_and_id(kind: &str, id: &str) -> Result<Self, Error> {
277        let uuid = Uuid::parse_str(id)?;
278        match kind {
279            "mint" => Ok(Self::Mint(uuid)),
280            "melt" => Ok(Self::Melt(uuid)),
281            "swap" => Ok(Self::Swap(uuid)),
282            _ => Err(Error::Custom(format!("Invalid operation kind: {kind}"))),
283        }
284    }
285}
286
287/// Mint Quote Info
288#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
289pub struct MintQuote {
290    /// Quote id
291    pub id: QuoteId,
292    /// Amount of quote
293    pub amount: Option<Amount>,
294    /// Unit of quote
295    pub unit: CurrencyUnit,
296    /// Quote payment request e.g. bolt11
297    pub request: String,
298    /// Expiration time of quote
299    pub expiry: u64,
300    /// Value used by ln backend to look up state of request
301    pub request_lookup_id: PaymentIdentifier,
302    /// Pubkey
303    pub pubkey: Option<PublicKey>,
304    /// Unix time quote was created
305    #[serde(default)]
306    pub created_time: u64,
307    /// Amount paid
308    #[serde(default)]
309    amount_paid: Amount,
310    /// Amount issued
311    #[serde(default)]
312    amount_issued: Amount,
313    /// Payment of payment(s) that filled quote
314    #[serde(default)]
315    pub payments: Vec<IncomingPayment>,
316    /// Payment Method
317    #[serde(default)]
318    pub payment_method: PaymentMethod,
319    /// Payment of payment(s) that filled quote
320    #[serde(default)]
321    pub issuance: Vec<Issuance>,
322}
323
324impl MintQuote {
325    /// Create new [`MintQuote`]
326    #[allow(clippy::too_many_arguments)]
327    pub fn new(
328        id: Option<QuoteId>,
329        request: String,
330        unit: CurrencyUnit,
331        amount: Option<Amount>,
332        expiry: u64,
333        request_lookup_id: PaymentIdentifier,
334        pubkey: Option<PublicKey>,
335        amount_paid: Amount,
336        amount_issued: Amount,
337        payment_method: PaymentMethod,
338        created_time: u64,
339        payments: Vec<IncomingPayment>,
340        issuance: Vec<Issuance>,
341    ) -> Self {
342        let id = id.unwrap_or_else(QuoteId::new_uuid);
343
344        Self {
345            id,
346            amount,
347            unit,
348            request,
349            expiry,
350            request_lookup_id,
351            pubkey,
352            created_time,
353            amount_paid,
354            amount_issued,
355            payment_method,
356            payments,
357            issuance,
358        }
359    }
360
361    /// Increment the amount paid on the mint quote by a given amount
362    #[instrument(skip(self))]
363    pub fn increment_amount_paid(
364        &mut self,
365        additional_amount: Amount,
366    ) -> Result<Amount, crate::Error> {
367        self.amount_paid = self
368            .amount_paid
369            .checked_add(additional_amount)
370            .ok_or(crate::Error::AmountOverflow)?;
371        Ok(self.amount_paid)
372    }
373
374    /// Amount paid
375    #[instrument(skip(self))]
376    pub fn amount_paid(&self) -> Amount {
377        self.amount_paid
378    }
379
380    /// Increment the amount issued on the mint quote by a given amount
381    #[instrument(skip(self))]
382    pub fn increment_amount_issued(
383        &mut self,
384        additional_amount: Amount,
385    ) -> Result<Amount, crate::Error> {
386        self.amount_issued = self
387            .amount_issued
388            .checked_add(additional_amount)
389            .ok_or(crate::Error::AmountOverflow)?;
390        Ok(self.amount_issued)
391    }
392
393    /// Amount issued
394    #[instrument(skip(self))]
395    pub fn amount_issued(&self) -> Amount {
396        self.amount_issued
397    }
398
399    /// Get state of mint quote
400    #[instrument(skip(self))]
401    pub fn state(&self) -> MintQuoteState {
402        self.compute_quote_state()
403    }
404
405    /// Existing payment ids of a mint quote
406    pub fn payment_ids(&self) -> Vec<&String> {
407        self.payments.iter().map(|a| &a.payment_id).collect()
408    }
409
410    /// Amount mintable
411    /// Returns the amount that is still available for minting.
412    ///
413    /// The value is computed as the difference between the total amount that
414    /// has been paid for this issuance (`self.amount_paid`) and the amount
415    /// that has already been issued (`self.amount_issued`). In other words,
416    pub fn amount_mintable(&self) -> Amount {
417        self.amount_paid - self.amount_issued
418    }
419
420    /// Add a payment ID to the list of payment IDs
421    ///
422    /// Returns an error if the payment ID is already in the list
423    #[instrument(skip(self))]
424    pub fn add_payment(
425        &mut self,
426        amount: Amount,
427        payment_id: String,
428        time: u64,
429    ) -> Result<(), crate::Error> {
430        let payment_ids = self.payment_ids();
431        if payment_ids.contains(&&payment_id) {
432            return Err(crate::Error::DuplicatePaymentId);
433        }
434
435        let payment = IncomingPayment::new(amount, payment_id, time);
436
437        self.payments.push(payment);
438        Ok(())
439    }
440
441    /// Compute quote state
442    #[instrument(skip(self))]
443    fn compute_quote_state(&self) -> MintQuoteState {
444        if self.amount_paid == Amount::ZERO && self.amount_issued == Amount::ZERO {
445            return MintQuoteState::Unpaid;
446        }
447
448        match self.amount_paid.cmp(&self.amount_issued) {
449            std::cmp::Ordering::Less => {
450                // self.amount_paid is less than other (amount issued)
451                // Handle case where paid amount is insufficient
452                tracing::error!("We should not have issued more then has been paid");
453                MintQuoteState::Issued
454            }
455            std::cmp::Ordering::Equal => {
456                // We do this extra check for backwards compatibility for quotes where amount paid/issed was not tracked
457                // self.amount_paid equals other (amount issued)
458                // Handle case where paid amount exactly matches
459                MintQuoteState::Issued
460            }
461            std::cmp::Ordering::Greater => {
462                // self.amount_paid is greater than other (amount issued)
463                // Handle case where paid amount exceeds required amount
464                MintQuoteState::Paid
465            }
466        }
467    }
468}
469
470/// Mint Payments
471#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
472pub struct IncomingPayment {
473    /// Amount
474    pub amount: Amount,
475    /// Pyament unix time
476    pub time: u64,
477    /// Payment id
478    pub payment_id: String,
479}
480
481impl IncomingPayment {
482    /// New [`IncomingPayment`]
483    pub fn new(amount: Amount, payment_id: String, time: u64) -> Self {
484        Self {
485            payment_id,
486            time,
487            amount,
488        }
489    }
490}
491
492/// Informattion about issued quote
493#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
494pub struct Issuance {
495    /// Amount
496    pub amount: Amount,
497    /// Time
498    pub time: u64,
499}
500
501impl Issuance {
502    /// Create new [`Issuance`]
503    pub fn new(amount: Amount, time: u64) -> Self {
504        Self { amount, time }
505    }
506}
507
508/// Melt Quote Info
509#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
510pub struct MeltQuote {
511    /// Quote id
512    pub id: QuoteId,
513    /// Quote unit
514    pub unit: CurrencyUnit,
515    /// Quote amount
516    pub amount: Amount,
517    /// Quote Payment request e.g. bolt11
518    pub request: MeltPaymentRequest,
519    /// Quote fee reserve
520    pub fee_reserve: Amount,
521    /// Quote state
522    pub state: MeltQuoteState,
523    /// Expiration time of quote
524    pub expiry: u64,
525    /// Payment preimage
526    pub payment_preimage: Option<String>,
527    /// Value used by ln backend to look up state of request
528    pub request_lookup_id: Option<PaymentIdentifier>,
529    /// Payment options
530    ///
531    /// Used for amountless invoices and MPP payments
532    pub options: Option<MeltOptions>,
533    /// Unix time quote was created
534    #[serde(default)]
535    pub created_time: u64,
536    /// Unix time quote was paid
537    pub paid_time: Option<u64>,
538    /// Payment method
539    #[serde(default)]
540    pub payment_method: PaymentMethod,
541}
542
543impl MeltQuote {
544    /// Create new [`MeltQuote`]
545    #[allow(clippy::too_many_arguments)]
546    pub fn new(
547        request: MeltPaymentRequest,
548        unit: CurrencyUnit,
549        amount: Amount,
550        fee_reserve: Amount,
551        expiry: u64,
552        request_lookup_id: Option<PaymentIdentifier>,
553        options: Option<MeltOptions>,
554        payment_method: PaymentMethod,
555    ) -> Self {
556        let id = Uuid::new_v4();
557
558        Self {
559            id: QuoteId::UUID(id),
560            amount,
561            unit,
562            request,
563            fee_reserve,
564            state: MeltQuoteState::Unpaid,
565            expiry,
566            payment_preimage: None,
567            request_lookup_id,
568            options,
569            created_time: unix_time(),
570            paid_time: None,
571            payment_method,
572        }
573    }
574}
575
576/// Mint Keyset Info
577#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
578pub struct MintKeySetInfo {
579    /// Keyset [`Id`]
580    pub id: Id,
581    /// Keyset [`CurrencyUnit`]
582    pub unit: CurrencyUnit,
583    /// Keyset active or inactive
584    /// Mint will only issue new signatures on active keysets
585    pub active: bool,
586    /// Starting unix time Keyset is valid from
587    pub valid_from: u64,
588    /// [`DerivationPath`] keyset
589    pub derivation_path: DerivationPath,
590    /// DerivationPath index of Keyset
591    pub derivation_path_index: Option<u32>,
592    /// Max order of keyset
593    pub max_order: u8,
594    /// Supported amounts
595    pub amounts: Vec<u64>,
596    /// Input Fee ppk
597    #[serde(default = "default_fee")]
598    pub input_fee_ppk: u64,
599    /// Final expiry
600    pub final_expiry: Option<u64>,
601}
602
603/// Default fee
604pub fn default_fee() -> u64 {
605    0
606}
607
608impl From<MintKeySetInfo> for KeySetInfo {
609    fn from(keyset_info: MintKeySetInfo) -> Self {
610        Self {
611            id: keyset_info.id,
612            unit: keyset_info.unit,
613            active: keyset_info.active,
614            input_fee_ppk: keyset_info.input_fee_ppk,
615            final_expiry: keyset_info.final_expiry,
616        }
617    }
618}
619
620impl From<MintQuote> for MintQuoteBolt11Response<QuoteId> {
621    fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt11Response<QuoteId> {
622        MintQuoteBolt11Response {
623            quote: mint_quote.id.clone(),
624            state: mint_quote.state(),
625            request: mint_quote.request,
626            expiry: Some(mint_quote.expiry),
627            pubkey: mint_quote.pubkey,
628            amount: mint_quote.amount,
629            unit: Some(mint_quote.unit.clone()),
630        }
631    }
632}
633
634impl From<MintQuote> for MintQuoteBolt11Response<String> {
635    fn from(quote: MintQuote) -> Self {
636        let quote: MintQuoteBolt11Response<QuoteId> = quote.into();
637
638        quote.into()
639    }
640}
641
642impl TryFrom<crate::mint::MintQuote> for MintQuoteBolt12Response<QuoteId> {
643    type Error = crate::Error;
644
645    fn try_from(mint_quote: crate::mint::MintQuote) -> Result<Self, Self::Error> {
646        Ok(MintQuoteBolt12Response {
647            quote: mint_quote.id.clone(),
648            request: mint_quote.request,
649            expiry: Some(mint_quote.expiry),
650            amount_paid: mint_quote.amount_paid,
651            amount_issued: mint_quote.amount_issued,
652            pubkey: mint_quote.pubkey.ok_or(crate::Error::PubkeyRequired)?,
653            amount: mint_quote.amount,
654            unit: mint_quote.unit,
655        })
656    }
657}
658
659impl TryFrom<MintQuote> for MintQuoteBolt12Response<String> {
660    type Error = crate::Error;
661
662    fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
663        let quote: MintQuoteBolt12Response<QuoteId> = quote.try_into()?;
664
665        Ok(quote.into())
666    }
667}
668
669impl From<&MeltQuote> for MeltQuoteBolt11Response<QuoteId> {
670    fn from(melt_quote: &MeltQuote) -> MeltQuoteBolt11Response<QuoteId> {
671        MeltQuoteBolt11Response {
672            quote: melt_quote.id.clone(),
673            payment_preimage: None,
674            change: None,
675            state: melt_quote.state,
676            paid: Some(melt_quote.state == MeltQuoteState::Paid),
677            expiry: melt_quote.expiry,
678            amount: melt_quote.amount,
679            fee_reserve: melt_quote.fee_reserve,
680            request: None,
681            unit: Some(melt_quote.unit.clone()),
682        }
683    }
684}
685
686impl From<MeltQuote> for MeltQuoteBolt11Response<QuoteId> {
687    fn from(melt_quote: MeltQuote) -> MeltQuoteBolt11Response<QuoteId> {
688        let paid = melt_quote.state == MeltQuoteState::Paid;
689        MeltQuoteBolt11Response {
690            quote: melt_quote.id.clone(),
691            amount: melt_quote.amount,
692            fee_reserve: melt_quote.fee_reserve,
693            paid: Some(paid),
694            state: melt_quote.state,
695            expiry: melt_quote.expiry,
696            payment_preimage: melt_quote.payment_preimage,
697            change: None,
698            request: Some(melt_quote.request.to_string()),
699            unit: Some(melt_quote.unit.clone()),
700        }
701    }
702}
703
704/// Payment request
705#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
706pub enum MeltPaymentRequest {
707    /// Bolt11 Payment
708    Bolt11 {
709        /// Bolt11 invoice
710        bolt11: Bolt11Invoice,
711    },
712    /// Bolt12 Payment
713    Bolt12 {
714        /// Offer
715        #[serde(with = "offer_serde")]
716        offer: Box<Offer>,
717    },
718}
719
720impl std::fmt::Display for MeltPaymentRequest {
721    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
722        match self {
723            MeltPaymentRequest::Bolt11 { bolt11 } => write!(f, "{bolt11}"),
724            MeltPaymentRequest::Bolt12 { offer } => write!(f, "{offer}"),
725        }
726    }
727}
728
729mod offer_serde {
730    use std::str::FromStr;
731
732    use serde::{self, Deserialize, Deserializer, Serializer};
733
734    use super::Offer;
735
736    pub fn serialize<S>(offer: &Offer, serializer: S) -> Result<S::Ok, S::Error>
737    where
738        S: Serializer,
739    {
740        let s = offer.to_string();
741        serializer.serialize_str(&s)
742    }
743
744    pub fn deserialize<'de, D>(deserializer: D) -> Result<Box<Offer>, D::Error>
745    where
746        D: Deserializer<'de>,
747    {
748        let s = String::deserialize(deserializer)?;
749        Ok(Box::new(Offer::from_str(&s).map_err(|_| {
750            serde::de::Error::custom("Invalid Bolt12 Offer")
751        })?))
752    }
753}