ostium-rust-sdk 0.1.0

Rust SDK for interacting with the Ostium trading platform on Arbitrum
Documentation
use ostium_rust_sdk::abi::{OSTIUM_TRADING_ABI, TRADING_STORAGE_ABI, USDC_ABI};
use ostium_rust_sdk::contracts::fetcher::ContractFetcher;
use ostium_rust_sdk::error::{OstiumError, Result};
use reqwest::Client;
use serde_json::Value;
use std::collections::HashMap;

const OSTIUM_PYTHON_SDK_ABI_URL: &str = "https://raw.githubusercontent.com/0xOstium/ostium-python-sdk/main/ostium_python_sdk/abi/abi.py";

#[tokio::main]
async fn main() -> Result<()> {
    println!("🔄 Fetching latest ABIs from Ostium Python SDK...");

    // Method 1: Use the built-in ABIs
    println!("✅ Built-in ABIs available:");
    println!("  📄 USDC ABI: {} characters", USDC_ABI.len());
    println!(
        "  📄 Ostium Trading ABI: {} characters",
        OSTIUM_TRADING_ABI.len()
    );
    println!(
        "  📄 Trading Storage ABI: {} characters",
        TRADING_STORAGE_ABI.len()
    );

    // Method 2: Fetch the latest ABIs dynamically from Python SDK
    match fetch_latest_abis_from_python_sdk().await {
        Ok(abis) => {
            println!(
                "✅ Successfully fetched {} ABIs from Python SDK:",
                abis.len()
            );
            for (name, abi_json) in abis.iter() {
                println!("  📄 {}: {} characters", name, abi_json.len());

                // Validate that it's proper JSON
                match serde_json::from_str::<Value>(abi_json) {
                    Ok(_) => println!("    ✅ Valid JSON ABI"),
                    Err(e) => println!("    ❌ Invalid JSON: {}", e),
                }
            }
        }
        Err(e) => {
            println!("❌ Failed to fetch ABIs from Python SDK: {}", e);
        }
    }

    // Method 3: Compare built-in vs fetched ABIs
    println!("\n🔍 Comparing built-in vs fetched ABIs...");
    if let Ok(fetched_abis) = fetch_latest_abis_from_python_sdk().await {
        for (name, fetched_abi) in fetched_abis.iter() {
            let built_in_abi = match name.as_str() {
                "usdc" => Some(USDC_ABI),
                "ostium_trading" => Some(OSTIUM_TRADING_ABI),
                "trading_storage" => Some(TRADING_STORAGE_ABI),
                _ => None,
            };

            if let Some(built_in) = built_in_abi {
                if fetched_abi.trim() == built_in.trim() {
                    println!("{} ABI matches", name);
                } else {
                    println!("  ⚠️  {} ABI differs from built-in version", name);
                    println!(
                        "     Built-in: {} chars, Fetched: {} chars",
                        built_in.len(),
                        fetched_abi.len()
                    );
                }
            } else {
                println!("  🆕 {} ABI is new (not in built-in)", name);
            }
        }
    }

    // Method 4: Test the contract fetcher functionality
    println!("\n🔄 Testing contract fetcher functionality...");
    if let Err(e) = check_for_updates_example().await {
        println!("❌ Contract fetcher test failed: {}", e);
    }

    Ok(())
}

async fn fetch_latest_abis_from_python_sdk() -> Result<HashMap<String, String>> {
    let client = Client::new();

    // Fetch the Python ABI file
    let response = client
        .get(OSTIUM_PYTHON_SDK_ABI_URL)
        .header("User-Agent", "ostium-rust-sdk")
        .send()
        .await
        .map_err(|e| OstiumError::network(format!("Failed to fetch Python SDK: {}", e)))?;

    let python_abi_content = response
        .text()
        .await
        .map_err(|e| OstiumError::network(format!("Failed to read response: {}", e)))?;

    // Parse the Python file to extract ABIs
    parse_python_abis(&python_abi_content)
}

fn parse_python_abis(python_content: &str) -> Result<HashMap<String, String>> {
    let mut abis = HashMap::new();

    // Parse the Python file to extract ABI definitions
    // Look for patterns like: contract_name_abi = [...]
    let lines: Vec<&str> = python_content.lines().collect();
    let mut current_abi_name = String::new();
    let mut current_abi_content = String::new();
    let mut in_abi = false;
    let mut bracket_count = 0;

    for line in lines {
        let trimmed = line.trim();

        // Check if this line starts an ABI definition
        if trimmed.ends_with("_abi = [") {
            if let Some(name_part) = trimmed.strip_suffix("_abi = [") {
                current_abi_name = name_part.trim().to_string();
                current_abi_content = String::from("[\n");
                in_abi = true;
                bracket_count = 1;
                continue;
            }
        }

        if in_abi {
            // Count brackets to know when the ABI definition ends
            for ch in trimmed.chars() {
                match ch {
                    '[' | '{' => bracket_count += 1,
                    ']' | '}' => bracket_count -= 1,
                    _ => {}
                }
            }

            current_abi_content.push_str(line);
            current_abi_content.push('\n');

            // If we've closed all brackets, we're done with this ABI
            if bracket_count == 0 {
                in_abi = false;

                // Convert Python format to JSON format
                let json_abi = convert_python_to_json(&current_abi_content)?;

                // Extract contract name from variable name (remove _abi suffix)
                let contract_name = if current_abi_name.ends_with("_abi") {
                    current_abi_name.trim_end_matches("_abi").to_string()
                } else {
                    current_abi_name.clone()
                };

                abis.insert(contract_name, json_abi);
                current_abi_name.clear();
                current_abi_content.clear();
            }
        }
    }

    Ok(abis)
}

fn convert_python_to_json(python_abi: &str) -> Result<String> {
    // Convert Python syntax to JSON syntax
    let mut json_content = python_abi
        .replace("True", "true")
        .replace("False", "false")
        .replace("None", "null");

    // Process line by line to handle comments and formatting
    json_content = json_content
        .lines()
        .map(|line| {
            // Remove Python comments (lines starting with # or inline comments)
            let line_without_comment = if let Some(comment_pos) = line.find('#') {
                // Check if the # is inside a string literal by counting quotes before it
                let before_comment = &line[..comment_pos];
                let quote_count = before_comment.matches('"').count();
                if quote_count % 2 == 0 {
                    // Even number of quotes means # is outside string, so it's a comment
                    before_comment.trim_end().to_string()
                } else {
                    // Odd number of quotes means # is inside string, keep the line
                    line.to_string()
                }
            } else {
                line.to_string()
            };

            // Handle trailing commas before closing brackets/braces
            let trimmed_no_comment = line_without_comment.trim();
            if trimmed_no_comment.ends_with(",}") {
                line_without_comment.replace(",}", "}")
            } else if trimmed_no_comment.ends_with(",]") {
                line_without_comment.replace(",]", "]")
            } else {
                line_without_comment
            }
        })
        .filter(|line| {
            let trimmed = line.trim();
            // Remove empty lines and lines that are just comments
            !trimmed.is_empty() && !trimmed.starts_with('#')
        })
        .collect::<Vec<String>>()
        .join("\n");

    // Clean up any remaining issues
    json_content = json_content.trim().to_string();

    // Remove any trailing content after the final closing bracket
    if let Some(last_bracket_pos) = json_content.rfind(']') {
        // Find if there's any non-whitespace content after the last bracket
        let after_bracket = &json_content[last_bracket_pos + 1..];
        if after_bracket.trim().is_empty() || after_bracket.trim().starts_with('#') {
            // Truncate at the last bracket if there's only whitespace or comments after
            json_content = json_content[..=last_bracket_pos].to_string();
        }
    }

    // Validate that it's proper JSON
    let _: serde_json::Value = serde_json::from_str(&json_content)
        .map_err(|e| OstiumError::parsing(format!("Failed to parse ABI as JSON: {}", e)))?;

    Ok(json_content)
}

/// Example function showing how to check for contract updates
pub async fn check_for_updates_example() -> Result<()> {
    let mut fetcher = ContractFetcher::new();

    // Simulate some known contracts (in practice, you'd load these from storage)
    let known_contracts = vec![
        // This would be loaded from your local storage/cache
    ];

    match fetcher.check_for_updates(&known_contracts).await {
        Ok(updates) => {
            if updates.is_empty() {
                println!("✅ All contracts are up to date");
            } else {
                println!("🔄 Found {} contract updates:", updates.len());
                for contract in updates {
                    println!("  📄 {} has been updated", contract.name);
                }
            }
        }
        Err(e) => {
            println!("❌ Failed to check for updates: {}", e);
        }
    }

    Ok(())
}