nautilus_dydx/grpc/
builder.rs

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