1use std::any::type_name;
6
7use log::debug;
8use reqwest::Client;
9use serde_json::json;
10
11use crate::config::NetworkConfig;
12use crate::errors::O2Error;
13use crate::models::*;
14
15#[derive(Debug, Clone)]
17pub struct O2Api {
18 client: Client,
19 config: NetworkConfig,
20}
21
22impl O2Api {
23 pub fn new(config: NetworkConfig) -> Self {
25 Self {
26 client: Client::new(),
27 config,
28 }
29 }
30
31 async fn parse_response<T: serde::de::DeserializeOwned>(
33 &self,
34 response: reqwest::Response,
35 ) -> Result<T, O2Error> {
36 let status = response.status();
37 let text = response.text().await?;
38 let target_type = type_name::<T>();
39 debug!(
40 "api.parse_response status={} target_type={} body_len={}",
41 status,
42 target_type,
43 text.len()
44 );
45
46 if !status.is_success() {
47 debug!(
48 "api.parse_response non_success status={} body={}",
49 status, text
50 );
51 if let Ok(err) = serde_json::from_str::<serde_json::Value>(&text) {
53 if let Some(code) = err.get("code").and_then(|c| c.as_u64()) {
54 let raw_message = err
55 .get("message")
56 .or_else(|| err.get("error"))
57 .and_then(|m| m.as_str())
58 .unwrap_or("Unknown error");
59 let message = if raw_message.contains("Revert")
63 || raw_message.contains("revert")
64 || raw_message.contains("Panic")
65 {
66 let reason = err.get("reason").and_then(|r| r.as_str()).unwrap_or("");
67 let receipts = err.get("receipts").cloned();
68 crate::onchain_revert::augment_revert_reason(
69 raw_message,
70 reason,
71 receipts.as_ref(),
72 )
73 } else {
74 raw_message.to_string()
75 };
76 return Err(O2Error::from_code(code as u32, message));
77 }
78 if let Some(message) = err
79 .get("message")
80 .or_else(|| err.get("error"))
81 .and_then(|m| m.as_str())
82 {
83 let raw_reason = err.get("reason").and_then(|r| r.as_str()).unwrap_or("");
84 let receipts = err.get("receipts").cloned();
85 let has_receipts = receipts.as_ref().is_some_and(|v| !v.is_null());
86 let has_revert_evidence = raw_reason.contains("Revert")
87 || raw_reason.to_lowercase().contains("receipt")
88 || message.to_lowercase().contains("transaction");
89
90 if has_receipts || has_revert_evidence {
94 let reason = crate::onchain_revert::augment_revert_reason(
95 message,
96 raw_reason,
97 receipts.as_ref(),
98 );
99 return Err(O2Error::OnChainRevert {
100 message: message.to_string(),
101 reason,
102 receipts,
103 });
104 }
105
106 return Err(O2Error::HttpError(format!("HTTP {}: {}", status, message)));
107 }
108 }
109 return Err(O2Error::HttpError(format!("HTTP {}: {}", status, text)));
110 }
111
112 match serde_json::from_str(&text) {
113 Ok(parsed) => {
114 debug!("api.parse_response decode_ok target_type={}", target_type);
115 Ok(parsed)
116 }
117 Err(e) => {
118 debug!(
119 "api.parse_response decode_failed target_type={} error={}",
120 target_type, e
121 );
122 Err(O2Error::JsonError(format!(
123 "Failed to parse response: {e}\nBody: {}",
124 &text[..text.len().min(500)]
125 )))
126 }
127 }
128 }
129
130 pub async fn get_markets(&self) -> Result<MarketsResponse, O2Error> {
136 debug!("api.get_markets");
137 let url = format!("{}/v1/markets", self.config.api_base);
138 let resp = self.client.get(&url).send().await?;
139 self.parse_response(resp).await
140 }
141
142 pub async fn get_market_summary(&self, market_id: &str) -> Result<Vec<MarketSummary>, O2Error> {
144 debug!("api.get_market_summary market_id={}", market_id);
145 let url = format!("{}/v1/markets/summary", self.config.api_base);
146 let resp = self
147 .client
148 .get(&url)
149 .query(&[("market_id", market_id)])
150 .send()
151 .await?;
152 self.parse_response(resp).await
153 }
154
155 pub async fn get_market_ticker(&self, market_id: &str) -> Result<Vec<MarketTicker>, O2Error> {
157 debug!("api.get_market_ticker market_id={}", market_id);
158 let url = format!("{}/v1/markets/ticker", self.config.api_base);
159 let resp = self
160 .client
161 .get(&url)
162 .query(&[("market_id", market_id)])
163 .send()
164 .await?;
165 self.parse_response(resp).await
166 }
167
168 pub async fn get_depth(
174 &self,
175 market_id: &str,
176 precision: u64,
177 limit: Option<usize>,
178 ) -> Result<DepthSnapshot, O2Error> {
179 debug!(
180 "api.get_depth market_id={} precision={} limit={:?}",
181 market_id, precision, limit
182 );
183 let url = format!("{}/v1/depth", self.config.api_base);
184 let precision_str = precision.to_string();
185 let mut pairs: Vec<(&str, String)> = vec![
186 ("market_id", market_id.to_string()),
187 ("precision", precision_str),
188 ];
189 if let Some(lim) = limit {
190 pairs.push(("limit", lim.to_string()));
191 }
192 let resp = self
193 .client
194 .get(&url)
195 .query(
196 &pairs
197 .iter()
198 .map(|(k, v)| (*k, v.as_str()))
199 .collect::<Vec<_>>(),
200 )
201 .send()
202 .await?;
203 let val: serde_json::Value = self.parse_response(resp).await?;
204 let depth = val
206 .get("orders")
207 .or_else(|| val.get("view"))
208 .unwrap_or(&val);
209 let mut snapshot: DepthSnapshot = serde_json::from_value(depth.clone())
210 .map_err(|e| O2Error::JsonError(format!("Failed to parse depth: {e}")))?;
211 if let Some(lim) = limit {
214 snapshot.bids.truncate(lim);
215 snapshot.asks.truncate(lim);
216 }
217 Ok(snapshot)
218 }
219
220 pub async fn get_trades(
226 &self,
227 market_id: &str,
228 direction: &str,
229 count: u32,
230 start_timestamp: Option<u64>,
231 start_trade_id: Option<&str>,
232 contract: Option<&str>,
233 ) -> Result<TradesResponse, O2Error> {
234 debug!(
235 "api.get_trades market_id={} direction={} count={} contract={:?}",
236 market_id, direction, count, contract
237 );
238 let url = format!("{}/v1/trades", self.config.api_base);
239 let count_str = count.to_string();
240 let start_timestamp_str = start_timestamp.map(|ts| ts.to_string());
241 let mut query: Vec<(&str, &str)> = vec![
242 ("market_id", market_id),
243 ("direction", direction),
244 ("count", count_str.as_str()),
245 ];
246 if let Some(ts) = start_timestamp_str.as_deref() {
247 query.push(("start_timestamp", ts));
248 }
249 if let Some(tid) = start_trade_id {
250 query.push(("start_trade_id", tid));
251 }
252 if let Some(c) = contract {
253 query.push(("contract", c));
254 }
255 let resp = self.client.get(&url).query(&query).send().await?;
256 self.parse_response(resp).await
257 }
258
259 pub async fn get_trades_by_account(
261 &self,
262 market_id: &str,
263 contract: &str,
264 direction: &str,
265 count: u32,
266 start_timestamp: Option<u64>,
267 start_trade_id: Option<&str>,
268 ) -> Result<TradesResponse, O2Error> {
269 debug!(
270 "api.get_trades_by_account market_id={} contract={} direction={} count={}",
271 market_id, contract, direction, count
272 );
273 let url = format!("{}/v1/trades_by_account", self.config.api_base);
274 let count_str = count.to_string();
275 let start_timestamp_str = start_timestamp.map(|ts| ts.to_string());
276 let mut query: Vec<(&str, &str)> = vec![
277 ("market_id", market_id),
278 ("contract", contract),
279 ("direction", direction),
280 ("count", count_str.as_str()),
281 ];
282 if let Some(ts) = start_timestamp_str.as_deref() {
283 query.push(("start_timestamp", ts));
284 }
285 if let Some(tid) = start_trade_id {
286 query.push(("start_trade_id", tid));
287 }
288 let resp = self.client.get(&url).query(&query).send().await?;
289 self.parse_response(resp).await
290 }
291
292 const VALID_RESOLUTIONS: &'static [&'static str] = &[
294 "1s", "1m", "2m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d",
295 "3d", "1w", "1M", "3M",
296 ];
297
298 pub async fn get_bars(
304 &self,
305 market_id: &str,
306 from_ts: u64,
307 to_ts: u64,
308 resolution: &str,
309 ) -> Result<Vec<Bar>, O2Error> {
310 if !Self::VALID_RESOLUTIONS.contains(&resolution) {
311 return Err(O2Error::InvalidRequest(format!(
312 "Invalid bar resolution \"{resolution}\". Valid values: {:?}",
313 Self::VALID_RESOLUTIONS
314 )));
315 }
316 debug!(
317 "api.get_bars market_id={} from_ts={} to_ts={} resolution={}",
318 market_id, from_ts, to_ts, resolution
319 );
320 let url = format!("{}/v1/bars", self.config.api_base);
321 let from_ts_str = from_ts.to_string();
322 let to_ts_str = to_ts.to_string();
323 let resp = self
324 .client
325 .get(&url)
326 .query(&[
327 ("market_id", market_id),
328 ("from", from_ts_str.as_str()),
329 ("to", to_ts_str.as_str()),
330 ("resolution", resolution),
331 ])
332 .send()
333 .await?;
334 let val: serde_json::Value = self.parse_response(resp).await?;
335 let bars_val = val.get("bars").unwrap_or(&val);
336 serde_json::from_value(bars_val.clone())
337 .map_err(|e| O2Error::JsonError(format!("Failed to parse bars: {e}")))
338 }
339
340 pub async fn create_account(
346 &self,
347 owner_address: &str,
348 ) -> Result<CreateAccountResponse, O2Error> {
349 debug!("api.create_account owner_address={}", owner_address);
350 let url = format!("{}/v1/accounts", self.config.api_base);
351 let body = json!({
352 "identity": {
353 "Address": owner_address
354 }
355 });
356 let resp = self
357 .client
358 .post(&url)
359 .header("Content-Type", "application/json")
360 .json(&body)
361 .send()
362 .await?;
363 self.parse_response(resp).await
364 }
365
366 pub async fn get_account_by_owner(&self, owner: &str) -> Result<AccountResponse, O2Error> {
368 debug!("api.get_account_by_owner owner={}", owner);
369 let url = format!("{}/v1/accounts", self.config.api_base);
370 let resp = self
371 .client
372 .get(&url)
373 .query(&[("owner", owner)])
374 .send()
375 .await?;
376 self.parse_response(resp).await
377 }
378
379 pub async fn get_account_by_id(
381 &self,
382 trade_account_id: &str,
383 ) -> Result<AccountResponse, O2Error> {
384 debug!(
385 "api.get_account_by_id trade_account_id={}",
386 trade_account_id
387 );
388 let url = format!("{}/v1/accounts", self.config.api_base);
389 let resp = self
390 .client
391 .get(&url)
392 .query(&[("trade_account_id", trade_account_id)])
393 .send()
394 .await?;
395 self.parse_response(resp).await
396 }
397
398 pub async fn get_balance(
400 &self,
401 asset_id: &str,
402 contract: Option<&str>,
403 address: Option<&str>,
404 ) -> Result<BalanceResponse, O2Error> {
405 debug!(
406 "api.get_balance asset_id={} contract={:?} address={:?}",
407 asset_id, contract, address
408 );
409 let url = format!("{}/v1/balance", self.config.api_base);
410 let mut query: Vec<(&str, &str)> = vec![("asset_id", asset_id)];
411 if let Some(c) = contract {
412 query.push(("contract", c));
413 }
414 if let Some(a) = address {
415 query.push(("address", a));
416 }
417 let resp = self.client.get(&url).query(&query).send().await?;
418 self.parse_response(resp).await
419 }
420
421 #[allow(clippy::too_many_arguments)]
427 pub async fn get_orders(
428 &self,
429 market_id: &str,
430 contract: &str,
431 direction: &str,
432 count: u32,
433 is_open: Option<bool>,
434 start_timestamp: Option<u64>,
435 start_order_id: Option<&str>,
436 ) -> Result<OrdersResponse, O2Error> {
437 debug!(
438 "api.get_orders market_id={} contract={} direction={} count={} is_open={:?} start_timestamp={:?} start_order_id={:?}",
439 market_id, contract, direction, count, is_open, start_timestamp, start_order_id
440 );
441 let url = format!("{}/v1/orders", self.config.api_base);
442 let count_str = count.to_string();
443 let is_open_str = is_open.map(|open| open.to_string());
444 let start_timestamp_str = start_timestamp.map(|ts| ts.to_string());
445 let mut query: Vec<(&str, &str)> = vec![
446 ("market_id", market_id),
447 ("contract", contract),
448 ("direction", direction),
449 ("count", count_str.as_str()),
450 ];
451 if let Some(open) = is_open_str.as_deref() {
452 query.push(("is_open", open));
453 }
454 if let Some(ts) = start_timestamp_str.as_deref() {
455 query.push(("start_timestamp", ts));
456 }
457 if let Some(oid) = start_order_id {
458 query.push(("start_order_id", oid));
459 }
460 let resp = self.client.get(&url).query(&query).send().await?;
461 self.parse_response(resp).await
462 }
463
464 pub async fn get_order(&self, market_id: &str, order_id: &str) -> Result<Order, O2Error> {
466 debug!(
467 "api.get_order market_id={} order_id={}",
468 market_id, order_id
469 );
470 let url = format!("{}/v1/order", self.config.api_base);
471 let resp = self
472 .client
473 .get(&url)
474 .query(&[("market_id", market_id), ("order_id", order_id)])
475 .send()
476 .await?;
477 let val: serde_json::Value = self.parse_response(resp).await?;
478 let order_val = val.get("order").unwrap_or(&val);
480 serde_json::from_value(order_val.clone())
481 .map_err(|e| O2Error::JsonError(format!("Failed to parse order: {e}")))
482 }
483
484 pub async fn create_session(
490 &self,
491 owner_id: &str,
492 request: &SessionRequest,
493 ) -> Result<SessionResponse, O2Error> {
494 debug!(
495 "api.create_session owner_id={} contract_id={} nonce={} expiry={}",
496 owner_id, request.contract_id, request.nonce, request.expiry
497 );
498 let url = format!("{}/v1/session", self.config.api_base);
499 let resp = self
500 .client
501 .put(&url)
502 .header("Content-Type", "application/json")
503 .header("O2-Owner-Id", owner_id)
504 .json(request)
505 .send()
506 .await?;
507 self.parse_response(resp).await
508 }
509
510 pub(crate) async fn submit_actions(
512 &self,
513 owner_id: &str,
514 request: &SessionActionsRequest,
515 ) -> Result<SessionActionsResponse, O2Error> {
516 debug!(
517 "api.submit_actions owner_id={} nonce={} markets={} collect_orders={:?}",
518 owner_id,
519 request.nonce,
520 request.actions.len(),
521 request.collect_orders
522 );
523 let url = format!("{}/v1/session/actions", self.config.api_base);
524 let resp = self
525 .client
526 .post(&url)
527 .header("Content-Type", "application/json")
528 .header("O2-Owner-Id", owner_id)
529 .json(request)
530 .send()
531 .await?;
532 let val: serde_json::Value = self.parse_response(resp).await?;
535
536 let tx_id = val.get("tx_id").and_then(|v| v.as_str()).map(TxId::from);
540 let code = val.get("code").and_then(|v| v.as_u64()).map(|v| v as u32);
541 let message = val
542 .get("message")
543 .and_then(|v| v.as_str())
544 .map(String::from);
545 let reason = val.get("reason").and_then(|v| v.as_str()).map(String::from);
546 let receipts = val.get("receipts").cloned();
547 let orders = val
548 .get("orders")
549 .and_then(|o| serde_json::from_value::<Vec<Order>>(o.clone()).ok());
550
551 let parsed = SessionActionsResponse {
552 tx_id,
553 orders,
554 code,
555 message,
556 reason,
557 receipts,
558 };
559
560 if parsed.is_success() {
562 debug!("api.submit_actions parsed=success tx_id={:?}", parsed.tx_id);
563 Ok(parsed)
564 } else if parsed.is_preflight_error() {
565 let code = parsed.code.unwrap_or(0);
566 let message = parsed.message.unwrap_or_default();
567 debug!(
568 "api.submit_actions parsed=preflight_error code={} message={}",
569 code, message
570 );
571 Err(O2Error::from_code(code, message))
572 } else if parsed.is_onchain_error() {
573 debug!(
574 "api.submit_actions parsed=onchain_error message={:?} reason={:?}",
575 parsed.message, parsed.reason
576 );
577 let message = parsed.message.unwrap_or_default();
578 let raw_reason = parsed.reason.unwrap_or_default();
579 let reason = crate::onchain_revert::augment_revert_reason(
580 &message,
581 &raw_reason,
582 parsed.receipts.as_ref(),
583 );
584 Err(O2Error::OnChainRevert {
585 message,
586 reason,
587 receipts: parsed.receipts,
588 })
589 } else {
590 debug!("api.submit_actions parsed=ambiguous returning_raw_response");
592 Ok(parsed)
593 }
594 }
595
596 pub async fn withdraw(
602 &self,
603 owner_id: &str,
604 request: &WithdrawRequest,
605 ) -> Result<WithdrawResponse, O2Error> {
606 debug!(
607 "api.withdraw owner_id={} trade_account_id={} asset_id={} amount={} nonce={}",
608 owner_id, request.trade_account_id, request.asset_id, request.amount, request.nonce
609 );
610 let url = format!("{}/v1/accounts/withdraw", self.config.api_base);
611 let resp = self
612 .client
613 .post(&url)
614 .header("Content-Type", "application/json")
615 .header("O2-Owner-Id", owner_id)
616 .json(request)
617 .send()
618 .await?;
619 self.parse_response(resp).await
620 }
621
622 pub async fn whitelist_account(
628 &self,
629 trade_account_id: &str,
630 ) -> Result<WhitelistResponse, O2Error> {
631 debug!(
632 "api.whitelist_account trade_account_id={}",
633 trade_account_id
634 );
635 let url = format!("{}/analytics/v1/whitelist", self.config.api_base);
636 let body = WhitelistRequest {
637 trade_account: trade_account_id.to_string(),
638 };
639 let resp = self
640 .client
641 .post(&url)
642 .header("Content-Type", "application/json")
643 .json(&body)
644 .send()
645 .await?;
646 self.parse_response(resp).await
647 }
648
649 pub async fn get_referral_info(&self, code: &str) -> Result<ReferralInfo, O2Error> {
651 debug!("api.get_referral_info code={}", code);
652 let url = format!("{}/analytics/v1/referral/code-info", self.config.api_base);
653 let resp = self
654 .client
655 .get(&url)
656 .query(&[("code", code)])
657 .send()
658 .await?;
659 self.parse_response(resp).await
660 }
661
662 pub async fn get_aggregated_assets(&self) -> Result<AggregatedAssets, O2Error> {
668 debug!("api.get_aggregated_assets");
669 let url = format!("{}/v1/aggregated/assets", self.config.api_base);
670 let resp = self.client.get(&url).send().await?;
671 self.parse_response(resp).await
672 }
673
674 pub async fn get_aggregated_orderbook(
676 &self,
677 market_pair: &str,
678 depth: u32,
679 level: u32,
680 ) -> Result<AggregatedOrderbook, O2Error> {
681 debug!(
682 "api.get_aggregated_orderbook market_pair={} depth={} level={}",
683 market_pair, depth, level
684 );
685 let url = format!("{}/v1/aggregated/orderbook", self.config.api_base);
686 let depth_str = depth.to_string();
687 let level_str = level.to_string();
688 let resp = self
689 .client
690 .get(&url)
691 .query(&[
692 ("market_pair", market_pair),
693 ("depth", depth_str.as_str()),
694 ("level", level_str.as_str()),
695 ])
696 .send()
697 .await?;
698 self.parse_response(resp).await
699 }
700
701 pub async fn get_aggregated_coingecko_orderbook(
703 &self,
704 ticker_id: &str,
705 depth: u32,
706 ) -> Result<CoingeckoAggregatedOrderbook, O2Error> {
707 debug!(
708 "api.get_aggregated_coingecko_orderbook ticker_id={} depth={}",
709 ticker_id, depth
710 );
711 let url = format!("{}/v1/aggregated/coingecko/orderbook", self.config.api_base);
712 let depth_str = depth.to_string();
713 let resp = self
714 .client
715 .get(&url)
716 .query(&[("ticker_id", ticker_id), ("depth", depth_str.as_str())])
717 .send()
718 .await?;
719 self.parse_response(resp).await
720 }
721
722 pub async fn get_aggregated_summary(&self) -> Result<Vec<PairSummary>, O2Error> {
724 debug!("api.get_aggregated_summary");
725 let url = format!("{}/v1/aggregated/summary", self.config.api_base);
726 let resp = self.client.get(&url).send().await?;
727 self.parse_response(resp).await
728 }
729
730 pub async fn get_aggregated_ticker(&self) -> Result<AggregatedTicker, O2Error> {
732 debug!("api.get_aggregated_ticker");
733 let url = format!("{}/v1/aggregated/ticker", self.config.api_base);
734 let resp = self.client.get(&url).send().await?;
735 self.parse_response(resp).await
736 }
737
738 pub async fn get_aggregated_coingecko_tickers(&self) -> Result<Vec<PairTicker>, O2Error> {
740 debug!("api.get_aggregated_coingecko_tickers");
741 let url = format!("{}/v1/aggregated/coingecko/tickers", self.config.api_base);
742 let resp = self.client.get(&url).send().await?;
743 self.parse_response(resp).await
744 }
745
746 pub async fn get_aggregated_trades(
748 &self,
749 market_pair: &str,
750 ) -> Result<Vec<AggregatedTrade>, O2Error> {
751 debug!("api.get_aggregated_trades market_pair={}", market_pair);
752 let url = format!("{}/v1/aggregated/trades", self.config.api_base);
753 let resp = self
754 .client
755 .get(&url)
756 .query(&[("market_pair", market_pair)])
757 .send()
758 .await?;
759 self.parse_response(resp).await
760 }
761
762 pub async fn mint_to_address(&self, address: &str) -> Result<FaucetResponse, O2Error> {
768 debug!("api.mint_to_address address={}", address);
769 let faucet_url = self
770 .config
771 .faucet_url
772 .as_ref()
773 .ok_or_else(|| O2Error::Other("Faucet not available on this network".into()))?;
774
775 let body = json!({ "address": address });
776 let resp = self
777 .client
778 .post(faucet_url)
779 .header("Content-Type", "application/json")
780 .json(&body)
781 .send()
782 .await?;
783 self.parse_response(resp).await
784 }
785
786 pub async fn mint_to_contract(&self, contract_id: &str) -> Result<FaucetResponse, O2Error> {
788 debug!("api.mint_to_contract contract_id={}", contract_id);
789 let faucet_url = self
790 .config
791 .faucet_url
792 .as_ref()
793 .ok_or_else(|| O2Error::Other("Faucet not available on this network".into()))?;
794
795 let body = json!({ "contract": contract_id });
796 let resp = self
797 .client
798 .post(faucet_url)
799 .header("Content-Type", "application/json")
800 .json(&body)
801 .send()
802 .await?;
803 self.parse_response(resp).await
804 }
805}