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