Skip to main content

nautilus_dydx/python/
grpc.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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//! Python bindings for dYdX gRPC client.
17
18use std::sync::Arc;
19
20use nautilus_core::{
21    hex,
22    python::{IntoPyObjectNautilusExt, to_pyruntime_err},
23};
24use pyo3::prelude::*;
25
26use crate::grpc::DydxGrpcClient;
27
28#[pyclass(name = "DydxGrpcClient", from_py_object)]
29#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.dydx")]
30#[derive(Debug, Clone)]
31pub struct PyDydxGrpcClient {
32    pub(crate) inner: Arc<DydxGrpcClient>,
33}
34
35#[pymethods]
36#[pyo3_stub_gen::derive::gen_stub_pymethods]
37impl PyDydxGrpcClient {
38    /// Create a new gRPC client.
39    ///
40    /// # Errors
41    ///
42    /// Returns an error if connection fails.
43    #[staticmethod]
44    #[pyo3(name = "connect")]
45    pub fn py_connect(py: Python<'_>, grpc_url: String) -> PyResult<Bound<'_, PyAny>> {
46        pyo3_async_runtimes::tokio::future_into_py(py, async move {
47            let client = DydxGrpcClient::new(grpc_url)
48                .await
49                .map_err(to_pyruntime_err)?;
50
51            Ok(Self {
52                inner: Arc::new(client),
53            })
54        })
55    }
56
57    /// Create a new gRPC client with fallback URLs.
58    ///
59    /// # Errors
60    ///
61    /// Returns an error if all connection attempts fail.
62    #[staticmethod]
63    #[pyo3(name = "connect_with_fallback")]
64    pub fn py_connect_with_fallback(
65        py: Python<'_>,
66        grpc_urls: Vec<String>,
67    ) -> PyResult<Bound<'_, PyAny>> {
68        pyo3_async_runtimes::tokio::future_into_py(py, async move {
69            let urls: Vec<&str> = grpc_urls.iter().map(String::as_str).collect();
70            let client = DydxGrpcClient::new_with_fallback(&urls)
71                .await
72                .map_err(to_pyruntime_err)?;
73
74            Ok(Self {
75                inner: Arc::new(client),
76            })
77        })
78    }
79
80    /// Fetch the latest block height from the chain.
81    ///
82    /// # Errors
83    ///
84    /// Returns an error if the gRPC request fails.
85    #[pyo3(name = "latest_block_height")]
86    pub fn py_latest_block_height<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
87        let client = self.inner.clone();
88        pyo3_async_runtimes::tokio::future_into_py(py, async move {
89            let mut client = (*client).clone();
90            let height = client
91                .latest_block_height()
92                .await
93                .map_err(to_pyruntime_err)?;
94            Ok(height.0 as u64)
95        })
96    }
97
98    /// Query account information (account_number, sequence).
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if the gRPC request fails.
103    #[pyo3(name = "get_account")]
104    pub fn py_get_account<'py>(
105        &self,
106        py: Python<'py>,
107        address: String,
108    ) -> PyResult<Bound<'py, PyAny>> {
109        let client = self.inner.clone();
110        pyo3_async_runtimes::tokio::future_into_py(py, async move {
111            let mut client = (*client).clone();
112            let account = client
113                .get_account(&address)
114                .await
115                .map_err(to_pyruntime_err)?;
116            Ok((account.account_number, account.sequence))
117        })
118    }
119
120    /// Query account balances.
121    ///
122    /// # Errors
123    ///
124    /// Returns an error if the gRPC request fails.
125    #[pyo3(name = "get_account_balances")]
126    pub fn py_get_account_balances<'py>(
127        &self,
128        py: Python<'py>,
129        address: String,
130    ) -> PyResult<Bound<'py, PyAny>> {
131        let client = self.inner.clone();
132        pyo3_async_runtimes::tokio::future_into_py(py, async move {
133            let mut client = (*client).clone();
134            let balances = client
135                .get_account_balances(&address)
136                .await
137                .map_err(to_pyruntime_err)?;
138            let result: Vec<(String, String)> =
139                balances.into_iter().map(|c| (c.denom, c.amount)).collect();
140            Ok(result)
141        })
142    }
143
144    /// Query subaccount information.
145    ///
146    /// # Errors
147    ///
148    /// Returns an error if the gRPC request fails.
149    #[pyo3(name = "get_subaccount")]
150    pub fn py_get_subaccount<'py>(
151        &self,
152        py: Python<'py>,
153        address: String,
154        subaccount_number: u32,
155    ) -> PyResult<Bound<'py, PyAny>> {
156        let client = self.inner.clone();
157        pyo3_async_runtimes::tokio::future_into_py(py, async move {
158            let mut client = (*client).clone();
159            let subaccount = client
160                .get_subaccount(&address, subaccount_number)
161                .await
162                .map_err(to_pyruntime_err)?;
163
164            // Return as dict-like structure
165            // quantums is bytes representing a big-endian signed integer
166            let result: Vec<(String, String)> = subaccount
167                .asset_positions
168                .into_iter()
169                .map(|p| {
170                    let quantums_str = if p.quantums.is_empty() {
171                        "0".to_string()
172                    } else {
173                        // Convert bytes to hex string for now
174                        hex::encode(&p.quantums)
175                    };
176                    (p.asset_id.to_string(), quantums_str)
177                })
178                .collect();
179            Ok(result)
180        })
181    }
182
183    /// Get node information from the gRPC endpoint.
184    ///
185    /// # Errors
186    ///
187    /// Returns an error if the gRPC request fails.
188    #[pyo3(name = "get_node_info")]
189    pub fn py_get_node_info<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
190        let client = self.inner.clone();
191        pyo3_async_runtimes::tokio::future_into_py(py, async move {
192            let mut client = (*client).clone();
193            let info = client.get_node_info().await.map_err(to_pyruntime_err)?;
194
195            // Return node info as a dict
196            Python::attach(|py| {
197                use pyo3::types::PyDict;
198                let dict = PyDict::new(py);
199
200                if let Some(default_node_info) = info.default_node_info {
201                    dict.set_item("network", default_node_info.network)?;
202                    dict.set_item("moniker", default_node_info.moniker)?;
203                    dict.set_item("version", default_node_info.version)?;
204                }
205
206                if let Some(app_info) = info.application_version {
207                    dict.set_item("app_name", app_info.name)?;
208                    dict.set_item("app_version", app_info.version)?;
209                }
210                Ok(dict.into_py_any_unwrap(py))
211            })
212        })
213    }
214
215    /// Simulate a transaction to estimate gas.
216    ///
217    /// # Errors
218    ///
219    /// Returns an error if the gRPC request fails.
220    #[pyo3(name = "simulate_tx")]
221    pub fn py_simulate_tx<'py>(
222        &self,
223        py: Python<'py>,
224        tx_bytes: Vec<u8>,
225    ) -> PyResult<Bound<'py, PyAny>> {
226        let client = self.inner.clone();
227        pyo3_async_runtimes::tokio::future_into_py(py, async move {
228            let mut client = (*client).clone();
229            let gas_used = client
230                .simulate_tx(tx_bytes)
231                .await
232                .map_err(to_pyruntime_err)?;
233            Ok(gas_used)
234        })
235    }
236
237    /// Get transaction details by hash.
238    ///
239    /// # Errors
240    ///
241    /// Returns an error if the gRPC request fails.
242    #[pyo3(name = "get_tx")]
243    pub fn py_get_tx<'py>(&self, py: Python<'py>, hash: String) -> PyResult<Bound<'py, PyAny>> {
244        let client = self.inner.clone();
245        pyo3_async_runtimes::tokio::future_into_py(py, async move {
246            let mut client = (*client).clone();
247            let tx = client.get_tx(&hash).await.map_err(to_pyruntime_err)?;
248
249            // Return tx as JSON string
250            let result = format!("Tx(body_bytes_len={})", tx.body.messages.len());
251            Ok(result)
252        })
253    }
254
255    fn __repr__(&self) -> String {
256        "DydxGrpcClient()".to_string()
257    }
258}