1use serde::{Deserialize, Serialize};
7
8use crate::client::MempoolClient;
9use crate::error::Result;
10
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct LightningStats {
14 pub latest: LightningNetworkStats,
16 #[serde(default)]
18 pub previous: Option<LightningNetworkStats>,
19}
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct LightningNetworkStats {
24 #[serde(default)]
26 pub capacity: u64,
27 #[serde(default)]
29 pub channel_count: u64,
30 #[serde(default)]
32 pub node_count: u64,
33 #[serde(default)]
35 pub tor_nodes: u64,
36 #[serde(default)]
38 pub clearnet_nodes: u64,
39 #[serde(default)]
41 pub unannounced_nodes: u64,
42 #[serde(default)]
44 pub avg_capacity: u64,
45 #[serde(default)]
47 pub avg_fee_rate: u64,
48 #[serde(default)]
50 pub avg_base_fee_mtokens: u64,
51 #[serde(default)]
53 pub med_capacity: u64,
54 #[serde(default)]
56 pub med_fee_rate: u64,
57 #[serde(default)]
59 pub med_base_fee_mtokens: u64,
60}
61
62impl LightningNetworkStats {
63 pub fn capacity_btc(&self) -> f64 {
65 self.capacity as f64 / 100_000_000.0
66 }
67
68 pub fn avg_capacity_btc(&self) -> f64 {
70 self.avg_capacity as f64 / 100_000_000.0
71 }
72
73 pub fn avg_fee_rate_percent(&self) -> f64 {
75 self.avg_fee_rate as f64 / 10_000.0
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81pub struct LightningNode {
82 pub public_key: String,
84 #[serde(default)]
86 pub alias: String,
87 #[serde(default)]
89 pub channel_count: u64,
90 #[serde(default)]
92 pub capacity: u64,
93 #[serde(default)]
95 pub first_seen: u64,
96 #[serde(default)]
98 pub updated_at: u64,
99 #[serde(default)]
101 pub city: Option<LightningNodeCity>,
102 #[serde(default)]
104 pub country: Option<LightningNodeCountry>,
105}
106
107#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
109pub struct LightningNodeCity {
110 #[serde(default)]
112 pub en: String,
113}
114
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
117pub struct LightningNodeCountry {
118 #[serde(default)]
120 pub en: String,
121 #[serde(default)]
123 pub code: String,
124}
125
126impl LightningNode {
127 pub fn capacity_btc(&self) -> f64 {
129 self.capacity as f64 / 100_000_000.0
130 }
131
132 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145pub struct LightningChannel {
146 pub id: String,
148 #[serde(default)]
150 pub short_id: String,
151 #[serde(default)]
153 pub capacity: u64,
154 #[serde(default)]
156 pub transaction_id: String,
157 #[serde(default)]
159 pub transaction_vout: u32,
160 #[serde(default)]
162 pub closing_transaction_id: Option<String>,
163 #[serde(default)]
165 pub status: u8,
166}
167
168impl LightningChannel {
169 pub fn capacity_btc(&self) -> f64 {
171 self.capacity as f64 / 100_000_000.0
172 }
173
174 pub fn is_open(&self) -> bool {
176 self.status == 1
177 }
178
179 pub fn is_closed(&self) -> bool {
181 self.closing_transaction_id.is_some()
182 }
183}
184
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187pub struct TopNodes {
188 #[serde(default)]
190 pub by_capacity: Vec<LightningNode>,
191 #[serde(default)]
193 pub by_channels: Vec<LightningNode>,
194}
195
196impl MempoolClient {
198 pub async fn get_lightning_stats(&self) -> Result<LightningStats> {
200 self.get_internal("/v1/lightning/statistics/latest").await
201 }
202
203 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 pub async fn get_lightning_node(&self, pubkey: &str) -> Result<LightningNode> {
211 self.get_internal(&format!("/v1/lightning/nodes/{}", pubkey)).await
212 }
213
214 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 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 async fn get_internal<T: serde::de::DeserializeOwned>(&self, endpoint: &str) -> Result<T> {
226 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, 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, avg_fee_rate: 500, 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, 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, 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}