nautilus_dydx/common/
credential.rs1#![allow(unused_assignments)] use std::fmt::Debug;
33
34use anyhow::Context;
35use cosmrs::{AccountId, crypto::secp256k1::SigningKey, tx::SignDoc};
36use nautilus_core::env::get_or_env_var_opt;
37
38use crate::common::consts::DYDX_BECH32_PREFIX;
39
40pub struct DydxCredential {
49 signing_key: SigningKey,
51 pub address: String,
53 pub authenticator_ids: Vec<u64>,
55}
56
57impl Debug for DydxCredential {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 f.debug_struct(stringify!(DydxCredential))
60 .field("address", &self.address)
61 .field("authenticator_ids", &self.authenticator_ids)
62 .field("signing_key", &"<redacted>")
63 .finish()
64 }
65}
66
67impl DydxCredential {
68 pub fn from_private_key(
74 private_key_hex: &str,
75 authenticator_ids: Vec<u64>,
76 ) -> anyhow::Result<Self> {
77 let key_bytes = hex::decode(private_key_hex.trim_start_matches("0x"))
79 .context("Invalid hex private key")?;
80
81 let signing_key = SigningKey::from_slice(&key_bytes)
82 .map_err(|e| anyhow::anyhow!("Invalid secp256k1 private key: {e}"))?;
83
84 let public_key = signing_key.public_key();
86 let account_id = public_key
87 .account_id(DYDX_BECH32_PREFIX)
88 .map_err(|e| anyhow::anyhow!("Failed to derive account ID: {e}"))?;
89 let address = account_id.to_string();
90
91 Ok(Self {
92 signing_key,
93 address,
94 authenticator_ids,
95 })
96 }
97
98 pub fn from_env(is_testnet: bool, authenticator_ids: Vec<u64>) -> anyhow::Result<Option<Self>> {
108 let private_key_env = if is_testnet {
109 "DYDX_TESTNET_PRIVATE_KEY"
110 } else {
111 "DYDX_PRIVATE_KEY"
112 };
113
114 if let Some(private_key) = std::env::var(private_key_env)
115 .ok()
116 .filter(|s| !s.trim().is_empty())
117 {
118 return Ok(Some(Self::from_private_key(
119 &private_key,
120 authenticator_ids,
121 )?));
122 }
123
124 Ok(None)
125 }
126
127 pub fn resolve(
139 private_key: Option<String>,
140 is_testnet: bool,
141 authenticator_ids: Vec<u64>,
142 ) -> anyhow::Result<Option<Self>> {
143 if let Some(ref pk) = private_key
145 && !pk.trim().is_empty()
146 {
147 return Ok(Some(Self::from_private_key(pk, authenticator_ids)?));
148 }
149
150 let private_key_env = if is_testnet {
152 "DYDX_TESTNET_PRIVATE_KEY"
153 } else {
154 "DYDX_PRIVATE_KEY"
155 };
156 if let Some(pk) = std::env::var(private_key_env)
157 .ok()
158 .filter(|s| !s.trim().is_empty())
159 {
160 return Ok(Some(Self::from_private_key(&pk, authenticator_ids)?));
161 }
162
163 Ok(None)
164 }
165
166 pub fn account_id(&self) -> anyhow::Result<AccountId> {
172 self.address
173 .parse()
174 .map_err(|e| anyhow::anyhow!("Failed to parse account ID: {e}"))
175 }
176
177 pub fn sign(&self, sign_doc: &SignDoc) -> anyhow::Result<Vec<u8>> {
185 let sign_bytes = sign_doc
186 .clone()
187 .into_bytes()
188 .map_err(|e| anyhow::anyhow!("Failed to serialize SignDoc: {e}"))?;
189
190 let signature = self
191 .signing_key
192 .sign(&sign_bytes)
193 .map_err(|e| anyhow::anyhow!("Failed to sign: {e}"))?;
194 Ok(signature.to_bytes().to_vec())
195 }
196
197 pub fn sign_bytes(&self, message: &[u8]) -> anyhow::Result<Vec<u8>> {
205 let signature = self
206 .signing_key
207 .sign(message)
208 .map_err(|e| anyhow::anyhow!("Failed to sign: {e}"))?;
209 Ok(signature.to_bytes().to_vec())
210 }
211
212 pub fn public_key(&self) -> cosmrs::crypto::PublicKey {
214 self.signing_key.public_key()
215 }
216}
217
218#[must_use]
230pub fn resolve_wallet_address(wallet_address: Option<String>, is_testnet: bool) -> Option<String> {
231 let env_var = if is_testnet {
232 "DYDX_TESTNET_WALLET_ADDRESS"
233 } else {
234 "DYDX_WALLET_ADDRESS"
235 };
236
237 get_or_env_var_opt(wallet_address, env_var).filter(|s| !s.trim().is_empty())
238}
239
240#[cfg(test)]
241mod tests {
242 use rstest::rstest;
243
244 use super::*;
245
246 const TEST_PRIVATE_KEY: &str =
248 "0000000000000000000000000000000000000000000000000000000000000001";
249
250 #[rstest]
251 fn test_from_private_key() {
252 let credential = DydxCredential::from_private_key(TEST_PRIVATE_KEY, vec![])
253 .expect("Failed to create credential from private key");
254
255 assert!(credential.address.starts_with("dydx"));
256 assert!(credential.authenticator_ids.is_empty());
257 }
258
259 #[rstest]
260 fn test_from_private_key_with_authenticators() {
261 let credential = DydxCredential::from_private_key(TEST_PRIVATE_KEY, vec![1, 2, 3])
262 .expect("Failed to create credential");
263
264 assert_eq!(credential.authenticator_ids, vec![1, 2, 3]);
265 }
266
267 #[rstest]
268 fn test_from_private_key_with_0x_prefix() {
269 let key_with_prefix = format!("0x{TEST_PRIVATE_KEY}");
270 let credential = DydxCredential::from_private_key(&key_with_prefix, vec![])
271 .expect("Failed to create credential from private key with 0x prefix");
272
273 assert!(credential.address.starts_with("dydx"));
274 }
275
276 #[rstest]
277 fn test_account_id() {
278 let credential = DydxCredential::from_private_key(TEST_PRIVATE_KEY, vec![])
279 .expect("Failed to create credential");
280
281 let account_id = credential.account_id().expect("Failed to get account ID");
282 assert_eq!(account_id.to_string(), credential.address);
283 }
284
285 #[rstest]
286 fn test_sign_bytes() {
287 let credential = DydxCredential::from_private_key(TEST_PRIVATE_KEY, vec![])
288 .expect("Failed to create credential");
289
290 let message = b"test message";
291 let signature = credential
292 .sign_bytes(message)
293 .expect("Failed to sign bytes");
294
295 assert_eq!(signature.len(), 64);
297 }
298
299 #[rstest]
300 fn test_debug_redacts_key() {
301 let credential = DydxCredential::from_private_key(TEST_PRIVATE_KEY, vec![])
302 .expect("Failed to create credential");
303
304 let debug_str = format!("{credential:?}");
305 assert!(debug_str.contains("<redacted>"));
307 assert!(debug_str.contains("DydxCredential"));
309 assert!(debug_str.contains(&credential.address));
311 }
312
313 #[rstest]
314 fn test_resolve_with_provided_private_key() {
315 let result = DydxCredential::resolve(Some(TEST_PRIVATE_KEY.to_string()), false, vec![])
316 .expect("Failed to resolve credential");
317
318 assert!(result.is_some());
319 let credential = result.unwrap();
320 assert!(credential.address.starts_with("dydx"));
321 }
322
323 #[rstest]
324 fn test_resolve_with_none_and_no_env_var() {
325 let result = DydxCredential::resolve(None, true, vec![])
327 .expect("Should not error when credential not available");
328
329 if std::env::var("DYDX_TESTNET_PRIVATE_KEY").is_err() {
331 assert!(result.is_none());
332 }
333 }
334
335 #[rstest]
336 fn test_resolve_wallet_address_with_provided_value() {
337 let result = resolve_wallet_address(Some("dydx1abc123".to_string()), false);
338 assert_eq!(result, Some("dydx1abc123".to_string()));
339 }
340
341 #[rstest]
342 fn test_resolve_wallet_address_empty_string_returns_none() {
343 let result = resolve_wallet_address(Some(String::new()), false);
344 assert!(result.is_none());
345
346 let result = resolve_wallet_address(Some(" ".to_string()), false);
347 assert!(result.is_none());
348 }
349
350 #[rstest]
351 fn test_resolve_wallet_address_with_none_and_no_env_var() {
352 let result = resolve_wallet_address(None, true);
354
355 if std::env::var("DYDX_TESTNET_WALLET_ADDRESS").is_err() {
357 assert!(result.is_none());
358 }
359 }
360}