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