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