pyth_lazer_protocol/
jrpc.rs

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