ostium-rust-sdk 0.1.0

Rust SDK for interacting with the Ostium trading platform on Arbitrum
Documentation
//! Integration tests for the Ostium Rust SDK
//!
//! These tests run against the actual testnet to verify functionality.
//! Make sure you have a valid private key set in the OSTIUM_PRIVATE_KEY environment variable
//! or the tests will be skipped.

use ostium_rust_sdk::{Network, OstiumClient, Result};
use rust_decimal::Decimal;
use std::env;

/// Helper function to create a test client
async fn create_test_client() -> Result<OstiumClient> {
    OstiumClient::builder(Network::Testnet)
        .with_network_retry()
        .build()
        .await
}

/// Helper function to create a test client with signer (if private key is available)
async fn create_test_client_with_signer() -> Option<OstiumClient> {
    if let Ok(private_key) = env::var("OSTIUM_PRIVATE_KEY") {
        OstiumClient::builder(Network::Testnet)
            .with_private_key(&private_key)
            .ok()?
            .with_network_retry()
            .build()
            .await
            .ok()
    } else {
        println!("Skipping tests requiring signer - set OSTIUM_PRIVATE_KEY environment variable");
        None
    }
}

#[tokio::test]
async fn test_client_creation() {
    let client = create_test_client().await.unwrap();
    assert_eq!(client.config().network, Network::Testnet);
    assert!(!client.has_signer());
}

#[tokio::test]
async fn test_client_creation_with_signer() {
    if let Some(client) = create_test_client_with_signer().await {
        assert_eq!(client.config().network, Network::Testnet);
        assert!(client.has_signer());
        assert!(client.signer_address().is_some());
    }
}

#[tokio::test]
async fn test_get_trading_pairs() {
    let client = create_test_client().await.unwrap();

    let pairs = client.get_pairs().await.unwrap();
    assert!(!pairs.is_empty(), "Should have at least one trading pair");

    // Check that we have some common pairs
    let symbols: Vec<&str> = pairs.iter().map(|p| p.symbol.as_str()).collect();
    println!("Available pairs: {:?}", symbols);

    // Verify structure of first pair
    let first_pair = &pairs[0];
    assert!(!first_pair.symbol.is_empty());
    assert!(!first_pair.base_asset.is_empty());
    assert!(!first_pair.quote_asset.is_empty());
    assert!(first_pair.symbol.contains('/'));
}

#[tokio::test]
async fn test_get_price_btc() {
    let client = create_test_client().await.unwrap();

    let price = client.get_price("BTC/USD").await.unwrap();

    assert_eq!(price.symbol, "BTC/USD");
    assert!(price.mark_price > Decimal::from(0));
    assert!(price.index_price > Decimal::from(0));
    assert!(price.high_24h > price.low_24h);
    println!("BTC/USD Price: ${}", price.mark_price);
}

#[tokio::test]
async fn test_get_price_eth() {
    let client = create_test_client().await.unwrap();

    let price = client.get_price("ETH/USD").await.unwrap();

    assert_eq!(price.symbol, "ETH/USD");
    assert!(price.mark_price > Decimal::from(0));
    println!("ETH/USD Price: ${}", price.mark_price);
}

#[tokio::test]
async fn test_get_trading_hours() {
    let client = create_test_client().await.unwrap();

    let hours = client.get_trading_hours("BTC/USD").await.unwrap();

    assert_eq!(hours.symbol, "BTC/USD");
    // BTC should typically be open 24/7
    assert!(hours.is_open);
    println!("BTC/USD Market Open: {}", hours.is_open);
}

#[tokio::test]
async fn test_get_balance_no_signer() {
    let client = create_test_client().await.unwrap();

    // This should fail because we don't have a signer
    let result = client.get_balance(None).await;
    assert!(result.is_err());
    println!("Expected error without signer: {:?}", result.unwrap_err());
}

#[tokio::test]
async fn test_get_balance_with_signer() {
    if let Some(client) = create_test_client_with_signer().await {
        let balance = client.get_balance(None).await.unwrap();

        assert_eq!(balance.asset, "USDC");
        assert!(balance.total >= Decimal::from(0));
        assert!(balance.available >= Decimal::from(0));
        println!("Account Balance: {} USDC", balance.total);
    }
}

#[tokio::test]
async fn test_get_positions_with_signer() {
    if let Some(client) = create_test_client_with_signer().await {
        let positions = client.get_positions(None).await.unwrap();

        // Positions array might be empty, which is fine
        println!("Found {} open positions", positions.len());

        for position in positions {
            assert!(!position.id.is_empty());
            assert!(!position.symbol.is_empty());
            assert!(position.size > Decimal::from(0));
            println!(
                "Position: {} {:?} {}",
                position.symbol, position.side, position.size
            );
        }
    }
}

#[tokio::test]
async fn test_get_orders_with_signer() {
    if let Some(client) = create_test_client_with_signer().await {
        let orders = client.get_orders(None).await.unwrap();

        // Orders array might be empty, which is fine
        println!("Found {} open orders", orders.len());

        for order in orders {
            assert!(!order.id.is_empty());
            assert!(!order.symbol.is_empty());
            assert!(order.size > Decimal::from(0));
            println!(
                "Order: {} {:?} {} @ {:?}",
                order.symbol, order.side, order.size, order.price
            );
        }
    }
}

#[tokio::test]
async fn test_multiple_price_requests_concurrent() {
    let client = create_test_client().await.unwrap();

    // Test concurrent requests to ensure no race conditions
    let (btc_result, eth_result, sol_result) = tokio::join!(
        client.get_price("BTC/USD"),
        client.get_price("ETH/USD"),
        client.get_price("SOL/USD")
    );

    let mut successful_requests = 0;

    if let Ok(price) = btc_result {
        successful_requests += 1;
        println!("Got price for {}: ${}", price.symbol, price.mark_price);
    }

    if let Ok(price) = eth_result {
        successful_requests += 1;
        println!("Got price for {}: ${}", price.symbol, price.mark_price);
    }

    if let Ok(price) = sol_result {
        successful_requests += 1;
        println!("Got price for {}: ${}", price.symbol, price.mark_price);
    }

    assert!(
        successful_requests >= 2,
        "At least 2 price requests should succeed"
    );
}

#[tokio::test]
async fn test_error_handling_invalid_symbol() {
    let client = create_test_client().await.unwrap();

    // Test error handling with invalid symbol
    let result = client.get_price("INVALID/SYMBOL").await;
    assert!(result.is_err());
    println!(
        "Expected error for invalid symbol: {:?}",
        result.unwrap_err()
    );
}

#[tokio::test]
async fn test_retry_mechanism() {
    let client = OstiumClient::builder(Network::Testnet)
        .with_network_retry() // This should handle transient failures
        .build()
        .await
        .unwrap();

    // This should work even if there are some transient network issues
    let pairs = client.get_pairs().await.unwrap();
    assert!(!pairs.is_empty());
}

#[tokio::test]
async fn test_client_configuration() {
    // Test different client configurations
    let standard_client = OstiumClient::builder(Network::Testnet)
        .build()
        .await
        .unwrap();

    let network_optimized_client = OstiumClient::builder(Network::Testnet)
        .with_network_retry()
        .build()
        .await
        .unwrap();

    let contract_optimized_client = OstiumClient::builder(Network::Testnet)
        .with_contract_retry()
        .build()
        .await
        .unwrap();

    let graphql_optimized_client = OstiumClient::builder(Network::Testnet)
        .with_graphql_retry()
        .build()
        .await
        .unwrap();

    // All should be able to get pairs
    assert!(standard_client.get_pairs().await.is_ok());
    assert!(network_optimized_client.get_pairs().await.is_ok());
    assert!(contract_optimized_client.get_pairs().await.is_ok());
    assert!(graphql_optimized_client.get_pairs().await.is_ok());
}

// Performance and stress tests
#[tokio::test]
async fn test_performance_multiple_requests() {
    let client = create_test_client().await.unwrap();
    let start_time = std::time::Instant::now();

    // Make 5 sequential requests (simpler than concurrent)
    let mut successful_requests = 0;

    for _ in 0..5 {
        if client.get_price("BTC/USD").await.is_ok() {
            successful_requests += 1;
        }
    }

    let elapsed = start_time.elapsed();
    println!(
        "Completed {} requests in {:?}",
        successful_requests, elapsed
    );

    assert!(
        successful_requests >= 4,
        "At least 4 out of 5 requests should succeed"
    );
    assert!(
        elapsed.as_secs() < 30,
        "All requests should complete within 30 seconds"
    );
}

// Test that would require actual trading (commented out for safety)
/*
#[tokio::test]
async fn test_open_position_integration() {
    if let Some(client) = create_test_client_with_signer().await {
        // Only run on testnet with very small amounts
        let params = OpenPositionParams {
            symbol: "BTC/USD".to_string(),
            side: PositionSide::Long,
            size: Decimal::try_from(0.001).unwrap(), // Very small test size
            leverage: Decimal::from(2),
            take_profit: None,
            stop_loss: None,
            slippage_tolerance: Decimal::try_from(0.01).unwrap(),
        };

        let result = client.open_position(params).await;

        // This might fail due to insufficient balance or other reasons
        // which is fine for testing
        match result {
            Ok(tx_hash) => println!("Position opened: {:?}", tx_hash),
            Err(e) => println!("Expected failure (insufficient balance?): {}", e),
        }
    }
}
*/