rust-okx 0.6.4

Async Rust client for the OKX v5 REST API
Documentation
use http::Method;
use serde_json::json;

use crate::model::OrderSide;
use crate::test_util::MockTransport;
use crate::{Credentials, OkxClient};

use super::*;

fn signed_client(mock: MockTransport) -> OkxClient<MockTransport> {
    OkxClient::with_transport(mock)
        .credentials(Credentials::new("key", "secret", "pass"))
        .build()
}

#[test]
fn currency_pair_serializes_official_fields() {
    let request =
        ConvertCurrencyPairRequest::new("USDT", "BTC").convert_mode(ConvertMode::LargeOrderVip);

    assert_eq!(
        serde_json::to_value(request).unwrap(),
        json!({
            "fromCcy": "USDT",
            "toCcy": "BTC",
            "convertMode": "1"
        })
    );
}

#[test]
fn estimate_quote_serializes_official_example() {
    let request = ConvertQuoteRequest::new("ETH", "USDT", OrderSide::Buy, "30", "USDT")
        .client_quote_request_id("quoteRequest1")
        .tag("broker-tag")
        .convert_mode(ConvertMode::Standard);

    assert_eq!(
        serde_json::to_value(request).unwrap(),
        json!({
            "baseCcy": "ETH",
            "quoteCcy": "USDT",
            "side": "buy",
            "rfqSz": "30",
            "rfqSzCcy": "USDT",
            "clQReqId": "quoteRequest1",
            "tag": "broker-tag",
            "convertMode": "0"
        })
    );
}

#[test]
fn convert_trade_serializes_official_example() {
    let request = ConvertTradeRequest::new(
        "quoterETH-USDT16461885104612381",
        "ETH",
        "USDT",
        OrderSide::Buy,
        "30",
        "USDT",
    )
    .client_trade_request_id("tradeRequest1");

    assert_eq!(
        serde_json::to_value(request).unwrap(),
        json!({
            "quoteId": "quoterETH-USDT16461885104612381",
            "baseCcy": "ETH",
            "quoteCcy": "USDT",
            "side": "buy",
            "sz": "30",
            "szCcy": "USDT",
            "clTReqId": "tradeRequest1"
        })
    );
}

#[test]
fn history_query_uses_string_wire_values_and_omits_unset_fields() {
    let request = ConvertHistoryRequest::new()
        .after(1_596_386_767_954)
        .before(1_596_386_768_000)
        .limit(100);

    assert_eq!(
        serde_urlencoded::to_string(request).unwrap(),
        "after=1596386767954&before=1596386768000&limit=100"
    );
}

#[tokio::test]
async fn get_currencies_sends_no_query_string() {
    let body = r#"{"code":"0","data":[{"min":"","max":"","ccy":"BTC"},{"min":"","max":"","ccy":"ETH"}],"msg":""}"#;
    let mock = MockTransport::new(body);
    let client = signed_client(mock.clone());

    let rows = client.convert().get_currencies().await.unwrap();
    assert_eq!(rows.len(), 2);

    let req = mock.captured();
    assert_eq!(req.method, Method::GET);
    assert!(req.uri.ends_with("/api/v5/asset/convert/currencies"));
    assert_eq!(req.query(), None);
    assert!(req.is_signed());
}

#[tokio::test]
async fn get_currency_pair_sends_signed_typed_query() {
    let body = r#"{"code":"0","msg":"","data":[{"instId":"BTC-USDT","baseCcy":"BTC","baseCcyMax":"0.5","baseCcyMin":"0.0001","quoteCcy":"USDT","quoteCcyMax":"10000","quoteCcyMin":"1"}]}"#;
    let mock = MockTransport::new(body);
    let client = signed_client(mock.clone());
    let request = ConvertCurrencyPairRequest::new("USDT", "BTC");

    let rows = client.convert().get_currency_pair(&request).await.unwrap();
    assert_eq!(rows.len(), 1);

    let req = mock.captured();
    assert_eq!(req.method, Method::GET);
    assert!(
        req.uri
            .ends_with("/api/v5/asset/convert/currency-pair?fromCcy=USDT&toCcy=BTC")
    );
    assert!(req.is_signed());
}

#[tokio::test]
async fn estimate_quote_posts_exact_typed_body() {
    let body = r#"{"code":"0","data":[{"baseCcy":"ETH","baseSz":"0.01023052","clQReqId":"","cnvtPx":"2932.40104429","origRfqSz":"30","quoteCcy":"USDT","quoteId":"quoterETH-USDT16461885104612381","quoteSz":"30","quoteTime":"1646188510461","rfqSz":"30","rfqSzCcy":"USDT","side":"buy","ttlMs":"10000"}],"msg":""}"#;
    let mock = MockTransport::new(body);
    let client = signed_client(mock.clone());
    let request = ConvertQuoteRequest::new("BTC", "USDT", OrderSide::Buy, "1", "USDT");

    let rows = client.convert().estimate_quote(&request).await.unwrap();
    assert_eq!(rows[0].quote_id, "quoterETH-USDT16461885104612381");

    let req = mock.captured();
    assert_eq!(req.method, Method::POST);
    assert!(req.uri.ends_with("/api/v5/asset/convert/estimate-quote"));
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(req.body_str()).unwrap(),
        json!({
            "baseCcy": "BTC",
            "quoteCcy": "USDT",
            "side": "buy",
            "rfqSz": "1",
            "rfqSzCcy": "USDT"
        })
    );
    assert!(req.is_signed());
}

#[tokio::test]
async fn convert_trade_posts_exact_typed_body() {
    let body = r#"{"code":"0","data":[{"baseCcy":"ETH","clTReqId":"","fillBaseSz":"0.01023052","fillPx":"2932.40104429","fillQuoteSz":"30","instId":"ETH-USDT","quoteCcy":"USDT","quoteId":"quoterETH-USDT16461885104612381","side":"buy","state":"fullyFilled","tradeId":"trader16461885203381437","ts":"1646188520338"}],"msg":""}"#;
    let mock = MockTransport::new(body);
    let client = signed_client(mock.clone());
    let request = ConvertTradeRequest::new("q1", "BTC", "USDT", OrderSide::Buy, "1", "USDT");

    let rows = client.convert().convert_trade(&request).await.unwrap();
    assert_eq!(rows[0].trade_id, "trader16461885203381437");

    let req = mock.captured();
    assert_eq!(req.method, Method::POST);
    assert!(req.uri.ends_with("/api/v5/asset/convert/trade"));
    assert_eq!(
        serde_json::from_str::<serde_json::Value>(req.body_str()).unwrap(),
        json!({
            "quoteId": "q1",
            "baseCcy": "BTC",
            "quoteCcy": "USDT",
            "side": "buy",
            "sz": "1",
            "szCcy": "USDT"
        })
    );
    assert!(req.is_signed());
}

#[tokio::test]
async fn get_convert_history_sends_typed_query() {
    let body = r#"{"code":"0","data":[{"clTReqId":"","instId":"ETH-USDT","side":"buy","fillPx":"2932.401044","baseCcy":"ETH","quoteCcy":"USDT","fillBaseSz":"0.01023052","state":"fullyFilled","tradeId":"trader16461885203381437","fillQuoteSz":"30","ts":"1646188520000"}],"msg":""}"#;
    let mock = MockTransport::new(body);
    let client = signed_client(mock.clone());
    let request = ConvertHistoryRequest::new().limit(1);

    let rows = client
        .convert()
        .get_convert_history(&request)
        .await
        .unwrap();
    assert_eq!(rows[0].trade_id, "trader16461885203381437");
    assert_eq!(rows[0].state, ConvertTradeState::FullyFilled);

    let req = mock.captured();
    assert_eq!(req.query(), Some("limit=1"));
    assert!(req.is_signed());
}

#[test]
fn quote_deserializes_official_example() {
    let row: ConvertQuote = serde_json::from_str(
        r#"{
                "baseCcy":"ETH",
                "baseSz":"0.01023052",
                "clQReqId":"",
                "cnvtPx":"2932.40104429",
                "origRfqSz":"30",
                "quoteCcy":"USDT",
                "quoteId":"quoterETH-USDT16461885104612381",
                "quoteSz":"30",
                "quoteTime":"1646188510461",
                "rfqSz":"30",
                "rfqSzCcy":"USDT",
                "side":"buy",
                "ttlMs":"10000"
            }"#,
    )
    .unwrap();

    assert_eq!(row.quote_id, "quoterETH-USDT16461885104612381");
    assert_eq!(row.side, OrderSide::Buy);
    assert_eq!(row.cnvt_px.as_str(), "2932.40104429");
    assert_eq!(row.ttl_ms.as_str(), "10000");
}

#[test]
fn trade_result_deserializes_official_example() {
    let row: ConvertTradeResult = serde_json::from_str(
        r#"{
                "baseCcy":"ETH",
                "clTReqId":"",
                "fillBaseSz":"0.01023052",
                "fillPx":"2932.40104429",
                "fillQuoteSz":"30",
                "instId":"ETH-USDT",
                "quoteCcy":"USDT",
                "quoteId":"quoterETH-USDT16461885104612381",
                "side":"buy",
                "state":"fullyFilled",
                "tradeId":"trader16461885203381437",
                "ts":"1646188520338"
            }"#,
    )
    .unwrap();

    assert_eq!(row.trade_id, "trader16461885203381437");
    assert_eq!(row.state, ConvertTradeState::FullyFilled);
    assert_eq!(row.fill_quote_sz.as_str(), "30");
}

#[test]
fn unknown_trade_state_is_preserved() {
    let state: ConvertTradeState = serde_json::from_str(r#""pendingReview""#).unwrap();
    assert_eq!(
        state,
        ConvertTradeState::Unknown("pendingReview".to_owned())
    );
    assert_eq!(state.as_str(), "pendingReview");
}