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};
41
42use crate::common::{consts::DYDX_BECH32_PREFIX, enums::DydxNetwork};
43
44#[must_use]
49pub fn credential_env_vars(network: DydxNetwork) -> (&'static str, &'static str) {
50 match network {
51 DydxNetwork::Testnet => ("DYDX_TESTNET_PRIVATE_KEY", "DYDX_TESTNET_WALLET_ADDRESS"),
52 DydxNetwork::Mainnet => ("DYDX_PRIVATE_KEY", "DYDX_WALLET_ADDRESS"),
53 }
54}
55
56pub struct DydxCredential {
65 signing_key: SigningKey,
67 pub address: String,
69 pub authenticator_ids: Vec<u64>,
71}
72
73impl Debug for DydxCredential {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.debug_struct(stringify!(DydxCredential))
76 .field("address", &self.address)
77 .field("authenticator_ids", &self.authenticator_ids)
78 .field("signing_key", &REDACTED)
79 .finish()
80 }
81}
82
83impl DydxCredential {
84 pub fn from_private_key(
90 private_key_hex: &str,
91 authenticator_ids: Vec<u64>,
92 ) -> anyhow::Result<Self> {
93 let key_bytes = hex::decode(private_key_hex.trim_start_matches("0x"))
95 .context("Invalid hex private key")?;
96
97 let signing_key = SigningKey::from_slice(&key_bytes)
98 .map_err(|e| anyhow::anyhow!("Invalid secp256k1 private key: {e}"))?;
99
100 let public_key = signing_key.public_key();
102 let account_id = public_key
103 .account_id(DYDX_BECH32_PREFIX)
104 .map_err(|e| anyhow::anyhow!("Failed to derive account ID: {e}"))?;
105 let address = account_id.to_string();
106
107 Ok(Self {
108 signing_key,
109 address,
110 authenticator_ids,
111 })
112 }
113
114 pub fn from_env(
124 network: DydxNetwork,
125 authenticator_ids: Vec<u64>,
126 ) -> anyhow::Result<Option<Self>> {
127 let (private_key_env, _) = credential_env_vars(network);
128
129 if let Some(private_key) =
130 get_or_env_var_opt(None, private_key_env).filter(|s| !s.trim().is_empty())
131 {
132 return Ok(Some(Self::from_private_key(
133 &private_key,
134 authenticator_ids,
135 )?));
136 }
137
138 Ok(None)
139 }
140
141 pub fn resolve(
153 private_key: Option<&str>,
154 network: DydxNetwork,
155 authenticator_ids: Vec<u64>,
156 ) -> anyhow::Result<Option<Self>> {
157 if let Some(pk) = private_key
159 && !pk.trim().is_empty()
160 {
161 return Ok(Some(Self::from_private_key(pk, authenticator_ids)?));
162 }
163
164 let (private_key_env, _) = credential_env_vars(network);
166 if let Some(pk) = get_or_env_var_opt(None, private_key_env).filter(|s| !s.trim().is_empty())
167 {
168 return Ok(Some(Self::from_private_key(&pk, authenticator_ids)?));
169 }
170
171 Ok(None)
172 }
173
174 pub fn account_id(&self) -> anyhow::Result<AccountId> {
180 self.address
181 .parse()
182 .map_err(|e| anyhow::anyhow!("Failed to parse account ID: {e}"))
183 }
184
185 pub fn sign(&self, sign_doc: &SignDoc) -> anyhow::Result<Vec<u8>> {
193 let sign_bytes = sign_doc
194 .clone()
195 .into_bytes()
196 .map_err(|e| anyhow::anyhow!("Failed to serialize SignDoc: {e}"))?;
197
198 let signature = self
199 .signing_key
200 .sign(&sign_bytes)
201 .map_err(|e| anyhow::anyhow!("Failed to sign: {e}"))?;
202 Ok(signature.to_bytes().to_vec())
203 }
204
205 pub fn sign_bytes(&self, message: &[u8]) -> anyhow::Result<Vec<u8>> {
213 let signature = self
214 .signing_key
215 .sign(message)
216 .map_err(|e| anyhow::anyhow!("Failed to sign: {e}"))?;
217 Ok(signature.to_bytes().to_vec())
218 }
219
220 pub fn public_key(&self) -> PublicKey {
222 self.signing_key.public_key()
223 }
224}
225
226#[must_use]
238pub fn resolve_wallet_address(
239 wallet_address: Option<String>,
240 network: DydxNetwork,
241) -> Option<String> {
242 let (_, wallet_env_var) = credential_env_vars(network);
243 get_or_env_var_opt(wallet_address, wallet_env_var).filter(|s| !s.trim().is_empty())
244}
245
246#[cfg(test)]
247mod tests {
248 use rstest::rstest;
249
250 use super::*;
251
252 const TEST_PRIVATE_KEY: &str =
254 "0000000000000000000000000000000000000000000000000000000000000001";
255
256 #[rstest]
257 fn test_from_private_key() {
258 let credential = DydxCredential::from_private_key(TEST_PRIVATE_KEY, vec![])
259 .expect("Failed to create credential from private key");
260
261 assert!(credential.address.starts_with("dydx"));
262 assert!(credential.authenticator_ids.is_empty());
263 }
264
265 #[rstest]
266 fn test_from_private_key_with_authenticators() {
267 let credential = DydxCredential::from_private_key(TEST_PRIVATE_KEY, vec![1, 2, 3])
268 .expect("Failed to create credential");
269
270 assert_eq!(credential.authenticator_ids, vec![1, 2, 3]);
271 }
272
273 #[rstest]
274 fn test_from_private_key_with_0x_prefix() {
275 let key_with_prefix = format!("0x{TEST_PRIVATE_KEY}");
276 let credential = DydxCredential::from_private_key(&key_with_prefix, vec![])
277 .expect("Failed to create credential from private key with 0x prefix");
278
279 assert!(credential.address.starts_with("dydx"));
280 }
281
282 #[rstest]
283 fn test_account_id() {
284 let credential = DydxCredential::from_private_key(TEST_PRIVATE_KEY, vec![])
285 .expect("Failed to create credential");
286
287 let account_id = credential.account_id().expect("Failed to get account ID");
288 assert_eq!(account_id.to_string(), credential.address);
289 }
290
291 #[rstest]
292 fn test_sign_bytes() {
293 let credential = DydxCredential::from_private_key(TEST_PRIVATE_KEY, vec![])
294 .expect("Failed to create credential");
295
296 let message = b"test message";
297 let signature = credential
298 .sign_bytes(message)
299 .expect("Failed to sign bytes");
300
301 assert_eq!(signature.len(), 64);
303 }
304
305 #[rstest]
306 fn test_debug_redacts_key() {
307 let credential = DydxCredential::from_private_key(TEST_PRIVATE_KEY, vec![])
308 .expect("Failed to create credential");
309
310 let debug_str = format!("{credential:?}");
311 assert!(debug_str.contains(REDACTED));
313 assert!(debug_str.contains("DydxCredential"));
315 assert!(debug_str.contains(&credential.address));
317 }
318
319 #[rstest]
320 fn test_resolve_with_provided_private_key() {
321 let result = DydxCredential::resolve(Some(TEST_PRIVATE_KEY), DydxNetwork::Mainnet, vec![])
322 .expect("Failed to resolve credential");
323
324 assert!(result.is_some());
325 let credential = result.unwrap();
326 assert!(credential.address.starts_with("dydx"));
327 }
328
329 #[rstest]
330 fn test_resolve_with_none_and_no_env_var() {
331 let result = DydxCredential::resolve(None, DydxNetwork::Testnet, vec![])
333 .expect("Should not error when credential not available");
334
335 if std::env::var("DYDX_TESTNET_PRIVATE_KEY").is_err() {
337 assert!(result.is_none());
338 }
339 }
340
341 #[rstest]
342 fn test_resolve_wallet_address_with_provided_value() {
343 let result = resolve_wallet_address(Some("dydx1abc123".to_string()), DydxNetwork::Mainnet);
344 assert_eq!(result, Some("dydx1abc123".to_string()));
345 }
346
347 #[rstest]
348 fn test_resolve_wallet_address_empty_string_returns_none() {
349 let result = resolve_wallet_address(Some(String::new()), DydxNetwork::Mainnet);
350 assert!(result.is_none());
351
352 let result = resolve_wallet_address(Some(" ".to_string()), DydxNetwork::Mainnet);
353 assert!(result.is_none());
354 }
355
356 #[rstest]
357 fn test_resolve_wallet_address_with_none_and_no_env_var() {
358 let result = resolve_wallet_address(None, DydxNetwork::Testnet);
360
361 if std::env::var("DYDX_TESTNET_WALLET_ADDRESS").is_err() {
363 assert!(result.is_none());
364 }
365 }
366}