pyth_lazer_protocol/
jrpc.rs

1use crate::rate::Rate;
2use crate::symbol_state::SymbolState;
3use crate::time::TimestampUs;
4use crate::PriceFeedId;
5use crate::{api::Channel, price::Price};
6use serde::{Deserialize, Serialize};
7use std::time::Duration;
8
9#[derive(Serialize, Deserialize, Clone, Debug, Default, Eq, PartialEq)]
10#[serde(untagged)]
11pub enum JrpcId {
12    String(String),
13    Int(i64),
14    #[default]
15    Null,
16}
17
18#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
19pub struct PythLazerAgentJrpcV1 {
20    pub jsonrpc: JsonRpcVersion,
21    #[serde(flatten)]
22    pub params: JrpcCall,
23    #[serde(default)]
24    pub id: JrpcId,
25}
26
27#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
28#[serde(tag = "method", content = "params")]
29#[serde(rename_all = "snake_case")]
30pub enum JrpcCall {
31    PushUpdate(FeedUpdateParams),
32    PushUpdates(Vec<FeedUpdateParams>),
33    GetMetadata(GetMetadataParams),
34}
35
36#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
37pub struct FeedUpdateParams {
38    pub feed_id: PriceFeedId,
39    pub source_timestamp: TimestampUs,
40    pub update: UpdateParams,
41}
42
43#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
44#[serde(tag = "type")]
45pub enum UpdateParams {
46    #[serde(rename = "price")]
47    PriceUpdate {
48        price: Price,
49        best_bid_price: Option<Price>,
50        best_ask_price: Option<Price>,
51    },
52    #[serde(rename = "funding_rate")]
53    FundingRateUpdate {
54        price: Option<Price>,
55        rate: Rate,
56        #[serde(default = "default_funding_rate_interval", with = "humantime_serde")]
57        funding_rate_interval: Option<Duration>,
58    },
59}
60
61fn default_funding_rate_interval() -> Option<Duration> {
62    None
63}
64
65#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
66pub struct Filter {
67    pub name: Option<String>,
68    pub asset_type: Option<String>,
69}
70
71#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
72pub struct GetMetadataParams {
73    pub names: Option<Vec<String>>,
74    pub asset_types: Option<Vec<String>>,
75}
76
77#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
78pub enum JsonRpcVersion {
79    #[serde(rename = "2.0")]
80    V2,
81}
82
83#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
84#[serde(untagged)]
85pub enum JrpcResponse<T> {
86    Success(JrpcSuccessResponse<T>),
87    Error(JrpcErrorResponse),
88}
89
90#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
91pub struct JrpcSuccessResponse<T> {
92    pub jsonrpc: JsonRpcVersion,
93    pub result: T,
94    pub id: JrpcId,
95}
96
97#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
98pub struct JrpcErrorResponse {
99    pub jsonrpc: JsonRpcVersion,
100    pub error: JrpcErrorObject,
101    pub id: JrpcId,
102}
103
104#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
105pub struct JrpcErrorObject {
106    pub code: i64,
107    pub message: String,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub data: Option<serde_json::Value>,
110}
111
112#[derive(Debug, Eq, PartialEq)]
113pub enum JrpcError {
114    ParseError(String),
115    InternalError(String),
116    SendUpdateError(FeedUpdateParams),
117}
118
119// note: error codes can be found in the rfc https://www.jsonrpc.org/specification#error_object
120impl From<JrpcError> for JrpcErrorObject {
121    fn from(error: JrpcError) -> Self {
122        match error {
123            JrpcError::ParseError(error_message) => JrpcErrorObject {
124                code: -32700,
125                message: "Parse error".to_string(),
126                data: Some(error_message.into()),
127            },
128            JrpcError::InternalError(error_message) => JrpcErrorObject {
129                code: -32603,
130                message: "Internal error".to_string(),
131                data: Some(error_message.into()),
132            },
133            JrpcError::SendUpdateError(feed_update_params) => JrpcErrorObject {
134                code: -32000,
135                message: "Internal error".to_string(),
136                data: Some(serde_json::to_value(feed_update_params).unwrap()),
137            },
138        }
139    }
140}
141
142#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
143pub struct SymbolMetadata {
144    pub pyth_lazer_id: PriceFeedId,
145    pub name: String,
146    pub symbol: String,
147    pub description: String,
148    pub asset_type: String,
149    pub exponent: i16,
150    pub cmc_id: Option<u32>,
151    #[serde(default, with = "humantime_serde", alias = "interval")]
152    pub funding_rate_interval: Option<Duration>,
153    pub min_publishers: u16,
154    pub min_channel: Channel,
155    pub state: SymbolState,
156    pub hermes_id: Option<String>,
157    pub quote_currency: Option<String>,
158    pub nasdaq_symbol: Option<String>,
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::jrpc::JrpcCall::{GetMetadata, PushUpdate};
165
166    #[test]
167    fn test_push_update_price() {
168        let json = r#"
169        {
170          "jsonrpc": "2.0",
171          "method": "push_update",
172          "params": {
173            "feed_id": 1,
174            "source_timestamp": 124214124124,
175
176            "update": {
177              "type": "price",
178              "price": 1234567890,
179              "best_bid_price": 1234567891,
180              "best_ask_price": 1234567892
181            }
182          },
183          "id": 1
184        }
185        "#;
186
187        let expected = PythLazerAgentJrpcV1 {
188            jsonrpc: JsonRpcVersion::V2,
189            params: PushUpdate(FeedUpdateParams {
190                feed_id: PriceFeedId(1),
191                source_timestamp: TimestampUs::from_micros(124214124124),
192                update: UpdateParams::PriceUpdate {
193                    price: Price::from_integer(1234567890, 0).unwrap(),
194                    best_bid_price: Some(Price::from_integer(1234567891, 0).unwrap()),
195                    best_ask_price: Some(Price::from_integer(1234567892, 0).unwrap()),
196                },
197            }),
198            id: JrpcId::Int(1),
199        };
200
201        assert_eq!(
202            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
203            expected
204        );
205    }
206
207    #[test]
208    fn test_push_update_price_string_id() {
209        let json = r#"
210        {
211          "jsonrpc": "2.0",
212          "method": "push_update",
213          "params": {
214            "feed_id": 1,
215            "source_timestamp": 124214124124,
216
217            "update": {
218              "type": "price",
219              "price": 1234567890,
220              "best_bid_price": 1234567891,
221              "best_ask_price": 1234567892
222            }
223          },
224          "id": "b6bb54a0-ea8d-439d-97a7-3b06befa0e76"
225        }
226        "#;
227
228        let expected = PythLazerAgentJrpcV1 {
229            jsonrpc: JsonRpcVersion::V2,
230            params: PushUpdate(FeedUpdateParams {
231                feed_id: PriceFeedId(1),
232                source_timestamp: TimestampUs::from_micros(124214124124),
233                update: UpdateParams::PriceUpdate {
234                    price: Price::from_integer(1234567890, 0).unwrap(),
235                    best_bid_price: Some(Price::from_integer(1234567891, 0).unwrap()),
236                    best_ask_price: Some(Price::from_integer(1234567892, 0).unwrap()),
237                },
238            }),
239            id: JrpcId::String("b6bb54a0-ea8d-439d-97a7-3b06befa0e76".to_string()),
240        };
241
242        assert_eq!(
243            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
244            expected
245        );
246    }
247
248    #[test]
249    fn test_push_update_price_null_id() {
250        let json = r#"
251        {
252          "jsonrpc": "2.0",
253          "method": "push_update",
254          "params": {
255            "feed_id": 1,
256            "source_timestamp": 124214124124,
257
258            "update": {
259              "type": "price",
260              "price": 1234567890,
261              "best_bid_price": 1234567891,
262              "best_ask_price": 1234567892
263            }
264          },
265          "id": null
266        }
267        "#;
268
269        let expected = PythLazerAgentJrpcV1 {
270            jsonrpc: JsonRpcVersion::V2,
271            params: PushUpdate(FeedUpdateParams {
272                feed_id: PriceFeedId(1),
273                source_timestamp: TimestampUs::from_micros(124214124124),
274                update: UpdateParams::PriceUpdate {
275                    price: Price::from_integer(1234567890, 0).unwrap(),
276                    best_bid_price: Some(Price::from_integer(1234567891, 0).unwrap()),
277                    best_ask_price: Some(Price::from_integer(1234567892, 0).unwrap()),
278                },
279            }),
280            id: JrpcId::Null,
281        };
282
283        assert_eq!(
284            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
285            expected
286        );
287    }
288
289    #[test]
290    fn test_push_update_price_without_id() {
291        let json = r#"
292        {
293          "jsonrpc": "2.0",
294          "method": "push_update",
295          "params": {
296            "feed_id": 1,
297            "source_timestamp": 745214124124,
298
299            "update": {
300              "type": "price",
301              "price": 5432,
302              "best_bid_price": 5432,
303              "best_ask_price": 5432
304            }
305          }
306        }
307        "#;
308
309        let expected = PythLazerAgentJrpcV1 {
310            jsonrpc: JsonRpcVersion::V2,
311            params: PushUpdate(FeedUpdateParams {
312                feed_id: PriceFeedId(1),
313                source_timestamp: TimestampUs::from_micros(745214124124),
314                update: UpdateParams::PriceUpdate {
315                    price: Price::from_integer(5432, 0).unwrap(),
316                    best_bid_price: Some(Price::from_integer(5432, 0).unwrap()),
317                    best_ask_price: Some(Price::from_integer(5432, 0).unwrap()),
318                },
319            }),
320            id: JrpcId::Null,
321        };
322
323        assert_eq!(
324            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
325            expected
326        );
327    }
328
329    #[test]
330    fn test_push_update_price_without_bid_ask() {
331        let json = r#"
332        {
333          "jsonrpc": "2.0",
334          "method": "push_update",
335          "params": {
336            "feed_id": 1,
337            "source_timestamp": 124214124124,
338
339            "update": {
340              "type": "price",
341              "price": 1234567890
342            }
343          },
344          "id": 1
345        }
346        "#;
347
348        let expected = PythLazerAgentJrpcV1 {
349            jsonrpc: JsonRpcVersion::V2,
350            params: PushUpdate(FeedUpdateParams {
351                feed_id: PriceFeedId(1),
352                source_timestamp: TimestampUs::from_micros(124214124124),
353                update: UpdateParams::PriceUpdate {
354                    price: Price::from_integer(1234567890, 0).unwrap(),
355                    best_bid_price: None,
356                    best_ask_price: None,
357                },
358            }),
359            id: JrpcId::Int(1),
360        };
361
362        assert_eq!(
363            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
364            expected
365        );
366    }
367
368    #[test]
369    fn test_push_update_funding_rate() {
370        let json = r#"
371        {
372          "jsonrpc": "2.0",
373          "method": "push_update",
374          "params": {
375            "feed_id": 1,
376            "source_timestamp": 124214124124,
377
378            "update": {
379              "type": "funding_rate",
380              "price": 1234567890,
381              "rate": 1234567891,
382              "funding_rate_interval": "8h"
383            }
384          },
385          "id": 1
386        }
387        "#;
388
389        let expected = PythLazerAgentJrpcV1 {
390            jsonrpc: JsonRpcVersion::V2,
391            params: PushUpdate(FeedUpdateParams {
392                feed_id: PriceFeedId(1),
393                source_timestamp: TimestampUs::from_micros(124214124124),
394                update: UpdateParams::FundingRateUpdate {
395                    price: Some(Price::from_integer(1234567890, 0).unwrap()),
396                    rate: Rate::from_integer(1234567891, 0).unwrap(),
397                    funding_rate_interval: Duration::from_secs(28800).into(),
398                },
399            }),
400            id: JrpcId::Int(1),
401        };
402
403        assert_eq!(
404            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
405            expected
406        );
407    }
408    #[test]
409    fn test_push_update_funding_rate_without_price() {
410        let json = r#"
411        {
412          "jsonrpc": "2.0",
413          "method": "push_update",
414          "params": {
415            "feed_id": 1,
416            "source_timestamp": 124214124124,
417
418            "update": {
419              "type": "funding_rate",
420              "rate": 1234567891
421            }
422          },
423          "id": 1
424        }
425        "#;
426
427        let expected = PythLazerAgentJrpcV1 {
428            jsonrpc: JsonRpcVersion::V2,
429            params: PushUpdate(FeedUpdateParams {
430                feed_id: PriceFeedId(1),
431                source_timestamp: TimestampUs::from_micros(124214124124),
432                update: UpdateParams::FundingRateUpdate {
433                    price: None,
434                    rate: Rate::from_integer(1234567891, 0).unwrap(),
435                    funding_rate_interval: None,
436                },
437            }),
438            id: JrpcId::Int(1),
439        };
440
441        assert_eq!(
442            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
443            expected
444        );
445    }
446
447    #[test]
448    fn test_send_get_metadata() {
449        let json = r#"
450        {
451          "jsonrpc": "2.0",
452          "method": "get_metadata",
453          "params": {
454            "names": ["BTC/USD"],
455            "asset_types": ["crypto"]
456          },
457          "id": 1
458        }
459        "#;
460
461        let expected = PythLazerAgentJrpcV1 {
462            jsonrpc: JsonRpcVersion::V2,
463            params: GetMetadata(GetMetadataParams {
464                names: Some(vec!["BTC/USD".to_string()]),
465                asset_types: Some(vec!["crypto".to_string()]),
466            }),
467            id: JrpcId::Int(1),
468        };
469
470        assert_eq!(
471            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
472            expected
473        );
474    }
475
476    #[test]
477    fn test_get_metadata_without_filters() {
478        let json = r#"
479        {
480          "jsonrpc": "2.0",
481          "method": "get_metadata",
482          "params": {},
483          "id": 1
484        }
485        "#;
486
487        let expected = PythLazerAgentJrpcV1 {
488            jsonrpc: JsonRpcVersion::V2,
489            params: GetMetadata(GetMetadataParams {
490                names: None,
491                asset_types: None,
492            }),
493            id: JrpcId::Int(1),
494        };
495
496        assert_eq!(
497            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
498            expected
499        );
500    }
501
502    #[test]
503    fn test_response_format_error() {
504        let response = serde_json::from_str::<JrpcErrorResponse>(
505            r#"
506            {
507              "jsonrpc": "2.0",
508              "id": 2,
509              "error": {
510                "message": "Internal error",
511                "code": -32603
512              }
513            }
514            "#,
515        )
516        .unwrap();
517
518        assert_eq!(
519            response,
520            JrpcErrorResponse {
521                jsonrpc: JsonRpcVersion::V2,
522                error: JrpcErrorObject {
523                    code: -32603,
524                    message: "Internal error".to_string(),
525                    data: None,
526                },
527                id: JrpcId::Int(2),
528            }
529        );
530    }
531
532    #[test]
533    fn test_response_format_error_string_id() {
534        let response = serde_json::from_str::<JrpcErrorResponse>(
535            r#"
536            {
537              "jsonrpc": "2.0",
538              "id": "62b627dc-5599-43dd-b2c2-9c4d30f4fdb4",
539              "error": {
540                "message": "Internal error",
541                "code": -32603
542              }
543            }
544            "#,
545        )
546        .unwrap();
547
548        assert_eq!(
549            response,
550            JrpcErrorResponse {
551                jsonrpc: JsonRpcVersion::V2,
552                error: JrpcErrorObject {
553                    code: -32603,
554                    message: "Internal error".to_string(),
555                    data: None,
556                },
557                id: JrpcId::String("62b627dc-5599-43dd-b2c2-9c4d30f4fdb4".to_string())
558            }
559        );
560    }
561
562    #[test]
563    pub fn test_response_format_success() {
564        let response = serde_json::from_str::<JrpcSuccessResponse<String>>(
565            r#"
566            {
567              "jsonrpc": "2.0",
568              "id": 2,
569              "result": "success"
570            }
571            "#,
572        )
573        .unwrap();
574
575        assert_eq!(
576            response,
577            JrpcSuccessResponse::<String> {
578                jsonrpc: JsonRpcVersion::V2,
579                result: "success".to_string(),
580                id: JrpcId::Int(2),
581            }
582        );
583    }
584
585    #[test]
586    pub fn test_response_format_success_string_id() {
587        let response = serde_json::from_str::<JrpcSuccessResponse<String>>(
588            r#"
589            {
590              "jsonrpc": "2.0",
591              "id": "62b627dc-5599-43dd-b2c2-9c4d30f4fdb4",
592              "result": "success"
593            }
594            "#,
595        )
596        .unwrap();
597
598        assert_eq!(
599            response,
600            JrpcSuccessResponse::<String> {
601                jsonrpc: JsonRpcVersion::V2,
602                result: "success".to_string(),
603                id: JrpcId::String("62b627dc-5599-43dd-b2c2-9c4d30f4fdb4".to_string()),
604            }
605        );
606    }
607
608    #[test]
609    pub fn test_parse_response() {
610        let success_response = serde_json::from_str::<JrpcResponse<String>>(
611            r#"
612            {
613              "jsonrpc": "2.0",
614              "id": 2,
615              "result": "success"
616            }"#,
617        )
618        .unwrap();
619
620        assert_eq!(
621            success_response,
622            JrpcResponse::Success(JrpcSuccessResponse::<String> {
623                jsonrpc: JsonRpcVersion::V2,
624                result: "success".to_string(),
625                id: JrpcId::Int(2),
626            })
627        );
628
629        let error_response = serde_json::from_str::<JrpcResponse<String>>(
630            r#"
631            {
632              "jsonrpc": "2.0",
633              "id": 3,
634              "error": {
635                "code": -32603,
636                "message": "Internal error"
637              }
638            }"#,
639        )
640        .unwrap();
641
642        assert_eq!(
643            error_response,
644            JrpcResponse::Error(JrpcErrorResponse {
645                jsonrpc: JsonRpcVersion::V2,
646                error: JrpcErrorObject {
647                    code: -32603,
648                    message: "Internal error".to_string(),
649                    data: None,
650                },
651                id: JrpcId::Int(3),
652            })
653        );
654    }
655
656    #[test]
657    pub fn test_parse_response_string_id() {
658        let success_response = serde_json::from_str::<JrpcResponse<String>>(
659            r#"
660            {
661              "jsonrpc": "2.0",
662              "id": "id-2",
663              "result": "success"
664            }"#,
665        )
666        .unwrap();
667
668        assert_eq!(
669            success_response,
670            JrpcResponse::Success(JrpcSuccessResponse::<String> {
671                jsonrpc: JsonRpcVersion::V2,
672                result: "success".to_string(),
673                id: JrpcId::String("id-2".to_string()),
674            })
675        );
676
677        let error_response = serde_json::from_str::<JrpcResponse<String>>(
678            r#"
679            {
680              "jsonrpc": "2.0",
681              "id": "id-3",
682              "error": {
683                "code": -32603,
684                "message": "Internal error"
685              }
686            }"#,
687        )
688        .unwrap();
689
690        assert_eq!(
691            error_response,
692            JrpcResponse::Error(JrpcErrorResponse {
693                jsonrpc: JsonRpcVersion::V2,
694                error: JrpcErrorObject {
695                    code: -32603,
696                    message: "Internal error".to_string(),
697                    data: None,
698                },
699                id: JrpcId::String("id-3".to_string()),
700            })
701        );
702    }
703}