rustywallet_mempool/
lightning.rs

1//! Lightning Network statistics from mempool.space.
2//!
3//! This module provides access to Lightning Network statistics
4//! including network capacity, node counts, and channel information.
5
6use serde::{Deserialize, Serialize};
7
8use crate::client::MempoolClient;
9use crate::error::Result;
10
11/// Lightning Network statistics.
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct LightningStats {
14    /// Latest statistics
15    pub latest: LightningNetworkStats,
16    /// Previous period statistics (for comparison)
17    #[serde(default)]
18    pub previous: Option<LightningNetworkStats>,
19}
20
21/// Lightning Network statistics snapshot.
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct LightningNetworkStats {
24    /// Total network capacity in satoshis
25    #[serde(default)]
26    pub capacity: u64,
27    /// Number of channels
28    #[serde(default)]
29    pub channel_count: u64,
30    /// Number of nodes
31    #[serde(default)]
32    pub node_count: u64,
33    /// Number of Tor nodes
34    #[serde(default)]
35    pub tor_nodes: u64,
36    /// Number of clearnet nodes
37    #[serde(default)]
38    pub clearnet_nodes: u64,
39    /// Number of unannounced nodes
40    #[serde(default)]
41    pub unannounced_nodes: u64,
42    /// Average channel capacity in satoshis
43    #[serde(default)]
44    pub avg_capacity: u64,
45    /// Average fee rate (ppm)
46    #[serde(default)]
47    pub avg_fee_rate: u64,
48    /// Average base fee (msats)
49    #[serde(default)]
50    pub avg_base_fee_mtokens: u64,
51    /// Median channel capacity
52    #[serde(default)]
53    pub med_capacity: u64,
54    /// Median fee rate
55    #[serde(default)]
56    pub med_fee_rate: u64,
57    /// Median base fee
58    #[serde(default)]
59    pub med_base_fee_mtokens: u64,
60}
61
62impl LightningNetworkStats {
63    /// Get capacity in BTC.
64    pub fn capacity_btc(&self) -> f64 {
65        self.capacity as f64 / 100_000_000.0
66    }
67
68    /// Get average capacity in BTC.
69    pub fn avg_capacity_btc(&self) -> f64 {
70        self.avg_capacity as f64 / 100_000_000.0
71    }
72
73    /// Get average fee rate in percent.
74    pub fn avg_fee_rate_percent(&self) -> f64 {
75        self.avg_fee_rate as f64 / 10_000.0
76    }
77}
78
79/// Lightning node information.
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81pub struct LightningNode {
82    /// Node public key
83    pub public_key: String,
84    /// Node alias
85    #[serde(default)]
86    pub alias: String,
87    /// Number of channels
88    #[serde(default)]
89    pub channel_count: u64,
90    /// Total capacity in satoshis
91    #[serde(default)]
92    pub capacity: u64,
93    /// First seen timestamp
94    #[serde(default)]
95    pub first_seen: u64,
96    /// Last update timestamp
97    #[serde(default)]
98    pub updated_at: u64,
99    /// City location
100    #[serde(default)]
101    pub city: Option<LightningNodeCity>,
102    /// Country information
103    #[serde(default)]
104    pub country: Option<LightningNodeCountry>,
105}
106
107/// Node city information.
108#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
109pub struct LightningNodeCity {
110    /// City name
111    #[serde(default)]
112    pub en: String,
113}
114
115/// Node country information.
116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
117pub struct LightningNodeCountry {
118    /// Country name
119    #[serde(default)]
120    pub en: String,
121    /// Country code
122    #[serde(default)]
123    pub code: String,
124}
125
126impl LightningNode {
127    /// Get capacity in BTC.
128    pub fn capacity_btc(&self) -> f64 {
129        self.capacity as f64 / 100_000_000.0
130    }
131
132    /// Get location string.
133    pub fn location(&self) -> Option<String> {
134        match (&self.city, &self.country) {
135            (Some(city), Some(country)) => Some(format!("{}, {}", city.en, country.en)),
136            (None, Some(country)) => Some(country.en.clone()),
137            (Some(city), None) => Some(city.en.clone()),
138            (None, None) => None,
139        }
140    }
141}
142
143/// Lightning channel information.
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145pub struct LightningChannel {
146    /// Channel ID
147    pub id: String,
148    /// Short channel ID
149    #[serde(default)]
150    pub short_id: String,
151    /// Channel capacity in satoshis
152    #[serde(default)]
153    pub capacity: u64,
154    /// Transaction ID
155    #[serde(default)]
156    pub transaction_id: String,
157    /// Transaction output index
158    #[serde(default)]
159    pub transaction_vout: u32,
160    /// Closing transaction ID (if closed)
161    #[serde(default)]
162    pub closing_transaction_id: Option<String>,
163    /// Channel status
164    #[serde(default)]
165    pub status: u8,
166}
167
168impl LightningChannel {
169    /// Get capacity in BTC.
170    pub fn capacity_btc(&self) -> f64 {
171        self.capacity as f64 / 100_000_000.0
172    }
173
174    /// Check if channel is open.
175    pub fn is_open(&self) -> bool {
176        self.status == 1
177    }
178
179    /// Check if channel is closed.
180    pub fn is_closed(&self) -> bool {
181        self.closing_transaction_id.is_some()
182    }
183}
184
185/// Top Lightning nodes ranking.
186#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187pub struct TopNodes {
188    /// Nodes ranked by capacity
189    #[serde(default)]
190    pub by_capacity: Vec<LightningNode>,
191    /// Nodes ranked by channel count
192    #[serde(default)]
193    pub by_channels: Vec<LightningNode>,
194}
195
196/// Lightning client extension for MempoolClient.
197impl MempoolClient {
198    /// Get Lightning Network statistics.
199    pub async fn get_lightning_stats(&self) -> Result<LightningStats> {
200        self.get_internal("/v1/lightning/statistics/latest").await
201    }
202
203    /// Get top Lightning nodes by capacity.
204    pub async fn get_top_nodes_by_capacity(&self, limit: Option<u32>) -> Result<Vec<LightningNode>> {
205        let limit = limit.unwrap_or(100);
206        self.get_internal(&format!("/v1/lightning/nodes/rankings/connectivity?limit={}", limit)).await
207    }
208
209    /// Get Lightning node by public key.
210    pub async fn get_lightning_node(&self, pubkey: &str) -> Result<LightningNode> {
211        self.get_internal(&format!("/v1/lightning/nodes/{}", pubkey)).await
212    }
213
214    /// Get channels for a Lightning node.
215    pub async fn get_node_channels(&self, pubkey: &str) -> Result<Vec<LightningChannel>> {
216        self.get_internal(&format!("/v1/lightning/nodes/{}/channels", pubkey)).await
217    }
218
219    /// Get Lightning channel by ID.
220    pub async fn get_lightning_channel(&self, channel_id: &str) -> Result<LightningChannel> {
221        self.get_internal(&format!("/v1/lightning/channels/{}", channel_id)).await
222    }
223
224    /// Internal GET method (to avoid duplicate code).
225    async fn get_internal<T: serde::de::DeserializeOwned>(&self, endpoint: &str) -> Result<T> {
226        // Use the existing get method from client
227        let url = format!("{}{}", self.base_url(), endpoint);
228        
229        let response = self.http_client().get(&url).send().await?;
230        
231        let status = response.status();
232        if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
233            return Err(crate::error::MempoolError::RateLimited);
234        }
235        
236        if !status.is_success() {
237            let message = response.text().await.unwrap_or_default();
238            return Err(crate::error::MempoolError::ApiError {
239                status: status.as_u16(),
240                message,
241            });
242        }
243
244        response
245            .json()
246            .await
247            .map_err(|e| crate::error::MempoolError::ParseError(e.to_string()))
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn test_lightning_stats() {
257        let stats = LightningNetworkStats {
258            capacity: 500_000_000_000, // 5000 BTC
259            channel_count: 80000,
260            node_count: 15000,
261            tor_nodes: 5000,
262            clearnet_nodes: 8000,
263            unannounced_nodes: 2000,
264            avg_capacity: 6_250_000, // 0.0625 BTC
265            avg_fee_rate: 500, // 0.05%
266            avg_base_fee_mtokens: 1000,
267            med_capacity: 5_000_000,
268            med_fee_rate: 300,
269            med_base_fee_mtokens: 500,
270        };
271        
272        assert_eq!(stats.capacity_btc(), 5000.0);
273        assert_eq!(stats.avg_capacity_btc(), 0.0625);
274        assert_eq!(stats.avg_fee_rate_percent(), 0.05);
275    }
276
277    #[test]
278    fn test_lightning_node() {
279        let node = LightningNode {
280            public_key: "abc123".to_string(),
281            alias: "TestNode".to_string(),
282            channel_count: 100,
283            capacity: 100_000_000, // 1 BTC
284            first_seen: 1234567890,
285            updated_at: 1234567900,
286            city: Some(LightningNodeCity { en: "New York".to_string() }),
287            country: Some(LightningNodeCountry { en: "United States".to_string(), code: "US".to_string() }),
288        };
289        
290        assert_eq!(node.capacity_btc(), 1.0);
291        assert_eq!(node.location(), Some("New York, United States".to_string()));
292    }
293
294    #[test]
295    fn test_lightning_channel() {
296        let channel = LightningChannel {
297            id: "channel123".to_string(),
298            short_id: "800000x1x0".to_string(),
299            capacity: 10_000_000, // 0.1 BTC
300            transaction_id: "txid123".to_string(),
301            transaction_vout: 0,
302            closing_transaction_id: None,
303            status: 1,
304        };
305        
306        assert_eq!(channel.capacity_btc(), 0.1);
307        assert!(channel.is_open());
308        assert!(!channel.is_closed());
309    }
310}