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