1use rust_decimal::Decimal;
19use serde::Deserialize;
20use ustr::Ustr;
21
22use crate::common::{
23 enums::{
24 CoinbaseAccountType, CoinbaseContractExpiryType, CoinbaseFcmTradingSessionClosedReason,
25 CoinbaseFcmTradingSessionState, CoinbaseFillTradeType, CoinbaseFuturesAssetType,
26 CoinbaseLiquidityIndicator, CoinbaseMarginType, CoinbaseOrderPlacementSource,
27 CoinbaseOrderSide, CoinbaseOrderStatus, CoinbaseOrderType, CoinbaseProductStatus,
28 CoinbaseProductType, CoinbaseProductVenue, CoinbaseRiskManagedBy, CoinbaseTimeInForce,
29 CoinbaseTriggerStatus,
30 },
31 parse::{
32 deserialize_decimal_from_str, deserialize_decimal_or_zero, deserialize_margin_type_or_none,
33 deserialize_product_status_or_unknown, deserialize_product_type_or_unknown,
34 deserialize_string_to_u64,
35 },
36};
37
38#[derive(Debug, Clone, Deserialize)]
40pub struct ProductsResponse {
41 pub products: Vec<Product>,
42 pub num_products: Option<i64>,
43}
44
45#[derive(Debug, Clone, Deserialize)]
47pub struct Product {
48 pub product_id: Ustr,
49 pub price: String,
50 pub price_percentage_change_24h: String,
51 pub volume_24h: String,
52 pub volume_percentage_change_24h: String,
53 pub base_increment: String,
54 pub quote_increment: String,
55 pub quote_min_size: String,
56 pub quote_max_size: String,
57 pub base_min_size: String,
58 pub base_max_size: String,
59 pub base_name: String,
60 pub quote_name: String,
61 pub watched: bool,
62 pub is_disabled: bool,
63 pub new: bool,
64 #[serde(deserialize_with = "deserialize_product_status_or_unknown")]
65 pub status: CoinbaseProductStatus,
66 pub cancel_only: bool,
67 pub limit_only: bool,
68 pub post_only: bool,
69 pub trading_disabled: bool,
70 pub auction_mode: bool,
71 #[serde(deserialize_with = "deserialize_product_type_or_unknown")]
72 pub product_type: CoinbaseProductType,
73 pub quote_currency_id: Ustr,
74 pub base_currency_id: Ustr,
75 #[serde(default)]
76 pub fcm_trading_session_details: Option<FcmTradingSessionDetails>,
77 #[serde(default)]
78 pub mid_market_price: String,
79 #[serde(default)]
80 pub alias: Ustr,
81 #[serde(default)]
82 pub alias_to: Vec<Ustr>,
83 #[serde(default)]
84 pub base_display_symbol: Ustr,
85 #[serde(default)]
86 pub quote_display_symbol: Ustr,
87 #[serde(default)]
88 pub view_only: bool,
89 pub price_increment: String,
90 #[serde(default)]
91 pub display_name: String,
92 #[serde(default)]
93 pub product_venue: Option<CoinbaseProductVenue>,
94 #[serde(default)]
95 pub approximate_quote_24h_volume: String,
96 #[serde(default)]
97 pub future_product_details: Option<FutureProductDetails>,
98}
99
100#[derive(Debug, Clone, Deserialize)]
102pub struct FcmTradingSessionDetails {
103 pub is_session_open: bool,
104 pub open_time: String,
105 pub close_time: String,
106 pub session_state: CoinbaseFcmTradingSessionState,
107 #[serde(default)]
108 pub after_hours_order_entry_disabled: bool,
109 pub closed_reason: CoinbaseFcmTradingSessionClosedReason,
110 #[serde(default)]
111 pub maintenance: Option<MaintenanceWindow>,
112}
113
114#[derive(Debug, Clone, Deserialize)]
116pub struct MaintenanceWindow {
117 pub start_time: String,
118 pub end_time: String,
119}
120
121#[derive(Debug, Clone, Deserialize)]
123pub struct FutureProductDetails {
124 pub venue: Ustr,
125 pub contract_code: Ustr,
126 pub contract_expiry: String,
127 pub contract_size: String,
128 pub contract_root_unit: Ustr,
129 pub group_description: String,
130 pub contract_expiry_timezone: String,
131 pub group_short_description: String,
132 pub risk_managed_by: CoinbaseRiskManagedBy,
133 pub contract_expiry_type: CoinbaseContractExpiryType,
134 #[serde(default)]
135 pub perpetual_details: Option<PerpetualDetails>,
136 pub contract_display_name: String,
137 #[serde(default)]
138 pub time_to_expiry_ms: String,
139 #[serde(default)]
140 pub non_crypto: bool,
141 #[serde(default)]
142 pub contract_expiry_name: String,
143 #[serde(default)]
144 pub twenty_four_by_seven: bool,
145 #[serde(default)]
146 pub open_interest: String,
147 #[serde(default)]
148 pub funding_rate: String,
149 #[serde(default)]
150 pub funding_time: Option<String>,
151 #[serde(default)]
152 pub funding_interval: Option<String>,
153 #[serde(default)]
154 pub index_price: Option<String>,
155 #[serde(default)]
156 pub display_name: String,
157 #[serde(default)]
158 pub futures_asset_type: Option<CoinbaseFuturesAssetType>,
159}
160
161#[derive(Debug, Clone, Deserialize)]
163pub struct PerpetualDetails {
164 #[serde(default)]
165 pub open_interest: String,
166 #[serde(default)]
167 pub funding_rate: String,
168 #[serde(default)]
169 pub funding_time: Option<String>,
170}
171
172#[derive(Debug, Clone, Deserialize)]
174pub struct CandlesResponse {
175 pub candles: Vec<Candle>,
176}
177
178#[derive(Debug, Clone, Deserialize)]
180pub struct Candle {
181 pub start: String,
182 pub low: String,
183 pub high: String,
184 pub open: String,
185 pub close: String,
186 pub volume: String,
187}
188
189#[derive(Debug, Clone, Deserialize)]
191pub struct TickerResponse {
192 pub trades: Vec<Trade>,
193 pub best_bid: String,
194 pub best_ask: String,
195}
196
197#[derive(Debug, Clone, Deserialize)]
199pub struct Trade {
200 pub trade_id: String,
201 pub product_id: Ustr,
202 pub price: String,
203 pub size: String,
204 pub time: String,
205 pub side: CoinbaseOrderSide,
206 #[serde(default)]
207 pub bid: String,
208 #[serde(default)]
209 pub ask: String,
210 #[serde(default)]
211 pub exchange: String,
212}
213
214#[derive(Debug, Clone, Deserialize)]
216pub struct ProductBookResponse {
217 pub pricebook: PriceBook,
218 #[serde(default)]
219 pub last: String,
220 #[serde(default)]
221 pub mid_market: String,
222 #[serde(default)]
223 pub spread_bps: String,
224 #[serde(default)]
225 pub spread_absolute: String,
226}
227
228#[derive(Debug, Clone, Deserialize)]
230pub struct PriceBook {
231 pub product_id: Ustr,
232 pub bids: Vec<BookLevel>,
233 pub asks: Vec<BookLevel>,
234 pub time: String,
235}
236
237#[derive(Debug, Clone, Deserialize)]
239pub struct BookLevel {
240 pub price: String,
241 pub size: String,
242}
243
244#[derive(Debug, Clone, Deserialize)]
246pub struct BestBidAskResponse {
247 pub pricebooks: Vec<BestBidAsk>,
248}
249
250#[derive(Debug, Clone, Deserialize)]
252pub struct BestBidAsk {
253 pub product_id: Ustr,
254 pub bids: Vec<BookLevel>,
255 pub asks: Vec<BookLevel>,
256 #[serde(default)]
257 pub time: String,
258}
259
260#[derive(Debug, Clone, Deserialize)]
262pub struct AccountsResponse {
263 pub accounts: Vec<Account>,
264 #[serde(default)]
265 pub has_next: bool,
266 #[serde(default)]
267 pub cursor: String,
268 #[serde(default)]
269 pub size: Option<i64>,
270}
271
272#[derive(Debug, Clone, Deserialize)]
274pub struct Account {
275 pub uuid: String,
276 pub name: String,
277 pub currency: Ustr,
278 pub available_balance: Balance,
279 #[serde(default)]
280 pub default: bool,
281 #[serde(default)]
282 pub active: bool,
283 #[serde(default)]
284 pub created_at: String,
285 #[serde(default)]
286 pub updated_at: String,
287 #[serde(default)]
288 pub deleted_at: Option<String>,
289 #[serde(rename = "type")]
290 pub account_type: CoinbaseAccountType,
291 #[serde(default)]
292 pub ready: bool,
293 #[serde(default)]
294 pub hold: Option<Balance>,
295 #[serde(default)]
296 pub retail_portfolio_id: String,
297}
298
299#[derive(Debug, Clone, Deserialize)]
301pub struct Balance {
302 #[serde(deserialize_with = "deserialize_decimal_from_str")]
303 pub value: Decimal,
304 pub currency: Ustr,
305}
306
307#[derive(Debug, Clone, Deserialize)]
309pub struct CreateOrderResponse {
310 pub success: bool,
311 #[serde(default)]
312 pub failure_reason: String,
313 #[serde(default)]
314 pub order_id: String,
315 #[serde(default)]
316 pub success_response: Option<OrderSuccessResponse>,
317 #[serde(default)]
318 pub error_response: Option<OrderErrorResponse>,
319}
320
321#[derive(Debug, Clone, Deserialize)]
323pub struct OrderSuccessResponse {
324 pub order_id: String,
325 pub product_id: Ustr,
326 pub side: CoinbaseOrderSide,
327 pub client_order_id: String,
328}
329
330#[derive(Debug, Clone, Deserialize)]
332pub struct OrderErrorResponse {
333 pub error: String,
334 pub message: String,
335 pub error_details: String,
336 #[serde(default)]
337 pub preview_failure_reason: String,
338 #[serde(default)]
339 pub new_order_failure_reason: String,
340}
341
342#[derive(Debug, Clone, Deserialize)]
344pub struct CancelOrdersResponse {
345 pub results: Vec<CancelResult>,
346}
347
348#[derive(Debug, Clone, Deserialize)]
350pub struct EditOrderResponse {
351 pub success: bool,
352 #[serde(default)]
353 pub errors: Vec<EditOrderError>,
354}
355
356#[derive(Debug, Clone, Deserialize)]
358pub struct EditOrderError {
359 #[serde(default)]
360 pub edit_failure_reason: String,
361 #[serde(default)]
362 pub preview_failure_reason: String,
363}
364
365#[derive(Debug, Clone, Deserialize)]
367pub struct CancelResult {
368 pub success: bool,
369 #[serde(default)]
370 pub failure_reason: String,
371 pub order_id: String,
372}
373
374#[derive(Debug, Clone, Deserialize)]
376pub struct OrderResponse {
377 pub order: Order,
378}
379
380#[derive(Debug, Clone, Deserialize)]
382pub struct OrdersListResponse {
383 pub orders: Vec<Order>,
384 #[serde(default)]
385 pub sequence: Option<String>,
386 #[serde(default)]
387 pub has_next: bool,
388 #[serde(default)]
389 pub cursor: String,
390}
391
392#[derive(Debug, Clone, Deserialize)]
403pub struct Order {
404 pub order_id: String,
405 pub product_id: Ustr,
406 #[serde(default)]
407 pub user_id: String,
408 #[serde(default)]
409 pub order_configuration: Option<serde_json::Value>,
410 pub side: CoinbaseOrderSide,
411 #[serde(default)]
412 pub client_order_id: String,
413 pub status: CoinbaseOrderStatus,
414 #[serde(default)]
415 pub time_in_force: Option<CoinbaseTimeInForce>,
416 #[serde(default)]
417 pub created_time: String,
418 #[serde(default)]
419 pub completion_percentage: String,
420 #[serde(default)]
421 pub filled_size: String,
422 #[serde(default)]
423 pub average_filled_price: String,
424 #[serde(default, deserialize_with = "deserialize_decimal_or_zero")]
429 pub fee: Decimal,
430 #[serde(default, deserialize_with = "deserialize_string_to_u64")]
431 pub number_of_fills: u64,
432 #[serde(default, deserialize_with = "deserialize_decimal_or_zero")]
433 pub filled_value: Decimal,
434 #[serde(default)]
435 pub pending_cancel: bool,
436 #[serde(default)]
437 pub size_in_quote: bool,
438 #[serde(default, deserialize_with = "deserialize_decimal_or_zero")]
439 pub total_fees: Decimal,
440 #[serde(default)]
441 pub size_inclusive_of_fees: bool,
442 #[serde(default, deserialize_with = "deserialize_decimal_or_zero")]
443 pub total_value_after_fees: Decimal,
444 pub trigger_status: CoinbaseTriggerStatus,
445 pub order_type: CoinbaseOrderType,
446 #[serde(default)]
447 pub reject_reason: String,
448 #[serde(default)]
449 pub settled: bool,
450 #[serde(deserialize_with = "deserialize_product_type_or_unknown")]
451 pub product_type: CoinbaseProductType,
452 #[serde(default)]
453 pub reject_message: String,
454 #[serde(default)]
455 pub cancel_message: String,
456 pub order_placement_source: CoinbaseOrderPlacementSource,
457 #[serde(default, deserialize_with = "deserialize_decimal_or_zero")]
458 pub outstanding_hold_amount: Decimal,
459 #[serde(default)]
460 pub is_liquidation: bool,
461 #[serde(default)]
462 pub last_fill_time: Option<String>,
463 #[serde(default)]
464 pub leverage: String,
465 #[serde(default, deserialize_with = "deserialize_margin_type_or_none")]
466 pub margin_type: Option<CoinbaseMarginType>,
467 #[serde(default)]
468 pub retail_portfolio_id: String,
469 #[serde(default)]
470 pub originating_order_id: String,
471 #[serde(default)]
472 pub attached_order_id: String,
473}
474
475#[derive(Debug, Clone, Deserialize)]
477pub struct CfmBalanceSummaryResponse {
478 pub balance_summary: CfmBalanceSummary,
479}
480
481#[derive(Debug, Clone, Deserialize)]
483pub struct CfmBalanceSummary {
484 pub futures_buying_power: CfmAmount,
485 pub total_usd_balance: CfmAmount,
486 pub cbi_usd_balance: CfmAmount,
487 pub cfm_usd_balance: CfmAmount,
488 pub total_open_orders_hold_amount: CfmAmount,
489 pub unrealized_pnl: CfmAmount,
490 pub daily_realized_pnl: CfmAmount,
491 pub initial_margin: CfmAmount,
492 pub available_margin: CfmAmount,
493 pub liquidation_threshold: CfmAmount,
494 pub liquidation_buffer_amount: CfmAmount,
495 #[serde(default)]
496 pub liquidation_buffer_percentage: String,
497 #[serde(default)]
498 pub intraday_margin_window_measure: Option<CfmMarginWindowMeasure>,
499 #[serde(default)]
500 pub overnight_margin_window_measure: Option<CfmMarginWindowMeasure>,
501}
502
503#[derive(Debug, Clone, Deserialize)]
509pub struct CfmAmount {
510 #[serde(deserialize_with = "deserialize_decimal_from_str")]
511 pub value: Decimal,
512 pub currency: Ustr,
513}
514
515#[derive(Debug, Clone, Deserialize)]
517pub struct CfmMarginWindowMeasure {
518 pub margin_window_type: crate::common::enums::CoinbaseMarginWindowType,
519 pub margin_level: crate::common::enums::CoinbaseMarginLevel,
520 pub initial_margin: CfmAmount,
521 pub maintenance_margin: CfmAmount,
522 #[serde(default)]
523 pub liquidation_buffer_percentage: String,
524 pub total_hold: CfmAmount,
525 pub futures_buying_power: CfmAmount,
526}
527
528#[derive(Debug, Clone, Deserialize)]
530pub struct CfmPositionsResponse {
531 pub positions: Vec<CfmPosition>,
532}
533
534#[derive(Debug, Clone, Deserialize)]
536pub struct CfmPositionResponse {
537 pub position: CfmPosition,
538}
539
540#[derive(Debug, Clone, Deserialize)]
542pub struct CfmPosition {
543 pub product_id: Ustr,
544 #[serde(default)]
545 pub expiration_time: String,
546 pub side: crate::common::enums::CoinbaseFcmPositionSide,
547 #[serde(deserialize_with = "deserialize_decimal_from_str")]
548 pub number_of_contracts: Decimal,
549 pub current_price: CfmAmount,
550 pub avg_entry_price: CfmAmount,
551 pub unrealized_pnl: CfmAmount,
552 pub daily_realized_pnl: CfmAmount,
553 #[serde(default)]
554 pub total_fees: Option<CfmAmount>,
555 #[serde(default)]
556 pub contract_size: String,
557 #[serde(default)]
558 pub entry_vwap: Option<CfmAmount>,
559 #[serde(default)]
560 pub liquidation_price: Option<CfmAmount>,
561 #[serde(default)]
562 pub leverage: String,
563 #[serde(default)]
564 pub im_contribution: Option<CfmAmount>,
565 #[serde(default)]
566 pub mm_contribution: Option<CfmAmount>,
567 #[serde(default)]
568 pub position_notional: Option<CfmAmount>,
569}
570
571#[derive(Debug, Clone, Deserialize)]
573pub struct FillsResponse {
574 pub fills: Vec<Fill>,
575 #[serde(default)]
576 pub cursor: String,
577}
578
579#[derive(Debug, Clone, Deserialize)]
581pub struct Fill {
582 pub entry_id: String,
583 pub trade_id: String,
584 pub order_id: String,
585 pub trade_time: String,
586 pub trade_type: CoinbaseFillTradeType,
587 pub price: String,
588 pub size: String,
589 #[serde(default, deserialize_with = "deserialize_decimal_or_zero")]
590 pub commission: Decimal,
591 pub product_id: Ustr,
592 pub sequence_timestamp: String,
593 pub liquidity_indicator: CoinbaseLiquidityIndicator,
594 pub size_in_quote: bool,
595 pub user_id: String,
596 pub side: CoinbaseOrderSide,
597 #[serde(default)]
598 pub retail_portfolio_id: String,
599}
600
601#[cfg(test)]
602mod tests {
603 use std::str::FromStr;
604
605 use rstest::rstest;
606 use rust_decimal::Decimal;
607
608 use super::*;
609 use crate::common::{consts::ORDER_CONFIG_LIMIT_GTC, testing::load_test_fixture};
610
611 #[rstest]
612 fn test_deserialize_product() {
613 let json = load_test_fixture("http_product.json");
614 let product: Product = serde_json::from_str(&json).unwrap();
615 assert_eq!(product.product_id, "BTC-USD");
616 assert_eq!(product.product_type, CoinbaseProductType::Spot);
617 assert_eq!(product.base_currency_id, "BTC");
618 assert_eq!(product.quote_currency_id, "USD");
619 assert_eq!(product.base_increment, "0.00000001");
620 assert_eq!(product.quote_increment, "0.01");
621 assert_eq!(product.price_increment, "0.01");
622 assert!(!product.is_disabled);
623 assert!(!product.trading_disabled);
624 }
625
626 #[rstest]
627 fn test_deserialize_products_list() {
628 let json = load_test_fixture("http_products.json");
629 let response: ProductsResponse = serde_json::from_str(&json).unwrap();
630 assert_eq!(response.products.len(), 2);
631 assert_eq!(response.products[0].product_id, "BTC-USD");
632 assert_eq!(response.products[1].product_id, "BTC-USDC");
633 }
634
635 #[rstest]
636 fn test_deserialize_products_future() {
637 let json = load_test_fixture("http_products_future.json");
638 let response: ProductsResponse = serde_json::from_str(&json).unwrap();
639 assert!(!response.products.is_empty());
640 assert_eq!(
641 response.products[0].product_type,
642 CoinbaseProductType::Future
643 );
644 assert!(response.products[0].fcm_trading_session_details.is_some());
645 }
646
647 #[rstest]
648 fn test_deserialize_candles() {
649 let json = load_test_fixture("http_candles.json");
650 let response: CandlesResponse = serde_json::from_str(&json).unwrap();
651 assert_eq!(response.candles.len(), 2);
652
653 let candle = &response.candles[0];
654 assert!(!candle.start.is_empty());
655 assert!(!candle.open.is_empty());
656 assert!(!candle.high.is_empty());
657 assert!(!candle.low.is_empty());
658 assert!(!candle.close.is_empty());
659 assert!(!candle.volume.is_empty());
660 }
661
662 #[rstest]
663 fn test_deserialize_ticker() {
664 let json = load_test_fixture("http_ticker.json");
665 let response: TickerResponse = serde_json::from_str(&json).unwrap();
666 assert_eq!(response.trades.len(), 3);
667 assert!(!response.best_bid.is_empty());
668 assert!(!response.best_ask.is_empty());
669
670 let trade = &response.trades[0];
671 assert_eq!(trade.product_id, "BTC-USD");
672 assert!(!trade.price.is_empty());
673 assert!(!trade.size.is_empty());
674 assert!(!trade.time.is_empty());
675 assert!(trade.side == CoinbaseOrderSide::Buy || trade.side == CoinbaseOrderSide::Sell);
676 }
677
678 #[rstest]
679 fn test_deserialize_product_book() {
680 let json = load_test_fixture("http_product_book.json");
681 let response: ProductBookResponse = serde_json::from_str(&json).unwrap();
682 assert_eq!(response.pricebook.product_id, "BTC-USD");
683 assert!(!response.pricebook.bids.is_empty());
684 assert!(!response.pricebook.asks.is_empty());
685 assert!(!response.pricebook.time.is_empty());
686 assert!(!response.spread_absolute.is_empty());
687
688 let bid = &response.pricebook.bids[0];
689 assert!(!bid.price.is_empty());
690 assert!(!bid.size.is_empty());
691 }
692
693 #[rstest]
694 fn test_product_spot_fields() {
695 let json = load_test_fixture("http_product.json");
696 let product: Product = serde_json::from_str(&json).unwrap();
697
698 assert_eq!(product.base_min_size, "0.00000001");
700 assert_eq!(product.base_max_size, "3400");
701 assert_eq!(product.quote_min_size, "1");
702 assert_eq!(product.quote_max_size, "150000000");
703 assert_eq!(product.product_venue, Some(CoinbaseProductVenue::Cbe));
704 }
705
706 #[rstest]
707 fn test_deserialize_order() {
708 let json = load_test_fixture("http_order.json");
709 let response: OrderResponse = serde_json::from_str(&json).unwrap();
710 let order = response.order;
711
712 assert_eq!(order.order_id, "0000-000000-000000");
713 assert_eq!(order.product_id, "BTC-USD");
714 assert_eq!(order.side, CoinbaseOrderSide::Buy);
715 assert_eq!(order.status, CoinbaseOrderStatus::Open);
716 assert_eq!(order.client_order_id, "11111-000000-000000");
717 assert_eq!(
718 order.time_in_force,
719 Some(CoinbaseTimeInForce::GoodUntilCancelled)
720 );
721 assert_eq!(order.order_type, CoinbaseOrderType::Limit);
722 assert_eq!(
723 order.trigger_status,
724 CoinbaseTriggerStatus::InvalidOrderType
725 );
726 assert_eq!(
727 order.order_placement_source,
728 CoinbaseOrderPlacementSource::RetailAdvanced
729 );
730 assert_eq!(order.margin_type, Some(CoinbaseMarginType::Cross));
731 assert_eq!(order.filled_size, "0.001");
732 assert_eq!(order.average_filled_price, "50");
733 assert_eq!(order.fee, Decimal::ZERO);
734 assert_eq!(order.number_of_fills, 2);
735 assert_eq!(order.filled_value, Decimal::from_str("10000").unwrap());
736 assert_eq!(order.total_fees, Decimal::from_str("5.00").unwrap());
737 assert_eq!(
738 order.total_value_after_fees,
739 Decimal::from_str("10000").unwrap()
740 );
741 assert_eq!(
742 order.outstanding_hold_amount,
743 Decimal::from_str("1.00").unwrap()
744 );
745 assert_eq!(
746 order.last_fill_time.as_deref(),
747 Some("2021-05-31T10:30:00Z")
748 );
749 let config = order
752 .order_configuration
753 .as_ref()
754 .and_then(|v| v.as_object())
755 .expect("order_configuration should be a JSON object");
756 assert!(config.contains_key(ORDER_CONFIG_LIMIT_GTC));
757 }
758
759 #[rstest]
760 fn test_deserialize_orders_list() {
761 let json = load_test_fixture("http_orders_list.json");
762 let response: OrdersListResponse = serde_json::from_str(&json).unwrap();
763
764 assert_eq!(response.orders.len(), 2);
765 assert!(!response.has_next);
766
767 let open_order = &response.orders[0];
768 assert_eq!(open_order.status, CoinbaseOrderStatus::Open);
769 assert_eq!(open_order.side, CoinbaseOrderSide::Buy);
770 assert_eq!(open_order.order_type, CoinbaseOrderType::Limit);
771 assert_eq!(
772 open_order.trigger_status,
773 CoinbaseTriggerStatus::InvalidOrderType
774 );
775 assert_eq!(open_order.margin_type, None);
776 assert_eq!(open_order.number_of_fills, 0);
777 assert_eq!(open_order.total_fees, Decimal::ZERO);
778
779 let filled_order = &response.orders[1];
780 assert_eq!(filled_order.status, CoinbaseOrderStatus::Filled);
781 assert_eq!(filled_order.side, CoinbaseOrderSide::Sell);
782 assert_eq!(filled_order.order_type, CoinbaseOrderType::Market);
783 assert_eq!(filled_order.margin_type, None);
784 assert!(filled_order.size_in_quote);
785 assert_eq!(
786 filled_order.time_in_force,
787 Some(CoinbaseTimeInForce::ImmediateOrCancel)
788 );
789 }
790
791 #[rstest]
792 fn test_deserialize_fills() {
793 let json = load_test_fixture("http_fills.json");
794 let response: FillsResponse = serde_json::from_str(&json).unwrap();
795
796 assert_eq!(response.fills.len(), 2);
797
798 let maker_fill = &response.fills[0];
799 assert_eq!(maker_fill.trade_id, "1111-11111-111111");
800 assert_eq!(maker_fill.order_id, "0000-000000-000000");
801 assert_eq!(maker_fill.product_id, "BTC-USD");
802 assert_eq!(maker_fill.price, "45123.45");
803 assert_eq!(maker_fill.size, "0.005");
804 assert_eq!(maker_fill.trade_type, CoinbaseFillTradeType::Fill);
805 assert_eq!(maker_fill.commission, Decimal::from_str("1.14").unwrap());
806 assert_eq!(maker_fill.side, CoinbaseOrderSide::Buy);
807 assert_eq!(
808 maker_fill.liquidity_indicator,
809 CoinbaseLiquidityIndicator::Maker
810 );
811
812 let taker_fill = &response.fills[1];
813 assert_eq!(
814 taker_fill.liquidity_indicator,
815 CoinbaseLiquidityIndicator::Taker
816 );
817 }
818
819 #[rstest]
820 fn test_deserialize_accounts() {
821 let json = load_test_fixture("http_accounts.json");
822 let response: AccountsResponse = serde_json::from_str(&json).unwrap();
823
824 assert_eq!(response.accounts.len(), 2);
825 assert!(!response.has_next);
826
827 let btc_account = &response.accounts[0];
828 assert_eq!(btc_account.currency, "BTC");
829 assert_eq!(
830 btc_account.available_balance.value,
831 Decimal::from_str("1.23456789").unwrap()
832 );
833 assert_eq!(btc_account.available_balance.currency, "BTC");
834 assert_eq!(btc_account.account_type, CoinbaseAccountType::Crypto);
835 assert!(btc_account.default);
836 assert_eq!(
837 btc_account.hold.as_ref().map(|b| b.value),
838 Some(Decimal::from_str("0.00500000").unwrap())
839 );
840
841 let usd_account = &response.accounts[1];
842 assert_eq!(usd_account.currency, "USD");
843 assert_eq!(
844 usd_account.available_balance.value,
845 Decimal::from_str("10000.50").unwrap()
846 );
847 assert_eq!(usd_account.account_type, CoinbaseAccountType::Fiat);
848 }
849
850 #[rstest]
851 fn test_future_product_fields() {
852 let json = load_test_fixture("http_products_future.json");
853 let response: ProductsResponse = serde_json::from_str(&json).unwrap();
854 let product = &response.products[0];
855
856 assert_eq!(product.product_type, CoinbaseProductType::Future);
857 assert_eq!(product.product_venue, Some(CoinbaseProductVenue::Fcm));
858 assert!(product.future_product_details.is_some());
859
860 let session = product.fcm_trading_session_details.as_ref().unwrap();
861 assert_eq!(session.session_state, CoinbaseFcmTradingSessionState::Open);
862 assert_eq!(
863 session.closed_reason,
864 CoinbaseFcmTradingSessionClosedReason::ExchangeMaintenance
865 );
866
867 let details = product.future_product_details.as_ref().unwrap();
868 assert!(!details.contract_code.is_empty());
869 assert!(!details.contract_size.is_empty());
870 assert_eq!(details.risk_managed_by, CoinbaseRiskManagedBy::ManagedByFcm);
871 }
872
873 #[rstest]
874 fn test_deserialize_edit_order_response_success() {
875 let json = r#"{"success": true, "errors": []}"#;
876 let resp: EditOrderResponse = serde_json::from_str(json).unwrap();
877 assert!(resp.success);
878 assert!(resp.errors.is_empty());
879 }
880
881 #[rstest]
882 fn test_deserialize_edit_order_response_failure() {
883 let json = r#"{
884 "success": false,
885 "errors": [
886 {
887 "edit_failure_reason": "ORDER_NOT_FOUND",
888 "preview_failure_reason": ""
889 }
890 ]
891 }"#;
892 let resp: EditOrderResponse = serde_json::from_str(json).unwrap();
893 assert!(!resp.success);
894 assert_eq!(resp.errors.len(), 1);
895 assert_eq!(resp.errors[0].edit_failure_reason, "ORDER_NOT_FOUND");
896 assert_eq!(resp.errors[0].preview_failure_reason, "");
897 }
898
899 #[rstest]
900 fn test_deserialize_edit_order_response_preview_failure() {
901 let json = r#"{
902 "success": false,
903 "errors": [
904 {
905 "edit_failure_reason": "",
906 "preview_failure_reason": "PREVIEW_INSUFFICIENT_FUNDS"
907 }
908 ]
909 }"#;
910 let resp: EditOrderResponse = serde_json::from_str(json).unwrap();
911 assert!(!resp.success);
912 assert_eq!(
913 resp.errors[0].preview_failure_reason,
914 "PREVIEW_INSUFFICIENT_FUNDS"
915 );
916 }
917
918 #[rstest]
919 fn test_deserialize_edit_order_response_omitted_errors_defaults_empty() {
920 let json = r#"{"success": true}"#;
921 let resp: EditOrderResponse = serde_json::from_str(json).unwrap();
922 assert!(resp.success);
923 assert!(resp.errors.is_empty());
924 }
925}