use alloy::providers::ProviderBuilder;
use alloy::signers::local::PrivateKeySigner;
use alloy_primitives::{aliases::U192, Address, U256};
use reqwest::Client as HttpClient;
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::sync::Arc;
use tracing::{debug, info, warn};
use crate::config::{Network, NetworkConfig};
use crate::contracts::{TradingContract, TradingStorageContract, UsdcContract};
use crate::error::{OstiumError, Result};
use crate::rate_limit::RateLimiterManager;
use crate::retry::{RetryConfig, RetryExecutor};
use crate::types::*;
type Provider = alloy::providers::fillers::FillProvider<
alloy::providers::fillers::JoinFill<
alloy_provider::Identity,
alloy::providers::fillers::JoinFill<
alloy::providers::fillers::GasFiller,
alloy::providers::fillers::JoinFill<
alloy::providers::fillers::BlobGasFiller,
alloy::providers::fillers::JoinFill<
alloy::providers::fillers::NonceFiller,
alloy::providers::fillers::ChainIdFiller,
>,
>,
>,
>,
alloy::providers::RootProvider<alloy::network::Ethereum>,
alloy::network::Ethereum,
>;
pub struct OstiumClientBuilder {
config: NetworkConfig,
signer: Option<PrivateKeySigner>,
http_client: Option<HttpClient>,
retry_config: RetryConfig,
enable_circuit_breaker: bool,
rate_limiter: Option<RateLimiterManager>,
}
impl OstiumClientBuilder {
pub fn new(network: Network) -> Self {
Self {
config: network.config(),
signer: None,
http_client: None,
retry_config: RetryConfig::default(),
enable_circuit_breaker: true,
rate_limiter: None,
}
}
pub fn with_config(config: NetworkConfig) -> Self {
Self {
config,
signer: None,
http_client: None,
retry_config: RetryConfig::default(),
enable_circuit_breaker: true,
rate_limiter: None,
}
}
pub fn with_private_key(mut self, private_key: &str) -> Result<Self> {
let signer = private_key
.parse::<PrivateKeySigner>()
.map_err(|e| OstiumError::wallet(format!("Invalid private key: {}", e)))?;
self.signer = Some(signer);
Ok(self)
}
pub fn with_rpc_url(mut self, url: &str) -> Result<Self> {
self.config.rpc_url = url
.parse()
.map_err(|e| OstiumError::config(format!("Invalid RPC URL: {}", e)))?;
Ok(self)
}
pub fn with_http_client(mut self, client: HttpClient) -> Self {
self.http_client = Some(client);
self
}
pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
self.retry_config = retry_config;
self
}
pub fn with_circuit_breaker(mut self, enabled: bool) -> Self {
self.enable_circuit_breaker = enabled;
self
}
pub fn with_network_retry(mut self) -> Self {
self.retry_config = RetryConfig::network();
self
}
pub fn with_contract_retry(mut self) -> Self {
self.retry_config = RetryConfig::contract();
self
}
pub fn with_graphql_retry(mut self) -> Self {
self.retry_config = RetryConfig::graphql();
self
}
pub fn with_rate_limiting(mut self) -> Self {
self.rate_limiter = Some(RateLimiterManager::new().with_default_limits());
self
}
pub fn with_conservative_rate_limiting(mut self) -> Self {
use crate::rate_limit::RateLimitConfig;
self.rate_limiter = Some(
RateLimiterManager::new()
.with_graphql_rate_limit(RateLimitConfig::conservative())
.with_rest_rate_limit(RateLimitConfig::conservative())
.with_blockchain_rate_limit(RateLimitConfig::conservative()),
);
self
}
pub fn with_rate_limiter(mut self, rate_limiter: RateLimiterManager) -> Self {
self.rate_limiter = Some(rate_limiter);
self
}
pub async fn build(self) -> Result<OstiumClient> {
self.config.validate()?;
let provider = ProviderBuilder::new()
.connect(self.config.rpc_url.as_str())
.await
.map_err(|e| OstiumError::network(format!("Failed to connect to RPC: {}", e)))?;
let http_client = self.http_client.unwrap_or_else(|| {
HttpClient::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.expect("Failed to create HTTP client")
});
info!(
"Initialized Ostium client for {:?} network",
self.config.network
);
Ok(OstiumClient {
config: self.config,
provider: Arc::new(provider),
signer: self.signer,
http_client: Arc::new(http_client),
rate_limiter: Arc::new(self.rate_limiter.unwrap_or_default()),
network_retry_executor: Arc::new(if self.enable_circuit_breaker {
RetryExecutor::new(RetryConfig::network())
.with_circuit_breaker(5, std::time::Duration::from_secs(60))
} else {
RetryExecutor::new(RetryConfig::network())
}),
_contract_retry_executor: Arc::new(if self.enable_circuit_breaker {
RetryExecutor::new(RetryConfig::contract())
.with_circuit_breaker(3, std::time::Duration::from_secs(120))
} else {
RetryExecutor::new(RetryConfig::contract())
}),
graphql_retry_executor: Arc::new(RetryExecutor::new(RetryConfig::graphql())),
})
}
}
#[derive(Clone)]
pub struct OstiumClient {
config: NetworkConfig,
provider: Arc<Provider>,
signer: Option<PrivateKeySigner>,
http_client: Arc<HttpClient>,
rate_limiter: Arc<RateLimiterManager>,
network_retry_executor: Arc<RetryExecutor>,
_contract_retry_executor: Arc<RetryExecutor>,
graphql_retry_executor: Arc<RetryExecutor>,
}
impl OstiumClient {
pub fn builder(network: Network) -> OstiumClientBuilder {
OstiumClientBuilder::new(network)
}
pub fn builder_with_config(config: NetworkConfig) -> OstiumClientBuilder {
OstiumClientBuilder::with_config(config)
}
pub async fn new(network: Network) -> Result<Self> {
OstiumClientBuilder::new(network).build().await
}
pub fn config(&self) -> &NetworkConfig {
&self.config
}
pub fn signer_address(&self) -> Option<Address> {
self.signer.as_ref().map(|s| s.address())
}
pub fn has_signer(&self) -> bool {
self.signer.is_some()
}
fn usdc_contract(&self) -> UsdcContract<Arc<Provider>> {
UsdcContract::new(self.config.usdc_address, self.provider.clone())
}
fn trading_contract(&self) -> TradingContract<Arc<Provider>> {
TradingContract::new(self.config.trading_contract, self.provider.clone())
}
fn trading_storage_contract(&self) -> TradingStorageContract<Arc<Provider>> {
TradingStorageContract::new(self.config.storage_contract, self.provider.clone())
}
async fn graphql_query(&self, query: &str, variables: Option<Value>) -> Result<Value> {
self.rate_limiter
.acquire_graphql()
.await
.map_err(|e| OstiumError::network(format!("Rate limit error: {}", e)))?;
let query = query.to_string();
let variables = variables.unwrap_or(json!({}));
let http_client = self.http_client.clone();
let url = self.config.graphql_url.clone();
self.graphql_retry_executor
.execute(|| {
let query = query.clone();
let variables = variables.clone();
let http_client = http_client.clone();
let url = url.clone();
async move {
let body = json!({
"query": query,
"variables": variables
});
debug!("Executing GraphQL query: {}", query);
let response = http_client
.post(url.as_str())
.json(&body)
.send()
.await
.map_err(|e| {
OstiumError::network(format!("GraphQL request failed: {}", e))
})?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(OstiumError::graphql(format!(
"GraphQL request failed with status {}: {}",
status, error_text
)));
}
let json: Value = response.json().await.map_err(|e| {
OstiumError::graphql(format!("Failed to parse GraphQL response: {}", e))
})?;
if let Some(errors) = json.get("errors") {
return Err(OstiumError::graphql(format!("GraphQL errors: {}", errors)));
}
json.get("data").cloned().ok_or_else(|| {
OstiumError::graphql("No data in GraphQL response".to_string())
})
}
})
.await
}
async fn rest_api_call(&self, url: String) -> Result<Value> {
self.rate_limiter
.acquire_rest()
.await
.map_err(|e| OstiumError::network(format!("Rate limit error: {}", e)))?;
let http_client = self.http_client.clone();
self.network_retry_executor
.execute(|| {
let url = url.clone();
let http_client = http_client.clone();
async move {
debug!("Making REST API call to: {}", url);
let response = http_client.get(&url).send().await.map_err(|e| {
OstiumError::network(format!("REST API request failed: {}", e))
})?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(OstiumError::network(format!(
"REST API request failed with status {}: {}",
status, error_text
)));
}
response.json().await.map_err(|e| {
OstiumError::network(format!("Failed to parse REST API response: {}", e))
})
}
})
.await
}
fn decimal_to_u256(&self, value: Decimal, decimals: u8) -> Result<U256> {
let scale = 10_u128.pow(decimals as u32);
let scaled = (value * Decimal::from(scale))
.to_u128()
.ok_or_else(|| OstiumError::conversion("Value too large for U256".to_string()))?;
Ok(U256::from(scaled))
}
fn decimal_to_u192(&self, value: Decimal) -> Result<U192> {
let scale = 10_u128.pow(18);
let scaled = (value * Decimal::from(scale))
.to_u128()
.ok_or_else(|| OstiumError::conversion("Value too large for U192".to_string()))?;
Ok(U192::from(scaled))
}
fn convert_optional_price(&self, price: Option<Decimal>) -> Result<U192> {
match price {
Some(p) => self.decimal_to_u192(p),
None => Ok(U192::ZERO),
}
}
pub fn map_contract_error(&self, error: &str) -> (String, String, HashMap<String, String>) {
let error_selectors: HashMap<&str, &str> = [
("0x5863f789", "WrongParams"),
("0xcb87b762", "PairNotListed"),
("0x1309a563", "IsPaused"),
("0x093650d5", "NotGov"),
("0x2a19e833", "NotManager"),
("0x084986e7", "IsDone"),
("0x432b6c83", "NotTradesUpKeep"),
("0xe6f47fab", "MaxTradesPerPairReached"),
("0x5c12ea62", "MaxPendingMarketOrdersReached"),
("0x35fe85c5", "WrongLeverage"),
("0x80a71fc5", "AboveMaxAllowedCollateral"),
("0xeca695e1", "BelowMinLevPos"),
("0xa41bb918", "WrongTP"),
("0x083fbd78", "WrongSL"),
("0x17e08e97", "NoTradeFound"),
("0xdd9397bb", "TriggerPending"),
("0xf77a8069", "AlreadyMarketClosed"),
("0xa35ee470", "NoLimitFound"),
("0x46c4ede2", "ExposureLimits"),
("0xefa9e5be", "NoTradeToTimeoutFound"),
("0x5ac89f62", "NotYourOrder"),
("0x1add0915", "NotOpenMarketTimeoutOrder"),
("0x3e0b1869", "WaitTimeout"),
("0xc7fe4d00", "NotCloseMarketTimeoutOrder"),
]
.iter()
.cloned()
.collect();
let mut error_message = error.to_string();
let mut error_type = "UnknownError".to_string();
let mut error_data = HashMap::new();
error_data.insert("original_error".to_string(), error.to_string());
if error.contains("Gas estimation failed") && error.contains("0x") {
if let Some(selector) = self.extract_error_selector(error) {
if let Some(&contract_error) = error_selectors.get(selector.as_str()) {
error_type = format!("GasEstimation_{}", contract_error);
error_message = format!(
"Gas estimation failed due to contract error: {}",
contract_error
);
error_data.insert("contract_error".to_string(), contract_error.to_string());
error_data.insert("selector".to_string(), selector.clone());
warn!(
"Contract error during gas estimation: {} (Selector: {})",
contract_error, selector
);
return (error_message, error_type, error_data);
}
}
}
if error.contains("execution reverted") {
if let Some(selector) = self.extract_error_selector(error) {
if let Some(&contract_error) = error_selectors.get(selector.as_str()) {
error_type = contract_error.to_string();
error_message = format!("Contract error: {}", contract_error);
error_data.insert("contract_error".to_string(), contract_error.to_string());
error_data.insert("selector".to_string(), selector.clone());
warn!(
"Contract execution reverted: {} (Selector: {})",
contract_error, selector
);
return (error_message, error_type, error_data);
}
}
}
if error.contains("insufficient funds") || error.contains("insufficient balance") {
error_type = "InsufficientFunds".to_string();
error_message = "Insufficient funds for transaction".to_string();
} else if error.contains("nonce too low") {
error_type = "NonceTooLow".to_string();
error_message = "Transaction nonce is too low".to_string();
} else if error.contains("gas required exceeds allowance") {
error_type = "OutOfGas".to_string();
error_message = "Transaction requires more gas than allowed".to_string();
} else if error.contains("replacement transaction underpriced") {
error_type = "UnderpricedReplacement".to_string();
error_message = "Replacement transaction gas price too low".to_string();
}
(error_message, error_type, error_data)
}
pub fn extract_error_selector(&self, error: &str) -> Option<String> {
let error_lower = error.to_lowercase();
let mut start_pos = 0;
while let Some(pos) = error_lower[start_pos..].find("0x") {
let actual_pos = start_pos + pos;
if actual_pos + 10 <= error_lower.len() {
let candidate = &error_lower[actual_pos..actual_pos + 10];
if candidate.len() == 10 && candidate.starts_with("0x") {
let hex_part = &candidate[2..];
if hex_part.chars().all(|c| c.is_ascii_hexdigit()) {
return Some(candidate.to_string());
}
}
}
start_pos = actual_pos + 2;
}
None
}
pub fn get_error_description(&self, selector: &str) -> Option<&'static str> {
match selector {
"0x5863f789" => Some("Wrong parameters provided to the contract function"),
"0xcb87b762" => Some("Trading pair is not listed or supported"),
"0x1309a563" => Some("Contract is currently paused"),
"0x093650d5" => Some("Caller is not the contract governor"),
"0x2a19e833" => Some("Caller is not a contract manager"),
"0x084986e7" => Some("Operation is already completed"),
"0x432b6c83" => Some("Caller is not authorized for trades upkeep"),
"0xe6f47fab" => Some("Maximum number of trades per pair reached"),
"0x5c12ea62" => Some("Maximum pending market orders reached"),
"0x35fe85c5" => Some("Leverage value is outside allowed range"),
"0x80a71fc5" => Some("Collateral amount exceeds maximum allowed"),
"0xeca695e1" => Some("Position size is below minimum leverage requirement"),
"0xa41bb918" => Some("Take profit price is invalid"),
"0x083fbd78" => Some("Stop loss price is invalid"),
"0x17e08e97" => Some("Trade not found"),
"0xdd9397bb" => Some("Trigger order is pending"),
"0xf77a8069" => Some("Market is already closed"),
"0xa35ee470" => Some("Limit order not found"),
"0x46c4ede2" => Some("Exposure limits exceeded"),
"0xefa9e5be" => Some("No trade found for timeout"),
"0x5ac89f62" => Some("Not your order"),
"0x1add0915" => Some("Not an open market timeout order"),
"0x3e0b1869" => Some("Must wait for timeout period"),
"0xc7fe4d00" => Some("Not a close market timeout order"),
_ => None,
}
}
pub fn map_contract_error_with_suggestions(
&self,
error: &str,
) -> (String, String, HashMap<String, String>, Option<String>) {
let (error_message, error_type, mut error_data) = self.map_contract_error(error);
let suggestion = match error_type.as_str() {
"WrongParams" | "GasEstimation_WrongParams" => Some(
"Check that all parameters (collateral, leverage, prices) are within valid ranges"
.to_string(),
),
"PairNotListed" | "GasEstimation_PairNotListed" => {
Some("Verify the trading pair symbol is correct and supported".to_string())
}
"IsPaused" | "GasEstimation_IsPaused" => {
Some("Trading is temporarily paused. Please try again later".to_string())
}
"MaxTradesPerPairReached" | "GasEstimation_MaxTradesPerPairReached" => {
Some("Close some existing positions before opening new ones".to_string())
}
"MaxPendingMarketOrdersReached" | "GasEstimation_MaxPendingMarketOrdersReached" => {
Some("Cancel some pending orders before placing new ones".to_string())
}
"WrongLeverage" | "GasEstimation_WrongLeverage" => {
Some("Adjust leverage to be within the allowed range for this pair".to_string())
}
"AboveMaxAllowedCollateral" | "GasEstimation_AboveMaxAllowedCollateral" => {
Some("Reduce the position size or collateral amount".to_string())
}
"BelowMinLevPos" | "GasEstimation_BelowMinLevPos" => Some(
"Increase position size or reduce leverage to meet minimum requirements"
.to_string(),
),
"WrongTP" | "GasEstimation_WrongTP" => Some(
"Check that take profit price is reasonable relative to entry price".to_string(),
),
"WrongSL" | "GasEstimation_WrongSL" => {
Some("Check that stop loss price is reasonable relative to entry price".to_string())
}
"NoTradeFound" | "GasEstimation_NoTradeFound" => {
Some("Verify the trade ID and ensure the position still exists".to_string())
}
"AlreadyMarketClosed" | "GasEstimation_AlreadyMarketClosed" => {
Some("This position has already been closed".to_string())
}
"NoLimitFound" | "GasEstimation_NoLimitFound" => {
Some("The limit order may have been executed or cancelled".to_string())
}
"ExposureLimits" | "GasEstimation_ExposureLimits" => {
Some("Reduce position size to stay within exposure limits".to_string())
}
"InsufficientFunds" => {
Some("Ensure sufficient USDC balance and allowance for the trade".to_string())
}
"OutOfGas" => Some("Increase gas limit for the transaction".to_string()),
_ => None,
};
if let Some(ref suggestion_text) = suggestion {
error_data.insert("suggestion".to_string(), suggestion_text.clone());
}
(error_message, error_type, error_data, suggestion)
}
pub async fn get_minimum_position_size(&self, symbol: &str) -> Result<Decimal> {
debug!("Getting minimum position size for symbol: {}", symbol);
match self.get_contract_minimum_size(symbol).await {
Ok(min_size) => Ok(min_size),
Err(_) => {
self.get_fallback_minimum_size(symbol)
}
}
}
pub async fn validate_trading_constraints(
&self,
symbol: &str,
side: PositionSide,
size: Decimal,
leverage: Decimal,
) -> Result<()> {
debug!(
"Validating trading constraints for {} {} {} at {}x leverage",
symbol,
match side {
PositionSide::Long => "Long",
PositionSide::Short => "Short",
},
size,
leverage
);
self.validate_trading_hours(symbol).await?;
self.validate_minimum_position_size(symbol, size, leverage)
.await?;
self.validate_open_interest_caps(symbol, side, size, leverage)
.await?;
Ok(())
}
async fn get_contract_minimum_size(&self, symbol: &str) -> Result<Decimal> {
let pairs = self.get_pairs().await?;
for pair in pairs {
if pair.symbol == symbol {
return Ok(self.calculate_minimum_size_from_pair(&pair));
}
}
Err(OstiumError::validation(format!(
"Symbol {} not found",
symbol
)))
}
fn calculate_minimum_size_from_pair(&self, pair: &crate::types::TradingPair) -> Decimal {
if pair.symbol.starts_with("BTC") {
dec!(0.0001) } else if pair.symbol.starts_with("ETH") {
dec!(0.001) } else if pair.symbol.contains("USD") {
dec!(1.0) } else {
dec!(0.01) }
}
fn get_fallback_minimum_size(&self, symbol: &str) -> Result<Decimal> {
let min_size = if symbol.starts_with("BTC") {
dec!(0.0001) } else if symbol.starts_with("ETH") {
dec!(0.001) } else if symbol.starts_with("SOL") {
dec!(0.01) } else if symbol.contains("EUR") || symbol.contains("GBP") || symbol.contains("JPY") {
dec!(1000.0) } else if symbol.contains("GOLD") || symbol.contains("SILVER") {
dec!(0.01) } else if symbol.contains("SPX") || symbol.contains("NAS") {
dec!(0.1) } else {
dec!(1.0) };
Ok(min_size)
}
async fn validate_trading_hours(&self, symbol: &str) -> Result<()> {
match self.get_trading_hours(symbol).await {
Ok(hours) => {
if !hours.is_open {
return Err(OstiumError::validation(format!(
"Market is closed for {}. {}",
symbol,
if let Some(next_open) = hours.next_open {
format!("Next opening: {}", next_open)
} else {
"Trading hours: Please check market schedule".to_string()
}
)));
}
Ok(())
}
Err(_e) => {
if symbol.contains("BTC") || symbol.contains("ETH") || symbol.contains("SOL") {
Ok(()) } else {
warn!("Could not verify trading hours for {}", symbol);
Ok(())
}
}
}
}
async fn validate_minimum_position_size(
&self,
symbol: &str,
size: Decimal,
leverage: Decimal,
) -> Result<()> {
let min_size = self.get_minimum_position_size(symbol).await?;
if size < min_size {
let price_info = match self.get_price(symbol).await {
Ok(price) => {
let min_collateral = min_size * price.mark_price / leverage;
format!(
"\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",
symbol,
price.mark_price,
min_collateral,
min_size,
symbol.split('/').next().unwrap_or("units")
)
}
Err(_) => format!(
"\n\nSolutions:\n• Increase position size to at least {} {}\n• Check minimum collateral requirements (typically 7+ USDC)",
min_size,
symbol.split('/').next().unwrap_or("units")
)
};
return Err(OstiumError::validation(format!(
"Position size {} is below minimum required size of {} for {}.{}",
size, min_size, symbol, price_info
)));
}
Ok(())
}
async fn validate_open_interest_caps(
&self,
symbol: &str,
side: PositionSide,
size: Decimal,
leverage: Decimal,
) -> Result<()> {
let price = match self.get_price(symbol).await {
Ok(p) => p.mark_price,
Err(_) => {
warn!(
"Could not get price for {} to validate open interest caps",
symbol
);
return Ok(());
}
};
let notional_value = size * price * leverage;
let max_single_position = match symbol {
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), };
if notional_value > max_single_position {
return Err(OstiumError::validation(format!(
"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",
notional_value,
max_single_position,
symbol
)));
}
let market_impact_threshold = max_single_position / dec!(2); if notional_value > market_impact_threshold {
warn!(
"Large position detected: ${:.2} notional value for {} {} position",
notional_value,
symbol,
match side {
PositionSide::Long => "Long",
PositionSide::Short => "Short",
}
);
}
Ok(())
}
}
impl OstiumClient {
pub async fn open_position(&self, params: OpenPositionParams) -> Result<TxHash> {
if !self.has_signer() {
return Err(OstiumError::wallet("No signer configured"));
}
debug!("Opening position: {:?}", params);
let trader = self.signer_address().unwrap();
let storage = self.trading_storage_contract();
let (base, quote) = params.symbol.split_once('/').ok_or_else(|| {
OstiumError::validation("Invalid symbol format, expected 'BASE/QUOTE'".to_string())
})?;
let pair_index = storage.get_pair_index(base, quote).await?;
let collateral = self.decimal_to_u256(params.size / params.leverage, 6)?;
let tp = match params.take_profit {
Some(p) => self.decimal_to_u192(p)?,
None => U192::ZERO,
};
let sl = match params.stop_loss {
Some(p) => self.decimal_to_u192(p)?,
None => U192::ZERO,
};
let trade = Trade {
collateral,
open_price: 0, tp: tp.try_into().map_err(|e| {
OstiumError::conversion(format!("Failed to convert take profit price: {}", e))
})?,
sl: sl.try_into().map_err(|e| {
OstiumError::conversion(format!("Failed to convert stop loss price: {}", e))
})?,
trader,
leverage: (params.leverage * Decimal::from(100))
.to_u32()
.ok_or_else(|| OstiumError::validation("Leverage value too large".to_string()))?,
pair_index,
index: 0, buy: params.side == PositionSide::Long,
};
let slippage_p = self.decimal_to_u256(params.slippage_tolerance * Decimal::from(100), 0)?;
let trading = self.trading_contract();
trading
.open_trade(trade, OpenOrderType::Market, slippage_p)
.await?;
Ok(alloy_primitives::TxHash::ZERO)
}
pub async fn close_position(&self, params: ClosePositionParams) -> Result<TxHash> {
if !self.has_signer() {
return Err(OstiumError::wallet("No signer configured"));
}
debug!("Closing position: {:?}", params);
let parts: Vec<&str> = params.position_id.split(':').collect();
if parts.len() != 3 {
return Err(OstiumError::validation(
"Invalid position ID format".to_string(),
));
}
let pair_index: u16 = parts[1].parse().map_err(|_| {
OstiumError::validation("Invalid pair index in position ID".to_string())
})?;
let index: u8 = parts[2]
.parse()
.map_err(|_| OstiumError::validation("Invalid index in position ID".to_string()))?;
let close_percentage = if let Some(_size) = params.size {
warn!("Partial close not fully implemented, closing 100%");
10000 } else {
10000 };
let trading = self.trading_contract();
trading
.close_trade_market(pair_index, index, close_percentage)
.await?;
Ok(alloy_primitives::TxHash::ZERO)
}
pub async fn update_tp_sl(&self, params: UpdateTPSLParams) -> Result<TxHash> {
if !self.has_signer() {
return Err(OstiumError::wallet("No signer configured"));
}
debug!("Updating TP/SL: {:?}", params);
let parts: Vec<&str> = params.position_id.split(':').collect();
if parts.len() != 3 {
return Err(OstiumError::validation(
"Invalid position ID format".to_string(),
));
}
let pair_index: u16 = parts[1].parse().map_err(|_| {
OstiumError::validation("Invalid pair index in position ID".to_string())
})?;
let index: u8 = parts[2]
.parse()
.map_err(|_| OstiumError::validation("Invalid index in position ID".to_string()))?;
let trading = self.trading_contract();
if let Some(tp) = params.take_profit {
let tp_u192 = self.decimal_to_u192(tp)?;
trading.update_tp(pair_index, index, tp_u192).await?;
}
if let Some(sl) = params.stop_loss {
let sl_u192 = self.decimal_to_u192(sl)?;
trading.update_sl(pair_index, index, sl_u192).await?;
}
Ok(alloy_primitives::TxHash::ZERO)
}
pub async fn open_position_unsigned(
&self,
params: OpenPositionParams,
trader_address: Address,
tx_params: UnsignedTransactionParams,
) -> Result<UnsignedTransaction> {
debug!(
"Building unsigned transaction for opening position: {:?}",
params
);
let storage = self.trading_storage_contract();
let (base, quote) = params.symbol.split_once('/').ok_or_else(|| {
OstiumError::validation("Invalid symbol format, expected 'BASE/QUOTE'".to_string())
})?;
let pair_index = storage.get_pair_index(base, quote).await?;
let collateral = self.decimal_to_u256(params.size / params.leverage, 6)?;
let tp = match params.take_profit {
Some(p) => self.decimal_to_u192(p)?,
None => U192::ZERO,
};
let sl = match params.stop_loss {
Some(p) => self.decimal_to_u192(p)?,
None => U192::ZERO,
};
let trade = Trade {
collateral,
open_price: 0, tp: tp.try_into().map_err(|e| {
OstiumError::conversion(format!("Failed to convert take profit price: {}", e))
})?,
sl: sl.try_into().map_err(|e| {
OstiumError::conversion(format!("Failed to convert stop loss price: {}", e))
})?,
trader: trader_address,
leverage: (params.leverage * Decimal::from(100))
.to_u32()
.ok_or_else(|| OstiumError::validation("Leverage value too large".to_string()))?,
pair_index,
index: 0, buy: params.side == PositionSide::Long,
};
let slippage_p = self.decimal_to_u256(params.slippage_tolerance * Decimal::from(100), 0)?;
let trading = self.trading_contract();
trading
.open_trade_unsigned(trade, OpenOrderType::Market, slippage_p, tx_params)
.await
}
pub async fn close_position_unsigned(
&self,
params: ClosePositionParams,
tx_params: UnsignedTransactionParams,
) -> Result<UnsignedTransaction> {
debug!(
"Building unsigned transaction for closing position: {:?}",
params
);
let parts: Vec<&str> = params.position_id.split(':').collect();
if parts.len() != 3 {
return Err(OstiumError::validation(
"Invalid position ID format".to_string(),
));
}
let pair_index: u16 = parts[1].parse().map_err(|_| {
OstiumError::validation("Invalid pair index in position ID".to_string())
})?;
let index: u8 = parts[2]
.parse()
.map_err(|_| OstiumError::validation("Invalid index in position ID".to_string()))?;
let close_percentage = if let Some(_size) = params.size {
warn!("Partial close not fully implemented, closing 100%");
10000 } else {
10000 };
let trading = self.trading_contract();
trading
.close_trade_market_unsigned(pair_index, index, close_percentage, tx_params)
.await
}
pub async fn update_tp_sl_unsigned(
&self,
params: UpdateTPSLParams,
tx_params: UnsignedTransactionParams,
) -> Result<Vec<UnsignedTransaction>> {
debug!(
"Building unsigned transactions for updating TP/SL: {:?}",
params
);
let parts: Vec<&str> = params.position_id.split(':').collect();
if parts.len() != 3 {
return Err(OstiumError::validation(
"Invalid position ID format".to_string(),
));
}
let pair_index: u16 = parts[1].parse().map_err(|_| {
OstiumError::validation("Invalid pair index in position ID".to_string())
})?;
let index: u8 = parts[2]
.parse()
.map_err(|_| OstiumError::validation("Invalid index in position ID".to_string()))?;
let trading = self.trading_contract();
let mut transactions = Vec::new();
if let Some(tp) = params.take_profit {
let tp_u192 = self.decimal_to_u192(tp)?;
let tx = trading
.update_tp_unsigned(pair_index, index, tp_u192, tx_params.clone())
.await?;
transactions.push(tx);
}
if let Some(sl) = params.stop_loss {
let sl_u192 = self.decimal_to_u192(sl)?;
let tx = trading
.update_sl_unsigned(pair_index, index, sl_u192, tx_params.clone())
.await?;
transactions.push(tx);
}
Ok(transactions)
}
pub async fn place_advanced_order_unsigned(
&self,
params: AdvancedOrderParams,
trader_address: Address,
tx_params: UnsignedTransactionParams,
) -> Result<UnsignedTransaction> {
debug!(
"Building unsigned transaction for advanced order: {:?}",
params
);
match params.order_type {
OrderExecutionType::Limit | OrderExecutionType::Stop => {
if params.price.is_none() {
return Err(OstiumError::validation(
"Price is required for limit and stop orders".to_string(),
));
}
}
OrderExecutionType::Market => {
}
}
let storage = self.trading_storage_contract();
let (base, quote) = params.symbol.split_once('/').ok_or_else(|| {
OstiumError::validation("Invalid symbol format, expected 'BASE/QUOTE'".to_string())
})?;
let pair_index = storage.get_pair_index(base, quote).await?;
let collateral = self.decimal_to_u256(params.size / params.leverage, 6)?;
let tp = self.convert_optional_price(params.take_profit)?;
let sl = self.convert_optional_price(params.stop_loss)?;
let open_price = if let Some(price) = params.price {
self.decimal_to_u192(price)?.try_into().unwrap_or(0)
} else {
0 };
let trade = Trade {
collateral,
open_price,
tp: tp.try_into().unwrap_or(0),
sl: sl.try_into().unwrap_or(0),
trader: trader_address,
leverage: (params.leverage * Decimal::from(100))
.to_u32()
.unwrap_or(100),
pair_index,
index: 0,
buy: params.side == PositionSide::Long,
};
let order_type = match params.order_type {
OrderExecutionType::Market => OpenOrderType::Market,
OrderExecutionType::Limit => OpenOrderType::Limit,
OrderExecutionType::Stop => OpenOrderType::Stop,
};
let slippage_p = self.decimal_to_u256(params.slippage_tolerance * Decimal::from(100), 0)?;
let trading = self.trading_contract();
trading
.open_trade_unsigned(trade, order_type, slippage_p, tx_params)
.await
}
pub async fn cancel_order_unsigned(
&self,
params: CancelOrderParams,
tx_params: UnsignedTransactionParams,
) -> Result<UnsignedTransaction> {
debug!(
"Building unsigned transaction for canceling order: {:?}",
params
);
let parts: Vec<&str> = params.order_id.split(':').collect();
if parts.len() != 3 {
return Err(OstiumError::validation(
"Invalid order ID format, expected 'trader:pair_index:index'".to_string(),
));
}
let pair_index: u16 = parts[1]
.parse()
.map_err(|_| OstiumError::validation("Invalid pair index in order ID".to_string()))?;
let index: u8 = parts[2]
.parse()
.map_err(|_| OstiumError::validation("Invalid index in order ID".to_string()))?;
let trading = self.trading_contract();
trading
.cancel_open_limit_order_unsigned(pair_index, index, tx_params)
.await
}
}
impl OstiumClient {
pub async fn place_advanced_order(&self, params: AdvancedOrderParams) -> Result<TxHash> {
if !self.has_signer() {
return Err(OstiumError::wallet("No signer configured"));
}
debug!("Placing advanced order: {:?}", params);
match params.order_type {
OrderExecutionType::Limit | OrderExecutionType::Stop => {
if params.price.is_none() {
return Err(OstiumError::validation(
"Price is required for limit and stop orders".to_string(),
));
}
}
OrderExecutionType::Market => {
}
}
let trader = self.signer_address().unwrap();
let storage = self.trading_storage_contract();
let (base, quote) = params.symbol.split_once('/').ok_or_else(|| {
OstiumError::validation("Invalid symbol format, expected 'BASE/QUOTE'".to_string())
})?;
let pair_index = storage.get_pair_index(base, quote).await?;
let collateral = self.decimal_to_u256(params.size / params.leverage, 6)?;
let tp = self.convert_optional_price(params.take_profit)?;
let sl = self.convert_optional_price(params.stop_loss)?;
let open_price = if let Some(price) = params.price {
self.decimal_to_u192(price)?.try_into().unwrap_or(0)
} else {
0 };
let trade = Trade {
collateral,
open_price,
tp: tp.try_into().unwrap_or(0),
sl: sl.try_into().unwrap_or(0),
trader,
leverage: (params.leverage * Decimal::from(100))
.to_u32()
.unwrap_or(100), pair_index,
index: 0, buy: params.side == PositionSide::Long,
};
let order_type = match params.order_type {
OrderExecutionType::Market => OpenOrderType::Market,
OrderExecutionType::Limit => OpenOrderType::Limit,
OrderExecutionType::Stop => OpenOrderType::Stop,
};
let slippage_p = self.decimal_to_u256(params.slippage_tolerance * Decimal::from(100), 0)?;
let trading = self.trading_contract();
trading.open_trade(trade, order_type, slippage_p).await?;
Ok(alloy_primitives::TxHash::ZERO)
}
pub async fn place_limit_order(&self, params: LimitOrderParams) -> Result<TxHash> {
let advanced_params = AdvancedOrderParams {
symbol: params.symbol,
side: params.side,
size: params.size,
leverage: params.leverage,
order_type: OrderExecutionType::Limit,
price: Some(params.limit_price),
take_profit: params.take_profit,
stop_loss: params.stop_loss,
slippage_tolerance: Decimal::from(2) / Decimal::from(100), };
self.place_advanced_order(advanced_params).await
}
pub async fn place_stop_order(&self, params: StopOrderParams) -> Result<TxHash> {
let advanced_params = AdvancedOrderParams {
symbol: params.symbol,
side: params.side,
size: params.size,
leverage: params.leverage,
order_type: OrderExecutionType::Stop,
price: Some(params.stop_price),
take_profit: params.take_profit,
stop_loss: params.stop_loss,
slippage_tolerance: Decimal::from(2) / Decimal::from(100), };
self.place_advanced_order(advanced_params).await
}
pub async fn cancel_order(&self, params: CancelOrderParams) -> Result<TxHash> {
if !self.has_signer() {
return Err(OstiumError::wallet("No signer configured"));
}
debug!("Canceling order: {:?}", params);
let parts: Vec<&str> = params.order_id.split(':').collect();
if parts.len() != 3 {
return Err(OstiumError::validation(
"Invalid order ID format, expected 'trader:pair_index:index'".to_string(),
));
}
let pair_index: u16 = parts[1]
.parse()
.map_err(|_| OstiumError::validation("Invalid pair index in order ID".to_string()))?;
let index: u8 = parts[2]
.parse()
.map_err(|_| OstiumError::validation("Invalid index in order ID".to_string()))?;
let trading = self.trading_contract();
trading.cancel_open_limit_order(pair_index, index).await?;
Ok(alloy_primitives::TxHash::ZERO)
}
pub async fn update_limit_order(&self, params: UpdateLimitOrderParams) -> Result<TxHash> {
if !self.has_signer() {
return Err(OstiumError::wallet("No signer configured"));
}
debug!("Updating limit order: {:?}", params);
let parts: Vec<&str> = params.order_id.split(':').collect();
if parts.len() != 3 {
return Err(OstiumError::validation(
"Invalid order ID format, expected 'trader:pair_index:index'".to_string(),
));
}
let pair_index: u16 = parts[1]
.parse()
.map_err(|_| OstiumError::validation("Invalid pair index in order ID".to_string()))?;
let index: u8 = parts[2]
.parse()
.map_err(|_| OstiumError::validation("Invalid index in order ID".to_string()))?;
let storage = self.trading_storage_contract();
let trader = self.signer_address().unwrap();
let current_order = storage
.get_open_limit_order(trader, pair_index, index)
.await?;
let new_price = match params.limit_price {
Some(p) => self.decimal_to_u192(p)?,
None => U192::from(current_order.target_price),
};
let new_tp = match params.take_profit {
Some(p) => self.decimal_to_u192(p)?,
None => U192::from(current_order.tp),
};
let new_sl = match params.stop_loss {
Some(p) => self.decimal_to_u192(p)?,
None => U192::from(current_order.sl),
};
let trading = self.trading_contract();
trading
.update_open_limit_order(pair_index, index, new_price, new_tp, new_sl)
.await?;
Ok(alloy_primitives::TxHash::ZERO)
}
pub async fn validate_order_price(
&self,
symbol: &str,
order_type: OrderExecutionType,
price: Decimal,
) -> Result<bool> {
let current_price = self.get_price(symbol).await?.mark_price;
match order_type {
OrderExecutionType::Market => Ok(true), OrderExecutionType::Limit => {
let price_diff = (price - current_price).abs() / current_price;
Ok(price_diff <= Decimal::from(50) / Decimal::from(100)) }
OrderExecutionType::Stop => {
let price_diff = (price - current_price).abs() / current_price;
Ok(price_diff <= Decimal::from(50) / Decimal::from(100)) }
}
}
}
impl OstiumClient {
pub async fn get_pairs(&self) -> Result<Vec<TradingPair>> {
debug!("Fetching trading pairs");
let query = r#"
query GetTradingPairs {
pairs {
id
from
to
feed
spreadP
maxLeverage
volume
}
}
"#;
let response = self.graphql_query(query, None).await?;
let pairs = response
.get("pairs")
.and_then(|p| p.as_array())
.ok_or_else(|| OstiumError::network("Invalid pairs response".to_string()))?;
let mut trading_pairs = Vec::new();
for pair in pairs {
let id = pair.get("id").and_then(|v| v.as_str()).ok_or_else(|| {
OstiumError::network("Missing 'id' field in pair data".to_string())
})?;
let from = pair.get("from").and_then(|v| v.as_str()).ok_or_else(|| {
OstiumError::network("Missing 'from' field in pair data".to_string())
})?;
let to = pair.get("to").and_then(|v| v.as_str()).ok_or_else(|| {
OstiumError::network("Missing 'to' field in pair data".to_string())
})?;
let max_leverage = pair
.get("maxLeverage")
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(1);
trading_pairs.push(TradingPair {
id: id.to_string(),
base_asset: from.to_string(),
quote_asset: to.to_string(),
symbol: format!("{}/{}", from, to),
is_active: max_leverage > 0, min_position_size: Decimal::from(1), max_position_size: Decimal::from(1000000),
price_precision: 8,
quantity_precision: 8,
});
}
Ok(trading_pairs)
}
pub async fn get_price(&self, symbol: &str) -> Result<Price> {
debug!("Fetching price for symbol: {}", symbol);
let rest_url = "https://metadata-backend.ostium.io/PricePublish/latest-price";
let asset = symbol.replace("/", ""); let url = format!("{}?asset={}", rest_url, asset);
match self.rest_api_call(url).await {
Ok(price_data) => {
if let (Some(mid), Some(bid), Some(ask)) = (
price_data.get("mid").and_then(|p| p.as_f64()),
price_data.get("bid").and_then(|p| p.as_f64()),
price_data.get("ask").and_then(|p| p.as_f64()),
) {
let mark_price = Decimal::try_from(mid).map_err(|e| {
OstiumError::network(format!("Invalid price format: {}", e))
})?;
let _bid_price = Decimal::try_from(bid)
.map_err(|e| OstiumError::network(format!("Invalid bid format: {}", e)))?;
let _ask_price = Decimal::try_from(ask)
.map_err(|e| OstiumError::network(format!("Invalid ask format: {}", e)))?;
let high_24h = mark_price * Decimal::try_from(1.02).unwrap();
let low_24h = mark_price * Decimal::try_from(0.98).unwrap();
return Ok(Price {
symbol: symbol.to_string(),
mark_price,
index_price: mark_price, high_24h,
low_24h,
volume_24h: Decimal::from(1000000), timestamp: chrono::Utc::now(),
});
}
}
Err(e) => {
debug!("Failed to fetch price from REST API: {}", e);
}
}
let pairs = self.get_pairs().await?;
let pair = pairs
.iter()
.find(|p| p.symbol == symbol)
.ok_or_else(|| OstiumError::network(format!("Pair {} not found", symbol)))?;
Ok(Price {
symbol: pair.symbol.clone(),
mark_price: Decimal::from(50000), index_price: Decimal::from(50000),
high_24h: Decimal::from(52000),
low_24h: Decimal::from(48000),
volume_24h: Decimal::from(1000000),
timestamp: chrono::Utc::now(),
})
}
pub async fn get_trading_hours(&self, symbol: &str) -> Result<TradingHours> {
debug!("Fetching trading hours for symbol: {}", symbol);
let rest_url = "https://metadata-backend.ostium.io/trading-hours/asset-schedule";
let asset = symbol.replace("/", ""); let url = format!("{}?asset={}", rest_url, asset);
match self.rest_api_call(url).await {
Ok(hours_data) => {
if let Some(error) = hours_data.get("error") {
debug!("Trading hours API error: {}", error);
} else {
let is_open = hours_data
.get("isOpenNow")
.and_then(|v| v.as_bool())
.unwrap_or(true);
return Ok(TradingHours {
symbol: symbol.to_string(),
is_open,
next_open: None, next_close: None,
});
}
}
Err(e) => {
debug!("Failed to fetch trading hours from REST API: {}", e);
}
}
let is_crypto = symbol.contains("BTC")
|| symbol.contains("ETH")
|| symbol.contains("SOL")
|| symbol.contains("COIN");
Ok(TradingHours {
symbol: symbol.to_string(),
is_open: is_crypto, next_open: None,
next_close: None,
})
}
}
impl OstiumClient {
pub async fn get_balance(&self, address: Option<Address>) -> Result<Balance> {
let account = address
.or_else(|| self.signer_address())
.ok_or_else(|| OstiumError::wallet("No address provided and no signer configured"))?;
debug!("Fetching balance for address: {}", account);
let usdc = self.usdc_contract();
let balance = usdc.balance_of(account).await?;
let balance_decimal = Decimal::from(balance.to::<u128>()) / Decimal::from(1_000_000);
Ok(Balance {
asset: "USDC".to_string(),
available: balance_decimal,
locked: Decimal::ZERO, total: balance_decimal,
})
}
pub async fn get_positions(&self, address: Option<Address>) -> Result<Vec<Position>> {
let account = address
.or_else(|| self.signer_address())
.ok_or_else(|| OstiumError::wallet("No address provided and no signer configured"))?;
debug!("Fetching positions for address: {}", account);
let storage = self.trading_storage_contract();
let mut positions = Vec::new();
let pairs = self.get_pairs().await?;
for (pair_index, pair) in pairs.iter().enumerate() {
let pair_index = pair_index as u16;
match storage.get_trades_count(account, pair_index).await {
Ok(count) => {
for index in 0..count {
match storage.get_trade(account, pair_index, index).await {
Ok(trade) => {
let position = Position {
id: format!("{}:{}:{}", account, pair_index, index),
symbol: pair.symbol.clone(),
side: if trade.buy {
PositionSide::Long
} else {
PositionSide::Short
},
size: Decimal::from(trade.collateral.to::<u128>())
/ Decimal::from(1_000_000), entry_price: Decimal::from(trade.open_price)
/ 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>())
/ Decimal::from(1_000_000),
leverage: Decimal::from(trade.leverage) / Decimal::from(100), liquidation_price: None, take_profit: if trade.tp > 0 {
Some(
Decimal::from(trade.tp)
/ Decimal::from(10_u128.pow(18)),
)
} else {
None
},
stop_loss: if trade.sl > 0 {
Some(
Decimal::from(trade.sl)
/ Decimal::from(10_u128.pow(18)),
)
} else {
None
},
created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(),
};
positions.push(position);
}
Err(_) => {
continue;
}
}
}
}
Err(_) => {
continue;
}
}
}
Ok(positions)
}
pub async fn get_orders(&self, address: Option<Address>) -> Result<Vec<Order>> {
let account = address
.or_else(|| self.signer_address())
.ok_or_else(|| OstiumError::wallet("No address provided and no signer configured"))?;
debug!("Fetching orders for address: {}", account);
let storage = self.trading_storage_contract();
let mut orders = Vec::new();
let pairs = self.get_pairs().await?;
for (pair_index, pair) in pairs.iter().enumerate() {
let pair_index = pair_index as u16;
match storage
.get_open_limit_orders_count(account, pair_index)
.await
{
Ok(count) => {
for index in 0..count {
match storage
.get_open_limit_order(account, pair_index, index)
.await
{
Ok(limit_order) => {
let order_type = match limit_order.order_type {
1 => OrderType::Limit,
2 => OrderType::StopMarket,
_ => OrderType::Market,
};
let order = Order {
id: format!("{}:{}:{}", account, pair_index, index),
symbol: pair.symbol.clone(),
order_type,
side: if limit_order.buy {
PositionSide::Long
} else {
PositionSide::Short
},
size: Decimal::from(limit_order.collateral.to::<u128>())
/ Decimal::from(1_000_000), price: Some(
Decimal::from(limit_order.target_price)
/ Decimal::from(10_u128.pow(18)),
), stop_price: None, status: OrderStatus::Pending, filled_size: Decimal::ZERO, avg_fill_price: None,
created_at: chrono::DateTime::from_timestamp(
limit_order.created_at as i64,
0,
)
.unwrap_or_else(chrono::Utc::now),
updated_at: chrono::DateTime::from_timestamp(
limit_order.last_updated as i64,
0,
)
.unwrap_or_else(chrono::Utc::now),
};
orders.push(order);
}
Err(_) => {
continue;
}
}
}
}
Err(_) => {
continue;
}
}
}
Ok(orders)
}
}
#[allow(async_fn_in_trait)]
pub trait TradingApi {
async fn open_position(&self, params: OpenPositionParams) -> Result<TxHash>;
async fn close_position(&self, params: ClosePositionParams) -> Result<TxHash>;
async fn update_tp_sl(&self, params: UpdateTPSLParams) -> Result<TxHash>;
}
#[allow(async_fn_in_trait)]
pub trait MarketDataApi {
async fn get_pairs(&self) -> Result<Vec<TradingPair>>;
async fn get_price(&self, symbol: &str) -> Result<Price>;
async fn get_trading_hours(&self, symbol: &str) -> Result<TradingHours>;
}
#[allow(async_fn_in_trait)]
pub trait AccountApi {
async fn get_balance(&self, address: Option<Address>) -> Result<Balance>;
async fn get_positions(&self, address: Option<Address>) -> Result<Vec<Position>>;
async fn get_orders(&self, address: Option<Address>) -> Result<Vec<Order>>;
}
#[allow(async_fn_in_trait)]
pub trait AdvancedOrderApi {
async fn place_advanced_order(&self, params: AdvancedOrderParams) -> Result<TxHash>;
async fn place_limit_order(&self, params: LimitOrderParams) -> Result<TxHash>;
async fn place_stop_order(&self, params: StopOrderParams) -> Result<TxHash>;
async fn cancel_order(&self, params: CancelOrderParams) -> Result<TxHash>;
async fn update_limit_order(&self, params: UpdateLimitOrderParams) -> Result<TxHash>;
async fn validate_order_price(
&self,
symbol: &str,
order_type: OrderExecutionType,
price: Decimal,
) -> Result<bool>;
}
#[allow(async_fn_in_trait)]
pub trait UnsignedTransactionApi {
async fn open_position_unsigned(
&self,
params: OpenPositionParams,
trader_address: Address,
tx_params: UnsignedTransactionParams,
) -> Result<UnsignedTransaction>;
async fn close_position_unsigned(
&self,
params: ClosePositionParams,
tx_params: UnsignedTransactionParams,
) -> Result<UnsignedTransaction>;
async fn update_tp_sl_unsigned(
&self,
params: UpdateTPSLParams,
tx_params: UnsignedTransactionParams,
) -> Result<Vec<UnsignedTransaction>>;
async fn place_advanced_order_unsigned(
&self,
params: AdvancedOrderParams,
trader_address: Address,
tx_params: UnsignedTransactionParams,
) -> Result<UnsignedTransaction>;
async fn cancel_order_unsigned(
&self,
params: CancelOrderParams,
tx_params: UnsignedTransactionParams,
) -> Result<UnsignedTransaction>;
}
impl TradingApi for OstiumClient {
async fn open_position(&self, params: OpenPositionParams) -> Result<TxHash> {
OstiumClient::open_position(self, params).await
}
async fn close_position(&self, params: ClosePositionParams) -> Result<TxHash> {
OstiumClient::close_position(self, params).await
}
async fn update_tp_sl(&self, params: UpdateTPSLParams) -> Result<TxHash> {
OstiumClient::update_tp_sl(self, params).await
}
}
impl MarketDataApi for OstiumClient {
async fn get_pairs(&self) -> Result<Vec<TradingPair>> {
OstiumClient::get_pairs(self).await
}
async fn get_price(&self, symbol: &str) -> Result<Price> {
OstiumClient::get_price(self, symbol).await
}
async fn get_trading_hours(&self, symbol: &str) -> Result<TradingHours> {
OstiumClient::get_trading_hours(self, symbol).await
}
}
impl AccountApi for OstiumClient {
async fn get_balance(&self, address: Option<Address>) -> Result<Balance> {
OstiumClient::get_balance(self, address).await
}
async fn get_positions(&self, address: Option<Address>) -> Result<Vec<Position>> {
OstiumClient::get_positions(self, address).await
}
async fn get_orders(&self, address: Option<Address>) -> Result<Vec<Order>> {
OstiumClient::get_orders(self, address).await
}
}
impl AdvancedOrderApi for OstiumClient {
async fn place_advanced_order(&self, params: AdvancedOrderParams) -> Result<TxHash> {
self.place_advanced_order(params).await
}
async fn place_limit_order(&self, params: LimitOrderParams) -> Result<TxHash> {
self.place_limit_order(params).await
}
async fn place_stop_order(&self, params: StopOrderParams) -> Result<TxHash> {
self.place_stop_order(params).await
}
async fn cancel_order(&self, params: CancelOrderParams) -> Result<TxHash> {
self.cancel_order(params).await
}
async fn update_limit_order(&self, params: UpdateLimitOrderParams) -> Result<TxHash> {
self.update_limit_order(params).await
}
async fn validate_order_price(
&self,
symbol: &str,
order_type: OrderExecutionType,
price: Decimal,
) -> Result<bool> {
self.validate_order_price(symbol, order_type, price).await
}
}
impl UnsignedTransactionApi for OstiumClient {
async fn open_position_unsigned(
&self,
params: OpenPositionParams,
trader_address: Address,
tx_params: UnsignedTransactionParams,
) -> Result<UnsignedTransaction> {
OstiumClient::open_position_unsigned(self, params, trader_address, tx_params).await
}
async fn close_position_unsigned(
&self,
params: ClosePositionParams,
tx_params: UnsignedTransactionParams,
) -> Result<UnsignedTransaction> {
OstiumClient::close_position_unsigned(self, params, tx_params).await
}
async fn update_tp_sl_unsigned(
&self,
params: UpdateTPSLParams,
tx_params: UnsignedTransactionParams,
) -> Result<Vec<UnsignedTransaction>> {
OstiumClient::update_tp_sl_unsigned(self, params, tx_params).await
}
async fn place_advanced_order_unsigned(
&self,
params: AdvancedOrderParams,
trader_address: Address,
tx_params: UnsignedTransactionParams,
) -> Result<UnsignedTransaction> {
OstiumClient::place_advanced_order_unsigned(self, params, trader_address, tx_params).await
}
async fn cancel_order_unsigned(
&self,
params: CancelOrderParams,
tx_params: UnsignedTransactionParams,
) -> Result<UnsignedTransaction> {
OstiumClient::cancel_order_unsigned(self, params, tx_params).await
}
}