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