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]
170 fn test_push_update_price() {
171 let json = r#"
172 {
173 "jsonrpc": "2.0",
174 "method": "push_update",
175 "params": {
176 "feed_id": 1,
177 "source_timestamp": 124214124124,
178
179 "update": {
180 "type": "price",
181 "price": 1234567890,
182 "best_bid_price": 1234567891,
183 "best_ask_price": 1234567892,
184 "trading_status": "halted",
185 "market_session": "postMarket"
186 }
187 },
188 "id": 1
189 }
190 "#;
191
192 let expected = PythLazerAgentJrpcV1 {
193 jsonrpc: JsonRpcVersion::V2,
194 params: PushUpdate(FeedUpdateParams {
195 feed_id: PriceFeedId(1),
196 source_timestamp: TimestampUs::from_micros(124214124124),
197 update: UpdateParams::PriceUpdate {
198 price: Some(Price::from_integer(1234567890, 0).unwrap()),
199 best_bid_price: Some(Price::from_integer(1234567891, 0).unwrap()),
200 best_ask_price: Some(Price::from_integer(1234567892, 0).unwrap()),
201 trading_status: Some(TradingStatus::Halted),
202 market_session: Some(MarketSession::PostMarket),
203 },
204 }),
205 id: JrpcId::Int(1),
206 };
207
208 assert_eq!(
209 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
210 expected
211 );
212 }
213
214 #[test]
215 fn test_push_update_price_string_id() {
216 let json = r#"
217 {
218 "jsonrpc": "2.0",
219 "method": "push_update",
220 "params": {
221 "feed_id": 1,
222 "source_timestamp": 124214124124,
223
224 "update": {
225 "type": "price",
226 "price": 1234567890,
227 "best_bid_price": 1234567891,
228 "best_ask_price": 1234567892
229 }
230 },
231 "id": "b6bb54a0-ea8d-439d-97a7-3b06befa0e76"
232 }
233 "#;
234
235 let expected = PythLazerAgentJrpcV1 {
236 jsonrpc: JsonRpcVersion::V2,
237 params: PushUpdate(FeedUpdateParams {
238 feed_id: PriceFeedId(1),
239 source_timestamp: TimestampUs::from_micros(124214124124),
240 update: UpdateParams::PriceUpdate {
241 price: Some(Price::from_integer(1234567890, 0).unwrap()),
242 best_bid_price: Some(Price::from_integer(1234567891, 0).unwrap()),
243 best_ask_price: Some(Price::from_integer(1234567892, 0).unwrap()),
244 trading_status: None,
245 market_session: None,
246 },
247 }),
248 id: JrpcId::String("b6bb54a0-ea8d-439d-97a7-3b06befa0e76".to_string()),
249 };
250
251 assert_eq!(
252 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
253 expected
254 );
255 }
256
257 #[test]
258 fn test_push_update_price_null_id() {
259 let json = r#"
260 {
261 "jsonrpc": "2.0",
262 "method": "push_update",
263 "params": {
264 "feed_id": 1,
265 "source_timestamp": 124214124124,
266
267 "update": {
268 "type": "price",
269 "price": 1234567890,
270 "best_bid_price": 1234567891,
271 "best_ask_price": 1234567892
272 }
273 },
274 "id": null
275 }
276 "#;
277
278 let expected = PythLazerAgentJrpcV1 {
279 jsonrpc: JsonRpcVersion::V2,
280 params: PushUpdate(FeedUpdateParams {
281 feed_id: PriceFeedId(1),
282 source_timestamp: TimestampUs::from_micros(124214124124),
283 update: UpdateParams::PriceUpdate {
284 price: Some(Price::from_integer(1234567890, 0).unwrap()),
285 best_bid_price: Some(Price::from_integer(1234567891, 0).unwrap()),
286 best_ask_price: Some(Price::from_integer(1234567892, 0).unwrap()),
287 trading_status: None,
288 market_session: None,
289 },
290 }),
291 id: JrpcId::Null,
292 };
293
294 assert_eq!(
295 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
296 expected
297 );
298 }
299
300 #[test]
301 fn test_push_update_price_without_id() {
302 let json = r#"
303 {
304 "jsonrpc": "2.0",
305 "method": "push_update",
306 "params": {
307 "feed_id": 1,
308 "source_timestamp": 745214124124,
309
310 "update": {
311 "type": "price",
312 "price": 5432,
313 "best_bid_price": 5432,
314 "best_ask_price": 5432
315 }
316 }
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(745214124124),
325 update: UpdateParams::PriceUpdate {
326 price: Some(Price::from_integer(5432, 0).unwrap()),
327 best_bid_price: Some(Price::from_integer(5432, 0).unwrap()),
328 best_ask_price: Some(Price::from_integer(5432, 0).unwrap()),
329 trading_status: None,
330 market_session: None,
331 },
332 }),
333 id: JrpcId::Null,
334 };
335
336 assert_eq!(
337 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
338 expected
339 );
340 }
341
342 #[test]
343 fn test_push_update_price_without_bid_ask() {
344 let json = r#"
345 {
346 "jsonrpc": "2.0",
347 "method": "push_update",
348 "params": {
349 "feed_id": 1,
350 "source_timestamp": 124214124124,
351
352 "update": {
353 "type": "price",
354 "price": 1234567890
355 }
356 },
357 "id": 1
358 }
359 "#;
360
361 let expected = PythLazerAgentJrpcV1 {
362 jsonrpc: JsonRpcVersion::V2,
363 params: PushUpdate(FeedUpdateParams {
364 feed_id: PriceFeedId(1),
365 source_timestamp: TimestampUs::from_micros(124214124124),
366 update: UpdateParams::PriceUpdate {
367 price: Some(Price::from_integer(1234567890, 0).unwrap()),
368 best_bid_price: None,
369 best_ask_price: None,
370 trading_status: None,
371 market_session: None,
372 },
373 }),
374 id: JrpcId::Int(1),
375 };
376
377 assert_eq!(
378 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
379 expected
380 );
381 }
382
383 #[test]
384 fn test_push_update_funding_rate() {
385 let json = r#"
386 {
387 "jsonrpc": "2.0",
388 "method": "push_update",
389 "params": {
390 "feed_id": 1,
391 "source_timestamp": 124214124124,
392
393 "update": {
394 "type": "funding_rate",
395 "price": 1234567890,
396 "rate": 1234567891,
397 "funding_rate_interval": "8h"
398 }
399 },
400 "id": 1
401 }
402 "#;
403
404 let expected = PythLazerAgentJrpcV1 {
405 jsonrpc: JsonRpcVersion::V2,
406 params: PushUpdate(FeedUpdateParams {
407 feed_id: PriceFeedId(1),
408 source_timestamp: TimestampUs::from_micros(124214124124),
409 update: UpdateParams::FundingRateUpdate {
410 price: Some(Price::from_integer(1234567890, 0).unwrap()),
411 rate: Rate::from_integer(1234567891, 0).unwrap(),
412 funding_rate_interval: Duration::from_secs(28800).into(),
413 },
414 }),
415 id: JrpcId::Int(1),
416 };
417
418 assert_eq!(
419 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
420 expected
421 );
422 }
423 #[test]
424 fn test_push_update_funding_rate_without_price() {
425 let json = r#"
426 {
427 "jsonrpc": "2.0",
428 "method": "push_update",
429 "params": {
430 "feed_id": 1,
431 "source_timestamp": 124214124124,
432
433 "update": {
434 "type": "funding_rate",
435 "rate": 1234567891
436 }
437 },
438 "id": 1
439 }
440 "#;
441
442 let expected = PythLazerAgentJrpcV1 {
443 jsonrpc: JsonRpcVersion::V2,
444 params: PushUpdate(FeedUpdateParams {
445 feed_id: PriceFeedId(1),
446 source_timestamp: TimestampUs::from_micros(124214124124),
447 update: UpdateParams::FundingRateUpdate {
448 price: None,
449 rate: Rate::from_integer(1234567891, 0).unwrap(),
450 funding_rate_interval: None,
451 },
452 }),
453 id: JrpcId::Int(1),
454 };
455
456 assert_eq!(
457 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
458 expected
459 );
460 }
461
462 #[test]
463 fn test_send_get_metadata() {
464 let json = r#"
465 {
466 "jsonrpc": "2.0",
467 "method": "get_metadata",
468 "params": {
469 "names": ["BTC/USD"],
470 "asset_types": ["crypto"]
471 },
472 "id": 1
473 }
474 "#;
475
476 let expected = PythLazerAgentJrpcV1 {
477 jsonrpc: JsonRpcVersion::V2,
478 params: GetMetadata(GetMetadataParams {
479 names: Some(vec!["BTC/USD".to_string()]),
480 asset_types: Some(vec!["crypto".to_string()]),
481 }),
482 id: JrpcId::Int(1),
483 };
484
485 assert_eq!(
486 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
487 expected
488 );
489 }
490
491 #[test]
492 fn test_get_metadata_without_filters() {
493 let json = r#"
494 {
495 "jsonrpc": "2.0",
496 "method": "get_metadata",
497 "params": {},
498 "id": 1
499 }
500 "#;
501
502 let expected = PythLazerAgentJrpcV1 {
503 jsonrpc: JsonRpcVersion::V2,
504 params: GetMetadata(GetMetadataParams {
505 names: None,
506 asset_types: None,
507 }),
508 id: JrpcId::Int(1),
509 };
510
511 assert_eq!(
512 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
513 expected
514 );
515 }
516
517 #[test]
518 fn test_response_format_error() {
519 let response = serde_json::from_str::<JrpcErrorResponse>(
520 r#"
521 {
522 "jsonrpc": "2.0",
523 "id": 2,
524 "error": {
525 "message": "Internal error",
526 "code": -32603
527 }
528 }
529 "#,
530 )
531 .unwrap();
532
533 assert_eq!(
534 response,
535 JrpcErrorResponse {
536 jsonrpc: JsonRpcVersion::V2,
537 error: JrpcErrorObject {
538 code: -32603,
539 message: "Internal error".to_string(),
540 data: None,
541 },
542 id: JrpcId::Int(2),
543 }
544 );
545 }
546
547 #[test]
548 fn test_response_format_error_string_id() {
549 let response = serde_json::from_str::<JrpcErrorResponse>(
550 r#"
551 {
552 "jsonrpc": "2.0",
553 "id": "62b627dc-5599-43dd-b2c2-9c4d30f4fdb4",
554 "error": {
555 "message": "Internal error",
556 "code": -32603
557 }
558 }
559 "#,
560 )
561 .unwrap();
562
563 assert_eq!(
564 response,
565 JrpcErrorResponse {
566 jsonrpc: JsonRpcVersion::V2,
567 error: JrpcErrorObject {
568 code: -32603,
569 message: "Internal error".to_string(),
570 data: None,
571 },
572 id: JrpcId::String("62b627dc-5599-43dd-b2c2-9c4d30f4fdb4".to_string())
573 }
574 );
575 }
576
577 #[test]
578 pub fn test_response_format_success() {
579 let response = serde_json::from_str::<JrpcSuccessResponse<String>>(
580 r#"
581 {
582 "jsonrpc": "2.0",
583 "id": 2,
584 "result": "success"
585 }
586 "#,
587 )
588 .unwrap();
589
590 assert_eq!(
591 response,
592 JrpcSuccessResponse::<String> {
593 jsonrpc: JsonRpcVersion::V2,
594 result: "success".to_string(),
595 id: JrpcId::Int(2),
596 }
597 );
598 }
599
600 #[test]
601 pub fn test_response_format_success_string_id() {
602 let response = serde_json::from_str::<JrpcSuccessResponse<String>>(
603 r#"
604 {
605 "jsonrpc": "2.0",
606 "id": "62b627dc-5599-43dd-b2c2-9c4d30f4fdb4",
607 "result": "success"
608 }
609 "#,
610 )
611 .unwrap();
612
613 assert_eq!(
614 response,
615 JrpcSuccessResponse::<String> {
616 jsonrpc: JsonRpcVersion::V2,
617 result: "success".to_string(),
618 id: JrpcId::String("62b627dc-5599-43dd-b2c2-9c4d30f4fdb4".to_string()),
619 }
620 );
621 }
622
623 #[test]
624 pub fn test_parse_response() {
625 let success_response = serde_json::from_str::<JrpcResponse<String>>(
626 r#"
627 {
628 "jsonrpc": "2.0",
629 "id": 2,
630 "result": "success"
631 }"#,
632 )
633 .unwrap();
634
635 assert_eq!(
636 success_response,
637 JrpcResponse::Success(JrpcSuccessResponse::<String> {
638 jsonrpc: JsonRpcVersion::V2,
639 result: "success".to_string(),
640 id: JrpcId::Int(2),
641 })
642 );
643
644 let error_response = serde_json::from_str::<JrpcResponse<String>>(
645 r#"
646 {
647 "jsonrpc": "2.0",
648 "id": 3,
649 "error": {
650 "code": -32603,
651 "message": "Internal error"
652 }
653 }"#,
654 )
655 .unwrap();
656
657 assert_eq!(
658 error_response,
659 JrpcResponse::Error(JrpcErrorResponse {
660 jsonrpc: JsonRpcVersion::V2,
661 error: JrpcErrorObject {
662 code: -32603,
663 message: "Internal error".to_string(),
664 data: None,
665 },
666 id: JrpcId::Int(3),
667 })
668 );
669 }
670
671 #[test]
672 pub fn test_parse_response_string_id() {
673 let success_response = serde_json::from_str::<JrpcResponse<String>>(
674 r#"
675 {
676 "jsonrpc": "2.0",
677 "id": "id-2",
678 "result": "success"
679 }"#,
680 )
681 .unwrap();
682
683 assert_eq!(
684 success_response,
685 JrpcResponse::Success(JrpcSuccessResponse::<String> {
686 jsonrpc: JsonRpcVersion::V2,
687 result: "success".to_string(),
688 id: JrpcId::String("id-2".to_string()),
689 })
690 );
691
692 let error_response = serde_json::from_str::<JrpcResponse<String>>(
693 r#"
694 {
695 "jsonrpc": "2.0",
696 "id": "id-3",
697 "error": {
698 "code": -32603,
699 "message": "Internal error"
700 }
701 }"#,
702 )
703 .unwrap();
704
705 assert_eq!(
706 error_response,
707 JrpcResponse::Error(JrpcErrorResponse {
708 jsonrpc: JsonRpcVersion::V2,
709 error: JrpcErrorObject {
710 code: -32603,
711 message: "Internal error".to_string(),
712 data: None,
713 },
714 id: JrpcId::String("id-3".to_string()),
715 })
716 );
717 }
718}