Skip to main content

df_hyperliquid_core/utils/sign/
l1.rs

1//! L1 actions: msgpack, hashed into a connection id, signed as a phantom
2//! agent `Agent{source, connectionId}` under `Exchange/1/1337/0x0`. The
3//! source is `"a"` on mainnet and `"b"` on testnet; the hash has no network.
4
5use crate::account::Signature;
6use crate::client::http::body;
7use crate::client::network::Network;
8use crate::utils::address::Address;
9use crate::utils::sign::eip712::{Domain, Field, TypedStruct};
10use crate::utils::sign::keccak::Keccak256;
11use crate::utils::sign::msgpack::Map;
12
13/// `keccak256(msgpack || nonce as u64 big-endian || vault marker)`: `0x00`,
14/// or `0x01` and the vault's twenty bytes.
15#[must_use]
16pub fn connection_id(msgpack: &[u8], nonce: u64, vault: Option<Address>) -> [u8; 32] {
17    connection_id_expiring(msgpack, nonce, vault, None)
18}
19
20/// [`connection_id`] for an action posted with `expiresAfter`: the expiry follows the vault
21/// marker as `0x00` and the milliseconds as a big-endian u64, as the venue's Python SDK hashes
22/// it. With no expiry it is [`connection_id`].
23#[must_use]
24pub fn connection_id_expiring(
25    msgpack: &[u8],
26    nonce: u64,
27    vault: Option<Address>,
28    expires_after: Option<u64>,
29) -> [u8; 32] {
30    let sponge = Keccak256::new().chain(msgpack).chain(&nonce.to_be_bytes());
31    let sponge = match vault {
32        Some(vault) => sponge.chain(&[0x01]).chain(vault.bytes()),
33        None => sponge.chain(&[0x00]),
34    };
35    match expires_after {
36        Some(expires_after) => sponge.chain(&[0x00]).chain(&expires_after.to_be_bytes()),
37        None => sponge,
38    }
39    .finalize()
40}
41
42/// The struct an L1 signature is really over.
43#[must_use]
44pub fn phantom_agent(connection_id: [u8; 32], network: Network) -> TypedStruct<'static> {
45    TypedStruct::new("Agent")
46        .field("source", Field::String(network.agent_source()))
47        .field("connectionId", Field::Bytes32(connection_id))
48}
49
50/// What the key signs for an L1 action on `network`.
51#[must_use]
52pub fn l1_digest(msgpack: &[u8], nonce: u64, vault: Option<Address>, network: Network) -> [u8; 32] {
53    phantom_agent(connection_id(msgpack, nonce, vault), network).digest(&Domain::EXCHANGE)
54}
55
56/// An action signed through the phantom agent. An implementor writes
57/// [`L1Action::action`], keys in the order the venue's clients write them, and
58/// gets everything else from it.
59pub trait L1Action {
60    /// The action's keys and values in wire order, which is the hashed order.
61    fn action(&self) -> Map;
62
63    #[must_use]
64    fn msgpack(&self) -> Vec<u8> {
65        self.action().to_msgpack()
66    }
67
68    /// The action as posted.
69    #[must_use]
70    fn wire(&self) -> String {
71        self.action().to_json().compact()
72    }
73
74    #[must_use]
75    fn connection_id(&self, nonce: u64, vault: Option<Address>) -> [u8; 32] {
76        connection_id(&self.msgpack(), nonce, vault)
77    }
78
79    #[must_use]
80    fn typed(&self, nonce: u64, vault: Option<Address>, network: Network) -> TypedStruct<'static> {
81        phantom_agent(self.connection_id(nonce, vault), network)
82    }
83
84    #[must_use]
85    fn digest(&self, nonce: u64, vault: Option<Address>, network: Network) -> [u8; 32] {
86        self.typed(nonce, vault, network).digest(&Domain::EXCHANGE)
87    }
88
89    /// For a master wallet signing in the browser. It needs chain 1337.
90    #[must_use]
91    fn wallet_json(&self, nonce: u64, vault: Option<Address>, network: Network) -> String {
92        self.typed(nonce, vault, network).wallet_json(&Domain::EXCHANGE)
93    }
94
95    #[must_use]
96    fn body(&self, nonce: u64, signature: &Signature, vault: Option<Address>) -> String {
97        body::l1(&self.action(), nonce, signature, vault)
98    }
99}
100
101/// A map is an action already.
102impl L1Action for Map {
103    fn action(&self) -> Map {
104        self.clone()
105    }
106}
107
108#[cfg(test)]
109#[path = "tests/l1.rs"]
110mod tests;