1use alloy::providers::ProviderBuilder;
7use alloy::signers::local::PrivateKeySigner;
8use alloy_primitives::{aliases::U192, Address, U256};
9use reqwest::Client as HttpClient;
10use rust_decimal::prelude::ToPrimitive;
11use rust_decimal::Decimal;
12use rust_decimal_macros::dec;
13use serde_json::{json, Value};
14use std::collections::HashMap;
15use std::sync::Arc;
16use tracing::{debug, info, warn};
17
18use crate::config::{Network, NetworkConfig};
19use crate::contracts::{TradingContract, TradingStorageContract, UsdcContract};
20use crate::error::{OstiumError, Result};
21use crate::rate_limit::RateLimiterManager;
22use crate::retry::{RetryConfig, RetryExecutor};
23use crate::types::*;
24
25type Provider = alloy::providers::fillers::FillProvider<
27 alloy::providers::fillers::JoinFill<
28 alloy_provider::Identity,
29 alloy::providers::fillers::JoinFill<
30 alloy::providers::fillers::GasFiller,
31 alloy::providers::fillers::JoinFill<
32 alloy::providers::fillers::BlobGasFiller,
33 alloy::providers::fillers::JoinFill<
34 alloy::providers::fillers::NonceFiller,
35 alloy::providers::fillers::ChainIdFiller,
36 >,
37 >,
38 >,
39 >,
40 alloy::providers::RootProvider<alloy::network::Ethereum>,
41 alloy::network::Ethereum,
42>;
43
44pub struct OstiumClientBuilder {
46 config: NetworkConfig,
47 signer: Option<PrivateKeySigner>,
48 http_client: Option<HttpClient>,
49 retry_config: RetryConfig,
50 enable_circuit_breaker: bool,
51 rate_limiter: Option<RateLimiterManager>,
52}
53
54impl OstiumClientBuilder {
55 pub fn new(network: Network) -> Self {
57 Self {
58 config: network.config(),
59 signer: None,
60 http_client: None,
61 retry_config: RetryConfig::default(),
62 enable_circuit_breaker: true,
63 rate_limiter: None,
64 }
65 }
66
67 pub fn with_config(config: NetworkConfig) -> Self {
69 Self {
70 config,
71 signer: None,
72 http_client: None,
73 retry_config: RetryConfig::default(),
74 enable_circuit_breaker: true,
75 rate_limiter: None,
76 }
77 }
78
79 pub fn with_private_key(mut self, private_key: &str) -> Result<Self> {
81 let signer = private_key
82 .parse::<PrivateKeySigner>()
83 .map_err(|e| OstiumError::wallet(format!("Invalid private key: {}", e)))?;
84 self.signer = Some(signer);
85 Ok(self)
86 }
87
88 pub fn with_rpc_url(mut self, url: &str) -> Result<Self> {
90 self.config.rpc_url = url
91 .parse()
92 .map_err(|e| OstiumError::config(format!("Invalid RPC URL: {}", e)))?;
93 Ok(self)
94 }
95
96 pub fn with_http_client(mut self, client: HttpClient) -> Self {
98 self.http_client = Some(client);
99 self
100 }
101
102 pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
104 self.retry_config = retry_config;
105 self
106 }
107
108 pub fn with_circuit_breaker(mut self, enabled: bool) -> Self {
110 self.enable_circuit_breaker = enabled;
111 self
112 }
113
114 pub fn with_network_retry(mut self) -> Self {
116 self.retry_config = RetryConfig::network();
117 self
118 }
119
120 pub fn with_contract_retry(mut self) -> Self {
122 self.retry_config = RetryConfig::contract();
123 self
124 }
125
126 pub fn with_graphql_retry(mut self) -> Self {
128 self.retry_config = RetryConfig::graphql();
129 self
130 }
131
132 pub fn with_rate_limiting(mut self) -> Self {
134 self.rate_limiter = Some(RateLimiterManager::new().with_default_limits());
135 self
136 }
137
138 pub fn with_conservative_rate_limiting(mut self) -> Self {
140 use crate::rate_limit::RateLimitConfig;
141 self.rate_limiter = Some(
142 RateLimiterManager::new()
143 .with_graphql_rate_limit(RateLimitConfig::conservative())
144 .with_rest_rate_limit(RateLimitConfig::conservative())
145 .with_blockchain_rate_limit(RateLimitConfig::conservative()),
146 );
147 self
148 }
149
150 pub fn with_rate_limiter(mut self, rate_limiter: RateLimiterManager) -> Self {
152 self.rate_limiter = Some(rate_limiter);
153 self
154 }
155
156 pub async fn build(self) -> Result<OstiumClient> {
158 self.config.validate()?;
160
161 let provider = ProviderBuilder::new()
163 .connect(self.config.rpc_url.as_str())
164 .await
165 .map_err(|e| OstiumError::network(format!("Failed to connect to RPC: {}", e)))?;
166
167 let http_client = self.http_client.unwrap_or_else(|| {
169 HttpClient::builder()
170 .timeout(std::time::Duration::from_secs(30))
171 .build()
172 .expect("Failed to create HTTP client")
173 });
174
175 info!(
176 "Initialized Ostium client for {:?} network",
177 self.config.network
178 );
179
180 Ok(OstiumClient {
181 config: self.config,
182 provider: Arc::new(provider),
183 signer: self.signer,
184 http_client: Arc::new(http_client),
185 rate_limiter: Arc::new(self.rate_limiter.unwrap_or_default()),
186 network_retry_executor: Arc::new(if self.enable_circuit_breaker {
187 RetryExecutor::new(RetryConfig::network())
188 .with_circuit_breaker(5, std::time::Duration::from_secs(60))
189 } else {
190 RetryExecutor::new(RetryConfig::network())
191 }),
192 _contract_retry_executor: Arc::new(if self.enable_circuit_breaker {
193 RetryExecutor::new(RetryConfig::contract())
194 .with_circuit_breaker(3, std::time::Duration::from_secs(120))
195 } else {
196 RetryExecutor::new(RetryConfig::contract())
197 }),
198 graphql_retry_executor: Arc::new(RetryExecutor::new(RetryConfig::graphql())),
199 })
200 }
201}
202
203#[derive(Clone)]
205pub struct OstiumClient {
206 config: NetworkConfig,
207 provider: Arc<Provider>,
208 signer: Option<PrivateKeySigner>,
209 http_client: Arc<HttpClient>,
210 rate_limiter: Arc<RateLimiterManager>,
211 network_retry_executor: Arc<RetryExecutor>,
212 _contract_retry_executor: Arc<RetryExecutor>,
213 graphql_retry_executor: Arc<RetryExecutor>,
214}
215
216impl OstiumClient {
217 pub fn builder(network: Network) -> OstiumClientBuilder {
219 OstiumClientBuilder::new(network)
220 }
221
222 pub fn builder_with_config(config: NetworkConfig) -> OstiumClientBuilder {
224 OstiumClientBuilder::with_config(config)
225 }
226
227 pub async fn new(network: Network) -> Result<Self> {
229 OstiumClientBuilder::new(network).build().await
230 }
231
232 pub fn config(&self) -> &NetworkConfig {
234 &self.config
235 }
236
237 pub fn signer_address(&self) -> Option<Address> {
239 self.signer.as_ref().map(|s| s.address())
240 }
241
242 pub fn has_signer(&self) -> bool {
244 self.signer.is_some()
245 }
246
247 fn usdc_contract(&self) -> UsdcContract<Arc<Provider>> {
249 UsdcContract::new(self.config.usdc_address, self.provider.clone())
250 }
251
252 fn trading_contract(&self) -> TradingContract<Arc<Provider>> {
254 TradingContract::new(self.config.trading_contract, self.provider.clone())
255 }
256
257 fn trading_storage_contract(&self) -> TradingStorageContract<Arc<Provider>> {
259 TradingStorageContract::new(self.config.storage_contract, self.provider.clone())
260 }
261
262 async fn graphql_query(&self, query: &str, variables: Option<Value>) -> Result<Value> {
264 self.rate_limiter
266 .acquire_graphql()
267 .await
268 .map_err(|e| OstiumError::network(format!("Rate limit error: {}", e)))?;
269
270 let query = query.to_string();
271 let variables = variables.unwrap_or(json!({}));
272 let http_client = self.http_client.clone();
273 let url = self.config.graphql_url.clone();
274
275 self.graphql_retry_executor
276 .execute(|| {
277 let query = query.clone();
278 let variables = variables.clone();
279 let http_client = http_client.clone();
280 let url = url.clone();
281
282 async move {
283 let body = json!({
284 "query": query,
285 "variables": variables
286 });
287
288 debug!("Executing GraphQL query: {}", query);
289
290 let response = http_client
291 .post(url.as_str())
292 .json(&body)
293 .send()
294 .await
295 .map_err(|e| {
296 OstiumError::network(format!("GraphQL request failed: {}", e))
297 })?;
298
299 if !response.status().is_success() {
300 let status = response.status();
301 let error_text = response.text().await.unwrap_or_default();
302 return Err(OstiumError::graphql(format!(
303 "GraphQL request failed with status {}: {}",
304 status, error_text
305 )));
306 }
307
308 let json: Value = response.json().await.map_err(|e| {
309 OstiumError::graphql(format!("Failed to parse GraphQL response: {}", e))
310 })?;
311
312 if let Some(errors) = json.get("errors") {
313 return Err(OstiumError::graphql(format!("GraphQL errors: {}", errors)));
314 }
315
316 json.get("data").cloned().ok_or_else(|| {
317 OstiumError::graphql("No data in GraphQL response".to_string())
318 })
319 }
320 })
321 .await
322 }
323
324 async fn rest_api_call(&self, url: String) -> Result<Value> {
326 self.rate_limiter
328 .acquire_rest()
329 .await
330 .map_err(|e| OstiumError::network(format!("Rate limit error: {}", e)))?;
331
332 let http_client = self.http_client.clone();
333
334 self.network_retry_executor
335 .execute(|| {
336 let url = url.clone();
337 let http_client = http_client.clone();
338
339 async move {
340 debug!("Making REST API call to: {}", url);
341
342 let response = http_client.get(&url).send().await.map_err(|e| {
343 OstiumError::network(format!("REST API request failed: {}", e))
344 })?;
345
346 if !response.status().is_success() {
347 let status = response.status();
348 let error_text = response.text().await.unwrap_or_default();
349 return Err(OstiumError::network(format!(
350 "REST API request failed with status {}: {}",
351 status, error_text
352 )));
353 }
354
355 response.json().await.map_err(|e| {
356 OstiumError::network(format!("Failed to parse REST API response: {}", e))
357 })
358 }
359 })
360 .await
361 }
362
363 fn decimal_to_u256(&self, value: Decimal, decimals: u8) -> Result<U256> {
365 let scale = 10_u128.pow(decimals as u32);
366 let scaled = (value * Decimal::from(scale))
367 .to_u128()
368 .ok_or_else(|| OstiumError::conversion("Value too large for U256".to_string()))?;
369 Ok(U256::from(scaled))
370 }
371
372 fn decimal_to_u192(&self, value: Decimal) -> Result<U192> {
374 let scale = 10_u128.pow(18);
375 let scaled = (value * Decimal::from(scale))
376 .to_u128()
377 .ok_or_else(|| OstiumError::conversion("Value too large for U192".to_string()))?;
378 Ok(U192::from(scaled))
379 }
380
381 fn convert_optional_price(&self, price: Option<Decimal>) -> Result<U192> {
383 match price {
384 Some(p) => self.decimal_to_u192(p),
385 None => Ok(U192::ZERO),
386 }
387 }
388
389 pub fn map_contract_error(&self, error: &str) -> (String, String, HashMap<String, String>) {
400 let error_selectors: HashMap<&str, &str> = [
402 ("0x5863f789", "WrongParams"),
403 ("0xcb87b762", "PairNotListed"),
404 ("0x1309a563", "IsPaused"),
405 ("0x093650d5", "NotGov"),
406 ("0x2a19e833", "NotManager"),
407 ("0x084986e7", "IsDone"),
408 ("0x432b6c83", "NotTradesUpKeep"),
409 ("0xe6f47fab", "MaxTradesPerPairReached"),
410 ("0x5c12ea62", "MaxPendingMarketOrdersReached"),
411 ("0x35fe85c5", "WrongLeverage"),
412 ("0x80a71fc5", "AboveMaxAllowedCollateral"),
413 ("0xeca695e1", "BelowMinLevPos"),
414 ("0xa41bb918", "WrongTP"),
415 ("0x083fbd78", "WrongSL"),
416 ("0x17e08e97", "NoTradeFound"),
417 ("0xdd9397bb", "TriggerPending"),
418 ("0xf77a8069", "AlreadyMarketClosed"),
419 ("0xa35ee470", "NoLimitFound"),
420 ("0x46c4ede2", "ExposureLimits"),
421 ("0xefa9e5be", "NoTradeToTimeoutFound"),
422 ("0x5ac89f62", "NotYourOrder"),
423 ("0x1add0915", "NotOpenMarketTimeoutOrder"),
424 ("0x3e0b1869", "WaitTimeout"),
425 ("0xc7fe4d00", "NotCloseMarketTimeoutOrder"),
426 ]
427 .iter()
428 .cloned()
429 .collect();
430
431 let mut error_message = error.to_string();
432 let mut error_type = "UnknownError".to_string();
433 let mut error_data = HashMap::new();
434
435 error_data.insert("original_error".to_string(), error.to_string());
437
438 if error.contains("Gas estimation failed") && error.contains("0x") {
440 if let Some(selector) = self.extract_error_selector(error) {
442 if let Some(&contract_error) = error_selectors.get(selector.as_str()) {
443 error_type = format!("GasEstimation_{}", contract_error);
444 error_message = format!(
445 "Gas estimation failed due to contract error: {}",
446 contract_error
447 );
448 error_data.insert("contract_error".to_string(), contract_error.to_string());
449 error_data.insert("selector".to_string(), selector.clone());
450
451 warn!(
452 "Contract error during gas estimation: {} (Selector: {})",
453 contract_error, selector
454 );
455 return (error_message, error_type, error_data);
456 }
457 }
458 }
459
460 if error.contains("execution reverted") {
462 if let Some(selector) = self.extract_error_selector(error) {
463 if let Some(&contract_error) = error_selectors.get(selector.as_str()) {
464 error_type = contract_error.to_string();
465 error_message = format!("Contract error: {}", contract_error);
466 error_data.insert("contract_error".to_string(), contract_error.to_string());
467 error_data.insert("selector".to_string(), selector.clone());
468
469 warn!(
470 "Contract execution reverted: {} (Selector: {})",
471 contract_error, selector
472 );
473 return (error_message, error_type, error_data);
474 }
475 }
476 }
477
478 if error.contains("insufficient funds") || error.contains("insufficient balance") {
480 error_type = "InsufficientFunds".to_string();
481 error_message = "Insufficient funds for transaction".to_string();
482 } else if error.contains("nonce too low") {
483 error_type = "NonceTooLow".to_string();
484 error_message = "Transaction nonce is too low".to_string();
485 } else if error.contains("gas required exceeds allowance") {
486 error_type = "OutOfGas".to_string();
487 error_message = "Transaction requires more gas than allowed".to_string();
488 } else if error.contains("replacement transaction underpriced") {
489 error_type = "UnderpricedReplacement".to_string();
490 error_message = "Replacement transaction gas price too low".to_string();
491 }
492
493 (error_message, error_type, error_data)
494 }
495
496 pub fn extract_error_selector(&self, error: &str) -> Option<String> {
498 let error_lower = error.to_lowercase();
501 let mut start_pos = 0;
502
503 while let Some(pos) = error_lower[start_pos..].find("0x") {
504 let actual_pos = start_pos + pos;
505 if actual_pos + 10 <= error_lower.len() {
506 let candidate = &error_lower[actual_pos..actual_pos + 10];
507 if candidate.len() == 10 && candidate.starts_with("0x") {
508 let hex_part = &candidate[2..];
509 if hex_part.chars().all(|c| c.is_ascii_hexdigit()) {
510 return Some(candidate.to_string());
511 }
512 }
513 }
514 start_pos = actual_pos + 2;
515 }
516
517 None
518 }
519
520 pub fn get_error_description(&self, selector: &str) -> Option<&'static str> {
522 match selector {
523 "0x5863f789" => Some("Wrong parameters provided to the contract function"),
524 "0xcb87b762" => Some("Trading pair is not listed or supported"),
525 "0x1309a563" => Some("Contract is currently paused"),
526 "0x093650d5" => Some("Caller is not the contract governor"),
527 "0x2a19e833" => Some("Caller is not a contract manager"),
528 "0x084986e7" => Some("Operation is already completed"),
529 "0x432b6c83" => Some("Caller is not authorized for trades upkeep"),
530 "0xe6f47fab" => Some("Maximum number of trades per pair reached"),
531 "0x5c12ea62" => Some("Maximum pending market orders reached"),
532 "0x35fe85c5" => Some("Leverage value is outside allowed range"),
533 "0x80a71fc5" => Some("Collateral amount exceeds maximum allowed"),
534 "0xeca695e1" => Some("Position size is below minimum leverage requirement"),
535 "0xa41bb918" => Some("Take profit price is invalid"),
536 "0x083fbd78" => Some("Stop loss price is invalid"),
537 "0x17e08e97" => Some("Trade not found"),
538 "0xdd9397bb" => Some("Trigger order is pending"),
539 "0xf77a8069" => Some("Market is already closed"),
540 "0xa35ee470" => Some("Limit order not found"),
541 "0x46c4ede2" => Some("Exposure limits exceeded"),
542 "0xefa9e5be" => Some("No trade found for timeout"),
543 "0x5ac89f62" => Some("Not your order"),
544 "0x1add0915" => Some("Not an open market timeout order"),
545 "0x3e0b1869" => Some("Must wait for timeout period"),
546 "0xc7fe4d00" => Some("Not a close market timeout order"),
547 _ => None,
548 }
549 }
550
551 pub fn map_contract_error_with_suggestions(
553 &self,
554 error: &str,
555 ) -> (String, String, HashMap<String, String>, Option<String>) {
556 let (error_message, error_type, mut error_data) = self.map_contract_error(error);
557
558 let suggestion = match error_type.as_str() {
559 "WrongParams" | "GasEstimation_WrongParams" => Some(
560 "Check that all parameters (collateral, leverage, prices) are within valid ranges"
561 .to_string(),
562 ),
563 "PairNotListed" | "GasEstimation_PairNotListed" => {
564 Some("Verify the trading pair symbol is correct and supported".to_string())
565 }
566 "IsPaused" | "GasEstimation_IsPaused" => {
567 Some("Trading is temporarily paused. Please try again later".to_string())
568 }
569 "MaxTradesPerPairReached" | "GasEstimation_MaxTradesPerPairReached" => {
570 Some("Close some existing positions before opening new ones".to_string())
571 }
572 "MaxPendingMarketOrdersReached" | "GasEstimation_MaxPendingMarketOrdersReached" => {
573 Some("Cancel some pending orders before placing new ones".to_string())
574 }
575 "WrongLeverage" | "GasEstimation_WrongLeverage" => {
576 Some("Adjust leverage to be within the allowed range for this pair".to_string())
577 }
578 "AboveMaxAllowedCollateral" | "GasEstimation_AboveMaxAllowedCollateral" => {
579 Some("Reduce the position size or collateral amount".to_string())
580 }
581 "BelowMinLevPos" | "GasEstimation_BelowMinLevPos" => Some(
582 "Increase position size or reduce leverage to meet minimum requirements"
583 .to_string(),
584 ),
585 "WrongTP" | "GasEstimation_WrongTP" => Some(
586 "Check that take profit price is reasonable relative to entry price".to_string(),
587 ),
588 "WrongSL" | "GasEstimation_WrongSL" => {
589 Some("Check that stop loss price is reasonable relative to entry price".to_string())
590 }
591 "NoTradeFound" | "GasEstimation_NoTradeFound" => {
592 Some("Verify the trade ID and ensure the position still exists".to_string())
593 }
594 "AlreadyMarketClosed" | "GasEstimation_AlreadyMarketClosed" => {
595 Some("This position has already been closed".to_string())
596 }
597 "NoLimitFound" | "GasEstimation_NoLimitFound" => {
598 Some("The limit order may have been executed or cancelled".to_string())
599 }
600 "ExposureLimits" | "GasEstimation_ExposureLimits" => {
601 Some("Reduce position size to stay within exposure limits".to_string())
602 }
603 "InsufficientFunds" => {
604 Some("Ensure sufficient USDC balance and allowance for the trade".to_string())
605 }
606 "OutOfGas" => Some("Increase gas limit for the transaction".to_string()),
607 _ => None,
608 };
609
610 if let Some(ref suggestion_text) = suggestion {
611 error_data.insert("suggestion".to_string(), suggestion_text.clone());
612 }
613
614 (error_message, error_type, error_data, suggestion)
615 }
616
617 pub async fn get_minimum_position_size(&self, symbol: &str) -> Result<Decimal> {
620 debug!("Getting minimum position size for symbol: {}", symbol);
621
622 match self.get_contract_minimum_size(symbol).await {
624 Ok(min_size) => Ok(min_size),
625 Err(_) => {
626 self.get_fallback_minimum_size(symbol)
628 }
629 }
630 }
631
632 pub async fn validate_trading_constraints(
635 &self,
636 symbol: &str,
637 side: PositionSide,
638 size: Decimal,
639 leverage: Decimal,
640 ) -> Result<()> {
641 debug!(
642 "Validating trading constraints for {} {} {} at {}x leverage",
643 symbol,
644 match side {
645 PositionSide::Long => "Long",
646 PositionSide::Short => "Short",
647 },
648 size,
649 leverage
650 );
651
652 self.validate_trading_hours(symbol).await?;
654
655 self.validate_minimum_position_size(symbol, size, leverage)
657 .await?;
658
659 self.validate_open_interest_caps(symbol, side, size, leverage)
661 .await?;
662
663 Ok(())
664 }
665
666 async fn get_contract_minimum_size(&self, symbol: &str) -> Result<Decimal> {
668 let pairs = self.get_pairs().await?;
670
671 for pair in pairs {
672 if pair.symbol == symbol {
673 return Ok(self.calculate_minimum_size_from_pair(&pair));
676 }
677 }
678
679 Err(OstiumError::validation(format!(
680 "Symbol {} not found",
681 symbol
682 )))
683 }
684
685 fn calculate_minimum_size_from_pair(&self, pair: &crate::types::TradingPair) -> Decimal {
687 if pair.symbol.starts_with("BTC") {
690 dec!(0.0001) } else if pair.symbol.starts_with("ETH") {
692 dec!(0.001) } else if pair.symbol.contains("USD") {
694 dec!(1.0) } else {
696 dec!(0.01) }
698 }
699
700 fn get_fallback_minimum_size(&self, symbol: &str) -> Result<Decimal> {
702 let min_size = if symbol.starts_with("BTC") {
703 dec!(0.0001) } else if symbol.starts_with("ETH") {
705 dec!(0.001) } else if symbol.starts_with("SOL") {
707 dec!(0.01) } else if symbol.contains("EUR") || symbol.contains("GBP") || symbol.contains("JPY") {
709 dec!(1000.0) } else if symbol.contains("GOLD") || symbol.contains("SILVER") {
712 dec!(0.01) } else if symbol.contains("SPX") || symbol.contains("NAS") {
714 dec!(0.1) } else {
716 dec!(1.0) };
718
719 Ok(min_size)
720 }
721
722 async fn validate_trading_hours(&self, symbol: &str) -> Result<()> {
724 match self.get_trading_hours(symbol).await {
725 Ok(hours) => {
726 if !hours.is_open {
727 return Err(OstiumError::validation(format!(
728 "Market is closed for {}. {}",
729 symbol,
730 if let Some(next_open) = hours.next_open {
731 format!("Next opening: {}", next_open)
732 } else {
733 "Trading hours: Please check market schedule".to_string()
734 }
735 )));
736 }
737 Ok(())
738 }
739 Err(_e) => {
740 if symbol.contains("BTC") || symbol.contains("ETH") || symbol.contains("SOL") {
742 Ok(()) } else {
744 warn!("Could not verify trading hours for {}", symbol);
747 Ok(())
748 }
749 }
750 }
751 }
752
753 async fn validate_minimum_position_size(
755 &self,
756 symbol: &str,
757 size: Decimal,
758 leverage: Decimal,
759 ) -> Result<()> {
760 let min_size = self.get_minimum_position_size(symbol).await?;
761
762 if size < min_size {
763 let price_info = match self.get_price(symbol).await {
765 Ok(price) => {
766 let min_collateral = min_size * price.mark_price / leverage;
767 format!(
768 "\n\nCurrent {} price: ${:.2}\nMinimum collateral required: ${:.2} USDC\n\nSolutions:\n• Increase position size to at least {} {}\n• Use higher leverage to reduce collateral requirements",
769 symbol,
770 price.mark_price,
771 min_collateral,
772 min_size,
773 symbol.split('/').next().unwrap_or("units")
774 )
775 }
776 Err(_) => format!(
777 "\n\nSolutions:\n• Increase position size to at least {} {}\n• Check minimum collateral requirements (typically 7+ USDC)",
778 min_size,
779 symbol.split('/').next().unwrap_or("units")
780 )
781 };
782
783 return Err(OstiumError::validation(format!(
784 "Position size {} is below minimum required size of {} for {}.{}",
785 size, min_size, symbol, price_info
786 )));
787 }
788
789 Ok(())
790 }
791
792 async fn validate_open_interest_caps(
794 &self,
795 symbol: &str,
796 side: PositionSide,
797 size: Decimal,
798 leverage: Decimal,
799 ) -> Result<()> {
800 let price = match self.get_price(symbol).await {
802 Ok(p) => p.mark_price,
803 Err(_) => {
804 warn!(
806 "Could not get price for {} to validate open interest caps",
807 symbol
808 );
809 return Ok(());
810 }
811 };
812
813 let notional_value = size * price * leverage;
814
815 let max_single_position = match symbol {
817 s if s.contains("BTC") => dec!(10_000_000), s if s.contains("ETH") => dec!(5_000_000), s if s.contains("SOL") => dec!(1_000_000), _ => dec!(2_000_000), };
822
823 if notional_value > max_single_position {
824 return Err(OstiumError::validation(format!(
825 "Position notional value ${:.2} exceeds maximum allowed exposure of ${:.2} for {}.\n\nSolutions:\n• Reduce position size\n• Use lower leverage\n• Split into multiple smaller positions",
826 notional_value,
827 max_single_position,
828 symbol
829 )));
830 }
831
832 let market_impact_threshold = max_single_position / dec!(2); if notional_value > market_impact_threshold {
835 warn!(
836 "Large position detected: ${:.2} notional value for {} {} position",
837 notional_value,
838 symbol,
839 match side {
840 PositionSide::Long => "Long",
841 PositionSide::Short => "Short",
842 }
843 );
844 }
845
846 Ok(())
847 }
848}
849
850impl OstiumClient {
852 pub async fn open_position(&self, params: OpenPositionParams) -> Result<TxHash> {
854 if !self.has_signer() {
855 return Err(OstiumError::wallet("No signer configured"));
856 }
857
858 debug!("Opening position: {:?}", params);
859
860 let trader = self.signer_address().unwrap();
861
862 let storage = self.trading_storage_contract();
864 let (base, quote) = params.symbol.split_once('/').ok_or_else(|| {
865 OstiumError::validation("Invalid symbol format, expected 'BASE/QUOTE'".to_string())
866 })?;
867
868 let pair_index = storage.get_pair_index(base, quote).await?;
869
870 let collateral = self.decimal_to_u256(params.size / params.leverage, 6)?;
872
873 let tp = match params.take_profit {
875 Some(p) => self.decimal_to_u192(p)?,
876 None => U192::ZERO,
877 };
878 let sl = match params.stop_loss {
879 Some(p) => self.decimal_to_u192(p)?,
880 None => U192::ZERO,
881 };
882
883 let trade = Trade {
884 collateral,
885 open_price: 0, tp: tp.try_into().map_err(|e| {
887 OstiumError::conversion(format!("Failed to convert take profit price: {}", e))
888 })?,
889 sl: sl.try_into().map_err(|e| {
890 OstiumError::conversion(format!("Failed to convert stop loss price: {}", e))
891 })?,
892 trader,
893 leverage: (params.leverage * Decimal::from(100))
894 .to_u32()
895 .ok_or_else(|| OstiumError::validation("Leverage value too large".to_string()))?,
896 pair_index,
897 index: 0, buy: params.side == PositionSide::Long,
899 };
900
901 let slippage_p = self.decimal_to_u256(params.slippage_tolerance * Decimal::from(100), 0)?; let trading = self.trading_contract();
904 trading
905 .open_trade(trade, OpenOrderType::Market, slippage_p)
906 .await?;
907
908 Ok(alloy_primitives::TxHash::ZERO)
911 }
912
913 pub async fn close_position(&self, params: ClosePositionParams) -> Result<TxHash> {
915 if !self.has_signer() {
916 return Err(OstiumError::wallet("No signer configured"));
917 }
918
919 debug!("Closing position: {:?}", params);
920
921 let parts: Vec<&str> = params.position_id.split(':').collect();
924 if parts.len() != 3 {
925 return Err(OstiumError::validation(
926 "Invalid position ID format".to_string(),
927 ));
928 }
929
930 let pair_index: u16 = parts[1].parse().map_err(|_| {
931 OstiumError::validation("Invalid pair index in position ID".to_string())
932 })?;
933 let index: u8 = parts[2]
934 .parse()
935 .map_err(|_| OstiumError::validation("Invalid index in position ID".to_string()))?;
936
937 let close_percentage = if let Some(_size) = params.size {
938 warn!("Partial close not fully implemented, closing 100%");
940 10000 } else {
942 10000 };
944
945 let trading = self.trading_contract();
946 trading
947 .close_trade_market(pair_index, index, close_percentage)
948 .await?;
949
950 Ok(alloy_primitives::TxHash::ZERO)
951 }
952
953 pub async fn update_tp_sl(&self, params: UpdateTPSLParams) -> Result<TxHash> {
955 if !self.has_signer() {
956 return Err(OstiumError::wallet("No signer configured"));
957 }
958
959 debug!("Updating TP/SL: {:?}", params);
960
961 let parts: Vec<&str> = params.position_id.split(':').collect();
963 if parts.len() != 3 {
964 return Err(OstiumError::validation(
965 "Invalid position ID format".to_string(),
966 ));
967 }
968
969 let pair_index: u16 = parts[1].parse().map_err(|_| {
970 OstiumError::validation("Invalid pair index in position ID".to_string())
971 })?;
972 let index: u8 = parts[2]
973 .parse()
974 .map_err(|_| OstiumError::validation("Invalid index in position ID".to_string()))?;
975
976 let trading = self.trading_contract();
977
978 if let Some(tp) = params.take_profit {
980 let tp_u192 = self.decimal_to_u192(tp)?;
981 trading.update_tp(pair_index, index, tp_u192).await?;
982 }
983
984 if let Some(sl) = params.stop_loss {
986 let sl_u192 = self.decimal_to_u192(sl)?;
987 trading.update_sl(pair_index, index, sl_u192).await?;
988 }
989
990 Ok(alloy_primitives::TxHash::ZERO)
991 }
992
993 pub async fn open_position_unsigned(
997 &self,
998 params: OpenPositionParams,
999 trader_address: Address,
1000 tx_params: UnsignedTransactionParams,
1001 ) -> Result<UnsignedTransaction> {
1002 debug!(
1003 "Building unsigned transaction for opening position: {:?}",
1004 params
1005 );
1006
1007 let storage = self.trading_storage_contract();
1009 let (base, quote) = params.symbol.split_once('/').ok_or_else(|| {
1010 OstiumError::validation("Invalid symbol format, expected 'BASE/QUOTE'".to_string())
1011 })?;
1012
1013 let pair_index = storage.get_pair_index(base, quote).await?;
1014
1015 let collateral = self.decimal_to_u256(params.size / params.leverage, 6)?;
1017
1018 let tp = match params.take_profit {
1020 Some(p) => self.decimal_to_u192(p)?,
1021 None => U192::ZERO,
1022 };
1023 let sl = match params.stop_loss {
1024 Some(p) => self.decimal_to_u192(p)?,
1025 None => U192::ZERO,
1026 };
1027
1028 let trade = Trade {
1029 collateral,
1030 open_price: 0, tp: tp.try_into().map_err(|e| {
1032 OstiumError::conversion(format!("Failed to convert take profit price: {}", e))
1033 })?,
1034 sl: sl.try_into().map_err(|e| {
1035 OstiumError::conversion(format!("Failed to convert stop loss price: {}", e))
1036 })?,
1037 trader: trader_address,
1038 leverage: (params.leverage * Decimal::from(100))
1039 .to_u32()
1040 .ok_or_else(|| OstiumError::validation("Leverage value too large".to_string()))?,
1041 pair_index,
1042 index: 0, buy: params.side == PositionSide::Long,
1044 };
1045
1046 let slippage_p = self.decimal_to_u256(params.slippage_tolerance * Decimal::from(100), 0)?;
1047
1048 let trading = self.trading_contract();
1049 trading
1050 .open_trade_unsigned(trade, OpenOrderType::Market, slippage_p, tx_params)
1051 .await
1052 }
1053
1054 pub async fn close_position_unsigned(
1056 &self,
1057 params: ClosePositionParams,
1058 tx_params: UnsignedTransactionParams,
1059 ) -> Result<UnsignedTransaction> {
1060 debug!(
1061 "Building unsigned transaction for closing position: {:?}",
1062 params
1063 );
1064
1065 let parts: Vec<&str> = params.position_id.split(':').collect();
1067 if parts.len() != 3 {
1068 return Err(OstiumError::validation(
1069 "Invalid position ID format".to_string(),
1070 ));
1071 }
1072
1073 let pair_index: u16 = parts[1].parse().map_err(|_| {
1074 OstiumError::validation("Invalid pair index in position ID".to_string())
1075 })?;
1076 let index: u8 = parts[2]
1077 .parse()
1078 .map_err(|_| OstiumError::validation("Invalid index in position ID".to_string()))?;
1079
1080 let close_percentage = if let Some(_size) = params.size {
1081 warn!("Partial close not fully implemented, closing 100%");
1083 10000 } else {
1085 10000 };
1087
1088 let trading = self.trading_contract();
1089 trading
1090 .close_trade_market_unsigned(pair_index, index, close_percentage, tx_params)
1091 .await
1092 }
1093
1094 pub async fn update_tp_sl_unsigned(
1096 &self,
1097 params: UpdateTPSLParams,
1098 tx_params: UnsignedTransactionParams,
1099 ) -> Result<Vec<UnsignedTransaction>> {
1100 debug!(
1101 "Building unsigned transactions for updating TP/SL: {:?}",
1102 params
1103 );
1104
1105 let parts: Vec<&str> = params.position_id.split(':').collect();
1107 if parts.len() != 3 {
1108 return Err(OstiumError::validation(
1109 "Invalid position ID format".to_string(),
1110 ));
1111 }
1112
1113 let pair_index: u16 = parts[1].parse().map_err(|_| {
1114 OstiumError::validation("Invalid pair index in position ID".to_string())
1115 })?;
1116 let index: u8 = parts[2]
1117 .parse()
1118 .map_err(|_| OstiumError::validation("Invalid index in position ID".to_string()))?;
1119
1120 let trading = self.trading_contract();
1121 let mut transactions = Vec::new();
1122
1123 if let Some(tp) = params.take_profit {
1125 let tp_u192 = self.decimal_to_u192(tp)?;
1126 let tx = trading
1127 .update_tp_unsigned(pair_index, index, tp_u192, tx_params.clone())
1128 .await?;
1129 transactions.push(tx);
1130 }
1131
1132 if let Some(sl) = params.stop_loss {
1134 let sl_u192 = self.decimal_to_u192(sl)?;
1135 let tx = trading
1136 .update_sl_unsigned(pair_index, index, sl_u192, tx_params.clone())
1137 .await?;
1138 transactions.push(tx);
1139 }
1140
1141 Ok(transactions)
1142 }
1143
1144 pub async fn place_advanced_order_unsigned(
1146 &self,
1147 params: AdvancedOrderParams,
1148 trader_address: Address,
1149 tx_params: UnsignedTransactionParams,
1150 ) -> Result<UnsignedTransaction> {
1151 debug!(
1152 "Building unsigned transaction for advanced order: {:?}",
1153 params
1154 );
1155
1156 match params.order_type {
1158 OrderExecutionType::Limit | OrderExecutionType::Stop => {
1159 if params.price.is_none() {
1160 return Err(OstiumError::validation(
1161 "Price is required for limit and stop orders".to_string(),
1162 ));
1163 }
1164 }
1165 OrderExecutionType::Market => {
1166 }
1168 }
1169
1170 let storage = self.trading_storage_contract();
1172 let (base, quote) = params.symbol.split_once('/').ok_or_else(|| {
1173 OstiumError::validation("Invalid symbol format, expected 'BASE/QUOTE'".to_string())
1174 })?;
1175
1176 let pair_index = storage.get_pair_index(base, quote).await?;
1177
1178 let collateral = self.decimal_to_u256(params.size / params.leverage, 6)?;
1180
1181 let tp = self.convert_optional_price(params.take_profit)?;
1183 let sl = self.convert_optional_price(params.stop_loss)?;
1184
1185 let open_price = if let Some(price) = params.price {
1187 self.decimal_to_u192(price)?.try_into().unwrap_or(0)
1188 } else {
1189 0 };
1191
1192 let trade = Trade {
1193 collateral,
1194 open_price,
1195 tp: tp.try_into().unwrap_or(0),
1196 sl: sl.try_into().unwrap_or(0),
1197 trader: trader_address,
1198 leverage: (params.leverage * Decimal::from(100))
1199 .to_u32()
1200 .unwrap_or(100),
1201 pair_index,
1202 index: 0,
1203 buy: params.side == PositionSide::Long,
1204 };
1205
1206 let order_type = match params.order_type {
1208 OrderExecutionType::Market => OpenOrderType::Market,
1209 OrderExecutionType::Limit => OpenOrderType::Limit,
1210 OrderExecutionType::Stop => OpenOrderType::Stop,
1211 };
1212
1213 let slippage_p = self.decimal_to_u256(params.slippage_tolerance * Decimal::from(100), 0)?;
1214
1215 let trading = self.trading_contract();
1216 trading
1217 .open_trade_unsigned(trade, order_type, slippage_p, tx_params)
1218 .await
1219 }
1220
1221 pub async fn cancel_order_unsigned(
1223 &self,
1224 params: CancelOrderParams,
1225 tx_params: UnsignedTransactionParams,
1226 ) -> Result<UnsignedTransaction> {
1227 debug!(
1228 "Building unsigned transaction for canceling order: {:?}",
1229 params
1230 );
1231
1232 let parts: Vec<&str> = params.order_id.split(':').collect();
1234 if parts.len() != 3 {
1235 return Err(OstiumError::validation(
1236 "Invalid order ID format, expected 'trader:pair_index:index'".to_string(),
1237 ));
1238 }
1239
1240 let pair_index: u16 = parts[1]
1241 .parse()
1242 .map_err(|_| OstiumError::validation("Invalid pair index in order ID".to_string()))?;
1243 let index: u8 = parts[2]
1244 .parse()
1245 .map_err(|_| OstiumError::validation("Invalid index in order ID".to_string()))?;
1246
1247 let trading = self.trading_contract();
1248 trading
1249 .cancel_open_limit_order_unsigned(pair_index, index, tx_params)
1250 .await
1251 }
1252}
1253
1254impl OstiumClient {
1256 pub async fn place_advanced_order(&self, params: AdvancedOrderParams) -> Result<TxHash> {
1258 if !self.has_signer() {
1259 return Err(OstiumError::wallet("No signer configured"));
1260 }
1261
1262 debug!("Placing advanced order: {:?}", params);
1263
1264 match params.order_type {
1266 OrderExecutionType::Limit | OrderExecutionType::Stop => {
1267 if params.price.is_none() {
1268 return Err(OstiumError::validation(
1269 "Price is required for limit and stop orders".to_string(),
1270 ));
1271 }
1272 }
1273 OrderExecutionType::Market => {
1274 }
1276 }
1277
1278 let trader = self.signer_address().unwrap();
1279
1280 let storage = self.trading_storage_contract();
1282 let (base, quote) = params.symbol.split_once('/').ok_or_else(|| {
1283 OstiumError::validation("Invalid symbol format, expected 'BASE/QUOTE'".to_string())
1284 })?;
1285
1286 let pair_index = storage.get_pair_index(base, quote).await?;
1287
1288 let collateral = self.decimal_to_u256(params.size / params.leverage, 6)?;
1290
1291 let tp = self.convert_optional_price(params.take_profit)?;
1293 let sl = self.convert_optional_price(params.stop_loss)?;
1294
1295 let open_price = if let Some(price) = params.price {
1297 self.decimal_to_u192(price)?.try_into().unwrap_or(0)
1298 } else {
1299 0 };
1301
1302 let trade = Trade {
1303 collateral,
1304 open_price,
1305 tp: tp.try_into().unwrap_or(0),
1306 sl: sl.try_into().unwrap_or(0),
1307 trader,
1308 leverage: (params.leverage * Decimal::from(100))
1309 .to_u32()
1310 .unwrap_or(100), pair_index,
1312 index: 0, buy: params.side == PositionSide::Long,
1314 };
1315
1316 let order_type = match params.order_type {
1318 OrderExecutionType::Market => OpenOrderType::Market,
1319 OrderExecutionType::Limit => OpenOrderType::Limit,
1320 OrderExecutionType::Stop => OpenOrderType::Stop,
1321 };
1322
1323 let slippage_p = self.decimal_to_u256(params.slippage_tolerance * Decimal::from(100), 0)?;
1324
1325 let trading = self.trading_contract();
1326 trading.open_trade(trade, order_type, slippage_p).await?;
1327
1328 Ok(alloy_primitives::TxHash::ZERO)
1329 }
1330
1331 pub async fn place_limit_order(&self, params: LimitOrderParams) -> Result<TxHash> {
1333 let advanced_params = AdvancedOrderParams {
1334 symbol: params.symbol,
1335 side: params.side,
1336 size: params.size,
1337 leverage: params.leverage,
1338 order_type: OrderExecutionType::Limit,
1339 price: Some(params.limit_price),
1340 take_profit: params.take_profit,
1341 stop_loss: params.stop_loss,
1342 slippage_tolerance: Decimal::from(2) / Decimal::from(100), };
1344
1345 self.place_advanced_order(advanced_params).await
1346 }
1347
1348 pub async fn place_stop_order(&self, params: StopOrderParams) -> Result<TxHash> {
1350 let advanced_params = AdvancedOrderParams {
1351 symbol: params.symbol,
1352 side: params.side,
1353 size: params.size,
1354 leverage: params.leverage,
1355 order_type: OrderExecutionType::Stop,
1356 price: Some(params.stop_price),
1357 take_profit: params.take_profit,
1358 stop_loss: params.stop_loss,
1359 slippage_tolerance: Decimal::from(2) / Decimal::from(100), };
1361
1362 self.place_advanced_order(advanced_params).await
1363 }
1364
1365 pub async fn cancel_order(&self, params: CancelOrderParams) -> Result<TxHash> {
1367 if !self.has_signer() {
1368 return Err(OstiumError::wallet("No signer configured"));
1369 }
1370
1371 debug!("Canceling order: {:?}", params);
1372
1373 let parts: Vec<&str> = params.order_id.split(':').collect();
1375 if parts.len() != 3 {
1376 return Err(OstiumError::validation(
1377 "Invalid order ID format, expected 'trader:pair_index:index'".to_string(),
1378 ));
1379 }
1380
1381 let pair_index: u16 = parts[1]
1382 .parse()
1383 .map_err(|_| OstiumError::validation("Invalid pair index in order ID".to_string()))?;
1384 let index: u8 = parts[2]
1385 .parse()
1386 .map_err(|_| OstiumError::validation("Invalid index in order ID".to_string()))?;
1387
1388 let trading = self.trading_contract();
1389 trading.cancel_open_limit_order(pair_index, index).await?;
1390
1391 Ok(alloy_primitives::TxHash::ZERO)
1392 }
1393
1394 pub async fn update_limit_order(&self, params: UpdateLimitOrderParams) -> Result<TxHash> {
1396 if !self.has_signer() {
1397 return Err(OstiumError::wallet("No signer configured"));
1398 }
1399
1400 debug!("Updating limit order: {:?}", params);
1401
1402 let parts: Vec<&str> = params.order_id.split(':').collect();
1404 if parts.len() != 3 {
1405 return Err(OstiumError::validation(
1406 "Invalid order ID format, expected 'trader:pair_index:index'".to_string(),
1407 ));
1408 }
1409
1410 let pair_index: u16 = parts[1]
1411 .parse()
1412 .map_err(|_| OstiumError::validation("Invalid pair index in order ID".to_string()))?;
1413 let index: u8 = parts[2]
1414 .parse()
1415 .map_err(|_| OstiumError::validation("Invalid index in order ID".to_string()))?;
1416
1417 let storage = self.trading_storage_contract();
1419 let trader = self.signer_address().unwrap();
1420 let current_order = storage
1421 .get_open_limit_order(trader, pair_index, index)
1422 .await?;
1423
1424 let new_price = match params.limit_price {
1426 Some(p) => self.decimal_to_u192(p)?,
1427 None => U192::from(current_order.target_price),
1428 };
1429
1430 let new_tp = match params.take_profit {
1431 Some(p) => self.decimal_to_u192(p)?,
1432 None => U192::from(current_order.tp),
1433 };
1434
1435 let new_sl = match params.stop_loss {
1436 Some(p) => self.decimal_to_u192(p)?,
1437 None => U192::from(current_order.sl),
1438 };
1439
1440 let trading = self.trading_contract();
1441 trading
1442 .update_open_limit_order(pair_index, index, new_price, new_tp, new_sl)
1443 .await?;
1444
1445 Ok(alloy_primitives::TxHash::ZERO)
1446 }
1447
1448 pub async fn validate_order_price(
1450 &self,
1451 symbol: &str,
1452 order_type: OrderExecutionType,
1453 price: Decimal,
1454 ) -> Result<bool> {
1455 let current_price = self.get_price(symbol).await?.mark_price;
1456
1457 match order_type {
1458 OrderExecutionType::Market => Ok(true), OrderExecutionType::Limit => {
1460 let price_diff = (price - current_price).abs() / current_price;
1462 Ok(price_diff <= Decimal::from(50) / Decimal::from(100)) }
1464 OrderExecutionType::Stop => {
1465 let price_diff = (price - current_price).abs() / current_price;
1467 Ok(price_diff <= Decimal::from(50) / Decimal::from(100)) }
1469 }
1470 }
1471}
1472
1473impl OstiumClient {
1475 pub async fn get_pairs(&self) -> Result<Vec<TradingPair>> {
1477 debug!("Fetching trading pairs");
1478
1479 let query = r#"
1480 query GetTradingPairs {
1481 pairs {
1482 id
1483 from
1484 to
1485 feed
1486 spreadP
1487 maxLeverage
1488 volume
1489 }
1490 }
1491 "#;
1492
1493 let response = self.graphql_query(query, None).await?;
1494
1495 let pairs = response
1497 .get("pairs")
1498 .and_then(|p| p.as_array())
1499 .ok_or_else(|| OstiumError::network("Invalid pairs response".to_string()))?;
1500
1501 let mut trading_pairs = Vec::new();
1502 for pair in pairs {
1503 let id = pair.get("id").and_then(|v| v.as_str()).ok_or_else(|| {
1504 OstiumError::network("Missing 'id' field in pair data".to_string())
1505 })?;
1506 let from = pair.get("from").and_then(|v| v.as_str()).ok_or_else(|| {
1507 OstiumError::network("Missing 'from' field in pair data".to_string())
1508 })?;
1509 let to = pair.get("to").and_then(|v| v.as_str()).ok_or_else(|| {
1510 OstiumError::network("Missing 'to' field in pair data".to_string())
1511 })?;
1512 let max_leverage = pair
1513 .get("maxLeverage")
1514 .and_then(|v| v.as_str())
1515 .and_then(|s| s.parse::<u64>().ok())
1516 .unwrap_or(1);
1517
1518 trading_pairs.push(TradingPair {
1519 id: id.to_string(),
1520 base_asset: from.to_string(),
1521 quote_asset: to.to_string(),
1522 symbol: format!("{}/{}", from, to),
1523 is_active: max_leverage > 0, min_position_size: Decimal::from(1), max_position_size: Decimal::from(1000000),
1526 price_precision: 8,
1527 quantity_precision: 8,
1528 });
1529 }
1530
1531 Ok(trading_pairs)
1532 }
1533
1534 pub async fn get_price(&self, symbol: &str) -> Result<Price> {
1536 debug!("Fetching price for symbol: {}", symbol);
1537
1538 let rest_url = "https://metadata-backend.ostium.io/PricePublish/latest-price";
1540 let asset = symbol.replace("/", ""); let url = format!("{}?asset={}", rest_url, asset);
1542
1543 match self.rest_api_call(url).await {
1544 Ok(price_data) => {
1545 if let (Some(mid), Some(bid), Some(ask)) = (
1548 price_data.get("mid").and_then(|p| p.as_f64()),
1549 price_data.get("bid").and_then(|p| p.as_f64()),
1550 price_data.get("ask").and_then(|p| p.as_f64()),
1551 ) {
1552 let mark_price = Decimal::try_from(mid).map_err(|e| {
1553 OstiumError::network(format!("Invalid price format: {}", e))
1554 })?;
1555 let _bid_price = Decimal::try_from(bid)
1556 .map_err(|e| OstiumError::network(format!("Invalid bid format: {}", e)))?;
1557 let _ask_price = Decimal::try_from(ask)
1558 .map_err(|e| OstiumError::network(format!("Invalid ask format: {}", e)))?;
1559
1560 let high_24h = mark_price * Decimal::try_from(1.02).unwrap();
1562 let low_24h = mark_price * Decimal::try_from(0.98).unwrap();
1563
1564 return Ok(Price {
1565 symbol: symbol.to_string(),
1566 mark_price,
1567 index_price: mark_price, high_24h,
1569 low_24h,
1570 volume_24h: Decimal::from(1000000), timestamp: chrono::Utc::now(),
1572 });
1573 }
1574 }
1575 Err(e) => {
1576 debug!("Failed to fetch price from REST API: {}", e);
1577 }
1579 }
1580
1581 let pairs = self.get_pairs().await?;
1583 let pair = pairs
1584 .iter()
1585 .find(|p| p.symbol == symbol)
1586 .ok_or_else(|| OstiumError::network(format!("Pair {} not found", symbol)))?;
1587
1588 Ok(Price {
1590 symbol: pair.symbol.clone(),
1591 mark_price: Decimal::from(50000), index_price: Decimal::from(50000),
1593 high_24h: Decimal::from(52000),
1594 low_24h: Decimal::from(48000),
1595 volume_24h: Decimal::from(1000000),
1596 timestamp: chrono::Utc::now(),
1597 })
1598 }
1599
1600 pub async fn get_trading_hours(&self, symbol: &str) -> Result<TradingHours> {
1602 debug!("Fetching trading hours for symbol: {}", symbol);
1603
1604 let rest_url = "https://metadata-backend.ostium.io/trading-hours/asset-schedule";
1606 let asset = symbol.replace("/", ""); let url = format!("{}?asset={}", rest_url, asset);
1608
1609 match self.rest_api_call(url).await {
1610 Ok(hours_data) => {
1611 if let Some(error) = hours_data.get("error") {
1613 debug!("Trading hours API error: {}", error);
1614 } else {
1616 let is_open = hours_data
1618 .get("isOpenNow")
1619 .and_then(|v| v.as_bool())
1620 .unwrap_or(true); return Ok(TradingHours {
1623 symbol: symbol.to_string(),
1624 is_open,
1625 next_open: None, next_close: None,
1627 });
1628 }
1629 }
1630 Err(e) => {
1631 debug!("Failed to fetch trading hours from REST API: {}", e);
1632 }
1634 }
1635
1636 let is_crypto = symbol.contains("BTC")
1639 || symbol.contains("ETH")
1640 || symbol.contains("SOL")
1641 || symbol.contains("COIN");
1642
1643 Ok(TradingHours {
1644 symbol: symbol.to_string(),
1645 is_open: is_crypto, next_open: None,
1647 next_close: None,
1648 })
1649 }
1650}
1651
1652impl OstiumClient {
1654 pub async fn get_balance(&self, address: Option<Address>) -> Result<Balance> {
1656 let account = address
1657 .or_else(|| self.signer_address())
1658 .ok_or_else(|| OstiumError::wallet("No address provided and no signer configured"))?;
1659
1660 debug!("Fetching balance for address: {}", account);
1661
1662 let usdc = self.usdc_contract();
1663 let balance = usdc.balance_of(account).await?;
1664
1665 let balance_decimal = Decimal::from(balance.to::<u128>()) / Decimal::from(1_000_000);
1667
1668 Ok(Balance {
1669 asset: "USDC".to_string(),
1670 available: balance_decimal,
1671 locked: Decimal::ZERO, total: balance_decimal,
1673 })
1674 }
1675
1676 pub async fn get_positions(&self, address: Option<Address>) -> Result<Vec<Position>> {
1678 let account = address
1679 .or_else(|| self.signer_address())
1680 .ok_or_else(|| OstiumError::wallet("No address provided and no signer configured"))?;
1681
1682 debug!("Fetching positions for address: {}", account);
1683
1684 let storage = self.trading_storage_contract();
1685 let mut positions = Vec::new();
1686
1687 let pairs = self.get_pairs().await?;
1689
1690 for (pair_index, pair) in pairs.iter().enumerate() {
1691 let pair_index = pair_index as u16;
1692
1693 match storage.get_trades_count(account, pair_index).await {
1695 Ok(count) => {
1696 for index in 0..count {
1698 match storage.get_trade(account, pair_index, index).await {
1699 Ok(trade) => {
1700 let position = Position {
1702 id: format!("{}:{}:{}", account, pair_index, index),
1703 symbol: pair.symbol.clone(),
1704 side: if trade.buy {
1705 PositionSide::Long
1706 } else {
1707 PositionSide::Short
1708 },
1709 size: Decimal::from(trade.collateral.to::<u128>())
1710 / Decimal::from(1_000_000), entry_price: Decimal::from(trade.open_price)
1712 / Decimal::from(10_u128.pow(18)), mark_price: Decimal::ZERO, unrealized_pnl: Decimal::ZERO, realized_pnl: Decimal::ZERO, margin: Decimal::from(trade.collateral.to::<u128>())
1717 / Decimal::from(1_000_000),
1718 leverage: Decimal::from(trade.leverage) / Decimal::from(100), liquidation_price: None, take_profit: if trade.tp > 0 {
1721 Some(
1722 Decimal::from(trade.tp)
1723 / Decimal::from(10_u128.pow(18)),
1724 )
1725 } else {
1726 None
1727 },
1728 stop_loss: if trade.sl > 0 {
1729 Some(
1730 Decimal::from(trade.sl)
1731 / Decimal::from(10_u128.pow(18)),
1732 )
1733 } else {
1734 None
1735 },
1736 created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(),
1738 };
1739 positions.push(position);
1740 }
1741 Err(_) => {
1742 continue;
1744 }
1745 }
1746 }
1747 }
1748 Err(_) => {
1749 continue;
1751 }
1752 }
1753 }
1754
1755 Ok(positions)
1756 }
1757
1758 pub async fn get_orders(&self, address: Option<Address>) -> Result<Vec<Order>> {
1760 let account = address
1761 .or_else(|| self.signer_address())
1762 .ok_or_else(|| OstiumError::wallet("No address provided and no signer configured"))?;
1763
1764 debug!("Fetching orders for address: {}", account);
1765
1766 let storage = self.trading_storage_contract();
1767 let mut orders = Vec::new();
1768
1769 let pairs = self.get_pairs().await?;
1771
1772 for (pair_index, pair) in pairs.iter().enumerate() {
1773 let pair_index = pair_index as u16;
1774
1775 match storage
1777 .get_open_limit_orders_count(account, pair_index)
1778 .await
1779 {
1780 Ok(count) => {
1781 for index in 0..count {
1783 match storage
1784 .get_open_limit_order(account, pair_index, index)
1785 .await
1786 {
1787 Ok(limit_order) => {
1788 let order_type = match limit_order.order_type {
1790 1 => OrderType::Limit,
1791 2 => OrderType::StopMarket,
1792 _ => OrderType::Market,
1793 };
1794
1795 let order = Order {
1796 id: format!("{}:{}:{}", account, pair_index, index),
1797 symbol: pair.symbol.clone(),
1798 order_type,
1799 side: if limit_order.buy {
1800 PositionSide::Long
1801 } else {
1802 PositionSide::Short
1803 },
1804 size: Decimal::from(limit_order.collateral.to::<u128>())
1805 / Decimal::from(1_000_000), price: Some(
1807 Decimal::from(limit_order.target_price)
1808 / Decimal::from(10_u128.pow(18)),
1809 ), stop_price: None, status: OrderStatus::Pending, filled_size: Decimal::ZERO, avg_fill_price: None,
1814 created_at: chrono::DateTime::from_timestamp(
1815 limit_order.created_at as i64,
1816 0,
1817 )
1818 .unwrap_or_else(chrono::Utc::now),
1819 updated_at: chrono::DateTime::from_timestamp(
1820 limit_order.last_updated as i64,
1821 0,
1822 )
1823 .unwrap_or_else(chrono::Utc::now),
1824 };
1825 orders.push(order);
1826 }
1827 Err(_) => {
1828 continue;
1830 }
1831 }
1832 }
1833 }
1834 Err(_) => {
1835 continue;
1837 }
1838 }
1839 }
1840
1841 Ok(orders)
1842 }
1843}
1844
1845#[allow(async_fn_in_trait)]
1847pub trait TradingApi {
1848 async fn open_position(&self, params: OpenPositionParams) -> Result<TxHash>;
1850
1851 async fn close_position(&self, params: ClosePositionParams) -> Result<TxHash>;
1853
1854 async fn update_tp_sl(&self, params: UpdateTPSLParams) -> Result<TxHash>;
1856}
1857
1858#[allow(async_fn_in_trait)]
1860pub trait MarketDataApi {
1861 async fn get_pairs(&self) -> Result<Vec<TradingPair>>;
1863
1864 async fn get_price(&self, symbol: &str) -> Result<Price>;
1866
1867 async fn get_trading_hours(&self, symbol: &str) -> Result<TradingHours>;
1869}
1870
1871#[allow(async_fn_in_trait)]
1873pub trait AccountApi {
1874 async fn get_balance(&self, address: Option<Address>) -> Result<Balance>;
1876
1877 async fn get_positions(&self, address: Option<Address>) -> Result<Vec<Position>>;
1879
1880 async fn get_orders(&self, address: Option<Address>) -> Result<Vec<Order>>;
1882}
1883
1884#[allow(async_fn_in_trait)]
1886pub trait AdvancedOrderApi {
1887 async fn place_advanced_order(&self, params: AdvancedOrderParams) -> Result<TxHash>;
1889
1890 async fn place_limit_order(&self, params: LimitOrderParams) -> Result<TxHash>;
1892
1893 async fn place_stop_order(&self, params: StopOrderParams) -> Result<TxHash>;
1895
1896 async fn cancel_order(&self, params: CancelOrderParams) -> Result<TxHash>;
1898
1899 async fn update_limit_order(&self, params: UpdateLimitOrderParams) -> Result<TxHash>;
1901
1902 async fn validate_order_price(
1904 &self,
1905 symbol: &str,
1906 order_type: OrderExecutionType,
1907 price: Decimal,
1908 ) -> Result<bool>;
1909}
1910
1911#[allow(async_fn_in_trait)]
1913pub trait UnsignedTransactionApi {
1914 async fn open_position_unsigned(
1916 &self,
1917 params: OpenPositionParams,
1918 trader_address: Address,
1919 tx_params: UnsignedTransactionParams,
1920 ) -> Result<UnsignedTransaction>;
1921
1922 async fn close_position_unsigned(
1924 &self,
1925 params: ClosePositionParams,
1926 tx_params: UnsignedTransactionParams,
1927 ) -> Result<UnsignedTransaction>;
1928
1929 async fn update_tp_sl_unsigned(
1931 &self,
1932 params: UpdateTPSLParams,
1933 tx_params: UnsignedTransactionParams,
1934 ) -> Result<Vec<UnsignedTransaction>>;
1935
1936 async fn place_advanced_order_unsigned(
1938 &self,
1939 params: AdvancedOrderParams,
1940 trader_address: Address,
1941 tx_params: UnsignedTransactionParams,
1942 ) -> Result<UnsignedTransaction>;
1943
1944 async fn cancel_order_unsigned(
1946 &self,
1947 params: CancelOrderParams,
1948 tx_params: UnsignedTransactionParams,
1949 ) -> Result<UnsignedTransaction>;
1950}
1951
1952impl TradingApi for OstiumClient {
1954 async fn open_position(&self, params: OpenPositionParams) -> Result<TxHash> {
1955 OstiumClient::open_position(self, params).await
1956 }
1957
1958 async fn close_position(&self, params: ClosePositionParams) -> Result<TxHash> {
1959 OstiumClient::close_position(self, params).await
1960 }
1961
1962 async fn update_tp_sl(&self, params: UpdateTPSLParams) -> Result<TxHash> {
1963 OstiumClient::update_tp_sl(self, params).await
1964 }
1965}
1966
1967impl MarketDataApi for OstiumClient {
1968 async fn get_pairs(&self) -> Result<Vec<TradingPair>> {
1969 OstiumClient::get_pairs(self).await
1970 }
1971
1972 async fn get_price(&self, symbol: &str) -> Result<Price> {
1973 OstiumClient::get_price(self, symbol).await
1974 }
1975
1976 async fn get_trading_hours(&self, symbol: &str) -> Result<TradingHours> {
1977 OstiumClient::get_trading_hours(self, symbol).await
1978 }
1979}
1980
1981impl AccountApi for OstiumClient {
1982 async fn get_balance(&self, address: Option<Address>) -> Result<Balance> {
1983 OstiumClient::get_balance(self, address).await
1984 }
1985
1986 async fn get_positions(&self, address: Option<Address>) -> Result<Vec<Position>> {
1987 OstiumClient::get_positions(self, address).await
1988 }
1989
1990 async fn get_orders(&self, address: Option<Address>) -> Result<Vec<Order>> {
1991 OstiumClient::get_orders(self, address).await
1992 }
1993}
1994
1995impl AdvancedOrderApi for OstiumClient {
1996 async fn place_advanced_order(&self, params: AdvancedOrderParams) -> Result<TxHash> {
1997 self.place_advanced_order(params).await
1998 }
1999
2000 async fn place_limit_order(&self, params: LimitOrderParams) -> Result<TxHash> {
2001 self.place_limit_order(params).await
2002 }
2003
2004 async fn place_stop_order(&self, params: StopOrderParams) -> Result<TxHash> {
2005 self.place_stop_order(params).await
2006 }
2007
2008 async fn cancel_order(&self, params: CancelOrderParams) -> Result<TxHash> {
2009 self.cancel_order(params).await
2010 }
2011
2012 async fn update_limit_order(&self, params: UpdateLimitOrderParams) -> Result<TxHash> {
2013 self.update_limit_order(params).await
2014 }
2015
2016 async fn validate_order_price(
2017 &self,
2018 symbol: &str,
2019 order_type: OrderExecutionType,
2020 price: Decimal,
2021 ) -> Result<bool> {
2022 self.validate_order_price(symbol, order_type, price).await
2023 }
2024}
2025
2026impl UnsignedTransactionApi for OstiumClient {
2027 async fn open_position_unsigned(
2028 &self,
2029 params: OpenPositionParams,
2030 trader_address: Address,
2031 tx_params: UnsignedTransactionParams,
2032 ) -> Result<UnsignedTransaction> {
2033 OstiumClient::open_position_unsigned(self, params, trader_address, tx_params).await
2034 }
2035
2036 async fn close_position_unsigned(
2037 &self,
2038 params: ClosePositionParams,
2039 tx_params: UnsignedTransactionParams,
2040 ) -> Result<UnsignedTransaction> {
2041 OstiumClient::close_position_unsigned(self, params, tx_params).await
2042 }
2043
2044 async fn update_tp_sl_unsigned(
2045 &self,
2046 params: UpdateTPSLParams,
2047 tx_params: UnsignedTransactionParams,
2048 ) -> Result<Vec<UnsignedTransaction>> {
2049 OstiumClient::update_tp_sl_unsigned(self, params, tx_params).await
2050 }
2051
2052 async fn place_advanced_order_unsigned(
2053 &self,
2054 params: AdvancedOrderParams,
2055 trader_address: Address,
2056 tx_params: UnsignedTransactionParams,
2057 ) -> Result<UnsignedTransaction> {
2058 OstiumClient::place_advanced_order_unsigned(self, params, trader_address, tx_params).await
2059 }
2060
2061 async fn cancel_order_unsigned(
2062 &self,
2063 params: CancelOrderParams,
2064 tx_params: UnsignedTransactionParams,
2065 ) -> Result<UnsignedTransaction> {
2066 OstiumClient::cancel_order_unsigned(self, params, tx_params).await
2067 }
2068}