lwk/
payment_instructions.rs

1//! Payment instructions parsing and categorization
2
3use std::{fmt::Display, str::FromStr, sync::Arc};
4
5use crate::{blockdata::address::BitcoinAddress, types::AssetId, Address, LwkError};
6
7/// The kind/type of a payment category without the associated data
8#[derive(uniffi::Enum, Clone, Copy, Debug, PartialEq, Eq)]
9pub enum PaymentKind {
10    /// A Bitcoin address
11    BitcoinAddress,
12    /// A Liquid address
13    LiquidAddress,
14    /// A Lightning BOLT11 invoice
15    LightningInvoice,
16    /// A Lightning BOLT12 offer
17    LightningOffer,
18    /// An LNURL
19    LnUrl,
20    /// A BIP353 payment instruction (₿user@domain)
21    Bip353,
22    /// A BIP21 URI
23    Bip21,
24    /// A BIP321 URI (BIP21 without address but with payment method)
25    Bip321,
26    /// A Liquid BIP21 URI with amount and asset
27    LiquidBip21,
28}
29
30impl From<lwk_payment_instructions::PaymentKind> for PaymentKind {
31    fn from(kind: lwk_payment_instructions::PaymentKind) -> Self {
32        match kind {
33            lwk_payment_instructions::PaymentKind::BitcoinAddress => PaymentKind::BitcoinAddress,
34            lwk_payment_instructions::PaymentKind::LiquidAddress => PaymentKind::LiquidAddress,
35            lwk_payment_instructions::PaymentKind::LightningInvoice => {
36                PaymentKind::LightningInvoice
37            }
38            lwk_payment_instructions::PaymentKind::LightningOffer => PaymentKind::LightningOffer,
39            lwk_payment_instructions::PaymentKind::LnUrl => PaymentKind::LnUrl,
40            lwk_payment_instructions::PaymentKind::Bip353 => PaymentKind::Bip353,
41            lwk_payment_instructions::PaymentKind::Bip21 => PaymentKind::Bip21,
42            lwk_payment_instructions::PaymentKind::Bip321 => PaymentKind::Bip321,
43            lwk_payment_instructions::PaymentKind::LiquidBip21 => PaymentKind::LiquidBip21,
44            _ => unreachable!("Unknown PaymentCategoryKind variant"),
45        }
46    }
47}
48
49/// Liquid BIP21 payment details
50#[derive(uniffi::Record, Clone)]
51pub struct LiquidBip21 {
52    /// The Liquid address
53    pub address: Arc<Address>,
54    /// The asset identifier
55    pub asset: AssetId,
56    /// The amount in satoshis
57    pub amount: u64,
58}
59
60/// A parsed payment category from a payment instruction string.
61///
62/// This can be a Bitcoin address, Liquid address, Lightning invoice,
63/// Lightning offer, LNURL, BIP353, BIP21 URI, or Liquid BIP21 URI.
64#[derive(uniffi::Object)]
65pub struct Payment {
66    inner: lwk_payment_instructions::Payment,
67}
68
69impl Display for Payment {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        use lwk_payment_instructions::Payment as P;
72        match &self.inner {
73            P::BitcoinAddress(addr) => write!(f, "{}", addr.clone().assume_checked()),
74            P::LiquidAddress(addr) => write!(f, "{addr}"),
75            P::LightningInvoice(invoice) => write!(f, "{invoice}"),
76            P::LightningOffer(offer) => write!(f, "{offer}"),
77            P::LnUrlCat(lnurl) => write!(f, "{lnurl}"),
78            P::Bip353(s) => write!(f, "{s}"),
79            P::Bip21(s) => write!(f, "{s}"),
80            P::Bip321(s) => write!(f, "{s}"),
81            P::LiquidBip21(bip21) => write!(f, "{}", bip21.address),
82            _ => write!(f, "{:?}", self.inner),
83        }
84    }
85}
86
87#[uniffi::export]
88impl Payment {
89    /// Parse a payment instruction string into a PaymentCategory
90    #[uniffi::constructor]
91    pub fn new(s: &str) -> Result<Arc<Self>, LwkError> {
92        let inner = lwk_payment_instructions::Payment::from_str(s)
93            .map_err(|e| LwkError::Generic { msg: e })?;
94        Ok(Arc::new(Self { inner }))
95    }
96
97    /// Returns the kind of payment category
98    pub fn kind(&self) -> PaymentKind {
99        self.inner.kind().into()
100    }
101
102    /// Returns the Bitcoin address if this is a BitcoinAddress category, None otherwise
103    ///
104    /// Returns the address portion of the original input string
105    pub fn bitcoin_address(&self) -> Option<Arc<BitcoinAddress>> {
106        self.inner
107            .bitcoin_address()
108            .map(|addr| Arc::new(addr.clone().into()))
109    }
110
111    /// Returns the Liquid address if this is a LiquidAddress category, None otherwise
112    pub fn liquid_address(&self) -> Option<Arc<Address>> {
113        self.inner
114            .liquid_address()
115            .map(|addr| Arc::new(Address::from(addr.clone())))
116    }
117
118    /// Returns the Lightning invoice if this is a `LightningInvoice` category, `None` otherwise
119    #[cfg(feature = "lightning")]
120    pub fn lightning_invoice(&self) -> Option<Arc<crate::Bolt11Invoice>> {
121        self.inner
122            .lightning_invoice()
123            .and_then(|inv| crate::Bolt11Invoice::new(&inv.to_string()).ok())
124    }
125
126    /// Returns the Lightning offer as a string if this is a LightningOffer category, None otherwise
127    pub fn lightning_offer(&self) -> Option<String> {
128        self.inner.lightning_offer().map(|offer| offer.to_string())
129    }
130
131    /// Returns the LNURL as a string if this is an LnUrl category, None otherwise
132    pub fn lnurl(&self) -> Option<String> {
133        self.inner.lnurl().map(|lnurl| lnurl.to_string())
134    }
135
136    /// Returns the BIP353 address (without the ₿ prefix) if this is a Bip353 category, None otherwise
137    pub fn bip353(&self) -> Option<String> {
138        self.inner.bip353().map(|s| s.to_string())
139    }
140
141    /// Returns the BIP21 URI if this is a Bip21 category, None otherwise
142    pub fn bip21(&self) -> Option<Arc<crate::bip21::Bip21>> {
143        self.inner
144            .bip21()
145            .map(|bip21| Arc::new(crate::bip21::Bip21::from(bip21.clone())))
146    }
147
148    /// Returns the BIP321 URI if this is a Bip321 category, None otherwise
149    pub fn bip321(&self) -> Option<Arc<crate::bip321::Bip321>> {
150        self.inner
151            .bip321()
152            .map(|bip321| Arc::new(crate::bip321::Bip321::from(bip321.clone())))
153    }
154
155    /// Returns the Liquid BIP21 details if this is a LiquidBip21 category, None otherwise
156    pub fn liquid_bip21(&self) -> Option<LiquidBip21> {
157        self.inner.liquid_bip21().map(|bip21| LiquidBip21 {
158            address: Arc::new(Address::from(bip21.address.clone())),
159            asset: bip21.asset.into(),
160            amount: bip21.amount,
161        })
162    }
163
164    /// Returns a `LightningPayment`` if this category is payable via Lightning
165    ///
166    /// Returns `Some` for `LightningInvoice`, `LightningOffer`, and `LnUrl` categories.
167    /// The returned `LightningPayment` can be used with `BoltzSession::prepare_pay()`.
168    #[cfg(feature = "lightning")]
169    pub fn lightning_payment(&self) -> Option<Arc<crate::LightningPayment>> {
170        use lwk_payment_instructions::Payment as P;
171        match &self.inner {
172            P::LightningInvoice(invoice) => crate::LightningPayment::new(&invoice.to_string()).ok(),
173            P::LightningOffer(offer) => crate::LightningPayment::new(&offer.to_string()).ok(),
174            P::LnUrlCat(lnurl) => crate::LightningPayment::new(&lnurl.to_string()).ok(),
175            _ => None,
176        }
177    }
178}