nautilus_dydx/common/
credential.rs1#![allow(unused_assignments)] use std::fmt::Debug;
33
34use anyhow::Context;
35use cosmrs::{
36 AccountId,
37 crypto::{PublicKey, secp256k1::SigningKey},
38 tx::SignDoc,
39};
40use nautilus_core::{env::get_or_env_var_opt, hex, string::secret::REDACTED};
41use zeroize::Zeroizing;
42
43use crate::common::{consts::DYDX_BECH32_PREFIX, enums::DydxNetwork};
44
45#[must_use]
50pub fn credential_env_vars(network: DydxNetwork) -> (&'static str, &'static str) {
51 match network {
52 DydxNetwork::Testnet => ("DYDX_TESTNET_PRIVATE_KEY", "DYDX_TESTNET_WALLET_ADDRESS"),
53 DydxNetwork::Mainnet => ("DYDX_PRIVATE_KEY", "DYDX_WALLET_ADDRESS"),
54 }
55}
56
57pub struct DydxCredential {
66 signing_key: SigningKey,
68 pub address: String,
70 pub authenticator_ids: Vec<u64>,
72}
73
74impl Debug for DydxCredential {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 f.debug_struct(stringify!(DydxCredential))
77 .field("address", &self.address)
78 .field("authenticator_ids", &self.authenticator_ids)
79 .field("signing_key", &REDACTED)
80 .finish()
81 }
82}
83
84impl DydxCredential {
85 pub fn from_private_key(
91 private_key_hex: &str,
92 authenticator_ids: Vec<u64>,
93 ) -> anyhow::Result<Self> {
94 let key_bytes = Zeroizing::new(
96 hex::decode(private_key_hex.trim_start_matches("0x"))
97 .context("Invalid hex private key")?,
98 );
99
100 let signing_key = SigningKey::from_slice(&key_bytes)
101 .map_err(|e| anyhow::anyhow!("Invalid secp256k1 private key: {e}"))?;
102
103 let public_key = signing_key.public_key();
105 let account_id = public_key
106 .account_id(DYDX_BECH32_PREFIX)
107 .map_err(|e| anyhow::anyhow!("Failed to derive account ID: {e}"))?;
108 let address = account_id.to_string();
109
110 Ok(Self {
111 signing_key,
112 address,
113 authenticator_ids,
114 })
115 }
116
117 pub fn from_env(
127 network: DydxNetwork,
128 authenticator_ids: Vec<u64>,
129 ) -> anyhow::Result<Option<Self>> {
130 let (private_key_env, _) = credential_env_vars(network);
131
132 if let Some(private_key) =
133 get_or_env_var_opt(None, private_key_env).filter(|s| !s.trim().is_empty())
134 {
135 return Ok(Some(Self::from_private_key(
136 &private_key,
137 authenticator_ids,
138 )?));
139 }
140
141 Ok(None)
142 }
143
144 pub fn resolve(
156 private_key: Option<&str>,
157 network: DydxNetwork,
158 authenticator_ids: Vec<u64>,
159 ) -> anyhow::Result<Option<Self>> {
160 if let Some(pk) = private_key
162 && !pk.trim().is_empty()
163 {
164 return Ok(Some(Self::from_private_key(pk, authenticator_ids)?));
165 }
166
167 let (private_key_env, _) = credential_env_vars(network);
169 if let Some(pk) = get_or_env_var_opt(None, private_key_env).filter(|s| !s.trim().is_empty())
170 {
171 return Ok(Some(Self::from_private_key(&pk, authenticator_ids)?));
172 }
173
174 Ok(None)
175 }
176
177 pub fn account_id(&self) -> anyhow::Result<AccountId> {
183 self.address
184 .parse()
185 .map_err(|e| anyhow::anyhow!("Failed to parse account ID: {e}"))
186 }
187
188 pub fn sign(&self, sign_doc: &SignDoc) -> anyhow::Result<Vec<u8>> {
196 let sign_bytes = sign_doc
197 .clone()
198 .into_bytes()
199 .map_err(|e| anyhow::anyhow!("Failed to serialize SignDoc: {e}"))?;
200
201 let signature = self
202 .signing_key
203 .sign(&sign_bytes)
204 .map_err(|e| anyhow::anyhow!("Failed to sign: {e}"))?;
205 Ok(signature.to_bytes().to_vec())
206 }
207
208 pub fn sign_bytes(&self, message: &[u8]) -> anyhow::Result<Vec<u8>> {
216 let signature = self
217 .signing_key
218 .sign(message)
219 .map_err(|e| anyhow::anyhow!("Failed to sign: {e}"))?;
220 Ok(signature.to_bytes().to_vec())
221 }
222
223 pub fn public_key(&self) -> PublicKey {
225 self.signing_key.public_key()
226 }
227}
228
229#[must_use]
241pub fn resolve_wallet_address(
242 wallet_address: Option<String>,
243 network: DydxNetwork,
244) -> Option<String> {
245 let (_, wallet_env_var) = credential_env_vars(network);
246 get_or_env_var_opt(wallet_address, wallet_env_var).filter(|s| !s.trim().is_empty())
247}
248
249#[cfg(test)]
250mod tests {
251 use rstest::rstest;
252
253 use super::*;
254
255 const TEST_PRIVATE_KEY: &str =
257 "0000000000000000000000000000000000000000000000000000000000000001";
258
259 #[rstest]
260 fn test_from_private_key() {
261 let credential = DydxCredential::from_private_key(TEST_PRIVATE_KEY, vec![])
262 .expect("Failed to create credential from private key");
263
264 assert!(credential.address.starts_with("dydx"));
265 assert!(credential.authenticator_ids.is_empty());
266 }
267
268 #[rstest]
269 fn test_from_private_key_with_authenticators() {
270 let credential = DydxCredential::from_private_key(TEST_PRIVATE_KEY, vec![1, 2, 3])
271 .expect("Failed to create credential");
272
273 assert_eq!(credential.authenticator_ids, vec![1, 2, 3]);
274 }
275
276 #[rstest]
277 fn test_from_private_key_with_0x_prefix() {
278 let key_with_prefix = format!("0x{TEST_PRIVATE_KEY}");
279 let credential = DydxCredential::from_private_key(&key_with_prefix, vec![])
280 .expect("Failed to create credential from private key with 0x prefix");
281
282 assert!(credential.address.starts_with("dydx"));
283 }
284
285 #[rstest]
286 fn test_account_id() {
287 let credential = DydxCredential::from_private_key(TEST_PRIVATE_KEY, vec![])
288 .expect("Failed to create credential");
289
290 let account_id = credential.account_id().expect("Failed to get account ID");
291 assert_eq!(account_id.to_string(), credential.address);
292 }
293
294 #[rstest]
295 fn test_sign_bytes() {
296 let credential = DydxCredential::from_private_key(TEST_PRIVATE_KEY, vec![])
297 .expect("Failed to create credential");
298
299 let message = b"test message";
300 let signature = credential
301 .sign_bytes(message)
302 .expect("Failed to sign bytes");
303
304 assert_eq!(signature.len(), 64);
306 }
307
308 #[rstest]
309 fn test_debug_redacts_key() {
310 let credential = DydxCredential::from_private_key(TEST_PRIVATE_KEY, vec![])
311 .expect("Failed to create credential");
312
313 let debug_str = format!("{credential:?}");
314 assert!(debug_str.contains(REDACTED));
316 assert!(debug_str.contains("DydxCredential"));
318 assert!(debug_str.contains(&credential.address));
320 }
321
322 #[rstest]
323 fn test_resolve_with_provided_private_key() {
324 let result = DydxCredential::resolve(Some(TEST_PRIVATE_KEY), DydxNetwork::Mainnet, vec![])
325 .expect("Failed to resolve credential");
326
327 assert!(result.is_some());
328 let credential = result.unwrap();
329 assert!(credential.address.starts_with("dydx"));
330 }
331
332 #[rstest]
333 fn test_resolve_with_none_and_no_env_var() {
334 let result = DydxCredential::resolve(None, DydxNetwork::Testnet, vec![])
336 .expect("Should not error when credential not available");
337
338 if std::env::var("DYDX_TESTNET_PRIVATE_KEY").is_err() {
340 assert!(result.is_none());
341 }
342 }
343
344 #[rstest]
345 fn test_resolve_wallet_address_with_provided_value() {
346 let result = resolve_wallet_address(Some("dydx1abc123".to_string()), DydxNetwork::Mainnet);
347 assert_eq!(result, Some("dydx1abc123".to_string()));
348 }
349
350 #[rstest]
351 fn test_resolve_wallet_address_empty_string_returns_none() {
352 let result = resolve_wallet_address(Some(String::new()), DydxNetwork::Mainnet);
353 assert!(result.is_none());
354
355 let result = resolve_wallet_address(Some(" ".to_string()), DydxNetwork::Mainnet);
356 assert!(result.is_none());
357 }
358
359 #[rstest]
360 fn test_resolve_wallet_address_with_none_and_no_env_var() {
361 let result = resolve_wallet_address(None, DydxNetwork::Testnet);
363
364 if std::env::var("DYDX_TESTNET_WALLET_ADDRESS").is_err() {
366 assert!(result.is_none());
367 }
368 }
369}