use crate::{
errors::HyperliquidError, providers::nonce::NonceManager, signers::HyperliquidSigner,
Network,
};
use alloy::primitives::Address;
use alloy::signers::local::PrivateKeySigner;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
#[derive(Clone)]
pub struct AgentWallet {
pub address: Address,
pub signer: PrivateKeySigner,
pub created_at: Instant,
pub nonce_manager: Arc<NonceManager>,
pub status: AgentStatus,
}
#[derive(Clone, Debug, PartialEq)]
pub enum AgentStatus {
Active,
PendingRotation,
Deregistered,
}
impl AgentWallet {
pub fn new(signer: PrivateKeySigner) -> Self {
Self {
address: signer.address(),
signer,
created_at: Instant::now(),
nonce_manager: Arc::new(NonceManager::new(false)), status: AgentStatus::Active,
}
}
pub fn should_rotate(&self, ttl: Duration) -> bool {
match self.status {
AgentStatus::Active => self.created_at.elapsed() > ttl,
AgentStatus::PendingRotation | AgentStatus::Deregistered => true,
}
}
pub fn next_nonce(&self) -> u64 {
self.nonce_manager.next_nonce(None)
}
}
#[derive(Clone, Debug)]
pub struct AgentConfig {
pub ttl: Duration,
pub health_check_interval: Duration,
pub proactive_rotation_buffer: Duration,
}
impl Default for AgentConfig {
fn default() -> Self {
Self {
ttl: Duration::from_secs(23 * 60 * 60), health_check_interval: Duration::from_secs(300), proactive_rotation_buffer: Duration::from_secs(60 * 60), }
}
}
pub struct AgentManager<S: HyperliquidSigner> {
master_signer: S,
agents: Arc<RwLock<std::collections::HashMap<String, AgentWallet>>>,
config: AgentConfig,
network: Network,
}
impl<S: HyperliquidSigner + Clone> AgentManager<S> {
pub fn new(master_signer: S, config: AgentConfig, network: Network) -> Self {
Self {
master_signer,
agents: Arc::new(RwLock::new(std::collections::HashMap::new())),
config,
network,
}
}
pub async fn get_or_rotate_agent(
&self,
name: &str,
) -> Result<AgentWallet, HyperliquidError> {
let mut agents = self.agents.write().await;
if let Some(agent) = agents.get(name) {
let effective_ttl = self
.config
.ttl
.saturating_sub(self.config.proactive_rotation_buffer);
if !agent.should_rotate(effective_ttl) {
return Ok(agent.clone());
}
let mut agent_mut = agent.clone();
agent_mut.status = AgentStatus::PendingRotation;
agents.insert(name.to_string(), agent_mut);
}
let new_agent = self.create_new_agent(name).await?;
agents.insert(name.to_string(), new_agent.clone());
Ok(new_agent)
}
async fn create_new_agent(
&self,
name: &str,
) -> Result<AgentWallet, HyperliquidError> {
let agent_signer = PrivateKeySigner::random();
let agent_wallet = AgentWallet::new(agent_signer.clone());
self.approve_agent_internal(agent_wallet.address, Some(name.to_string()))
.await?;
Ok(agent_wallet)
}
async fn approve_agent_internal(
&self,
agent_address: Address,
name: Option<String>,
) -> Result<(), HyperliquidError> {
use crate::providers::RawExchangeProvider;
let raw_provider = match self.network {
Network::Mainnet => RawExchangeProvider::mainnet(self.master_signer.clone()),
Network::Testnet => RawExchangeProvider::testnet(self.master_signer.clone()),
};
raw_provider.approve_agent(agent_address, name).await?;
Ok(())
}
pub async fn get_active_agents(&self) -> Vec<(String, AgentWallet)> {
let agents = self.agents.read().await;
agents
.iter()
.filter(|(_, agent)| agent.status == AgentStatus::Active)
.map(|(name, agent)| (name.clone(), agent.clone()))
.collect()
}
pub async fn mark_deregistered(&self, name: &str) {
let mut agents = self.agents.write().await;
if let Some(agent) = agents.get_mut(name) {
agent.status = AgentStatus::Deregistered;
}
}
pub async fn cleanup_deregistered(&self) {
let mut agents = self.agents.write().await;
agents.retain(|_, agent| agent.status != AgentStatus::Deregistered);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_rotation_check() {
let signer = PrivateKeySigner::random();
let agent = AgentWallet::new(signer);
assert!(!agent.should_rotate(Duration::from_secs(24 * 60 * 60)));
assert!(agent.should_rotate(Duration::ZERO));
}
#[test]
fn test_agent_nonce_generation() {
let signer = PrivateKeySigner::random();
let agent = AgentWallet::new(signer);
let nonce1 = agent.next_nonce();
let nonce2 = agent.next_nonce();
assert!(nonce2 > nonce1);
}
}