use rust_decimal::Decimal;
use crate::error::{Error, Result};
use crate::types::{
Cursor, Interval, MarginMode, Market, Network, OrderStatus, OrderType, Side, Size, TimeInForce,
Timestamp, TransferDestination,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DepositAddressRequest {
pub asset: String,
pub network: Network,
pub amount: Option<Decimal>,
}
impl DepositAddressRequest {
pub fn new(asset: impl Into<String>, network: Network) -> Self {
Self {
asset: asset.into().to_ascii_uppercase(),
network,
amount: None,
}
}
#[must_use]
pub fn amount(mut self, amount: Decimal) -> Self {
self.amount = Some(amount);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WithdrawRequest {
pub asset: String,
pub network: Network,
pub amount: Decimal,
pub destination: TransferDestination,
pub client_id: Option<String>,
}
impl WithdrawRequest {
pub fn new(
asset: impl Into<String>,
network: Network,
amount: Decimal,
destination: TransferDestination,
) -> Self {
Self {
asset: asset.into().to_ascii_uppercase(),
network,
amount,
destination,
client_id: None,
}
}
#[must_use]
pub fn client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = Some(client_id.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TransferLookupRequest {
pub asset: String,
pub id: Option<String>,
pub tx_id: Option<String>,
}
impl TransferLookupRequest {
pub fn by_id(asset: impl Into<String>, id: impl Into<String>) -> Self {
Self {
asset: asset.into().to_ascii_uppercase(),
id: Some(id.into()),
tx_id: None,
}
}
pub fn by_tx_id(asset: impl Into<String>, tx_id: impl Into<String>) -> Self {
Self {
asset: asset.into().to_ascii_uppercase(),
id: None,
tx_id: Some(tx_id.into()),
}
}
pub fn validate(&self) -> Result<()> {
self.reference().map(|_| ())
}
pub(crate) fn reference(&self) -> Result<(&'static str, &str)> {
if self.asset.trim().is_empty() {
return Err(Error::invalid_request("asset", "asset must not be empty"));
}
match (&self.id, &self.tx_id) {
(Some(id), None) if !id.trim().is_empty() => Ok(("uuid", id)),
(None, Some(tx_id)) if !tx_id.trim().is_empty() => Ok(("txid", tx_id)),
(None, None) => Err(Error::invalid_request(
"reference",
"set either an exchange transfer ID or a transaction ID",
)),
(Some(_), Some(_)) => Err(Error::invalid_request(
"reference",
"set exactly one of the exchange transfer ID or transaction ID",
)),
(Some(_), None) => Err(Error::invalid_request(
"id",
"exchange transfer ID must not be empty",
)),
(None, Some(_)) => Err(Error::invalid_request(
"tx_id",
"transaction ID must not be empty",
)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TransferHistoryRequest {
pub asset: Option<String>,
pub network: Option<Network>,
pub cursor: Option<Cursor>,
pub limit: Option<u32>,
}
impl TransferHistoryRequest {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn asset(mut self, asset: impl Into<String>) -> Self {
self.asset = Some(asset.into().to_ascii_uppercase());
self
}
#[must_use]
pub fn network(mut self, network: Network) -> Self {
self.network = Some(network);
self
}
#[must_use]
pub fn cursor(mut self, cursor: Cursor) -> Self {
self.cursor = Some(cursor);
self
}
#[must_use]
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CandleRequest {
pub market: Market,
pub interval: Interval,
pub from: Option<Timestamp>,
pub to: Option<Timestamp>,
pub limit: Option<u32>,
}
impl CandleRequest {
pub fn new(market: Market, interval: Interval) -> Self {
Self {
market,
interval,
from: None,
to: None,
limit: None,
}
}
#[must_use]
pub fn from(mut self, from: Timestamp) -> Self {
self.from = Some(from);
self
}
#[must_use]
pub fn to(mut self, to: Timestamp) -> Self {
self.to = Some(to);
self
}
#[must_use]
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrderRequest {
pub market: Market,
pub side: Side,
pub order_type: OrderType,
pub size: Size,
pub price: Option<Decimal>,
pub time_in_force: Option<TimeInForce>,
pub reduce_only: bool,
pub client_id: Option<String>,
}
impl OrderRequest {
pub fn market(market: Market, side: Side, size: Size) -> Self {
Self {
market,
side,
order_type: OrderType::Market,
size,
price: None,
time_in_force: None,
reduce_only: false,
client_id: None,
}
}
pub fn limit(market: Market, side: Side, size: Size, price: Decimal) -> Self {
Self {
market,
side,
order_type: OrderType::Limit,
size,
price: Some(price),
time_in_force: None,
reduce_only: false,
client_id: None,
}
}
pub fn best(market: Market, side: Side, size: Size, time_in_force: TimeInForce) -> Self {
Self {
market,
side,
order_type: OrderType::Best,
size,
price: None,
time_in_force: Some(time_in_force),
reduce_only: false,
client_id: None,
}
}
#[must_use]
pub fn time_in_force(mut self, time_in_force: TimeInForce) -> Self {
self.time_in_force = Some(time_in_force);
self
}
#[must_use]
pub fn reduce_only(mut self) -> Self {
self.reduce_only = true;
self
}
#[must_use]
pub fn client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = Some(client_id.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OrderIdKind {
Exchange,
Client,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrderLookupRequest {
pub kind: OrderIdKind,
pub ids: Vec<String>,
pub market: Option<Market>,
}
impl OrderLookupRequest {
pub fn exchange(ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
kind: OrderIdKind::Exchange,
ids: ids.into_iter().map(Into::into).collect(),
market: None,
}
}
pub fn client(ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
kind: OrderIdKind::Client,
ids: ids.into_iter().map(Into::into).collect(),
market: None,
}
}
#[must_use]
pub fn market(mut self, market: Market) -> Self {
self.market = Some(market);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CancelOrdersRequest {
pub kind: OrderIdKind,
pub ids: Vec<String>,
}
impl CancelOrdersRequest {
pub fn exchange(ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
kind: OrderIdKind::Exchange,
ids: ids.into_iter().map(Into::into).collect(),
}
}
pub fn client(ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
kind: OrderIdKind::Client,
ids: ids.into_iter().map(Into::into).collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct OrderHistoryRequest {
pub market: Option<Market>,
pub statuses: Vec<OrderStatus>,
pub from: Option<Timestamp>,
pub to: Option<Timestamp>,
pub cursor: Option<Cursor>,
pub limit: Option<u32>,
}
impl OrderHistoryRequest {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn market(mut self, market: Market) -> Self {
self.market = Some(market);
self
}
#[must_use]
pub fn status(mut self, status: OrderStatus) -> Self {
self.statuses = vec![status];
self
}
#[must_use]
pub fn statuses(mut self, statuses: impl IntoIterator<Item = OrderStatus>) -> Self {
self.statuses = statuses.into_iter().collect();
self
}
#[must_use]
pub fn from(mut self, from: Timestamp) -> Self {
self.from = Some(from);
self
}
#[must_use]
pub fn to(mut self, to: Timestamp) -> Self {
self.to = Some(to);
self
}
#[must_use]
pub fn cursor(mut self, cursor: Cursor) -> Self {
self.cursor = Some(cursor);
self
}
#[must_use]
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HistoryRequest {
pub market: Market,
pub from: Option<Timestamp>,
pub to: Option<Timestamp>,
pub cursor: Option<Cursor>,
pub limit: Option<u32>,
}
impl HistoryRequest {
pub fn new(market: Market) -> Self {
Self {
market,
from: None,
to: None,
cursor: None,
limit: None,
}
}
#[must_use]
pub fn from(mut self, from: Timestamp) -> Self {
self.from = Some(from);
self
}
#[must_use]
pub fn to(mut self, to: Timestamp) -> Self {
self.to = Some(to);
self
}
#[must_use]
pub fn cursor(mut self, cursor: Cursor) -> Self {
self.cursor = Some(cursor);
self
}
#[must_use]
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MarginRequest {
pub market: Market,
pub leverage: Option<Decimal>,
pub margin_mode: Option<MarginMode>,
}
impl MarginRequest {
pub fn new(market: Market) -> Self {
Self {
market,
leverage: None,
margin_mode: None,
}
}
#[must_use]
pub fn leverage(mut self, leverage: Decimal) -> Self {
self.leverage = Some(leverage);
self
}
#[must_use]
pub fn margin_mode(mut self, margin_mode: MarginMode) -> Self {
self.margin_mode = Some(margin_mode);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Exchange;
fn btc_krw() -> Market {
Market::spot(Exchange::Upbit, "BTC", "KRW")
}
#[test]
fn constructors_keep_price_and_client_id_in_their_explicit_fields() {
let market_order =
OrderRequest::market(btc_krw(), Side::Buy, Size::Quote(Decimal::from(10_000)));
let limit_order = OrderRequest::limit(
btc_krw(),
Side::Sell,
Size::Base(Decimal::new(1, 2)),
Decimal::from(100_000_000),
);
let best_order = OrderRequest::best(
btc_krw(),
Side::Buy,
Size::Quote(Decimal::from(10_000)),
TimeInForce::ImmediateOrCancel,
)
.client_id("client-1");
assert_eq!(market_order.order_type, OrderType::Market);
assert_eq!(market_order.price, None);
assert_eq!(limit_order.order_type, OrderType::Limit);
assert_eq!(limit_order.price, Some(Decimal::from(100_000_000)));
assert_eq!(best_order.order_type, OrderType::Best);
assert_eq!(best_order.price, None);
assert_eq!(best_order.client_id.as_deref(), Some("client-1"));
}
#[test]
fn orders_are_not_reduce_only_unless_asked() {
let plain = OrderRequest::market(btc_krw(), Side::Buy, Size::Base(Decimal::ONE));
let reducing = plain.clone().reduce_only();
assert!(!plain.reduce_only);
assert!(reducing.reduce_only);
}
#[test]
fn builders_leave_unset_fields_alone() {
let request = CandleRequest::new(btc_krw(), Interval::Min1).limit(200);
assert_eq!(request.limit, Some(200));
assert_eq!(request.from, None);
assert_eq!(request.to, None);
}
#[test]
fn a_history_page_resumes_from_the_previous_cursor() {
let first = HistoryRequest::new(btc_krw());
assert!(first.cursor.is_none());
let second = HistoryRequest::new(btc_krw()).cursor(Cursor("page-2".to_string()));
assert_eq!(second.cursor.unwrap().as_str(), "page-2");
}
#[test]
fn a_margin_request_can_set_either_field_independently() {
let leverage_only = MarginRequest::new(btc_krw()).leverage(Decimal::from(10));
let mode_only = MarginRequest::new(btc_krw()).margin_mode(MarginMode::Isolated);
assert_eq!(leverage_only.leverage, Some(Decimal::from(10)));
assert_eq!(leverage_only.margin_mode, None);
assert_eq!(mode_only.leverage, None);
assert_eq!(mode_only.margin_mode, Some(MarginMode::Isolated));
}
#[test]
fn transfer_lookup_requires_one_nonempty_reference() {
assert_eq!(
TransferLookupRequest::by_id("btc", "deposit-1")
.reference()
.expect("exchange ID"),
("uuid", "deposit-1")
);
assert_eq!(
TransferLookupRequest::by_tx_id("btc", "tx-1")
.reference()
.expect("transaction ID"),
("txid", "tx-1")
);
assert!(
TransferLookupRequest {
asset: "BTC".to_string(),
id: Some("deposit-1".to_string()),
tx_id: Some("tx-1".to_string()),
}
.reference()
.is_err()
);
assert!(
TransferLookupRequest {
asset: "BTC".to_string(),
id: None,
tx_id: None,
}
.validate()
.is_err()
);
}
}