1use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fmt;
10use std::str::FromStr;
11use std::time::Duration;
12
13use crate::error::{BitcoinError, Result};
14
15#[async_trait]
17pub trait LightningProvider: Send + Sync {
18 async fn get_info(&self) -> Result<NodeInfo>;
20
21 async fn create_invoice(&self, request: InvoiceRequest) -> Result<Invoice>;
23
24 async fn get_invoice(&self, payment_hash: &str) -> Result<Invoice>;
26
27 async fn pay_invoice(&self, bolt11: &str, max_fee_msat: Option<u64>) -> Result<Payment>;
29
30 async fn get_balance(&self) -> Result<ChannelBalance>;
32
33 async fn list_channels(&self) -> Result<Vec<Channel>>;
35
36 async fn open_channel(&self, request: OpenChannelRequest) -> Result<ChannelPoint>;
38
39 async fn close_channel(&self, channel_point: &ChannelPoint, force: bool) -> Result<String>;
41
42 async fn subscribe_invoices(&self) -> Result<InvoiceSubscription>;
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct NodeInfo {
49 pub pubkey: String,
51 pub alias: String,
53 pub num_active_channels: u32,
55 pub num_pending_channels: u32,
57 pub num_peers: u32,
59 pub block_height: u64,
61 pub synced_to_chain: bool,
63 pub version: String,
65 pub network: String,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct InvoiceRequest {
72 pub amount_msat: u64,
74 pub description: String,
76 pub expiry_secs: Option<u32>,
78 pub metadata: HashMap<String, String>,
80 pub private: bool,
82}
83
84impl InvoiceRequest {
85 pub fn new(amount_sats: u64, description: impl Into<String>) -> Self {
87 Self {
88 amount_msat: amount_sats * 1000,
89 description: description.into(),
90 expiry_secs: Some(3600), metadata: HashMap::new(),
92 private: false,
93 }
94 }
95
96 pub fn expiry(mut self, secs: u32) -> Self {
98 self.expiry_secs = Some(secs);
99 self
100 }
101
102 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
104 self.metadata.insert(key.into(), value.into());
105 self
106 }
107
108 pub fn private(mut self, is_private: bool) -> Self {
110 self.private = is_private;
111 self
112 }
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct Invoice {
118 pub payment_hash: String,
120 pub payment_preimage: Option<String>,
122 pub bolt11: String,
124 pub amount_msat: u64,
126 pub description: String,
128 pub created_at: u64,
130 pub expires_at: u64,
132 pub status: InvoiceStatus,
134 pub amount_received_msat: Option<u64>,
136 pub settled_at: Option<u64>,
138 pub metadata: HashMap<String, String>,
140}
141
142impl Invoice {
143 pub fn is_expired(&self) -> bool {
145 let now = std::time::SystemTime::now()
146 .duration_since(std::time::UNIX_EPOCH)
147 .unwrap()
148 .as_secs();
149 now > self.expires_at
150 }
151
152 pub fn is_paid(&self) -> bool {
154 self.status == InvoiceStatus::Settled
155 }
156
157 pub fn amount_sats(&self) -> u64 {
159 self.amount_msat / 1000
160 }
161
162 pub fn time_remaining(&self) -> Option<Duration> {
164 let now = std::time::SystemTime::now()
165 .duration_since(std::time::UNIX_EPOCH)
166 .unwrap()
167 .as_secs();
168
169 if now < self.expires_at {
170 Some(Duration::from_secs(self.expires_at - now))
171 } else {
172 None
173 }
174 }
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
179pub enum InvoiceStatus {
180 Open,
182 Settled,
184 Expired,
186 Cancelled,
188 Accepted,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct Payment {
195 pub payment_hash: String,
197 pub payment_preimage: String,
199 pub amount_msat: u64,
201 pub fee_msat: u64,
203 pub status: PaymentStatus,
205 pub created_at: u64,
207 pub num_hops: u32,
209 pub route: Option<Vec<RouteHop>>,
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
215pub enum PaymentStatus {
216 InFlight,
218 Succeeded,
220 Failed,
222 Unknown,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct RouteHop {
229 pub pubkey: String,
231 pub channel_id: u64,
233 pub amount_msat: u64,
235 pub fee_msat: u64,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct ChannelBalance {
242 pub local_balance_msat: u64,
244 pub remote_balance_msat: u64,
246 pub pending_local_msat: u64,
248 pub pending_remote_msat: u64,
250 pub unsettled_local_msat: u64,
252 pub unsettled_remote_msat: u64,
254}
255
256impl ChannelBalance {
257 pub fn can_send_sats(&self) -> u64 {
259 self.local_balance_msat / 1000
260 }
261
262 pub fn can_receive_sats(&self) -> u64 {
264 self.remote_balance_msat / 1000
265 }
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct Channel {
271 pub channel_id: u64,
273 pub channel_point: ChannelPoint,
275 pub remote_pubkey: String,
277 pub local_balance_msat: u64,
279 pub remote_balance_msat: u64,
281 pub capacity_sats: u64,
283 pub active: bool,
285 pub private: bool,
287 pub num_updates: u64,
289 pub commit_fee_sats: u64,
291 pub time_lock_delta: u32,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct ChannelPoint {
298 pub txid: String,
300 pub output_index: u32,
302}
303
304impl FromStr for ChannelPoint {
305 type Err = BitcoinError;
306
307 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
308 let parts: Vec<&str> = s.split(':').collect();
309 if parts.len() == 2 {
310 let output_index = parts[1].parse().map_err(|_| {
311 BitcoinError::InvalidAddress("Invalid channel point format".to_string())
312 })?;
313 Ok(Self {
314 txid: parts[0].to_string(),
315 output_index,
316 })
317 } else {
318 Err(BitcoinError::InvalidAddress(
319 "Invalid channel point format".to_string(),
320 ))
321 }
322 }
323}
324
325impl fmt::Display for ChannelPoint {
326 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327 write!(f, "{}:{}", self.txid, self.output_index)
328 }
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct OpenChannelRequest {
334 pub node_pubkey: String,
336 pub local_funding_sats: u64,
338 pub push_sats: Option<u64>,
340 pub target_conf: Option<u32>,
342 pub sat_per_vbyte: Option<u64>,
344 pub private: bool,
346 pub min_htlc_msat: Option<u64>,
348}
349
350impl OpenChannelRequest {
351 pub fn new(node_pubkey: impl Into<String>, local_funding_sats: u64) -> Self {
352 Self {
353 node_pubkey: node_pubkey.into(),
354 local_funding_sats,
355 push_sats: None,
356 target_conf: Some(3),
357 sat_per_vbyte: None,
358 private: false,
359 min_htlc_msat: None,
360 }
361 }
362}
363
364pub struct InvoiceSubscription {
366 receiver: tokio::sync::mpsc::Receiver<InvoiceUpdate>,
367}
368
369impl InvoiceSubscription {
370 pub fn new(receiver: tokio::sync::mpsc::Receiver<InvoiceUpdate>) -> Self {
372 Self { receiver }
373 }
374
375 pub async fn next(&mut self) -> Option<InvoiceUpdate> {
377 self.receiver.recv().await
378 }
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct InvoiceUpdate {
384 pub payment_hash: String,
386 pub status: InvoiceStatus,
388 pub amount_received_msat: Option<u64>,
390 pub settled_at: Option<u64>,
392}
393
394pub struct LndClient {
396 endpoint: String,
398 macaroon: String,
400 #[allow(dead_code)]
402 tls_cert: Option<Vec<u8>>,
403 http_client: reqwest::Client,
405}
406
407impl LndClient {
408 pub fn new(endpoint: impl Into<String>, macaroon: impl Into<String>) -> Self {
410 Self {
411 endpoint: endpoint.into(),
412 macaroon: macaroon.into(),
413 tls_cert: None,
414 http_client: reqwest::Client::builder()
415 .timeout(Duration::from_secs(30))
416 .danger_accept_invalid_certs(true) .build()
418 .expect("Failed to create HTTP client"),
419 }
420 }
421
422 pub fn from_env() -> Option<Self> {
424 let endpoint = std::env::var("LND_REST_ENDPOINT").ok()?;
425 let macaroon = std::env::var("LND_MACAROON").ok()?;
426 Some(Self::new(endpoint, macaroon))
427 }
428
429 async fn request<T: for<'de> Deserialize<'de>>(
431 &self,
432 method: reqwest::Method,
433 path: &str,
434 body: Option<serde_json::Value>,
435 ) -> Result<T> {
436 let url = format!("{}{}", self.endpoint, path);
437
438 let mut request = self
439 .http_client
440 .request(method, &url)
441 .header("Grpc-Metadata-macaroon", &self.macaroon);
442
443 if let Some(body) = body {
444 request = request.json(&body);
445 }
446
447 let response = request
448 .send()
449 .await
450 .map_err(|e| BitcoinError::ConnectionFailed(format!("LND request failed: {}", e)))?;
451
452 if !response.status().is_success() {
453 let status = response.status();
454 let error_text = response.text().await.unwrap_or_default();
455 return Err(BitcoinError::Wallet(format!(
456 "LND error ({}): {}",
457 status, error_text
458 )));
459 }
460
461 response
462 .json()
463 .await
464 .map_err(|e| BitcoinError::Wallet(format!("Failed to parse LND response: {}", e)))
465 }
466}
467
468#[async_trait]
469impl LightningProvider for LndClient {
470 async fn get_info(&self) -> Result<NodeInfo> {
471 #[derive(Deserialize)]
472 struct LndGetInfo {
473 identity_pubkey: String,
474 alias: String,
475 num_active_channels: u32,
476 num_pending_channels: u32,
477 num_peers: u32,
478 block_height: u64,
479 synced_to_chain: bool,
480 version: String,
481 chains: Vec<LndChain>,
482 }
483
484 #[derive(Deserialize)]
485 struct LndChain {
486 network: String,
487 }
488
489 let info: LndGetInfo = self
490 .request(reqwest::Method::GET, "/v1/getinfo", None)
491 .await?;
492
493 Ok(NodeInfo {
494 pubkey: info.identity_pubkey,
495 alias: info.alias,
496 num_active_channels: info.num_active_channels,
497 num_pending_channels: info.num_pending_channels,
498 num_peers: info.num_peers,
499 block_height: info.block_height,
500 synced_to_chain: info.synced_to_chain,
501 version: info.version,
502 network: info
503 .chains
504 .first()
505 .map(|c| c.network.clone())
506 .unwrap_or_default(),
507 })
508 }
509
510 async fn create_invoice(&self, request: InvoiceRequest) -> Result<Invoice> {
511 let body = serde_json::json!({
512 "value_msat": request.amount_msat.to_string(),
513 "memo": request.description,
514 "expiry": request.expiry_secs.unwrap_or(3600).to_string(),
515 "private": request.private,
516 });
517
518 #[derive(Deserialize)]
519 #[allow(dead_code)]
520 struct LndInvoice {
521 r_hash: String,
522 payment_request: String,
523 add_index: String,
524 }
525
526 let invoice: LndInvoice = self
527 .request(reqwest::Method::POST, "/v1/invoices", Some(body))
528 .await?;
529
530 let now = std::time::SystemTime::now()
531 .duration_since(std::time::UNIX_EPOCH)
532 .unwrap()
533 .as_secs();
534
535 Ok(Invoice {
536 payment_hash: invoice.r_hash,
537 payment_preimage: None,
538 bolt11: invoice.payment_request,
539 amount_msat: request.amount_msat,
540 description: request.description,
541 created_at: now,
542 expires_at: now + request.expiry_secs.unwrap_or(3600) as u64,
543 status: InvoiceStatus::Open,
544 amount_received_msat: None,
545 settled_at: None,
546 metadata: request.metadata,
547 })
548 }
549
550 async fn get_invoice(&self, payment_hash: &str) -> Result<Invoice> {
551 #[derive(Deserialize)]
552 #[allow(dead_code)]
553 struct LndInvoiceLookup {
554 r_hash: String,
555 r_preimage: Option<String>,
556 payment_request: String,
557 value_msat: String,
558 memo: String,
559 creation_date: String,
560 expiry: String,
561 settled: bool,
562 amt_paid_msat: Option<String>,
563 settle_date: Option<String>,
564 state: String,
565 }
566
567 let path = format!("/v1/invoice/{}", payment_hash);
568 let invoice: LndInvoiceLookup = self.request(reqwest::Method::GET, &path, None).await?;
569
570 let created_at = invoice.creation_date.parse().unwrap_or(0);
571 let expiry = invoice.expiry.parse().unwrap_or(3600);
572
573 let status = match invoice.state.as_str() {
574 "OPEN" => InvoiceStatus::Open,
575 "SETTLED" => InvoiceStatus::Settled,
576 "CANCELED" => InvoiceStatus::Cancelled,
577 "ACCEPTED" => InvoiceStatus::Accepted,
578 _ => InvoiceStatus::Open,
579 };
580
581 Ok(Invoice {
582 payment_hash: invoice.r_hash,
583 payment_preimage: invoice.r_preimage,
584 bolt11: invoice.payment_request,
585 amount_msat: invoice.value_msat.parse().unwrap_or(0),
586 description: invoice.memo,
587 created_at,
588 expires_at: created_at + expiry,
589 status,
590 amount_received_msat: invoice.amt_paid_msat.and_then(|s| s.parse().ok()),
591 settled_at: invoice.settle_date.and_then(|s| s.parse().ok()),
592 metadata: HashMap::new(),
593 })
594 }
595
596 async fn pay_invoice(&self, bolt11: &str, max_fee_msat: Option<u64>) -> Result<Payment> {
597 let mut body = serde_json::json!({
598 "payment_request": bolt11,
599 });
600
601 if let Some(fee) = max_fee_msat {
602 body["fee_limit_msat"] = serde_json::json!(fee.to_string());
603 }
604
605 #[derive(Deserialize)]
606 struct LndPayment {
607 payment_hash: String,
608 payment_preimage: String,
609 value_msat: String,
610 payment_route: Option<LndRoute>,
611 status: String,
612 fee_msat: String,
613 creation_time_ns: String,
614 }
615
616 #[derive(Deserialize)]
617 struct LndRoute {
618 hops: Vec<LndHop>,
619 }
620
621 #[derive(Deserialize)]
622 struct LndHop {
623 pub_key: String,
624 chan_id: String,
625 amt_to_forward_msat: String,
626 fee_msat: String,
627 }
628
629 let payment: LndPayment = self
630 .request(
631 reqwest::Method::POST,
632 "/v1/channels/transactions",
633 Some(body),
634 )
635 .await?;
636
637 let status = match payment.status.as_str() {
638 "SUCCEEDED" => PaymentStatus::Succeeded,
639 "FAILED" => PaymentStatus::Failed,
640 "IN_FLIGHT" => PaymentStatus::InFlight,
641 _ => PaymentStatus::Unknown,
642 };
643
644 let route: Option<Vec<RouteHop>> = payment.payment_route.map(|r| {
645 r.hops
646 .into_iter()
647 .map(|h| RouteHop {
648 pubkey: h.pub_key,
649 channel_id: h.chan_id.parse().unwrap_or(0),
650 amount_msat: h.amt_to_forward_msat.parse().unwrap_or(0),
651 fee_msat: h.fee_msat.parse().unwrap_or(0),
652 })
653 .collect()
654 });
655
656 let num_hops = route.as_ref().map(|r| r.len() as u32).unwrap_or(0);
657
658 Ok(Payment {
659 payment_hash: payment.payment_hash,
660 payment_preimage: payment.payment_preimage,
661 amount_msat: payment.value_msat.parse().unwrap_or(0),
662 fee_msat: payment.fee_msat.parse().unwrap_or(0),
663 status,
664 created_at: payment.creation_time_ns.parse::<u64>().unwrap_or(0) / 1_000_000_000,
665 num_hops,
666 route,
667 })
668 }
669
670 async fn get_balance(&self) -> Result<ChannelBalance> {
671 #[derive(Deserialize)]
672 struct LndBalance {
673 local_balance: Option<LndBalanceDetail>,
674 remote_balance: Option<LndBalanceDetail>,
675 pending_open_local_balance: Option<LndBalanceDetail>,
676 pending_open_remote_balance: Option<LndBalanceDetail>,
677 unsettled_local_balance: Option<LndBalanceDetail>,
678 unsettled_remote_balance: Option<LndBalanceDetail>,
679 }
680
681 #[derive(Deserialize)]
682 struct LndBalanceDetail {
683 msat: Option<String>,
684 }
685
686 let balance: LndBalance = self
687 .request(reqwest::Method::GET, "/v1/balance/channels", None)
688 .await?;
689
690 let parse_msat = |detail: Option<LndBalanceDetail>| -> u64 {
691 detail
692 .and_then(|d| d.msat)
693 .and_then(|s| s.parse().ok())
694 .unwrap_or(0)
695 };
696
697 Ok(ChannelBalance {
698 local_balance_msat: parse_msat(balance.local_balance),
699 remote_balance_msat: parse_msat(balance.remote_balance),
700 pending_local_msat: parse_msat(balance.pending_open_local_balance),
701 pending_remote_msat: parse_msat(balance.pending_open_remote_balance),
702 unsettled_local_msat: parse_msat(balance.unsettled_local_balance),
703 unsettled_remote_msat: parse_msat(balance.unsettled_remote_balance),
704 })
705 }
706
707 async fn list_channels(&self) -> Result<Vec<Channel>> {
708 #[derive(Deserialize)]
709 struct LndChannels {
710 channels: Option<Vec<LndChannel>>,
711 }
712
713 #[derive(Deserialize)]
714 struct LndChannel {
715 chan_id: String,
716 channel_point: String,
717 remote_pubkey: String,
718 local_balance: String,
719 remote_balance: String,
720 capacity: String,
721 active: bool,
722 private: bool,
723 num_updates: String,
724 commit_fee: String,
725 csv_delay: u32,
726 }
727
728 let channels: LndChannels = self
729 .request(reqwest::Method::GET, "/v1/channels", None)
730 .await?;
731
732 Ok(channels
733 .channels
734 .unwrap_or_default()
735 .into_iter()
736 .filter_map(|c| {
737 let channel_point = ChannelPoint::from_str(&c.channel_point).ok()?;
738 Some(Channel {
739 channel_id: c.chan_id.parse().ok()?,
740 channel_point,
741 remote_pubkey: c.remote_pubkey,
742 local_balance_msat: c.local_balance.parse::<u64>().ok()? * 1000,
743 remote_balance_msat: c.remote_balance.parse::<u64>().ok()? * 1000,
744 capacity_sats: c.capacity.parse().ok()?,
745 active: c.active,
746 private: c.private,
747 num_updates: c.num_updates.parse().unwrap_or(0),
748 commit_fee_sats: c.commit_fee.parse().unwrap_or(0),
749 time_lock_delta: c.csv_delay,
750 })
751 })
752 .collect())
753 }
754
755 async fn open_channel(&self, request: OpenChannelRequest) -> Result<ChannelPoint> {
756 let body = serde_json::json!({
757 "node_pubkey_string": request.node_pubkey,
758 "local_funding_amount": request.local_funding_sats.to_string(),
759 "push_sat": request.push_sats.unwrap_or(0).to_string(),
760 "target_conf": request.target_conf.unwrap_or(3),
761 "sat_per_vbyte": request.sat_per_vbyte.unwrap_or(1),
762 "private": request.private,
763 });
764
765 #[derive(Deserialize)]
766 struct LndOpenChannel {
767 funding_txid_bytes: Option<String>,
768 funding_txid_str: Option<String>,
769 output_index: u32,
770 }
771
772 let result: LndOpenChannel = self
773 .request(reqwest::Method::POST, "/v1/channels", Some(body))
774 .await?;
775
776 let txid = result
777 .funding_txid_str
778 .or(result.funding_txid_bytes)
779 .ok_or_else(|| BitcoinError::Wallet("No funding txid returned".to_string()))?;
780
781 Ok(ChannelPoint {
782 txid,
783 output_index: result.output_index,
784 })
785 }
786
787 async fn close_channel(&self, channel_point: &ChannelPoint, force: bool) -> Result<String> {
788 let path = format!(
789 "/v1/channels/{}/{}?force={}",
790 channel_point.txid, channel_point.output_index, force
791 );
792
793 #[derive(Deserialize)]
794 struct LndCloseResult {
795 closing_txid: Option<String>,
796 }
797
798 let result: LndCloseResult = self.request(reqwest::Method::DELETE, &path, None).await?;
799
800 result
801 .closing_txid
802 .ok_or_else(|| BitcoinError::Wallet("No closing txid returned".to_string()))
803 }
804
805 async fn subscribe_invoices(&self) -> Result<InvoiceSubscription> {
806 let (tx, rx) = tokio::sync::mpsc::channel(100);
809
810 tokio::spawn(async move {
812 let _tx = tx;
814 loop {
815 tokio::time::sleep(Duration::from_secs(3600)).await;
816 }
817 });
818
819 Ok(InvoiceSubscription::new(rx))
820 }
821}
822
823pub struct LightningPaymentManager {
825 provider: Box<dyn LightningProvider>,
826 min_capacity_sats: u64,
828 default_expiry_secs: u32,
830}
831
832impl LightningPaymentManager {
833 pub fn new(provider: Box<dyn LightningProvider>) -> Self {
835 Self {
836 provider,
837 min_capacity_sats: 100_000, default_expiry_secs: 3600, }
840 }
841
842 pub async fn create_order_invoice(
844 &self,
845 order_id: &str,
846 amount_sats: u64,
847 description: &str,
848 ) -> Result<Invoice> {
849 let request = InvoiceRequest::new(amount_sats, description)
850 .expiry(self.default_expiry_secs)
851 .with_metadata("order_id", order_id)
852 .with_metadata("type", "order_payment");
853
854 self.provider.create_invoice(request).await
855 }
856
857 pub async fn check_order_payment(&self, payment_hash: &str) -> Result<OrderPaymentStatus> {
859 let invoice = self.provider.get_invoice(payment_hash).await?;
860
861 let is_expired = invoice.is_expired();
862 Ok(OrderPaymentStatus {
863 payment_hash: invoice.payment_hash,
864 status: invoice.status,
865 amount_paid_msat: invoice.amount_received_msat,
866 settled_at: invoice.settled_at,
867 is_expired,
868 })
869 }
870
871 pub async fn can_receive(&self, amount_sats: u64) -> Result<bool> {
873 let balance = self.provider.get_balance().await?;
874 Ok(balance.can_receive_sats() >= amount_sats)
875 }
876
877 pub async fn can_send(&self, amount_sats: u64) -> Result<bool> {
879 let balance = self.provider.get_balance().await?;
880 Ok(balance.can_send_sats() >= amount_sats)
881 }
882
883 pub async fn get_health(&self) -> Result<LightningHealth> {
885 let info = self.provider.get_info().await?;
886 let balance = self.provider.get_balance().await?;
887 let channels = self.provider.list_channels().await?;
888
889 let active_channels = channels.iter().filter(|c| c.active).count();
890 let total_capacity = channels.iter().map(|c| c.capacity_sats).sum();
891
892 Ok(LightningHealth {
893 node_pubkey: info.pubkey,
894 synced: info.synced_to_chain,
895 active_channels: active_channels as u32,
896 total_channels: channels.len() as u32,
897 can_send_sats: balance.can_send_sats(),
898 can_receive_sats: balance.can_receive_sats(),
899 total_capacity_sats: total_capacity,
900 has_sufficient_liquidity: balance.can_receive_sats() >= self.min_capacity_sats,
901 })
902 }
903}
904
905#[derive(Debug, Clone, Serialize, Deserialize)]
907pub struct OrderPaymentStatus {
908 pub payment_hash: String,
910 pub status: InvoiceStatus,
912 pub amount_paid_msat: Option<u64>,
914 pub settled_at: Option<u64>,
916 pub is_expired: bool,
918}
919
920#[derive(Debug, Clone, Serialize, Deserialize)]
922pub struct LightningHealth {
923 pub node_pubkey: String,
925 pub synced: bool,
927 pub active_channels: u32,
929 pub total_channels: u32,
931 pub can_send_sats: u64,
933 pub can_receive_sats: u64,
935 pub total_capacity_sats: u64,
937 pub has_sufficient_liquidity: bool,
939}
940
941#[cfg(test)]
942mod tests {
943 use super::*;
944
945 #[test]
946 fn test_invoice_request_builder() {
947 let request = InvoiceRequest::new(1000, "Test payment")
948 .expiry(1800)
949 .with_metadata("order_id", "order123")
950 .private(true);
951
952 assert_eq!(request.amount_msat, 1_000_000);
953 assert_eq!(request.expiry_secs, Some(1800));
954 assert!(request.private);
955 assert_eq!(
956 request.metadata.get("order_id"),
957 Some(&"order123".to_string())
958 );
959 }
960
961 #[test]
962 fn test_channel_point_parsing() {
963 let cp = ChannelPoint::from_str("abc123:0").unwrap();
964
965 assert_eq!(cp.txid, "abc123");
966 assert_eq!(cp.output_index, 0);
967 assert_eq!(cp.to_string(), "abc123:0");
968 }
969
970 #[test]
971 fn test_invoice_status() {
972 let now = std::time::SystemTime::now()
973 .duration_since(std::time::UNIX_EPOCH)
974 .unwrap()
975 .as_secs();
976
977 let invoice = Invoice {
978 payment_hash: "hash".to_string(),
979 payment_preimage: None,
980 bolt11: "lnbc...".to_string(),
981 amount_msat: 1_000_000,
982 description: "Test".to_string(),
983 created_at: now,
984 expires_at: now + 3600,
985 status: InvoiceStatus::Open,
986 amount_received_msat: None,
987 settled_at: None,
988 metadata: HashMap::new(),
989 };
990
991 assert!(!invoice.is_expired());
992 assert!(!invoice.is_paid());
993 assert_eq!(invoice.amount_sats(), 1000);
994 }
995}