use crate::account::Signature;
use crate::account::keys::agents::name::{AgentName, VALID_UNTIL};
use crate::client::Network;
use crate::client::http::body;
use crate::utils::Address;
use crate::utils::sign::{Field, Map, NonceKey, UserAction, UserSigned, Value};
pub const MAX_AGENT_VALIDITY_MS: u64 = 180 * 86_400_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("an agent's approval ends ahead of now ({now}) and within 180 days, which {until} does not")]
pub struct BadExpiry {
pub until: u64,
pub now: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApproveAgent {
agent: Address,
name: String,
null: bool,
nonce: u64,
}
impl ApproveAgent {
#[must_use]
pub fn new(agent: Address, name: &str, nonce: u64) -> ApproveAgent {
ApproveAgent { agent, name: name.to_owned(), null: false, nonce }
}
#[must_use]
pub const fn unnamed(agent: Address, nonce: u64) -> ApproveAgent {
ApproveAgent { agent, name: String::new(), null: true, nonce }
}
#[must_use]
pub fn named(agent: Address, name: &AgentName, nonce: u64) -> ApproveAgent {
ApproveAgent::new(agent, name.as_str(), nonce)
}
pub fn expiring(
agent: Address,
name: &AgentName,
until: u64,
now: u64,
nonce: u64,
) -> Result<ApproveAgent, BadExpiry> {
let ahead = until.checked_sub(now).filter(|ahead| *ahead > 0 && *ahead <= MAX_AGENT_VALIDITY_MS);
match ahead {
Some(_) => Ok(ApproveAgent::new(agent, &format!("{name}{VALID_UNTIL}{until}"), nonce)),
None => Err(BadExpiry { until, now }),
}
}
#[must_use]
pub const fn agent(&self) -> Address {
self.agent
}
#[must_use]
pub fn name(&self) -> Option<&str> {
Some(self.name.as_str()).filter(|name| !name.is_empty())
}
}
impl UserAction for ApproveAgent {
fn user_signed(&self) -> UserSigned<'_> {
UserSigned::new("approveAgent", "HyperliquidTransaction:ApproveAgent", NonceKey::Nonce, self.nonce)
.field("agentAddress", Field::Address(self.agent))
.field("agentName", Field::String(&self.name))
}
fn action(&self, network: Network) -> Map {
let action = self.user_signed().action(network);
if !self.null {
return action;
}
action.into_pairs().fold(Map::new(), |map, (key, value)| match key == "agentName" {
true => map.put(key, Value::Nil),
false => map.put(key, value),
})
}
fn wire(&self, network: Network) -> String {
self.action(network).to_json().compact()
}
fn body(&self, signature: &Signature, network: Network) -> String {
body::user_signed(&self.action(network), self.nonce, signature)
}
}
#[cfg(test)]
#[path = "tests/approve.rs"]
mod tests;