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