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