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