hyperliquid_sdk_rs/providers/
agent.rs

1//! Agent wallet management with automatic rotation and safety features
2
3use crate::{
4    errors::HyperliquidError, providers::nonce::NonceManager, signers::HyperliquidSigner,
5    Network,
6};
7use alloy::primitives::Address;
8use alloy::signers::local::PrivateKeySigner;
9use std::sync::Arc;
10use std::time::{Duration, Instant};
11use tokio::sync::RwLock;
12
13/// Agent wallet with lifecycle tracking
14#[derive(Clone)]
15pub struct AgentWallet {
16    /// Agent's address
17    pub address: Address,
18    /// Agent's signer
19    pub signer: PrivateKeySigner,
20    /// When this agent was created
21    pub created_at: Instant,
22    /// Dedicated nonce manager for this agent
23    pub nonce_manager: Arc<NonceManager>,
24    /// Current status
25    pub status: AgentStatus,
26}
27
28#[derive(Clone, Debug, PartialEq)]
29pub enum AgentStatus {
30    /// Agent is active and healthy
31    Active,
32    /// Agent is marked for rotation
33    PendingRotation,
34    /// Agent has been deregistered
35    Deregistered,
36}
37
38impl AgentWallet {
39    /// Create a new agent wallet
40    pub fn new(signer: PrivateKeySigner) -> Self {
41        Self {
42            address: signer.address(),
43            signer,
44            created_at: Instant::now(),
45            nonce_manager: Arc::new(NonceManager::new(false)), // No isolation within agent
46            status: AgentStatus::Active,
47        }
48    }
49
50    /// Check if agent should be rotated based on TTL
51    pub fn should_rotate(&self, ttl: Duration) -> bool {
52        match self.status {
53            AgentStatus::Active => self.created_at.elapsed() > ttl,
54            AgentStatus::PendingRotation | AgentStatus::Deregistered => true,
55        }
56    }
57
58    /// Get next nonce for this agent
59    pub fn next_nonce(&self) -> u64 {
60        self.nonce_manager.next_nonce(None)
61    }
62}
63
64/// Configuration for agent management
65#[derive(Clone, Debug)]
66pub struct AgentConfig {
67    /// Time before rotating an agent
68    pub ttl: Duration,
69    /// Check agent health at this interval
70    pub health_check_interval: Duration,
71    /// Rotate agents proactively before expiry
72    pub proactive_rotation_buffer: Duration,
73}
74
75impl Default for AgentConfig {
76    fn default() -> Self {
77        Self {
78            ttl: Duration::from_secs(23 * 60 * 60), // Rotate daily
79            health_check_interval: Duration::from_secs(300), // Check every 5 min
80            proactive_rotation_buffer: Duration::from_secs(60 * 60), // Rotate 1hr before expiry
81        }
82    }
83}
84
85/// Manages agent lifecycle with automatic rotation
86pub struct AgentManager<S: HyperliquidSigner> {
87    /// Master signer that approves agents
88    master_signer: S,
89    /// Currently active agents by name
90    agents: Arc<RwLock<std::collections::HashMap<String, AgentWallet>>>,
91    /// Configuration
92    config: AgentConfig,
93    /// Network for agent operations
94    network: Network,
95}
96
97impl<S: HyperliquidSigner + Clone> AgentManager<S> {
98    /// Create a new agent manager
99    pub fn new(master_signer: S, config: AgentConfig, network: Network) -> Self {
100        Self {
101            master_signer,
102            agents: Arc::new(RwLock::new(std::collections::HashMap::new())),
103            config,
104            network,
105        }
106    }
107
108    /// Get or create an agent, rotating if necessary
109    pub async fn get_or_rotate_agent(
110        &self,
111        name: &str,
112    ) -> Result<AgentWallet, HyperliquidError> {
113        let mut agents = self.agents.write().await;
114
115        // Check if we have an active agent
116        if let Some(agent) = agents.get(name) {
117            let effective_ttl = self
118                .config
119                .ttl
120                .saturating_sub(self.config.proactive_rotation_buffer);
121
122            if !agent.should_rotate(effective_ttl) {
123                return Ok(agent.clone());
124            }
125
126            // Mark for rotation
127            let mut agent_mut = agent.clone();
128            agent_mut.status = AgentStatus::PendingRotation;
129            agents.insert(name.to_string(), agent_mut);
130        }
131
132        // Create new agent
133        let new_agent = self.create_new_agent(name).await?;
134        agents.insert(name.to_string(), new_agent.clone());
135
136        Ok(new_agent)
137    }
138
139    /// Create and approve a new agent
140    async fn create_new_agent(
141        &self,
142        name: &str,
143    ) -> Result<AgentWallet, HyperliquidError> {
144        // Generate new key for agent
145        let agent_signer = PrivateKeySigner::random();
146        let agent_wallet = AgentWallet::new(agent_signer.clone());
147
148        // We need to approve this agent using the exchange provider
149        // This is a bit circular, but we'll handle it carefully
150        self.approve_agent_internal(agent_wallet.address, Some(name.to_string()))
151            .await?;
152
153        Ok(agent_wallet)
154    }
155
156    /// Internal method to approve agent (will use exchange provider)
157    async fn approve_agent_internal(
158        &self,
159        agent_address: Address,
160        name: Option<String>,
161    ) -> Result<(), HyperliquidError> {
162        use crate::providers::RawExchangeProvider;
163
164        // Create a temporary raw provider just for agent approval
165        let raw_provider = match self.network {
166            Network::Mainnet => RawExchangeProvider::mainnet(self.master_signer.clone()),
167            Network::Testnet => RawExchangeProvider::testnet(self.master_signer.clone()),
168        };
169
170        // Approve the agent
171        raw_provider.approve_agent(agent_address, name).await?;
172
173        Ok(())
174    }
175
176    /// Get all active agents
177    pub async fn get_active_agents(&self) -> Vec<(String, AgentWallet)> {
178        let agents = self.agents.read().await;
179        agents
180            .iter()
181            .filter(|(_, agent)| agent.status == AgentStatus::Active)
182            .map(|(name, agent)| (name.clone(), agent.clone()))
183            .collect()
184    }
185
186    /// Mark an agent as deregistered
187    pub async fn mark_deregistered(&self, name: &str) {
188        let mut agents = self.agents.write().await;
189        if let Some(agent) = agents.get_mut(name) {
190            agent.status = AgentStatus::Deregistered;
191        }
192    }
193
194    /// Clean up deregistered agents
195    pub async fn cleanup_deregistered(&self) {
196        let mut agents = self.agents.write().await;
197        agents.retain(|_, agent| agent.status != AgentStatus::Deregistered);
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_agent_rotation_check() {
207        let signer = PrivateKeySigner::random();
208        let agent = AgentWallet::new(signer);
209
210        // Should not rotate immediately
211        assert!(!agent.should_rotate(Duration::from_secs(24 * 60 * 60)));
212
213        // Test with zero duration (should always rotate)
214        assert!(agent.should_rotate(Duration::ZERO));
215    }
216
217    #[test]
218    fn test_agent_nonce_generation() {
219        let signer = PrivateKeySigner::random();
220        let agent = AgentWallet::new(signer);
221
222        let nonce1 = agent.next_nonce();
223        let nonce2 = agent.next_nonce();
224
225        assert!(nonce2 > nonce1);
226    }
227}