ostium_rust_sdk/
client.rs

1//! Main client for interacting with the Ostium platform
2//!
3//! The `OstiumClient` provides a unified interface for all SDK functionality
4//! including trading operations, market data queries, and account management.
5
6use 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
25// Type alias for the provider returned by ProviderBuilder
26type 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
44/// Builder for creating an `OstiumClient` instance
45pub 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    /// Create a new builder with the specified network
56    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    /// Create a new builder with custom configuration
68    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    /// Set the private key for signing transactions
80    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    /// Set a custom RPC URL
89    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    /// Set a custom HTTP client for GraphQL requests
97    pub fn with_http_client(mut self, client: HttpClient) -> Self {
98        self.http_client = Some(client);
99        self
100    }
101
102    /// Configure retry behavior for network operations
103    pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
104        self.retry_config = retry_config;
105        self
106    }
107
108    /// Enable/disable circuit breaker (enabled by default)
109    pub fn with_circuit_breaker(mut self, enabled: bool) -> Self {
110        self.enable_circuit_breaker = enabled;
111        self
112    }
113
114    /// Configure optimized retry settings for different operation types
115    pub fn with_network_retry(mut self) -> Self {
116        self.retry_config = RetryConfig::network();
117        self
118    }
119
120    /// Configure optimized retry settings for contract operations
121    pub fn with_contract_retry(mut self) -> Self {
122        self.retry_config = RetryConfig::contract();
123        self
124    }
125
126    /// Configure optimized retry settings for GraphQL operations
127    pub fn with_graphql_retry(mut self) -> Self {
128        self.retry_config = RetryConfig::graphql();
129        self
130    }
131
132    /// Enable rate limiting with default configurations
133    pub fn with_rate_limiting(mut self) -> Self {
134        self.rate_limiter = Some(RateLimiterManager::new().with_default_limits());
135        self
136    }
137
138    /// Enable conservative rate limiting for production environments
139    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    /// Set a custom rate limiter manager
151    pub fn with_rate_limiter(mut self, rate_limiter: RateLimiterManager) -> Self {
152        self.rate_limiter = Some(rate_limiter);
153        self
154    }
155
156    /// Build the `OstiumClient` instance
157    pub async fn build(self) -> Result<OstiumClient> {
158        // Validate configuration
159        self.config.validate()?;
160
161        // Create the Alloy provider using ProviderBuilder
162        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        // Create HTTP client for GraphQL
168        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/// Main client for interacting with the Ostium platform
204#[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    /// Create a new client builder for the specified network
218    pub fn builder(network: Network) -> OstiumClientBuilder {
219        OstiumClientBuilder::new(network)
220    }
221
222    /// Create a new client builder with custom configuration
223    pub fn builder_with_config(config: NetworkConfig) -> OstiumClientBuilder {
224        OstiumClientBuilder::with_config(config)
225    }
226
227    /// Create a new client with default configuration (no signer)
228    pub async fn new(network: Network) -> Result<Self> {
229        OstiumClientBuilder::new(network).build().await
230    }
231
232    /// Get the network configuration
233    pub fn config(&self) -> &NetworkConfig {
234        &self.config
235    }
236
237    /// Get the signer address if available
238    pub fn signer_address(&self) -> Option<Address> {
239        self.signer.as_ref().map(|s| s.address())
240    }
241
242    /// Check if the client has a signer configured
243    pub fn has_signer(&self) -> bool {
244        self.signer.is_some()
245    }
246
247    /// Get USDC contract instance
248    fn usdc_contract(&self) -> UsdcContract<Arc<Provider>> {
249        UsdcContract::new(self.config.usdc_address, self.provider.clone())
250    }
251
252    /// Get Trading contract instance
253    fn trading_contract(&self) -> TradingContract<Arc<Provider>> {
254        TradingContract::new(self.config.trading_contract, self.provider.clone())
255    }
256
257    /// Get Trading Storage contract instance
258    fn trading_storage_contract(&self) -> TradingStorageContract<Arc<Provider>> {
259        TradingStorageContract::new(self.config.storage_contract, self.provider.clone())
260    }
261
262    /// Execute a GraphQL query
263    async fn graphql_query(&self, query: &str, variables: Option<Value>) -> Result<Value> {
264        // Apply rate limiting
265        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    /// Execute a REST API call with retry logic
325    async fn rest_api_call(&self, url: String) -> Result<Value> {
326        // Apply rate limiting
327        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    /// Convert decimal to U256 with proper scaling
364    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    /// Convert decimal to U192 with 18 decimals
373    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    /// Convert optional decimal to U192, returning ZERO if None
382    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    /// Maps Ethereum contract error selectors to human-readable messages
390    ///
391    /// This function takes a contract error and attempts to decode it into
392    /// a more meaningful error message based on known Ostium contract error selectors.
393    ///
394    /// # Arguments
395    /// * `error` - The error object or string containing the contract error
396    ///
397    /// # Returns
398    /// A tuple containing (error_message, error_type, error_data)
399    pub fn map_contract_error(&self, error: &str) -> (String, String, HashMap<String, String>) {
400        // Error selector mapping based on Keccak hashes from Ostium contracts
401        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        // Store original error
436        error_data.insert("original_error".to_string(), error.to_string());
437
438        // Check if this is a gas estimation error containing a contract error
439        if error.contains("Gas estimation failed") && error.contains("0x") {
440            // Extract contract error selector from gas estimation error
441            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        // Check for execution reverted errors with data
461        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        // Check for specific error patterns
479        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    /// Extracts error selector (0x + 8 hex chars) from error message
497    pub fn extract_error_selector(&self, error: &str) -> Option<String> {
498        // Look for pattern: 0x followed by exactly 8 hexadecimal characters
499        // Simple implementation without regex
500        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    /// Get human-readable error message for a specific error selector
521    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    /// Enhanced error mapping that includes suggestions for common issues
552    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    /// Get minimum position size for a given symbol
618    /// Returns the minimum position size in the base asset units
619    pub async fn get_minimum_position_size(&self, symbol: &str) -> Result<Decimal> {
620        debug!("Getting minimum position size for symbol: {}", symbol);
621
622        // Try to get minimum size from contract data first
623        match self.get_contract_minimum_size(symbol).await {
624            Ok(min_size) => Ok(min_size),
625            Err(_) => {
626                // Fallback to asset-type-based minimum sizes
627                self.get_fallback_minimum_size(symbol)
628            }
629        }
630    }
631
632    /// Validate trading constraints before executing a trade
633    /// Checks: 1) Trading hours, 2) Minimum position size, 3) Open interest caps
634    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        // 1. Check trading hours
653        self.validate_trading_hours(symbol).await?;
654
655        // 2. Check minimum position size
656        self.validate_minimum_position_size(symbol, size, leverage)
657            .await?;
658
659        // 3. Check open interest caps
660        self.validate_open_interest_caps(symbol, side, size, leverage)
661            .await?;
662
663        Ok(())
664    }
665
666    /// Get minimum position size from contract data
667    async fn get_contract_minimum_size(&self, symbol: &str) -> Result<Decimal> {
668        // Try to get from trading pairs data
669        let pairs = self.get_pairs().await?;
670
671        for pair in pairs {
672            if pair.symbol == symbol {
673                // For now, return a calculated minimum based on spread and volume
674                // This would ideally come from contract configuration
675                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    /// Calculate minimum size from pair data
686    fn calculate_minimum_size_from_pair(&self, pair: &crate::types::TradingPair) -> Decimal {
687        // Use a reasonable minimum based on the asset type
688        // This is a fallback calculation when contract data isn't available
689        if pair.symbol.starts_with("BTC") {
690            dec!(0.0001) // 0.0001 BTC minimum
691        } else if pair.symbol.starts_with("ETH") {
692            dec!(0.001) // 0.001 ETH minimum
693        } else if pair.symbol.contains("USD") {
694            dec!(1.0) // $1 minimum for fiat pairs
695        } else {
696            dec!(0.01) // Default 0.01 for other assets
697        }
698    }
699
700    /// Get fallback minimum sizes based on asset type
701    fn get_fallback_minimum_size(&self, symbol: &str) -> Result<Decimal> {
702        let min_size = if symbol.starts_with("BTC") {
703            dec!(0.0001) // 0.0001 BTC (~$4-5 at $50k BTC)
704        } else if symbol.starts_with("ETH") {
705            dec!(0.001) // 0.001 ETH (~$2-4 at $3k ETH)
706        } else if symbol.starts_with("SOL") {
707            dec!(0.01) // 0.01 SOL
708        } else if symbol.contains("EUR") || symbol.contains("GBP") || symbol.contains("JPY") {
709            // Forex pairs - typically have larger minimum sizes
710            dec!(1000.0) // 1000 units of base currency
711        } else if symbol.contains("GOLD") || symbol.contains("SILVER") {
712            dec!(0.01) // 0.01 oz
713        } else if symbol.contains("SPX") || symbol.contains("NAS") {
714            dec!(0.1) // 0.1 index units
715        } else {
716            dec!(1.0) // Default minimum
717        };
718
719        Ok(min_size)
720    }
721
722    /// Validate trading hours for the given symbol
723    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 we can't get trading hours, assume crypto is 24/7 and others might be closed
741                if symbol.contains("BTC") || symbol.contains("ETH") || symbol.contains("SOL") {
742                    Ok(()) // Crypto markets are typically 24/7
743                } else {
744                    // For traditional markets, be conservative and allow the trade
745                    // but warn that hours couldn't be verified
746                    warn!("Could not verify trading hours for {}", symbol);
747                    Ok(())
748                }
749            }
750        }
751    }
752
753    /// Validate minimum position size requirements
754    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            // Try to get current price for better error message
764            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    /// Validate open interest caps to prevent excessive exposure
793    async fn validate_open_interest_caps(
794        &self,
795        symbol: &str,
796        side: PositionSide,
797        size: Decimal,
798        leverage: Decimal,
799    ) -> Result<()> {
800        // Calculate notional value of the position
801        let price = match self.get_price(symbol).await {
802            Ok(p) => p.mark_price,
803            Err(_) => {
804                // If we can't get price, skip this validation
805                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        // Define reasonable exposure limits (these would ideally come from contract)
816        let max_single_position = match symbol {
817            s if s.contains("BTC") => dec!(10_000_000), // $10M max BTC position
818            s if s.contains("ETH") => dec!(5_000_000),  // $5M max ETH position
819            s if s.contains("SOL") => dec!(1_000_000),  // $1M max SOL position
820            _ => dec!(2_000_000),                       // $2M max for other assets
821        };
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        // Additional check for very large positions that might affect market
833        let market_impact_threshold = max_single_position / dec!(2); // 50% of max
834        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
850// Trading API implementation
851impl OstiumClient {
852    /// Open a new trading position
853    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        // Get pair index from symbol
863        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        // Calculate collateral amount (USDC has 6 decimals)
871        let collateral = self.decimal_to_u256(params.size / params.leverage, 6)?;
872
873        // Convert prices to U192 (18 decimals)
874        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, // Will be set by the contract
886            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, // Will be set by the contract
898            buy: params.side == PositionSide::Long,
899        };
900
901        let slippage_p = self.decimal_to_u256(params.slippage_tolerance * Decimal::from(100), 0)?; // Convert to percentage
902
903        let trading = self.trading_contract();
904        trading
905            .open_trade(trade, OpenOrderType::Market, slippage_p)
906            .await?;
907
908        // Return a placeholder transaction hash for now
909        // In a real implementation, this would come from the transaction receipt
910        Ok(alloy_primitives::TxHash::ZERO)
911    }
912
913    /// Close an existing position
914    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        // Parse position ID to get pair_index and index
922        // Format: "trader_address:pair_index:index"
923        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            // Calculate percentage based on size
939            warn!("Partial close not fully implemented, closing 100%");
940            10000 // 100% in basis points
941        } else {
942            10000 // 100% in basis points
943        };
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    /// Update take profit and stop loss for a position
954    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        // Parse position ID
962        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        // Update take profit if provided
979        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        // Update stop loss if provided
985        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    // ==================== UNSIGNED TRANSACTION METHODS ====================
994
995    /// Build unsigned transaction for opening a position
996    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        // Get pair index from symbol
1008        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        // Calculate collateral amount (USDC has 6 decimals)
1016        let collateral = self.decimal_to_u256(params.size / params.leverage, 6)?;
1017
1018        // Convert prices to U192 (18 decimals)
1019        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, // Will be set by the contract
1031            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, // Will be set by the contract
1043            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    /// Build unsigned transaction for closing a position
1055    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        // Parse position ID to get pair_index and index
1066        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            // Calculate percentage based on size
1082            warn!("Partial close not fully implemented, closing 100%");
1083            10000 // 100% in basis points
1084        } else {
1085            10000 // 100% in basis points
1086        };
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    /// Build unsigned transactions for updating TP/SL
1095    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        // Parse position ID
1106        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        // Build transaction for updating take profit if provided
1124        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        // Build transaction for updating stop loss if provided
1133        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    /// Build unsigned transaction for placing an advanced order
1145    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        // Validate order parameters
1157        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                // Market orders don't need a price
1167            }
1168        }
1169
1170        // Get pair index from symbol
1171        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        // Calculate collateral amount (USDC has 6 decimals)
1179        let collateral = self.decimal_to_u256(params.size / params.leverage, 6)?;
1180
1181        // Convert prices to U192 (18 decimals)
1182        let tp = self.convert_optional_price(params.take_profit)?;
1183        let sl = self.convert_optional_price(params.stop_loss)?;
1184
1185        // For limit and stop orders, set the open_price to the specified price
1186        let open_price = if let Some(price) = params.price {
1187            self.decimal_to_u192(price)?.try_into().unwrap_or(0)
1188        } else {
1189            0 // Will be set by the contract for market orders
1190        };
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        // Convert order type
1207        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    /// Build unsigned transaction for canceling an order
1222    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        // Parse order ID
1233        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
1254// Advanced Order API implementation
1255impl OstiumClient {
1256    /// Place an advanced order (market, limit, or stop)
1257    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        // Validate order parameters
1265        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                // Market orders don't need a price
1275            }
1276        }
1277
1278        let trader = self.signer_address().unwrap();
1279
1280        // Get pair index from symbol
1281        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        // Calculate collateral amount (USDC has 6 decimals)
1289        let collateral = self.decimal_to_u256(params.size / params.leverage, 6)?;
1290
1291        // Convert prices to U192 (18 decimals)
1292        let tp = self.convert_optional_price(params.take_profit)?;
1293        let sl = self.convert_optional_price(params.stop_loss)?;
1294
1295        // For limit and stop orders, set the open_price to the specified price
1296        let open_price = if let Some(price) = params.price {
1297            self.decimal_to_u192(price)?.try_into().unwrap_or(0)
1298        } else {
1299            0 // Will be set by the contract for market orders
1300        };
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), // Convert to basis points
1311            pair_index,
1312            index: 0, // Will be set by the contract
1313            buy: params.side == PositionSide::Long,
1314        };
1315
1316        // Convert order type
1317        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    /// Place a limit order
1332    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), // Default 2% slippage
1343        };
1344
1345        self.place_advanced_order(advanced_params).await
1346    }
1347
1348    /// Place a stop order
1349    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), // Default 2% slippage
1360        };
1361
1362        self.place_advanced_order(advanced_params).await
1363    }
1364
1365    /// Cancel an open limit or stop order
1366    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        // Parse order ID
1374        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    /// Update an existing limit order
1395    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        // Parse order ID
1403        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        // Get current order details if we need to preserve some values
1418        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        // Use new values if provided, otherwise keep current values
1425        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    /// Get current market price for validation purposes
1449    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), // Market orders are always valid
1459            OrderExecutionType::Limit => {
1460                // For limit orders, the price should be reasonable compared to current market
1461                let price_diff = (price - current_price).abs() / current_price;
1462                Ok(price_diff <= Decimal::from(50) / Decimal::from(100)) // Within 50% of current price
1463            }
1464            OrderExecutionType::Stop => {
1465                // For stop orders, the price should also be reasonable
1466                let price_diff = (price - current_price).abs() / current_price;
1467                Ok(price_diff <= Decimal::from(50) / Decimal::from(100)) // Within 50% of current price
1468            }
1469        }
1470    }
1471}
1472
1473// Market Data API implementation
1474impl OstiumClient {
1475    /// Get all available trading pairs
1476    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        // Parse the response and convert to TradingPair structs
1496        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, // Assume active if max leverage > 0
1524                min_position_size: Decimal::from(1), // Default values - would need to get from contract
1525                max_position_size: Decimal::from(1000000),
1526                price_precision: 8,
1527                quantity_precision: 8,
1528            });
1529        }
1530
1531        Ok(trading_pairs)
1532    }
1533
1534    /// Get current price for a trading pair
1535    pub async fn get_price(&self, symbol: &str) -> Result<Price> {
1536        debug!("Fetching price for symbol: {}", symbol);
1537
1538        // Try to get price from the REST API first
1539        let rest_url = "https://metadata-backend.ostium.io/PricePublish/latest-price";
1540        let asset = symbol.replace("/", ""); // Convert BTC/USD to BTCUSD
1541        let url = format!("{}?asset={}", rest_url, asset);
1542
1543        match self.rest_api_call(url).await {
1544            Ok(price_data) => {
1545                // Parse the price data from the REST API
1546                // The API returns: {"bid": 108502.06, "mid": 108502.97, "ask": 108503.87, "isMarketOpen": true, ...}
1547                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                    // Calculate 24h high/low as estimates (±2% from current price)
1561                    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, // Use mid price for both mark and index
1568                        high_24h,
1569                        low_24h,
1570                        volume_24h: Decimal::from(1000000), // Still placeholder - would need different API
1571                        timestamp: chrono::Utc::now(),
1572                    });
1573                }
1574            }
1575            Err(e) => {
1576                debug!("Failed to fetch price from REST API: {}", e);
1577                // Continue to fallback
1578            }
1579        }
1580
1581        // Fallback: get the pair data and use placeholder prices
1582        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        // Return placeholder price data if REST API fails
1589        Ok(Price {
1590            symbol: pair.symbol.clone(),
1591            mark_price: Decimal::from(50000), // Placeholder values
1592            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    /// Get trading hours for a symbol
1601    pub async fn get_trading_hours(&self, symbol: &str) -> Result<TradingHours> {
1602        debug!("Fetching trading hours for symbol: {}", symbol);
1603
1604        // Try to get trading hours from the REST API
1605        let rest_url = "https://metadata-backend.ostium.io/trading-hours/asset-schedule";
1606        let asset = symbol.replace("/", ""); // Convert BTC/USD to BTCUSD
1607        let url = format!("{}?asset={}", rest_url, asset);
1608
1609        match self.rest_api_call(url).await {
1610            Ok(hours_data) => {
1611                // Check if we got an error response
1612                if let Some(error) = hours_data.get("error") {
1613                    debug!("Trading hours API error: {}", error);
1614                    // Fall through to default behavior
1615                } else {
1616                    // Parse the trading hours data from the REST API
1617                    let is_open = hours_data
1618                        .get("isOpenNow")
1619                        .and_then(|v| v.as_bool())
1620                        .unwrap_or(true); // Default to open for crypto markets when field is missing
1621
1622                    return Ok(TradingHours {
1623                        symbol: symbol.to_string(),
1624                        is_open,
1625                        next_open: None, // Could parse opening hours if needed
1626                        next_close: None,
1627                    });
1628                }
1629            }
1630            Err(e) => {
1631                debug!("Failed to fetch trading hours from REST API: {}", e);
1632                // Continue to fallback
1633            }
1634        }
1635
1636        // Fallback: For crypto markets (BTC, ETH), trading is typically 24/7
1637        // For traditional markets, we'd need more sophisticated logic
1638        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, // Crypto markets are always open, others might be closed
1646            next_open: None,
1647            next_close: None,
1648        })
1649    }
1650}
1651
1652// Account API implementation
1653impl OstiumClient {
1654    /// Get account balance
1655    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        // Convert from USDC decimals (6) to Decimal
1666        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, // Would need to calculate from open positions
1672            total: balance_decimal,
1673        })
1674    }
1675
1676    /// Get open positions for an account
1677    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        // Get all trading pairs to iterate through
1688        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            // Get the count of trades for this pair
1694            match storage.get_trades_count(account, pair_index).await {
1695                Ok(count) => {
1696                    // Iterate through all trades for this pair
1697                    for index in 0..count {
1698                        match storage.get_trade(account, pair_index, index).await {
1699                            Ok(trade) => {
1700                                // Convert contract Trade to SDK Position
1701                                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), // Convert from USDC decimals
1711                                    entry_price: Decimal::from(trade.open_price)
1712                                        / Decimal::from(10_u128.pow(18)), // Convert from 18 decimals
1713                                    mark_price: Decimal::ZERO, // Would need to fetch current price
1714                                    unrealized_pnl: Decimal::ZERO, // Would need to calculate
1715                                    realized_pnl: Decimal::ZERO, // Would need to track
1716                                    margin: Decimal::from(trade.collateral.to::<u128>())
1717                                        / Decimal::from(1_000_000),
1718                                    leverage: Decimal::from(trade.leverage) / Decimal::from(100), // Convert from basis points
1719                                    liquidation_price: None, // Would need to calculate
1720                                    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(), // Would need to get from events
1737                                    updated_at: chrono::Utc::now(),
1738                                };
1739                                positions.push(position);
1740                            }
1741                            Err(_) => {
1742                                // Trade might not exist or be closed, continue
1743                                continue;
1744                            }
1745                        }
1746                    }
1747                }
1748                Err(_) => {
1749                    // Pair might not have any trades, continue
1750                    continue;
1751                }
1752            }
1753        }
1754
1755        Ok(positions)
1756    }
1757
1758    /// Get open orders for an account
1759    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        // Get all trading pairs to iterate through
1770        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            // Get the count of open limit orders for this pair
1776            match storage
1777                .get_open_limit_orders_count(account, pair_index)
1778                .await
1779            {
1780                Ok(count) => {
1781                    // Iterate through all open limit orders for this pair
1782                    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                                // Convert contract OpenLimitOrder to SDK Order
1789                                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), // Convert from USDC decimals
1806                                    price: Some(
1807                                        Decimal::from(limit_order.target_price)
1808                                            / Decimal::from(10_u128.pow(18)),
1809                                    ), // Convert from 18 decimals
1810                                    stop_price: None, // Would need additional logic for stop orders
1811                                    status: OrderStatus::Pending, // Assume pending if it exists
1812                                    filled_size: Decimal::ZERO, // Would need to track fills
1813                                    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                                // Order might not exist, continue
1829                                continue;
1830                            }
1831                        }
1832                    }
1833                }
1834                Err(_) => {
1835                    // Pair might not have any orders, continue
1836                    continue;
1837                }
1838            }
1839        }
1840
1841        Ok(orders)
1842    }
1843}
1844
1845/// Trait for trading operations
1846#[allow(async_fn_in_trait)]
1847pub trait TradingApi {
1848    /// Open a new position
1849    async fn open_position(&self, params: OpenPositionParams) -> Result<TxHash>;
1850
1851    /// Close an existing position
1852    async fn close_position(&self, params: ClosePositionParams) -> Result<TxHash>;
1853
1854    /// Update take profit and stop loss
1855    async fn update_tp_sl(&self, params: UpdateTPSLParams) -> Result<TxHash>;
1856}
1857
1858/// Trait for market data operations
1859#[allow(async_fn_in_trait)]
1860pub trait MarketDataApi {
1861    /// Get all trading pairs
1862    async fn get_pairs(&self) -> Result<Vec<TradingPair>>;
1863
1864    /// Get price for a symbol
1865    async fn get_price(&self, symbol: &str) -> Result<Price>;
1866
1867    /// Get trading hours for a symbol
1868    async fn get_trading_hours(&self, symbol: &str) -> Result<TradingHours>;
1869}
1870
1871/// Trait for account operations
1872#[allow(async_fn_in_trait)]
1873pub trait AccountApi {
1874    /// Get account balance
1875    async fn get_balance(&self, address: Option<Address>) -> Result<Balance>;
1876
1877    /// Get open positions
1878    async fn get_positions(&self, address: Option<Address>) -> Result<Vec<Position>>;
1879
1880    /// Get open orders
1881    async fn get_orders(&self, address: Option<Address>) -> Result<Vec<Order>>;
1882}
1883
1884/// Trait for advanced order operations (market, limit, stop orders)
1885#[allow(async_fn_in_trait)]
1886pub trait AdvancedOrderApi {
1887    /// Place an advanced order (market, limit, or stop)
1888    async fn place_advanced_order(&self, params: AdvancedOrderParams) -> Result<TxHash>;
1889
1890    /// Place a limit order
1891    async fn place_limit_order(&self, params: LimitOrderParams) -> Result<TxHash>;
1892
1893    /// Place a stop order
1894    async fn place_stop_order(&self, params: StopOrderParams) -> Result<TxHash>;
1895
1896    /// Cancel an open order
1897    async fn cancel_order(&self, params: CancelOrderParams) -> Result<TxHash>;
1898
1899    /// Update an existing limit order
1900    async fn update_limit_order(&self, params: UpdateLimitOrderParams) -> Result<TxHash>;
1901
1902    /// Validate order price against current market
1903    async fn validate_order_price(
1904        &self,
1905        symbol: &str,
1906        order_type: OrderExecutionType,
1907        price: Decimal,
1908    ) -> Result<bool>;
1909}
1910
1911/// Trait for unsigned transaction operations (for frontend integration)
1912#[allow(async_fn_in_trait)]
1913pub trait UnsignedTransactionApi {
1914    /// Build unsigned transaction for opening a position
1915    async fn open_position_unsigned(
1916        &self,
1917        params: OpenPositionParams,
1918        trader_address: Address,
1919        tx_params: UnsignedTransactionParams,
1920    ) -> Result<UnsignedTransaction>;
1921
1922    /// Build unsigned transaction for closing a position
1923    async fn close_position_unsigned(
1924        &self,
1925        params: ClosePositionParams,
1926        tx_params: UnsignedTransactionParams,
1927    ) -> Result<UnsignedTransaction>;
1928
1929    /// Build unsigned transactions for updating TP/SL
1930    async fn update_tp_sl_unsigned(
1931        &self,
1932        params: UpdateTPSLParams,
1933        tx_params: UnsignedTransactionParams,
1934    ) -> Result<Vec<UnsignedTransaction>>;
1935
1936    /// Build unsigned transaction for placing an advanced order
1937    async fn place_advanced_order_unsigned(
1938        &self,
1939        params: AdvancedOrderParams,
1940        trader_address: Address,
1941        tx_params: UnsignedTransactionParams,
1942    ) -> Result<UnsignedTransaction>;
1943
1944    /// Build unsigned transaction for canceling an order
1945    async fn cancel_order_unsigned(
1946        &self,
1947        params: CancelOrderParams,
1948        tx_params: UnsignedTransactionParams,
1949    ) -> Result<UnsignedTransaction>;
1950}
1951
1952// Implement the traits for OstiumClient
1953impl 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}