rust_blocktank_client/
utils.rs

1use crate::error::Result;
2use chrono::{DateTime, TimeZone, Utc};
3use url::Url;
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 =
135            "023c22c02645293e1600b8c5f14136844af0cc6701f2d3422c571bbb4208aa5781@127.0.0.1:9735";
136        let (pubkey, host, port) = parse_ln_connection_string(conn_str).unwrap();
137        assert_eq!(
138            pubkey,
139            "023c22c02645293e1600b8c5f14136844af0cc6701f2d3422c571bbb4208aa5781"
140        );
141        assert_eq!(host, "127.0.0.1");
142        assert_eq!(port, 9735);
143
144        assert!(parse_ln_connection_string("invalid").is_none());
145    }
146
147    #[test]
148    fn test_validate_channel_params() {
149        // Valid parameters
150        assert!(validate_channel_params(
151            50_000,    // lsp_balance_sat
152            50_000,    // client_balance_sat
153            4,         // channel_expiry_weeks
154            50_000,    // min_channel_size
155            1_000_000, // max_channel_size
156            1,         // min_expiry
157            52         // max_expiry
158        )
159        .is_ok());
160
161        // Channel too small
162        assert!(validate_channel_params(
163            10_000,    // lsp_balance_sat
164            10_000,    // client_balance_sat
165            4,         // channel_expiry_weeks
166            50_000,    // min_channel_size
167            1_000_000, // max_channel_size
168            1,         // min_expiry
169            52         // max_expiry
170        )
171        .is_err());
172
173        // Channel too large
174        assert!(validate_channel_params(
175            2_000_000, // lsp_balance_sat
176            0,         // client_balance_sat
177            4,         // channel_expiry_weeks
178            50_000,    // min_channel_size
179            1_000_000, // max_channel_size
180            1,         // min_expiry
181            52         // max_expiry
182        )
183        .is_err());
184
185        // Invalid expiry
186        assert!(validate_channel_params(
187            50_000,    // lsp_balance_sat
188            50_000,    // client_balance_sat
189            60,        // channel_expiry_weeks
190            50_000,    // min_channel_size
191            1_000_000, // max_channel_size
192            1,         // min_expiry
193            52         // max_expiry
194        )
195        .is_err());
196    }
197
198    #[test]
199    fn test_timestamp_conversions() {
200        let timestamp = 1706013296; // 2024-01-23T12:34:56Z
201        let dt = timestamp_to_datetime(timestamp);
202        assert_eq!(datetime_to_timestamp(&dt), timestamp);
203    }
204}