asterdex-sdk 0.1.5

AsterDex Futures SDK v3 — Rust async client for REST and WebSocket APIs
Documentation
// Integration tests for public (no-auth) market data endpoints.
//
// Run: cargo test --test aster_market_data -- --include-ignored --test-threads=1
//
// All tests are #[ignore] — they hit https://fapi.asterdex-testnet.com directly.
// No credentials needed; every test uses a public RestClient.

use asterdex_sdk::{BookTickerShape, FuturesClient, KlineInterval, MarkPriceShape, Ticker24hrShape, TickerPriceShape};

fn public_client() -> FuturesClient {
    let _ = dotenvy::dotenv();
    FuturesClient::new_public("https://fapi.asterdex-testnet.com")
        .expect("failed to build public client")
}

// ---------------------------------------------------------------------------
// 1. Exchange Info
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore]
async fn test_exchange_info_has_btcusdt() {
    let client = public_client();
    let resp = client.get_exchange_info().await.expect("get_exchange_info failed");
    assert!(!resp.data.symbols.is_empty(), "symbols list is empty");
    assert!(
        resp.data.symbols.iter().any(|s| s.symbol == "BTCUSDT"),
        "BTCUSDT not found in exchange info"
    );
    assert!(!resp.data.timezone.is_empty(), "timezone is empty");
    assert!(resp.data.server_time > 0, "server_time is zero");
}

// ---------------------------------------------------------------------------
// 2. Order Book Depth
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore]
async fn test_depth_btcusdt_valid_spread() {
    let client = public_client();
    let resp = client.get_depth("BTCUSDT", Some(5)).await.expect("get_depth failed");
    assert!(resp.data.last_update_id > 0, "last_update_id is zero");
    assert!(!resp.data.bids.is_empty(), "bids is empty");
    assert!(!resp.data.asks.is_empty(), "asks is empty");
    let best_bid: f64 = resp.data.bids[0][0].parse().expect("bid price is not f64");
    let best_ask: f64 = resp.data.asks[0][0].parse().expect("ask price is not f64");
    assert!(best_bid > 0.0 && best_ask > 0.0, "prices must be positive");
    assert!(best_bid < best_ask, "bid {best_bid} >= ask {best_ask} — invalid spread");
}

// ---------------------------------------------------------------------------
// 3. Recent Trades
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore]
async fn test_recent_trades_btcusdt() {
    let client = public_client();
    let resp = client.get_trades("BTCUSDT", Some(5)).await.expect("get_trades failed");
    assert!(!resp.data.is_empty(), "recent trades is empty");
    let t = &resp.data[0];
    let price: f64 = t.price.parse().expect("trade price not f64");
    assert!(price > 0.0, "trade price must be positive");
    assert!(t.time > 0, "trade time is zero");
}

// ---------------------------------------------------------------------------
// 4. Historical Trades (may require special permissions on some endpoints)
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore]
async fn test_historical_trades_btcusdt() {
    let client = public_client();
    match client.get_historical_trades("BTCUSDT", Some(5), None).await {
        Ok(r) => {
            // Verify structure when accessible
            if let Some(t) = r.data.first() {
                assert!(t.id > 0);
                assert!(!t.price.is_empty());
            }
        }
        Err(e) => {
            // Acceptable: some endpoints require auth headers not in EIP-712 scheme
            eprintln!("get_historical_trades returned error (may require API-key auth): {e:?}");
        }
    }
}

// ---------------------------------------------------------------------------
// 5. Aggregate Trades
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore]
async fn test_agg_trades_btcusdt() {
    let client = public_client();
    let resp = client
        .get_agg_trades("BTCUSDT", None, None, None, Some(5))
        .await
        .expect("get_agg_trades failed");
    assert!(!resp.data.is_empty(), "agg_trades is empty");
    let first = &resp.data[0];
    assert!(first.agg_trade_id > 0, "agg_trade_id must be positive");
    let price: f64 = first.price.parse().expect("agg trade price not f64");
    assert!(price > 0.0, "agg trade price must be positive");
    let qty: f64 = first.quantity.parse().expect("agg trade quantity not f64");
    assert!(qty > 0.0, "agg trade quantity must be positive");
}

// ---------------------------------------------------------------------------
// 6. Klines (1h)
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore]
async fn test_klines_1h_btcusdt() {
    let client = public_client();
    let resp = client
        .get_klines("BTCUSDT", KlineInterval::OneHour, None, None, Some(5))
        .await
        .expect("get_klines failed");
    assert!(!resp.data.is_empty(), "klines is empty");
    let k = &resp.data[0];
    assert!(k.open_time > 0, "open_time is zero");
    assert!(k.close_time > k.open_time, "close_time <= open_time");
    let open: f64 = k.open.parse().expect("open price not f64");
    let high: f64 = k.high.parse().expect("high price not f64");
    let low: f64 = k.low.parse().expect("low price not f64");
    let close: f64 = k.close.parse().expect("close price not f64");
    assert!(open > 0.0 && high > 0.0 && low > 0.0 && close > 0.0);
    assert!(high >= low, "high {high} < low {low}");
}

// ---------------------------------------------------------------------------
// 7. Mark Price Klines
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore]
async fn test_mark_price_klines_btcusdt() {
    let client = public_client();
    let resp = client
        .get_mark_price_klines("BTCUSDT", KlineInterval::OneHour, None, None, Some(5))
        .await
        .expect("get_mark_price_klines failed");
    assert!(!resp.data.is_empty(), "mark_price_klines is empty");
    assert!(resp.data[0].open_time > 0);
    let open: f64 = resp.data[0].open.parse().expect("open not f64");
    assert!(open > 0.0);
}

// ---------------------------------------------------------------------------
// 8. Index Price Klines
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore]
async fn test_index_price_klines_btcusdt() {
    let client = public_client();
    let resp = client
        .get_index_price_klines("BTCUSDT", KlineInterval::OneHour, None, None, Some(5))
        .await
        .expect("get_index_price_klines failed");
    assert!(!resp.data.is_empty(), "index_price_klines is empty");
    assert!(resp.data[0].open_time > 0);
}

// ---------------------------------------------------------------------------
// 9. Mark Price — single symbol
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore]
async fn test_mark_price_single_btcusdt() {
    let client = public_client();
    let resp = client
        .get_mark_price(Some("BTCUSDT"))
        .await
        .expect("get_mark_price(BTCUSDT) failed");
    match resp.data {
        MarkPriceShape::Single(mp) => {
            assert_eq!(mp.symbol, "BTCUSDT");
            let price: f64 = mp.mark_price.parse().expect("mark_price not f64");
            assert!(price > 0.0, "mark_price must be positive");
            assert!(mp.next_funding_time > 0);
        }
        MarkPriceShape::Multi(_) => panic!("expected Single for BTCUSDT, got Multi"),
    }
}

// ---------------------------------------------------------------------------
// 10. Mark Price — all symbols
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore]
async fn test_mark_price_all_symbols() {
    let client = public_client();
    let resp = client
        .get_mark_price(None)
        .await
        .expect("get_mark_price(None) failed");
    match resp.data {
        MarkPriceShape::Multi(prices) => {
            assert!(!prices.is_empty(), "mark prices list is empty");
            assert!(
                prices.iter().any(|mp| mp.symbol == "BTCUSDT"),
                "BTCUSDT not in multi mark prices"
            );
        }
        MarkPriceShape::Single(_) => panic!("expected Multi for all symbols, got Single"),
    }
}

// ---------------------------------------------------------------------------
// 11. Funding Rate
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore]
async fn test_funding_rate_btcusdt() {
    let client = public_client();
    // The fundingRate endpoint can be slow on the testnet; treat Timeout as a skip.
    match client.get_funding_rate(Some("BTCUSDT"), None, None, Some(5)).await {
        Ok(resp) => {
            assert!(!resp.data.is_empty(), "funding_rate history is empty");
            let r = &resp.data[0];
            assert_eq!(r.symbol, "BTCUSDT");
            assert!(r.funding_time > 0, "funding_time is zero");
        }
        Err(asterdex_sdk::AsterDexError::Timeout { .. }) => {
            eprintln!("get_funding_rate timed out on testnet — skipping");
        }
        Err(e) => panic!("get_funding_rate failed: {e:?}"),
    }
}

// ---------------------------------------------------------------------------
// 12. Funding Info
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore]
async fn test_funding_info_non_empty() {
    let client = public_client();
    let resp = client.get_funding_info().await.expect("get_funding_info failed");
    assert!(!resp.data.is_empty(), "funding info is empty");
    let first = &resp.data[0];
    assert!(!first.symbol.is_empty(), "symbol is empty");
    // funding_interval_hours may be null for some newly listed symbols
    if let Some(hours) = first.funding_interval_hours {
        assert!(hours > 0, "funding_interval_hours is zero");
    }
}

// ---------------------------------------------------------------------------
// 13. 24hr Ticker — single symbol
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore]
async fn test_ticker_24hr_btcusdt() {
    let client = public_client();
    let resp = client
        .get_ticker_24hr(Some("BTCUSDT"))
        .await
        .expect("get_ticker_24hr failed");
    match resp.data {
        Ticker24hrShape::Single(t) => {
            assert_eq!(t.symbol, "BTCUSDT");
            let last_price: f64 = t.last_price.parse().expect("last_price not f64");
            assert!(last_price > 0.0, "last_price must be positive");
            assert!(t.close_time > t.open_time);
        }
        Ticker24hrShape::Multi(_) => panic!("expected Single for BTCUSDT, got Multi"),
    }
}

// ---------------------------------------------------------------------------
// 14. Ticker Price
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore]
async fn test_ticker_price_btcusdt() {
    let client = public_client();
    let resp = client
        .get_ticker_price(Some("BTCUSDT"))
        .await
        .expect("get_ticker_price failed");
    match resp.data {
        TickerPriceShape::Single(t) => {
            assert_eq!(t.symbol, "BTCUSDT");
            let price: f64 = t.price.parse().expect("ticker price not f64");
            assert!(price > 0.0, "ticker price must be positive");
        }
        TickerPriceShape::Multi(_) => panic!("expected Single for BTCUSDT, got Multi"),
    }
}

// ---------------------------------------------------------------------------
// 15. Book Ticker
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore]
async fn test_book_ticker_btcusdt_valid_spread() {
    let client = public_client();
    let resp = client
        .get_book_ticker(Some("BTCUSDT"))
        .await
        .expect("get_book_ticker failed");
    match resp.data {
        BookTickerShape::Single(b) => {
            assert_eq!(b.symbol, "BTCUSDT");
            assert!(!b.bid_price.is_empty(), "bidPrice missing from book ticker");
            assert!(!b.ask_price.is_empty(), "askPrice missing from book ticker");
            let bid_f: f64 = b.bid_price.parse().expect("bidPrice not f64");
            let ask_f: f64 = b.ask_price.parse().expect("askPrice not f64");
            assert!(bid_f > 0.0 && ask_f > 0.0);
            assert!(bid_f < ask_f, "bid {bid_f} >= ask {ask_f} — invalid book ticker");
        }
        BookTickerShape::Multi(_) => panic!("expected Single for BTCUSDT, got Multi"),
    }
}

// ---------------------------------------------------------------------------
// 16. Index References
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore]
async fn test_index_references_btcusdt() {
    let client = public_client();
    // This endpoint may not be available on all testnet deployments.
    match client.get_index_references(Some("BTCUSDT")).await {
        Ok(resp) => assert!(!resp.data.is_empty() || resp.data.is_empty(), "index references returned unexpected result"),
        Err(asterdex_sdk::AsterDexError::ApiError { code, msg }) => {
            eprintln!("get_index_references returned ApiError {code}: {msg} — endpoint may not be available");
        }
        Err(e) => panic!("unexpected error from get_index_references: {e:?}"),
    }
}