crypto_botters_bitflyer/
lib.rs

1//! A crate for communicating with the [bitFlyer API](https://lightning.bitflyer.com/docs).
2//! For example usages, see files in the examples/ directory.
3
4use std::{
5    marker::PhantomData,
6    time::SystemTime,
7};
8use hmac::{Hmac, Mac};
9use sha2::Sha256;
10use rand::{Rng, distributions::Alphanumeric};
11use serde::{de::DeserializeOwned, Deserialize, Serialize};
12use serde_json::json;
13use crypto_botters_api::{HandlerOption, HandlerOptions, HttpOption, WebSocketOption};
14use generic_api_client::{http::{*, header::HeaderValue}, websocket::*};
15
16/// The type returned by [Client::request()].
17pub type BitFlyerRequestResult<T> = Result<T, BitFlyerRequestError>;
18pub type BitFlyerRequestError = RequestError<&'static str, BitFlyerHandlerError>;
19
20/// Options that can be set when creating handlers
21pub enum BitFlyerOption {
22    /// [Default] variant, does nothing
23    Default,
24    /// API key
25    Key(String),
26    /// Api secret
27    Secret(String),
28    /// Base url for HTTP requests
29    HttpUrl(BitFlyerHttpUrl),
30    /// Whether [BitFlyerRequestHandler] should perform authentication
31    HttpAuth(bool),
32    /// [RequestConfig] used when sending requests.
33    /// `url_prefix` will be overridden by [HttpUrl](Self::HttpUrl) unless `HttpUrl` is [BitFlyerHttpUrl::None].
34    RequestConfig(RequestConfig),
35    /// Base url for WebSocket connections
36    WebSocketUrl(BitFlyerWebSocketUrl),
37    /// Whether [BitFlyerWebSocketHandler] should perform authentication
38    WebSocketAuth(bool),
39    /// The channels to be subscribed by [BitFlyerWebSocketHandler].
40    WebSocketChannels(Vec<String>),
41    /// [WebSocketConfig] used for creating [WebSocketConnection]s
42    /// `url_prefix` will be overridden by [WebSocketUrl](Self::WebSocketUrl) unless `WebSocketUrl` is [BitFlyerWebSocketUrl::None].
43    /// By default, ignore_duplicate_during_reconnection` is set to `true`.
44    WebSocketConfig(WebSocketConfig),
45}
46
47/// A `struct` that represents a set of [BitFlyerOption] s.
48#[derive(Clone, Debug)]
49pub struct BitFlyerOptions {
50    /// see [BitFlyerOption::Key]
51    pub key: Option<String>,
52    /// see [BitFlyerOption::Secret]
53    pub secret: Option<String>,
54    /// see [BitFlyerOption::HttpUrl]
55    pub http_url: BitFlyerHttpUrl,
56    /// see [BitFlyerOption::HttpAuth]
57    pub http_auth: bool,
58    /// see [BitFlyerOption::RequestConfig]
59    pub request_config: RequestConfig,
60    /// see [BitFlyerOption::WebSocketUrl]
61    pub websocket_url: BitFlyerWebSocketUrl,
62    /// see [BitFlyerOption::WebSocketAuth]
63    pub websocket_auth: bool,
64    /// see [BitFlyerOptions::WebSocketChannels]
65    pub websocket_channels: Vec<String>,
66    /// see [BitFlyerOption::WebSocketConfig]
67    pub websocket_config: WebSocketConfig,
68}
69
70/// A `enum` that represents the base url of the BitFlyer HTTP API.
71#[derive(Debug, Eq, PartialEq, Copy, Clone)]
72pub enum BitFlyerHttpUrl {
73    /// https://api.bitflyer.com
74    Default,
75    /// The url will not be modified by [BitFlyerRequestHandler]
76    None,
77}
78
79/// A `enum` that represents the base url of the BitFlyer Realtime API
80#[derive(Debug, Eq, PartialEq, Copy, Clone)]
81#[non_exhaustive]
82pub enum BitFlyerWebSocketUrl {
83    /// `wss://ws.lightstream.bitflyer.com`
84    Default,
85    /// The url will not be modified by [BitFlyerWebSocketHandler]
86    None,
87}
88
89#[derive(Deserialize, Debug)]
90pub struct BitFlyerChannelMessage {
91    pub channel: String,
92    pub message: serde_json::Value,
93}
94
95#[derive(Debug)]
96pub enum BitFlyerHandlerError {
97    ApiError(serde_json::Value),
98    ParseError,
99}
100
101/// A `struct` that implements [RequestHandler]
102pub struct BitFlyerRequestHandler<'a, R: DeserializeOwned> {
103    options: BitFlyerOptions,
104    _phantom: PhantomData<&'a R>,
105}
106
107/// A `struct` that implements [WebSocketHandler]
108pub struct BitFlyerWebSocketHandler<H: FnMut(BitFlyerChannelMessage) + Send + 'static> {
109    message_handler: H,
110    auth_id: Option<String>,
111    options: BitFlyerOptions,
112}
113
114impl<'a, B, R> RequestHandler<B> for BitFlyerRequestHandler<'a, R>
115where
116    B: Serialize,
117    R: DeserializeOwned,
118{
119    type Successful = R;
120    type Unsuccessful = BitFlyerHandlerError;
121    type BuildError = &'static str;
122
123    fn request_config(&self) -> RequestConfig {
124        let mut config = self.options.request_config.clone();
125        if self.options.http_url != BitFlyerHttpUrl::None {
126            config.url_prefix = self.options.http_url.as_str().to_owned();
127        }
128        config
129    }
130
131    fn build_request(&self, mut builder: RequestBuilder, request_body: &Option<B>, _: u8) -> Result<Request, Self::BuildError> {
132        if let Some(body) = request_body {
133            let json = serde_json::to_vec(body).or(Err("could not serialize body as application/json"))?;
134            builder = builder
135                .header(header::CONTENT_TYPE, "application/json")
136                .body(json);
137        }
138
139        let mut request = builder.build().or(Err("failed to build request"))?;
140
141        if self.options.http_auth {
142            // https://lightning.bitflyer.com/docs?lang=en#authentication
143            let time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); // always after the epoch
144            let timestamp = time.as_millis() as u64;
145
146            let mut path = request.url().path().to_owned();
147            if let Some(query) = request.url().query() {
148                path.push('?');
149                path.push_str(query)
150            }
151            let body = request.body()
152                .and_then(|body| body.as_bytes())
153                .map(String::from_utf8_lossy)
154                .unwrap_or_default();
155
156            let sign_contents = format!("{}{}{}{}", timestamp, request.method(), path, body);
157
158            let secret = self.options.secret.as_deref().ok_or("API secret not set")?;
159            let mut hmac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap(); // hmac accepts key of any length
160
161            hmac.update(sign_contents.as_bytes());
162            let signature = hex::encode(hmac.finalize().into_bytes());
163
164            let key = HeaderValue::from_str(self.options.key.as_deref().ok_or("API key not set")?).or(
165                Err("invalid character in API key")
166            )?;
167            let headers = request.headers_mut();
168            headers.insert("ACCESS-KEY", key);
169            headers.insert("ACCESS-TIMESTAMP", HeaderValue::from(timestamp));
170            headers.insert("ACCESS-SIGN", HeaderValue::from_str(&signature).unwrap()); // hex digits are valid
171            headers.insert(header::CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap()); // only contains valid letters
172        }
173
174        Ok(request)
175    }
176
177    fn handle_response(&self, status: StatusCode, _: HeaderMap, response_body: Bytes) -> Result<Self::Successful, Self::Unsuccessful> {
178        if status.is_success() {
179            serde_json::from_slice(&response_body).map_err(|error| {
180                log::error!("Failed to parse response due to an error: {}", error);
181                BitFlyerHandlerError::ParseError
182            })
183        } else {
184            let error = match serde_json::from_slice(&response_body) {
185                Ok(parsed_error) => BitFlyerHandlerError::ApiError(parsed_error),
186                Err(error) => {
187                    log::error!("Failed to parse error response due to an error: {}", error);
188                    BitFlyerHandlerError::ParseError
189                }
190            };
191            Err(error)
192        }
193    }
194}
195
196impl<H> WebSocketHandler for BitFlyerWebSocketHandler<H> where H: FnMut(BitFlyerChannelMessage) + Send + 'static, {
197    fn websocket_config(&self) -> WebSocketConfig {
198        let mut config = self.options.websocket_config.clone();
199        if self.options.websocket_url != BitFlyerWebSocketUrl::None {
200            config.url_prefix = self.options.websocket_url.as_str().to_owned();
201        }
202        config
203    }
204
205    fn handle_start(&mut self) -> Vec<WebSocketMessage> {
206        if self.options.websocket_auth {
207            // https://bf-lightning-api.readme.io/docs/realtime-api-auth
208            if let Some(key) = self.options.key.as_deref() {
209                if let Some(secret) = self.options.secret.as_deref() {
210                    let time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); // always after the epoch
211                    let timestamp = time.as_millis() as u64;
212                    let nonce: String = rand::thread_rng()
213                        .sample_iter(&Alphanumeric)
214                        .take(16)
215                        .map(char::from)
216                        .collect();
217
218                    let mut hmac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap(); // hmac accepts key of any length
219
220                    hmac.update(format!("{timestamp}{nonce}").as_bytes());
221                    let signature = hex::encode(hmac.finalize().into_bytes());
222
223                    let id = format!("_auth{}", time.as_nanos());
224                    self.auth_id = Some(id.clone());
225
226                    return vec![WebSocketMessage::Text(json!({
227                        "method": "auth",
228                        "params": {
229                            "api_key": key,
230                            "timestamp": timestamp,
231                            "nonce": nonce,
232                            "signature": signature,
233                        },
234                        "id": id,
235                    }).to_string())];
236                } else {
237                    log::error!("API secret not set.");
238                };
239            } else {
240                log::error!("API key not set.");
241            };
242        }
243        self.message_subscribe()
244    }
245
246    fn handle_message(&mut self, message: WebSocketMessage) -> Vec<WebSocketMessage> {
247        #[derive(Deserialize)]
248        struct Message {
249            #[allow(dead_code)]
250            jsonrpc: String, // 2.0
251            method: Option<String>,
252            result: Option<serde_json::Value>,
253            params: Option<BitFlyerChannelMessage>,
254            id: Option<String>,
255        }
256
257        match message {
258            WebSocketMessage::Text(message) => {
259                let message: Message = match serde_json::from_str(&message) {
260                    Ok(message) => message,
261                    Err(_) => {
262                        log::warn!("Invalid JSON-RPC message received");
263                        return vec![];
264                    },
265                };
266                if self.options.websocket_auth && self.auth_id == message.id {
267                    // result of auth
268                    if message.result == Some(serde_json::Value::Bool(true)) {
269                        log::debug!("WebSocket authentication successful");
270                        return self.message_subscribe();
271                    } else {
272                        log::error!("WebSocket authentication unsuccessful");
273                    }
274                    self.auth_id = None;
275                } else if message.method.as_deref() == Some("channelMessage") {
276                    if let Some(channel_message) = message.params {
277                        (self.message_handler)(channel_message);
278                    }
279                }
280            },
281            WebSocketMessage::Binary(_) => log::warn!("Unexpected binary message received"),
282            WebSocketMessage::Ping(_) | WebSocketMessage::Pong(_) => (),
283        }
284        vec![]
285    }
286}
287
288impl<H> BitFlyerWebSocketHandler<H> where H: FnMut(BitFlyerChannelMessage) + Send + 'static, {
289    #[inline]
290    fn message_subscribe(&self) -> Vec<WebSocketMessage> {
291        self.options.websocket_channels.clone().into_iter().map(|channel| {
292            WebSocketMessage::Text(json!({ "method": "subscribe", "params": { "channel": channel } }).to_string())
293        }).collect()
294    }
295}
296
297impl BitFlyerHttpUrl {
298    /// The base URL that this variant represents.
299    #[inline(always)]
300    fn as_str(&self) -> &'static str {
301        match self {
302            Self::Default => "https://api.bitflyer.com",
303            Self::None => "",
304        }
305    }
306}
307
308impl BitFlyerWebSocketUrl {
309    /// The base URL that this variant represents.
310    #[inline(always)]
311    fn as_str(&self) -> &'static str {
312        match self {
313            Self::Default => "wss://ws.lightstream.bitflyer.com",
314            Self::None => "",
315        }
316    }
317}
318
319impl HandlerOptions for BitFlyerOptions {
320    type OptionItem = BitFlyerOption;
321
322    fn update(&mut self, option: Self::OptionItem) {
323        match option {
324            BitFlyerOption::Default => (),
325            BitFlyerOption::Key(v) => self.key = Some(v),
326            BitFlyerOption::Secret(v) => self.secret = Some(v),
327            BitFlyerOption::HttpUrl(v) => self.http_url = v,
328            BitFlyerOption::HttpAuth(v) => self.http_auth = v,
329            BitFlyerOption::RequestConfig(v) => self.request_config = v,
330            BitFlyerOption::WebSocketUrl(v) => self.websocket_url = v,
331            BitFlyerOption::WebSocketAuth(v) => self.websocket_auth = v,
332            BitFlyerOption::WebSocketChannels(v) => self.websocket_channels = v,
333            BitFlyerOption::WebSocketConfig(v) => self.websocket_config = v,
334        }
335    }
336}
337
338impl Default for BitFlyerOptions {
339    fn default() -> Self {
340        let mut websocket_config = WebSocketConfig::new();
341        websocket_config.ignore_duplicate_during_reconnection = true;
342        Self {
343            key: None,
344            secret: None,
345            http_url: BitFlyerHttpUrl::Default,
346            http_auth: false,
347            request_config: RequestConfig::default(),
348            websocket_url: BitFlyerWebSocketUrl::Default,
349            websocket_auth: false,
350            websocket_channels: vec![],
351            websocket_config,
352        }
353    }
354}
355
356impl<'a, R: DeserializeOwned + 'a> HttpOption<'a, R> for BitFlyerOption {
357    type RequestHandler = BitFlyerRequestHandler<'a, R>;
358
359    #[inline(always)]
360    fn request_handler(options: Self::Options) -> Self::RequestHandler {
361        BitFlyerRequestHandler::<'a, R> {
362            options,
363            _phantom: PhantomData,
364        }
365    }
366}
367
368impl<H: FnMut(BitFlyerChannelMessage) + Send + 'static> WebSocketOption<H> for BitFlyerOption {
369    type WebSocketHandler = BitFlyerWebSocketHandler<H>;
370
371    #[inline(always)]
372    fn websocket_handler(handler: H, options: Self::Options) -> Self::WebSocketHandler {
373        BitFlyerWebSocketHandler {
374            message_handler: handler,
375            auth_id: None,
376            options,
377        }
378    }
379}
380
381impl HandlerOption for BitFlyerOption {
382    type Options = BitFlyerOptions;
383}
384
385impl Default for BitFlyerOption {
386    fn default() -> Self {
387        Self::Default
388    }
389}