eth-etl-core 0.1.0

Core types and utilities for Ethereum ETL
Documentation
use tiny_keccak::{Hasher, Keccak};

/// Convert a hex string (with or without 0x prefix) to u64
pub fn hex_to_dec(hex: &str) -> Option<u64> {
    let hex = hex.strip_prefix("0x").unwrap_or(hex);
    if hex.is_empty() {
        return Some(0);
    }
    u64::from_str_radix(hex, 16).ok()
}

/// Convert a hex string to u128 for larger values
pub fn hex_to_dec_u128(hex: &str) -> Option<u128> {
    let hex = hex.strip_prefix("0x").unwrap_or(hex);
    if hex.is_empty() {
        return Some(0);
    }
    u128::from_str_radix(hex, 16).ok()
}

/// Convert a hex string to a big integer string for very large values (256-bit)
pub fn hex_to_dec_string(hex: &str) -> Option<String> {
    let hex = hex.strip_prefix("0x").unwrap_or(hex);
    if hex.is_empty() {
        return Some("0".to_string());
    }

    // Parse as U256 and convert to decimal string
    let padded = format!("{:0>64}", hex);
    let bytes = hex::decode(&padded).ok()?;
    let value = primitive_types::U256::from_big_endian(&bytes);
    Some(value.to_string())
}

/// Normalize an Ethereum address to lowercase with 0x prefix
pub fn normalize_address(address: Option<&str>) -> Option<String> {
    address.map(|a| {
        let addr = a.strip_prefix("0x").unwrap_or(a);
        format!("0x{}", addr.to_lowercase())
    })
}

/// Compute Keccak-256 hash of input string
pub fn keccak256(input: &str) -> String {
    let mut hasher = Keccak::v256();
    let mut output = [0u8; 32];
    hasher.update(input.as_bytes());
    hasher.finalize(&mut output);
    format!("0x{}", hex::encode(output))
}

/// Compute Keccak-256 hash and return first 4 bytes (function selector)
pub fn keccak256_selector(input: &str) -> String {
    let mut hasher = Keccak::v256();
    let mut output = [0u8; 32];
    hasher.update(input.as_bytes());
    hasher.finalize(&mut output);
    format!("0x{}", hex::encode(&output[..4]))
}

/// Convert unix timestamp to ISO-8601 string
pub fn timestamp_to_iso(timestamp: u64) -> String {
    use chrono::{DateTime, Utc};
    let dt = DateTime::<Utc>::from_timestamp(timestamp as i64, 0);
    dt.map(|d| d.format("%Y-%m-%d %H:%M:%S").to_string())
        .unwrap_or_default()
}

/// Convert a number to hex string with 0x prefix
pub fn dec_to_hex(value: u64) -> String {
    format!("0x{:x}", value)
}

/// Parse a boolean-like value from JSON (handles string "true"/"false" and actual bools)
pub fn parse_bool(value: &serde_json::Value) -> Option<bool> {
    match value {
        serde_json::Value::Bool(b) => Some(*b),
        serde_json::Value::String(s) => match s.to_lowercase().as_str() {
            "true" | "1" => Some(true),
            "false" | "0" => Some(false),
            _ => None,
        },
        serde_json::Value::Number(n) => n.as_u64().map(|v| v != 0),
        _ => None,
    }
}

/// ERC20 Transfer event topic (keccak256("Transfer(address,address,uint256)"))
pub const TRANSFER_EVENT_TOPIC: &str =
    "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";

/// Check if a topic matches the ERC20 Transfer event
pub fn is_transfer_event(topic: &str) -> bool {
    topic.to_lowercase() == TRANSFER_EVENT_TOPIC
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_hex_to_dec() {
        assert_eq!(hex_to_dec("0x1"), Some(1));
        assert_eq!(hex_to_dec("0xff"), Some(255));
        assert_eq!(hex_to_dec("0x"), Some(0));
        assert_eq!(hex_to_dec(""), Some(0));
        assert_eq!(hex_to_dec("0x10"), Some(16));
        assert_eq!(hex_to_dec("10"), Some(16));
    }

    #[test]
    fn test_normalize_address() {
        assert_eq!(
            normalize_address(Some("0xABC123")),
            Some("0xabc123".to_string())
        );
        assert_eq!(
            normalize_address(Some("ABC123")),
            Some("0xabc123".to_string())
        );
        assert_eq!(normalize_address(None), None);
    }

    #[test]
    fn test_keccak256() {
        let hash = keccak256("Transfer(address,address,uint256)");
        assert_eq!(hash, TRANSFER_EVENT_TOPIC);
    }

    #[test]
    fn test_keccak256_selector() {
        let selector = keccak256_selector("transfer(address,uint256)");
        assert_eq!(selector, "0xa9059cbb");
    }
}