nautilus_derive/common/
credential.rs1use std::fmt::{Debug, Display};
42
43use anyhow::Context;
44use nautilus_core::{
45 env::{get_or_env_var, get_or_env_var_opt},
46 string::secret::REDACTED,
47};
48use zeroize::{Zeroize, ZeroizeOnDrop};
49
50use crate::common::enums::DeriveEnvironment;
51
52#[must_use]
55pub fn credential_env_vars(
56 environment: DeriveEnvironment,
57) -> (&'static str, &'static str, &'static str) {
58 match environment {
59 DeriveEnvironment::Mainnet => (
60 "DERIVE_WALLET_ADDRESS",
61 "DERIVE_SESSION_PRIVATE_KEY",
62 "DERIVE_SUBACCOUNT_ID",
63 ),
64 DeriveEnvironment::Testnet => (
65 "DERIVE_TESTNET_WALLET_ADDRESS",
66 "DERIVE_TESTNET_SESSION_PRIVATE_KEY",
67 "DERIVE_TESTNET_SUBACCOUNT_ID",
68 ),
69 }
70}
71
72#[derive(Clone, Zeroize, ZeroizeOnDrop)]
74pub struct DeriveCredential {
75 wallet_address: String,
76 session_key: String,
77 #[zeroize(skip)]
78 subaccount_id: u64,
79}
80
81impl DeriveCredential {
82 #[must_use]
84 pub fn new(wallet_address: String, session_key: String, subaccount_id: u64) -> Self {
85 Self {
86 wallet_address,
87 session_key,
88 subaccount_id,
89 }
90 }
91
92 #[must_use]
94 pub fn wallet_address(&self) -> &str {
95 &self.wallet_address
96 }
97
98 #[must_use]
100 pub fn session_key(&self) -> &str {
101 &self.session_key
102 }
103
104 #[must_use]
106 pub const fn subaccount_id(&self) -> u64 {
107 self.subaccount_id
108 }
109
110 pub fn resolve(
122 wallet_address: Option<String>,
123 session_key: Option<String>,
124 subaccount_id: Option<u64>,
125 environment: DeriveEnvironment,
126 ) -> anyhow::Result<Self> {
127 let (wallet_var, key_var, subaccount_var) = credential_env_vars(environment);
128
129 let wallet_address = get_or_env_var(wallet_address, wallet_var).with_context(|| {
130 format!("Derive wallet address missing (set {wallet_var} or config)")
131 })?;
132 let session_key = get_or_env_var(session_key, key_var)
133 .with_context(|| format!("Derive session key missing (set {key_var} or config)"))?;
134
135 let subaccount_id = match subaccount_id {
136 Some(id) => id,
137 None => get_or_env_var_opt(None, subaccount_var)
138 .with_context(|| {
139 format!("Derive subaccount id missing (set {subaccount_var} or config)")
140 })?
141 .parse::<u64>()
142 .with_context(|| format!("failed to parse {subaccount_var} as u64"))?,
143 };
144
145 Ok(Self::new(wallet_address, session_key, subaccount_id))
146 }
147}
148
149impl Debug for DeriveCredential {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 f.debug_struct(stringify!(DeriveCredential))
152 .field("wallet_address", &self.wallet_address)
153 .field("session_key", &REDACTED)
154 .field("subaccount_id", &self.subaccount_id)
155 .finish()
156 }
157}
158
159impl Display for DeriveCredential {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 write!(
162 f,
163 "DeriveCredential(wallet={}, subaccount={})",
164 self.wallet_address, self.subaccount_id
165 )
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use rstest::rstest;
172
173 use super::*;
174
175 const TEST_WALLET: &str = "0x0000000000000000000000000000000000001234";
176 const TEST_SESSION_KEY: &str =
177 "0x2ae8be44db8a590d20bffbe3b6872df9b569147d3bf6801a35a28281a4816bbd";
178 const TEST_SUBACCOUNT: u64 = 30769;
179
180 #[rstest]
181 fn test_credential_debug_redacts_session_key() {
182 let cred = DeriveCredential::new(
183 TEST_WALLET.to_string(),
184 TEST_SESSION_KEY.to_string(),
185 TEST_SUBACCOUNT,
186 );
187 let debug = format!("{cred:?}");
188 assert!(debug.contains("redacted"));
189 assert!(!debug.contains(TEST_SESSION_KEY));
190 assert!(debug.contains(TEST_WALLET));
191 assert!(debug.contains(&TEST_SUBACCOUNT.to_string()));
192 }
193
194 #[rstest]
195 fn test_credential_display_omits_session_key() {
196 let cred = DeriveCredential::new(
197 TEST_WALLET.to_string(),
198 TEST_SESSION_KEY.to_string(),
199 TEST_SUBACCOUNT,
200 );
201 let display = format!("{cred}");
202 assert!(display.contains(TEST_WALLET));
203 assert!(!display.contains(TEST_SESSION_KEY));
204 }
205
206 #[rstest]
207 fn test_credential_env_vars_for_mainnet() {
208 let (wallet, key, sub) = credential_env_vars(DeriveEnvironment::Mainnet);
209 assert_eq!(wallet, "DERIVE_WALLET_ADDRESS");
210 assert_eq!(key, "DERIVE_SESSION_PRIVATE_KEY");
211 assert_eq!(sub, "DERIVE_SUBACCOUNT_ID");
212 }
213
214 #[rstest]
215 fn test_credential_env_vars_for_testnet() {
216 let (wallet, key, sub) = credential_env_vars(DeriveEnvironment::Testnet);
217 assert_eq!(wallet, "DERIVE_TESTNET_WALLET_ADDRESS");
218 assert_eq!(key, "DERIVE_TESTNET_SESSION_PRIVATE_KEY");
219 assert_eq!(sub, "DERIVE_TESTNET_SUBACCOUNT_ID");
220 }
221
222 #[rstest]
223 fn test_credential_accessors() {
224 let cred = DeriveCredential::new(
225 TEST_WALLET.to_string(),
226 TEST_SESSION_KEY.to_string(),
227 TEST_SUBACCOUNT,
228 );
229 assert_eq!(cred.wallet_address(), TEST_WALLET);
230 assert_eq!(cred.session_key(), TEST_SESSION_KEY);
231 assert_eq!(cred.subaccount_id(), TEST_SUBACCOUNT);
232 }
233
234 #[rstest]
235 fn test_resolve_prefers_explicit_values() {
236 let cred = DeriveCredential::resolve(
237 Some(TEST_WALLET.to_string()),
238 Some(TEST_SESSION_KEY.to_string()),
239 Some(TEST_SUBACCOUNT),
240 DeriveEnvironment::Testnet,
241 )
242 .unwrap();
243 assert_eq!(cred.wallet_address(), TEST_WALLET);
244 assert_eq!(cred.session_key(), TEST_SESSION_KEY);
245 assert_eq!(cred.subaccount_id(), TEST_SUBACCOUNT);
246 }
247}