Skip to main content

df_hyperliquid_core/utils/sign/
user_signed.rs

1//! User-signed actions: EIP-712 typed data signed by the account's own wallet
2//! (never an agent) under `HyperliquidSignTransaction/1/<chain>/0x0`, the chain
3//! being the network's signature chain (42161 on mainnet, 421614 on testnet).
4//!
5//! One field list drives both the posted action and the typed struct: `type`,
6//! `signatureChainId`, `hyperliquidChain`, the action's own keys, then the
7//! nonce under its own name, as the `@nktkas/hyperliquid` SDK writes them.
8
9use crate::account::Signature;
10use crate::client::http::body;
11use crate::client::network::Network;
12use crate::utils::sign::eip712::{Domain, Field, TypedStruct};
13use crate::utils::sign::msgpack::Map;
14
15/// What an action calls its nonce. The older actions (`usdSend`, `spotSend`,
16/// `withdraw3`) say `time`; the number is the same.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum NonceKey {
19    Nonce,
20    Time,
21}
22
23impl NonceKey {
24    #[must_use]
25    pub const fn as_str(self) -> &'static str {
26        match self {
27            NonceKey::Nonce => "nonce",
28            NonceKey::Time => "time",
29        }
30    }
31}
32
33/// A user-signed action, everything that goes into its signature.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct UserSigned<'a> {
36    kind: &'static str,
37    primary_type: &'static str,
38    fields: Vec<(&'static str, Field<'a>)>,
39    nonce_key: NonceKey,
40    nonce: u64,
41}
42
43impl<'a> UserSigned<'a> {
44    /// `kind` is the posted `type` (`withdraw3`), `primary_type` the EIP-712
45    /// name (`HyperliquidTransaction:Withdraw`).
46    #[must_use]
47    pub const fn new(kind: &'static str, primary_type: &'static str, nonce_key: NonceKey, nonce: u64) -> Self {
48        UserSigned { kind, primary_type, fields: Vec::new(), nonce_key, nonce }
49    }
50
51    /// Appends one of the action's own keys; call order is wire and type order.
52    #[must_use]
53    pub fn field(mut self, name: &'static str, value: Field<'a>) -> Self {
54        self.fields.push((name, value));
55        self
56    }
57
58    #[must_use]
59    pub const fn kind(&self) -> &'static str {
60        self.kind
61    }
62
63    #[must_use]
64    pub const fn nonce(&self) -> u64 {
65        self.nonce
66    }
67
68    /// The action as posted on `network`, in wire order.
69    #[must_use]
70    pub fn action(&self, network: Network) -> Map {
71        let mut map = Map::new()
72            .put("type", self.kind)
73            .put("signatureChainId", network.signature_chain_id_hex())
74            .put("hyperliquidChain", network.hyperliquid_chain());
75        for (name, value) in &self.fields {
76            map = map.put(*name, value.to_wire());
77        }
78        map.put(self.nonce_key.as_str(), self.nonce)
79    }
80
81    /// [`UserSigned::action`] as JSON.
82    #[must_use]
83    pub fn wire(&self, network: Network) -> String {
84        self.action(network).to_json().compact()
85    }
86
87    /// `hyperliquidChain`, the action's own keys, then the nonce as `uint64`.
88    #[must_use]
89    pub fn typed(&self, network: Network) -> TypedStruct<'a> {
90        let mut data =
91            TypedStruct::new(self.primary_type).field("hyperliquidChain", Field::String(network.hyperliquid_chain()));
92        for &(name, value) in &self.fields {
93            data = data.field(name, value);
94        }
95        data.field(self.nonce_key.as_str(), Field::Uint64(self.nonce))
96    }
97
98    #[must_use]
99    pub fn digest(&self, network: Network) -> [u8; 32] {
100        self.typed(network).digest(&Domain::user_signed(network))
101    }
102
103    /// For the wallet. It wants to be on the network's signature chain to
104    /// sign this: Arbitrum One, or Arbitrum Sepolia on testnet.
105    #[must_use]
106    pub fn wallet_json(&self, network: Network) -> String {
107        self.typed(network).wallet_json(&Domain::user_signed(network))
108    }
109
110    /// The `/exchange` body, with no `vaultAddress`.
111    #[must_use]
112    pub fn body(&self, signature: &Signature, network: Network) -> String {
113        body::user_signed(&self.action(network), self.nonce, signature)
114    }
115}
116
117/// An action defined by its [`UserSigned`] form, from which come the posted
118/// JSON, typed data, digest, wallet JSON and body.
119pub trait UserAction {
120    fn user_signed(&self) -> UserSigned<'_>;
121
122    #[must_use]
123    fn nonce(&self) -> u64 {
124        self.user_signed().nonce()
125    }
126
127    #[must_use]
128    fn action(&self, network: Network) -> Map {
129        self.user_signed().action(network)
130    }
131
132    #[must_use]
133    fn wire(&self, network: Network) -> String {
134        self.user_signed().wire(network)
135    }
136
137    #[must_use]
138    fn typed(&self, network: Network) -> TypedStruct<'_> {
139        self.user_signed().typed(network)
140    }
141
142    #[must_use]
143    fn digest(&self, network: Network) -> [u8; 32] {
144        self.user_signed().digest(network)
145    }
146
147    #[must_use]
148    fn wallet_json(&self, network: Network) -> String {
149        self.user_signed().wallet_json(network)
150    }
151
152    #[must_use]
153    fn body(&self, signature: &Signature, network: Network) -> String {
154        self.user_signed().body(signature, network)
155    }
156}