1use serde::{Deserialize, Serialize};
7use std::sync::atomic::{AtomicU64, Ordering};
8
9static REQUEST_ID: AtomicU64 = AtomicU64::new(1);
11
12fn next_request_id() -> u64 {
13 REQUEST_ID.fetch_add(1, Ordering::SeqCst)
14}
15
16#[derive(Debug, Clone, Serialize)]
18pub struct JsonRpcRequest<T: Serialize> {
19 pub jsonrpc: &'static str,
20 pub method: String,
21 pub params: T,
22 pub id: u64,
23}
24
25impl<T: Serialize> JsonRpcRequest<T> {
26 pub fn new(method: impl Into<String>, params: T) -> Self {
27 Self {
28 jsonrpc: "2.0",
29 method: method.into(),
30 params,
31 id: next_request_id(),
32 }
33 }
34}
35
36#[derive(Debug, Clone, Serialize)]
38pub struct AuthParams {
39 pub api_key: String,
41}
42
43#[derive(Debug, Clone, Serialize)]
45pub struct SubscriptionConfig {
46 pub frequency: String,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub client_id: Option<String>,
51 pub batch: Vec<BatchItem>,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub options: Option<SubscriptionOptions>,
56}
57
58#[derive(Debug, Clone, Serialize)]
60pub struct BatchItem {
61 pub sid: String,
63 pub feed: String,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub exchange: Option<String>,
68 pub base_asset: String,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub quote_asset: Option<String>,
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub model: Option<String>,
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub strike: Option<Vec<f64>>,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub delta: Option<Vec<f64>>,
82 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
84 pub option_type: Option<String>,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub asset: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub expiry: Option<String>,
91}
92
93#[derive(Debug, Clone, Serialize)]
95pub struct SubscriptionOptions {
96 pub format: FormatOptions,
97}
98
99#[derive(Debug, Clone, Serialize)]
101pub struct FormatOptions {
102 pub timestamp: String,
104 pub hexify: bool,
106 pub decimals: u8,
108}
109
110impl Default for SubscriptionOptions {
111 fn default() -> Self {
112 Self {
113 format: FormatOptions {
114 timestamp: "ms".to_string(),
115 hexify: false,
116 decimals: 5,
117 },
118 }
119 }
120}
121
122#[derive(Debug, Clone, Serialize)]
124pub struct BlockScholesSubscribe {
125 pub jsonrpc: &'static str,
126 pub method: String,
127 pub params: Vec<SubscriptionConfig>,
129 pub id: u64,
130}
131
132impl BlockScholesSubscribe {
133 pub fn delta_iv(
139 base_asset: impl Into<String>,
140 expiry: impl Into<String>,
141 client_id: Option<String>,
142 ) -> Self {
143 let base_asset = base_asset.into();
144 let expiry = expiry.into();
145 let sid = format!("delta_iv_{}_{}", base_asset, next_request_id());
146
147 Self {
148 jsonrpc: "2.0",
149 method: "subscribe".to_string(),
150 params: vec![SubscriptionConfig {
151 frequency: "1000ms".to_string(),
152 client_id,
153 batch: vec![BatchItem {
154 sid,
155 feed: "delta.iv".to_string(),
156 exchange: Some("composite".to_string()),
157 base_asset,
158 quote_asset: None,
159 model: Some("SVI".to_string()),
160 strike: None,
161 delta: Some(vec![0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95]),
162 option_type: None,
163 asset: None,
164 expiry: Some(expiry),
165 }],
166 options: Some(SubscriptionOptions::default()),
167 }],
168 id: next_request_id(),
169 }
170 }
171
172 pub fn index_price(base_asset: impl Into<String>, client_id: Option<String>) -> Self {
174 let base_asset = base_asset.into();
175 let sid = format!("index_{}_{}", base_asset, next_request_id());
176
177 Self {
178 jsonrpc: "2.0",
179 method: "subscribe".to_string(),
180 params: vec![SubscriptionConfig {
181 frequency: "1000ms".to_string(),
182 client_id,
183 batch: vec![BatchItem {
184 sid,
185 feed: "index.px".to_string(),
186 exchange: None,
187 base_asset,
188 quote_asset: Some("USD".to_string()),
189 model: None,
190 strike: None,
191 delta: None,
192 option_type: None,
193 asset: Some("spot".to_string()),
194 expiry: None,
195 }],
196 options: Some(SubscriptionOptions::default()),
197 }],
198 id: next_request_id(),
199 }
200 }
201
202 pub fn atm_iv(
203 base_asset: impl Into<String>,
204 expiry: impl Into<String>,
205 client_id: Option<String>,
206 ) -> Self {
207 let base_asset = base_asset.into();
208 let expiry = expiry.into();
209 let sid = format!("atm_iv_{}_{}", base_asset, next_request_id());
210
211 Self {
212 jsonrpc: "2.0",
213 method: "subscribe".to_string(),
214 params: vec![SubscriptionConfig {
215 frequency: "1000ms".to_string(),
216 client_id,
217 batch: vec![BatchItem {
218 sid,
219 feed: "delta.iv".to_string(),
220 exchange: Some("composite".to_string()),
221 base_asset,
222 quote_asset: None,
223 model: Some("SVI".to_string()),
224 strike: None,
225 delta: Some(vec![0.5]),
226 option_type: None,
227 asset: None,
228 expiry: Some(expiry),
229 }],
230 options: Some(SubscriptionOptions::default()),
231 }],
232 id: next_request_id(),
233 }
234 }
235}
236
237#[derive(Debug, Clone, Serialize)]
239pub struct BlockScholesAuth {
240 pub jsonrpc: &'static str,
241 pub method: String,
242 pub params: AuthParams,
243 pub id: u64,
244}
245
246impl BlockScholesAuth {
247 pub fn new(api_key: impl Into<String>, _api_secret: Option<String>) -> Self {
250 Self {
251 jsonrpc: "2.0",
252 method: "authenticate".to_string(),
253 params: AuthParams {
254 api_key: api_key.into(),
255 },
256 id: next_request_id(),
257 }
258 }
259}
260
261#[derive(Debug, Clone, Deserialize)]
263pub struct JsonRpcResponse {
264 pub jsonrpc: String,
265 #[serde(default)]
266 pub result: Option<sonic_rs::Value>,
267 #[serde(default)]
268 pub error: Option<JsonRpcError>,
269 pub id: Option<u64>,
270 #[serde(default)]
272 pub method: Option<String>,
273 #[serde(default)]
275 pub params: Option<sonic_rs::Value>,
276}
277
278#[derive(Debug, Clone, Deserialize)]
280pub struct JsonRpcError {
281 pub message: String,
282 pub code: i32,
283}
284
285impl JsonRpcResponse {
286 pub fn is_success(&self) -> bool {
288 self.error.is_none() && self.result.is_some()
289 }
290
291 pub fn is_error(&self) -> bool {
293 self.error.is_some()
294 }
295
296 pub fn is_subscription_data(&self) -> bool {
298 self.id.is_none() && self.method.is_some() && self.params.is_some()
299 }
300}
301
302#[derive(Debug, Clone, Deserialize)]
306#[serde(tag = "type", rename_all = "snake_case")]
307pub enum BlockScholesMessage {
308 #[serde(alias = "vol_surface_update", alias = "volatility_surface")]
310 VolSurfaceUpdate(VolSurfaceUpdateMessage),
311
312 #[serde(alias = "atm_vol_update", alias = "atm_volatility")]
314 AtmVolUpdate(AtmVolUpdateMessage),
315
316 Subscribed { channel: String, symbol: String },
318
319 Unsubscribed { channel: String, symbol: String },
321
322 #[serde(alias = "auth_success")]
324 Authenticated {
325 #[serde(default)]
326 message: Option<String>,
327 },
328
329 Error {
331 message: String,
332 #[serde(default)]
333 code: Option<i32>,
334 },
335
336 Heartbeat { timestamp: i64 },
338
339 #[serde(alias = "welcome", alias = "connected")]
341 Info {
342 #[serde(default)]
343 message: Option<String>,
344 #[serde(default)]
345 version: Option<String>,
346 },
347}
348
349#[derive(Debug, Clone, Deserialize)]
351pub struct VolSurfaceUpdateMessage {
352 pub symbol: String,
354 pub timestamp: i64,
356 pub points: Vec<VolSurfacePoint>,
358 #[serde(default)]
360 pub atm_vols: Vec<AtmVolPoint>,
361}
362
363#[derive(Debug, Clone, Deserialize, Serialize)]
365pub struct VolSurfacePoint {
366 pub strike: f64,
368 pub expiry: i64,
370 pub iv: f64,
372 #[serde(default)]
374 pub delta: Option<f64>,
375 #[serde(default)]
377 pub forward: Option<f64>,
378 #[serde(default)]
380 pub signature: Option<String>,
381}
382
383#[derive(Debug, Clone, Deserialize, Serialize)]
385pub struct AtmVolPoint {
386 pub expiry: i64,
388 pub iv: f64,
390 #[serde(default)]
392 pub forward: Option<f64>,
393}
394
395#[derive(Debug, Clone, Deserialize)]
397pub struct AtmVolUpdateMessage {
398 pub symbol: String,
400 pub timestamp: i64,
402 pub points: Vec<AtmVolPoint>,
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 #[test]
411 fn test_subscribe_serialization() {
412 let sub =
413 BlockScholesSubscribe::atm_iv("BTC", "2026-03-28T08:00:00Z", Some("test".to_string()));
414 let json = sonic_rs::to_string(&sub).unwrap();
415 assert!(json.contains("\"jsonrpc\":\"2.0\""));
416 assert!(json.contains("\"method\":\"subscribe\""));
417 assert!(json.contains("\"feed\":\"delta.iv\""));
418 assert!(json.contains("\"base_asset\":\"BTC\""));
419 assert!(json.contains("\"delta\":[0.5]"));
420 }
421
422 #[test]
423 fn test_vol_surface_update_deserialization() {
424 let json = r#"{
425 "type": "vol_surface_update",
426 "symbol": "BTC",
427 "timestamp": 1735689600000,
428 "points": [
429 {"strike": 100000.0, "expiry": 1735689600, "iv": 0.75},
430 {"strike": 110000.0, "expiry": 1735689600, "iv": 0.72, "delta": 0.25}
431 ],
432 "atm_vols": [
433 {"expiry": 1735689600, "iv": 0.70}
434 ]
435 }"#;
436
437 let msg: BlockScholesMessage = sonic_rs::from_str(json).unwrap();
438 match msg {
439 BlockScholesMessage::VolSurfaceUpdate(update) => {
440 assert_eq!(update.symbol, "BTC");
441 assert_eq!(update.points.len(), 2);
442 assert_eq!(update.points[0].strike, 100000.0);
443 assert_eq!(update.points[0].iv, 0.75);
444 assert_eq!(update.points[1].delta, Some(0.25));
445 assert_eq!(update.atm_vols.len(), 1);
446 }
447 _ => panic!("Expected VolSurfaceUpdate"),
448 }
449 }
450
451 #[test]
452 fn test_error_message_deserialization() {
453 let json = r#"{
454 "type": "error",
455 "message": "Invalid subscription",
456 "code": 400
457 }"#;
458
459 let msg: BlockScholesMessage = sonic_rs::from_str(json).unwrap();
460 match msg {
461 BlockScholesMessage::Error { message, code } => {
462 assert_eq!(message, "Invalid subscription");
463 assert_eq!(code, Some(400));
464 }
465 _ => panic!("Expected Error"),
466 }
467 }
468
469 #[test]
470 fn test_subscribed_message_deserialization() {
471 let json = r#"{
472 "type": "subscribed",
473 "channel": "vol_surface",
474 "symbol": "ETH"
475 }"#;
476
477 let msg: BlockScholesMessage = sonic_rs::from_str(json).unwrap();
478 match msg {
479 BlockScholesMessage::Subscribed { channel, symbol } => {
480 assert_eq!(channel, "vol_surface");
481 assert_eq!(symbol, "ETH");
482 }
483 _ => panic!("Expected Subscribed"),
484 }
485 }
486}