pyth_lazer_protocol/
jrpc.rs

1use crate::router::{Channel, Price, PriceFeedId, Rate, TimestampUs};
2use crate::symbol_state::SymbolState;
3use serde::{Deserialize, Serialize};
4use std::time::Duration;
5
6#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
7pub struct PythLazerAgentJrpcV1 {
8    pub jsonrpc: JsonRpcVersion,
9    #[serde(flatten)]
10    pub params: JrpcCall,
11    pub id: i64,
12}
13
14#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
15#[serde(tag = "method", content = "params")]
16#[serde(rename_all = "snake_case")]
17pub enum JrpcCall {
18    PushUpdate(FeedUpdateParams),
19    GetMetadata(GetMetadataParams),
20}
21
22#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
23pub struct FeedUpdateParams {
24    pub feed_id: PriceFeedId,
25    pub source_timestamp: TimestampUs,
26    pub update: UpdateParams,
27}
28
29#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
30#[serde(tag = "type")]
31pub enum UpdateParams {
32    #[serde(rename = "price")]
33    PriceUpdate {
34        price: Price,
35        best_bid_price: Option<Price>,
36        best_ask_price: Option<Price>,
37    },
38    #[serde(rename = "funding_rate")]
39    FundingRateUpdate { price: Option<Price>, rate: Rate },
40}
41
42#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
43pub struct Filter {
44    pub name: Option<String>,
45    pub asset_type: Option<String>,
46}
47
48#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
49pub struct GetMetadataParams {
50    pub names: Option<Vec<String>>,
51    pub asset_types: Option<Vec<String>>,
52}
53
54#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
55pub enum JsonRpcVersion {
56    #[serde(rename = "2.0")]
57    V2,
58}
59
60#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
61pub enum JrpcResponse<T> {
62    Success(JrpcSuccessResponse<T>),
63    Error(JrpcErrorResponse),
64}
65
66#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
67pub struct JrpcSuccessResponse<T> {
68    pub jsonrpc: JsonRpcVersion,
69    pub result: T,
70    pub id: i64,
71}
72
73#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
74pub struct JrpcErrorResponse {
75    pub jsonrpc: JsonRpcVersion,
76    pub error: JrpcErrorObject,
77    pub id: Option<i64>,
78}
79
80#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
81pub struct JrpcErrorObject {
82    pub code: i64,
83    pub message: String,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub data: Option<serde_json::Value>,
86}
87
88#[derive(Debug, Eq, PartialEq)]
89pub enum JrpcError {
90    ParseError(String),
91    InternalError,
92}
93
94// note: error codes can be found in the rfc https://www.jsonrpc.org/specification#error_object
95impl From<JrpcError> for JrpcErrorObject {
96    fn from(error: JrpcError) -> Self {
97        match error {
98            JrpcError::ParseError(error_message) => JrpcErrorObject {
99                code: -32700,
100                message: "Parse error".to_string(),
101                data: Some(error_message.into()),
102            },
103            JrpcError::InternalError => JrpcErrorObject {
104                code: -32603,
105                message: "Internal error".to_string(),
106                data: None,
107            },
108        }
109    }
110}
111
112#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
113pub struct SymbolMetadata {
114    pub pyth_lazer_id: PriceFeedId,
115    pub name: String,
116    pub symbol: String,
117    pub description: String,
118    pub asset_type: String,
119    pub exponent: i16,
120    pub cmc_id: Option<u32>,
121    #[serde(default, with = "humantime_serde", alias = "interval")]
122    pub funding_rate_interval: Option<Duration>,
123    pub min_publishers: u16,
124    pub min_channel: Channel,
125    pub state: SymbolState,
126    pub hermes_id: Option<String>,
127    pub quote_currency: Option<String>,
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::jrpc::JrpcCall::{GetMetadata, PushUpdate};
134
135    #[test]
136    fn test_push_update_price() {
137        let json = r#"
138        {
139          "jsonrpc": "2.0",
140          "method": "push_update",
141          "params": {
142            "feed_id": 1,
143            "source_timestamp": 124214124124,
144
145            "update": {
146              "type": "price",
147              "price": 1234567890,
148              "best_bid_price": 1234567891,
149              "best_ask_price": 1234567892
150            }
151          },
152          "id": 1
153        }
154        "#;
155
156        let expected = PythLazerAgentJrpcV1 {
157            jsonrpc: JsonRpcVersion::V2,
158            params: PushUpdate(FeedUpdateParams {
159                feed_id: PriceFeedId(1),
160                source_timestamp: TimestampUs(124214124124),
161                update: UpdateParams::PriceUpdate {
162                    price: Price::from_integer(1234567890, 0).unwrap(),
163                    best_bid_price: Some(Price::from_integer(1234567891, 0).unwrap()),
164                    best_ask_price: Some(Price::from_integer(1234567892, 0).unwrap()),
165                },
166            }),
167            id: 1,
168        };
169
170        assert_eq!(
171            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
172            expected
173        );
174    }
175
176    #[test]
177    fn test_push_update_price_without_bid_ask() {
178        let json = r#"
179        {
180          "jsonrpc": "2.0",
181          "method": "push_update",
182          "params": {
183            "feed_id": 1,
184            "source_timestamp": 124214124124,
185
186            "update": {
187              "type": "price",
188              "price": 1234567890
189            }
190          },
191          "id": 1
192        }
193        "#;
194
195        let expected = PythLazerAgentJrpcV1 {
196            jsonrpc: JsonRpcVersion::V2,
197            params: PushUpdate(FeedUpdateParams {
198                feed_id: PriceFeedId(1),
199                source_timestamp: TimestampUs(124214124124),
200                update: UpdateParams::PriceUpdate {
201                    price: Price::from_integer(1234567890, 0).unwrap(),
202                    best_bid_price: None,
203                    best_ask_price: None,
204                },
205            }),
206            id: 1,
207        };
208
209        assert_eq!(
210            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
211            expected
212        );
213    }
214
215    #[test]
216    fn test_push_update_funding_rate() {
217        let json = r#"
218        {
219          "jsonrpc": "2.0",
220          "method": "push_update",
221          "params": {
222            "feed_id": 1,
223            "source_timestamp": 124214124124,
224
225            "update": {
226              "type": "funding_rate",
227              "price": 1234567890,
228              "rate": 1234567891
229            }
230          },
231          "id": 1
232        }
233        "#;
234
235        let expected = PythLazerAgentJrpcV1 {
236            jsonrpc: JsonRpcVersion::V2,
237            params: PushUpdate(FeedUpdateParams {
238                feed_id: PriceFeedId(1),
239                source_timestamp: TimestampUs(124214124124),
240                update: UpdateParams::FundingRateUpdate {
241                    price: Some(Price::from_integer(1234567890, 0).unwrap()),
242                    rate: Rate::from_integer(1234567891, 0).unwrap(),
243                },
244            }),
245            id: 1,
246        };
247
248        assert_eq!(
249            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
250            expected
251        );
252    }
253    #[test]
254    fn test_push_update_funding_rate_without_price() {
255        let json = r#"
256        {
257          "jsonrpc": "2.0",
258          "method": "push_update",
259          "params": {
260            "feed_id": 1,
261            "source_timestamp": 124214124124,
262
263            "update": {
264              "type": "funding_rate",
265              "rate": 1234567891
266            }
267          },
268          "id": 1
269        }
270        "#;
271
272        let expected = PythLazerAgentJrpcV1 {
273            jsonrpc: JsonRpcVersion::V2,
274            params: PushUpdate(FeedUpdateParams {
275                feed_id: PriceFeedId(1),
276                source_timestamp: TimestampUs(124214124124),
277                update: UpdateParams::FundingRateUpdate {
278                    price: None,
279                    rate: Rate::from_integer(1234567891, 0).unwrap(),
280                },
281            }),
282            id: 1,
283        };
284
285        assert_eq!(
286            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
287            expected
288        );
289    }
290
291    #[test]
292    fn test_send_get_metadata() {
293        let json = r#"
294        {
295          "jsonrpc": "2.0",
296          "method": "get_metadata",
297          "params": {
298            "names": ["BTC/USD"],
299            "asset_types": ["crypto"]
300          },
301          "id": 1
302        }
303        "#;
304
305        let expected = PythLazerAgentJrpcV1 {
306            jsonrpc: JsonRpcVersion::V2,
307            params: GetMetadata(GetMetadataParams {
308                names: Some(vec!["BTC/USD".to_string()]),
309                asset_types: Some(vec!["crypto".to_string()]),
310            }),
311            id: 1,
312        };
313
314        assert_eq!(
315            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
316            expected
317        );
318    }
319
320    #[test]
321    fn test_get_metadata_without_filters() {
322        let json = r#"
323        {
324          "jsonrpc": "2.0",
325          "method": "get_metadata",
326          "params": {},
327          "id": 1
328        }
329        "#;
330
331        let expected = PythLazerAgentJrpcV1 {
332            jsonrpc: JsonRpcVersion::V2,
333            params: GetMetadata(GetMetadataParams {
334                names: None,
335                asset_types: None,
336            }),
337            id: 1,
338        };
339
340        assert_eq!(
341            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
342            expected
343        );
344    }
345
346    #[test]
347    fn test_response_format_error() {
348        let response = serde_json::from_str::<JrpcErrorResponse>(
349            r#"
350            {
351              "jsonrpc": "2.0",
352              "id": 2,
353              "error": {
354                "message": "Internal error",
355                "code": -32603
356              }
357            }
358            "#,
359        )
360        .unwrap();
361
362        assert_eq!(
363            response,
364            JrpcErrorResponse {
365                jsonrpc: JsonRpcVersion::V2,
366                error: JrpcErrorObject {
367                    code: -32603,
368                    message: "Internal error".to_string(),
369                    data: None,
370                },
371                id: Some(2),
372            }
373        );
374    }
375
376    #[test]
377    pub fn test_response_format_success() {
378        let response = serde_json::from_str::<JrpcSuccessResponse<String>>(
379            r#"
380            {
381              "jsonrpc": "2.0",
382              "id": 2,
383              "result": "success"
384            }
385            "#,
386        )
387        .unwrap();
388
389        assert_eq!(
390            response,
391            JrpcSuccessResponse::<String> {
392                jsonrpc: JsonRpcVersion::V2,
393                result: "success".to_string(),
394                id: 2,
395            }
396        );
397    }
398}