mod parse;
mod private;
mod rest;
mod stream;
mod wallet;
use crate::adapter::{Adapter, BoxFuture};
use crate::error::{Error, Result};
use crate::feature::Feature;
use crate::request::{
CancelOrdersRequest, CandleRequest, DepositAddressRequest, OrderHistoryRequest,
OrderLookupRequest, OrderRequest, TransferHistoryRequest, TransferLookupRequest,
WithdrawRequest,
};
use crate::stream::{AccountStream, MarketStream};
use crate::transport::HttpTransport;
use crate::types::{
AssetNetwork, Balance, CancelOrdersResult, Candle, Cursor, Deposit, DepositAddress,
DepositAddressEntry, Exchange, Market, MarketInfo, MarketKind, Network, Order, OrderBook,
OrderRules, OrderType, Page, Side, StreamConfig, Subscription, Ticker, Timestamp, Trade,
Withdrawal, WithdrawalFee, WithdrawalQuote,
};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct BithumbKrwWithdrawalsRequest {
pub state: Option<String>,
pub uuids: Vec<String>,
pub txids: Vec<String>,
pub page: Option<u32>,
pub limit: Option<u32>,
pub order_by: Option<BithumbOrderDirection>,
}
impl BithumbKrwWithdrawalsRequest {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn state(mut self, state: impl Into<String>) -> Self {
self.state = Some(state.into());
self
}
#[must_use]
pub fn uuids(mut self, uuids: impl Into<Vec<String>>) -> Self {
self.uuids = uuids.into();
self
}
#[must_use]
pub fn txids(mut self, txids: impl Into<Vec<String>>) -> Self {
self.txids = txids.into();
self
}
#[must_use]
pub fn page(mut self, page: u32) -> Self {
self.page = Some(page);
self
}
#[must_use]
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
#[must_use]
pub fn order_by(mut self, order_by: BithumbOrderDirection) -> Self {
self.order_by = Some(order_by);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct BithumbKrwDepositsRequest {
pub state: Option<String>,
pub uuids: Vec<String>,
pub txids: Vec<String>,
pub page: Option<u32>,
pub limit: Option<u32>,
pub order_by: Option<BithumbOrderDirection>,
}
impl BithumbKrwDepositsRequest {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn state(mut self, state: impl Into<String>) -> Self {
self.state = Some(state.into());
self
}
#[must_use]
pub fn uuids(mut self, uuids: impl Into<Vec<String>>) -> Self {
self.uuids = uuids.into();
self
}
#[must_use]
pub fn txids(mut self, txids: impl Into<Vec<String>>) -> Self {
self.txids = txids.into();
self
}
#[must_use]
pub fn page(mut self, page: u32) -> Self {
self.page = Some(page);
self
}
#[must_use]
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
#[must_use]
pub fn order_by(mut self, order_by: BithumbOrderDirection) -> Self {
self.order_by = Some(order_by);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BithumbKrwTransferRequest {
pub amount: rust_decimal::Decimal,
}
impl BithumbKrwTransferRequest {
pub fn new(amount: rust_decimal::Decimal) -> Self {
Self { amount }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BithumbKrwWithdrawal {
pub transfer_type: String,
pub uuid: String,
pub currency: String,
pub net_type: Option<String>,
pub txid: Option<String>,
pub state: String,
pub created_at: Option<Timestamp>,
pub done_at: Option<Timestamp>,
pub amount: rust_decimal::Decimal,
pub fee: rust_decimal::Decimal,
pub transaction_type: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BithumbKrwDeposit {
pub transfer_type: String,
pub uuid: String,
pub currency: String,
pub net_type: Option<String>,
pub txid: Option<String>,
pub state: String,
pub created_at: Option<Timestamp>,
pub done_at: Option<Timestamp>,
pub amount: rust_decimal::Decimal,
pub fee: rust_decimal::Decimal,
pub transaction_type: Option<String>,
}
pub(crate) const REST_BASE_URL: &str = "https://api.bithumb.com";
pub(crate) const WEBSOCKET_URL: &str = "wss://ws-api.bithumb.com/websocket/v1";
pub(crate) const PRIVATE_WEBSOCKET_URL: &str = "wss://ws-api.bithumb.com/websocket/v2/private";
pub(crate) fn network_from_provider(raw: &str) -> Network {
match raw.trim().to_ascii_uppercase().as_str() {
"BTC" | "BITCOIN" => Network::Bitcoin,
"ETH" | "ETHEREUM" => Network::Ethereum,
"ARB" | "ARBITRUM" | "ARBITRUM ONE" => Network::Arbitrum,
"BSC" | "BEP20" | "BNB SMART CHAIN" => Network::BnbSmartChain,
"TRX" | "TRON" => Network::Tron,
"SOL" | "SOLANA" => Network::Solana,
"MATIC" | "POLYGON" | "POLYGON POS" => Network::Polygon,
"BASE" => Network::Base,
"OP" | "OPTIMISM" => Network::Optimism,
"AVAXC" | "AVAX-C" | "AVALANCHE C-CHAIN" => Network::AvalancheC,
"XRP" | "XRP LEDGER" => Network::XrpLedger,
"XLM" | "STELLAR" => Network::Stellar,
"ATOM" | "COSMOS" | "COSMOS HUB" => Network::Cosmos,
"APT" | "APTOS" => Network::Aptos,
"SUI" => Network::Sui,
"TON" => Network::Ton,
"NEAR" => Network::Near,
"DOT" | "POLKADOT" => Network::Polkadot,
_ => Network::Other(raw.trim().to_owned()),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum BithumbAlertStep {
Caution,
Warning,
Danger,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct BithumbMarketAlert {
pub kind: String,
pub step: BithumbAlertStep,
pub ends_at: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BithumbNotice {
pub categories: Vec<String>,
pub title: String,
pub url: String,
pub published_at: Timestamp,
pub modified_at: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BithumbApiKey {
pub access_key: String,
pub expires_at: Timestamp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BithumbPendingOrderState {
Wait,
Watch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BithumbOrderDirection {
Ascending,
Descending,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct BithumbPendingOrdersRequest {
pub market: Option<Market>,
pub state: Option<BithumbPendingOrderState>,
pub limit: Option<u32>,
pub order_by: Option<BithumbOrderDirection>,
pub cursor: Option<Cursor>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BithumbBatchOrdersRequest {
pub orders: Vec<OrderRequest>,
}
impl BithumbBatchOrdersRequest {
pub fn new(orders: impl Into<Vec<OrderRequest>>) -> Self {
Self {
orders: orders.into(),
}
}
}
impl From<Vec<OrderRequest>> for BithumbBatchOrdersRequest {
fn from(orders: Vec<OrderRequest>) -> Self {
Self::new(orders)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BithumbBatchOrder {
pub order_id: String,
pub client_order_id: Option<String>,
pub market: Market,
pub side: Side,
pub order_type: OrderType,
pub time_in_force: Option<String>,
pub stp_type: Option<String>,
pub created_at: Option<Timestamp>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BithumbBatchOrderFailure {
pub client_order_id: Option<String>,
pub time_in_force: Option<String>,
pub code: String,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BithumbBatchOrderOutcome {
Accepted(BithumbBatchOrder),
Rejected(BithumbBatchOrderFailure),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BithumbBatchOrdersResult {
pub outcomes: Vec<BithumbBatchOrderOutcome>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BithumbTwapState {
Progress,
Done,
Cancel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BithumbTwapOrderDirection {
Ascending,
Descending,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct BithumbTwapOrdersRequest {
pub market: Option<Market>,
pub uuids: Vec<String>,
pub state: Option<BithumbTwapState>,
pub cursor: Option<Cursor>,
pub limit: Option<u32>,
pub order_by: Option<BithumbTwapOrderDirection>,
}
impl BithumbTwapOrdersRequest {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn market(mut self, market: Market) -> Self {
self.market = Some(market);
self
}
#[must_use]
pub fn uuids(mut self, uuids: impl Into<Vec<String>>) -> Self {
self.uuids = uuids.into();
self
}
#[must_use]
pub fn state(mut self, state: BithumbTwapState) -> Self {
self.state = Some(state);
self
}
#[must_use]
pub fn cursor(mut self, cursor: Cursor) -> Self {
self.cursor = Some(cursor);
self
}
#[must_use]
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
#[must_use]
pub fn order_by(mut self, order_by: BithumbTwapOrderDirection) -> Self {
self.order_by = Some(order_by);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BithumbTwapOrderRequest {
pub market: Market,
pub side: Side,
pub volume: Option<rust_decimal::Decimal>,
pub price: Option<rust_decimal::Decimal>,
pub duration: u32,
pub frequency: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BithumbTwapOrder {
pub id: String,
pub side: Side,
pub price: rust_decimal::Decimal,
pub state: BithumbTwapState,
pub market: Market,
pub created_at: Timestamp,
pub volume: rust_decimal::Decimal,
pub finished_at: Option<Timestamp>,
pub total_order_count: u32,
pub total_trades_count: u32,
pub progress_count: u32,
pub total_executed_amount: rust_decimal::Decimal,
pub total_executed_volume: rust_decimal::Decimal,
pub avg_trade_price: rust_decimal::Decimal,
pub wallet_id: Option<String>,
pub canceled_at: Option<Timestamp>,
pub cancel_type: Option<String>,
}
impl BithumbPendingOrdersRequest {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn market(mut self, market: Market) -> Self {
self.market = Some(market);
self
}
#[must_use]
pub fn state(mut self, state: BithumbPendingOrderState) -> Self {
self.state = Some(state);
self
}
#[must_use]
pub fn limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
#[must_use]
pub fn order_by(mut self, order_by: BithumbOrderDirection) -> Self {
self.order_by = Some(order_by);
self
}
#[must_use]
pub fn cursor(mut self, cursor: Cursor) -> Self {
self.cursor = Some(cursor);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BithumbAssetFee {
pub display_name: String,
pub asset: String,
pub networks: Vec<BithumbNetworkFee>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BithumbNetworkFee {
pub network: Network,
pub provider_name: String,
pub deposit_fee: rust_decimal::Decimal,
pub minimum_deposit: rust_decimal::Decimal,
pub withdrawal_fee: WithdrawalFee,
pub minimum_withdrawal: rust_decimal::Decimal,
}
#[derive(Debug, Clone)]
pub struct BithumbAdapter {
credentials: Option<BithumbCredentials>,
http: Result<HttpTransport>,
}
#[derive(Debug, Clone)]
pub(crate) struct BithumbCredentials {
pub(crate) access_key: String,
pub(crate) secret_key: String,
}
impl BithumbAdapter {
pub fn new() -> Self {
Self {
credentials: None,
http: HttpTransport::new(REST_BASE_URL),
}
}
#[must_use]
pub fn with_credentials(
mut self,
access_key: impl Into<String>,
secret_key: impl Into<String>,
) -> Self {
self.credentials = Some(BithumbCredentials {
access_key: access_key.into(),
secret_key: secret_key.into(),
});
self
}
pub async fn market_warnings(&self) -> Result<Vec<(Market, String)>> {
rest::market_warnings(self.http()?).await
}
pub async fn market_alerts(&self) -> Result<Vec<(Market, BithumbMarketAlert)>> {
rest::market_alerts(self.http()?).await
}
pub async fn notices(&self, count: Option<u32>) -> Result<Vec<BithumbNotice>> {
rest::notices(self.http()?, count).await
}
pub async fn transfer_fees(&self, currency: &str) -> Result<Vec<BithumbAssetFee>> {
rest::transfer_fees(self.http()?, currency).await
}
pub async fn krw_withdrawals(
&self,
request: &BithumbKrwWithdrawalsRequest,
) -> Result<Vec<BithumbKrwWithdrawal>> {
wallet::krw_withdrawals(self.http()?, self.credentials()?, request).await
}
pub async fn withdraw_krw(
&self,
request: &BithumbKrwTransferRequest,
) -> Result<BithumbKrwWithdrawal> {
wallet::withdraw_krw(self.http()?, self.credentials()?, request).await
}
pub async fn krw_deposits(
&self,
request: &BithumbKrwDepositsRequest,
) -> Result<Vec<BithumbKrwDeposit>> {
wallet::krw_deposits(self.http()?, self.credentials()?, request).await
}
pub async fn deposit_krw(
&self,
request: &BithumbKrwTransferRequest,
) -> Result<BithumbKrwDeposit> {
wallet::deposit_krw(self.http()?, self.credentials()?, request).await
}
pub async fn api_keys(&self) -> Result<Vec<BithumbApiKey>> {
private::api_keys(self.http()?, self.credentials()?).await
}
pub async fn pending_orders(
&self,
request: &BithumbPendingOrdersRequest,
) -> Result<Page<Order>> {
private::pending_orders(self.http()?, self.credentials()?, request).await
}
pub async fn twap_orders(
&self,
request: &BithumbTwapOrdersRequest,
) -> Result<Page<BithumbTwapOrder>> {
private::twap_orders(self.http()?, self.credentials()?, request).await
}
pub async fn create_twap_order(&self, request: &BithumbTwapOrderRequest) -> Result<String> {
private::create_twap_order(self.http()?, self.credentials()?, request).await
}
pub async fn cancel_twap_order(&self, algo_order_id: &str) -> Result<String> {
private::cancel_twap_order(self.http()?, self.credentials()?, algo_order_id).await
}
pub async fn batch_orders(
&self,
request: &BithumbBatchOrdersRequest,
) -> Result<BithumbBatchOrdersResult> {
private::batch_orders(self.http()?, self.credentials()?, request).await
}
pub(crate) fn is_authenticated(&self) -> bool {
self.credentials.is_some()
}
fn credentials(&self) -> Result<&BithumbCredentials> {
self.credentials
.as_ref()
.ok_or_else(|| Error::auth("bithumb needs both an access key and a secret key"))
}
pub(crate) fn http(&self) -> Result<&HttpTransport> {
self.http.as_ref().map_err(Clone::clone)
}
}
impl Default for BithumbAdapter {
fn default() -> Self {
Self::new()
}
}
impl Adapter for BithumbAdapter {
fn exchange(&self) -> Exchange {
Exchange::Bithumb
}
fn supports(&self, feature: Feature) -> bool {
if matches!(feature, Feature::TravelRule) {
return false;
}
if feature.is_derivatives_only() {
return false;
}
if matches!(feature, Feature::CandleStream) {
return false;
}
if feature.needs_credentials() {
return self.is_authenticated();
}
true
}
fn markets(&self, kind: MarketKind) -> BoxFuture<'_, Result<Vec<MarketInfo>>> {
Box::pin(async move { rest::markets(self.http()?, kind).await })
}
fn trades(&self, market: &Market, limit: Option<u32>) -> BoxFuture<'_, Result<Vec<Trade>>> {
let market = market.clone();
Box::pin(async move { rest::trades(self.http()?, &market, limit).await })
}
fn order_book(&self, market: &Market, depth: Option<u32>) -> BoxFuture<'_, Result<OrderBook>> {
let market = market.clone();
Box::pin(async move { rest::order_book(self.http()?, &market, depth).await })
}
fn ticker(&self, market: &Market) -> BoxFuture<'_, Result<Ticker>> {
let market = market.clone();
Box::pin(async move { rest::ticker(self.http()?, &market).await })
}
fn candles(&self, request: &CandleRequest) -> BoxFuture<'_, Result<Vec<Candle>>> {
let request = request.clone();
Box::pin(async move { rest::candles(self.http()?, &request).await })
}
fn subscribe(
&self,
subscription: &Subscription,
config: &StreamConfig,
) -> BoxFuture<'_, Result<MarketStream>> {
let subscription = subscription.clone();
let config = config.clone();
Box::pin(async move { stream::subscribe(&subscription, &config).await })
}
fn subscribe_account(&self, config: &StreamConfig) -> BoxFuture<'_, Result<AccountStream>> {
let config = config.clone();
Box::pin(async move { stream::subscribe_account(self.credentials()?, &config).await })
}
fn balances(&self) -> BoxFuture<'_, Result<Vec<Balance>>> {
Box::pin(async move { private::balances(self.http()?, self.credentials()?).await })
}
fn order_rules(&self, market: &Market) -> BoxFuture<'_, Result<OrderRules>> {
let market = market.clone();
Box::pin(
async move { private::order_rules(self.http()?, self.credentials()?, &market).await },
)
}
fn asset_networks(&self, asset: &str) -> BoxFuture<'_, Result<Vec<AssetNetwork>>> {
let asset = asset.to_string();
Box::pin(
async move { wallet::asset_networks(self.http()?, self.credentials()?, &asset).await },
)
}
fn deposit_addresses(&self) -> BoxFuture<'_, Result<Vec<DepositAddressEntry>>> {
Box::pin(async move { wallet::deposit_addresses(self.http()?, self.credentials()?).await })
}
fn deposit_address(
&self,
request: &DepositAddressRequest,
) -> BoxFuture<'_, Result<DepositAddress>> {
let request = request.clone();
Box::pin(async move {
wallet::deposit_address(self.http()?, self.credentials()?, &request).await
})
}
fn create_deposit_address(
&self,
request: &DepositAddressRequest,
) -> BoxFuture<'_, Result<DepositAddress>> {
let request = request.clone();
Box::pin(async move {
wallet::create_deposit_address(self.http()?, self.credentials()?, &request).await
})
}
fn prepare_withdrawal(
&self,
request: &WithdrawRequest,
) -> BoxFuture<'_, Result<WithdrawalQuote>> {
let request = request.clone();
Box::pin(async move {
wallet::prepare_withdrawal(self.http()?, self.credentials()?, &request).await
})
}
fn withdraw(&self, request: &WithdrawRequest) -> BoxFuture<'_, Result<Withdrawal>> {
let request = request.clone();
Box::pin(async move { wallet::withdraw(self.http()?, self.credentials()?, &request).await })
}
fn deposit(&self, request: &TransferLookupRequest) -> BoxFuture<'_, Result<Deposit>> {
let request = request.clone();
Box::pin(async move { wallet::deposit(self.http()?, self.credentials()?, &request).await })
}
fn withdrawal(&self, request: &TransferLookupRequest) -> BoxFuture<'_, Result<Withdrawal>> {
let request = request.clone();
Box::pin(
async move { wallet::withdrawal(self.http()?, self.credentials()?, &request).await },
)
}
fn cancel_withdrawal(&self, withdrawal_id: &str) -> BoxFuture<'_, Result<()>> {
let withdrawal_id = withdrawal_id.to_owned();
Box::pin(async move {
wallet::cancel_withdrawal(self.http()?, self.credentials()?, &withdrawal_id).await
})
}
fn deposits(&self, request: &TransferHistoryRequest) -> BoxFuture<'_, Result<Page<Deposit>>> {
let request = request.clone();
Box::pin(async move { wallet::deposits(self.http()?, self.credentials()?, &request).await })
}
fn withdrawals(
&self,
request: &TransferHistoryRequest,
) -> BoxFuture<'_, Result<Page<Withdrawal>>> {
let request = request.clone();
Box::pin(
async move { wallet::withdrawals(self.http()?, self.credentials()?, &request).await },
)
}
fn open_orders(&self, market: Option<&Market>) -> BoxFuture<'_, Result<Vec<Order>>> {
let market = market.cloned();
Box::pin(async move {
private::open_orders(self.http()?, self.credentials()?, market.as_ref()).await
})
}
fn order(&self, market: &Market, order_id: &str) -> BoxFuture<'_, Result<Order>> {
let market = market.clone();
let order_id = order_id.to_string();
Box::pin(async move {
private::order(self.http()?, self.credentials()?, &market, &order_id).await
})
}
fn order_by_client_id(&self, market: &Market, client_id: &str) -> BoxFuture<'_, Result<Order>> {
let market = market.clone();
let client_id = client_id.to_string();
Box::pin(async move {
private::order_by_client_id(self.http()?, self.credentials()?, &market, &client_id)
.await
})
}
fn orders_by_ids(&self, request: &OrderLookupRequest) -> BoxFuture<'_, Result<Vec<Order>>> {
let request = request.clone();
Box::pin(async move {
private::orders_by_ids(self.http()?, self.credentials()?, &request).await
})
}
fn order_history(&self, request: &OrderHistoryRequest) -> BoxFuture<'_, Result<Page<Order>>> {
let request = request.clone();
Box::pin(async move {
private::order_history(self.http()?, self.credentials()?, &request).await
})
}
fn place_order(&self, request: &OrderRequest) -> BoxFuture<'_, Result<Order>> {
let request = request.clone();
Box::pin(
async move { private::place_order(self.http()?, self.credentials()?, &request).await },
)
}
fn cancel_order(&self, market: &Market, order_id: &str) -> BoxFuture<'_, Result<()>> {
let market = market.clone();
let order_id = order_id.to_string();
Box::pin(async move {
private::cancel_order(self.http()?, self.credentials()?, &market, &order_id).await
})
}
fn cancel_order_by_client_id(
&self,
market: &Market,
client_id: &str,
) -> BoxFuture<'_, Result<()>> {
let market = market.clone();
let client_id = client_id.to_string();
Box::pin(async move {
private::cancel_order_by_client_id(
self.http()?,
self.credentials()?,
&market,
&client_id,
)
.await
})
}
fn cancel_orders(
&self,
request: &CancelOrdersRequest,
) -> BoxFuture<'_, Result<CancelOrdersResult>> {
let request = request.clone();
Box::pin(async move {
private::cancel_orders(self.http()?, self.credentials()?, &request).await
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn candles_are_available_over_rest_but_not_as_a_stream() {
let adapter = BithumbAdapter::new();
assert!(adapter.supports(Feature::Candles));
assert!(!adapter.supports(Feature::CandleStream));
}
#[test]
fn every_other_public_stream_is_available() {
let adapter = BithumbAdapter::new();
for feature in [
Feature::TradeStream,
Feature::OrderBookStream,
Feature::TickerStream,
] {
assert!(adapter.supports(feature), "{feature:?}");
}
}
#[test]
fn a_spot_exchange_never_claims_derivatives_features() {
let adapter = BithumbAdapter::new().with_credentials("access", "secret");
for feature in [Feature::Positions, Feature::Margin, Feature::FundingRates] {
assert!(!adapter.supports(feature), "{feature:?}");
}
}
#[tokio::test]
async fn subscribing_to_candles_is_refused_before_a_socket_is_opened() {
use crate::types::{Feed, Interval, Market, StreamConfig, Subscription};
let subscription = Subscription::new()
.market(Market::spot(Exchange::Bithumb, "BTC", "KRW"))
.feed(Feed::Candles(Interval::Min1));
let error = BithumbAdapter::new()
.subscribe(&subscription, &StreamConfig::default())
.await
.expect_err("bithumb publishes no candle stream");
assert!(matches!(
error,
Error::Unsupported {
feature: Feature::CandleStream,
exchange: "bithumb",
..
}
));
}
#[tokio::test]
async fn a_private_call_without_credentials_is_an_auth_failure_not_a_missing_feature() {
let error = BithumbAdapter::new()
.balances()
.await
.expect_err("no credentials were supplied");
assert!(
matches!(error, Error::Auth { .. }),
"expected an auth failure, got {error:?}"
);
}
#[tokio::test]
async fn pending_orders_without_credentials_are_rejected_before_network_io() {
let error = BithumbAdapter::new()
.pending_orders(&BithumbPendingOrdersRequest::new())
.await
.expect_err("no credentials were supplied");
assert!(matches!(error, Error::Auth { .. }));
}
#[tokio::test]
async fn batch_order_writes_without_credentials_are_rejected_before_network_io() {
let request = BithumbBatchOrdersRequest::new(vec![OrderRequest::market(
Market::spot(Exchange::Bithumb, "BTC", "KRW"),
Side::Buy,
crate::types::Size::Quote(crate::Decimal::ONE),
)]);
let error = BithumbAdapter::new()
.batch_orders(&request)
.await
.expect_err("no credentials were supplied");
assert!(matches!(error, Error::Auth { .. }));
}
#[test]
fn credentials_are_what_unlock_the_private_half() {
let public = BithumbAdapter::new();
let private = BithumbAdapter::new().with_credentials("access", "secret");
assert!(!private.supports(Feature::TravelRule));
for feature in [
Feature::Balances,
Feature::AssetNetworks,
Feature::DepositAddresses,
Feature::DepositHistory,
Feature::DepositLookup,
Feature::WithdrawalQuotes,
Feature::Withdrawals,
Feature::WithdrawalHistory,
Feature::WithdrawalLookup,
Feature::WithdrawalCancellation,
Feature::Trading,
Feature::AccountStream,
] {
assert!(!public.supports(feature), "{feature:?}");
assert!(private.supports(feature), "{feature:?}");
}
}
}