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