diff --git a/Cargo.lock b/Cargo.lock index 89c569a..48764de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2927,6 +2927,8 @@ dependencies = [ "tokio", "tokio-tungstenite 0.26.2", "tokio-util", + "tracing", + "tracing-subscriber", ] [[package]] @@ -3321,7 +3323,7 @@ dependencies = [ "lalrpop-util", "petgraph", "regex", - "regex-syntax", + "regex-syntax 0.8.5", "string_cache", "term", "tiny-keccak", @@ -3335,7 +3337,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" dependencies = [ - "regex-automata", + "regex-automata 0.4.9", ] [[package]] @@ -3456,6 +3458,15 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matchit" version = "0.8.4" @@ -3552,6 +3563,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -3727,6 +3748,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parity-scale-codec" version = "3.7.5" @@ -4082,7 +4109,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", - "regex-syntax", + "regex-syntax 0.8.5", "rusty-fork", "tempfile", "unarray", @@ -4243,8 +4270,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -4255,9 +4291,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.5" @@ -5000,6 +5042,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -5388,6 +5439,15 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "threadpool" version = "1.8.1" @@ -5692,6 +5752,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", + "valuable", ] [[package]] @@ -5706,6 +5767,48 @@ dependencies = [ "tracing", ] +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + [[package]] name = "try-lock" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index 476f577..127c6db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,12 +12,16 @@ path = "src/lib.rs" [[bin]] name = "strat" -path = "src/bin/strategy.rs" +path = "src/bin/dn_strat/main.rs" [[bin]] name = "deployer" path = "src/bin/perp_deployer.rs" +[[bin]] +name = "dex" +path = "src/bin/perp_dex.rs" + [dependencies] alloy = "1.0.9" anyhow = "1.0.98" @@ -41,3 +45,5 @@ thiserror = "2.0.12" tokio = "1.45.1" tokio-tungstenite = {version = "0.26.2",features = ["native-tls"] } tokio-util = "0.7.15" +tracing = "0.1.41" +tracing-subscriber = {version = "0.3.19", features = ["json","env-filter"] } diff --git a/Makefile b/Makefile index 991476b..72b6bb9 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,9 @@ run-strat: run-deployer: cargo run --bin deployer + +run-dex: + cargo run --bin dex debug: cargo build diff --git a/src/config.rs b/src/bin/dn_strat/config.rs similarity index 85% rename from src/config.rs rename to src/bin/dn_strat/config.rs index 090d3f3..e61092e 100644 --- a/src/config.rs +++ b/src/bin/dn_strat/config.rs @@ -11,9 +11,6 @@ pub struct Config { #[envconfig(from = "USER_ADDRESS")] pub user_address: String, - #[envconfig(from = "EXISTING_ORDER_ID")] - pub existing_order_id: String, - #[envconfig(from = "BOT_URL")] pub bot_url: String, diff --git a/src/handlers.rs b/src/bin/dn_strat/handlers.rs similarity index 92% rename from src/handlers.rs rename to src/bin/dn_strat/handlers.rs index a08d9f5..66e43ad 100644 --- a/src/handlers.rs +++ b/src/bin/dn_strat/handlers.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use axum::{Json, extract::State}; use serde::{Deserialize, Serialize}; -use crate::services::{CurrentUserPositionResponse, OpenPositionRequest, StrategyManagerService}; +use crate::service::{CurrentUserPositionResponse, OpenPositionRequest, StrategyManagerService}; #[derive(Deserialize, Serialize)] pub struct ResponseJson { diff --git a/src/bin/strategy.rs b/src/bin/dn_strat/main.rs similarity index 83% rename from src/bin/strategy.rs rename to src/bin/dn_strat/main.rs index e341029..a938e77 100644 --- a/src/bin/strategy.rs +++ b/src/bin/dn_strat/main.rs @@ -1,17 +1,36 @@ -use std::sync::Arc; use std::time::Duration; +use std::{env, sync::Arc}; use alloy::primitives::Address; use envconfig::Envconfig; use hyperqit::*; -use log::info; use tokio::signal; use tokio_util::sync::CancellationToken; +use tracing::info; + +mod config; +mod handlers; +mod notifier; +mod router; +mod service; +mod strategy; + +use config::Config; +use notifier::NotifierService; +use router::create_router; +use service::StrategyManagerService; +use strategy::{Asset, Strategy}; +use tracing_subscriber::EnvFilter; #[tokio::main] async fn main() { - env_logger::init(); - let config = hyperqit::Config::init_from_env().unwrap(); + tracing_subscriber::fmt() + .json() + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) + .init(); + let config = Config::init_from_env().unwrap(); let signer = Signers::Local(hyperqit::LocalWallet::signer(config.private_key)); let user_address: Address = config.user_address.parse().unwrap(); diff --git a/src/bin/dn_strat/notifier/mod.rs b/src/bin/dn_strat/notifier/mod.rs new file mode 100644 index 0000000..e19dc6b --- /dev/null +++ b/src/bin/dn_strat/notifier/mod.rs @@ -0,0 +1,3 @@ +mod notifier; + +pub use notifier::*; diff --git a/src/notifier/mod.rs b/src/bin/dn_strat/notifier/notifier.rs similarity index 98% rename from src/notifier/mod.rs rename to src/bin/dn_strat/notifier/notifier.rs index 36276a3..10b22eb 100644 --- a/src/notifier/mod.rs +++ b/src/bin/dn_strat/notifier/notifier.rs @@ -1,6 +1,6 @@ -use log::{error, info}; use reqwest::Client; use serde::Serialize; +use tracing::{error, info}; #[derive(Debug, Clone)] pub struct NotifierService { diff --git a/src/router.rs b/src/bin/dn_strat/router.rs similarity index 92% rename from src/router.rs rename to src/bin/dn_strat/router.rs index b37e6c2..55d9fe1 100644 --- a/src/router.rs +++ b/src/bin/dn_strat/router.rs @@ -7,7 +7,7 @@ use axum::{ use crate::{ handlers::{close_position, create_new_position, get_current_position}, - services::StrategyManagerService, + service::StrategyManagerService, }; pub fn create_router(manager_svc: Arc) -> Router { diff --git a/src/services/manager.rs b/src/bin/dn_strat/service/manager.rs similarity index 94% rename from src/services/manager.rs rename to src/bin/dn_strat/service/manager.rs index 095b7d4..db7fd8d 100644 --- a/src/services/manager.rs +++ b/src/bin/dn_strat/service/manager.rs @@ -3,7 +3,9 @@ use std::sync::Arc; use anyhow::Ok; use serde::{Deserialize, Serialize}; -use crate::{Amount, Asset, Strategy, StrategyState, errors::Result}; +use hyperqit::Result; + +use crate::strategy::{Amount, Asset, Strategy, StrategyState}; #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] diff --git a/src/services/mod.rs b/src/bin/dn_strat/service/mod.rs similarity index 100% rename from src/services/mod.rs rename to src/bin/dn_strat/service/mod.rs diff --git a/src/strategy/mod.rs b/src/bin/dn_strat/strategy/mod.rs similarity index 100% rename from src/strategy/mod.rs rename to src/bin/dn_strat/strategy/mod.rs diff --git a/src/strategy/strategy.rs b/src/bin/dn_strat/strategy/strategy.rs similarity index 88% rename from src/strategy/strategy.rs rename to src/bin/dn_strat/strategy/strategy.rs index d08e8f8..36bdfac 100644 --- a/src/strategy/strategy.rs +++ b/src/bin/dn_strat/strategy/strategy.rs @@ -1,20 +1,20 @@ use crate::{ - Amount, AssetPosition, Balance, HyperliquidClient, PerpMarketInfo, SpotMarketInfo, - StrategyStatus, create_unified_market_info, - errors::{self, Result}, - find_market_by_name, + AssetPosition, Balance, CmpError, Errors, HyperliquidClient, PerpMarketInfo, Result, + SpotMarketInfo, create_unified_market_info, find_market_by_name, notifier::NotifierService, strategy::{ + Amount, Asset::{self, CommonAsset, WithPerpAndSpot}, - Position, StrategyState, + Position, StrategyState, StrategyStatus, }, }; + use std::time::{Duration, SystemTime}; -use log::{info, warn}; use std::result::Result::Ok; use tokio::time; use tokio_util::sync::CancellationToken; +use tracing::{info, warn}; pub struct Strategy { asset: Asset, @@ -102,7 +102,7 @@ impl Strategy { } let current_mark_px: f64 = perp_info.mark_px.parse()?; - let user_pos = user_state.position.ok_or(errors::Errors::DataError( + let user_pos = user_state.position.ok_or(Errors::DataError( "user_state".to_owned(), "perp_position".to_owned(), ))?; @@ -171,14 +171,14 @@ impl Strategy { match &self.asset { CommonAsset(key) => { let common_info = find_market_by_name(&unified_info, key.as_str()).ok_or( - errors::Errors::DataError("unified_info".to_owned(), key.to_string()), + Errors::DataError("unified_info".to_owned(), key.to_string()), )?; Ok(( - common_info.perp.clone().ok_or(errors::Errors::DataError( + common_info.perp.clone().ok_or(Errors::DataError( "perp_info_from_common".to_owned(), key.to_string(), ))?, - common_info.spot.clone().ok_or(errors::Errors::DataError( + common_info.spot.clone().ok_or(Errors::DataError( "spot_info_from_common".to_owned(), key.to_string(), ))?, @@ -188,14 +188,14 @@ impl Strategy { let perp_item = unified_info .perp_markets .get(perp_asset_name.as_str()) - .ok_or(errors::Errors::DataError( + .ok_or(Errors::DataError( "from_perp_info".to_owned(), perp_asset_name.to_string(), ))?; let spot_item = unified_info .spot_markets .get(spot_asset_name.as_str()) - .ok_or(errors::Errors::DataError( + .ok_or(Errors::DataError( "from_spot_info".to_owned(), spot_asset_name.to_string(), ))?; @@ -243,14 +243,10 @@ impl Strategy { position: None, }); } - let perp_pos = current_perp_pos.ok_or(errors::Errors::DataError( - "user_perp_pos".to_owned(), - perp_key, - ))?; - let spot_pos = current_spot_pos.ok_or(errors::Errors::DataError( - "user_spot_pos".to_owned(), - spot_key, - ))?; + let perp_pos = + current_perp_pos.ok_or(Errors::DataError("user_perp_pos".to_owned(), perp_key))?; + let spot_pos = + current_spot_pos.ok_or(Errors::DataError("user_spot_pos".to_owned(), spot_key))?; let perp_price: f64 = perp_info .clone() .mid_px @@ -318,20 +314,16 @@ impl Strategy { .clone() .is_some_and(|spot| spot.total.parse::().unwrap_or(0f64) < self.dust_threshold) { - return Err(errors::CmpError { + return Err(CmpError { expected: StrategyStatus::Active, actual: StrategyStatus::InActive, } .into()); } - let perp_pos = current_perp_pos.ok_or(errors::Errors::DataError( - "user_perp_pos".to_owned(), - perp_key, - ))?; - let spot_pos = current_spot_pos.ok_or(errors::Errors::DataError( - "user_spot_pos".to_owned(), - spot_key, - ))?; + let perp_pos = + current_perp_pos.ok_or(Errors::DataError("user_perp_pos".to_owned(), perp_key))?; + let spot_pos = + current_spot_pos.ok_or(Errors::DataError("user_spot_pos".to_owned(), spot_key))?; let (perp_info, spot_info) = self.get_market_data().await?; let spot_mid: f64 = spot_info .mid_px @@ -355,7 +347,7 @@ impl Strategy { perp_decimals, ) .await - .map_err(|err| errors::Errors::PlaceOrderError(err.to_string()))?; + .map_err(|err| Errors::PlaceOrderError(err.to_string()))?; self.executor .create_position_with_size( @@ -369,7 +361,7 @@ impl Strategy { spot_decimals, ) .await - .map_err(|err| errors::Errors::PlaceOrderError(err.to_string()))?; + .map_err(|err| Errors::PlaceOrderError(err.to_string()))?; self.notifier.notify_text("exit", "completed").await; Ok(()) @@ -378,7 +370,7 @@ impl Strategy { pub async fn enter(&self, amount: Amount) -> Result<()> { let existing = self.state().await?; if existing.status == StrategyStatus::Active { - return Err(errors::CmpError { + return Err(CmpError { expected: StrategyStatus::InActive, actual: existing.status, } @@ -388,7 +380,7 @@ impl Strategy { let (perp_info, spot_info) = self.get_market_data().await?; let current_rate: f64 = perp_info.funding.parse()?; if current_rate < 0.0 { - return Err(errors::Errors::FundRateNegative(current_rate).into()); + return Err(Errors::FundRateNegative(current_rate).into()); } let is_size_usd = matches!(amount, Amount::Usd(_)); @@ -423,7 +415,7 @@ impl Strategy { spot_decimals, ) .await - .map_err(|err| errors::Errors::PlaceOrderError(err.to_string()))?; + .map_err(|err| Errors::PlaceOrderError(err.to_string()))?; self.executor .create_position_with_size_in_usd( @@ -437,7 +429,7 @@ impl Strategy { perp_decimals, ) .await - .map_err(|err| errors::Errors::PlaceOrderError(err.to_string()))?; + .map_err(|err| Errors::PlaceOrderError(err.to_string()))?; } else { self.executor .create_position_with_size( @@ -451,7 +443,7 @@ impl Strategy { spot_decimals, ) .await - .map_err(|err| errors::Errors::PlaceOrderError(err.to_string()))?; + .map_err(|err| Errors::PlaceOrderError(err.to_string()))?; self.executor .create_position_with_size( @@ -465,7 +457,7 @@ impl Strategy { perp_decimals, ) .await - .map_err(|err| errors::Errors::PlaceOrderError(err.to_string()))?; + .map_err(|err| Errors::PlaceOrderError(err.to_string()))?; } self.notifier.notify_text("enter", "completed").await; Ok(()) diff --git a/src/bin/perp_deployer.rs b/src/bin/perp_deployer.rs index 3fe4213..46e5946 100644 --- a/src/bin/perp_deployer.rs +++ b/src/bin/perp_deployer.rs @@ -1,11 +1,43 @@ use alloy::primitives::Address; -use envconfig::Envconfig; + use hyperqit::*; +use envconfig::Envconfig; +use tracing_subscriber::EnvFilter; + +#[derive(Envconfig)] +pub struct Config { + #[envconfig(from = "PRIVATE_KEY")] + pub private_key: String, + + #[envconfig(from = "RUST_LOG")] + pub log_level: String, + + #[envconfig(from = "USER_ADDRESS")] + pub user_address: String, + + #[envconfig(from = "EXISTING_ORDER_ID")] + pub existing_order_id: String, + + #[envconfig(from = "BOT_URL")] + pub bot_url: String, + + #[envconfig(from = "CHECK_EVERY")] + pub check_every: u64, + + #[envconfig(from = "BIND_ADDR")] + pub bind_addr: String, +} + #[tokio::main] async fn main() { - env_logger::init(); - let config = hyperqit::Config::init_from_env().unwrap(); + tracing_subscriber::fmt() + .json() + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) + .init(); + let config = Config::init_from_env().unwrap(); let signer = Signers::Local(hyperqit::LocalWallet::signer(config.private_key)); let user_address: Address = config.user_address.parse().unwrap(); diff --git a/src/bin/perp_dex.rs b/src/bin/perp_dex.rs index bf62f6a..b81603b 100644 --- a/src/bin/perp_dex.rs +++ b/src/bin/perp_dex.rs @@ -1,11 +1,41 @@ use alloy::primitives::Address; use envconfig::Envconfig; use hyperqit::*; +use tracing_subscriber::EnvFilter; + +#[derive(Envconfig)] +pub struct Config { + #[envconfig(from = "PRIVATE_KEY")] + pub private_key: String, + + #[envconfig(from = "RUST_LOG")] + pub log_level: String, + + #[envconfig(from = "USER_ADDRESS")] + pub user_address: String, + + #[envconfig(from = "EXISTING_ORDER_ID")] + pub existing_order_id: String, + + #[envconfig(from = "BOT_URL")] + pub bot_url: String, + + #[envconfig(from = "CHECK_EVERY")] + pub check_every: u64, + + #[envconfig(from = "BIND_ADDR")] + pub bind_addr: String, +} #[tokio::main] async fn main() { - env_logger::init(); - let config = hyperqit::Config::init_from_env().unwrap(); + tracing_subscriber::fmt() + .json() + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) + .init(); + let config = Config::init_from_env().unwrap(); let signer = Signers::Local(hyperqit::LocalWallet::signer(config.private_key)); let user_address: Address = config.user_address.parse().unwrap(); @@ -38,4 +68,7 @@ async fn main() { .await .unwrap(); println!("{:?}", user_info); + + let response = executor.cancel_order(69, 1004).await.unwrap(); + println!("{:?}", response) } diff --git a/src/hl/client.rs b/src/hl/client.rs index f407f4b..7769d0a 100644 --- a/src/hl/client.rs +++ b/src/hl/client.rs @@ -1,5 +1,5 @@ -use log::{debug, error, info}; use std::time::SystemTime; +use tracing::{debug, error, info}; use alloy::primitives::{Address, FixedBytes}; @@ -17,8 +17,8 @@ use crate::hl::user_info::{ use crate::hl::utils::*; use crate::hl::{Actions, TransferRequest}; use crate::{ - CancelOrder, HyperLiquidSigningHash, Order, OrderRequest, PerpDeployAction, SendAssetRequest, - Signers, + BulkCancel, BulkOrder, CancelOrder, ExchangeOrderResponse, HyperLiquidSigningHash, Order, + OrderRequest, PerpDeployAction, SendAssetRequest, Signers, }; pub trait HlAgentWallet { @@ -35,7 +35,7 @@ pub struct HyperliquidClient { impl HyperliquidClient { pub fn new(network: Network, signer: Signers, user: Address) -> Self { - info!("creating hyperliquid client for {} on {:?}", user, network); + debug!("creating hyperliquid client for {} on {:?}", user, network); HyperliquidClient { client: reqwest::Client::new(), signer, @@ -79,6 +79,7 @@ impl HyperliquidClient { let out: FundingHistory = serde_json::from_str(body.as_str())?; debug!("retrieved funding history with {} entries", out.len()); + Ok(out) } @@ -130,7 +131,7 @@ impl HyperliquidClient { let out: ExchangeResponse = serde_json::from_str(body.as_str())?; debug!("leverage update response: {:?}", out); - info!("successfully updated leverage for asset {}", a); + Ok(()) } @@ -144,8 +145,8 @@ impl HyperliquidClient { reduce_only: bool, slippage: f64, sz_decimals: i32, - ) -> Result<()> { - info!( + ) -> Result { + debug!( "creating {} position for asset {} with ${} USD (price: {}, slippage: {}%)", if is_buy { "buy" } else { "sell" }, a, @@ -163,8 +164,7 @@ impl HyperliquidClient { slippage, ); - self.create_position(a, is_buy, px, sz, reduce_only).await?; - Ok(()) + self.create_position(a, is_buy, px, sz, reduce_only).await } pub async fn create_position_with_size( @@ -177,8 +177,8 @@ impl HyperliquidClient { reduce_only: bool, slippage: f64, sz_decimals: i32, - ) -> Result<()> { - info!( + ) -> Result { + debug!( "creating {} position for asset {} with size {} (price: {}, slippage: {}%)", if is_buy { "buy" } else { "sell" }, a, @@ -196,52 +196,7 @@ impl HyperliquidClient { slippage, ); - self.create_position(a, is_buy, px, sz, reduce_only).await?; - Ok(()) - } - - pub async fn create_position_raw(&self, orders: Vec) -> Result<()> { - let nonce = self.nonce_manager.get_next_nonce(); - - let action: Actions = Actions::Order(crate::BulkOrder { - orders: orders, - grouping: "na".to_string(), - }); - - let is_mainnet = self.network == Network::Mainnet; - let (to_sign, domain) = generate_action_params(&action, is_mainnet, nonce)?; - let hash = to_sign.hyperliquid_signing_hash(&domain); - let signature: SignedMessage = self.signer.sign_order(hash).await?; - - let payload = ExchangeRequest { - action: serde_json::to_value(action)?, - signature, - nonce, - }; - - debug!( - "order payload: {}", - serde_json::to_string(&payload).unwrap() - ); - - let resp = self - .client - .post(format!("{}/exchange", Into::::into(self.network))) - .json(&payload) - .send() - .await?; - - let status_code = resp.status().as_u16(); - let body = resp.text().await?; - if status_code != 200 { - error!("failed to create position: {} - {}", status_code, body); - return Err(Errors::HyperLiquidApiError(status_code, body).into()); - } - - let out: ExchangeResponse = serde_json::from_str(body.as_str())?; - info!("order response: {:?}", out); - info!("successfully placed batch order)"); - Ok(()) + self.create_position(a, is_buy, px, sz, reduce_only).await } async fn create_position( @@ -251,10 +206,8 @@ impl HyperliquidClient { px: String, sz: String, reduce_only: bool, - ) -> Result<()> { - let nonce = self.nonce_manager.get_next_nonce(); - - let action: Actions = Actions::Order(crate::BulkOrder { + ) -> Result { + self.create_position_raw(crate::BulkOrder { orders: vec![OrderRequest { asset: a, is_buy, @@ -265,7 +218,14 @@ impl HyperliquidClient { cloid: None, }], grouping: "na".to_string(), - }); + }) + .await + } + + pub async fn create_position_raw(&self, orders: BulkOrder) -> Result { + let nonce: u64 = self.nonce_manager.get_next_nonce(); + + let action: Actions = Actions::Order(orders); let is_mainnet = self.network == Network::Mainnet; let (to_sign, domain) = generate_action_params(&action, is_mainnet, nonce)?; @@ -293,24 +253,21 @@ impl HyperliquidClient { let status_code = resp.status().as_u16(); let body = resp.text().await?; if status_code != 200 { - error!("failed to create position: {} - {}", status_code, body); + debug!("failed to create position: {} - {}", status_code, body); return Err(Errors::HyperLiquidApiError(status_code, body).into()); } let out: ExchangeResponse = serde_json::from_str(body.as_str())?; - info!("order response: {:?}", out); - info!( - "successfully placed {} order for asset {} (px: {}, sz: {})", - if is_buy { "buy" } else { "sell" }, - a, - px, - sz - ); - Ok(()) + debug!("order response: {:?}", out); + if out.status != "ok".to_string() { + return Err(Errors::HyperLiquidApiError(100, out.response.to_string()).into()); + } + + Ok(serde_json::from_value(out.response)?) } pub async fn transfer_usd_to_spot(&self, amount: u64) -> Result<()> { - info!("transferring ${} USD to spot", amount); + debug!("transferring ${} USD to spot", amount); let nonce = self.nonce_manager.get_next_nonce(); @@ -325,11 +282,8 @@ impl HyperliquidClient { debug!("transfer request: {:?}", transfer_req); let (to_sign, domain) = generate_transfer_params(&transfer_req)?; - debug!("transfer domain: {:?}", domain); - let hash = to_sign.hyperliquid_signing_hash(&domain); let signature = self.signer.sign_order(hash).await?; - let payload = ExchangeRequest { nonce, signature, @@ -357,12 +311,15 @@ impl HyperliquidClient { let out: ExchangeResponse = serde_json::from_str(body.as_str())?; debug!("transfer response: {:?}", out); - info!("successfully transferred ${} USD to spot", amount); + if out.status != "ok".to_string() { + return Err(Errors::HyperLiquidApiError(100, out.response.to_string()).into()); + } + Ok(()) } pub async fn send_asset_to_dex(&self, req: SendAssetRequest) -> Result<()> { - info!("transferring to dex {}", req.dst_dex.clone()); + debug!("transferring to dex {}", req.dst_dex.clone()); let mut transfer_req = req.clone(); let nonce = self.nonce_manager.get_next_nonce(); transfer_req.nonce = nonce; @@ -402,7 +359,10 @@ impl HyperliquidClient { let out: ExchangeResponse = serde_json::from_str(body.as_str())?; debug!("send asset response: {:?}", out); - info!("successfully sent asset"); + if out.status != "ok".to_string() { + return Err(Errors::HyperLiquidApiError(100, out.response.to_string()).into()); + } + Ok(()) } @@ -486,7 +446,7 @@ impl HyperliquidClient { debug!("user spot response: {}", body); let out: UserSpotPosition = serde_json::from_str(body.as_str())?; - debug!("retrieved spot positions for user {}", self.user); + Ok(out) } @@ -514,19 +474,23 @@ impl HyperliquidClient { return Err(Errors::HyperLiquidApiError(status_code, body).into()); } - let out: UserPerpPosition = serde_json::from_str(body.as_str())?; - debug!("retrieved perp positions for user {}", self.user); - Ok(out) + Ok(serde_json::from_str(body.as_str())?) } - pub async fn cancel_order(&self, oid: i64, a: u32) -> Result<()> { - info!("cancelling order {} for asset {}", oid, a); + pub async fn cancel_order(&self, oid: i64, a: u32) -> Result { + debug!("cancelling order {} for asset {}", oid, a); - let nonce = self.nonce_manager.get_next_nonce(); - - let action: Actions = Actions::Cancel(crate::BulkCancel { + self.cancel_order_raw(BulkCancel { cancels: vec![CancelOrder { asset: a, oid }], - }); + }) + .await + } + + pub async fn cancel_order_raw(&self, orders: BulkCancel) -> Result { + debug!("cancelling order raw {:?}", orders); + + let nonce = self.nonce_manager.get_next_nonce(); + let action: Actions = Actions::Cancel(orders); let is_mainnet = self.network == Network::Mainnet; let (to_sign, domain) = generate_action_params(&action, is_mainnet, nonce)?; @@ -560,8 +524,8 @@ impl HyperliquidClient { let out: ExchangeResponse = serde_json::from_str(body.as_str())?; debug!("cancel order response: {:?}", out); - info!("successfully cancelled order {} for asset {}", oid, a); - Ok(()) + + Ok(serde_json::from_value(out.response)?) } pub async fn perp_deploy_action(&self, deploy_params: PerpDeployAction) -> Result<()> { @@ -606,6 +570,7 @@ impl HyperliquidClient { let out: ExchangeResponse = serde_json::from_str(body.as_str())?; debug!("perp deploy action response: {:?}", out); + Ok(()) } } diff --git a/src/hl/exchange.rs b/src/hl/exchange.rs index a3b58ee..b4b21fc 100644 --- a/src/hl/exchange.rs +++ b/src/hl/exchange.rs @@ -16,8 +16,8 @@ use crate::{ #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct ExchangeResponse { - status: String, - response: serde_json::Value, + pub status: String, + pub response: serde_json::Value, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/hl/info.rs b/src/hl/info.rs index 4de9b43..c01f2ec 100644 --- a/src/hl/info.rs +++ b/src/hl/info.rs @@ -1,6 +1,6 @@ -use log::debug; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use tracing::debug; pub type PerpetualsInfo = (UniverseInfo, Vec); diff --git a/src/hl/mod.rs b/src/hl/mod.rs index f6100c0..85ad848 100644 --- a/src/hl/mod.rs +++ b/src/hl/mod.rs @@ -4,6 +4,7 @@ mod exchange; mod info; mod message; mod nonce; +mod response; mod user_info; mod utils; @@ -11,5 +12,6 @@ pub use actions::*; pub use client::{HlAgentWallet, HyperliquidClient}; pub use info::*; pub use message::*; +pub use response::*; pub use user_info::*; pub use utils::*; diff --git a/src/hl/response.rs b/src/hl/response.rs new file mode 100644 index 0000000..a6a8868 --- /dev/null +++ b/src/hl/response.rs @@ -0,0 +1,140 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct RestingOrder { + pub oid: u64, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct FilledOrder { + pub total_sz: String, + pub avg_px: String, + pub oid: u64, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub enum OrderStatus { + Resting(RestingOrder), + Error(String), + Filled(FilledOrder), + Success, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct OrderStatusResponse { + pub statuses: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase", tag = "type", content = "data")] +pub enum ExchangeOrderResponse { + Order(OrderStatusResponse), + Default, + Cancel(OrderStatusResponse), +} + +#[cfg(test)] +mod test { + use crate::hl::exchange::ExchangeResponse; + + use super::*; + + #[test] + fn test_all_exchange_responses() { + let test_cases = vec![ + r#"{ + "status":"ok", + "response":{ + "type":"order", + "data":{ + "statuses":[ + { + "resting":{ + "oid":77738308 + } + } + ] + } + } + }"#, + r#"{ + "status":"ok", + "response":{ + "type":"order", + "data":{ + "statuses":[ + { + "error":"Order must have minimum value of $10." + } + ] + } + } + }"#, + r#"{ + "status":"ok", + "response":{ + "type":"order", + "data":{ + "statuses":[ + { + "filled":{ + "totalSz":"0.02", + "avgPx":"1891.4", + "oid":77747314 + } + } + ] + } + } + }"#, + r#"{ + "status":"ok", + "response":{ + "type":"cancel", + "data":{ + "statuses":[ + "success" + ] + } + } + }"#, + r#"{ + "status":"ok", + "response":{ + "type":"cancel", + "data":{ + "statuses":[ + { + "error":"Order was never placed, already canceled, or filled." + } + ] + } + } + }"#, + r#"{"status": "ok", "response": {"type": "default"}}"#, + ]; + + for (i, json_str) in test_cases.iter().enumerate() { + println!("test case {}", i + 1,); + + let exchange_response: Result = serde_json::from_str(json_str); + match &exchange_response { + Ok(resp) => println!("base parsed successfully: status={}", resp.status), + Err(e) => println!(" base parse error: {}", e), + } + + if let Ok(resp) = exchange_response { + let order_response: Result = + serde_json::from_value(resp.response); + match order_response { + Ok(order_resp) => println!("inner parsed: {:?}", order_resp), + Err(e) => println!(" inner parse error: {}", e), + } + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index b89a4c8..c2236d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,7 @@ -mod config; mod errors; -mod handlers; mod hl; -mod notifier; -mod router; -mod services; mod signer; -mod strategy; -pub use config::*; +pub use errors::*; pub use hl::*; -pub use notifier::*; -pub use router::create_router; -pub use services::StrategyManagerService; pub use signer::*; -pub use strategy::*;