use std::fmt::{Debug, Display};
use anyhow::Context;
use nautilus_core::env::{get_or_env_var, get_or_env_var_opt};
use zeroize::{Zeroize, ZeroizeOnDrop};
use crate::common::enums::DeriveEnvironment;
#[must_use]
pub fn credential_env_vars(
environment: DeriveEnvironment,
) -> (&'static str, &'static str, &'static str) {
match environment {
DeriveEnvironment::Mainnet => (
"DERIVE_WALLET_ADDRESS",
"DERIVE_SESSION_PRIVATE_KEY",
"DERIVE_SUBACCOUNT_ID",
),
DeriveEnvironment::Testnet => (
"DERIVE_TESTNET_WALLET_ADDRESS",
"DERIVE_TESTNET_SESSION_PRIVATE_KEY",
"DERIVE_TESTNET_SUBACCOUNT_ID",
),
}
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct DeriveCredential {
wallet_address: String,
session_key: String,
#[zeroize(skip)]
subaccount_id: u64,
}
impl DeriveCredential {
#[must_use]
pub fn new(wallet_address: String, session_key: String, subaccount_id: u64) -> Self {
Self {
wallet_address,
session_key,
subaccount_id,
}
}
#[must_use]
pub fn wallet_address(&self) -> &str {
&self.wallet_address
}
#[must_use]
pub fn session_key(&self) -> &str {
&self.session_key
}
#[must_use]
pub const fn subaccount_id(&self) -> u64 {
self.subaccount_id
}
pub fn resolve(
wallet_address: Option<String>,
session_key: Option<String>,
subaccount_id: Option<u64>,
environment: DeriveEnvironment,
) -> anyhow::Result<Self> {
let (wallet_var, key_var, subaccount_var) = credential_env_vars(environment);
let wallet_address = get_or_env_var(wallet_address, wallet_var).with_context(|| {
format!("Derive wallet address missing (set {wallet_var} or config)")
})?;
let session_key = get_or_env_var(session_key, key_var)
.with_context(|| format!("Derive session key missing (set {key_var} or config)"))?;
let subaccount_id = match subaccount_id {
Some(id) => id,
None => get_or_env_var_opt(None, subaccount_var)
.with_context(|| {
format!("Derive subaccount id missing (set {subaccount_var} or config)")
})?
.parse::<u64>()
.with_context(|| format!("failed to parse {subaccount_var} as u64"))?,
};
Ok(Self::new(wallet_address, session_key, subaccount_id))
}
}
impl Debug for DeriveCredential {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(stringify!(DeriveCredential))
.field("wallet_address", &self.wallet_address)
.field("session_key", &"***redacted***")
.field("subaccount_id", &self.subaccount_id)
.finish()
}
}
impl Display for DeriveCredential {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"DeriveCredential(wallet={}, subaccount={})",
self.wallet_address, self.subaccount_id
)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
const TEST_WALLET: &str = "0x0000000000000000000000000000000000001234";
const TEST_SESSION_KEY: &str =
"0x2ae8be44db8a590d20bffbe3b6872df9b569147d3bf6801a35a28281a4816bbd";
const TEST_SUBACCOUNT: u64 = 30769;
#[rstest]
fn test_credential_debug_redacts_session_key() {
let cred = DeriveCredential::new(
TEST_WALLET.to_string(),
TEST_SESSION_KEY.to_string(),
TEST_SUBACCOUNT,
);
let debug = format!("{cred:?}");
assert!(debug.contains("redacted"));
assert!(!debug.contains(TEST_SESSION_KEY));
assert!(debug.contains(TEST_WALLET));
assert!(debug.contains(&TEST_SUBACCOUNT.to_string()));
}
#[rstest]
fn test_credential_display_omits_session_key() {
let cred = DeriveCredential::new(
TEST_WALLET.to_string(),
TEST_SESSION_KEY.to_string(),
TEST_SUBACCOUNT,
);
let display = format!("{cred}");
assert!(display.contains(TEST_WALLET));
assert!(!display.contains(TEST_SESSION_KEY));
}
#[rstest]
fn test_credential_env_vars_for_mainnet() {
let (wallet, key, sub) = credential_env_vars(DeriveEnvironment::Mainnet);
assert_eq!(wallet, "DERIVE_WALLET_ADDRESS");
assert_eq!(key, "DERIVE_SESSION_PRIVATE_KEY");
assert_eq!(sub, "DERIVE_SUBACCOUNT_ID");
}
#[rstest]
fn test_credential_env_vars_for_testnet() {
let (wallet, key, sub) = credential_env_vars(DeriveEnvironment::Testnet);
assert_eq!(wallet, "DERIVE_TESTNET_WALLET_ADDRESS");
assert_eq!(key, "DERIVE_TESTNET_SESSION_PRIVATE_KEY");
assert_eq!(sub, "DERIVE_TESTNET_SUBACCOUNT_ID");
}
#[rstest]
fn test_credential_accessors() {
let cred = DeriveCredential::new(
TEST_WALLET.to_string(),
TEST_SESSION_KEY.to_string(),
TEST_SUBACCOUNT,
);
assert_eq!(cred.wallet_address(), TEST_WALLET);
assert_eq!(cred.session_key(), TEST_SESSION_KEY);
assert_eq!(cred.subaccount_id(), TEST_SUBACCOUNT);
}
#[rstest]
fn test_resolve_prefers_explicit_values() {
let cred = DeriveCredential::resolve(
Some(TEST_WALLET.to_string()),
Some(TEST_SESSION_KEY.to_string()),
Some(TEST_SUBACCOUNT),
DeriveEnvironment::Testnet,
)
.unwrap();
assert_eq!(cred.wallet_address(), TEST_WALLET);
assert_eq!(cred.session_key(), TEST_SESSION_KEY);
assert_eq!(cred.subaccount_id(), TEST_SUBACCOUNT);
}
}