Skip to main content

pyth_lazer_protocol/
jrpc.rs

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