Skip to main content

df_hyperliquid_core/utils/json/
msgpack.rs

1//! JSON as the msgpack an L1 signature is over: keys in the order written, integers as the venue's
2//! own clients pack them, and never a float.
3
4use crate::utils::json::Json;
5use crate::utils::sign::msgpack::{Map, Value};
6
7/// Why a JSON value has no msgpack form the venue would hash the same way.
8#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
9pub enum Unpackable {
10    #[error("{0} is not a whole number; the venue hashes decimals as strings, so write it in quotes")]
11    Fraction(String),
12    #[error("{0} does not fit in 64 bits")]
13    TooWide(String),
14    #[error("an action is an object, and this is {0}")]
15    NotAnObject(&'static str),
16}
17
18impl Json {
19    /// The msgpack value this hashes as, keys in the order they were written.
20    pub fn to_msgpack(&self) -> Result<Value, Unpackable> {
21        Ok(match self {
22            Json::Null => Value::Nil,
23            Json::Bool(value) => Value::Bool(*value),
24            Json::Number(digits) => whole(digits)?,
25            Json::String(text) => Value::from(text.clone()),
26            Json::Array(items) => Value::Array(items.iter().map(Json::to_msgpack).collect::<Result<_, _>>()?),
27            Json::Object(entries) => Value::Map(map_of(entries)?),
28        })
29    }
30
31    /// The action map an L1 signature is over. Only an object is one.
32    pub fn to_action(&self) -> Result<Map, Unpackable> {
33        match self {
34            Json::Object(entries) => map_of(entries),
35            other => Err(Unpackable::NotAnObject(other.kind())),
36        }
37    }
38}
39
40fn map_of(entries: &[(String, Json)]) -> Result<Map, Unpackable> {
41    let mut map = Map::new();
42    for (key, value) in entries {
43        map = map.put(key.clone(), value.to_msgpack()?);
44    }
45    Ok(map)
46}
47
48/// An integer as msgpack encodes it: unsigned when it can be, as the venue's
49/// own clients do. A fraction or an exponent would be a float, and a float is
50/// never what an action means.
51fn whole(digits: &str) -> Result<Value, Unpackable> {
52    if digits.contains(['.', 'e', 'E']) {
53        return Err(Unpackable::Fraction(digits.to_owned()));
54    }
55    match digits.strip_prefix('-') {
56        Some(_) => digits.parse::<i64>().map(Value::Int).map_err(|_| Unpackable::TooWide(digits.to_owned())),
57        None => digits.parse::<u64>().map(Value::Uint).map_err(|_| Unpackable::TooWide(digits.to_owned())),
58    }
59}
60
61#[cfg(test)]
62#[path = "tests/msgpack.rs"]
63mod tests;