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}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use crate::jrpc::JrpcCall::{GetMetadata, PushUpdate};
164
165    #[test]
166    fn test_push_update_price() {
167        let json = r#"
168        {
169          "jsonrpc": "2.0",
170          "method": "push_update",
171          "params": {
172            "feed_id": 1,
173            "source_timestamp": 124214124124,
174
175            "update": {
176              "type": "price",
177              "price": 1234567890,
178              "best_bid_price": 1234567891,
179              "best_ask_price": 1234567892
180            }
181          },
182          "id": 1
183        }
184        "#;
185
186        let expected = PythLazerAgentJrpcV1 {
187            jsonrpc: JsonRpcVersion::V2,
188            params: PushUpdate(FeedUpdateParams {
189                feed_id: PriceFeedId(1),
190                source_timestamp: TimestampUs::from_micros(124214124124),
191                update: UpdateParams::PriceUpdate {
192                    price: Price::from_integer(1234567890, 0).unwrap(),
193                    best_bid_price: Some(Price::from_integer(1234567891, 0).unwrap()),
194                    best_ask_price: Some(Price::from_integer(1234567892, 0).unwrap()),
195                },
196            }),
197            id: JrpcId::Int(1),
198        };
199
200        assert_eq!(
201            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
202            expected
203        );
204    }
205
206    #[test]
207    fn test_push_update_price_string_id() {
208        let json = r#"
209        {
210          "jsonrpc": "2.0",
211          "method": "push_update",
212          "params": {
213            "feed_id": 1,
214            "source_timestamp": 124214124124,
215
216            "update": {
217              "type": "price",
218              "price": 1234567890,
219              "best_bid_price": 1234567891,
220              "best_ask_price": 1234567892
221            }
222          },
223          "id": "b6bb54a0-ea8d-439d-97a7-3b06befa0e76"
224        }
225        "#;
226
227        let expected = PythLazerAgentJrpcV1 {
228            jsonrpc: JsonRpcVersion::V2,
229            params: PushUpdate(FeedUpdateParams {
230                feed_id: PriceFeedId(1),
231                source_timestamp: TimestampUs::from_micros(124214124124),
232                update: UpdateParams::PriceUpdate {
233                    price: Price::from_integer(1234567890, 0).unwrap(),
234                    best_bid_price: Some(Price::from_integer(1234567891, 0).unwrap()),
235                    best_ask_price: Some(Price::from_integer(1234567892, 0).unwrap()),
236                },
237            }),
238            id: JrpcId::String("b6bb54a0-ea8d-439d-97a7-3b06befa0e76".to_string()),
239        };
240
241        assert_eq!(
242            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
243            expected
244        );
245    }
246
247    #[test]
248    fn test_push_update_price_null_id() {
249        let json = r#"
250        {
251          "jsonrpc": "2.0",
252          "method": "push_update",
253          "params": {
254            "feed_id": 1,
255            "source_timestamp": 124214124124,
256
257            "update": {
258              "type": "price",
259              "price": 1234567890,
260              "best_bid_price": 1234567891,
261              "best_ask_price": 1234567892
262            }
263          },
264          "id": null
265        }
266        "#;
267
268        let expected = PythLazerAgentJrpcV1 {
269            jsonrpc: JsonRpcVersion::V2,
270            params: PushUpdate(FeedUpdateParams {
271                feed_id: PriceFeedId(1),
272                source_timestamp: TimestampUs::from_micros(124214124124),
273                update: UpdateParams::PriceUpdate {
274                    price: Price::from_integer(1234567890, 0).unwrap(),
275                    best_bid_price: Some(Price::from_integer(1234567891, 0).unwrap()),
276                    best_ask_price: Some(Price::from_integer(1234567892, 0).unwrap()),
277                },
278            }),
279            id: JrpcId::Null,
280        };
281
282        assert_eq!(
283            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
284            expected
285        );
286    }
287
288    #[test]
289    fn test_push_update_price_without_id() {
290        let json = r#"
291        {
292          "jsonrpc": "2.0",
293          "method": "push_update",
294          "params": {
295            "feed_id": 1,
296            "source_timestamp": 745214124124,
297
298            "update": {
299              "type": "price",
300              "price": 5432,
301              "best_bid_price": 5432,
302              "best_ask_price": 5432
303            }
304          }
305        }
306        "#;
307
308        let expected = PythLazerAgentJrpcV1 {
309            jsonrpc: JsonRpcVersion::V2,
310            params: PushUpdate(FeedUpdateParams {
311                feed_id: PriceFeedId(1),
312                source_timestamp: TimestampUs::from_micros(745214124124),
313                update: UpdateParams::PriceUpdate {
314                    price: Price::from_integer(5432, 0).unwrap(),
315                    best_bid_price: Some(Price::from_integer(5432, 0).unwrap()),
316                    best_ask_price: Some(Price::from_integer(5432, 0).unwrap()),
317                },
318            }),
319            id: JrpcId::Null,
320        };
321
322        assert_eq!(
323            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
324            expected
325        );
326    }
327
328    #[test]
329    fn test_push_update_price_without_bid_ask() {
330        let json = r#"
331        {
332          "jsonrpc": "2.0",
333          "method": "push_update",
334          "params": {
335            "feed_id": 1,
336            "source_timestamp": 124214124124,
337
338            "update": {
339              "type": "price",
340              "price": 1234567890
341            }
342          },
343          "id": 1
344        }
345        "#;
346
347        let expected = PythLazerAgentJrpcV1 {
348            jsonrpc: JsonRpcVersion::V2,
349            params: PushUpdate(FeedUpdateParams {
350                feed_id: PriceFeedId(1),
351                source_timestamp: TimestampUs::from_micros(124214124124),
352                update: UpdateParams::PriceUpdate {
353                    price: Price::from_integer(1234567890, 0).unwrap(),
354                    best_bid_price: None,
355                    best_ask_price: None,
356                },
357            }),
358            id: JrpcId::Int(1),
359        };
360
361        assert_eq!(
362            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
363            expected
364        );
365    }
366
367    #[test]
368    fn test_push_update_funding_rate() {
369        let json = r#"
370        {
371          "jsonrpc": "2.0",
372          "method": "push_update",
373          "params": {
374            "feed_id": 1,
375            "source_timestamp": 124214124124,
376
377            "update": {
378              "type": "funding_rate",
379              "price": 1234567890,
380              "rate": 1234567891,
381              "funding_rate_interval": "8h"
382            }
383          },
384          "id": 1
385        }
386        "#;
387
388        let expected = PythLazerAgentJrpcV1 {
389            jsonrpc: JsonRpcVersion::V2,
390            params: PushUpdate(FeedUpdateParams {
391                feed_id: PriceFeedId(1),
392                source_timestamp: TimestampUs::from_micros(124214124124),
393                update: UpdateParams::FundingRateUpdate {
394                    price: Some(Price::from_integer(1234567890, 0).unwrap()),
395                    rate: Rate::from_integer(1234567891, 0).unwrap(),
396                    funding_rate_interval: Duration::from_secs(28800).into(),
397                },
398            }),
399            id: JrpcId::Int(1),
400        };
401
402        assert_eq!(
403            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
404            expected
405        );
406    }
407    #[test]
408    fn test_push_update_funding_rate_without_price() {
409        let json = r#"
410        {
411          "jsonrpc": "2.0",
412          "method": "push_update",
413          "params": {
414            "feed_id": 1,
415            "source_timestamp": 124214124124,
416
417            "update": {
418              "type": "funding_rate",
419              "rate": 1234567891
420            }
421          },
422          "id": 1
423        }
424        "#;
425
426        let expected = PythLazerAgentJrpcV1 {
427            jsonrpc: JsonRpcVersion::V2,
428            params: PushUpdate(FeedUpdateParams {
429                feed_id: PriceFeedId(1),
430                source_timestamp: TimestampUs::from_micros(124214124124),
431                update: UpdateParams::FundingRateUpdate {
432                    price: None,
433                    rate: Rate::from_integer(1234567891, 0).unwrap(),
434                    funding_rate_interval: None,
435                },
436            }),
437            id: JrpcId::Int(1),
438        };
439
440        assert_eq!(
441            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
442            expected
443        );
444    }
445
446    #[test]
447    fn test_send_get_metadata() {
448        let json = r#"
449        {
450          "jsonrpc": "2.0",
451          "method": "get_metadata",
452          "params": {
453            "names": ["BTC/USD"],
454            "asset_types": ["crypto"]
455          },
456          "id": 1
457        }
458        "#;
459
460        let expected = PythLazerAgentJrpcV1 {
461            jsonrpc: JsonRpcVersion::V2,
462            params: GetMetadata(GetMetadataParams {
463                names: Some(vec!["BTC/USD".to_string()]),
464                asset_types: Some(vec!["crypto".to_string()]),
465            }),
466            id: JrpcId::Int(1),
467        };
468
469        assert_eq!(
470            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
471            expected
472        );
473    }
474
475    #[test]
476    fn test_get_metadata_without_filters() {
477        let json = r#"
478        {
479          "jsonrpc": "2.0",
480          "method": "get_metadata",
481          "params": {},
482          "id": 1
483        }
484        "#;
485
486        let expected = PythLazerAgentJrpcV1 {
487            jsonrpc: JsonRpcVersion::V2,
488            params: GetMetadata(GetMetadataParams {
489                names: None,
490                asset_types: None,
491            }),
492            id: JrpcId::Int(1),
493        };
494
495        assert_eq!(
496            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
497            expected
498        );
499    }
500
501    #[test]
502    fn test_response_format_error() {
503        let response = serde_json::from_str::<JrpcErrorResponse>(
504            r#"
505            {
506              "jsonrpc": "2.0",
507              "id": 2,
508              "error": {
509                "message": "Internal error",
510                "code": -32603
511              }
512            }
513            "#,
514        )
515        .unwrap();
516
517        assert_eq!(
518            response,
519            JrpcErrorResponse {
520                jsonrpc: JsonRpcVersion::V2,
521                error: JrpcErrorObject {
522                    code: -32603,
523                    message: "Internal error".to_string(),
524                    data: None,
525                },
526                id: JrpcId::Int(2),
527            }
528        );
529    }
530
531    #[test]
532    fn test_response_format_error_string_id() {
533        let response = serde_json::from_str::<JrpcErrorResponse>(
534            r#"
535            {
536              "jsonrpc": "2.0",
537              "id": "62b627dc-5599-43dd-b2c2-9c4d30f4fdb4",
538              "error": {
539                "message": "Internal error",
540                "code": -32603
541              }
542            }
543            "#,
544        )
545        .unwrap();
546
547        assert_eq!(
548            response,
549            JrpcErrorResponse {
550                jsonrpc: JsonRpcVersion::V2,
551                error: JrpcErrorObject {
552                    code: -32603,
553                    message: "Internal error".to_string(),
554                    data: None,
555                },
556                id: JrpcId::String("62b627dc-5599-43dd-b2c2-9c4d30f4fdb4".to_string())
557            }
558        );
559    }
560
561    #[test]
562    pub fn test_response_format_success() {
563        let response = serde_json::from_str::<JrpcSuccessResponse<String>>(
564            r#"
565            {
566              "jsonrpc": "2.0",
567              "id": 2,
568              "result": "success"
569            }
570            "#,
571        )
572        .unwrap();
573
574        assert_eq!(
575            response,
576            JrpcSuccessResponse::<String> {
577                jsonrpc: JsonRpcVersion::V2,
578                result: "success".to_string(),
579                id: JrpcId::Int(2),
580            }
581        );
582    }
583
584    #[test]
585    pub fn test_response_format_success_string_id() {
586        let response = serde_json::from_str::<JrpcSuccessResponse<String>>(
587            r#"
588            {
589              "jsonrpc": "2.0",
590              "id": "62b627dc-5599-43dd-b2c2-9c4d30f4fdb4",
591              "result": "success"
592            }
593            "#,
594        )
595        .unwrap();
596
597        assert_eq!(
598            response,
599            JrpcSuccessResponse::<String> {
600                jsonrpc: JsonRpcVersion::V2,
601                result: "success".to_string(),
602                id: JrpcId::String("62b627dc-5599-43dd-b2c2-9c4d30f4fdb4".to_string()),
603            }
604        );
605    }
606
607    #[test]
608    pub fn test_parse_response() {
609        let success_response = serde_json::from_str::<JrpcResponse<String>>(
610            r#"
611            {
612              "jsonrpc": "2.0",
613              "id": 2,
614              "result": "success"
615            }"#,
616        )
617        .unwrap();
618
619        assert_eq!(
620            success_response,
621            JrpcResponse::Success(JrpcSuccessResponse::<String> {
622                jsonrpc: JsonRpcVersion::V2,
623                result: "success".to_string(),
624                id: JrpcId::Int(2),
625            })
626        );
627
628        let error_response = serde_json::from_str::<JrpcResponse<String>>(
629            r#"
630            {
631              "jsonrpc": "2.0",
632              "id": 3,
633              "error": {
634                "code": -32603,
635                "message": "Internal error"
636              }
637            }"#,
638        )
639        .unwrap();
640
641        assert_eq!(
642            error_response,
643            JrpcResponse::Error(JrpcErrorResponse {
644                jsonrpc: JsonRpcVersion::V2,
645                error: JrpcErrorObject {
646                    code: -32603,
647                    message: "Internal error".to_string(),
648                    data: None,
649                },
650                id: JrpcId::Int(3),
651            })
652        );
653    }
654
655    #[test]
656    pub fn test_parse_response_string_id() {
657        let success_response = serde_json::from_str::<JrpcResponse<String>>(
658            r#"
659            {
660              "jsonrpc": "2.0",
661              "id": "id-2",
662              "result": "success"
663            }"#,
664        )
665        .unwrap();
666
667        assert_eq!(
668            success_response,
669            JrpcResponse::Success(JrpcSuccessResponse::<String> {
670                jsonrpc: JsonRpcVersion::V2,
671                result: "success".to_string(),
672                id: JrpcId::String("id-2".to_string()),
673            })
674        );
675
676        let error_response = serde_json::from_str::<JrpcResponse<String>>(
677            r#"
678            {
679              "jsonrpc": "2.0",
680              "id": "id-3",
681              "error": {
682                "code": -32603,
683                "message": "Internal error"
684              }
685            }"#,
686        )
687        .unwrap();
688
689        assert_eq!(
690            error_response,
691            JrpcResponse::Error(JrpcErrorResponse {
692                jsonrpc: JsonRpcVersion::V2,
693                error: JrpcErrorObject {
694                    code: -32603,
695                    message: "Internal error".to_string(),
696                    data: None,
697                },
698                id: JrpcId::String("id-3".to_string()),
699            })
700        );
701    }
702}