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: Option<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, Clone)]
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, Clone)]
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 {
41 price: Option<Price>,
42 rate: Rate,
43 #[serde(default = "default_funding_rate_interval", with = "humantime_serde")]
44 funding_rate_interval: Option<Duration>,
45 },
46}
47
48fn default_funding_rate_interval() -> Option<Duration> {
49 None
50}
51
52#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
53pub struct Filter {
54 pub name: Option<String>,
55 pub asset_type: Option<String>,
56}
57
58#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
59pub struct GetMetadataParams {
60 pub names: Option<Vec<String>>,
61 pub asset_types: Option<Vec<String>>,
62}
63
64#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
65pub enum JsonRpcVersion {
66 #[serde(rename = "2.0")]
67 V2,
68}
69
70#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
71#[serde(untagged)]
72pub enum JrpcResponse<T> {
73 Success(JrpcSuccessResponse<T>),
74 Error(JrpcErrorResponse),
75}
76
77#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
78pub struct JrpcSuccessResponse<T> {
79 pub jsonrpc: JsonRpcVersion,
80 pub result: T,
81 pub id: i64,
82}
83
84#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
85pub struct JrpcErrorResponse {
86 pub jsonrpc: JsonRpcVersion,
87 pub error: JrpcErrorObject,
88 pub id: Option<i64>,
89}
90
91#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
92pub struct JrpcErrorObject {
93 pub code: i64,
94 pub message: String,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub data: Option<serde_json::Value>,
97}
98
99#[derive(Debug, Eq, PartialEq)]
100pub enum JrpcError {
101 ParseError(String),
102 InternalError(String),
103 SendUpdateError(FeedUpdateParams),
104}
105
106impl From<JrpcError> for JrpcErrorObject {
108 fn from(error: JrpcError) -> Self {
109 match error {
110 JrpcError::ParseError(error_message) => JrpcErrorObject {
111 code: -32700,
112 message: "Parse error".to_string(),
113 data: Some(error_message.into()),
114 },
115 JrpcError::InternalError(error_message) => JrpcErrorObject {
116 code: -32603,
117 message: "Internal error".to_string(),
118 data: Some(error_message.into()),
119 },
120 JrpcError::SendUpdateError(feed_update_params) => JrpcErrorObject {
121 code: -32000,
122 message: "Internal error".to_string(),
123 data: Some(serde_json::to_value(feed_update_params).unwrap()),
124 },
125 }
126 }
127}
128
129#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
130pub struct SymbolMetadata {
131 pub pyth_lazer_id: PriceFeedId,
132 pub name: String,
133 pub symbol: String,
134 pub description: String,
135 pub asset_type: String,
136 pub exponent: i16,
137 pub cmc_id: Option<u32>,
138 #[serde(default, with = "humantime_serde", alias = "interval")]
139 pub funding_rate_interval: Option<Duration>,
140 pub min_publishers: u16,
141 pub min_channel: Channel,
142 pub state: SymbolState,
143 pub hermes_id: Option<String>,
144 pub quote_currency: Option<String>,
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::jrpc::JrpcCall::{GetMetadata, PushUpdate};
151
152 #[test]
153 fn test_push_update_price() {
154 let json = r#"
155 {
156 "jsonrpc": "2.0",
157 "method": "push_update",
158 "params": {
159 "feed_id": 1,
160 "source_timestamp": 124214124124,
161
162 "update": {
163 "type": "price",
164 "price": 1234567890,
165 "best_bid_price": 1234567891,
166 "best_ask_price": 1234567892
167 }
168 },
169 "id": 1
170 }
171 "#;
172
173 let expected = PythLazerAgentJrpcV1 {
174 jsonrpc: JsonRpcVersion::V2,
175 params: PushUpdate(FeedUpdateParams {
176 feed_id: PriceFeedId(1),
177 source_timestamp: TimestampUs::from_micros(124214124124),
178 update: UpdateParams::PriceUpdate {
179 price: Price::from_integer(1234567890, 0).unwrap(),
180 best_bid_price: Some(Price::from_integer(1234567891, 0).unwrap()),
181 best_ask_price: Some(Price::from_integer(1234567892, 0).unwrap()),
182 },
183 }),
184 id: Some(1),
185 };
186
187 assert_eq!(
188 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
189 expected
190 );
191 }
192
193 #[test]
194 fn test_push_update_price_without_id() {
195 let json = r#"
196 {
197 "jsonrpc": "2.0",
198 "method": "push_update",
199 "params": {
200 "feed_id": 1,
201 "source_timestamp": 745214124124,
202
203 "update": {
204 "type": "price",
205 "price": 5432,
206 "best_bid_price": 5432,
207 "best_ask_price": 5432
208 }
209 }
210 }
211 "#;
212
213 let expected = PythLazerAgentJrpcV1 {
214 jsonrpc: JsonRpcVersion::V2,
215 params: PushUpdate(FeedUpdateParams {
216 feed_id: PriceFeedId(1),
217 source_timestamp: TimestampUs::from_micros(745214124124),
218 update: UpdateParams::PriceUpdate {
219 price: Price::from_integer(5432, 0).unwrap(),
220 best_bid_price: Some(Price::from_integer(5432, 0).unwrap()),
221 best_ask_price: Some(Price::from_integer(5432, 0).unwrap()),
222 },
223 }),
224 id: None,
225 };
226
227 assert_eq!(
228 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
229 expected
230 );
231 }
232
233 #[test]
234 fn test_push_update_price_without_bid_ask() {
235 let json = r#"
236 {
237 "jsonrpc": "2.0",
238 "method": "push_update",
239 "params": {
240 "feed_id": 1,
241 "source_timestamp": 124214124124,
242
243 "update": {
244 "type": "price",
245 "price": 1234567890
246 }
247 },
248 "id": 1
249 }
250 "#;
251
252 let expected = PythLazerAgentJrpcV1 {
253 jsonrpc: JsonRpcVersion::V2,
254 params: PushUpdate(FeedUpdateParams {
255 feed_id: PriceFeedId(1),
256 source_timestamp: TimestampUs::from_micros(124214124124),
257 update: UpdateParams::PriceUpdate {
258 price: Price::from_integer(1234567890, 0).unwrap(),
259 best_bid_price: None,
260 best_ask_price: None,
261 },
262 }),
263 id: Some(1),
264 };
265
266 assert_eq!(
267 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
268 expected
269 );
270 }
271
272 #[test]
273 fn test_push_update_funding_rate() {
274 let json = r#"
275 {
276 "jsonrpc": "2.0",
277 "method": "push_update",
278 "params": {
279 "feed_id": 1,
280 "source_timestamp": 124214124124,
281
282 "update": {
283 "type": "funding_rate",
284 "price": 1234567890,
285 "rate": 1234567891,
286 "funding_rate_interval": "8h"
287 }
288 },
289 "id": 1
290 }
291 "#;
292
293 let expected = PythLazerAgentJrpcV1 {
294 jsonrpc: JsonRpcVersion::V2,
295 params: PushUpdate(FeedUpdateParams {
296 feed_id: PriceFeedId(1),
297 source_timestamp: TimestampUs::from_micros(124214124124),
298 update: UpdateParams::FundingRateUpdate {
299 price: Some(Price::from_integer(1234567890, 0).unwrap()),
300 rate: Rate::from_integer(1234567891, 0).unwrap(),
301 funding_rate_interval: Duration::from_secs(28800).into(),
302 },
303 }),
304 id: Some(1),
305 };
306
307 assert_eq!(
308 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
309 expected
310 );
311 }
312 #[test]
313 fn test_push_update_funding_rate_without_price() {
314 let json = r#"
315 {
316 "jsonrpc": "2.0",
317 "method": "push_update",
318 "params": {
319 "feed_id": 1,
320 "source_timestamp": 124214124124,
321
322 "update": {
323 "type": "funding_rate",
324 "rate": 1234567891
325 }
326 },
327 "id": 1
328 }
329 "#;
330
331 let expected = PythLazerAgentJrpcV1 {
332 jsonrpc: JsonRpcVersion::V2,
333 params: PushUpdate(FeedUpdateParams {
334 feed_id: PriceFeedId(1),
335 source_timestamp: TimestampUs::from_micros(124214124124),
336 update: UpdateParams::FundingRateUpdate {
337 price: None,
338 rate: Rate::from_integer(1234567891, 0).unwrap(),
339 funding_rate_interval: None,
340 },
341 }),
342 id: Some(1),
343 };
344
345 assert_eq!(
346 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
347 expected
348 );
349 }
350
351 #[test]
352 fn test_send_get_metadata() {
353 let json = r#"
354 {
355 "jsonrpc": "2.0",
356 "method": "get_metadata",
357 "params": {
358 "names": ["BTC/USD"],
359 "asset_types": ["crypto"]
360 },
361 "id": 1
362 }
363 "#;
364
365 let expected = PythLazerAgentJrpcV1 {
366 jsonrpc: JsonRpcVersion::V2,
367 params: GetMetadata(GetMetadataParams {
368 names: Some(vec!["BTC/USD".to_string()]),
369 asset_types: Some(vec!["crypto".to_string()]),
370 }),
371 id: Some(1),
372 };
373
374 assert_eq!(
375 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
376 expected
377 );
378 }
379
380 #[test]
381 fn test_get_metadata_without_filters() {
382 let json = r#"
383 {
384 "jsonrpc": "2.0",
385 "method": "get_metadata",
386 "params": {},
387 "id": 1
388 }
389 "#;
390
391 let expected = PythLazerAgentJrpcV1 {
392 jsonrpc: JsonRpcVersion::V2,
393 params: GetMetadata(GetMetadataParams {
394 names: None,
395 asset_types: None,
396 }),
397 id: Some(1),
398 };
399
400 assert_eq!(
401 serde_json::from_str::<PythLazerAgentJrpcV1>(json).unwrap(),
402 expected
403 );
404 }
405
406 #[test]
407 fn test_response_format_error() {
408 let response = serde_json::from_str::<JrpcErrorResponse>(
409 r#"
410 {
411 "jsonrpc": "2.0",
412 "id": 2,
413 "error": {
414 "message": "Internal error",
415 "code": -32603
416 }
417 }
418 "#,
419 )
420 .unwrap();
421
422 assert_eq!(
423 response,
424 JrpcErrorResponse {
425 jsonrpc: JsonRpcVersion::V2,
426 error: JrpcErrorObject {
427 code: -32603,
428 message: "Internal error".to_string(),
429 data: None,
430 },
431 id: Some(2),
432 }
433 );
434 }
435
436 #[test]
437 pub fn test_response_format_success() {
438 let response = serde_json::from_str::<JrpcSuccessResponse<String>>(
439 r#"
440 {
441 "jsonrpc": "2.0",
442 "id": 2,
443 "result": "success"
444 }
445 "#,
446 )
447 .unwrap();
448
449 assert_eq!(
450 response,
451 JrpcSuccessResponse::<String> {
452 jsonrpc: JsonRpcVersion::V2,
453 result: "success".to_string(),
454 id: 2,
455 }
456 );
457 }
458
459 #[test]
460 pub fn test_parse_response() {
461 let success_response = serde_json::from_str::<JrpcResponse<String>>(
462 r#"
463 {
464 "jsonrpc": "2.0",
465 "id": 2,
466 "result": "success"
467 }"#,
468 )
469 .unwrap();
470
471 assert_eq!(
472 success_response,
473 JrpcResponse::Success(JrpcSuccessResponse::<String> {
474 jsonrpc: JsonRpcVersion::V2,
475 result: "success".to_string(),
476 id: 2,
477 })
478 );
479
480 let error_response = serde_json::from_str::<JrpcResponse<String>>(
481 r#"
482 {
483 "jsonrpc": "2.0",
484 "id": 3,
485 "error": {
486 "code": -32603,
487 "message": "Internal error"
488 }
489 }"#,
490 )
491 .unwrap();
492
493 assert_eq!(
494 error_response,
495 JrpcResponse::Error(JrpcErrorResponse {
496 jsonrpc: JsonRpcVersion::V2,
497 error: JrpcErrorObject {
498 code: -32603,
499 message: "Internal error".to_string(),
500 data: None,
501 },
502 id: Some(3),
503 })
504 );
505 }
506}