1use cosmrs::Tx;
22use tonic::transport::Channel;
23
24use crate::{
25 error::DydxError,
26 proto::{
27 AccountAuthenticator, AccountPlusClient, GetAuthenticatorsRequest,
28 cosmos_sdk_proto::{
29 cosmos::{
30 auth::v1beta1::{
31 BaseAccount, QueryAccountRequest, query_client::QueryClient as AuthClient,
32 },
33 bank::v1beta1::{QueryAllBalancesRequest, query_client::QueryClient as BankClient},
34 base::{
35 tendermint::v1beta1::{
36 Block, GetLatestBlockRequest, GetNodeInfoRequest, GetNodeInfoResponse,
37 service_client::ServiceClient as BaseClient,
38 },
39 v1beta1::Coin,
40 },
41 tx::v1beta1::{
42 BroadcastMode, BroadcastTxRequest, GetTxRequest, SimulateRequest,
43 service_client::ServiceClient as TxClient,
44 },
45 },
46 traits::Message as ProstMessage,
47 },
48 dydxprotocol::{
49 clob::{ClobPair, QueryAllClobPairRequest, query_client::QueryClient as ClobClient},
50 perpetuals::{
51 Perpetual, QueryAllPerpetualsRequest, query_client::QueryClient as PerpetualsClient,
52 },
53 subaccounts::{
54 QueryGetSubaccountRequest, Subaccount as SubaccountInfo,
55 query_client::QueryClient as SubaccountsClient,
56 },
57 },
58 },
59};
60
61pub type TxHash = String;
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
66pub struct Height(pub u32);
67
68#[derive(Debug, Clone)]
76pub struct DydxGrpcClient {
77 channel: Channel,
78 auth: AuthClient<Channel>,
79 bank: BankClient<Channel>,
80 base: BaseClient<Channel>,
81 tx: TxClient<Channel>,
82 clob: ClobClient<Channel>,
83 perpetuals: PerpetualsClient<Channel>,
84 subaccounts: SubaccountsClient<Channel>,
85 accountplus: AccountPlusClient<Channel>,
86 current_url: String,
87}
88
89impl DydxGrpcClient {
90 pub async fn new(grpc_url: String) -> Result<Self, DydxError> {
96 let mut endpoint = Channel::from_shared(grpc_url.clone())
97 .map_err(|e| DydxError::Config(format!("Invalid gRPC URL: {e}")))?
98 .connect_timeout(std::time::Duration::from_secs(10))
99 .timeout(std::time::Duration::from_secs(30));
100
101 if grpc_url.starts_with("https://") {
103 let tls = tonic::transport::ClientTlsConfig::new().with_enabled_roots();
104 endpoint = endpoint
105 .tls_config(tls)
106 .map_err(|e| DydxError::Config(format!("TLS config failed: {e}")))?;
107 }
108
109 let channel = endpoint.connect().await.map_err(|e| {
110 DydxError::Grpc(Box::new(tonic::Status::unavailable(format!(
111 "Connection failed: {e}"
112 ))))
113 })?;
114
115 Ok(Self {
116 auth: AuthClient::new(channel.clone()),
117 bank: BankClient::new(channel.clone()),
118 base: BaseClient::new(channel.clone()),
119 tx: TxClient::new(channel.clone()),
120 clob: ClobClient::new(channel.clone()),
121 perpetuals: PerpetualsClient::new(channel.clone()),
122 subaccounts: SubaccountsClient::new(channel.clone()),
123 accountplus: AccountPlusClient::new(channel.clone()),
124 channel,
125 current_url: grpc_url,
126 })
127 }
128
129 pub async fn new_with_fallback(grpc_urls: &[impl AsRef<str>]) -> Result<Self, DydxError> {
139 if grpc_urls.is_empty() {
140 return Err(DydxError::Config("No gRPC URLs provided".to_string()));
141 }
142
143 let mut last_error = None;
144
145 for (idx, url) in grpc_urls.iter().enumerate() {
146 let url_str = url.as_ref();
147 log::debug!(
148 "Attempting to connect to gRPC node: {url_str} (attempt {}/{})",
149 idx + 1,
150 grpc_urls.len()
151 );
152
153 match Self::new(url_str.to_string()).await {
154 Ok(client) => {
155 log::debug!("Successfully connected to gRPC node: {url_str}");
156 return Ok(client);
157 }
158 Err(e) => {
159 log::warn!("Failed to connect to gRPC node {url_str}: {e}");
160 last_error = Some(e);
161 }
162 }
163 }
164
165 Err(last_error.unwrap_or_else(|| {
166 DydxError::Grpc(Box::new(tonic::Status::unavailable(
167 "All gRPC connection attempts failed".to_string(),
168 )))
169 }))
170 }
171
172 pub async fn reconnect_with_fallback(
182 &mut self,
183 grpc_urls: &[impl AsRef<str>],
184 ) -> Result<(), DydxError> {
185 if grpc_urls.is_empty() {
186 return Err(DydxError::Config("No gRPC URLs provided".to_string()));
187 }
188
189 let mut last_error = None;
190
191 for (idx, url) in grpc_urls.iter().enumerate() {
192 let url_str = url.as_ref();
193
194 if url_str == self.current_url {
196 log::debug!("Skipping current URL: {url_str}");
197 continue;
198 }
199
200 log::debug!(
201 "Attempting to reconnect to gRPC node: {url_str} (attempt {}/{})",
202 idx + 1,
203 grpc_urls.len()
204 );
205
206 let mut endpoint = match Channel::from_shared(url_str.to_string())
207 .map_err(|e| DydxError::Config(format!("Invalid gRPC URL: {e}")))
208 {
209 Ok(ep) => ep
210 .connect_timeout(std::time::Duration::from_secs(10))
211 .timeout(std::time::Duration::from_secs(30)),
212 Err(e) => {
213 last_error = Some(e);
214 continue;
215 }
216 };
217
218 if url_str.starts_with("https://") {
220 let tls = tonic::transport::ClientTlsConfig::new().with_enabled_roots();
221 endpoint = match endpoint.tls_config(tls) {
222 Ok(ep) => ep,
223 Err(e) => {
224 last_error = Some(DydxError::Config(format!("TLS config failed: {e}")));
225 continue;
226 }
227 };
228 }
229
230 match endpoint.connect().await {
231 Ok(connected_channel) => {
232 log::debug!("Successfully reconnected to gRPC node: {url_str}");
233
234 self.channel = connected_channel.clone();
236 self.auth = AuthClient::new(connected_channel.clone());
237 self.bank = BankClient::new(connected_channel.clone());
238 self.base = BaseClient::new(connected_channel.clone());
239 self.tx = TxClient::new(connected_channel.clone());
240 self.clob = ClobClient::new(connected_channel.clone());
241 self.perpetuals = PerpetualsClient::new(connected_channel.clone());
242 self.subaccounts = SubaccountsClient::new(connected_channel);
243 self.current_url = url_str.to_string();
244
245 return Ok(());
246 }
247 Err(e) => {
248 log::warn!("Failed to reconnect to gRPC node {url_str}: {e}");
249 last_error = Some(DydxError::Grpc(Box::new(tonic::Status::unavailable(
250 format!("Connection failed: {e}"),
251 ))));
252 }
253 }
254 }
255
256 Err(last_error.unwrap_or_else(|| {
257 DydxError::Grpc(Box::new(tonic::Status::unavailable(
258 "All gRPC reconnection attempts failed".to_string(),
259 )))
260 }))
261 }
262
263 #[must_use]
265 pub fn current_url(&self) -> &str {
266 &self.current_url
267 }
268
269 #[must_use]
273 pub fn channel(&self) -> &Channel {
274 &self.channel
275 }
276
277 pub async fn query_address(&mut self, address: &str) -> Result<(u64, u64), anyhow::Error> {
285 let req = QueryAccountRequest {
286 address: address.to_string(),
287 };
288 let resp = self
289 .auth
290 .account(req)
291 .await?
292 .into_inner()
293 .account
294 .ok_or_else(|| {
295 anyhow::anyhow!("Query account request failure, account should exist")
296 })?;
297
298 let account = BaseAccount::decode(&*resp.value)?;
299 Ok((account.account_number, account.sequence))
300 }
301
302 pub async fn get_account(&mut self, address: &str) -> Result<BaseAccount, anyhow::Error> {
309 let req = QueryAccountRequest {
310 address: address.to_string(),
311 };
312 let resp = self
313 .auth
314 .account(req)
315 .await?
316 .into_inner()
317 .account
318 .ok_or_else(|| {
319 anyhow::anyhow!("Query account request failure, account should exist")
320 })?;
321
322 Ok(BaseAccount::decode(&*resp.value)?)
323 }
324
325 pub async fn get_account_balances(
332 &mut self,
333 address: &str,
334 ) -> Result<Vec<Coin>, anyhow::Error> {
335 let req = QueryAllBalancesRequest {
336 address: address.to_string(),
337 resolve_denom: false,
338 pagination: None,
339 };
340 let balances = self.bank.all_balances(req).await?.into_inner().balances;
341 Ok(balances)
342 }
343
344 pub async fn get_authenticators(
353 &mut self,
354 address: &str,
355 ) -> Result<Vec<AccountAuthenticator>, anyhow::Error> {
356 let req = GetAuthenticatorsRequest {
357 account: address.to_string(),
358 };
359 let resp = self.accountplus.get_authenticators(req).await?.into_inner();
360 Ok(resp.account_authenticators)
361 }
362
363 pub async fn get_node_info(&mut self) -> Result<GetNodeInfoResponse, anyhow::Error> {
369 let req = GetNodeInfoRequest {};
370 let info = self.base.get_node_info(req).await?.into_inner();
371 Ok(info)
372 }
373
374 pub async fn latest_block(&mut self) -> Result<Block, anyhow::Error> {
380 let req = GetLatestBlockRequest::default();
381 let latest_block = self
382 .base
383 .get_latest_block(req)
384 .await?
385 .into_inner()
386 .sdk_block
387 .ok_or_else(|| anyhow::anyhow!("The latest block is empty"))?;
388 Ok(latest_block)
389 }
390
391 pub async fn latest_block_height(&mut self) -> Result<Height, anyhow::Error> {
397 let latest_block = self.latest_block().await?;
398 let header = latest_block
399 .header
400 .ok_or_else(|| anyhow::anyhow!("The block doesn't contain a header"))?;
401 let height = Height(header.height.try_into()?);
402 Ok(height)
403 }
404
405 pub async fn get_perpetuals(&mut self) -> Result<Vec<Perpetual>, anyhow::Error> {
411 let req = QueryAllPerpetualsRequest { pagination: None };
412 let response = self.perpetuals.all_perpetuals(req).await?.into_inner();
413 Ok(response.perpetual)
414 }
415
416 pub async fn get_clob_pairs(&mut self) -> Result<Vec<ClobPair>, anyhow::Error> {
422 let req = QueryAllClobPairRequest { pagination: None };
423 let pairs = self.clob.clob_pair_all(req).await?.into_inner().clob_pair;
424 Ok(pairs)
425 }
426
427 pub async fn get_subaccount(
433 &mut self,
434 address: &str,
435 number: u32,
436 ) -> Result<SubaccountInfo, anyhow::Error> {
437 let req = QueryGetSubaccountRequest {
438 owner: address.to_string(),
439 number,
440 };
441 let subaccount = self
442 .subaccounts
443 .subaccount(req)
444 .await?
445 .into_inner()
446 .subaccount
447 .ok_or_else(|| {
448 anyhow::anyhow!("Subaccount query response does not contain subaccount")
449 })?;
450 Ok(subaccount)
451 }
452
453 pub async fn simulate_tx(&mut self, tx_bytes: Vec<u8>) -> Result<u64, anyhow::Error> {
459 let req = SimulateRequest {
460 tx_bytes,
461 ..Default::default()
462 };
463 let gas_used = self
464 .tx
465 .simulate(req)
466 .await?
467 .into_inner()
468 .gas_info
469 .ok_or_else(|| anyhow::anyhow!("Simulation response does not contain gas info"))?
470 .gas_used;
471 Ok(gas_used)
472 }
473
474 pub async fn broadcast_tx(&mut self, tx_bytes: Vec<u8>) -> Result<TxHash, anyhow::Error> {
480 let req = BroadcastTxRequest {
481 tx_bytes,
482 mode: BroadcastMode::Sync as i32,
483 };
484 let response = self.tx.broadcast_tx(req).await?.into_inner();
485
486 if let Some(tx_response) = response.tx_response {
487 if tx_response.code != 0 {
488 anyhow::bail!(
489 "Transaction broadcast failed: code={}, log={}",
490 tx_response.code,
491 tx_response.raw_log
492 );
493 }
494 Ok(tx_response.txhash)
495 } else {
496 Err(anyhow::anyhow!(
497 "Broadcast response does not contain tx_response"
498 ))
499 }
500 }
501
502 pub async fn get_tx(&mut self, hash: &str) -> Result<Tx, anyhow::Error> {
508 let req = GetTxRequest {
509 hash: hash.to_string(),
510 };
511 let response = self.tx.get_tx(req).await?.into_inner();
512
513 if let Some(tx) = response.tx {
514 let tx_bytes = tx.encode_to_vec();
516 Tx::try_from(tx_bytes.as_slice()).map_err(|e| anyhow::anyhow!("{e}"))
517 } else {
518 anyhow::bail!("Transaction not found")
519 }
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use rstest::rstest;
526
527 use super::*;
528
529 #[rstest]
530 fn test_height_ordering() {
531 let h1 = Height(100);
532 let h2 = Height(200);
533 assert!(h1 < h2);
534 assert_eq!(h1, Height(100));
535 }
536
537 #[tokio::test]
538 async fn test_new_with_fallback_empty_urls() {
539 let result = DydxGrpcClient::new_with_fallback(&[] as &[&str]).await;
540 assert!(result.is_err());
541 if let Err(DydxError::Config(msg)) = result {
542 assert_eq!(msg, "No gRPC URLs provided");
543 } else {
544 panic!("Expected Config error");
545 }
546 }
547
548 #[tokio::test]
549 async fn test_new_with_fallback_invalid_urls() {
550 let invalid_urls = vec!["invalid://bad-url", "http://0.0.0.0:1"];
552 let result = DydxGrpcClient::new_with_fallback(&invalid_urls).await;
553
554 assert!(result.is_err());
556 }
557}