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    let sponge = Keccak256::new().chain(msgpack).chain(&nonce.to_be_bytes());
18    match vault {
19        Some(vault) => sponge.chain(&[0x01]).chain(vault.bytes()),
20        None => sponge.chain(&[0x00]),
21    }
22    .finalize()
23}
24
25/// The struct an L1 signature is really over.
26#[must_use]
27pub fn phantom_agent(connection_id: [u8; 32], network: Network) -> TypedStruct<'static> {
28    TypedStruct::new("Agent")
29        .field("source", Field::String(network.agent_source()))
30        .field("connectionId", Field::Bytes32(connection_id))
31}
32
33/// What the key signs for an L1 action on `network`.
34#[must_use]
35pub fn l1_digest(msgpack: &[u8], nonce: u64, vault: Option<Address>, network: Network) -> [u8; 32] {
36    phantom_agent(connection_id(msgpack, nonce, vault), network).digest(&Domain::EXCHANGE)
37}
38
39/// An action signed through the phantom agent. An implementor writes
40/// [`L1Action::action`], keys in the order the venue's clients write them, and
41/// gets everything else from it.
42pub trait L1Action {
43    /// The action's keys and values in wire order, which is the hashed order.
44    fn action(&self) -> Map;
45
46    #[must_use]
47    fn msgpack(&self) -> Vec<u8> {
48        self.action().to_msgpack()
49    }
50
51    /// The action as posted.
52    #[must_use]
53    fn wire(&self) -> String {
54        self.action().to_json().compact()
55    }
56
57    #[must_use]
58    fn connection_id(&self, nonce: u64, vault: Option<Address>) -> [u8; 32] {
59        connection_id(&self.msgpack(), nonce, vault)
60    }
61
62    #[must_use]
63    fn typed(&self, nonce: u64, vault: Option<Address>, network: Network) -> TypedStruct<'static> {
64        phantom_agent(self.connection_id(nonce, vault), network)
65    }
66
67    #[must_use]
68    fn digest(&self, nonce: u64, vault: Option<Address>, network: Network) -> [u8; 32] {
69        self.typed(nonce, vault, network).digest(&Domain::EXCHANGE)
70    }
71
72    /// For a master wallet signing in the browser. It needs chain 1337.
73    #[must_use]
74    fn wallet_json(&self, nonce: u64, vault: Option<Address>, network: Network) -> String {
75        self.typed(nonce, vault, network).wallet_json(&Domain::EXCHANGE)
76    }
77
78    #[must_use]
79    fn body(&self, nonce: u64, signature: &Signature, vault: Option<Address>) -> String {
80        body::l1(&self.action(), nonce, signature, vault)
81    }
82}
83
84/// A map is an action already.
85impl L1Action for Map {
86    fn action(&self) -> Map {
87        self.clone()
88    }
89}
90
91#[cfg(test)]
92#[path = "tests/l1.rs"]
93mod tests;