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