nautilus_dydx/grpc/
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 deriving accounts from BIP-39 mnemonics
19//! and managing signing keys for Cosmos SDK transactions.
20
21use std::{fmt::Debug, str::FromStr};
22
23use cosmrs::{
24    AccountId,
25    bip32::{DerivationPath, Language, Mnemonic, Seed},
26    crypto::{PublicKey, secp256k1::SigningKey},
27    tx,
28};
29
30/// Account prefix for dYdX addresses.
31///
32/// See [Cosmos accounts](https://docs.cosmos.network/main/learn/beginner/accounts).
33const BECH32_PREFIX_DYDX: &str = "dydx";
34
35/// Hierarchical Deterministic (HD) [wallet](https://dydx.exchange/crypto-learning/glossary?#wallet)
36/// which allows multiple addresses and signing keys from one master seed.
37///
38/// [BIP-44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) introduced a wallet
39/// standard to derive multiple accounts for different chains from a single seed (which allows
40/// recovery of the whole tree of keys).
41///
42/// This `Wallet` uses the Cosmos ATOM derivation path to generate dYdX addresses.
43///
44/// # Security
45///
46/// The `Seed` type from bip32 implements `Drop` for secure cleanup of seed material.
47///
48/// Note: Deriving `zeroize::ZeroizeOnDrop` is not possible because `bip32::Seed` does not
49/// expose the `zeroize::Zeroize` trait (it uses internal Drop-based cleanup).
50///
51/// See also [Mastering Bitcoin](https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch05_wallets.adoc).
52pub struct Wallet {
53    seed: Seed,
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("seed", &"<redacted>")
60            .finish()
61    }
62}
63
64impl Wallet {
65    /// Derive a seed from a 24-word English mnemonic phrase.
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if the mnemonic is invalid or cannot be converted to a seed.
70    pub fn from_mnemonic(mnemonic: &str) -> Result<Self, anyhow::Error> {
71        let seed = Mnemonic::new(mnemonic, Language::English)?.to_seed("");
72        Ok(Self { seed })
73    }
74
75    /// Derive a dYdX account with zero account and sequence numbers.
76    ///
77    /// Account and sequence numbers must be fetched from the chain before signing transactions.
78    ///
79    /// # Errors
80    ///
81    /// Returns an error if the account derivation fails.
82    pub fn account_offline(&self, index: u32) -> Result<Account, anyhow::Error> {
83        self.derive_account(index, BECH32_PREFIX_DYDX)
84    }
85
86    fn derive_account(&self, index: u32, prefix: &str) -> Result<Account, anyhow::Error> {
87        // BIP-44 derivation path for Cosmos (coin type 118)
88        // See https://github.com/satoshilabs/slips/blob/master/slip-0044.md
89        let derivation_str = format!("m/44'/118'/0'/0/{index}");
90        let derivation_path = DerivationPath::from_str(&derivation_str)?;
91        let private_key = SigningKey::derive_from_path(&self.seed, &derivation_path)?;
92        let public_key = private_key.public_key();
93        let account_id = public_key.account_id(prefix).map_err(anyhow::Error::msg)?;
94        let address = account_id.to_string();
95
96        Ok(Account {
97            index,
98            address,
99            account_id,
100            key: private_key,
101            account_number: 0,
102            sequence_number: 0,
103        })
104    }
105}
106
107/// Represents a derived dYdX account.
108///
109/// An account contains the signing key and metadata needed to sign and broadcast transactions.
110/// The `account_number` and `sequence_number` must be set from on-chain data before signing.
111///
112/// See also [`Wallet`].
113pub struct Account {
114    /// Derivation index of the account.
115    pub index: u32,
116    /// dYdX address (bech32 encoded).
117    pub address: String,
118    /// Cosmos SDK account ID.
119    pub account_id: AccountId,
120    /// Private signing key.
121    key: SigningKey,
122    /// On-chain account number (must be fetched before signing).
123    pub account_number: u64,
124    /// Transaction sequence number (must be fetched before signing).
125    pub sequence_number: u64,
126}
127
128impl Debug for Account {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        f.debug_struct(stringify!(Account))
131            .field("index", &self.index)
132            .field("address", &self.address)
133            .field("account_id", &self.account_id)
134            .field("key", &"<redacted>")
135            .field("account_number", &self.account_number)
136            .field("sequence_number", &self.sequence_number)
137            .finish()
138    }
139}
140
141impl Account {
142    /// Get the public key associated with this account.
143    #[must_use]
144    pub fn public_key(&self) -> PublicKey {
145        self.key.public_key()
146    }
147
148    /// Sign a [`SignDoc`](tx::SignDoc) with the private key.
149    ///
150    /// # Errors
151    ///
152    /// Returns an error if signing fails.
153    pub fn sign(&self, doc: tx::SignDoc) -> Result<tx::Raw, anyhow::Error> {
154        doc.sign(&self.key)
155            .map_err(|e| anyhow::anyhow!("Failed to sign transaction: {e}"))
156    }
157
158    /// Update account and sequence numbers from on-chain data.
159    pub fn set_account_info(&mut self, account_number: u64, sequence_number: u64) {
160        self.account_number = account_number;
161        self.sequence_number = sequence_number;
162    }
163
164    /// Increment the sequence number (used after successful transaction broadcast).
165    pub fn increment_sequence(&mut self) {
166        self.sequence_number += 1;
167    }
168
169    /// Derive a subaccount for this account.
170    ///
171    /// # Errors
172    ///
173    /// Returns an error if the subaccount number is invalid.
174    pub fn subaccount(&self, number: u32) -> Result<Subaccount, anyhow::Error> {
175        Ok(Subaccount {
176            address: self.address.clone(),
177            number,
178        })
179    }
180}
181
182/// A subaccount within a dYdX account.
183///
184/// Each account can have multiple subaccounts for organizing positions and balances.
185#[derive(Clone, Debug, PartialEq, Eq)]
186pub struct Subaccount {
187    /// Parent account address.
188    pub address: String,
189    /// Subaccount number.
190    pub number: u32,
191}
192
193impl Subaccount {
194    /// Create a new subaccount.
195    #[must_use]
196    pub fn new(address: String, number: u32) -> Self {
197        Self { address, number }
198    }
199}