use crate::{
apis::{
configuration::Configuration,
order_api::{
OrderControllerCancelParams, OrderControllerListBySubaccountIdParams,
OrderControllerSubmitParams,
},
product_api::ProductControllerListParams,
subaccount_api::SubaccountControllerListByAccountParams,
},
async_client::{
funding::FundingClient,
linked_signer::LinkedSignerClient,
maintenance::MaintenanceClient,
order::OrderClient,
points::PointsClient,
position::PositionClient,
product::{self, ProductClient},
rate_limit::RateLimitClient,
referral::ReferralClient,
rpc::RpcClient,
subaccount::SubaccountClient,
time::TimeClient,
token::TokenClient,
whitelist::WhitelistClient,
},
enums::Environment,
models::{
CancelOrderDto, CancelOrderDtoData, CancelOrderResultDto, OrderSide, OrderStatus,
OrderType, ProductDto, SubaccountDto, SubmitOrderCreatedDto, SubmitOrderDto,
SubmitOrderDtoData, SubmitOrderLimitDtoData, TimeInForce,
},
signable_messages::{CancelOrder, TradeOrder},
signing::{hex_to_bytes32, to_scaled_e9, SigningContext},
};
use anyhow::Result;
use crate::signing::Eip712;
use ethers::{
signers::{LocalWallet, Signer},
utils::hex,
};
use log::debug;
use rust_decimal::Decimal;
use std::{collections::HashMap, str::FromStr};
use uuid::Uuid;
fn get_server_url(environment: &Environment) -> &str {
match environment {
Environment::Mainnet => "https://api.ethereal.trade",
Environment::Testnet => "https://api.etherealtest.net",
}
}
fn round_to_tick(value: Decimal, tick_size: Decimal) -> Result<Decimal> {
if tick_size.is_zero() {
return Ok(value);
}
let ticks = (value / tick_size).round();
Ok(ticks * tick_size)
}
#[macro_export]
macro_rules! with_signing_fields {
($signing_fn:ident, $ctx:expr, $struct:ident { $($rest:tt)* }) => {{
let s = $ctx.$signing_fn();
$struct {
sender: s.sender,
subaccount: s.subaccount,
nonce: s.nonce,
signed_at: s.signed_at as _,
$($rest)*
}
}};
}
pub struct HttpClient {
pub env: Environment,
config: Configuration,
pub wallet: LocalWallet,
pub address: String,
pub owner_address: Option<String>,
pub subaccounts: Vec<SubaccountDto>,
pub product_hashmap: HashMap<String, ProductDto>,
pub product_id_hashmap: HashMap<Uuid, ProductDto>,
}
impl HttpClient {
pub async fn new(env: Environment, private_key: &str, owner_address: Option<String>) -> Self {
let config = Configuration {
base_path: get_server_url(&env).to_string(),
..Default::default()
};
let wallet = private_key.parse::<LocalWallet>().unwrap();
let address = format!("{:?}", wallet.address());
let sender_address = owner_address
.clone()
.map(|s| s.to_string())
.unwrap_or_else(|| address.clone());
let subaccounts = SubaccountClient { config: &config }
.list_by_account(SubaccountControllerListByAccountParams {
sender: sender_address,
..Default::default()
})
.await
.unwrap()
.data;
let product_hashmap = product::ProductClient { config: &config }
.list(ProductControllerListParams {
..Default::default()
})
.await
.unwrap()
.data
.into_iter()
.map(|p| (p.display_ticker.clone(), p))
.collect();
let product_id_hashmap = product::ProductClient { config: &config }
.list(ProductControllerListParams {
..Default::default()
})
.await
.unwrap()
.data
.into_iter()
.map(|p| (p.id, p))
.collect();
Self {
env,
config,
wallet,
address,
owner_address,
subaccounts,
product_hashmap,
product_id_hashmap,
}
}
pub fn product(&self) -> ProductClient<'_> {
ProductClient {
config: &self.config,
}
}
pub fn funding(&self) -> FundingClient<'_> {
FundingClient {
config: &self.config,
}
}
pub fn linked_signer(&self) -> LinkedSignerClient<'_> {
LinkedSignerClient {
config: &self.config,
}
}
pub fn maintenance(&self) -> MaintenanceClient<'_> {
MaintenanceClient {
config: &self.config,
}
}
pub fn order(&self) -> OrderClient<'_> {
OrderClient {
config: &self.config,
}
}
pub fn points(&self) -> PointsClient<'_> {
PointsClient {
config: &self.config,
}
}
pub fn position(&self) -> PositionClient<'_> {
PositionClient {
config: &self.config,
}
}
pub fn referral(&self) -> ReferralClient<'_> {
ReferralClient {
config: &self.config,
}
}
pub fn rpc(&self) -> RpcClient<'_> {
RpcClient {
config: &self.config,
}
}
pub fn subaccount(&self) -> SubaccountClient<'_> {
SubaccountClient {
config: &self.config,
}
}
pub fn time(&self) -> TimeClient<'_> {
TimeClient {
config: &self.config,
}
}
pub fn token(&self) -> TokenClient<'_> {
TokenClient {
config: &self.config,
}
}
pub fn whitelist(&self) -> WhitelistClient<'_> {
WhitelistClient {
config: &self.config,
}
}
pub fn rate_limits(&self) -> RateLimitClient<'_> {
RateLimitClient {
config: &self.config,
}
}
#[allow(clippy::too_many_arguments)]
pub async fn submit_order(
&self,
ticker: &str,
quantity: Decimal,
price: Decimal,
side: OrderSide,
r#type: OrderType,
time_in_force: TimeInForce,
post_only: bool,
reduce_only: bool,
expires_at: Option<i64>,
) -> Result<SubmitOrderCreatedDto, Box<dyn std::error::Error>> {
if !self.product_hashmap.contains_key(ticker) {
return Err(format!("Ticker {ticker} not found").into());
}
if OrderType::Limit != r#type {
return Err("Only limit orders are supported in this method".into());
}
let product_info = self.product_hashmap.get(ticker).unwrap();
let tick_size = Decimal::from_str(&product_info.tick_size)?;
let lot_size = Decimal::from_str(&product_info.lot_size)?;
let price = round_to_tick(price, tick_size)?;
let quantity = round_to_tick(quantity, lot_size)?;
debug!(
"Submitting order with quantity: {}, price: {}, side: {:?}, type: {:?}, time_in_force: {:?}, post_only: {}, reduce_only: {}, expires_at: {:?}",
quantity, price, side, r#type, time_in_force, post_only, reduce_only, expires_at
);
let ctx = SigningContext::new(&self.wallet, &self.subaccounts[0]);
let message = with_signing_fields!(
eip_signing_fields,
ctx,
TradeOrder {
quantity: to_scaled_e9(quantity)?,
price: to_scaled_e9(price)?,
reduce_only,
side: side as u8,
engine_type: product_info.engine_type.to_string().parse()?,
product_id: product_info.onchain_id.to_string().parse()?,
}
);
let signature = message.sign(self.env, &self.wallet)?;
let order_dto = with_signing_fields!(
dto_signing_fields,
ctx,
SubmitOrderLimitDtoData {
quantity,
price,
side,
onchain_id: product_info.onchain_id,
engine_type: product_info.engine_type,
reduce_only: Some(reduce_only),
post_only,
expires_at: expires_at.map(|ts| ts as f64),
time_in_force,
r#type,
..Default::default()
}
);
let dto = SubmitOrderDto {
data: SubmitOrderDtoData::SubmitOrderLimitDtoData(order_dto),
signature: "0x".to_string() + &hex::encode(signature.to_vec()),
};
let result = self
.order()
.submit(OrderControllerSubmitParams {
submit_order_dto: dto,
})
.await;
match result {
Ok(response) => Ok(response),
Err(e) => Err(Box::new(e)),
}
}
pub async fn cancel_orders(
&self,
order_ids: Vec<String>,
) -> Result<Vec<CancelOrderResultDto>, Box<dyn std::error::Error>> {
let subaccount = &self.subaccounts[0];
let ctx = SigningContext::new(&self.wallet, &self.subaccounts[0]);
let message = CancelOrder {
sender: self.address.clone().parse()?,
subaccount: hex_to_bytes32(&subaccount.name.clone())?,
nonce: ctx.nonce, };
let signature = message.sign(self.env, &self.wallet)?;
let ids: Vec<Uuid> = order_ids
.iter()
.map(|id| Uuid::parse_str(id).unwrap())
.collect();
let cancel_result = self
.order()
.cancel(OrderControllerCancelParams {
cancel_order_dto: CancelOrderDto {
data: CancelOrderDtoData {
subaccount: subaccount.name.clone(),
sender: self.address.to_string(),
nonce: ctx.nonce.to_string(),
order_ids: ids.into(),
..Default::default()
},
signature: "0x".to_string() + &hex::encode(signature.to_vec()),
},
})
.await;
match cancel_result {
Err(e) => Err(Box::new(e)),
Ok(result) => Ok(result.data),
}
}
pub async fn get_open_orders(
&self,
) -> Result<Vec<crate::models::OrderDto>, Box<dyn std::error::Error>> {
let orders = self
.order()
.list_by_subaccount_id(OrderControllerListBySubaccountIdParams {
subaccount_id: self.subaccounts[0].id.clone().to_string(),
..Default::default()
})
.await?
.data;
let open_orders = orders
.into_iter()
.filter(|order| order.status == OrderStatus::New);
Ok(open_orders.collect())
}
}