1#![allow(clippy::missing_errors_doc)]
19
20use std::{str::FromStr, sync::Arc};
21
22use nautilus_core::python::IntoPyObjectNautilusExt;
23use nautilus_model::{
24 enums::{OrderSide, TimeInForce},
25 identifiers::InstrumentId,
26 types::{Price, Quantity},
27};
28use pyo3::prelude::*;
29
30use crate::{
31 execution::submitter::OrderSubmitter,
32 grpc::{DydxGrpcClient, Wallet, types::ChainId},
33 http::client::DydxHttpClient,
34};
35
36#[pyclass(name = "DydxWallet")]
38#[derive(Debug, Clone)]
39pub struct PyDydxWallet {
40 pub(crate) inner: Arc<Wallet>,
41}
42
43#[pymethods]
44impl PyDydxWallet {
45 #[staticmethod]
51 #[pyo3(name = "from_mnemonic")]
52 pub fn py_from_mnemonic(mnemonic: &str) -> PyResult<Self> {
53 let wallet = Wallet::from_mnemonic(mnemonic)
54 .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("{e}")))?;
55 Ok(Self {
56 inner: Arc::new(wallet),
57 })
58 }
59
60 #[pyo3(name = "address")]
66 pub fn py_address(&self) -> PyResult<String> {
67 let account = self
68 .inner
69 .account_offline(0)
70 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
71 Ok(account.address)
72 }
73
74 fn __repr__(&self) -> String {
75 "DydxWallet(<redacted>)".to_string()
76 }
77}
78
79#[pyclass(name = "DydxOrderSubmitter")]
81#[derive(Debug)]
82pub struct PyDydxOrderSubmitter {
83 pub(crate) inner: Arc<OrderSubmitter>,
84}
85
86#[pymethods]
87impl PyDydxOrderSubmitter {
88 #[new]
94 #[pyo3(signature = (grpc_client, http_client, wallet_address, subaccount_number=0, chain_id=None, authenticator_ids=None))]
95 pub fn py_new(
96 grpc_client: PyDydxGrpcClient,
97 http_client: DydxHttpClient,
98 wallet_address: String,
99 subaccount_number: u32,
100 chain_id: Option<&str>,
101 authenticator_ids: Option<Vec<u64>>,
102 ) -> PyResult<Self> {
103 let chain_id = if let Some(chain_str) = chain_id {
104 ChainId::from_str(chain_str)
105 .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("{e}")))?
106 } else {
107 ChainId::Mainnet1
108 };
109
110 let submitter = OrderSubmitter::new(
111 grpc_client.inner.as_ref().clone(),
112 http_client,
113 wallet_address,
114 subaccount_number,
115 chain_id,
116 authenticator_ids.unwrap_or_default(),
117 );
118
119 Ok(Self {
120 inner: Arc::new(submitter),
121 })
122 }
123
124 #[pyo3(name = "submit_market_order")]
126 #[allow(clippy::too_many_arguments)]
127 fn py_submit_market_order<'py>(
128 &self,
129 py: Python<'py>,
130 wallet: PyDydxWallet,
131 instrument_id: &str,
132 client_order_id: u32,
133 side: i64,
134 quantity: &str,
135 block_height: u32,
136 ) -> PyResult<Bound<'py, PyAny>> {
137 let submitter = self.inner.clone();
138 let wallet_inner = wallet.inner;
139 let instrument_id = InstrumentId::from(instrument_id);
140 let side = OrderSide::from_repr(side as usize)
141 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid OrderSide"))?;
142 let quantity = Quantity::from(quantity);
143
144 pyo3_async_runtimes::tokio::future_into_py(py, async move {
145 submitter
146 .submit_market_order(
147 &wallet_inner,
148 instrument_id,
149 client_order_id,
150 side,
151 quantity,
152 block_height,
153 )
154 .await
155 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
156 Ok(())
157 })
158 }
159
160 #[pyo3(name = "submit_limit_order")]
162 #[allow(clippy::too_many_arguments)]
163 fn py_submit_limit_order<'py>(
164 &self,
165 py: Python<'py>,
166 wallet: PyDydxWallet,
167 instrument_id: &str,
168 client_order_id: u32,
169 side: i64,
170 price: &str,
171 quantity: &str,
172 time_in_force: i64,
173 post_only: bool,
174 reduce_only: bool,
175 block_height: u32,
176 expire_time: Option<i64>,
177 ) -> PyResult<Bound<'py, PyAny>> {
178 let submitter = self.inner.clone();
179 let wallet_inner = wallet.inner;
180 let instrument_id = InstrumentId::from(instrument_id);
181 let side = OrderSide::from_repr(side as usize)
182 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid OrderSide"))?;
183 let price = Price::from(price);
184 let quantity = Quantity::from(quantity);
185 let time_in_force = TimeInForce::from_repr(time_in_force as usize).ok_or_else(|| {
186 PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid TimeInForce")
187 })?;
188
189 pyo3_async_runtimes::tokio::future_into_py(py, async move {
190 submitter
191 .submit_limit_order(
192 &wallet_inner,
193 instrument_id,
194 client_order_id,
195 side,
196 price,
197 quantity,
198 time_in_force,
199 post_only,
200 reduce_only,
201 block_height,
202 expire_time,
203 )
204 .await
205 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
206 Ok(())
207 })
208 }
209
210 #[pyo3(name = "submit_stop_market_order")]
211 #[allow(clippy::too_many_arguments)]
212 fn py_submit_stop_market_order<'py>(
213 &self,
214 py: Python<'py>,
215 wallet: PyDydxWallet,
216 instrument_id: &str,
217 client_order_id: u32,
218 side: i64,
219 trigger_price: &str,
220 quantity: &str,
221 reduce_only: bool,
222 expire_time: Option<i64>,
223 ) -> PyResult<Bound<'py, PyAny>> {
224 let submitter = self.inner.clone();
225 let wallet_inner = wallet.inner;
226 let instrument_id = InstrumentId::from(instrument_id);
227 let side = OrderSide::from_repr(side as usize)
228 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid OrderSide"))?;
229 let trigger_price = Price::from(trigger_price);
230 let quantity = Quantity::from(quantity);
231
232 pyo3_async_runtimes::tokio::future_into_py(py, async move {
233 submitter
234 .submit_stop_market_order(
235 &wallet_inner,
236 instrument_id,
237 client_order_id,
238 side,
239 trigger_price,
240 quantity,
241 reduce_only,
242 expire_time,
243 )
244 .await
245 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
246 Ok(())
247 })
248 }
249
250 #[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")]
301 #[allow(clippy::too_many_arguments)]
302 fn py_submit_take_profit_market_order<'py>(
303 &self,
304 py: Python<'py>,
305 wallet: PyDydxWallet,
306 instrument_id: &str,
307 client_order_id: u32,
308 side: i64,
309 trigger_price: &str,
310 quantity: &str,
311 reduce_only: bool,
312 expire_time: Option<i64>,
313 ) -> PyResult<Bound<'py, PyAny>> {
314 let submitter = self.inner.clone();
315 let wallet_inner = wallet.inner;
316 let instrument_id = InstrumentId::from(instrument_id);
317 let side = OrderSide::from_repr(side as usize)
318 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid OrderSide"))?;
319 let trigger_price = Price::from(trigger_price);
320 let quantity = Quantity::from(quantity);
321
322 pyo3_async_runtimes::tokio::future_into_py(py, async move {
323 submitter
324 .submit_take_profit_market_order(
325 &wallet_inner,
326 instrument_id,
327 client_order_id,
328 side,
329 trigger_price,
330 quantity,
331 reduce_only,
332 expire_time,
333 )
334 .await
335 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
336 Ok(())
337 })
338 }
339
340 #[pyo3(name = "submit_take_profit_limit_order")]
341 #[allow(clippy::too_many_arguments)]
342 fn py_submit_take_profit_limit_order<'py>(
343 &self,
344 py: Python<'py>,
345 wallet: PyDydxWallet,
346 instrument_id: &str,
347 client_order_id: u32,
348 side: i64,
349 trigger_price: &str,
350 limit_price: &str,
351 quantity: &str,
352 time_in_force: i64,
353 post_only: bool,
354 reduce_only: bool,
355 expire_time: Option<i64>,
356 ) -> PyResult<Bound<'py, PyAny>> {
357 let submitter = self.inner.clone();
358 let wallet_inner = wallet.inner;
359 let instrument_id = InstrumentId::from(instrument_id);
360 let side = OrderSide::from_repr(side as usize)
361 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid OrderSide"))?;
362 let trigger_price = Price::from(trigger_price);
363 let limit_price = Price::from(limit_price);
364 let quantity = Quantity::from(quantity);
365 let time_in_force = TimeInForce::from_repr(time_in_force as usize).ok_or_else(|| {
366 PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid TimeInForce")
367 })?;
368
369 pyo3_async_runtimes::tokio::future_into_py(py, async move {
370 submitter
371 .submit_take_profit_limit_order(
372 &wallet_inner,
373 instrument_id,
374 client_order_id,
375 side,
376 trigger_price,
377 limit_price,
378 quantity,
379 time_in_force,
380 post_only,
381 reduce_only,
382 expire_time,
383 )
384 .await
385 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
386 Ok(())
387 })
388 }
389
390 #[pyo3(name = "cancel_order")]
391 fn py_cancel_order<'py>(
392 &self,
393 py: Python<'py>,
394 wallet: PyDydxWallet,
395 instrument_id: &str,
396 client_order_id: u32,
397 block_height: u32,
398 ) -> PyResult<Bound<'py, PyAny>> {
399 let submitter = self.inner.clone();
400 let wallet_inner = wallet.inner;
401 let instrument_id = InstrumentId::from(instrument_id);
402
403 pyo3_async_runtimes::tokio::future_into_py(py, async move {
404 submitter
405 .cancel_order(&wallet_inner, instrument_id, client_order_id, block_height)
406 .await
407 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
408 Ok(())
409 })
410 }
411
412 #[pyo3(name = "cancel_orders_batch")]
413 fn py_cancel_orders_batch<'py>(
414 &self,
415 py: Python<'py>,
416 wallet: PyDydxWallet,
417 orders: Vec<(String, u32)>,
418 block_height: u32,
419 ) -> PyResult<Bound<'py, PyAny>> {
420 let submitter = self.inner.clone();
421 let wallet_inner = wallet.inner;
422 let orders: Vec<(InstrumentId, u32)> = orders
423 .into_iter()
424 .map(|(id, client_id)| (InstrumentId::from(id.as_str()), client_id))
425 .collect();
426
427 pyo3_async_runtimes::tokio::future_into_py(py, async move {
428 submitter
429 .cancel_orders_batch(&wallet_inner, &orders, block_height)
430 .await
431 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
432 Ok(())
433 })
434 }
435
436 fn __repr__(&self) -> String {
437 "DydxOrderSubmitter()".to_string()
438 }
439}
440
441#[pyclass(name = "DydxGrpcClient")]
442#[derive(Debug, Clone)]
443pub struct PyDydxGrpcClient {
444 pub(crate) inner: Arc<DydxGrpcClient>,
445}
446
447#[pymethods]
448impl PyDydxGrpcClient {
449 #[staticmethod]
455 #[pyo3(name = "connect")]
456 pub fn py_connect(py: Python<'_>, grpc_url: String) -> PyResult<Bound<'_, PyAny>> {
457 pyo3_async_runtimes::tokio::future_into_py(py, async move {
458 let client = DydxGrpcClient::new(grpc_url)
459 .await
460 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
461
462 Ok(Self {
463 inner: Arc::new(client),
464 })
465 })
466 }
467
468 #[staticmethod]
474 #[pyo3(name = "connect_with_fallback")]
475 pub fn py_connect_with_fallback(
476 py: Python<'_>,
477 grpc_urls: Vec<String>,
478 ) -> PyResult<Bound<'_, PyAny>> {
479 pyo3_async_runtimes::tokio::future_into_py(py, async move {
480 let urls: Vec<&str> = grpc_urls.iter().map(String::as_str).collect();
481 let client = DydxGrpcClient::new_with_fallback(&urls)
482 .await
483 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
484
485 Ok(Self {
486 inner: Arc::new(client),
487 })
488 })
489 }
490
491 #[pyo3(name = "latest_block_height")]
497 pub fn py_latest_block_height<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
498 let client = self.inner.clone();
499 pyo3_async_runtimes::tokio::future_into_py(py, async move {
500 let mut client = (*client).clone();
501 let height = client
502 .latest_block_height()
503 .await
504 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
505 Ok(height.0 as u64)
506 })
507 }
508
509 #[pyo3(name = "get_account")]
515 pub fn py_get_account<'py>(
516 &self,
517 py: Python<'py>,
518 address: String,
519 ) -> PyResult<Bound<'py, PyAny>> {
520 let client = self.inner.clone();
521 pyo3_async_runtimes::tokio::future_into_py(py, async move {
522 let mut client = (*client).clone();
523 let account = client
524 .get_account(&address)
525 .await
526 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
527 Ok((account.account_number, account.sequence))
528 })
529 }
530
531 #[pyo3(name = "get_account_balances")]
537 pub fn py_get_account_balances<'py>(
538 &self,
539 py: Python<'py>,
540 address: String,
541 ) -> PyResult<Bound<'py, PyAny>> {
542 let client = self.inner.clone();
543 pyo3_async_runtimes::tokio::future_into_py(py, async move {
544 let mut client = (*client).clone();
545 let balances = client
546 .get_account_balances(&address)
547 .await
548 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
549 let result: Vec<(String, String)> =
550 balances.into_iter().map(|c| (c.denom, c.amount)).collect();
551 Ok(result)
552 })
553 }
554
555 #[pyo3(name = "get_subaccount")]
561 pub fn py_get_subaccount<'py>(
562 &self,
563 py: Python<'py>,
564 address: String,
565 subaccount_number: u32,
566 ) -> PyResult<Bound<'py, PyAny>> {
567 let client = self.inner.clone();
568 pyo3_async_runtimes::tokio::future_into_py(py, async move {
569 let mut client = (*client).clone();
570 let subaccount = client
571 .get_subaccount(&address, subaccount_number)
572 .await
573 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
574
575 let result: Vec<(String, String)> = subaccount
578 .asset_positions
579 .into_iter()
580 .map(|p| {
581 let quantums_str = if p.quantums.is_empty() {
582 "0".to_string()
583 } else {
584 hex::encode(&p.quantums)
586 };
587 (p.asset_id.to_string(), quantums_str)
588 })
589 .collect();
590 Ok(result)
591 })
592 }
593
594 #[pyo3(name = "get_node_info")]
600 pub fn py_get_node_info<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
601 let client = self.inner.clone();
602 pyo3_async_runtimes::tokio::future_into_py(py, async move {
603 let mut client = (*client).clone();
604 let info = client
605 .get_node_info()
606 .await
607 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
608
609 Python::attach(|py| {
611 use pyo3::types::PyDict;
612 let dict = PyDict::new(py);
613 if let Some(default_node_info) = info.default_node_info {
614 dict.set_item("network", default_node_info.network)?;
615 dict.set_item("moniker", default_node_info.moniker)?;
616 dict.set_item("version", default_node_info.version)?;
617 }
618 if let Some(app_info) = info.application_version {
619 dict.set_item("app_name", app_info.name)?;
620 dict.set_item("app_version", app_info.version)?;
621 }
622 Ok(dict.into_py_any_unwrap(py))
623 })
624 })
625 }
626
627 #[pyo3(name = "simulate_tx")]
633 pub fn py_simulate_tx<'py>(
634 &self,
635 py: Python<'py>,
636 tx_bytes: Vec<u8>,
637 ) -> PyResult<Bound<'py, PyAny>> {
638 let client = self.inner.clone();
639 pyo3_async_runtimes::tokio::future_into_py(py, async move {
640 let mut client = (*client).clone();
641 let gas_used = client
642 .simulate_tx(tx_bytes)
643 .await
644 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
645 Ok(gas_used)
646 })
647 }
648
649 #[pyo3(name = "get_tx")]
655 pub fn py_get_tx<'py>(&self, py: Python<'py>, hash: String) -> PyResult<Bound<'py, PyAny>> {
656 let client = self.inner.clone();
657 pyo3_async_runtimes::tokio::future_into_py(py, async move {
658 let mut client = (*client).clone();
659 let tx = client
660 .get_tx(&hash)
661 .await
662 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("{e}")))?;
663
664 let result = format!("Tx(body_bytes_len={})", tx.body.messages.len());
666 Ok(result)
667 })
668 }
669
670 fn __repr__(&self) -> String {
671 "DydxGrpcClient()".to_string()
672 }
673}