1use crate::error::LightningError;
7use crate::payment::PaymentHash;
8use crate::route::RouteHint;
9use std::time::{SystemTime, UNIX_EPOCH};
10
11#[derive(Debug, Clone)]
13pub struct Bolt11Invoice {
14 #[allow(dead_code)]
16 hrp: String,
17 data: InvoiceData,
19 raw: String,
21}
22
23impl Bolt11Invoice {
24 pub fn parse(invoice: &str) -> Result<Self, LightningError> {
29 let invoice = invoice.to_lowercase();
30
31 if !invoice.starts_with("ln") {
33 return Err(LightningError::InvalidInvoice(
34 "Invoice must start with 'ln'".into(),
35 ));
36 }
37
38 let separator_pos = invoice.rfind('1').ok_or_else(|| {
40 LightningError::InvalidInvoice("Missing separator '1'".into())
41 })?;
42
43 let hrp = invoice[..separator_pos].to_string();
44
45 let network = if hrp.starts_with("lnbc") {
47 Network::Mainnet
48 } else if hrp.starts_with("lntb") {
49 Network::Testnet
50 } else if hrp.starts_with("lnbcrt") {
51 Network::Regtest
52 } else {
53 return Err(LightningError::InvalidInvoice(format!(
54 "Unknown network prefix: {}",
55 hrp
56 )));
57 };
58
59 let amount_msat = Self::parse_amount(&hrp)?;
61
62 let data = InvoiceData {
65 network,
66 amount_msat,
67 timestamp: SystemTime::now()
68 .duration_since(UNIX_EPOCH)
69 .unwrap_or_default()
70 .as_secs(),
71 expiry: 3600, payment_hash: None,
73 description: None,
74 description_hash: None,
75 payee_pubkey: None,
76 route_hints: Vec::new(),
77 min_final_cltv_expiry: 18,
78 };
79
80 Ok(Self {
81 hrp,
82 data,
83 raw: invoice,
84 })
85 }
86
87 fn parse_amount(hrp: &str) -> Result<Option<u64>, LightningError> {
89 let amount_str = if let Some(stripped) = hrp.strip_prefix("lnbcrt") {
91 stripped
92 } else if let Some(stripped) = hrp.strip_prefix("lnbc") {
93 stripped
94 } else if let Some(stripped) = hrp.strip_prefix("lntb") {
95 stripped
96 } else {
97 return Ok(None);
98 };
99
100 if amount_str.is_empty() {
101 return Ok(None);
102 }
103
104 let (num_str, multiplier) = if let Some(last) = amount_str.chars().last() {
106 match last {
107 'm' => (&amount_str[..amount_str.len()-1], 100_000_000u64), 'u' => (&amount_str[..amount_str.len()-1], 100_000u64), 'n' => (&amount_str[..amount_str.len()-1], 100u64), 'p' => (&amount_str[..amount_str.len()-1], 1u64), _ => (amount_str, 100_000_000_000u64), }
113 } else {
114 return Ok(None);
115 };
116
117 let amount: u64 = num_str.parse().map_err(|_| {
118 LightningError::InvalidInvoice(format!("Invalid amount: {}", amount_str))
119 })?;
120
121 Ok(Some(amount * multiplier))
122 }
123
124 pub fn network(&self) -> Network {
126 self.data.network
127 }
128
129 pub fn amount_msat(&self) -> Option<u64> {
131 self.data.amount_msat
132 }
133
134 pub fn payment_hash(&self) -> Option<&PaymentHash> {
136 self.data.payment_hash.as_ref()
137 }
138
139 pub fn description(&self) -> Option<&str> {
141 self.data.description.as_deref()
142 }
143
144 pub fn expiry(&self) -> u64 {
146 self.data.expiry
147 }
148
149 pub fn timestamp(&self) -> u64 {
151 self.data.timestamp
152 }
153
154 pub fn is_expired(&self) -> bool {
156 let now = SystemTime::now()
157 .duration_since(UNIX_EPOCH)
158 .unwrap_or_default()
159 .as_secs();
160
161 now > self.data.timestamp + self.data.expiry
162 }
163
164 pub fn raw(&self) -> &str {
166 &self.raw
167 }
168
169 pub fn data(&self) -> &InvoiceData {
171 &self.data
172 }
173}
174
175#[derive(Debug, Clone)]
177pub struct InvoiceData {
178 pub network: Network,
180 pub amount_msat: Option<u64>,
182 pub timestamp: u64,
184 pub expiry: u64,
186 pub payment_hash: Option<PaymentHash>,
188 pub description: Option<String>,
190 pub description_hash: Option<[u8; 32]>,
192 pub payee_pubkey: Option<[u8; 33]>,
194 pub route_hints: Vec<RouteHint>,
196 pub min_final_cltv_expiry: u32,
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub enum Network {
203 Mainnet,
205 Testnet,
207 Regtest,
209}
210
211impl Network {
212 pub fn hrp_prefix(&self) -> &'static str {
214 match self {
215 Network::Mainnet => "lnbc",
216 Network::Testnet => "lntb",
217 Network::Regtest => "lnbcrt",
218 }
219 }
220}
221
222pub struct InvoiceBuilder {
224 network: Network,
225 amount_msat: Option<u64>,
226 description: Option<String>,
227 payment_hash: Option<PaymentHash>,
228 expiry: u64,
229 route_hints: Vec<RouteHint>,
230 min_final_cltv_expiry: u32,
231}
232
233impl InvoiceBuilder {
234 pub fn new(network: Network) -> Self {
236 Self {
237 network,
238 amount_msat: None,
239 description: None,
240 payment_hash: None,
241 expiry: 3600, route_hints: Vec::new(),
243 min_final_cltv_expiry: 18,
244 }
245 }
246
247 pub fn amount_msat(mut self, amount: u64) -> Self {
249 self.amount_msat = Some(amount);
250 self
251 }
252
253 pub fn amount_sats(mut self, sats: u64) -> Self {
255 self.amount_msat = Some(sats * 1000);
256 self
257 }
258
259 pub fn description(mut self, desc: &str) -> Self {
261 self.description = Some(desc.to_string());
262 self
263 }
264
265 pub fn payment_hash(mut self, hash: PaymentHash) -> Self {
267 self.payment_hash = Some(hash);
268 self
269 }
270
271 pub fn expiry(mut self, seconds: u64) -> Self {
273 self.expiry = seconds;
274 self
275 }
276
277 pub fn route_hint(mut self, hint: RouteHint) -> Self {
279 self.route_hints.push(hint);
280 self
281 }
282
283 pub fn min_final_cltv_expiry(mut self, blocks: u32) -> Self {
285 self.min_final_cltv_expiry = blocks;
286 self
287 }
288
289 pub fn build(self) -> Result<InvoiceData, LightningError> {
294 let payment_hash = self.payment_hash.ok_or_else(|| {
295 LightningError::InvalidInvoice("Payment hash is required".into())
296 })?;
297
298 let timestamp = SystemTime::now()
299 .duration_since(UNIX_EPOCH)
300 .unwrap_or_default()
301 .as_secs();
302
303 Ok(InvoiceData {
304 network: self.network,
305 amount_msat: self.amount_msat,
306 timestamp,
307 expiry: self.expiry,
308 payment_hash: Some(payment_hash),
309 description: self.description,
310 description_hash: None,
311 payee_pubkey: None,
312 route_hints: self.route_hints,
313 min_final_cltv_expiry: self.min_final_cltv_expiry,
314 })
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321 use crate::payment::PaymentPreimage;
322
323 #[test]
324 fn test_parse_mainnet_invoice() {
325 let invoice = "lnbc1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygshp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq9qrsgq357wnc5r2ueh7ck6q93dj32dlqnls087fxdwk8qakdyafkq3yap9us6v52vjjsrvywa6rt52cm9r9zqt8r2t7mlcwspyetp5h2tztugp9lfyql";
327
328 let parsed = Bolt11Invoice::parse(invoice).unwrap();
329 assert_eq!(parsed.network(), Network::Mainnet);
330 }
331
332 #[test]
333 fn test_parse_testnet_invoice() {
334 let invoice = "lntb1u1p0xxxx";
335 let parsed = Bolt11Invoice::parse(invoice).unwrap();
336 assert_eq!(parsed.network(), Network::Testnet);
337 }
338
339 #[test]
340 fn test_parse_amount() {
341 assert_eq!(Bolt11Invoice::parse_amount("lnbc1m").unwrap(), Some(100_000_000));
343
344 assert_eq!(Bolt11Invoice::parse_amount("lnbc1u").unwrap(), Some(100_000));
346
347 assert_eq!(Bolt11Invoice::parse_amount("lnbc1n").unwrap(), Some(100));
349 }
350
351 #[test]
352 fn test_invoice_builder() {
353 let preimage = PaymentPreimage::random();
354 let payment_hash = preimage.payment_hash();
355
356 let data = InvoiceBuilder::new(Network::Mainnet)
357 .amount_sats(10000)
358 .description("Test payment")
359 .payment_hash(payment_hash)
360 .expiry(3600)
361 .build()
362 .unwrap();
363
364 assert_eq!(data.network, Network::Mainnet);
365 assert_eq!(data.amount_msat, Some(10_000_000));
366 assert_eq!(data.description, Some("Test payment".to_string()));
367 }
368
369 #[test]
370 fn test_network_hrp() {
371 assert_eq!(Network::Mainnet.hrp_prefix(), "lnbc");
372 assert_eq!(Network::Testnet.hrp_prefix(), "lntb");
373 assert_eq!(Network::Regtest.hrp_prefix(), "lnbcrt");
374 }
375}