rust_blocktank_client/
utils.rs

1use chrono::{DateTime, TimeZone, Utc};
2use url::Url;
3use crate::error::Result;
4
5/// Validates and normalizes a URL by ensuring:
6/// - It's a valid URL
7/// - Removes trailing slash if present
8/// - Contains the required base path
9pub(crate) fn normalize_url(url: &str) -> Result<Url> {
10    let url = if url.ends_with('/') {
11        &url[..url.len() - 1]
12    } else {
13        url
14    };
15
16    let parsed = Url::parse(url)?;
17    Ok(parsed)
18}
19
20/// Converts satoshis to bitcoin
21pub fn sats_to_btc(sats: u64) -> f64 {
22    sats as f64 / 100_000_000.0
23}
24
25/// Converts bitcoin to satoshis
26pub fn btc_to_sats(btc: f64) -> u64 {
27    (btc * 100_000_000.0) as u64
28}
29
30/// Parses a Lightning Network connection string into its components
31pub fn parse_ln_connection_string(conn_str: &str) -> Option<(String, String, u16)> {
32    let parts: Vec<&str> = conn_str.split('@').collect();
33    if parts.len() != 2 {
34        return None;
35    }
36
37    let pubkey = parts[0].to_string();
38    let addr_parts: Vec<&str> = parts[1].split(':').collect();
39    if addr_parts.len() != 2 {
40        return None;
41    }
42
43    let host = addr_parts[0].to_string();
44    let port = addr_parts[1].parse().ok()?;
45
46    Some((pubkey, host, port))
47}
48
49/// Validates channel parameters against service limits
50pub(crate) fn validate_channel_params(
51    lsp_balance_sat: u64,
52    client_balance_sat: u64,
53    channel_expiry_weeks: u32,
54    min_channel_size: u64,
55    max_channel_size: u64,
56    min_expiry: u32,
57    max_expiry: u32,
58) -> Result<()> {
59    let total_size = lsp_balance_sat + client_balance_sat;
60
61    if total_size < min_channel_size {
62        return Err(crate::error::BlocktankError::BlocktankClient {
63            message: "Channel size too small".to_string(),
64            data: serde_json::json!({
65                "min_size": min_channel_size,
66                "requested_size": total_size
67            }),
68        });
69    }
70
71    if total_size > max_channel_size {
72        return Err(crate::error::BlocktankError::BlocktankClient {
73            message: "Channel size too large".to_string(),
74            data: serde_json::json!({
75                "max_size": max_channel_size,
76                "requested_size": total_size
77            }),
78        });
79    }
80
81    if channel_expiry_weeks < min_expiry || channel_expiry_weeks > max_expiry {
82        return Err(crate::error::BlocktankError::BlocktankClient {
83            message: "Invalid expiry period".to_string(),
84            data: serde_json::json!({
85                "min_weeks": min_expiry,
86                "max_weeks": max_expiry,
87                "requested_weeks": channel_expiry_weeks
88            }),
89        });
90    }
91
92    Ok(())
93}
94
95/// Convert a DateTime<Utc> to RFC3339 string
96/// Example: DateTime<Utc> -> "2024-01-23T12:34:56Z"
97pub fn datetime_to_string(date: &DateTime<Utc>) -> String {
98    date.to_rfc3339()
99}
100
101/// Convert a Unix timestamp (in seconds) to DateTime<Utc>
102pub fn timestamp_to_datetime(timestamp: i64) -> DateTime<Utc> {
103    Utc.timestamp_opt(timestamp, 0)
104        .single()
105        .expect("Invalid timestamp")
106}
107
108/// Convert a DateTime<Utc> to Unix timestamp (in seconds)
109pub fn datetime_to_timestamp(date: &DateTime<Utc>) -> i64 {
110    date.timestamp()
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_normalize_url() {
119        let url = normalize_url("http://example.com/api/").unwrap();
120        assert_eq!(url.as_str(), "http://example.com/api");
121
122        let url = normalize_url("http://example.com/api").unwrap();
123        assert_eq!(url.as_str(), "http://example.com/api");
124    }
125
126    #[test]
127    fn test_sats_conversion() {
128        assert_eq!(sats_to_btc(100_000_000), 1.0);
129        assert_eq!(btc_to_sats(1.0), 100_000_000);
130    }
131
132    #[test]
133    fn test_parse_ln_connection_string() {
134        let conn_str = "023c22c02645293e1600b8c5f14136844af0cc6701f2d3422c571bbb4208aa5781@127.0.0.1:9735";
135        let (pubkey, host, port) = parse_ln_connection_string(conn_str).unwrap();
136        assert_eq!(pubkey, "023c22c02645293e1600b8c5f14136844af0cc6701f2d3422c571bbb4208aa5781");
137        assert_eq!(host, "127.0.0.1");
138        assert_eq!(port, 9735);
139
140        assert!(parse_ln_connection_string("invalid").is_none());
141    }
142
143    #[test]
144    fn test_validate_channel_params() {
145        // Valid parameters
146        assert!(validate_channel_params(
147            50_000,     // lsp_balance_sat
148            50_000,     // client_balance_sat
149            4,          // channel_expiry_weeks
150            50_000,     // min_channel_size
151            1_000_000,  // max_channel_size
152            1,          // min_expiry
153            52          // max_expiry
154        ).is_ok());
155
156        // Channel too small
157        assert!(validate_channel_params(
158            10_000,     // lsp_balance_sat
159            10_000,     // client_balance_sat
160            4,          // channel_expiry_weeks
161            50_000,     // min_channel_size
162            1_000_000,  // max_channel_size
163            1,          // min_expiry
164            52          // max_expiry
165        ).is_err());
166
167        // Channel too large
168        assert!(validate_channel_params(
169            2_000_000,  // lsp_balance_sat
170            0,          // client_balance_sat
171            4,          // channel_expiry_weeks
172            50_000,     // min_channel_size
173            1_000_000,  // max_channel_size
174            1,          // min_expiry
175            52          // max_expiry
176        ).is_err());
177
178        // Invalid expiry
179        assert!(validate_channel_params(
180            50_000,     // lsp_balance_sat
181            50_000,     // client_balance_sat
182            60,         // channel_expiry_weeks
183            50_000,     // min_channel_size
184            1_000_000,  // max_channel_size
185            1,          // min_expiry
186            52          // max_expiry
187        ).is_err());
188    }
189
190    #[test]
191    fn test_timestamp_conversions() {
192        let timestamp = 1706013296; // 2024-01-23T12:34:56Z
193        let dt = timestamp_to_datetime(timestamp);
194        assert_eq!(datetime_to_timestamp(&dt), timestamp);
195    }
196}