Skip to main content

df_hyperliquid_core/account/keys/agents/
approve.rs

1//! `approveAgent`: an agent approved under a name, until an expiry, or unnamed, signed by the
2//! account's own wallet.
3
4use crate::account::Signature;
5use crate::account::keys::agents::name::{AgentName, VALID_UNTIL};
6use crate::client::Network;
7use crate::client::http::body;
8use crate::utils::Address;
9use crate::utils::sign::{Field, Map, NonceKey, UserAction, UserSigned, Value};
10
11/// The longest an approval may run: 180 days ahead.
12pub const MAX_AGENT_VALIDITY_MS: u64 = 180 * 86_400_000;
13
14/// An expiry that does not lie ahead of `now`, or lies more than 180 days ahead.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
16#[error("an agent's approval ends ahead of now ({now}) and within 180 days, which {until} does not")]
17pub struct BadExpiry {
18    pub until: u64,
19    pub now: u64,
20}
21
22/// `approveAgent`, signed by the account's own wallet. There is no revoke: approving a name again
23/// replaces that agent. Never approve an address that was an agent before, since what it signed
24/// then can be replayed.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct ApproveAgent {
27    agent: Address,
28    name: String,
29    /// The unnamed agent, posted with `agentName` null.
30    null: bool,
31    nonce: u64,
32}
33
34impl ApproveAgent {
35    /// Under `name` as given. Empty approves the unnamed agent, signed and posted as the empty string.
36    #[must_use]
37    pub fn new(agent: Address, name: &str, nonce: u64) -> ApproveAgent {
38        ApproveAgent { agent, name: name.to_owned(), null: false, nonce }
39    }
40
41    /// The unnamed agent, posted with `agentName` null and signed as the empty string: the digest is
42    /// the empty name's, only the posted JSON differs.
43    #[must_use]
44    pub const fn unnamed(agent: Address, nonce: u64) -> ApproveAgent {
45        ApproveAgent { agent, name: String::new(), null: true, nonce }
46    }
47
48    /// A named agent that never lapses.
49    #[must_use]
50    pub fn named(agent: Address, name: &AgentName, nonce: u64) -> ApproveAgent {
51        ApproveAgent::new(agent, name.as_str(), nonce)
52    }
53
54    /// A named agent until `until` milliseconds: `<name> valid_until <until>`,
55    /// refused unless it lies ahead of `now` and at most 180 days ahead.
56    pub fn expiring(
57        agent: Address,
58        name: &AgentName,
59        until: u64,
60        now: u64,
61        nonce: u64,
62    ) -> Result<ApproveAgent, BadExpiry> {
63        let ahead = until.checked_sub(now).filter(|ahead| *ahead > 0 && *ahead <= MAX_AGENT_VALIDITY_MS);
64        match ahead {
65            Some(_) => Ok(ApproveAgent::new(agent, &format!("{name}{VALID_UNTIL}{until}"), nonce)),
66            None => Err(BadExpiry { until, now }),
67        }
68    }
69
70    #[must_use]
71    pub const fn agent(&self) -> Address {
72        self.agent
73    }
74
75    /// `agentName` as signed; `None` for the unnamed agent.
76    #[must_use]
77    pub fn name(&self) -> Option<&str> {
78        Some(self.name.as_str()).filter(|name| !name.is_empty())
79    }
80}
81
82impl UserAction for ApproveAgent {
83    fn user_signed(&self) -> UserSigned<'_> {
84        UserSigned::new("approveAgent", "HyperliquidTransaction:ApproveAgent", NonceKey::Nonce, self.nonce)
85            .field("agentAddress", Field::Address(self.agent))
86            .field("agentName", Field::String(&self.name))
87    }
88
89    fn action(&self, network: Network) -> Map {
90        let action = self.user_signed().action(network);
91        if !self.null {
92            return action;
93        }
94        action.into_pairs().fold(Map::new(), |map, (key, value)| match key == "agentName" {
95            true => map.put(key, Value::Nil),
96            false => map.put(key, value),
97        })
98    }
99
100    fn wire(&self, network: Network) -> String {
101        self.action(network).to_json().compact()
102    }
103
104    fn body(&self, signature: &Signature, network: Network) -> String {
105        body::user_signed(&self.action(network), self.nonce, signature)
106    }
107}
108
109#[cfg(test)]
110#[path = "tests/approve.rs"]
111mod tests;