use std::{marker::PhantomData, time::SystemTime};
use generics::{
http::{BuildError, HandleError, header::HeaderValue, *},
tokio_tungstenite::tungstenite::protocol::WebSocketConfig,
};
use hmac::{Hmac, KeyInit as _, Mac};
use secrecy::{ExposeSecret as _, SecretString};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use sha2::Sha256;
use crate::traits::*;
pub type BitFlyerRequestResult<T> = Result<T, RequestError>;
#[derive(Default)]
pub enum BitFlyerOption {
#[default]
Default,
Key(String),
Secret(SecretString),
HttpUrl(BitFlyerHttpUrl),
HttpAuth(bool),
RequestConfig(RequestConfig),
WebSocketUrl(BitFlyerWebSocketUrl),
WebSocketAuth(bool),
WebSocketChannels(Vec<String>),
WebSocketConfig(WebSocketConfig),
}
#[derive(Clone, derive_more::Debug)]
pub struct BitFlyerOptions {
pub key: Option<String>,
#[debug("[REDACTED]")]
pub secret: Option<SecretString>,
pub http_url: BitFlyerHttpUrl,
pub http_auth: bool,
pub request_config: RequestConfig,
pub websocket_url: BitFlyerWebSocketUrl,
pub websocket_auth: bool,
pub websocket_channels: Vec<String>,
pub websocket_config: WebSocketConfig,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum BitFlyerHttpUrl {
Main,
#[default]
None,
}
impl BitFlyerHttpUrl {
fn as_str(&self) -> &'static str {
match self {
Self::Main => "https://api.bitflyer.com",
Self::None => "",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum BitFlyerWebSocketUrl {
Default,
None,
}
impl BitFlyerWebSocketUrl {
fn as_str(&self) -> &'static str {
match self {
Self::Default => "wss://ws.lightstream.bitflyer.com",
Self::None => "",
}
}
}
#[derive(Debug, Deserialize)]
pub struct BitFlyerChannelMessage {
pub channel: String,
pub message: serde_json::Value,
}
#[non_exhaustive]
#[derive(Debug)]
pub enum BitFlyerHandlerError {
ApiError(serde_json::Value),
ParseError,
}
pub struct BitFlyerRequestHandler<'a, R: DeserializeOwned> {
options: BitFlyerOptions,
_phantom: PhantomData<&'a R>,
}
pub struct BitFlyerWebSocketHandler {
message_handler: Box<dyn FnMut(BitFlyerChannelMessage) + Send>,
auth_id: Option<String>,
options: BitFlyerOptions,
}
impl<B, R> RequestHandler<B> for BitFlyerRequestHandler<'_, R>
where
B: Serialize,
R: DeserializeOwned,
{
type Successful = R;
fn base_url(&self, is_test: bool) -> Result<url::Url, generics::UrlError> {
match is_test {
true => unimplemented!(),
false => url::Url::parse(self.options.http_url.as_str()).map_err(generics::UrlError::Parse),
}
}
fn build_request(&self, mut builder: RequestBuilder, request_body: &Option<B>, _: u8) -> Result<Request, BuildError> {
if let Some(body) = request_body {
let json = serde_json::to_vec(body).map_err(BuildError::JsonSerialization)?;
builder = builder.header(header::CONTENT_TYPE, "application/json").body(json);
}
let mut request = builder.build().map_err(|e| BuildError::Other(eyre::eyre!("failed to build request: {e}")))?;
if self.options.http_auth {
let time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); let timestamp = time.as_millis() as u64;
let mut path = request.url().path().to_owned();
if let Some(query) = request.url().query() {
path.push('?');
path.push_str(query)
}
let body = request.body().and_then(|body| body.as_bytes()).map(String::from_utf8_lossy).unwrap_or_default();
let sign_contents = format!("{timestamp}{}{path}{body}", request.method());
let secret = self
.options
.secret
.as_ref()
.map(|s| s.expose_secret())
.ok_or(BuildError::Auth(generics::ConstructAuthError::new_missing_secret()))?;
let mut hmac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
hmac.update(sign_contents.as_bytes());
let signature = hex::encode(hmac.finalize().into_bytes());
let key = HeaderValue::from_str(self.options.key.as_deref().ok_or(BuildError::Auth(generics::ConstructAuthError::new_missing_pubkey()))?)
.map_err(|e| BuildError::Auth(generics::ConstructAuthError::new_invalid_character_in_api_key(e.to_string())))?;
let headers = request.headers_mut();
headers.insert("ACCESS-KEY", key);
headers.insert("ACCESS-TIMESTAMP", HeaderValue::from(timestamp));
headers.insert("ACCESS-SIGN", HeaderValue::from_str(&signature).unwrap()); headers.insert(header::CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap()); }
Ok(request)
}
fn handle_response(&self, status: StatusCode, _: HeaderMap, response_body: Bytes) -> Result<Self::Successful, HandleError> {
if status.is_success() {
serde_json::from_slice(&response_body).map_err(|error| {
let response_str = v_utils::utils::truncate_msg(String::from_utf8_lossy(&response_body));
HandleError::Parse(eyre::eyre!("Failed to parse response: {error}\nResponse body: {response_str}"))
})
} else {
let error = match serde_json::from_slice::<serde_json::Value>(&response_body) {
Ok(parsed_error) => HandleError::Api(ApiError::Other(eyre::eyre!("BitFlyer API error (status {status}): {parsed_error}"))),
Err(error) => {
let response_str = v_utils::utils::truncate_msg(String::from_utf8_lossy(&response_body));
HandleError::Parse(eyre::eyre!("Failed to parse error response: {error}\nResponse body: {response_str}"))
}
};
Err(error)
}
}
}
impl HandlerOptions for BitFlyerOptions {
type OptionItem = BitFlyerOption;
fn update(&mut self, option: Self::OptionItem) {
match option {
BitFlyerOption::Default => (),
BitFlyerOption::Key(v) => self.key = Some(v),
BitFlyerOption::Secret(v) => self.secret = Some(v),
BitFlyerOption::HttpUrl(v) => self.http_url = v,
BitFlyerOption::HttpAuth(v) => self.http_auth = v,
BitFlyerOption::RequestConfig(v) => self.request_config = v,
BitFlyerOption::WebSocketUrl(v) => self.websocket_url = v,
BitFlyerOption::WebSocketAuth(v) => self.websocket_auth = v,
BitFlyerOption::WebSocketChannels(v) => self.websocket_channels = v,
BitFlyerOption::WebSocketConfig(v) => self.websocket_config = v,
}
}
fn is_authenticated(&self) -> bool {
self.key.is_some() }
}
impl Default for BitFlyerOptions {
fn default() -> Self {
let websocket_config = WebSocketConfig::default();
Self {
key: None,
secret: None,
http_url: BitFlyerHttpUrl::Main,
http_auth: false,
request_config: RequestConfig::default(),
websocket_url: BitFlyerWebSocketUrl::Default,
websocket_auth: false,
websocket_channels: vec![],
websocket_config,
}
}
}
impl<'a, R, B> HttpOption<'a, R, B> for BitFlyerOption
where
R: DeserializeOwned + 'a,
B: Serialize,
{
type RequestHandler = BitFlyerRequestHandler<'a, R>;
fn request_handler(options: Self::Options) -> Self::RequestHandler {
BitFlyerRequestHandler::<'a, R> { options, _phantom: PhantomData }
}
}
impl HandlerOption for BitFlyerOption {
type Options = BitFlyerOptions;
}