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: 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)]
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)]
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)]
62pub enum JrpcResponse<T> {
63    Success(JrpcSuccessResponse<T>),
64    Error(JrpcErrorResponse),
65}
66
67#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
68pub struct JrpcSuccessResponse<T> {
69    pub jsonrpc: JsonRpcVersion,
70    pub result: T,
71    pub id: i64,
72}
73
74#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
75pub struct JrpcErrorResponse {
76    pub jsonrpc: JsonRpcVersion,
77    pub error: JrpcErrorObject,
78    pub id: Option<i64>,
79}
80
81#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
82pub struct JrpcErrorObject {
83    pub code: i64,
84    pub message: String,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub data: Option<serde_json::Value>,
87}
88
89#[derive(Debug, Eq, PartialEq)]
90pub enum JrpcError {
91    ParseError(String),
92    InternalError,
93}
94
95// note: error codes can be found in the rfc https://www.jsonrpc.org/specification#error_object
96impl From<JrpcError> for JrpcErrorObject {
97    fn from(error: JrpcError) -> Self {
98        match error {
99            JrpcError::ParseError(error_message) => JrpcErrorObject {
100                code: -32700,
101                message: "Parse error".to_string(),
102                data: Some(error_message.into()),
103            },
104            JrpcError::InternalError => JrpcErrorObject {
105                code: -32603,
106                message: "Internal error".to_string(),
107                data: None,
108            },
109        }
110    }
111}
112
113#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
114pub struct SymbolMetadata {
115    pub pyth_lazer_id: PriceFeedId,
116    pub name: String,
117    pub symbol: String,
118    pub description: String,
119    pub asset_type: String,
120    pub exponent: i16,
121    pub cmc_id: Option<u32>,
122    #[serde(default, with = "humantime_serde", alias = "interval")]
123    pub funding_rate_interval: Option<Duration>,
124    pub min_publishers: u16,
125    pub min_channel: Channel,
126    pub state: SymbolState,
127    pub hermes_id: Option<String>,
128    pub quote_currency: Option<String>,
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::jrpc::JrpcCall::{GetMetadata, PushUpdate};
135
136    #[test]
137    fn test_push_update_price() {
138        let json = r#"
139        {
140          "jsonrpc": "2.0",
141          "method": "push_update",
142          "params": {
143            "feed_id": 1,
144            "source_timestamp": 124214124124,
145
146            "update": {
147              "type": "price",
148              "price": 1234567890,
149              "best_bid_price": 1234567891,
150              "best_ask_price": 1234567892
151            }
152          },
153          "id": 1
154        }
155        "#;
156
157        let expected = PythLazerAgentJrpcV1 {
158            jsonrpc: JsonRpcVersion::V2,
159            params: PushUpdate(FeedUpdateParams {
160                feed_id: PriceFeedId(1),
161                source_timestamp: TimestampUs::from_micros(124214124124),
162                update: UpdateParams::PriceUpdate {
163                    price: Price::from_integer(1234567890, 0).unwrap(),
164                    best_bid_price: Some(Price::from_integer(1234567891, 0).unwrap()),
165                    best_ask_price: Some(Price::from_integer(1234567892, 0).unwrap()),
166                },
167            }),
168            id: 1,
169        };
170
171        assert_eq!(
172            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
173            expected
174        );
175    }
176
177    #[test]
178    fn test_push_update_price_without_bid_ask() {
179        let json = r#"
180        {
181          "jsonrpc": "2.0",
182          "method": "push_update",
183          "params": {
184            "feed_id": 1,
185            "source_timestamp": 124214124124,
186
187            "update": {
188              "type": "price",
189              "price": 1234567890
190            }
191          },
192          "id": 1
193        }
194        "#;
195
196        let expected = PythLazerAgentJrpcV1 {
197            jsonrpc: JsonRpcVersion::V2,
198            params: PushUpdate(FeedUpdateParams {
199                feed_id: PriceFeedId(1),
200                source_timestamp: TimestampUs::from_micros(124214124124),
201                update: UpdateParams::PriceUpdate {
202                    price: Price::from_integer(1234567890, 0).unwrap(),
203                    best_bid_price: None,
204                    best_ask_price: None,
205                },
206            }),
207            id: 1,
208        };
209
210        assert_eq!(
211            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
212            expected
213        );
214    }
215
216    #[test]
217    fn test_push_update_funding_rate() {
218        let json = r#"
219        {
220          "jsonrpc": "2.0",
221          "method": "push_update",
222          "params": {
223            "feed_id": 1,
224            "source_timestamp": 124214124124,
225
226            "update": {
227              "type": "funding_rate",
228              "price": 1234567890,
229              "rate": 1234567891
230            }
231          },
232          "id": 1
233        }
234        "#;
235
236        let expected = PythLazerAgentJrpcV1 {
237            jsonrpc: JsonRpcVersion::V2,
238            params: PushUpdate(FeedUpdateParams {
239                feed_id: PriceFeedId(1),
240                source_timestamp: TimestampUs::from_micros(124214124124),
241                update: UpdateParams::FundingRateUpdate {
242                    price: Some(Price::from_integer(1234567890, 0).unwrap()),
243                    rate: Rate::from_integer(1234567891, 0).unwrap(),
244                },
245            }),
246            id: 1,
247        };
248
249        assert_eq!(
250            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
251            expected
252        );
253    }
254    #[test]
255    fn test_push_update_funding_rate_without_price() {
256        let json = r#"
257        {
258          "jsonrpc": "2.0",
259          "method": "push_update",
260          "params": {
261            "feed_id": 1,
262            "source_timestamp": 124214124124,
263
264            "update": {
265              "type": "funding_rate",
266              "rate": 1234567891
267            }
268          },
269          "id": 1
270        }
271        "#;
272
273        let expected = PythLazerAgentJrpcV1 {
274            jsonrpc: JsonRpcVersion::V2,
275            params: PushUpdate(FeedUpdateParams {
276                feed_id: PriceFeedId(1),
277                source_timestamp: TimestampUs::from_micros(124214124124),
278                update: UpdateParams::FundingRateUpdate {
279                    price: None,
280                    rate: Rate::from_integer(1234567891, 0).unwrap(),
281                },
282            }),
283            id: 1,
284        };
285
286        assert_eq!(
287            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
288            expected
289        );
290    }
291
292    #[test]
293    fn test_send_get_metadata() {
294        let json = r#"
295        {
296          "jsonrpc": "2.0",
297          "method": "get_metadata",
298          "params": {
299            "names": ["BTC/USD"],
300            "asset_types": ["crypto"]
301          },
302          "id": 1
303        }
304        "#;
305
306        let expected = PythLazerAgentJrpcV1 {
307            jsonrpc: JsonRpcVersion::V2,
308            params: GetMetadata(GetMetadataParams {
309                names: Some(vec!["BTC/USD".to_string()]),
310                asset_types: Some(vec!["crypto".to_string()]),
311            }),
312            id: 1,
313        };
314
315        assert_eq!(
316            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
317            expected
318        );
319    }
320
321    #[test]
322    fn test_get_metadata_without_filters() {
323        let json = r#"
324        {
325          "jsonrpc": "2.0",
326          "method": "get_metadata",
327          "params": {},
328          "id": 1
329        }
330        "#;
331
332        let expected = PythLazerAgentJrpcV1 {
333            jsonrpc: JsonRpcVersion::V2,
334            params: GetMetadata(GetMetadataParams {
335                names: None,
336                asset_types: None,
337            }),
338            id: 1,
339        };
340
341        assert_eq!(
342            serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
343            expected
344        );
345    }
346
347    #[test]
348    fn test_response_format_error() {
349        let response = serde_json::from_str::<JrpcErrorResponse>(
350            r#"
351            {
352              "jsonrpc": "2.0",
353              "id": 2,
354              "error": {
355                "message": "Internal error",
356                "code": -32603
357              }
358            }
359            "#,
360        )
361        .unwrap();
362
363        assert_eq!(
364            response,
365            JrpcErrorResponse {
366                jsonrpc: JsonRpcVersion::V2,
367                error: JrpcErrorObject {
368                    code: -32603,
369                    message: "Internal error".to_string(),
370                    data: None,
371                },
372                id: Some(2),
373            }
374        );
375    }
376
377    #[test]
378    pub fn test_response_format_success() {
379        let response = serde_json::from_str::<JrpcSuccessResponse<String>>(
380            r#"
381            {
382              "jsonrpc": "2.0",
383              "id": 2,
384              "result": "success"
385            }
386            "#,
387        )
388        .unwrap();
389
390        assert_eq!(
391            response,
392            JrpcSuccessResponse::<String> {
393                jsonrpc: JsonRpcVersion::V2,
394                result: "success".to_string(),
395                id: 2,
396            }
397        );
398    }
399}