Skip to main content

nautilus_derive/common/
credential.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Derive credential storage.
17//!
18//! Derive identifies a trading account through a three-part tuple rather than a
19//! single API key:
20//!
21//! 1. `wallet_address`: the Derive Chain smart-contract wallet (NOT the user's
22//!    EOA). This is the value placed in the `X-LYRAWALLET` header and the
23//!    `owner` slot of every signed action. Visible in the Derive web app under
24//!    Home -> Developers -> "Derive Wallet".
25//! 2. `session_key`: a secp256k1 private key registered to the wallet. Signs
26//!    REST/WS auth headers and EIP-712 typed-data actions. May be the owner
27//!    EOA's key but is more commonly a scoped session key.
28//! 3. `subaccount_id`: per-wallet integer slot that holds the positions and
29//!    signs each `private/order` request.
30//!
31//! # Credential resolution
32//!
33//! Credentials are resolved in the following priority order:
34//!
35//! 1. Explicit values from config
36//! 2. `DERIVE_WALLET_ADDRESS` / `DERIVE_SESSION_PRIVATE_KEY` / `DERIVE_SUBACCOUNT_ID`
37//!    env vars (or the `_TESTNET_` variants when targeting testnet)
38//!
39//! The session-key bytes are zeroized on drop.
40
41use 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/// Returns the environment-variable triple `(wallet, session_key, subaccount)`
53/// for the given environment.
54#[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 Chain smart-contract wallet + session-key + subaccount triple.
73#[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    /// Creates a new [`DeriveCredential`] instance.
83    #[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    /// Returns the Derive Chain smart-contract wallet address (`X-LYRAWALLET`).
93    #[must_use]
94    pub fn wallet_address(&self) -> &str {
95        &self.wallet_address
96    }
97
98    /// Returns the secp256k1 session-key private key (hex-encoded).
99    #[must_use]
100    pub fn session_key(&self) -> &str {
101        &self.session_key
102    }
103
104    /// Returns the subaccount integer ID.
105    #[must_use]
106    pub const fn subaccount_id(&self) -> u64 {
107        self.subaccount_id
108    }
109
110    /// Resolves a [`DeriveCredential`] from explicit values, falling back to
111    /// the documented environment variables when fields are unset.
112    ///
113    /// Resolution order per field is: explicit value, then env var. The env
114    /// var name set is selected by `environment` via [`credential_env_vars`].
115    ///
116    /// # Errors
117    ///
118    /// Returns an error when any of the wallet address, session key, or
119    /// subaccount ID cannot be resolved from either source, or when the
120    /// subaccount id env var is not a valid `u64`.
121    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}