ndaxrs 0.1.0

Rust client library for the NDAX cryptocurrency exchange API
Documentation
// Copyright (C) 2026 ndaxrs Art Morozov
// SPDX-License-Identifier: GPL-3.0-only

//! Common NDAX message types and enums.
//!
//! This module contains shared types used across various NDAX API messages,
//! including generic responses and trading enums.

use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};

/// Generic response structure used by many NDAX API endpoints.
///
/// Most NDAX API calls return this structure (or an extension of it)
/// to indicate success or failure.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenericResponse {
  /// Whether the operation was successful
  pub result:    bool,
  /// Error message if the operation failed
  pub errormsg:  Option<String>,
  /// Error code (0 = success)
  pub errorcode: i32,
  /// Additional details about the response
  pub detail:    Option<String>,
}

/// Order side enum.
///
/// Indicates whether an order is a buy, sell, or short.
#[derive(
  Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr,
)]
#[repr(u8)]
pub enum Side {
  /// Buy order (bid)
  Buy     = 0,
  /// Sell order (ask)
  Sell    = 1,
  /// Short sell order
  Short   = 2,
  /// Unknown side
  Unknown = 3,
}

/// Order type enum.
///
/// Defines the execution type of an order.
#[derive(
  Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr,
)]
#[repr(u8)]
pub enum OrderType {
  /// Unknown order type
  Unknown            = 0,
  /// Market order - executes immediately at best available price
  Market             = 1,
  /// Limit order - executes at specified price or better
  Limit              = 2,
  /// Stop market order - becomes market order when stop price is reached
  StopMarket         = 3,
  /// Stop limit order - becomes limit order when stop price is reached
  StopLimit          = 4,
  /// Trailing stop market order
  TrailingStopMarket = 5,
  /// Trailing stop limit order
  TrailingStopLimit  = 6,
  /// Block trade
  BlockTrade         = 7,
}

/// Time in force enum.
///
/// Specifies how long an order remains active before it is executed or expires.
#[derive(
  Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr,
)]
#[repr(u8)]
pub enum TimeInForce {
  /// Unknown time in force
  Unknown = 0,
  /// Good Till Canceled - remains active until filled or canceled
  GTC     = 1,
  /// Opening auction only
  OPG     = 2,
  /// Immediate Or Cancel - fills immediately or cancels unfilled portion
  IOC     = 3,
  /// Fill Or Kill - must be completely filled or completely canceled
  FOK     = 4,
  /// Good Till Crossing - for auction orders
  GTX     = 5,
  /// Good Till Date - remains active until specified date
  GTD     = 6,
}

/// Action type enum for market data updates.
///
/// Indicates whether a market data entry is new, updated, or deleted.
#[derive(
  Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr,
)]
#[repr(u8)]
pub enum ActionType {
  /// New entry
  New    = 0,
  /// Updated entry
  Update = 1,
  /// Deleted entry
  Delete = 2,
}

/// Order state enum.
///
/// Represents the current state of an order.
#[derive(
  Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr,
)]
#[repr(u8)]
pub enum OrderState {
  /// Order is active and working
  Working           = 0,
  /// Order was rejected
  Rejected          = 1,
  /// Order has been fully executed
  FullyExecuted     = 2,
  /// Order was canceled
  Canceled          = 3,
  /// Order has expired
  Expired           = 4,
  /// Order has been partially executed
  PartiallyExecuted = 5,
}

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

  #[test]
  fn test_side_serialization() {
    assert_eq!(serde_json::to_string(&Side::Buy).unwrap(), "0");
    assert_eq!(serde_json::to_string(&Side::Sell).unwrap(), "1");
    assert_eq!(serde_json::to_string(&Side::Short).unwrap(), "2");
    assert_eq!(serde_json::to_string(&Side::Unknown).unwrap(), "3");
  }

  #[test]
  fn test_side_deserialization() {
    assert_eq!(serde_json::from_str::<Side>("0").unwrap(), Side::Buy);
    assert_eq!(serde_json::from_str::<Side>("1").unwrap(), Side::Sell);
    assert_eq!(serde_json::from_str::<Side>("2").unwrap(), Side::Short);
    assert_eq!(serde_json::from_str::<Side>("3").unwrap(), Side::Unknown);
  }

  #[test]
  fn test_order_type_values() {
    assert_eq!(OrderType::Unknown as u8, 0);
    assert_eq!(OrderType::Market as u8, 1);
    assert_eq!(OrderType::Limit as u8, 2);
    assert_eq!(OrderType::StopMarket as u8, 3);
    assert_eq!(OrderType::StopLimit as u8, 4);
    assert_eq!(OrderType::TrailingStopMarket as u8, 5);
    assert_eq!(OrderType::TrailingStopLimit as u8, 6);
    assert_eq!(OrderType::BlockTrade as u8, 7);
  }

  #[test]
  fn test_order_type_serialization() {
    assert_eq!(serde_json::to_string(&OrderType::Market).unwrap(), "1");
    assert_eq!(serde_json::to_string(&OrderType::Limit).unwrap(), "2");

    assert_eq!(
      serde_json::from_str::<OrderType>("1").unwrap(),
      OrderType::Market
    );
    assert_eq!(
      serde_json::from_str::<OrderType>("2").unwrap(),
      OrderType::Limit
    );
  }

  #[test]
  fn test_time_in_force_values() {
    assert_eq!(TimeInForce::Unknown as u8, 0);
    assert_eq!(TimeInForce::GTC as u8, 1);
    assert_eq!(TimeInForce::OPG as u8, 2);
    assert_eq!(TimeInForce::IOC as u8, 3);
    assert_eq!(TimeInForce::FOK as u8, 4);
    assert_eq!(TimeInForce::GTX as u8, 5);
    assert_eq!(TimeInForce::GTD as u8, 6);
  }

  #[test]
  fn test_time_in_force_serialization() {
    assert_eq!(serde_json::to_string(&TimeInForce::GTC).unwrap(), "1");
    assert_eq!(serde_json::to_string(&TimeInForce::IOC).unwrap(), "3");
    assert_eq!(serde_json::to_string(&TimeInForce::FOK).unwrap(), "4");

    assert_eq!(
      serde_json::from_str::<TimeInForce>("1").unwrap(),
      TimeInForce::GTC
    );
  }

  #[test]
  fn test_action_type_values() {
    assert_eq!(ActionType::New as u8, 0);
    assert_eq!(ActionType::Update as u8, 1);
    assert_eq!(ActionType::Delete as u8, 2);

    assert_eq!(serde_json::to_string(&ActionType::New).unwrap(), "0");
    assert_eq!(
      serde_json::from_str::<ActionType>("2").unwrap(),
      ActionType::Delete
    );
  }

  #[test]
  fn test_order_state_values() {
    assert_eq!(OrderState::Working as u8, 0);
    assert_eq!(OrderState::Rejected as u8, 1);
    assert_eq!(OrderState::FullyExecuted as u8, 2);
    assert_eq!(OrderState::Canceled as u8, 3);
    assert_eq!(OrderState::Expired as u8, 4);
    assert_eq!(OrderState::PartiallyExecuted as u8, 5);
  }

  #[test]
  fn test_order_state_serialization() {
    assert_eq!(serde_json::to_string(&OrderState::Working).unwrap(), "0");
    assert_eq!(
      serde_json::to_string(&OrderState::FullyExecuted).unwrap(),
      "2"
    );

    assert_eq!(
      serde_json::from_str::<OrderState>("3").unwrap(),
      OrderState::Canceled
    );
  }

  #[test]
  fn test_generic_response_success() {
    let json = r#"{"result":true,"errormsg":"","errorcode":0,"detail":null}"#;
    let response: GenericResponse = serde_json::from_str(json).unwrap();

    assert!(response.result);
    assert_eq!(response.errorcode, 0);
  }

  #[test]
  fn test_generic_response_success_empty_errormsg() {
    let json = r#"{"result":true,"errormsg":null,"errorcode":0}"#;
    let response: GenericResponse = serde_json::from_str(json).unwrap();

    assert!(response.result);
    assert!(response.errormsg.is_none());
    assert!(response.detail.is_none());
  }

  #[test]
  fn test_generic_response_error() {
    let json = r#"{"result":false,"errormsg":"Invalid InstrumentId","errorcode":100,"detail":"ID not found"}"#;
    let response: GenericResponse = serde_json::from_str(json).unwrap();

    assert!(!response.result);
    assert_eq!(response.errormsg, Some("Invalid InstrumentId".to_string()));
    assert_eq!(response.errorcode, 100);
    assert_eq!(response.detail, Some("ID not found".to_string()));
  }
}