Skip to main content

nautilus_dydx/grpc/
builder.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//! Transaction builder for dYdX v4 protocol.
17//!
18//! This module provides utilities for building and signing Cosmos SDK transactions
19//! for the dYdX v4 protocol, including support for permissioned key trading via
20//! authenticators.
21//!
22//! # Permissioned Keys
23//!
24//! dYdX supports permissioned keys (authenticators) that allow an account to add
25//! custom logic for verifying and confirming transactions. This enables features like:
26//!
27//! - Delegated signing keys for sub-accounts
28//! - Separated hot/cold wallet architectures
29//! - Trading key separation from withdrawal keys
30//!
31//! See <https://docs.dydx.xyz/concepts/trading/authenticators> for details.
32
33use std::fmt::Debug;
34
35use cosmrs::{
36    Any, Coin,
37    tx::{self, Fee, SignDoc, SignerInfo},
38};
39use dydx_proto::{ToAny, dydxprotocol::accountplus::TxExtension};
40use rust_decimal::{Decimal, prelude::ToPrimitive};
41
42use super::types::ChainId;
43use crate::execution::wallet::Account;
44
45/// Gas adjustment value to avoid rejected transactions caused by gas underestimation.
46const GAS_MULTIPLIER: f64 = 1.8;
47
48/// Transaction builder.
49///
50/// Handles fee calculation, transaction construction, and signing.
51pub struct TxBuilder {
52    chain_id: cosmrs::tendermint::chain::Id,
53    fee_denom: String,
54}
55
56impl TxBuilder {
57    /// Create a new transaction builder.
58    ///
59    /// # Errors
60    ///
61    /// Returns an error if the chain ID cannot be converted.
62    pub fn new(chain_id: ChainId, fee_denom: String) -> Result<Self, anyhow::Error> {
63        Ok(Self {
64            chain_id: chain_id.try_into()?,
65            fee_denom,
66        })
67    }
68
69    /// Estimate a transaction fee.
70    ///
71    /// See also [What Are Crypto Gas Fees?](https://dydx.exchange/crypto-learning/what-are-crypto-gas-fees).
72    ///
73    /// # Errors
74    ///
75    /// Returns an error if fee calculation fails.
76    pub fn calculate_fee(&self, gas_used: Option<u64>) -> Result<Fee, anyhow::Error> {
77        if let Some(gas) = gas_used {
78            self.calculate_fee_from_gas(gas)
79        } else {
80            Ok(Self::default_fee())
81        }
82    }
83
84    /// Calculate fee from gas usage.
85    fn calculate_fee_from_gas(&self, gas_used: u64) -> Result<Fee, anyhow::Error> {
86        let gas_multiplier = Decimal::try_from(GAS_MULTIPLIER)?;
87        let gas_limit = Decimal::from(gas_used) * gas_multiplier;
88
89        // Gas price for dYdX (typically 0.025 adydx per gas)
90        let gas_price = Decimal::new(25, 3); // 0.025
91        let amount = (gas_price * gas_limit).ceil();
92
93        let gas_limit_u64 = gas_limit
94            .to_u64()
95            .ok_or_else(|| anyhow::anyhow!("Failed converting gas limit to u64"))?;
96
97        let amount_u128 = amount
98            .to_u128()
99            .ok_or_else(|| anyhow::anyhow!("Failed converting gas cost to u128"))?;
100
101        Ok(Fee::from_amount_and_gas(
102            Coin {
103                amount: amount_u128,
104                denom: self
105                    .fee_denom
106                    .parse()
107                    .map_err(|e| anyhow::anyhow!("Invalid fee denom: {e}"))?,
108            },
109            gas_limit_u64,
110        ))
111    }
112
113    /// Get default fee (zero fee).
114    fn default_fee() -> Fee {
115        Fee {
116            amount: vec![],
117            gas_limit: 0,
118            payer: None,
119            granter: None,
120        }
121    }
122
123    /// Build a transaction for given messages.
124    ///
125    /// When `authenticator_ids` is provided, the transaction will include a `TxExtension`
126    /// for permissioned key trading, allowing sub-accounts to trade using delegated keys.
127    ///
128    /// # Errors
129    ///
130    /// Returns an error if transaction building or signing fails.
131    pub fn build_transaction(
132        &self,
133        account: &Account,
134        msgs: impl IntoIterator<Item = Any>,
135        fee: Option<Fee>,
136        authenticator_ids: Option<&[u64]>,
137    ) -> Result<tx::Raw, anyhow::Error> {
138        let mut builder = tx::BodyBuilder::new();
139        builder.msgs(msgs).memo("");
140
141        // Add authenticators for permissioned key trading if provided
142        if let Some(auth_ids) = authenticator_ids
143            && !auth_ids.is_empty()
144        {
145            let ext = TxExtension {
146                selected_authenticators: auth_ids.to_vec(),
147            };
148            builder.non_critical_extension_option(ext.to_any());
149        }
150
151        let tx_body = builder.finish();
152
153        let fee = fee.unwrap_or_else(|| {
154            self.calculate_fee(None)
155                .unwrap_or_else(|_| Self::default_fee())
156        });
157
158        let auth_info =
159            SignerInfo::single_direct(Some(account.public_key()), account.sequence_number)
160                .auth_info(fee);
161
162        let sign_doc = SignDoc::new(&tx_body, &auth_info, &self.chain_id, account.account_number)
163            .map_err(|e| anyhow::anyhow!("Cannot create sign doc: {e}"))?;
164
165        account.sign(sign_doc)
166    }
167
168    /// Build and simulate a transaction to estimate gas.
169    ///
170    /// Returns the raw transaction bytes suitable for simulation.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if transaction building fails.
175    pub fn build_for_simulation(
176        &self,
177        account: &Account,
178        msgs: impl IntoIterator<Item = Any>,
179    ) -> Result<Vec<u8>, anyhow::Error> {
180        let tx_raw = self.build_transaction(account, msgs, None, None)?;
181        tx_raw.to_bytes().map_err(|e| anyhow::anyhow!("{e}"))
182    }
183}
184
185impl Debug for TxBuilder {
186    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187        f.debug_struct(stringify!(TxBuilder))
188            .field("chain_id", &self.chain_id)
189            .field("fee_denom", &self.fee_denom)
190            .finish()
191    }
192}