Skip to main content

nautilus_dydx/execution/
wallet.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//! Wallet and account management for dYdX v4.
17//!
18//! This module provides wallet functionality for managing signing keys for Cosmos SDK transactions.
19//! Wallets are created from hex-encoded private keys.
20
21use std::fmt::Debug;
22
23use anyhow::Context;
24use cosmrs::{
25    AccountId,
26    crypto::{PublicKey, secp256k1::SigningKey},
27    tx,
28};
29use nautilus_core::{hex, string::secret::REDACTED};
30
31/// Account prefix for dYdX addresses.
32///
33/// See [Cosmos accounts](https://docs.cosmos.network/main/learn/beginner/accounts).
34const BECH32_PREFIX_DYDX: &str = "dydx";
35
36/// Wallet for dYdX v4 transaction signing.
37///
38/// A wallet holds a secp256k1 private key used to sign Cosmos SDK transactions.
39/// The private key bytes are stored to allow recreating SigningKey (which doesn't
40/// implement Clone). Address and account_id are pre-computed during construction
41/// to avoid repeated derivation.
42///
43/// # Security
44///
45/// Private key bytes should be treated as sensitive material.
46pub struct Wallet {
47    /// Raw private key bytes (32 bytes for secp256k1).
48    /// Stored separately because SigningKey doesn't implement Clone or expose bytes.
49    private_key_bytes: Box<[u8]>,
50    /// Pre-computed dYdX address (bech32 encoded).
51    address: String,
52    /// Pre-computed Cosmos SDK account ID.
53    account_id: AccountId,
54}
55
56impl Debug for Wallet {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        f.debug_struct(stringify!(Wallet))
59            .field("private_key_bytes", &REDACTED)
60            .field("address", &self.address)
61            .finish()
62    }
63}
64
65impl Clone for Wallet {
66    fn clone(&self) -> Self {
67        Self {
68            private_key_bytes: self.private_key_bytes.clone(),
69            address: self.address.clone(),
70            account_id: self.account_id.clone(),
71        }
72    }
73}
74
75impl Wallet {
76    /// Create a wallet from a hex-encoded private key.
77    ///
78    /// The private key should be a 32-byte secp256k1 key encoded as hex,
79    /// optionally with a `0x` prefix. Address and account ID are derived
80    /// during construction.
81    ///
82    /// # Errors
83    ///
84    /// Returns an error if the private key is invalid hex or not a valid secp256k1 key.
85    pub fn from_private_key(private_key_hex: &str) -> anyhow::Result<Self> {
86        let key_bytes = hex::decode(private_key_hex.trim_start_matches("0x"))
87            .context("Invalid hex private key")?;
88
89        // Validate the key and derive address/account_id
90        let signing_key = SigningKey::from_slice(&key_bytes)
91            .map_err(|e| anyhow::anyhow!("Invalid secp256k1 private key: {e}"))?;
92
93        let public_key = signing_key.public_key();
94        let account_id = public_key
95            .account_id(BECH32_PREFIX_DYDX)
96            .map_err(|e| anyhow::anyhow!("Failed to derive account ID: {e}"))?;
97        let address = account_id.to_string();
98
99        Ok(Self {
100            private_key_bytes: key_bytes.into_boxed_slice(),
101            address,
102            account_id,
103        })
104    }
105
106    /// Get a dYdX account with zero account and sequence numbers.
107    ///
108    /// Creates an account using the pre-computed address/account_id.
109    /// SigningKey is recreated from stored bytes (it doesn't implement Clone).
110    /// Account and sequence numbers must be set before signing.
111    ///
112    /// # Errors
113    ///
114    /// Returns an error if the signing key creation fails.
115    pub fn account_offline(&self) -> Result<Account, anyhow::Error> {
116        // SigningKey doesn't impl Clone, so recreate from stored bytes
117        let key = SigningKey::from_slice(&self.private_key_bytes)
118            .map_err(|e| anyhow::anyhow!("Failed to create signing key: {e}"))?;
119
120        Ok(Account {
121            address: self.address.clone(),
122            account_id: self.account_id.clone(),
123            key,
124            account_number: 0,
125            sequence_number: 0,
126        })
127    }
128
129    /// Returns the pre-computed wallet address.
130    #[must_use]
131    pub fn address(&self) -> &str {
132        &self.address
133    }
134}
135
136/// Represents a dYdX account.
137///
138/// An account contains the signing key and metadata needed to sign and broadcast transactions.
139/// The `account_number` and `sequence_number` must be set from on-chain data before signing.
140///
141/// See also [`Wallet`].
142pub struct Account {
143    /// dYdX address (bech32 encoded).
144    pub address: String,
145    /// Cosmos SDK account ID.
146    pub account_id: AccountId,
147    /// Private signing key.
148    key: SigningKey,
149    /// On-chain account number (must be fetched before signing).
150    pub account_number: u64,
151    /// Transaction sequence number (must be fetched before signing).
152    pub sequence_number: u64,
153}
154
155impl Debug for Account {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        f.debug_struct(stringify!(Account))
158            .field("address", &self.address)
159            .field("account_id", &self.account_id)
160            .field("key", &REDACTED)
161            .field("account_number", &self.account_number)
162            .field("sequence_number", &self.sequence_number)
163            .finish()
164    }
165}
166
167impl Account {
168    /// Get the public key associated with this account.
169    #[must_use]
170    pub fn public_key(&self) -> PublicKey {
171        self.key.public_key()
172    }
173
174    /// Sign a [`SignDoc`](tx::SignDoc) with the private key.
175    ///
176    /// # Errors
177    ///
178    /// Returns an error if signing fails.
179    pub fn sign(&self, doc: tx::SignDoc) -> Result<tx::Raw, anyhow::Error> {
180        doc.sign(&self.key)
181            .map_err(|e| anyhow::anyhow!("Failed to sign transaction: {e}"))
182    }
183
184    /// Update account and sequence numbers from on-chain data.
185    pub fn set_account_info(&mut self, account_number: u64, sequence_number: u64) {
186        self.account_number = account_number;
187        self.sequence_number = sequence_number;
188    }
189
190    /// Increment the sequence number (used after successful transaction broadcast).
191    pub fn increment_sequence(&mut self) {
192        self.sequence_number += 1;
193    }
194
195    /// Derive a subaccount for this account.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if the subaccount number is invalid.
200    pub fn subaccount(&self, number: u32) -> Result<Subaccount, anyhow::Error> {
201        Ok(Subaccount {
202            address: self.address.clone(),
203            number,
204        })
205    }
206}
207
208/// A subaccount within a dYdX account.
209///
210/// Each account can have multiple subaccounts for organizing positions and balances.
211#[derive(Clone, Debug, PartialEq, Eq)]
212pub struct Subaccount {
213    /// Parent account address.
214    pub address: String,
215    /// Subaccount number.
216    pub number: u32,
217}
218
219impl Subaccount {
220    /// Create a new subaccount.
221    #[must_use]
222    pub fn new(address: String, number: u32) -> Self {
223        Self { address, number }
224    }
225}