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::env::{get_or_env_var, get_or_env_var_opt};
45use zeroize::{Zeroize, ZeroizeOnDrop};
46
47use crate::common::enums::DeriveEnvironment;
48
49/// Returns the environment-variable triple `(wallet, session_key, subaccount)`
50/// for the given environment.
51#[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 Chain smart-contract wallet + session-key + subaccount triple.
70#[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    /// Creates a new [`DeriveCredential`] instance.
80    #[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    /// Returns the Derive Chain smart-contract wallet address (`X-LYRAWALLET`).
90    #[must_use]
91    pub fn wallet_address(&self) -> &str {
92        &self.wallet_address
93    }
94
95    /// Returns the secp256k1 session-key private key (hex-encoded).
96    #[must_use]
97    pub fn session_key(&self) -> &str {
98        &self.session_key
99    }
100
101    /// Returns the subaccount integer ID.
102    #[must_use]
103    pub const fn subaccount_id(&self) -> u64 {
104        self.subaccount_id
105    }
106
107    /// Resolves a [`DeriveCredential`] from explicit values, falling back to
108    /// the documented environment variables when fields are unset.
109    ///
110    /// Resolution order per field is: explicit value, then env var. The env
111    /// var name set is selected by `environment` via [`credential_env_vars`].
112    ///
113    /// # Errors
114    ///
115    /// Returns an error when any of the wallet address, session key, or
116    /// subaccount ID cannot be resolved from either source, or when the
117    /// subaccount id env var is not a valid `u64`.
118    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}