dydx_http_private/
http_private.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Manual verification script for dYdX HTTP private endpoints (account data).
17//!
18//! Tests subaccount queries including balance, open orders, and fill history.
19//! Requires a wallet address but no API key authentication (dYdX v4 uses blockchain addresses).
20//!
21//! Usage:
22//! ```bash
23//! # Test against testnet (default)
24//! DYDX_MNEMONIC="your mnemonic" cargo run --bin dydx-http-private -p nautilus-dydx
25//!
26//! # Test against mainnet
27//! DYDX_MNEMONIC="your mnemonic" \
28//! DYDX_HTTP_URL=https://indexer.dydx.trade \
29//! cargo run --bin dydx-http-private -p nautilus-dydx -- --mainnet
30//!
31//! # With custom subaccount and market filter
32//! DYDX_MNEMONIC="your mnemonic" cargo run --bin dydx-http-private -p nautilus-dydx -- \
33//!   --subaccount 1 \
34//!   --market BTC-USD
35//! ```
36
37use std::env;
38
39use nautilus_dydx::{
40    common::consts::DYDX_TESTNET_HTTP_URL, grpc::wallet::Wallet, http::client::DydxHttpClient,
41};
42use tracing::level_filters::LevelFilter;
43
44const DEFAULT_SUBACCOUNT: u32 = 0;
45
46#[tokio::main]
47async fn main() -> Result<(), Box<dyn std::error::Error>> {
48    tracing_subscriber::fmt()
49        .with_max_level(LevelFilter::INFO)
50        .init();
51
52    let args: Vec<String> = env::args().collect();
53    let is_mainnet = args.iter().any(|a| a == "--mainnet");
54    let subaccount_number = args
55        .iter()
56        .position(|a| a == "--subaccount")
57        .and_then(|i| args.get(i + 1))
58        .and_then(|s| s.parse::<u32>().ok())
59        .unwrap_or(DEFAULT_SUBACCOUNT);
60
61    let market_filter = args
62        .iter()
63        .position(|a| a == "--market")
64        .and_then(|i| args.get(i + 1))
65        .map(|s| s.as_str());
66
67    let mnemonic = env::var("DYDX_MNEMONIC").expect("DYDX_MNEMONIC environment variable not set");
68
69    let http_url = if is_mainnet {
70        env::var("DYDX_HTTP_URL").unwrap_or_else(|_| "https://indexer.dydx.trade".to_string())
71    } else {
72        env::var("DYDX_HTTP_URL").unwrap_or_else(|_| DYDX_TESTNET_HTTP_URL.to_string())
73    };
74
75    tracing::info!("Connecting to dYdX HTTP API: {}", http_url);
76    tracing::info!(
77        "Environment: {}",
78        if is_mainnet { "MAINNET" } else { "TESTNET" }
79    );
80    tracing::info!("Subaccount: {}", subaccount_number);
81    if let Some(market) = market_filter {
82        tracing::info!("Market filter: {}", market);
83    }
84    tracing::info!("");
85
86    let wallet = Wallet::from_mnemonic(&mnemonic)?;
87    let account = wallet.account_offline(subaccount_number)?;
88    let wallet_address = account.address.clone();
89    tracing::info!("Wallet address: {}", wallet_address);
90    tracing::info!("");
91
92    let client = DydxHttpClient::new(Some(http_url), Some(30), None, !is_mainnet, None)?;
93
94    tracing::info!("Fetching subaccount info...");
95    let start = std::time::Instant::now();
96    let subaccount = client
97        .raw_client()
98        .get_subaccount(&wallet_address, subaccount_number)
99        .await?;
100    let elapsed = start.elapsed();
101
102    tracing::info!(
103        "SUCCESS: Fetched subaccount data in {:.2}s",
104        elapsed.as_secs_f64()
105    );
106    tracing::info!(
107        "   Subaccount: {}/{}",
108        subaccount.subaccount.address,
109        subaccount.subaccount.subaccount_number
110    );
111    tracing::info!("   Equity: {}", subaccount.subaccount.equity);
112    tracing::info!(
113        "   Free collateral: {}",
114        subaccount.subaccount.free_collateral
115    );
116    if !subaccount.subaccount.open_perpetual_positions.is_empty() {
117        tracing::info!(
118            "   Open positions: {}",
119            subaccount.subaccount.open_perpetual_positions.len()
120        );
121        for pos in subaccount
122            .subaccount
123            .open_perpetual_positions
124            .values()
125            .take(5)
126        {
127            tracing::info!(
128                "     - {}: size={}, entry_price={}, unrealized_pnl={}",
129                pos.market,
130                pos.size,
131                pos.entry_price,
132                pos.unrealized_pnl
133            );
134        }
135    } else {
136        tracing::info!("   Open positions: 0");
137    }
138    tracing::info!("");
139
140    tracing::info!("Fetching open orders...");
141    let start = std::time::Instant::now();
142    let orders = client
143        .raw_client()
144        .get_orders(&wallet_address, subaccount_number, market_filter, None)
145        .await?;
146    let elapsed = start.elapsed();
147
148    tracing::info!(
149        "SUCCESS: Fetched {} open orders in {:.2}s",
150        orders.len(),
151        elapsed.as_secs_f64()
152    );
153    if !orders.is_empty() {
154        tracing::info!("   Sample orders:");
155        for order in orders.iter().take(5) {
156            tracing::info!(
157                "   - {}: {} {} @ {} ({})",
158                order.id,
159                order.side,
160                order.size,
161                order.price,
162                order.status
163            );
164        }
165        if orders.len() > 5 {
166            tracing::info!("   ... and {} more", orders.len() - 5);
167        }
168    }
169    tracing::info!("");
170
171    tracing::info!("Fetching recent fills...");
172    let start = std::time::Instant::now();
173    let fills = client
174        .raw_client()
175        .get_fills(&wallet_address, subaccount_number, market_filter, Some(100))
176        .await?;
177    let elapsed = start.elapsed();
178
179    tracing::info!(
180        "SUCCESS: Fetched {} fills in {:.2}s",
181        fills.fills.len(),
182        elapsed.as_secs_f64()
183    );
184    if !fills.fills.is_empty() {
185        tracing::info!("   Recent fills:");
186        for fill in fills.fills.iter().take(5) {
187            tracing::info!(
188                "   - {}: {} {} @ {} (fee: {})",
189                fill.market_type,
190                fill.side,
191                fill.size,
192                fill.price,
193                fill.fee
194            );
195        }
196        if fills.fills.len() > 5 {
197            tracing::info!("   ... and {} more", fills.fills.len() - 5);
198        }
199    }
200    tracing::info!("");
201
202    tracing::info!("ALL TESTS COMPLETED SUCCESSFULLY");
203    tracing::info!("");
204    tracing::info!("Summary:");
205    tracing::info!(
206        "  [PASS] get_subaccount: Equity={}, Positions={}",
207        subaccount.subaccount.equity,
208        subaccount.subaccount.open_perpetual_positions.len()
209    );
210    tracing::info!("  [PASS] get_orders: {} open orders", orders.len());
211    tracing::info!("  [PASS] get_fills: {} fills fetched", fills.fills.len());
212
213    Ok(())
214}