kaccy_bitcoin/
lightning.rs

1//! Lightning Network integration module
2//!
3//! This module provides Lightning Network capabilities for instant payments
4//! using LND (Lightning Network Daemon) or CLN (Core Lightning).
5
6use 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/// Lightning Network provider trait
16#[async_trait]
17pub trait LightningProvider: Send + Sync {
18    /// Get node info
19    async fn get_info(&self) -> Result<NodeInfo>;
20
21    /// Create an invoice
22    async fn create_invoice(&self, request: InvoiceRequest) -> Result<Invoice>;
23
24    /// Check invoice status
25    async fn get_invoice(&self, payment_hash: &str) -> Result<Invoice>;
26
27    /// Pay an invoice
28    async fn pay_invoice(&self, bolt11: &str, max_fee_msat: Option<u64>) -> Result<Payment>;
29
30    /// Get channel balance
31    async fn get_balance(&self) -> Result<ChannelBalance>;
32
33    /// List channels
34    async fn list_channels(&self) -> Result<Vec<Channel>>;
35
36    /// Open a channel
37    async fn open_channel(&self, request: OpenChannelRequest) -> Result<ChannelPoint>;
38
39    /// Close a channel
40    async fn close_channel(&self, channel_point: &ChannelPoint, force: bool) -> Result<String>;
41
42    /// Subscribe to invoice updates
43    async fn subscribe_invoices(&self) -> Result<InvoiceSubscription>;
44}
45
46/// Node information
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct NodeInfo {
49    /// Node public key
50    pub pubkey: String,
51    /// Node alias
52    pub alias: String,
53    /// Number of active channels
54    pub num_active_channels: u32,
55    /// Number of pending channels
56    pub num_pending_channels: u32,
57    /// Number of peers
58    pub num_peers: u32,
59    /// Block height
60    pub block_height: u64,
61    /// Synced to chain
62    pub synced_to_chain: bool,
63    /// Version
64    pub version: String,
65    /// Network (mainnet, testnet, signet)
66    pub network: String,
67}
68
69/// Invoice request parameters
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct InvoiceRequest {
72    /// Amount in millisatoshis
73    pub amount_msat: u64,
74    /// Invoice description
75    pub description: String,
76    /// Expiry time in seconds (default: 3600)
77    pub expiry_secs: Option<u32>,
78    /// Custom metadata (e.g., order_id)
79    pub metadata: HashMap<String, String>,
80    /// Private routing hints
81    pub private: bool,
82}
83
84impl InvoiceRequest {
85    /// Create a new invoice request
86    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), // 1 hour default
91            metadata: HashMap::new(),
92            private: false,
93        }
94    }
95
96    /// Set expiry time
97    pub fn expiry(mut self, secs: u32) -> Self {
98        self.expiry_secs = Some(secs);
99        self
100    }
101
102    /// Add metadata
103    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    /// Set as private (include routing hints)
109    pub fn private(mut self, is_private: bool) -> Self {
110        self.private = is_private;
111        self
112    }
113}
114
115/// Lightning invoice
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct Invoice {
118    /// Payment hash (hex)
119    pub payment_hash: String,
120    /// Payment preimage (hex, only available after payment)
121    pub payment_preimage: Option<String>,
122    /// BOLT11 encoded invoice string
123    pub bolt11: String,
124    /// Amount in millisatoshis
125    pub amount_msat: u64,
126    /// Description
127    pub description: String,
128    /// Creation timestamp (unix)
129    pub created_at: u64,
130    /// Expiry timestamp (unix)
131    pub expires_at: u64,
132    /// Invoice status
133    pub status: InvoiceStatus,
134    /// Amount received (if paid)
135    pub amount_received_msat: Option<u64>,
136    /// Settlement timestamp (if paid)
137    pub settled_at: Option<u64>,
138    /// Custom metadata
139    pub metadata: HashMap<String, String>,
140}
141
142impl Invoice {
143    /// Check if invoice is expired
144    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    /// Check if invoice is paid
153    pub fn is_paid(&self) -> bool {
154        self.status == InvoiceStatus::Settled
155    }
156
157    /// Get amount in satoshis
158    pub fn amount_sats(&self) -> u64 {
159        self.amount_msat / 1000
160    }
161
162    /// Get remaining time until expiry
163    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/// Invoice status
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
179pub enum InvoiceStatus {
180    /// Invoice created, awaiting payment
181    Open,
182    /// Payment received, settled
183    Settled,
184    /// Invoice expired without payment
185    Expired,
186    /// Invoice cancelled
187    Cancelled,
188    /// Payment in progress (accepted HTLC)
189    Accepted,
190}
191
192/// Payment result
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct Payment {
195    /// Payment hash
196    pub payment_hash: String,
197    /// Payment preimage
198    pub payment_preimage: String,
199    /// Amount paid (excluding fees) in millisatoshis
200    pub amount_msat: u64,
201    /// Fee paid in millisatoshis
202    pub fee_msat: u64,
203    /// Payment status
204    pub status: PaymentStatus,
205    /// Creation timestamp
206    pub created_at: u64,
207    /// Number of hops
208    pub num_hops: u32,
209    /// Payment route (if available)
210    pub route: Option<Vec<RouteHop>>,
211}
212
213/// Payment status
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
215pub enum PaymentStatus {
216    /// Payment in flight
217    InFlight,
218    /// Payment succeeded
219    Succeeded,
220    /// Payment failed
221    Failed,
222    /// Payment unknown
223    Unknown,
224}
225
226/// Route hop information
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct RouteHop {
229    /// Node public key
230    pub pubkey: String,
231    /// Channel ID
232    pub channel_id: u64,
233    /// Amount forwarded
234    pub amount_msat: u64,
235    /// Fee charged
236    pub fee_msat: u64,
237}
238
239/// Channel balance information
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct ChannelBalance {
242    /// Total local balance (can send)
243    pub local_balance_msat: u64,
244    /// Total remote balance (can receive)
245    pub remote_balance_msat: u64,
246    /// Pending local balance
247    pub pending_local_msat: u64,
248    /// Pending remote balance
249    pub pending_remote_msat: u64,
250    /// Unsettled local balance
251    pub unsettled_local_msat: u64,
252    /// Unsettled remote balance
253    pub unsettled_remote_msat: u64,
254}
255
256impl ChannelBalance {
257    /// Get sendable amount in satoshis
258    pub fn can_send_sats(&self) -> u64 {
259        self.local_balance_msat / 1000
260    }
261
262    /// Get receivable amount in satoshis
263    pub fn can_receive_sats(&self) -> u64 {
264        self.remote_balance_msat / 1000
265    }
266}
267
268/// Channel information
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct Channel {
271    /// Channel ID
272    pub channel_id: u64,
273    /// Channel point (funding txid:output_index)
274    pub channel_point: ChannelPoint,
275    /// Remote node public key
276    pub remote_pubkey: String,
277    /// Local balance in millisatoshis
278    pub local_balance_msat: u64,
279    /// Remote balance in millisatoshis
280    pub remote_balance_msat: u64,
281    /// Channel capacity in satoshis
282    pub capacity_sats: u64,
283    /// Whether the channel is active
284    pub active: bool,
285    /// Whether the channel is private
286    pub private: bool,
287    /// Number of updates
288    pub num_updates: u64,
289    /// Commit fee in satoshis
290    pub commit_fee_sats: u64,
291    /// Time lock delta
292    pub time_lock_delta: u32,
293}
294
295/// Channel point (funding transaction reference)
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct ChannelPoint {
298    /// Funding transaction ID
299    pub txid: String,
300    /// Output index
301    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/// Open channel request
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct OpenChannelRequest {
334    /// Node public key to connect to
335    pub node_pubkey: String,
336    /// Local funding amount in satoshis
337    pub local_funding_sats: u64,
338    /// Push amount to remote (gift) in satoshis
339    pub push_sats: Option<u64>,
340    /// Target confirmation blocks for funding tx
341    pub target_conf: Option<u32>,
342    /// Sat/vbyte for funding tx
343    pub sat_per_vbyte: Option<u64>,
344    /// Whether to make the channel private
345    pub private: bool,
346    /// Minimum HTLC size
347    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
364/// Invoice subscription for real-time updates
365pub struct InvoiceSubscription {
366    receiver: tokio::sync::mpsc::Receiver<InvoiceUpdate>,
367}
368
369impl InvoiceSubscription {
370    /// Create a new subscription
371    pub fn new(receiver: tokio::sync::mpsc::Receiver<InvoiceUpdate>) -> Self {
372        Self { receiver }
373    }
374
375    /// Wait for next invoice update
376    pub async fn next(&mut self) -> Option<InvoiceUpdate> {
377        self.receiver.recv().await
378    }
379}
380
381/// Invoice update event
382#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct InvoiceUpdate {
384    /// Payment hash
385    pub payment_hash: String,
386    /// New status
387    pub status: InvoiceStatus,
388    /// Amount received (if paid)
389    pub amount_received_msat: Option<u64>,
390    /// Settlement timestamp
391    pub settled_at: Option<u64>,
392}
393
394/// LND client implementation
395pub struct LndClient {
396    /// LND REST endpoint
397    endpoint: String,
398    /// Macaroon for authentication (hex encoded)
399    macaroon: String,
400    /// TLS certificate (optional, for custom CA)
401    #[allow(dead_code)]
402    tls_cert: Option<Vec<u8>>,
403    /// HTTP client
404    http_client: reqwest::Client,
405}
406
407impl LndClient {
408    /// Create a new LND client
409    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) // LND often uses self-signed certs
417                .build()
418                .expect("Failed to create HTTP client"),
419        }
420    }
421
422    /// Create from environment variables
423    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    /// Make an authenticated request to LND
430    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        // For real implementation, this would use WebSocket/gRPC streaming
807        // Here we create a mock channel that would be populated by a background task
808        let (tx, rx) = tokio::sync::mpsc::channel(100);
809
810        // In production, spawn a task to poll or stream invoice updates
811        tokio::spawn(async move {
812            // Keep the sender alive - in production this would be connected to LND streaming
813            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
823/// Lightning payment manager for order processing
824pub struct LightningPaymentManager {
825    provider: Box<dyn LightningProvider>,
826    /// Minimum channel capacity to maintain
827    min_capacity_sats: u64,
828    /// Default invoice expiry
829    default_expiry_secs: u32,
830}
831
832impl LightningPaymentManager {
833    /// Create a new payment manager
834    pub fn new(provider: Box<dyn LightningProvider>) -> Self {
835        Self {
836            provider,
837            min_capacity_sats: 100_000, // 0.001 BTC
838            default_expiry_secs: 3600,  // 1 hour
839        }
840    }
841
842    /// Create an invoice for an order
843    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    /// Check if an order invoice is paid
858    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    /// Check if we have enough inbound liquidity to receive a payment
872    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    /// Check if we have enough outbound liquidity to send a payment
878    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    /// Get node health status
884    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/// Order payment status
906#[derive(Debug, Clone, Serialize, Deserialize)]
907pub struct OrderPaymentStatus {
908    /// Payment hash
909    pub payment_hash: String,
910    /// Invoice status
911    pub status: InvoiceStatus,
912    /// Amount paid in millisatoshis
913    pub amount_paid_msat: Option<u64>,
914    /// Settlement timestamp
915    pub settled_at: Option<u64>,
916    /// Whether the invoice has expired
917    pub is_expired: bool,
918}
919
920/// Lightning node health status
921#[derive(Debug, Clone, Serialize, Deserialize)]
922pub struct LightningHealth {
923    /// Node public key
924    pub node_pubkey: String,
925    /// Whether node is synced to chain
926    pub synced: bool,
927    /// Number of active channels
928    pub active_channels: u32,
929    /// Total number of channels
930    pub total_channels: u32,
931    /// Amount we can send (satoshis)
932    pub can_send_sats: u64,
933    /// Amount we can receive (satoshis)
934    pub can_receive_sats: u64,
935    /// Total channel capacity
936    pub total_capacity_sats: u64,
937    /// Whether we have sufficient liquidity for operations
938    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}