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