1use std::{str::FromStr, sync::Arc};
19
20use nautilus_core::python::IntoPyObjectNautilusExt;
21use nautilus_model::{
22 enums::{OrderSide, TimeInForce},
23 identifiers::InstrumentId,
24 types::{Price, Quantity},
25};
26use pyo3::prelude::*;
27
28use crate::{
29 execution::submitter::OrderSubmitter,
30 grpc::{DydxGrpcClient, Wallet, types::ChainId},
31 http::client::DydxHttpClient,
32};
33
34#[pyclass(name = "DydxWallet")]
36#[derive(Debug, Clone)]
37pub struct PyDydxWallet {
38 pub(crate) inner: Arc<Wallet>,
39}
40
41#[pymethods]
42impl PyDydxWallet {
43 #[staticmethod]
49 #[pyo3(name = "from_mnemonic")]
50 pub fn py_from_mnemonic(mnemonic: &str) -> PyResult<Self> {
51 let wallet = Wallet::from_mnemonic(mnemonic)
52 .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("{e}")))?;
53 Ok(Self {
54 inner: Arc::new(wallet),
55 })
56 }
57
58 #[pyo3(name = "address")]
64 pub fn py_address(&self) -> PyResult<String> {
65 let account = self
66 .inner
67 .account_offline(0)
68 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
69 Ok(account.address)
70 }
71
72 fn __repr__(&self) -> String {
73 "DydxWallet(<redacted>)".to_string()
74 }
75}
76
77#[pyclass(name = "DydxOrderSubmitter")]
79#[derive(Debug)]
80pub struct PyDydxOrderSubmitter {
81 pub(crate) inner: Arc<OrderSubmitter>,
82}
83
84#[pymethods]
85impl PyDydxOrderSubmitter {
86 #[new]
92 #[pyo3(signature = (grpc_client, http_client, wallet_address, subaccount_number=0, chain_id=None, authenticator_ids=None))]
93 pub fn py_new(
94 grpc_client: PyDydxGrpcClient,
95 http_client: DydxHttpClient,
96 wallet_address: String,
97 subaccount_number: u32,
98 chain_id: Option<&str>,
99 authenticator_ids: Option<Vec<u64>>,
100 ) -> PyResult<Self> {
101 let chain_id = if let Some(chain_str) = chain_id {
102 ChainId::from_str(chain_str)
103 .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("{e}")))?
104 } else {
105 ChainId::Mainnet1
106 };
107
108 let submitter = OrderSubmitter::new(
109 grpc_client.inner.as_ref().clone(),
110 http_client,
111 wallet_address,
112 subaccount_number,
113 chain_id,
114 authenticator_ids.unwrap_or_default(),
115 );
116
117 Ok(Self {
118 inner: Arc::new(submitter),
119 })
120 }
121
122 #[pyo3(name = "submit_market_order")]
124 #[allow(clippy::too_many_arguments)]
125 fn py_submit_market_order<'py>(
126 &self,
127 py: Python<'py>,
128 wallet: PyDydxWallet,
129 instrument_id: &str,
130 client_order_id: u32,
131 side: i64,
132 quantity: &str,
133 block_height: u32,
134 ) -> PyResult<Bound<'py, PyAny>> {
135 let submitter = self.inner.clone();
136 let wallet_inner = wallet.inner;
137 let instrument_id = InstrumentId::from(instrument_id);
138 let side = OrderSide::from_repr(side as usize)
139 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid OrderSide"))?;
140 let quantity = Quantity::from(quantity);
141
142 pyo3_async_runtimes::tokio::future_into_py(py, async move {
143 submitter
144 .submit_market_order(
145 &wallet_inner,
146 instrument_id,
147 client_order_id,
148 side,
149 quantity,
150 block_height,
151 )
152 .await
153 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
154 Ok(())
155 })
156 }
157
158 #[pyo3(name = "submit_limit_order")]
160 #[allow(clippy::too_many_arguments)]
161 fn py_submit_limit_order<'py>(
162 &self,
163 py: Python<'py>,
164 wallet: PyDydxWallet,
165 instrument_id: &str,
166 client_order_id: u32,
167 side: i64,
168 price: &str,
169 quantity: &str,
170 time_in_force: i64,
171 post_only: bool,
172 reduce_only: bool,
173 block_height: u32,
174 expire_time: Option<i64>,
175 ) -> PyResult<Bound<'py, PyAny>> {
176 let submitter = self.inner.clone();
177 let wallet_inner = wallet.inner;
178 let instrument_id = InstrumentId::from(instrument_id);
179 let side = OrderSide::from_repr(side as usize)
180 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid OrderSide"))?;
181 let price = Price::from(price);
182 let quantity = Quantity::from(quantity);
183 let time_in_force = TimeInForce::from_repr(time_in_force as usize).ok_or_else(|| {
184 PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid TimeInForce")
185 })?;
186
187 pyo3_async_runtimes::tokio::future_into_py(py, async move {
188 submitter
189 .submit_limit_order(
190 &wallet_inner,
191 instrument_id,
192 client_order_id,
193 side,
194 price,
195 quantity,
196 time_in_force,
197 post_only,
198 reduce_only,
199 block_height,
200 expire_time,
201 )
202 .await
203 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
204 Ok(())
205 })
206 }
207
208 #[pyo3(name = "submit_stop_market_order")]
210 #[allow(clippy::too_many_arguments)]
211 fn py_submit_stop_market_order<'py>(
212 &self,
213 py: Python<'py>,
214 wallet: PyDydxWallet,
215 instrument_id: &str,
216 client_order_id: u32,
217 side: i64,
218 trigger_price: &str,
219 quantity: &str,
220 reduce_only: bool,
221 expire_time: Option<i64>,
222 ) -> PyResult<Bound<'py, PyAny>> {
223 let submitter = self.inner.clone();
224 let wallet_inner = wallet.inner;
225 let instrument_id = InstrumentId::from(instrument_id);
226 let side = OrderSide::from_repr(side as usize)
227 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid OrderSide"))?;
228 let trigger_price = Price::from(trigger_price);
229 let quantity = Quantity::from(quantity);
230
231 pyo3_async_runtimes::tokio::future_into_py(py, async move {
232 submitter
233 .submit_stop_market_order(
234 &wallet_inner,
235 instrument_id,
236 client_order_id,
237 side,
238 trigger_price,
239 quantity,
240 reduce_only,
241 expire_time,
242 )
243 .await
244 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
245 Ok(())
246 })
247 }
248
249 #[pyo3(name = "submit_stop_limit_order")]
251 #[allow(clippy::too_many_arguments)]
252 fn py_submit_stop_limit_order<'py>(
253 &self,
254 py: Python<'py>,
255 wallet: PyDydxWallet,
256 instrument_id: &str,
257 client_order_id: u32,
258 side: i64,
259 trigger_price: &str,
260 limit_price: &str,
261 quantity: &str,
262 time_in_force: i64,
263 post_only: bool,
264 reduce_only: bool,
265 expire_time: Option<i64>,
266 ) -> PyResult<Bound<'py, PyAny>> {
267 let submitter = self.inner.clone();
268 let wallet_inner = wallet.inner;
269 let instrument_id = InstrumentId::from(instrument_id);
270 let side = OrderSide::from_repr(side as usize)
271 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid OrderSide"))?;
272 let trigger_price = Price::from(trigger_price);
273 let limit_price = Price::from(limit_price);
274 let quantity = Quantity::from(quantity);
275 let time_in_force = TimeInForce::from_repr(time_in_force as usize).ok_or_else(|| {
276 PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid TimeInForce")
277 })?;
278
279 pyo3_async_runtimes::tokio::future_into_py(py, async move {
280 submitter
281 .submit_stop_limit_order(
282 &wallet_inner,
283 instrument_id,
284 client_order_id,
285 side,
286 trigger_price,
287 limit_price,
288 quantity,
289 time_in_force,
290 post_only,
291 reduce_only,
292 expire_time,
293 )
294 .await
295 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
296 Ok(())
297 })
298 }
299
300 #[pyo3(name = "submit_take_profit_market_order")]
302 #[allow(clippy::too_many_arguments)]
303 fn py_submit_take_profit_market_order<'py>(
304 &self,
305 py: Python<'py>,
306 wallet: PyDydxWallet,
307 instrument_id: &str,
308 client_order_id: u32,
309 side: i64,
310 trigger_price: &str,
311 quantity: &str,
312 reduce_only: bool,
313 expire_time: Option<i64>,
314 ) -> PyResult<Bound<'py, PyAny>> {
315 let submitter = self.inner.clone();
316 let wallet_inner = wallet.inner;
317 let instrument_id = InstrumentId::from(instrument_id);
318 let side = OrderSide::from_repr(side as usize)
319 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid OrderSide"))?;
320 let trigger_price = Price::from(trigger_price);
321 let quantity = Quantity::from(quantity);
322
323 pyo3_async_runtimes::tokio::future_into_py(py, async move {
324 submitter
325 .submit_take_profit_market_order(
326 &wallet_inner,
327 instrument_id,
328 client_order_id,
329 side,
330 trigger_price,
331 quantity,
332 reduce_only,
333 expire_time,
334 )
335 .await
336 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
337 Ok(())
338 })
339 }
340
341 #[pyo3(name = "submit_take_profit_limit_order")]
343 #[allow(clippy::too_many_arguments)]
344 fn py_submit_take_profit_limit_order<'py>(
345 &self,
346 py: Python<'py>,
347 wallet: PyDydxWallet,
348 instrument_id: &str,
349 client_order_id: u32,
350 side: i64,
351 trigger_price: &str,
352 limit_price: &str,
353 quantity: &str,
354 time_in_force: i64,
355 post_only: bool,
356 reduce_only: bool,
357 expire_time: Option<i64>,
358 ) -> PyResult<Bound<'py, PyAny>> {
359 let submitter = self.inner.clone();
360 let wallet_inner = wallet.inner;
361 let instrument_id = InstrumentId::from(instrument_id);
362 let side = OrderSide::from_repr(side as usize)
363 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid OrderSide"))?;
364 let trigger_price = Price::from(trigger_price);
365 let limit_price = Price::from(limit_price);
366 let quantity = Quantity::from(quantity);
367 let time_in_force = TimeInForce::from_repr(time_in_force as usize).ok_or_else(|| {
368 PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid TimeInForce")
369 })?;
370
371 pyo3_async_runtimes::tokio::future_into_py(py, async move {
372 submitter
373 .submit_take_profit_limit_order(
374 &wallet_inner,
375 instrument_id,
376 client_order_id,
377 side,
378 trigger_price,
379 limit_price,
380 quantity,
381 time_in_force,
382 post_only,
383 reduce_only,
384 expire_time,
385 )
386 .await
387 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
388 Ok(())
389 })
390 }
391
392 #[pyo3(name = "cancel_order")]
394 fn py_cancel_order<'py>(
395 &self,
396 py: Python<'py>,
397 wallet: PyDydxWallet,
398 instrument_id: &str,
399 client_order_id: u32,
400 block_height: u32,
401 ) -> PyResult<Bound<'py, PyAny>> {
402 let submitter = self.inner.clone();
403 let wallet_inner = wallet.inner;
404 let instrument_id = InstrumentId::from(instrument_id);
405
406 pyo3_async_runtimes::tokio::future_into_py(py, async move {
407 submitter
408 .cancel_order(&wallet_inner, instrument_id, client_order_id, block_height)
409 .await
410 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
411 Ok(())
412 })
413 }
414
415 #[pyo3(name = "cancel_orders_batch")]
417 fn py_cancel_orders_batch<'py>(
418 &self,
419 py: Python<'py>,
420 wallet: PyDydxWallet,
421 orders: Vec<(String, u32)>,
422 block_height: u32,
423 ) -> PyResult<Bound<'py, PyAny>> {
424 let submitter = self.inner.clone();
425 let wallet_inner = wallet.inner;
426 let orders: Vec<(InstrumentId, u32)> = orders
427 .into_iter()
428 .map(|(id, client_id)| (InstrumentId::from(id.as_str()), client_id))
429 .collect();
430
431 pyo3_async_runtimes::tokio::future_into_py(py, async move {
432 submitter
433 .cancel_orders_batch(&wallet_inner, &orders, block_height)
434 .await
435 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
436 Ok(())
437 })
438 }
439
440 fn __repr__(&self) -> String {
441 "DydxOrderSubmitter()".to_string()
442 }
443}
444
445#[pyclass(name = "DydxGrpcClient")]
447#[derive(Debug, Clone)]
448pub struct PyDydxGrpcClient {
449 pub(crate) inner: Arc<DydxGrpcClient>,
450}
451
452#[pymethods]
453impl PyDydxGrpcClient {
454 #[staticmethod]
460 #[pyo3(name = "connect")]
461 pub fn py_connect(py: Python<'_>, grpc_url: String) -> PyResult<Bound<'_, PyAny>> {
462 pyo3_async_runtimes::tokio::future_into_py(py, async move {
463 let client = DydxGrpcClient::new(grpc_url)
464 .await
465 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
466
467 Ok(Self {
468 inner: Arc::new(client),
469 })
470 })
471 }
472
473 #[staticmethod]
479 #[pyo3(name = "connect_with_fallback")]
480 pub fn py_connect_with_fallback(
481 py: Python<'_>,
482 grpc_urls: Vec<String>,
483 ) -> PyResult<Bound<'_, PyAny>> {
484 pyo3_async_runtimes::tokio::future_into_py(py, async move {
485 let urls: Vec<&str> = grpc_urls.iter().map(String::as_str).collect();
486 let client = DydxGrpcClient::new_with_fallback(&urls)
487 .await
488 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
489
490 Ok(Self {
491 inner: Arc::new(client),
492 })
493 })
494 }
495
496 #[pyo3(name = "latest_block_height")]
502 pub fn py_latest_block_height<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
503 let client = self.inner.clone();
504 pyo3_async_runtimes::tokio::future_into_py(py, async move {
505 let mut client = (*client).clone();
506 let height = client
507 .latest_block_height()
508 .await
509 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
510 Ok(height.0 as u64)
511 })
512 }
513
514 #[pyo3(name = "get_account")]
520 pub fn py_get_account<'py>(
521 &self,
522 py: Python<'py>,
523 address: String,
524 ) -> PyResult<Bound<'py, PyAny>> {
525 let client = self.inner.clone();
526 pyo3_async_runtimes::tokio::future_into_py(py, async move {
527 let mut client = (*client).clone();
528 let account = client
529 .get_account(&address)
530 .await
531 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
532 Ok((account.account_number, account.sequence))
533 })
534 }
535
536 #[pyo3(name = "get_account_balances")]
542 pub fn py_get_account_balances<'py>(
543 &self,
544 py: Python<'py>,
545 address: String,
546 ) -> PyResult<Bound<'py, PyAny>> {
547 let client = self.inner.clone();
548 pyo3_async_runtimes::tokio::future_into_py(py, async move {
549 let mut client = (*client).clone();
550 let balances = client
551 .get_account_balances(&address)
552 .await
553 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
554 let result: Vec<(String, String)> =
555 balances.into_iter().map(|c| (c.denom, c.amount)).collect();
556 Ok(result)
557 })
558 }
559
560 #[pyo3(name = "get_subaccount")]
566 pub fn py_get_subaccount<'py>(
567 &self,
568 py: Python<'py>,
569 address: String,
570 subaccount_number: u32,
571 ) -> PyResult<Bound<'py, PyAny>> {
572 let client = self.inner.clone();
573 pyo3_async_runtimes::tokio::future_into_py(py, async move {
574 let mut client = (*client).clone();
575 let subaccount = client
576 .get_subaccount(&address, subaccount_number)
577 .await
578 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
579
580 let result: Vec<(String, String)> = subaccount
583 .asset_positions
584 .into_iter()
585 .map(|p| {
586 let quantums_str = if p.quantums.is_empty() {
587 "0".to_string()
588 } else {
589 hex::encode(&p.quantums)
591 };
592 (p.asset_id.to_string(), quantums_str)
593 })
594 .collect();
595 Ok(result)
596 })
597 }
598
599 #[pyo3(name = "get_node_info")]
605 pub fn py_get_node_info<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
606 let client = self.inner.clone();
607 pyo3_async_runtimes::tokio::future_into_py(py, async move {
608 let mut client = (*client).clone();
609 let info = client
610 .get_node_info()
611 .await
612 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
613
614 Python::attach(|py| {
616 use pyo3::types::PyDict;
617 let dict = PyDict::new(py);
618 if let Some(default_node_info) = info.default_node_info {
619 dict.set_item("network", default_node_info.network)?;
620 dict.set_item("moniker", default_node_info.moniker)?;
621 dict.set_item("version", default_node_info.version)?;
622 }
623 if let Some(app_info) = info.application_version {
624 dict.set_item("app_name", app_info.name)?;
625 dict.set_item("app_version", app_info.version)?;
626 }
627 Ok(dict.into_py_any_unwrap(py))
628 })
629 })
630 }
631
632 #[pyo3(name = "simulate_tx")]
638 pub fn py_simulate_tx<'py>(
639 &self,
640 py: Python<'py>,
641 tx_bytes: Vec<u8>,
642 ) -> PyResult<Bound<'py, PyAny>> {
643 let client = self.inner.clone();
644 pyo3_async_runtimes::tokio::future_into_py(py, async move {
645 let mut client = (*client).clone();
646 let gas_used = client
647 .simulate_tx(tx_bytes)
648 .await
649 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
650 Ok(gas_used)
651 })
652 }
653
654 #[pyo3(name = "get_tx")]
660 pub fn py_get_tx<'py>(&self, py: Python<'py>, hash: String) -> PyResult<Bound<'py, PyAny>> {
661 let client = self.inner.clone();
662 pyo3_async_runtimes::tokio::future_into_py(py, async move {
663 let mut client = (*client).clone();
664 let tx = client
665 .get_tx(&hash)
666 .await
667 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
668
669 let result = format!("Tx(body_bytes_len={})", tx.body.messages.len());
671 Ok(result)
672 })
673 }
674
675 fn __repr__(&self) -> String {
676 "DydxGrpcClient()".to_string()
677 }
678}