use crate::error::Result;
use chrono::{DateTime, TimeZone, Utc};
use url::Url;
pub(crate) fn normalize_url(url: &str) -> Result<Url> {
let url = if url.ends_with('/') {
&url[..url.len() - 1]
} else {
url
};
let parsed = Url::parse(url)?;
Ok(parsed)
}
pub fn sats_to_btc(sats: u64) -> f64 {
sats as f64 / 100_000_000.0
}
pub fn btc_to_sats(btc: f64) -> u64 {
(btc * 100_000_000.0) as u64
}
pub fn parse_ln_connection_string(conn_str: &str) -> Option<(String, String, u16)> {
let parts: Vec<&str> = conn_str.split('@').collect();
if parts.len() != 2 {
return None;
}
let pubkey = parts[0].to_string();
let addr_parts: Vec<&str> = parts[1].split(':').collect();
if addr_parts.len() != 2 {
return None;
}
let host = addr_parts[0].to_string();
let port = addr_parts[1].parse().ok()?;
Some((pubkey, host, port))
}
pub(crate) fn validate_channel_params(
lsp_balance_sat: u64,
client_balance_sat: u64,
channel_expiry_weeks: u32,
min_channel_size: u64,
max_channel_size: u64,
min_expiry: u32,
max_expiry: u32,
) -> Result<()> {
let total_size = lsp_balance_sat + client_balance_sat;
if total_size < min_channel_size {
return Err(crate::error::BlocktankError::BlocktankClient {
message: "Channel size too small".to_string(),
data: serde_json::json!({
"min_size": min_channel_size,
"requested_size": total_size
}),
});
}
if total_size > max_channel_size {
return Err(crate::error::BlocktankError::BlocktankClient {
message: "Channel size too large".to_string(),
data: serde_json::json!({
"max_size": max_channel_size,
"requested_size": total_size
}),
});
}
if channel_expiry_weeks < min_expiry || channel_expiry_weeks > max_expiry {
return Err(crate::error::BlocktankError::BlocktankClient {
message: "Invalid expiry period".to_string(),
data: serde_json::json!({
"min_weeks": min_expiry,
"max_weeks": max_expiry,
"requested_weeks": channel_expiry_weeks
}),
});
}
Ok(())
}
pub fn datetime_to_string(date: &DateTime<Utc>) -> String {
date.to_rfc3339()
}
pub fn timestamp_to_datetime(timestamp: i64) -> DateTime<Utc> {
Utc.timestamp_opt(timestamp, 0)
.single()
.expect("Invalid timestamp")
}
pub fn datetime_to_timestamp(date: &DateTime<Utc>) -> i64 {
date.timestamp()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_url() {
let url = normalize_url("http://example.com/api/").unwrap();
assert_eq!(url.as_str(), "http://example.com/api");
let url = normalize_url("http://example.com/api").unwrap();
assert_eq!(url.as_str(), "http://example.com/api");
}
#[test]
fn test_sats_conversion() {
assert_eq!(sats_to_btc(100_000_000), 1.0);
assert_eq!(btc_to_sats(1.0), 100_000_000);
}
#[test]
fn test_parse_ln_connection_string() {
let conn_str =
"023c22c02645293e1600b8c5f14136844af0cc6701f2d3422c571bbb4208aa5781@127.0.0.1:9735";
let (pubkey, host, port) = parse_ln_connection_string(conn_str).unwrap();
assert_eq!(
pubkey,
"023c22c02645293e1600b8c5f14136844af0cc6701f2d3422c571bbb4208aa5781"
);
assert_eq!(host, "127.0.0.1");
assert_eq!(port, 9735);
assert!(parse_ln_connection_string("invalid").is_none());
}
#[test]
fn test_validate_channel_params() {
assert!(validate_channel_params(
50_000, 50_000, 4, 50_000, 1_000_000, 1, 52 )
.is_ok());
assert!(validate_channel_params(
10_000, 10_000, 4, 50_000, 1_000_000, 1, 52 )
.is_err());
assert!(validate_channel_params(
2_000_000, 0, 4, 50_000, 1_000_000, 1, 52 )
.is_err());
assert!(validate_channel_params(
50_000, 50_000, 60, 50_000, 1_000_000, 1, 52 )
.is_err());
}
#[test]
fn test_timestamp_conversions() {
let timestamp = 1706013296; let dt = timestamp_to_datetime(timestamp);
assert_eq!(datetime_to_timestamp(&dt), timestamp);
}
}