Skip to main content

df_elysium_core/client/rpc/
query.rs

1//! [`Query`], one JSON-RPC method with its params: the request body it writes, the envelope its
2//! answer comes in, how its `result` is read, and [`run`], which sends one through any [`Rpc`].
3//! The same shape as df-hyperliquid-core's, open to the methods Nitro adds.
4
5use serde::{Deserialize, Deserializer, de::DeserializeOwned};
6use serde_json::value::RawValue;
7
8use df_hyperliquid_core::client::{Http, Request, TransportError};
9
10use crate::client::rpc::{BlockTag, NodeError, Rpc, RpcError};
11use crate::utils::{Json, hex};
12use crate::{B256, U256};
13
14/// One JSON-RPC method with its params, and how its `result` is read.
15#[derive(Debug, Clone)]
16pub struct Query<T> {
17    method: &'static str,
18    params: Vec<Json>,
19    read: fn(&str) -> Result<T, String>,
20}
21
22impl<T> Query<T> {
23    /// `method` with `params`, its `result` read by `read` from the JSON text the node wrote.
24    #[must_use]
25    pub const fn new(method: &'static str, params: Vec<Json>, read: fn(&str) -> Result<T, String>) -> Query<T> {
26        Query { method, params, read }
27    }
28
29    #[must_use]
30    pub const fn method(&self) -> &'static str {
31        self.method
32    }
33
34    /// `{"jsonrpc":"2.0","id":1,"method":...,"params":[...]}`.
35    #[must_use]
36    pub fn body(&self) -> String {
37        Json::Object(vec![
38            ("jsonrpc".to_owned(), Json::String("2.0".to_owned())),
39            ("id".to_owned(), Json::Number("1".to_owned())),
40            ("method".to_owned(), Json::String(self.method.to_owned())),
41            ("params".to_owned(), Json::Array(self.params.clone())),
42        ])
43        .compact()
44    }
45
46    /// The answer's text, read: its `result` as `T`, or its `error`.
47    pub fn answer(&self, text: &str) -> Result<T, RpcError> {
48        let unreadable = |detail: String| RpcError::Unreadable { method: self.method, detail };
49        let envelope: Envelope<'_> = serde_json::from_str(text)
50            .map_err(|error| unreadable(format!("{error} in {}", text.chars().take(300).collect::<String>())))?;
51        if let Some(error) = envelope.error {
52            let data = error
53                .data
54                .map(|data| serde_json::from_str::<String>(data.get()).unwrap_or_else(|_| data.get().to_owned()));
55            return Err(RpcError::Node {
56                method: self.method,
57                error: NodeError { code: error.code, message: error.message, data },
58            });
59        }
60        let result = envelope.result.ok_or_else(|| unreadable("neither a result nor an error".to_owned()))?;
61        (self.read)(result.get()).map_err(|detail| unreadable(format!("{detail} in {}", result.get())))
62    }
63}
64
65/// Posts the query once to `rpc`'s node and reads the answer, as [`Rpc::run`] does for
66/// df-hyperliquid-core's own queries. A node's refusal reads as [`RpcError::Node`] whatever the
67/// HTTP status (Elysium's refuses an oversized `eth_getLogs` with a 400). Nothing is retried.
68pub async fn run<H: Http, T>(rpc: &Rpc<H>, query: Query<T>) -> Result<T, RpcError> {
69    let response = rpc.http().send(Request::post_json(rpc.url().to_owned(), query.body())).await?;
70    if response.status == 429 {
71        return Err(TransportError::RateLimited { retry_after_ms: response.retry_after_ms() }.into());
72    }
73    let text = response.text();
74    match (response.is_success(), query.answer(&text)) {
75        (_, Err(refused @ RpcError::Node { .. })) => Err(refused),
76        (true, answer) => answer,
77        (false, _) => Err(TransportError::Status { status: response.status, body: text }.into()),
78    }
79}
80
81#[derive(Deserialize)]
82struct Envelope<'a> {
83    #[serde(default, borrow, deserialize_with = "raw")]
84    result: Option<&'a RawValue>,
85    #[serde(default, borrow)]
86    error: Option<ErrorBody<'a>>,
87}
88
89#[derive(Deserialize)]
90struct ErrorBody<'a> {
91    code: i64,
92    #[serde(default)]
93    message: String,
94    #[serde(default, borrow, deserialize_with = "raw")]
95    data: Option<&'a RawValue>,
96}
97
98/// A value as the text it was written in, `null` included.
99fn raw<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<&'de RawValue>, D::Error> {
100    <&RawValue>::deserialize(deserializer).map(Some)
101}
102
103/// The `result` read as `T` through serde.
104pub fn json<T: DeserializeOwned>(text: &str) -> Result<T, String> {
105    serde_json::from_str(text).map_err(|error| error.to_string())
106}
107
108/// A quantity that fits a `u64`.
109pub fn small(text: &str) -> Result<u64, String> {
110    let value: U256 = json(text)?;
111    value.to_u64().ok_or_else(|| format!("{value} does not fit 64 bits"))
112}
113
114/// `0x` and hex bytes.
115pub fn data(text: &str) -> Result<Vec<u8>, String> {
116    let said: String = json(text)?;
117    let digits = said.strip_prefix("0x").ok_or_else(|| format!("`{said}` is not 0x and hex"))?;
118    hex::decode(digits).map_err(|error| error.to_string())
119}
120
121pub(crate) fn tag(block: BlockTag) -> Json {
122    Json::String(block.to_string())
123}
124
125pub(crate) fn word(word: B256) -> Json {
126    Json::String(word.to_hex())
127}
128
129#[cfg(test)]
130#[path = "tests/query.rs"]
131mod tests;