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