1use anyhow::{Result, bail};
44use log::{trace, warn};
45use reqwest::{Method, Response};
46use rusty_common::{SmartString, auth::exchanges::coinbase::CoinbaseAuth};
47use rusty_model::enums::{OrderSide, OrderStatus};
48use serde::{Deserialize, Serialize};
49use simd_json::value::owned::Value as JsonValue;
50use std::sync::Arc;
51use std::time::Duration;
52
53use crate::error::exchange_errors::extract_rate_limit_info_detailed;
54
55const COINBASE_API_URL: &str = "https://api.coinbase.com";
57const COINBASE_SANDBOX_URL: &str = "https://api-public.sandbox.exchange.coinbase.com";
58const COINBASE_ADVANCED_API_URL: &str = "https://api.coinbase.com/api/v3/brokerage";
59const COINBASE_ADVANCED_SANDBOX_URL: &str =
60 "https://api-public.sandbox.exchange.coinbase.com/api/v3/brokerage";
61
62const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
64const MARKET_DATA_TIMEOUT: Duration = Duration::from_secs(10);
65
66#[derive(Debug, Clone, Deserialize, Serialize)]
68pub struct CoinbaseAccount {
69 pub uuid: SmartString,
71 pub name: SmartString,
73 pub currency: SmartString,
75 pub available_balance: CoinbaseAccountBalance,
77 pub default: bool,
79 pub active: bool,
81 pub created_at: SmartString,
83 pub updated_at: SmartString,
85 pub deleted_at: Option<SmartString>,
87 #[serde(rename = "type")]
89 pub account_type: SmartString,
90 pub ready: bool,
92 pub hold: CoinbaseAccountBalance,
94}
95
96#[derive(Debug, Clone, Deserialize, Serialize)]
98pub struct CoinbaseAccountBalance {
99 pub value: SmartString,
101 pub currency: SmartString,
103}
104
105#[derive(Debug, Clone, Deserialize, Serialize)]
107pub struct CoinbaseProduct {
108 pub product_id: SmartString,
110 pub price: SmartString,
112 pub price_percentage_change_24h: SmartString,
114 pub volume_24h: SmartString,
116 pub volume_percentage_change_24h: SmartString,
118 pub base_increment: SmartString,
120 pub quote_increment: SmartString,
122 pub quote_min_size: SmartString,
124 pub quote_max_size: SmartString,
126 pub base_min_size: SmartString,
128 pub base_max_size: SmartString,
130 pub base_name: SmartString,
132 pub quote_name: SmartString,
134 pub watched: bool,
136 pub is_disabled: bool,
138 pub new: bool,
140 pub status: SmartString,
142 pub cancel_only: bool,
144 pub limit_only: bool,
146 pub post_only: bool,
148 pub trading_disabled: bool,
150 pub auction_mode: bool,
152 pub product_type: SmartString,
154 pub quote_currency_id: SmartString,
156 pub base_currency_id: SmartString,
158 pub mid_market_price: SmartString,
160}
161
162#[derive(Debug, Clone, Deserialize, Serialize)]
164pub struct CoinbaseOrder {
165 pub order_id: SmartString,
167 pub product_id: SmartString,
169 pub user_id: SmartString,
171 pub order_configuration: CoinbaseOrderConfiguration,
173 pub side: SmartString, pub client_order_id: SmartString,
177 pub status: SmartString,
179 pub time_in_force: SmartString,
181 pub created_time: SmartString,
183 pub completion_percentage: SmartString,
185 pub filled_size: SmartString,
187 pub average_filled_price: SmartString,
189 pub fee: SmartString,
191 pub number_of_fills: SmartString,
193 pub filled_value: SmartString,
195 pub pending_cancel: bool,
197 pub size_in_quote: bool,
199 pub total_fees: SmartString,
201 pub size_inclusive_of_fees: bool,
203 pub total_value_after_fees: SmartString,
205 pub trigger_status: SmartString,
207 pub order_type: SmartString,
209 pub reject_reason: SmartString,
211 pub settled: bool,
213 pub product_type: SmartString,
215 pub reject_message: SmartString,
217 pub cancel_message: SmartString,
219}
220
221#[derive(Debug, Clone, Deserialize, Serialize)]
223#[serde(tag = "order_type")]
224pub enum CoinbaseOrderConfiguration {
225 #[serde(rename = "market_market_ioc")]
227 MarketIoc {
228 market_market_ioc: CoinbaseMarketOrder,
230 },
231 #[serde(rename = "limit_limit_gtc")]
233 LimitGtc {
234 limit_limit_gtc: CoinbaseLimitOrder,
236 },
237 #[serde(rename = "limit_limit_gtd")]
239 LimitGtd {
240 limit_limit_gtd: CoinbaseLimitOrderGtd,
242 },
243 #[serde(rename = "stop_limit_stop_limit_gtc")]
245 StopLimitGtc {
246 stop_limit_stop_limit_gtc: CoinbaseStopLimitOrder,
248 },
249 #[serde(rename = "stop_limit_stop_limit_gtd")]
251 StopLimitGtd {
252 stop_limit_stop_limit_gtd: CoinbaseStopLimitOrderGtd,
254 },
255}
256
257#[derive(Debug, Clone, Deserialize, Serialize)]
259pub struct CoinbaseMarketOrder {
260 #[serde(skip_serializing_if = "Option::is_none")]
262 pub quote_size: Option<SmartString>,
263 #[serde(skip_serializing_if = "Option::is_none")]
265 pub base_size: Option<SmartString>,
266}
267
268#[derive(Debug, Clone, Deserialize, Serialize)]
270pub struct CoinbaseLimitOrder {
271 pub base_size: SmartString,
273 pub limit_price: SmartString,
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub post_only: Option<bool>,
278}
279
280#[derive(Debug, Clone, Deserialize, Serialize)]
282pub struct CoinbaseLimitOrderGtd {
283 pub base_size: SmartString,
285 pub limit_price: SmartString,
287 pub end_time: SmartString,
289 #[serde(skip_serializing_if = "Option::is_none")]
291 pub post_only: Option<bool>,
292}
293
294#[derive(Debug, Clone, Deserialize, Serialize)]
296pub struct CoinbaseStopLimitOrder {
297 pub base_size: SmartString,
299 pub limit_price: SmartString,
301 pub stop_price: SmartString,
303 pub stop_direction: SmartString, }
306
307#[derive(Debug, Clone, Deserialize, Serialize)]
309pub struct CoinbaseStopLimitOrderGtd {
310 pub base_size: SmartString,
312 pub limit_price: SmartString,
314 pub stop_price: SmartString,
316 pub end_time: SmartString,
318 pub stop_direction: SmartString,
320}
321
322#[derive(Debug, Clone, Serialize)]
324pub struct CoinbaseOrderRequest {
325 pub client_order_id: SmartString,
327 pub product_id: SmartString,
329 pub side: SmartString,
331 pub order_configuration: CoinbaseOrderConfiguration,
333}
334
335#[derive(Debug, Clone, Deserialize)]
337pub struct CoinbaseOrderResponse {
338 pub success: bool,
340 pub failure_reason: SmartString,
342 pub order_id: SmartString,
344 pub success_response: Option<CoinbaseOrderSuccessResponse>,
346 pub error_response: Option<CoinbaseOrderErrorResponse>,
348}
349
350#[derive(Debug, Clone, Deserialize)]
352pub struct CoinbaseOrderSuccessResponse {
353 pub order_id: SmartString,
355 pub product_id: SmartString,
357 pub side: SmartString,
359 pub client_order_id: SmartString,
361}
362
363#[derive(Debug, Clone, Deserialize)]
365pub struct CoinbaseOrderErrorResponse {
366 pub error: SmartString,
368 pub message: SmartString,
370 pub error_details: SmartString,
372 pub preview_failure_reason: SmartString,
374 pub new_order_failure_reason: SmartString,
376}
377
378#[derive(Debug, Clone, Deserialize)]
380pub struct CoinbaseOrderBook {
381 pub pricebook: CoinbasePriceBook,
383}
384
385#[derive(Debug, Clone, Deserialize)]
387pub struct CoinbasePriceBook {
388 pub product_id: SmartString,
390 pub bids: Vec<CoinbasePriceLevel>,
392 pub asks: Vec<CoinbasePriceLevel>,
394 pub time: SmartString,
396}
397
398#[derive(Debug, Clone, Deserialize)]
400pub struct CoinbasePriceLevel {
401 pub price: SmartString,
403 pub size: SmartString,
405}
406
407#[derive(Debug, Clone, Deserialize)]
409pub struct CoinbaseCandle {
410 pub start: SmartString,
412 pub low: SmartString,
414 pub high: SmartString,
416 pub open: SmartString,
418 pub close: SmartString,
420 pub volume: SmartString,
422}
423
424#[derive(Debug, Clone, Deserialize)]
426pub struct CoinbaseTrade {
427 pub trade_id: SmartString,
429 pub product_id: SmartString,
431 pub price: SmartString,
433 pub size: SmartString,
435 pub time: SmartString,
437 pub side: SmartString,
439 pub bid: SmartString,
441 pub ask: SmartString,
443}
444
445#[derive(Debug, Clone, Deserialize)]
447pub struct CoinbaseFill {
448 pub entry_id: SmartString,
450 pub trade_id: SmartString,
452 pub order_id: SmartString,
454 pub trade_time: SmartString,
456 pub trade_type: SmartString,
458 pub price: SmartString,
460 pub size: SmartString,
462 pub commission: SmartString,
464 pub product_id: SmartString,
466 pub sequence_timestamp: SmartString,
468 pub liquidity_indicator: SmartString,
470 pub size_in_quote: bool,
472 pub user_id: SmartString,
474 pub side: SmartString,
476}
477
478#[derive(Debug, Clone, Deserialize)]
480pub struct CoinbasePortfolio {
481 pub name: SmartString,
483 pub uuid: SmartString,
485 #[serde(rename = "type")]
487 pub portfolio_type: SmartString,
488 pub deleted: bool,
490}
491
492#[derive(Debug, Clone, Deserialize)]
494pub struct CoinbaseFeeStructure {
495 pub maker_fee_rate: SmartString,
497 pub taker_fee_rate: SmartString,
499 pub usd_volume: SmartString,
501}
502
503#[derive(Debug, Clone, Deserialize)]
505pub struct CoinbaseTransaction {
506 pub id: SmartString,
508 #[serde(rename = "type")]
510 pub transaction_type: SmartString,
511 pub status: SmartString,
513 pub amount: CoinbaseAccountBalance,
515 pub native_amount: CoinbaseAccountBalance,
517 pub description: SmartString,
519 pub created_at: SmartString,
521 pub updated_at: SmartString,
523 pub resource: SmartString,
525 pub resource_path: SmartString,
527 pub instant_exchange: bool,
529 pub details: JsonValue,
531}
532
533pub struct CoinbaseRestClient {
535 auth: Arc<CoinbaseAuth>,
537 http_client: reqwest::Client,
539 base_url: SmartString,
541 advanced_api_url: SmartString,
543 sandbox: bool,
545}
546
547impl CoinbaseRestClient {
548 pub fn new(auth: Arc<CoinbaseAuth>, sandbox: bool) -> Result<Self, Box<dyn std::error::Error>> {
550 let base_url = if sandbox {
551 COINBASE_SANDBOX_URL.into()
552 } else {
553 COINBASE_API_URL.into()
554 };
555
556 let advanced_api_url = if sandbox {
557 COINBASE_ADVANCED_SANDBOX_URL.into()
558 } else {
559 COINBASE_ADVANCED_API_URL.into()
560 };
561
562 let http_client = rusty_common::http::create_http_client_with_timeout(DEFAULT_TIMEOUT)?;
563
564 Ok(Self {
565 auth,
566 http_client,
567 base_url,
568 advanced_api_url,
569 sandbox,
570 })
571 }
572
573 async fn make_request(
575 &self,
576 method: Method,
577 endpoint: &str,
578 body: Option<&str>,
579 use_advanced_api: bool,
580 ) -> Result<Response> {
581 let base_url = if use_advanced_api {
582 &self.advanced_api_url
583 } else {
584 &self.base_url
585 };
586
587 let url = format!("{base_url}{endpoint}");
588
589 let headers = self
590 .auth
591 .generate_headers(method.as_str(), endpoint, body)?;
592
593 let mut request = self.http_client.request(method, &url);
594
595 for (key, value) in headers {
597 request = request.header(key.as_str(), value.as_str());
598 }
599
600 if let Some(body_str) = body {
602 request = request
603 .header("Content-Type", "application/json")
604 .body(body_str.to_string());
605 }
606
607 let response = request.send().await?;
608 let status = response.status();
609
610 let rate_limit_info = extract_rate_limit_info_detailed(response.headers());
612 let summary = rate_limit_info.summary();
613 if summary != "no_rate_limit_info" {
614 trace!("[Coinbase] Rate limits: {summary}");
615
616 if rate_limit_info.is_approaching_limit() {
618 warn!("[Coinbase] Rate limit approaching: {summary}");
619 }
620 }
621
622 if !status.is_success() {
623 if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
625 let error_text = response.text().await.unwrap_or_default();
626 let retry_info = if let Some(retry_ms) = rate_limit_info.get_retry_after_ms() {
627 format!(" (retry after {retry_ms}ms)")
628 } else {
629 String::new()
630 };
631
632 warn!("[Coinbase] Rate limit exceeded{retry_info}. Details: {summary}");
633 bail!("Rate limit exceeded: {}{}", error_text, retry_info);
634 }
635
636 let error_text = response.text().await.unwrap_or_default();
637 bail!("HTTP error {}: {}", status, error_text);
638 }
639
640 Ok(response)
641 }
642
643 async fn parse_response<T: for<'de> serde::Deserialize<'de>>(response: Response) -> Result<T> {
645 let text = response.text().await?;
646 let mut json_bytes = text.into_bytes();
647
648 simd_json::serde::from_slice(&mut json_bytes)
649 .map_err(|e| anyhow::anyhow!("JSON parse error: {}", e))
650 }
651
652 pub async fn get_accounts(&self) -> Result<Vec<CoinbaseAccount>> {
658 let response = self
659 .make_request(
660 Method::GET,
661 "/accounts",
662 None,
663 true, )
665 .await?;
666
667 #[derive(Deserialize)]
668 struct AccountsResponse {
669 accounts: Vec<CoinbaseAccount>,
670 }
671
672 let accounts_response: AccountsResponse = Self::parse_response(response).await?;
673 Ok(accounts_response.accounts)
674 }
675
676 pub async fn get_account(&self, account_uuid: &str) -> Result<CoinbaseAccount> {
678 let endpoint = format!("/accounts/{account_uuid}");
679 let response = self
680 .make_request(Method::GET, &endpoint, None, true)
681 .await?;
682
683 #[derive(Deserialize)]
684 struct AccountResponse {
685 account: CoinbaseAccount,
686 }
687
688 let account_response: AccountResponse = Self::parse_response(response).await?;
689 Ok(account_response.account)
690 }
691
692 pub async fn place_order(
698 &self,
699 order_request: &CoinbaseOrderRequest,
700 ) -> Result<CoinbaseOrderResponse> {
701 let body = simd_json::serde::to_string(order_request)?;
702
703 let response = self
704 .make_request(Method::POST, "/orders", Some(&body), true)
705 .await?;
706
707 Self::parse_response(response).await
708 }
709
710 pub async fn cancel_order(&self, order_id: &str) -> Result<CoinbaseOrderResponse> {
712 let endpoint = "/orders/batch_cancel".to_string();
713
714 #[derive(Serialize)]
715 struct CancelRequest<'a> {
716 order_ids: [&'a str; 1],
717 }
718
719 let request_body = CancelRequest {
720 order_ids: [order_id],
721 };
722 let body = simd_json::serde::to_string(&request_body)?;
723
724 let response = self
725 .make_request(Method::POST, &endpoint, Some(&body), true)
726 .await?;
727
728 #[derive(Deserialize)]
729 struct CancelResponse {
730 results: Vec<CoinbaseOrderResponse>,
731 }
732
733 let cancel_response: CancelResponse = Self::parse_response(response).await?;
734
735 cancel_response
736 .results
737 .into_iter()
738 .next()
739 .ok_or_else(|| anyhow::anyhow!("No cancel result returned"))
740 }
741
742 pub async fn get_order(&self, order_id: &str) -> Result<CoinbaseOrder> {
744 let endpoint = format!("/orders/historical/{order_id}");
745 let response = self
746 .make_request(Method::GET, &endpoint, None, true)
747 .await?;
748
749 #[derive(Deserialize)]
750 struct OrderResponse {
751 order: CoinbaseOrder,
752 }
753
754 let order_response: OrderResponse = Self::parse_response(response).await?;
755 Ok(order_response.order)
756 }
757
758 pub async fn list_orders(
760 &self,
761 product_id: Option<&str>,
762 order_status: Option<&str>,
763 limit: Option<u32>,
764 start_date: Option<&str>,
765 end_date: Option<&str>,
766 ) -> Result<Vec<CoinbaseOrder>> {
767 let mut endpoint = "/orders/historical/batch".to_string();
768 let mut params = Vec::new();
769
770 if let Some(pid) = product_id {
771 params.push(format!("product_id={pid}"));
772 }
773 if let Some(status) = order_status {
774 params.push(format!("order_status={status}"));
775 }
776 if let Some(lim) = limit {
777 params.push(format!("limit={lim}"));
778 }
779 if let Some(start) = start_date {
780 params.push(format!("start_date={start}"));
781 }
782 if let Some(end) = end_date {
783 params.push(format!("end_date={end}"));
784 }
785
786 if !params.is_empty() {
787 endpoint.push('?');
788 endpoint.push_str(¶ms.join("&"));
789 }
790
791 let response = self
792 .make_request(Method::GET, &endpoint, None, true)
793 .await?;
794
795 #[derive(Deserialize)]
796 struct OrdersResponse {
797 orders: Vec<CoinbaseOrder>,
798 }
799
800 let orders_response: OrdersResponse = Self::parse_response(response).await?;
801 Ok(orders_response.orders)
802 }
803
804 pub async fn get_fills(
806 &self,
807 order_id: Option<&str>,
808 product_id: Option<&str>,
809 ) -> Result<Vec<CoinbaseFill>> {
810 let mut endpoint = "/orders/historical/fills".to_string();
811 let mut params = Vec::new();
812
813 if let Some(oid) = order_id {
814 params.push(format!("order_id={oid}"));
815 }
816 if let Some(pid) = product_id {
817 params.push(format!("product_id={pid}"));
818 }
819
820 if !params.is_empty() {
821 endpoint.push('?');
822 endpoint.push_str(¶ms.join("&"));
823 }
824
825 let response = self
826 .make_request(Method::GET, &endpoint, None, true)
827 .await?;
828
829 #[derive(Deserialize)]
830 struct FillsResponse {
831 fills: Vec<CoinbaseFill>,
832 }
833
834 let fills_response: FillsResponse = Self::parse_response(response).await?;
835 Ok(fills_response.fills)
836 }
837
838 pub async fn get_products(&self) -> Result<Vec<CoinbaseProduct>> {
844 let response = self
845 .make_request(Method::GET, "/products", None, true)
846 .await?;
847
848 #[derive(Deserialize)]
849 struct ProductsResponse {
850 products: Vec<CoinbaseProduct>,
851 }
852
853 let products_response: ProductsResponse = Self::parse_response(response).await?;
854 Ok(products_response.products)
855 }
856
857 pub async fn get_product(&self, product_id: &str) -> Result<CoinbaseProduct> {
859 let endpoint = format!("/products/{product_id}");
860 let response = self
861 .make_request(Method::GET, &endpoint, None, true)
862 .await?;
863
864 Self::parse_response(response).await
865 }
866
867 pub async fn get_order_book(
869 &self,
870 product_id: &str,
871 limit: Option<u32>,
872 ) -> Result<CoinbaseOrderBook> {
873 let mut endpoint = format!("/products/{product_id}/book");
874
875 if let Some(lim) = limit {
876 endpoint.push_str(&format!("?limit={lim}"));
877 }
878
879 let response = self
880 .make_request(Method::GET, &endpoint, None, true)
881 .await?;
882
883 Self::parse_response(response).await
884 }
885
886 pub async fn get_candles(
888 &self,
889 product_id: &str,
890 start: &str,
891 end: &str,
892 granularity: &str,
893 ) -> Result<Vec<CoinbaseCandle>> {
894 let endpoint = format!(
895 "/products/{product_id}/candles?start={start}&end={end}&granularity={granularity}"
896 );
897
898 let response = self
899 .make_request(Method::GET, &endpoint, None, true)
900 .await?;
901
902 #[derive(Deserialize)]
903 struct CandlesResponse {
904 candles: Vec<CoinbaseCandle>,
905 }
906
907 let candles_response: CandlesResponse = Self::parse_response(response).await?;
908 Ok(candles_response.candles)
909 }
910
911 pub async fn get_trades(
913 &self,
914 product_id: &str,
915 limit: Option<u32>,
916 ) -> Result<Vec<CoinbaseTrade>> {
917 let mut endpoint = format!("/products/{product_id}/trades");
918
919 if let Some(lim) = limit {
920 endpoint.push_str(&format!("?limit={lim}"));
921 }
922
923 let response = self
924 .make_request(Method::GET, &endpoint, None, true)
925 .await?;
926
927 #[derive(Deserialize)]
928 struct TradesResponse {
929 trades: Vec<CoinbaseTrade>,
930 }
931
932 let trades_response: TradesResponse = Self::parse_response(response).await?;
933 Ok(trades_response.trades)
934 }
935
936 pub async fn get_portfolios(&self) -> Result<Vec<CoinbasePortfolio>> {
942 let response = self
943 .make_request(Method::GET, "/portfolios", None, true)
944 .await?;
945
946 #[derive(Deserialize)]
947 struct PortfoliosResponse {
948 portfolios: Vec<CoinbasePortfolio>,
949 }
950
951 let portfolios_response: PortfoliosResponse = Self::parse_response(response).await?;
952 Ok(portfolios_response.portfolios)
953 }
954
955 pub async fn create_portfolio(&self, name: &str) -> Result<CoinbasePortfolio> {
957 #[derive(Serialize)]
958 struct CreatePortfolioRequest<'a> {
959 name: &'a str,
960 }
961
962 let request_body = CreatePortfolioRequest { name };
963 let body = simd_json::serde::to_string(&request_body)?;
964
965 let response = self
966 .make_request(Method::POST, "/portfolios", Some(&body), true)
967 .await?;
968
969 #[derive(Deserialize)]
970 struct PortfolioResponse {
971 portfolio: CoinbasePortfolio,
972 }
973
974 let portfolio_response: PortfolioResponse = Self::parse_response(response).await?;
975 Ok(portfolio_response.portfolio)
976 }
977
978 pub async fn get_transaction_summary(
984 &self,
985 product_id: Option<&str>,
986 product_type: Option<&str>,
987 ) -> Result<CoinbaseFeeStructure> {
988 let mut endpoint = "/transaction_summary".to_string();
989 let mut params = Vec::new();
990
991 if let Some(pid) = product_id {
992 params.push(format!("product_id={pid}"));
993 }
994 if let Some(ptype) = product_type {
995 params.push(format!("product_type={ptype}"));
996 }
997
998 if !params.is_empty() {
999 endpoint.push('?');
1000 endpoint.push_str(¶ms.join("&"));
1001 }
1002
1003 let response = self
1004 .make_request(Method::GET, &endpoint, None, true)
1005 .await?;
1006
1007 #[derive(Deserialize)]
1008 struct FeeSummaryResponse {
1009 fee_tier: CoinbaseFeeStructure,
1010 }
1011
1012 let fee_response: FeeSummaryResponse = Self::parse_response(response).await?;
1013 Ok(fee_response.fee_tier)
1014 }
1015
1016 #[must_use]
1022 pub fn order_side_to_string(side: OrderSide) -> SmartString {
1023 match side {
1024 OrderSide::Buy => "BUY".into(),
1025 OrderSide::Sell => "SELL".into(),
1026 }
1027 }
1028
1029 pub fn string_to_order_side(side: &str) -> Result<OrderSide> {
1031 match side.to_uppercase().as_str() {
1032 "BUY" => Ok(OrderSide::Buy),
1033 "SELL" => Ok(OrderSide::Sell),
1034 _ => bail!("Invalid order side: {}", side),
1035 }
1036 }
1037
1038 #[must_use]
1040 pub fn string_to_order_status(status: &str) -> OrderStatus {
1041 match status.to_uppercase().as_str() {
1042 "PENDING" | "OPEN" | "QUEUED" => OrderStatus::New,
1043 "PARTIALLY_FILLED" => OrderStatus::PartiallyFilled,
1044 "FILLED" => OrderStatus::Filled,
1045 "CANCELLED" | "CANCELED" => OrderStatus::Cancelled,
1046 "REJECTED" | "FAILED" | "EXPIRED" => OrderStatus::Rejected,
1047 _ => {
1048 log::warn!("Unknown Coinbase order status: {status}");
1049 OrderStatus::Unknown
1050 }
1051 }
1052 }
1053
1054 #[must_use]
1056 pub fn create_market_order_request(
1057 client_order_id: SmartString,
1058 product_id: SmartString,
1059 side: OrderSide,
1060 quote_size: Option<SmartString>,
1061 base_size: Option<SmartString>,
1062 ) -> CoinbaseOrderRequest {
1063 CoinbaseOrderRequest {
1064 client_order_id,
1065 product_id,
1066 side: Self::order_side_to_string(side),
1067 order_configuration: CoinbaseOrderConfiguration::MarketIoc {
1068 market_market_ioc: CoinbaseMarketOrder {
1069 quote_size,
1070 base_size,
1071 },
1072 },
1073 }
1074 }
1075
1076 #[must_use]
1078 pub fn create_limit_order_request(
1079 client_order_id: SmartString,
1080 product_id: SmartString,
1081 side: OrderSide,
1082 base_size: SmartString,
1083 limit_price: SmartString,
1084 post_only: Option<bool>,
1085 ) -> CoinbaseOrderRequest {
1086 CoinbaseOrderRequest {
1087 client_order_id,
1088 product_id,
1089 side: Self::order_side_to_string(side),
1090 order_configuration: CoinbaseOrderConfiguration::LimitGtc {
1091 limit_limit_gtc: CoinbaseLimitOrder {
1092 base_size,
1093 limit_price,
1094 post_only,
1095 },
1096 },
1097 }
1098 }
1099
1100 #[must_use]
1102 pub fn create_stop_limit_order_request(
1103 client_order_id: SmartString,
1104 product_id: SmartString,
1105 side: OrderSide,
1106 base_size: SmartString,
1107 limit_price: SmartString,
1108 stop_price: SmartString,
1109 stop_direction: SmartString,
1110 ) -> CoinbaseOrderRequest {
1111 CoinbaseOrderRequest {
1112 client_order_id,
1113 product_id,
1114 side: Self::order_side_to_string(side),
1115 order_configuration: CoinbaseOrderConfiguration::StopLimitGtc {
1116 stop_limit_stop_limit_gtc: CoinbaseStopLimitOrder {
1117 base_size,
1118 limit_price,
1119 stop_price,
1120 stop_direction,
1121 },
1122 },
1123 }
1124 }
1125
1126 #[must_use]
1128 pub const fn is_sandbox(&self) -> bool {
1129 self.sandbox
1130 }
1131
1132 #[must_use]
1134 pub fn get_base_url(&self) -> &str {
1135 &self.base_url
1136 }
1137
1138 #[must_use]
1140 pub fn get_advanced_api_url(&self) -> &str {
1141 &self.advanced_api_url
1142 }
1143}
1144
1145#[cfg(test)]
1146mod tests {
1147 use super::*;
1148 #[test]
1149 fn test_order_side_conversion() {
1150 assert_eq!(
1151 CoinbaseRestClient::order_side_to_string(OrderSide::Buy),
1152 "BUY"
1153 );
1154 assert_eq!(
1155 CoinbaseRestClient::order_side_to_string(OrderSide::Sell),
1156 "SELL"
1157 );
1158
1159 assert_eq!(
1160 CoinbaseRestClient::string_to_order_side("BUY").unwrap(),
1161 OrderSide::Buy
1162 );
1163 assert_eq!(
1164 CoinbaseRestClient::string_to_order_side("SELL").unwrap(),
1165 OrderSide::Sell
1166 );
1167 assert_eq!(
1168 CoinbaseRestClient::string_to_order_side("buy").unwrap(),
1169 OrderSide::Buy
1170 );
1171 assert_eq!(
1172 CoinbaseRestClient::string_to_order_side("sell").unwrap(),
1173 OrderSide::Sell
1174 );
1175 }
1176
1177 #[test]
1178 fn test_order_status_conversion() {
1179 assert_eq!(
1180 CoinbaseRestClient::string_to_order_status("PENDING"),
1181 OrderStatus::New
1182 );
1183 assert_eq!(
1184 CoinbaseRestClient::string_to_order_status("OPEN"),
1185 OrderStatus::New
1186 );
1187 assert_eq!(
1188 CoinbaseRestClient::string_to_order_status("PARTIALLY_FILLED"),
1189 OrderStatus::PartiallyFilled
1190 );
1191 assert_eq!(
1192 CoinbaseRestClient::string_to_order_status("FILLED"),
1193 OrderStatus::Filled
1194 );
1195 assert_eq!(
1196 CoinbaseRestClient::string_to_order_status("CANCELLED"),
1197 OrderStatus::Cancelled
1198 );
1199 assert_eq!(
1200 CoinbaseRestClient::string_to_order_status("REJECTED"),
1201 OrderStatus::Rejected
1202 );
1203 }
1204
1205 #[test]
1206 fn test_market_order_creation() {
1207 let order = CoinbaseRestClient::create_market_order_request(
1208 "test_123".into(),
1209 "BTC-USD".into(),
1210 OrderSide::Buy,
1211 Some("1000.00".into()),
1212 None,
1213 );
1214
1215 assert_eq!(order.client_order_id, "test_123");
1216 assert_eq!(order.product_id, "BTC-USD");
1217 assert_eq!(order.side, "BUY");
1218
1219 match order.order_configuration {
1220 CoinbaseOrderConfiguration::MarketIoc { market_market_ioc } => {
1221 assert_eq!(market_market_ioc.quote_size, Some("1000.00".into()));
1222 assert_eq!(market_market_ioc.base_size, None);
1223 }
1224 _ => panic!("Expected market order configuration"),
1225 }
1226 }
1227
1228 #[test]
1229 fn test_limit_order_creation() {
1230 let order = CoinbaseRestClient::create_limit_order_request(
1231 "test_456".into(),
1232 "ETH-USD".into(),
1233 OrderSide::Sell,
1234 "0.5".into(),
1235 "3000.00".into(),
1236 Some(true),
1237 );
1238
1239 assert_eq!(order.client_order_id, "test_456");
1240 assert_eq!(order.product_id, "ETH-USD");
1241 assert_eq!(order.side, "SELL");
1242
1243 match order.order_configuration {
1244 CoinbaseOrderConfiguration::LimitGtc { limit_limit_gtc } => {
1245 assert_eq!(limit_limit_gtc.base_size, "0.5");
1246 assert_eq!(limit_limit_gtc.limit_price, "3000.00");
1247 assert_eq!(limit_limit_gtc.post_only, Some(true));
1248 }
1249 _ => panic!("Expected limit order configuration"),
1250 }
1251 }
1252}