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, Clone, Debug, Default, Eq, PartialEq)]
10#[serde(untagged)]
11pub enum JrpcId {
12 String(String),
13 Int(i64),
14 #[default]
15 Null,
16}
17
18#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
19pub struct PythLazerAgentJrpcV1 {
20 pub jsonrpc: JsonRpcVersion,
21 #[serde(flatten)]
22 pub params: JrpcCall,
23 #[serde(default)]
24 pub id: JrpcId,
25}
26
27#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
28#[serde(tag = "method", content = "params")]
29#[serde(rename_all = "snake_case")]
30pub enum JrpcCall {
31 PushUpdate(FeedUpdateParams),
32 PushUpdates(Vec<FeedUpdateParams>),
33 GetMetadata(GetMetadataParams),
34}
35
36#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
37pub struct FeedUpdateParams {
38 pub feed_id: PriceFeedId,
39 pub source_timestamp: TimestampUs,
40 pub update: UpdateParams,
41}
42
43#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
44#[serde(tag = "type")]
45pub enum UpdateParams {
46 #[serde(rename = "price")]
47 PriceUpdate {
48 price: Price,
49 best_bid_price: Option<Price>,
50 best_ask_price: Option<Price>,
51 },
52 #[serde(rename = "funding_rate")]
53 FundingRateUpdate {
54 price: Option<Price>,
55 rate: Rate,
56 #[serde(default = "default_funding_rate_interval", with = "humantime_serde")]
57 funding_rate_interval: Option<Duration>,
58 },
59}
60
61fn default_funding_rate_interval() -> Option<Duration> {
62 None
63}
64
65#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
66pub struct Filter {
67 pub name: Option<String>,
68 pub asset_type: Option<String>,
69}
70
71#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
72pub struct GetMetadataParams {
73 pub names: Option<Vec<String>>,
74 pub asset_types: Option<Vec<String>>,
75}
76
77#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
78pub enum JsonRpcVersion {
79 #[serde(rename = "2.0")]
80 V2,
81}
82
83#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
84#[serde(untagged)]
85pub enum JrpcResponse<T> {
86 Success(JrpcSuccessResponse<T>),
87 Error(JrpcErrorResponse),
88}
89
90#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
91pub struct JrpcSuccessResponse<T> {
92 pub jsonrpc: JsonRpcVersion,
93 pub result: T,
94 pub id: JrpcId,
95}
96
97#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
98pub struct JrpcErrorResponse {
99 pub jsonrpc: JsonRpcVersion,
100 pub error: JrpcErrorObject,
101 pub id: JrpcId,
102}
103
104#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
105pub struct JrpcErrorObject {
106 pub code: i64,
107 pub message: String,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub data: Option<serde_json::Value>,
110}
111
112#[derive(Debug, Eq, PartialEq)]
113pub enum JrpcError {
114 ParseError(String),
115 InternalError(String),
116 SendUpdateError(FeedUpdateParams),
117}
118
119impl From<JrpcError> for JrpcErrorObject {
121 fn from(error: JrpcError) -> Self {
122 match error {
123 JrpcError::ParseError(error_message) => JrpcErrorObject {
124 code: -32700,
125 message: "Parse error".to_string(),
126 data: Some(error_message.into()),
127 },
128 JrpcError::InternalError(error_message) => JrpcErrorObject {
129 code: -32603,
130 message: "Internal error".to_string(),
131 data: Some(error_message.into()),
132 },
133 JrpcError::SendUpdateError(feed_update_params) => JrpcErrorObject {
134 code: -32000,
135 message: "Internal error".to_string(),
136 data: Some(serde_json::to_value(feed_update_params).unwrap()),
137 },
138 }
139 }
140}
141
142#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
143pub struct SymbolMetadata {
144 pub pyth_lazer_id: PriceFeedId,
145 pub name: String,
146 pub symbol: String,
147 pub description: String,
148 pub asset_type: String,
149 pub exponent: i16,
150 pub cmc_id: Option<u32>,
151 #[serde(default, with = "humantime_serde", alias = "interval")]
152 pub funding_rate_interval: Option<Duration>,
153 pub min_publishers: u16,
154 pub min_channel: Channel,
155 pub state: SymbolState,
156 pub hermes_id: Option<String>,
157 pub quote_currency: Option<String>,
158 pub nasdaq_symbol: Option<String>,
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use crate::jrpc::JrpcCall::{GetMetadata, PushUpdate};
165
166 #[test]
167 fn test_push_update_price() {
168 let json = r#"
169 {
170 "jsonrpc": "2.0",
171 "method": "push_update",
172 "params": {
173 "feed_id": 1,
174 "source_timestamp": 124214124124,
175
176 "update": {
177 "type": "price",
178 "price": 1234567890,
179 "best_bid_price": 1234567891,
180 "best_ask_price": 1234567892
181 }
182 },
183 "id": 1
184 }
185 "#;
186
187 let expected = PythLazerAgentJrpcV1 {
188 jsonrpc: JsonRpcVersion::V2,
189 params: PushUpdate(FeedUpdateParams {
190 feed_id: PriceFeedId(1),
191 source_timestamp: TimestampUs::from_micros(124214124124),
192 update: UpdateParams::PriceUpdate {
193 price: Price::from_integer(1234567890, 0).unwrap(),
194 best_bid_price: Some(Price::from_integer(1234567891, 0).unwrap()),
195 best_ask_price: Some(Price::from_integer(1234567892, 0).unwrap()),
196 },
197 }),
198 id: JrpcId::Int(1),
199 };
200
201 assert_eq!(
202 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
203 expected
204 );
205 }
206
207 #[test]
208 fn test_push_update_price_string_id() {
209 let json = r#"
210 {
211 "jsonrpc": "2.0",
212 "method": "push_update",
213 "params": {
214 "feed_id": 1,
215 "source_timestamp": 124214124124,
216
217 "update": {
218 "type": "price",
219 "price": 1234567890,
220 "best_bid_price": 1234567891,
221 "best_ask_price": 1234567892
222 }
223 },
224 "id": "b6bb54a0-ea8d-439d-97a7-3b06befa0e76"
225 }
226 "#;
227
228 let expected = PythLazerAgentJrpcV1 {
229 jsonrpc: JsonRpcVersion::V2,
230 params: PushUpdate(FeedUpdateParams {
231 feed_id: PriceFeedId(1),
232 source_timestamp: TimestampUs::from_micros(124214124124),
233 update: UpdateParams::PriceUpdate {
234 price: Price::from_integer(1234567890, 0).unwrap(),
235 best_bid_price: Some(Price::from_integer(1234567891, 0).unwrap()),
236 best_ask_price: Some(Price::from_integer(1234567892, 0).unwrap()),
237 },
238 }),
239 id: JrpcId::String("b6bb54a0-ea8d-439d-97a7-3b06befa0e76".to_string()),
240 };
241
242 assert_eq!(
243 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
244 expected
245 );
246 }
247
248 #[test]
249 fn test_push_update_price_null_id() {
250 let json = r#"
251 {
252 "jsonrpc": "2.0",
253 "method": "push_update",
254 "params": {
255 "feed_id": 1,
256 "source_timestamp": 124214124124,
257
258 "update": {
259 "type": "price",
260 "price": 1234567890,
261 "best_bid_price": 1234567891,
262 "best_ask_price": 1234567892
263 }
264 },
265 "id": null
266 }
267 "#;
268
269 let expected = PythLazerAgentJrpcV1 {
270 jsonrpc: JsonRpcVersion::V2,
271 params: PushUpdate(FeedUpdateParams {
272 feed_id: PriceFeedId(1),
273 source_timestamp: TimestampUs::from_micros(124214124124),
274 update: UpdateParams::PriceUpdate {
275 price: Price::from_integer(1234567890, 0).unwrap(),
276 best_bid_price: Some(Price::from_integer(1234567891, 0).unwrap()),
277 best_ask_price: Some(Price::from_integer(1234567892, 0).unwrap()),
278 },
279 }),
280 id: JrpcId::Null,
281 };
282
283 assert_eq!(
284 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
285 expected
286 );
287 }
288
289 #[test]
290 fn test_push_update_price_without_id() {
291 let json = r#"
292 {
293 "jsonrpc": "2.0",
294 "method": "push_update",
295 "params": {
296 "feed_id": 1,
297 "source_timestamp": 745214124124,
298
299 "update": {
300 "type": "price",
301 "price": 5432,
302 "best_bid_price": 5432,
303 "best_ask_price": 5432
304 }
305 }
306 }
307 "#;
308
309 let expected = PythLazerAgentJrpcV1 {
310 jsonrpc: JsonRpcVersion::V2,
311 params: PushUpdate(FeedUpdateParams {
312 feed_id: PriceFeedId(1),
313 source_timestamp: TimestampUs::from_micros(745214124124),
314 update: UpdateParams::PriceUpdate {
315 price: Price::from_integer(5432, 0).unwrap(),
316 best_bid_price: Some(Price::from_integer(5432, 0).unwrap()),
317 best_ask_price: Some(Price::from_integer(5432, 0).unwrap()),
318 },
319 }),
320 id: JrpcId::Null,
321 };
322
323 assert_eq!(
324 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
325 expected
326 );
327 }
328
329 #[test]
330 fn test_push_update_price_without_bid_ask() {
331 let json = r#"
332 {
333 "jsonrpc": "2.0",
334 "method": "push_update",
335 "params": {
336 "feed_id": 1,
337 "source_timestamp": 124214124124,
338
339 "update": {
340 "type": "price",
341 "price": 1234567890
342 }
343 },
344 "id": 1
345 }
346 "#;
347
348 let expected = PythLazerAgentJrpcV1 {
349 jsonrpc: JsonRpcVersion::V2,
350 params: PushUpdate(FeedUpdateParams {
351 feed_id: PriceFeedId(1),
352 source_timestamp: TimestampUs::from_micros(124214124124),
353 update: UpdateParams::PriceUpdate {
354 price: Price::from_integer(1234567890, 0).unwrap(),
355 best_bid_price: None,
356 best_ask_price: None,
357 },
358 }),
359 id: JrpcId::Int(1),
360 };
361
362 assert_eq!(
363 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
364 expected
365 );
366 }
367
368 #[test]
369 fn test_push_update_funding_rate() {
370 let json = r#"
371 {
372 "jsonrpc": "2.0",
373 "method": "push_update",
374 "params": {
375 "feed_id": 1,
376 "source_timestamp": 124214124124,
377
378 "update": {
379 "type": "funding_rate",
380 "price": 1234567890,
381 "rate": 1234567891,
382 "funding_rate_interval": "8h"
383 }
384 },
385 "id": 1
386 }
387 "#;
388
389 let expected = PythLazerAgentJrpcV1 {
390 jsonrpc: JsonRpcVersion::V2,
391 params: PushUpdate(FeedUpdateParams {
392 feed_id: PriceFeedId(1),
393 source_timestamp: TimestampUs::from_micros(124214124124),
394 update: UpdateParams::FundingRateUpdate {
395 price: Some(Price::from_integer(1234567890, 0).unwrap()),
396 rate: Rate::from_integer(1234567891, 0).unwrap(),
397 funding_rate_interval: Duration::from_secs(28800).into(),
398 },
399 }),
400 id: JrpcId::Int(1),
401 };
402
403 assert_eq!(
404 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
405 expected
406 );
407 }
408 #[test]
409 fn test_push_update_funding_rate_without_price() {
410 let json = r#"
411 {
412 "jsonrpc": "2.0",
413 "method": "push_update",
414 "params": {
415 "feed_id": 1,
416 "source_timestamp": 124214124124,
417
418 "update": {
419 "type": "funding_rate",
420 "rate": 1234567891
421 }
422 },
423 "id": 1
424 }
425 "#;
426
427 let expected = PythLazerAgentJrpcV1 {
428 jsonrpc: JsonRpcVersion::V2,
429 params: PushUpdate(FeedUpdateParams {
430 feed_id: PriceFeedId(1),
431 source_timestamp: TimestampUs::from_micros(124214124124),
432 update: UpdateParams::FundingRateUpdate {
433 price: None,
434 rate: Rate::from_integer(1234567891, 0).unwrap(),
435 funding_rate_interval: None,
436 },
437 }),
438 id: JrpcId::Int(1),
439 };
440
441 assert_eq!(
442 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
443 expected
444 );
445 }
446
447 #[test]
448 fn test_send_get_metadata() {
449 let json = r#"
450 {
451 "jsonrpc": "2.0",
452 "method": "get_metadata",
453 "params": {
454 "names": ["BTC/USD"],
455 "asset_types": ["crypto"]
456 },
457 "id": 1
458 }
459 "#;
460
461 let expected = PythLazerAgentJrpcV1 {
462 jsonrpc: JsonRpcVersion::V2,
463 params: GetMetadata(GetMetadataParams {
464 names: Some(vec!["BTC/USD".to_string()]),
465 asset_types: Some(vec!["crypto".to_string()]),
466 }),
467 id: JrpcId::Int(1),
468 };
469
470 assert_eq!(
471 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
472 expected
473 );
474 }
475
476 #[test]
477 fn test_get_metadata_without_filters() {
478 let json = r#"
479 {
480 "jsonrpc": "2.0",
481 "method": "get_metadata",
482 "params": {},
483 "id": 1
484 }
485 "#;
486
487 let expected = PythLazerAgentJrpcV1 {
488 jsonrpc: JsonRpcVersion::V2,
489 params: GetMetadata(GetMetadataParams {
490 names: None,
491 asset_types: None,
492 }),
493 id: JrpcId::Int(1),
494 };
495
496 assert_eq!(
497 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
498 expected
499 );
500 }
501
502 #[test]
503 fn test_response_format_error() {
504 let response = serde_json::from_str::<JrpcErrorResponse>(
505 r#"
506 {
507 "jsonrpc": "2.0",
508 "id": 2,
509 "error": {
510 "message": "Internal error",
511 "code": -32603
512 }
513 }
514 "#,
515 )
516 .unwrap();
517
518 assert_eq!(
519 response,
520 JrpcErrorResponse {
521 jsonrpc: JsonRpcVersion::V2,
522 error: JrpcErrorObject {
523 code: -32603,
524 message: "Internal error".to_string(),
525 data: None,
526 },
527 id: JrpcId::Int(2),
528 }
529 );
530 }
531
532 #[test]
533 fn test_response_format_error_string_id() {
534 let response = serde_json::from_str::<JrpcErrorResponse>(
535 r#"
536 {
537 "jsonrpc": "2.0",
538 "id": "62b627dc-5599-43dd-b2c2-9c4d30f4fdb4",
539 "error": {
540 "message": "Internal error",
541 "code": -32603
542 }
543 }
544 "#,
545 )
546 .unwrap();
547
548 assert_eq!(
549 response,
550 JrpcErrorResponse {
551 jsonrpc: JsonRpcVersion::V2,
552 error: JrpcErrorObject {
553 code: -32603,
554 message: "Internal error".to_string(),
555 data: None,
556 },
557 id: JrpcId::String("62b627dc-5599-43dd-b2c2-9c4d30f4fdb4".to_string())
558 }
559 );
560 }
561
562 #[test]
563 pub fn test_response_format_success() {
564 let response = serde_json::from_str::<JrpcSuccessResponse<String>>(
565 r#"
566 {
567 "jsonrpc": "2.0",
568 "id": 2,
569 "result": "success"
570 }
571 "#,
572 )
573 .unwrap();
574
575 assert_eq!(
576 response,
577 JrpcSuccessResponse::<String> {
578 jsonrpc: JsonRpcVersion::V2,
579 result: "success".to_string(),
580 id: JrpcId::Int(2),
581 }
582 );
583 }
584
585 #[test]
586 pub fn test_response_format_success_string_id() {
587 let response = serde_json::from_str::<JrpcSuccessResponse<String>>(
588 r#"
589 {
590 "jsonrpc": "2.0",
591 "id": "62b627dc-5599-43dd-b2c2-9c4d30f4fdb4",
592 "result": "success"
593 }
594 "#,
595 )
596 .unwrap();
597
598 assert_eq!(
599 response,
600 JrpcSuccessResponse::<String> {
601 jsonrpc: JsonRpcVersion::V2,
602 result: "success".to_string(),
603 id: JrpcId::String("62b627dc-5599-43dd-b2c2-9c4d30f4fdb4".to_string()),
604 }
605 );
606 }
607
608 #[test]
609 pub fn test_parse_response() {
610 let success_response = serde_json::from_str::<JrpcResponse<String>>(
611 r#"
612 {
613 "jsonrpc": "2.0",
614 "id": 2,
615 "result": "success"
616 }"#,
617 )
618 .unwrap();
619
620 assert_eq!(
621 success_response,
622 JrpcResponse::Success(JrpcSuccessResponse::<String> {
623 jsonrpc: JsonRpcVersion::V2,
624 result: "success".to_string(),
625 id: JrpcId::Int(2),
626 })
627 );
628
629 let error_response = serde_json::from_str::<JrpcResponse<String>>(
630 r#"
631 {
632 "jsonrpc": "2.0",
633 "id": 3,
634 "error": {
635 "code": -32603,
636 "message": "Internal error"
637 }
638 }"#,
639 )
640 .unwrap();
641
642 assert_eq!(
643 error_response,
644 JrpcResponse::Error(JrpcErrorResponse {
645 jsonrpc: JsonRpcVersion::V2,
646 error: JrpcErrorObject {
647 code: -32603,
648 message: "Internal error".to_string(),
649 data: None,
650 },
651 id: JrpcId::Int(3),
652 })
653 );
654 }
655
656 #[test]
657 pub fn test_parse_response_string_id() {
658 let success_response = serde_json::from_str::<JrpcResponse<String>>(
659 r#"
660 {
661 "jsonrpc": "2.0",
662 "id": "id-2",
663 "result": "success"
664 }"#,
665 )
666 .unwrap();
667
668 assert_eq!(
669 success_response,
670 JrpcResponse::Success(JrpcSuccessResponse::<String> {
671 jsonrpc: JsonRpcVersion::V2,
672 result: "success".to_string(),
673 id: JrpcId::String("id-2".to_string()),
674 })
675 );
676
677 let error_response = serde_json::from_str::<JrpcResponse<String>>(
678 r#"
679 {
680 "jsonrpc": "2.0",
681 "id": "id-3",
682 "error": {
683 "code": -32603,
684 "message": "Internal error"
685 }
686 }"#,
687 )
688 .unwrap();
689
690 assert_eq!(
691 error_response,
692 JrpcResponse::Error(JrpcErrorResponse {
693 jsonrpc: JsonRpcVersion::V2,
694 error: JrpcErrorObject {
695 code: -32603,
696 message: "Internal error".to_string(),
697 data: None,
698 },
699 id: JrpcId::String("id-3".to_string()),
700 })
701 );
702 }
703}