Skip to main content

rustrade_data/exchange/bitfinex/
subscription.rs

1use rustrade_integration::{Validator, error::SocketError};
2use serde::{Deserialize, Serialize};
3
4/// [`Bitfinex`](super::Bitfinex) platform event detailing the variants expected to be received
5/// while connecting and subscribing.
6///
7/// ### Raw Payload Examples
8/// See docs: <https://docs.bitfinex.com/docs/ws-general>
9/// #### Platform Status Online
10/// ``` json
11/// {
12///   "event": "info",
13///   "version": VERSION,
14///   "platform": {
15///     "status": 1
16///   }
17/// }
18/// ```
19///
20/// #### Subscription Trades Success
21/// ``` json
22/// {
23///   event: "subscribed",
24///   channel: "trades",
25///   chanId: CHANNEL_ID,
26///   symbol: "tBTCUSD"
27///   pair: "BTCUSD"
28/// }
29/// ```
30///
31/// #### Subscription Failure
32/// ``` json
33/// {
34///    "channel": "trades",
35///    "symbol": "tNOSUCH",
36///    "event": "error",
37///    "msg": ERROR_MSG,
38///    "code": ERROR_CODE
39/// }
40#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
41#[serde(tag = "event", rename_all = "lowercase")]
42pub enum BitfinexPlatformEvent {
43    #[serde(rename = "info")]
44    PlatformStatus(BitfinexPlatformStatus),
45    Subscribed(BitfinexSubResponse),
46    Error(BitfinexError),
47}
48
49impl Validator for BitfinexPlatformEvent {
50    type Error = SocketError;
51
52    fn validate(self) -> Result<Self, SocketError>
53    where
54        Self: Sized,
55    {
56        match &self {
57            BitfinexPlatformEvent::PlatformStatus(status) => match status.status {
58                Status::Operative => Ok(self),
59                Status::Maintenance => Err(SocketError::Subscribe(format!(
60                    "exchange version: {} with server_id: {} is in maintenance mode",
61                    status.api_version, status.server_id,
62                ))),
63            },
64            BitfinexPlatformEvent::Subscribed(_) => Ok(self),
65            BitfinexPlatformEvent::Error(BitfinexError {
66                msg,
67                code,
68                channel: Some(channel),
69                symbol: Some(symbol),
70            }) => Err(SocketError::Subscribe(format!(
71                "Bitfinex refused {channel} {symbol}: {msg} (code {code})"
72            ))),
73            BitfinexPlatformEvent::Error(error) => Err(SocketError::Subscribe(format!(
74                "received failure subscription response code: {} with message: {}",
75                error.code, error.msg,
76            ))),
77        }
78    }
79}
80
81/// [`Bitfinex`](super::Bitfinex) platform status message containing the server we are connecting
82/// to, the version of the API, and if it is in maintenance mode.
83///
84/// ### Raw Payload Examples
85/// See docs: <https://docs.bitfinex.com/docs/ws-general#info-messages>
86/// #### Platform Status Operative
87/// ``` json
88/// {
89///   "event": "info",
90///   "version": 2,
91///   "serverId": ""
92///   "platform": {
93///     "status": 1
94///   }
95/// }
96/// ```
97///
98/// #### Platform Status In Maintenance
99/// ``` json
100/// {
101///   "event": "info",
102///   "version": 2,
103///   "serverId": ""
104///   "platform": {
105///     "status": 0
106///   }
107/// }
108/// ```
109#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
110pub struct BitfinexPlatformStatus {
111    #[serde(rename = "version")]
112    api_version: u8,
113    #[serde(rename = "serverId")]
114    server_id: String,
115    #[serde(rename = "platform")]
116    status: Status,
117}
118
119/// [`Bitfinex`](super::Bitfinex) platform [`Status`] indicating if the API is in maintenance mode.
120///
121/// See [`BitfinexPlatformStatus`] for full raw payload examples.
122///
123/// See docs: <https://docs.bitfinex.com/docs/ws-general#info-messages>
124#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize)]
125pub enum Status {
126    Maintenance,
127    Operative,
128}
129
130/// [`Bitfinex`](super::Bitfinex) subscription success response variants for each channel.
131///
132/// ### Raw Payload Examples
133/// See docs: <https://docs.bitfinex.com/docs/ws-general>
134/// #### Subscription Trades Success
135/// ``` json
136/// {
137///   event: "subscribed",
138///   channel: "trades",
139///   chanId: CHANNEL_ID,
140///   symbol: "tBTCUSD"
141///   pair: "BTCUSD"
142/// }
143/// ```
144///
145/// #### Subscription Failure
146/// ``` json
147/// {
148///    "channel": "trades",
149///    "symbol": "tNOSUCH",
150///    "event": "error",
151///    "msg": ERROR_MSG,
152///    "code": ERROR_CODE
153/// }
154#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
155pub struct BitfinexSubResponse {
156    pub channel: String,
157    #[serde(rename = "symbol")]
158    pub market: String,
159    #[serde(rename = "chanId")]
160    pub channel_id: BitfinexChannelId,
161}
162
163/// [`Bitfinex`](super::Bitfinex) channel identifier that is used to identify the subscription
164/// associated with incoming events. See the module level "SubscriptionId" documentation notes
165/// for more details.
166///
167/// See docs: <https://docs.bitfinex.com/docs/ws-general#subscribe-to-channels>
168#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
169pub struct BitfinexChannelId(pub u32);
170
171/// [`Bitfinex`](super::Bitfinex) error message that is received if a [`BitfinexSubResponse`]
172/// indicates a WebSocket subscription failure.
173///
174/// ### Subscription Error Codes:
175/// 10300: Generic failure
176/// 10301: Already subscribed
177/// 10302: Unknown channel
178///
179/// See [`BitfinexPlatformStatus`] for full raw payload examples.
180///
181/// See docs: <https://docs.bitfinex.com/docs/ws-general>
182#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
183pub struct BitfinexError {
184    msg: String,
185    code: u32,
186    /// The channel of the subscription refused, when the error answers one.
187    #[serde(default)]
188    channel: Option<String>,
189    /// The symbol of the subscription refused, when the error answers one.
190    #[serde(default)]
191    symbol: Option<String>,
192}
193
194impl<'de> Deserialize<'de> for Status {
195    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
196    where
197        D: serde::de::Deserializer<'de>,
198    {
199        #[derive(Deserialize)]
200        struct Outer {
201            #[serde(deserialize_with = "de_status_from_integer")]
202            status: Status,
203        }
204
205        // Deserialise Outer struct
206        let Outer { status } = Outer::deserialize(deserializer)?;
207
208        Ok(status)
209    }
210}
211
212/// Deserialize an integer as a `Bitfinex` platform [`Status`].
213///
214/// 0 => [`Status::Maintenance`](Status), <br>
215/// 1 => [`Status::Operative`](Status), <br>
216/// other => [`de::Error`](serde::de::Error)
217fn de_status_from_integer<'de, D>(deserializer: D) -> Result<Status, D::Error>
218where
219    D: serde::de::Deserializer<'de>,
220{
221    match Deserialize::deserialize(deserializer)? {
222        0 => Ok(Status::Maintenance),
223        1 => Ok(Status::Operative),
224        other => Err(serde::de::Error::invalid_value(
225            serde::de::Unexpected::Signed(other),
226            &"0 or 1",
227        )),
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn test_de_bitfinex_platform_event() {
237        struct TestCase {
238            input: &'static str,
239            expected: Result<BitfinexPlatformEvent, SocketError>,
240        }
241
242        let cases = vec![
243            // TC0: platform status is online
244            TestCase {
245                input: r#"{"event": "info", "version": 2, "serverId": "5b73a436-19ca-4a15-8160-9069bdd7f181", "platform": { "status": 1 }}"#,
246                expected: Ok(BitfinexPlatformEvent::PlatformStatus(
247                    BitfinexPlatformStatus {
248                        api_version: 2,
249                        server_id: "5b73a436-19ca-4a15-8160-9069bdd7f181".to_string(),
250                        status: Status::Operative,
251                    },
252                )),
253            },
254            // TC1: platform status is offline
255            TestCase {
256                input: r#"{"event": "info", "version": 2, "serverId": "5b73a436-19ca-4a15-8160-9069bdd7f181", "platform": { "status": 0 }}"#,
257                expected: Ok(BitfinexPlatformEvent::PlatformStatus(
258                    BitfinexPlatformStatus {
259                        api_version: 2,
260                        server_id: "5b73a436-19ca-4a15-8160-9069bdd7f181".to_string(),
261                        status: Status::Maintenance,
262                    },
263                )),
264            },
265            // TC1: successful trades channel subscription
266            TestCase {
267                input: r#"{"event": "subscribed", "channel": "trades", "chanId": 2203, "symbol": "tBTCUSD", "pair": "BTCUSD"}"#,
268                expected: Ok(BitfinexPlatformEvent::Subscribed(BitfinexSubResponse {
269                    channel: "trades".to_string(),
270                    channel_id: BitfinexChannelId(2203),
271                    market: "tBTCUSD".to_owned(),
272                })),
273            },
274            // TC2: Input response is error
275            TestCase {
276                input: r#"{"event": "error", "msg": "Already subscribed", "code": 10202}"#,
277                expected: Ok(BitfinexPlatformEvent::Error(BitfinexError {
278                    msg: "Already subscribed".to_owned(),
279                    code: 10202,
280                    channel: None,
281                    symbol: None,
282                })),
283            },
284            // TC3: Input response is an error naming the subscription it refuses
285            TestCase {
286                input: r#"{"channel": "trades", "symbol": "tNOSUCH", "event": "error", "msg": "symbol: invalid", "code": 10300, "pair": "NOSUCH"}"#,
287                expected: Ok(BitfinexPlatformEvent::Error(BitfinexError {
288                    msg: "symbol: invalid".to_owned(),
289                    code: 10300,
290                    channel: Some("trades".to_owned()),
291                    symbol: Some("tNOSUCH".to_owned()),
292                })),
293            },
294        ];
295
296        for (index, test) in cases.into_iter().enumerate() {
297            let actual = serde_json::from_str::<BitfinexPlatformEvent>(test.input);
298            match (actual, test.expected) {
299                (Ok(actual), Ok(expected)) => {
300                    assert_eq!(actual, expected, "TC{} failed", index)
301                }
302                (Err(_), Err(_)) => {
303                    // Test passed
304                }
305                (actual, expected) => {
306                    // Test failed
307                    panic!(
308                        "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
309                    );
310                }
311            }
312        }
313    }
314
315    #[test]
316    fn test_bitfinex_platform_sub_response_validate() {
317        struct TestCase {
318            input: BitfinexPlatformEvent,
319            expected: Result<BitfinexPlatformEvent, SocketError>,
320        }
321
322        let tests = vec![
323            TestCase {
324                // TC0: bitfinex server is offline
325                input: BitfinexPlatformEvent::PlatformStatus(BitfinexPlatformStatus {
326                    api_version: 2,
327                    server_id: "server_id".to_string(),
328                    status: Status::Maintenance,
329                }),
330                expected: Err(SocketError::Subscribe(format!(
331                    "exchange version: {} with server_id: {} is in maintenance mode",
332                    2, "server_id",
333                ))),
334            },
335            TestCase {
336                // TC1: bitfinex server is online
337                input: BitfinexPlatformEvent::PlatformStatus(BitfinexPlatformStatus {
338                    api_version: 2,
339                    server_id: "server_id".to_string(),
340                    status: Status::Operative,
341                }),
342                expected: Ok(BitfinexPlatformEvent::PlatformStatus(
343                    BitfinexPlatformStatus {
344                        api_version: 2,
345                        server_id: "server_id".to_string(),
346                        status: Status::Operative,
347                    },
348                )),
349            },
350            TestCase {
351                // TC2: subscription success
352                input: BitfinexPlatformEvent::Subscribed(BitfinexSubResponse {
353                    channel: "channel".to_string(),
354                    market: "market".to_string(),
355                    channel_id: BitfinexChannelId(1),
356                }),
357                expected: Ok(BitfinexPlatformEvent::Subscribed(BitfinexSubResponse {
358                    channel: "channel".to_string(),
359                    market: "market".to_string(),
360                    channel_id: BitfinexChannelId(1),
361                })),
362            },
363            TestCase {
364                // TC3: subscription error
365                input: BitfinexPlatformEvent::Error(BitfinexError {
366                    msg: "error message".to_string(),
367                    code: 0,
368                    channel: None,
369                    symbol: None,
370                }),
371                expected: Err(SocketError::Subscribe(format!(
372                    "received failure subscription response code: {} with message: {}",
373                    0, "error message",
374                ))),
375            },
376        ];
377
378        for (index, test) in tests.into_iter().enumerate() {
379            let actual = test.input.validate();
380            match (actual, test.expected) {
381                (Ok(actual), Ok(expected)) => {
382                    assert_eq!(actual, expected, "TC{} failed", index)
383                }
384                (Err(_), Err(_)) => {
385                    // Test passed
386                }
387                (actual, expected) => {
388                    // Test failed
389                    panic!(
390                        "TC{index} failed because actual != expected. \nActual: {actual:?}\nExpected: {expected:?}\n"
391                    );
392                }
393            }
394        }
395    }
396}