rust-blocktank-client 0.0.16

A Rust client for the Blocktank LSP HTTP API
Documentation
use crate::error::Result;
use chrono::{DateTime, TimeZone, Utc};
use url::Url;

/// Validates and normalizes a URL by ensuring:
/// - It's a valid URL
/// - Removes trailing slash if present
/// - Contains the required base path
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)
}

/// Converts satoshis to bitcoin
pub fn sats_to_btc(sats: u64) -> f64 {
    sats as f64 / 100_000_000.0
}

/// Converts bitcoin to satoshis
pub fn btc_to_sats(btc: f64) -> u64 {
    (btc * 100_000_000.0) as u64
}

/// Parses a Lightning Network connection string into its components
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))
}

/// Validates channel parameters against service limits
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(())
}

/// Convert a DateTime<Utc> to RFC3339 string
/// Example: DateTime<Utc> -> "2024-01-23T12:34:56Z"
pub fn datetime_to_string(date: &DateTime<Utc>) -> String {
    date.to_rfc3339()
}

/// Convert a Unix timestamp (in seconds) to DateTime<Utc>
pub fn timestamp_to_datetime(timestamp: i64) -> DateTime<Utc> {
    Utc.timestamp_opt(timestamp, 0)
        .single()
        .expect("Invalid timestamp")
}

/// Convert a DateTime<Utc> to Unix timestamp (in seconds)
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() {
        // Valid parameters
        assert!(validate_channel_params(
            50_000,    // lsp_balance_sat
            50_000,    // client_balance_sat
            4,         // channel_expiry_weeks
            50_000,    // min_channel_size
            1_000_000, // max_channel_size
            1,         // min_expiry
            52         // max_expiry
        )
        .is_ok());

        // Channel too small
        assert!(validate_channel_params(
            10_000,    // lsp_balance_sat
            10_000,    // client_balance_sat
            4,         // channel_expiry_weeks
            50_000,    // min_channel_size
            1_000_000, // max_channel_size
            1,         // min_expiry
            52         // max_expiry
        )
        .is_err());

        // Channel too large
        assert!(validate_channel_params(
            2_000_000, // lsp_balance_sat
            0,         // client_balance_sat
            4,         // channel_expiry_weeks
            50_000,    // min_channel_size
            1_000_000, // max_channel_size
            1,         // min_expiry
            52         // max_expiry
        )
        .is_err());

        // Invalid expiry
        assert!(validate_channel_params(
            50_000,    // lsp_balance_sat
            50_000,    // client_balance_sat
            60,        // channel_expiry_weeks
            50_000,    // min_channel_size
            1_000_000, // max_channel_size
            1,         // min_expiry
            52         // max_expiry
        )
        .is_err());
    }

    #[test]
    fn test_timestamp_conversions() {
        let timestamp = 1706013296; // 2024-01-23T12:34:56Z
        let dt = timestamp_to_datetime(timestamp);
        assert_eq!(datetime_to_timestamp(&dt), timestamp);
    }
}