pyth_lazer_protocol/
jrpc.rs

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