mod parse;
mod pockets;
mod private;
mod rest;
mod stream;
mod travel_rule;
mod wallet;
use std::collections::HashMap;
use std::fmt;
use std::pin::Pin;
use std::sync::{Arc, Mutex, Weak};
use std::task::{Context, Poll};
use futures_core::Stream;
use futures_util::StreamExt;
use rust_decimal::Decimal;
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, TypedStream};
use crate::transport::{HttpTransport, WsCommand, WsConnect, WsSession, ws};
use crate::types::{
AccountEvent, AssetNetwork, Balance, CancelOrdersResult, Candle, Deposit, DepositAddress,
DepositAddressEntry, Exchange, Market, MarketEvent, MarketInfo, MarketKind, Network, Order,
OrderBook, OrderRules, Page, Side, StreamConfig, Subscription, Ticker, TimeInForce, Timestamp,
Trade, TransferDestination, Withdrawal, WithdrawalQuote,
};
pub use stream::{
ListedSubscription as UpbitListedSubscription, SubscriptionList as UpbitSubscriptionList,
};
pub use travel_rule::{UpbitTravelRuleVasp, UpbitTravelRuleVerification};
pub use wallet::UpbitWithdrawalAddress;
#[derive(Clone, PartialEq, Eq, Hash)]
struct SubscriptionKey {
markets: Vec<Market>,
feeds: Vec<crate::types::Feed>,
}
impl From<&Subscription> for SubscriptionKey {
fn from(subscription: &Subscription) -> Self {
Self {
markets: subscription.markets().to_vec(),
feeds: subscription.feeds().to_vec(),
}
}
}
#[derive(Default)]
struct ActiveSubscriptions {
connections: Mutex<HashMap<SubscriptionKey, Vec<Weak<stream::SubscriptionControl>>>>,
}
impl std::fmt::Debug for ActiveSubscriptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ActiveSubscriptions")
.finish_non_exhaustive()
}
}
impl ActiveSubscriptions {
fn register(&self, subscription: SubscriptionKey, control: &Arc<stream::SubscriptionControl>) {
self.connections
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.entry(subscription)
.or_default()
.push(Arc::downgrade(control));
}
fn control(&self, subscription: &Subscription) -> Result<Arc<stream::SubscriptionControl>> {
let key = SubscriptionKey::from(subscription);
let live = {
let mut connections = self
.connections
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let Some(controls) = connections.get_mut(&key) else {
return Err(Error::invalid_request(
"subscription",
"no active Upbit connection matches this subscription",
));
};
controls.retain(|control| control.strong_count() > 0);
let live = controls
.iter()
.filter_map(Weak::upgrade)
.collect::<Vec<_>>();
if controls.is_empty() {
connections.remove(&key);
}
live
};
match live.as_slice() {
[control] => Ok(Arc::clone(control)),
[] => Err(Error::invalid_request(
"subscription",
"no active Upbit connection matches this subscription",
)),
_ => Err(Error::invalid_request(
"subscription",
"more than one active Upbit connection matches this subscription",
)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum UpbitRegion {
#[default]
Korea,
Singapore,
Indonesia,
Thailand,
}
impl UpbitRegion {
pub(crate) const fn rest_base_url(self) -> &'static str {
match self {
Self::Korea => "https://api.upbit.com",
Self::Singapore => "https://sg-api.upbit.com",
Self::Indonesia => "https://id-api.upbit.com",
Self::Thailand => "https://th-api.upbit.com",
}
}
pub(crate) const fn websocket_url(self) -> &'static str {
match self {
Self::Korea => "wss://api.upbit.com/websocket/v1",
Self::Singapore => "wss://sg-api.upbit.com/websocket/v1",
Self::Indonesia => "wss://id-api.upbit.com/websocket/v1",
Self::Thailand => "wss://th-api.upbit.com/websocket/v1",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct UpbitMarketEvent {
pub warning: bool,
pub cautions: Vec<String>,
}
pub struct UpbitMarketStream {
inner: TypedStream<UpbitMarketStreamEvent>,
}
impl UpbitMarketStream {
fn new_with_close<F, Fut>(
inner: impl Stream<Item = Result<UpbitMarketStreamEvent>> + Send + 'static,
close: F,
) -> Self
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = Result<()>> + Send + 'static,
{
Self {
inner: TypedStream::new_with_close(inner, close),
}
}
pub async fn close(&mut self) -> Result<()> {
self.inner.close().await
}
}
impl Stream for UpbitMarketStream {
type Item = Result<UpbitMarketStreamEvent>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Pin::new(&mut self.inner).poll_next(cx)
}
}
impl fmt::Debug for UpbitMarketStream {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("UpbitMarketStream").finish_non_exhaustive()
}
}
pub struct UpbitAccountStream {
inner: TypedStream<UpbitAccountStreamEvent>,
}
impl UpbitAccountStream {
fn new_with_close<F, Fut>(
inner: impl Stream<Item = Result<UpbitAccountStreamEvent>> + Send + 'static,
close: F,
) -> Self
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = Result<()>> + Send + 'static,
{
Self {
inner: TypedStream::new_with_close(inner, close),
}
}
pub async fn close(&mut self) -> Result<()> {
self.inner.close().await
}
}
impl Stream for UpbitAccountStream {
type Item = Result<UpbitAccountStreamEvent>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Pin::new(&mut self.inner).poll_next(cx)
}
}
impl fmt::Debug for UpbitAccountStream {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("UpbitAccountStream").finish_non_exhaustive()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum UpbitMarketStreamEvent {
Trade(UpbitTradeStreamEvent),
OrderBook(UpbitOrderBookStreamEvent),
Ticker(UpbitTickerStreamEvent),
Candle(UpbitCandleStreamEvent),
Reconnected,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum UpbitAccountStreamEvent {
Asset(UpbitAssetStreamEvent),
Order(UpbitOrderStreamEvent),
Reconnected,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UpbitTradeStreamEvent {
pub common: Trade,
pub previous_closing_price: Option<Decimal>,
pub change: Option<String>,
pub change_price: Option<Decimal>,
pub best_ask_price: Option<Decimal>,
pub best_ask_size: Option<Decimal>,
pub best_bid_price: Option<Decimal>,
pub best_bid_size: Option<Decimal>,
pub raw_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UpbitOrderBookStreamEvent {
pub common: OrderBook,
pub total_ask_size: Option<Decimal>,
pub total_bid_size: Option<Decimal>,
pub level: Option<Decimal>,
pub stream_type: Option<String>,
pub raw_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UpbitTickerStreamEvent {
pub common: Ticker,
pub change_direction: Option<String>,
pub market_state: Option<String>,
pub trading_suspended: Option<bool>,
pub delisting_date: Option<String>,
pub market_warning: Option<String>,
pub raw_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UpbitCandleStreamEvent {
pub common: Candle,
pub stream_type: Option<String>,
pub published_at: Option<Timestamp>,
pub raw_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UpbitAssetStreamEvent {
pub balances: Vec<Balance>,
pub asset_uuid: Option<String>,
pub asset_timestamp: Option<Timestamp>,
pub published_at: Option<Timestamp>,
pub raw_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UpbitOrderStreamEvent {
pub common: Order,
pub order_type: Option<String>,
pub trade_uuid: Option<String>,
pub time_in_force: Option<String>,
pub trade_timestamp: Option<Timestamp>,
pub trade_fee: Option<Decimal>,
pub is_maker: Option<bool>,
pub raw_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpbitYearCandle {
pub market: Market,
pub open_time: crate::types::Timestamp,
pub korea_open_time: Option<crate::types::Timestamp>,
pub timestamp: crate::types::Timestamp,
pub open: Decimal,
pub high: Decimal,
pub low: Decimal,
pub close: Decimal,
pub volume: Decimal,
pub quote_volume: Decimal,
pub first_day_of_period: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpbitOrderBookInstrument {
pub market: Market,
pub quote_currency: String,
pub tick_size: Decimal,
pub supported_levels: Vec<Decimal>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpbitOrderDetailRequest {
pub market: Market,
pub uuid: Option<String>,
pub identifier: Option<String>,
}
impl UpbitOrderDetailRequest {
pub fn new(market: Market) -> Self {
Self {
market,
uuid: None,
identifier: None,
}
}
pub fn by_uuid(market: Market, uuid: impl Into<String>) -> Self {
Self::new(market).uuid(uuid)
}
pub fn by_identifier(market: Market, identifier: impl Into<String>) -> Self {
Self::new(market).identifier(identifier)
}
#[must_use]
pub fn uuid(mut self, uuid: impl Into<String>) -> Self {
self.uuid = Some(uuid.into());
self
}
#[must_use]
pub fn identifier(mut self, identifier: impl Into<String>) -> Self {
self.identifier = Some(identifier.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpbitClosedOrderState {
Done,
Cancel,
}
impl UpbitClosedOrderState {
const fn wire_name(self) -> &'static str {
match self {
Self::Done => "done",
Self::Cancel => "cancel",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct UpbitClosedOrdersRequest {
pub market: Option<Market>,
pub state: Option<UpbitClosedOrderState>,
pub states: Vec<UpbitClosedOrderState>,
pub start_time: Option<crate::types::Timestamp>,
pub end_time: Option<crate::types::Timestamp>,
pub limit: Option<u32>,
pub order_by: Option<UpbitOrderDirection>,
}
impl UpbitClosedOrdersRequest {
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: UpbitClosedOrderState) -> Self {
self.state = Some(state);
self
}
#[must_use]
pub fn states(mut self, states: impl Into<Vec<UpbitClosedOrderState>>) -> Self {
self.states = states.into();
self
}
#[must_use]
pub fn start_time(mut self, start_time: crate::types::Timestamp) -> Self {
self.start_time = Some(start_time);
self
}
#[must_use]
pub fn end_time(mut self, end_time: crate::types::Timestamp) -> Self {
self.end_time = Some(end_time);
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: UpbitOrderDirection) -> Self {
self.order_by = Some(order_by);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UpbitClosedOrder {
pub market: Market,
pub uuid: String,
pub side: String,
pub ord_type: String,
pub state: String,
pub created_at: crate::types::Timestamp,
pub volume: Option<Decimal>,
pub price: Option<Decimal>,
pub remaining_volume: Decimal,
pub executed_volume: Decimal,
pub executed_funds: Option<Decimal>,
pub reserved_fee: Decimal,
pub remaining_fee: Decimal,
pub paid_fee: Decimal,
pub locked: Decimal,
pub trades_count: u32,
pub prevented_volume: Decimal,
pub prevented_locked: Decimal,
pub time_in_force: Option<String>,
pub identifier: Option<String>,
pub smp_type: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UpbitOrderDetailTrade {
pub market: Market,
pub uuid: String,
pub price: Decimal,
pub volume: Decimal,
pub funds: Decimal,
pub trend: String,
pub created_at: crate::types::Timestamp,
pub side: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UpbitOrderDetail {
pub market: Market,
pub uuid: String,
pub side: String,
pub order_type: String,
pub price: Option<Decimal>,
pub state: String,
pub created_at: crate::types::Timestamp,
pub volume: Option<Decimal>,
pub remaining_volume: Decimal,
pub executed_volume: Decimal,
pub reserved_fee: Decimal,
pub remaining_fee: Decimal,
pub paid_fee: Decimal,
pub locked: Decimal,
pub trades_count: u32,
pub prevented_volume: Decimal,
pub prevented_locked: Decimal,
pub time_in_force: Option<String>,
pub identifier: Option<String>,
pub smp_type: Option<String>,
pub trades: Vec<UpbitOrderDetailTrade>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UpbitOrderResponse {
pub common: Order,
pub order_type: Option<String>,
pub volume: Option<Decimal>,
pub reserved_fee: Option<Decimal>,
pub remaining_fee: Option<Decimal>,
pub paid_fee: Option<Decimal>,
pub locked: Option<Decimal>,
pub trades_count: Option<u32>,
pub prevented_volume: Option<Decimal>,
pub prevented_locked: Option<Decimal>,
pub time_in_force: Option<String>,
pub identifier: Option<String>,
pub smp_type: Option<String>,
pub raw_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UpbitDepositResponse {
pub common: Deposit,
pub raw_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UpbitWithdrawalResponse {
pub common: Withdrawal,
pub raw_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UpbitCancelWithdrawalResponse {
pub withdrawal_id: String,
pub raw_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UpbitCancelOrdersResponse {
pub common: CancelOrdersResult,
pub raw_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpbitDepositInfo {
pub asset: String,
pub network: Option<Network>,
pub provider_network: Option<String>,
pub is_deposit_possible: bool,
pub deposit_impossible_reason: Option<String>,
pub minimum_deposit_amount: Decimal,
pub minimum_deposit_confirmations: u64,
pub decimal_precision: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpbitKrwTwoFactorType {
Kakao,
Naver,
Hana,
}
impl UpbitKrwTwoFactorType {
pub(crate) const fn wire_name(self) -> &'static str {
match self {
Self::Kakao => "kakao",
Self::Naver => "naver",
Self::Hana => "hana",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpbitKrwTransferRequest {
pub amount: Decimal,
pub two_factor_type: UpbitKrwTwoFactorType,
}
impl UpbitKrwTransferRequest {
pub fn new(amount: Decimal, two_factor_type: UpbitKrwTwoFactorType) -> Self {
Self {
amount,
two_factor_type,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpbitKrwDeposit {
pub transfer_type: String,
pub uuid: String,
pub currency: String,
pub net_type: Option<String>,
pub txid: String,
pub state: String,
pub created_at: crate::types::Timestamp,
pub done_at: Option<crate::types::Timestamp>,
pub amount: Decimal,
pub fee: Decimal,
pub transaction_type: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpbitKrwWithdrawal {
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: crate::types::Timestamp,
pub done_at: Option<crate::types::Timestamp>,
pub amount: Decimal,
pub fee: Decimal,
pub transaction_type: String,
pub is_cancelable: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpbitApiKey {
pub access_key: String,
pub expires_at: crate::types::Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpbitPocket {
pub uuid: String,
pub name: String,
pub kind: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpbitPocketApiKey {
pub access_key: String,
pub permissions: Vec<String>,
pub allowed_ips: Vec<String>,
pub created_at: crate::types::Timestamp,
pub expired_at: crate::types::Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpbitPocketApiKeyGroup {
pub uuid: String,
pub keys: Vec<UpbitPocketApiKey>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct UpbitPocketApiKeysRequest {
pub uuids: Vec<String>,
pub include_expired: bool,
}
impl UpbitPocketApiKeysRequest {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn uuids(mut self, uuids: impl Into<Vec<String>>) -> Self {
self.uuids = uuids.into();
self
}
#[must_use]
pub fn include_expired(mut self) -> Self {
self.include_expired = true;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpbitPocketBalance {
pub currency: String,
pub balance: Decimal,
pub locked: Decimal,
pub avg_buy_price: Decimal,
pub avg_buy_price_modified: bool,
pub unit_currency: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpbitPocketTransferState {
Submitted,
Processing,
Done,
Failed,
}
impl UpbitPocketTransferState {
pub(crate) const fn wire_name(self) -> &'static str {
match self {
Self::Submitted => "submitted",
Self::Processing => "processing",
Self::Done => "done",
Self::Failed => "failed",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpbitPocketTransferDirection {
Incoming,
Outgoing,
All,
}
impl UpbitPocketTransferDirection {
pub(crate) const fn wire_name(self) -> &'static str {
match self {
Self::Incoming => "in",
Self::Outgoing => "out",
Self::All => "all",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpbitPocketTransferOrder {
Ascending,
Descending,
}
impl UpbitPocketTransferOrder {
pub(crate) const fn wire_name(self) -> &'static str {
match self {
Self::Ascending => "asc",
Self::Descending => "desc",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct UpbitPocketTransferQuery {
pub from: Option<String>,
pub to: Option<String>,
pub direction: Option<UpbitPocketTransferDirection>,
pub states: Vec<UpbitPocketTransferState>,
pub uuids: Vec<String>,
pub identifiers: Vec<String>,
pub start_time: Option<crate::types::Timestamp>,
pub end_time: Option<crate::types::Timestamp>,
pub currency: Option<String>,
pub limit: Option<u32>,
pub order_by: Option<UpbitPocketTransferOrder>,
}
impl UpbitPocketTransferQuery {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from(mut self, value: impl Into<String>) -> Self {
self.from = Some(value.into());
self
}
#[must_use]
pub fn to(mut self, value: impl Into<String>) -> Self {
self.to = Some(value.into());
self
}
#[must_use]
pub fn direction(mut self, value: UpbitPocketTransferDirection) -> Self {
self.direction = Some(value);
self
}
#[must_use]
pub fn states(mut self, values: impl Into<Vec<UpbitPocketTransferState>>) -> Self {
self.states = values.into();
self
}
#[must_use]
pub fn uuids(mut self, values: impl Into<Vec<String>>) -> Self {
self.uuids = values.into();
self
}
#[must_use]
pub fn identifiers(mut self, values: impl Into<Vec<String>>) -> Self {
self.identifiers = values.into();
self
}
#[must_use]
pub fn start_time(mut self, value: crate::types::Timestamp) -> Self {
self.start_time = Some(value);
self
}
#[must_use]
pub fn end_time(mut self, value: crate::types::Timestamp) -> Self {
self.end_time = Some(value);
self
}
#[must_use]
pub fn currency(mut self, value: impl Into<String>) -> Self {
self.currency = Some(value.into());
self
}
#[must_use]
pub fn limit(mut self, value: u32) -> Self {
self.limit = Some(value);
self
}
#[must_use]
pub fn order_by(mut self, value: UpbitPocketTransferOrder) -> Self {
self.order_by = Some(value);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpbitPocketUniversalTransferRequest {
pub from: Option<String>,
pub to: String,
pub currency: String,
pub amount: Decimal,
pub identifier: Option<String>,
}
impl UpbitPocketUniversalTransferRequest {
pub fn new(to: impl Into<String>, currency: impl Into<String>, amount: Decimal) -> Self {
Self {
from: None,
to: to.into(),
currency: currency.into(),
amount,
identifier: None,
}
}
#[must_use]
pub fn from(mut self, value: impl Into<String>) -> Self {
self.from = Some(value.into());
self
}
#[must_use]
pub fn identifier(mut self, value: impl Into<String>) -> Self {
self.identifier = Some(value.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpbitPocketTransferRequest {
pub to: String,
pub currency: String,
pub amount: Decimal,
pub identifier: Option<String>,
}
impl UpbitPocketTransferRequest {
pub fn new(to: impl Into<String>, currency: impl Into<String>, amount: Decimal) -> Self {
Self {
to: to.into(),
currency: currency.into(),
amount,
identifier: None,
}
}
#[must_use]
pub fn identifier(mut self, value: impl Into<String>) -> Self {
self.identifier = Some(value.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpbitPocketTransfer {
pub uuid: String,
pub identifier: Option<String>,
pub from: String,
pub to: String,
pub state: String,
pub currency: String,
pub amount: Decimal,
pub created_at: crate::types::Timestamp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpbitOrderDirection {
Ascending,
Descending,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UpbitBatchCancelScope {
All,
QuoteCurrencies {
values: Vec<String>,
},
Pairs {
values: Vec<Market>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpbitBatchCancelRequest {
pub scope: UpbitBatchCancelScope,
pub excluded_pairs: Option<Vec<Market>>,
pub side: Option<Side>,
pub count: Option<u32>,
pub order_by: Option<UpbitOrderDirection>,
}
impl UpbitBatchCancelRequest {
pub fn new(scope: UpbitBatchCancelScope) -> Self {
Self {
scope,
excluded_pairs: None,
side: None,
count: None,
order_by: None,
}
}
#[must_use]
pub fn excluded_pairs(mut self, pairs: impl Into<Vec<Market>>) -> Self {
self.excluded_pairs = Some(pairs.into());
self
}
#[must_use]
pub fn side(mut self, side: Side) -> Self {
self.side = Some(side);
self
}
#[must_use]
pub fn count(mut self, count: u32) -> Self {
self.count = Some(count);
self
}
#[must_use]
pub fn order_by(mut self, order_by: UpbitOrderDirection) -> Self {
self.order_by = Some(order_by);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UpbitOrderReference {
Uuid(String),
Identifier(String),
}
impl UpbitOrderReference {
pub fn uuid(value: impl Into<String>) -> Self {
Self::Uuid(value.into())
}
pub fn identifier(value: impl Into<String>) -> Self {
Self::Identifier(value.into())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UpbitOrderVolume {
Amount(Decimal),
RemainOnly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpbitSmpType {
CancelMaker,
CancelTaker,
Reduce,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UpbitCancelAndNewOrder {
Limit {
volume: UpbitOrderVolume,
price: Decimal,
time_in_force: Option<TimeInForce>,
},
MarketBuy {
price: Decimal,
},
MarketSell {
volume: UpbitOrderVolume,
},
BestBuy {
price: Decimal,
time_in_force: TimeInForce,
},
BestSell {
volume: UpbitOrderVolume,
time_in_force: TimeInForce,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpbitCancelAndNewOrderRequest {
pub previous_order: UpbitOrderReference,
pub new_order: UpbitCancelAndNewOrder,
pub new_identifier: Option<String>,
pub new_smp_type: Option<UpbitSmpType>,
}
impl UpbitCancelAndNewOrderRequest {
pub fn new(previous_order: UpbitOrderReference, new_order: UpbitCancelAndNewOrder) -> Self {
Self {
previous_order,
new_order,
new_identifier: None,
new_smp_type: None,
}
}
#[must_use]
pub fn new_identifier(mut self, value: impl Into<String>) -> Self {
self.new_identifier = Some(value.into());
self
}
#[must_use]
pub fn new_smp_type(mut self, value: UpbitSmpType) -> Self {
self.new_smp_type = Some(value);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpbitCancelAndNewOrderResult {
pub previous_order: Order,
pub new_order_uuid: Option<String>,
pub new_order_identifier: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UpbitCancelAndNewOrderDetailResult {
pub common: UpbitCancelAndNewOrderResult,
pub previous_order: UpbitOrderResponse,
pub raw_json: String,
}
impl UpbitCancelAndNewOrderResult {
pub fn replacement_created(&self) -> bool {
self.new_order_uuid.is_some()
}
}
#[derive(Debug, Clone)]
pub struct UpbitAdapter {
region: UpbitRegion,
credentials: Option<UpbitCredentials>,
http: std::result::Result<HttpTransport, Error>,
active_subscriptions: Arc<ActiveSubscriptions>,
}
#[derive(Debug, Clone)]
pub(crate) struct UpbitCredentials {
pub(crate) access_key: String,
pub(crate) secret_key: String,
}
impl UpbitCredentials {
fn validate(&self) -> Result<()> {
if self.access_key.trim().is_empty() || self.secret_key.trim().is_empty() {
return Err(Error::auth(
"upbit needs both an access key and a secret key",
));
}
Ok(())
}
}
impl UpbitAdapter {
pub fn new() -> Self {
Self::with_region(UpbitRegion::Korea)
}
pub fn with_region(region: UpbitRegion) -> Self {
Self {
region,
credentials: None,
http: HttpTransport::new(region.rest_base_url()),
active_subscriptions: Arc::default(),
}
}
#[must_use]
pub fn with_credentials(
mut self,
access_key: impl Into<String>,
secret_key: impl Into<String>,
) -> Self {
self.credentials = Some(UpbitCredentials {
access_key: access_key.into(),
secret_key: secret_key.into(),
});
self
}
pub fn region(&self) -> UpbitRegion {
self.region
}
pub async fn order_books(
&self,
markets: &[Market],
depth: Option<u32>,
) -> Result<Vec<OrderBook>> {
rest::order_books(self.http()?, markets, depth).await
}
pub async fn order_books_at_level(
&self,
markets: &[Market],
level: Decimal,
depth: Option<u32>,
) -> Result<Vec<OrderBook>> {
if self.region != UpbitRegion::Korea {
return Err(Error::unsupported(
Feature::OrderBook,
Exchange::Upbit.id(),
"order-book aggregation levels are available only in the Upbit Korea region",
));
}
rest::order_books_at_level(self.http()?, markets, level, depth).await
}
pub async fn tickers(&self, markets: &[Market]) -> Result<Vec<Ticker>> {
rest::tickers(self.http()?, markets).await
}
pub async fn tickers_by_quote(&self, quote_currencies: &[String]) -> Result<Vec<Ticker>> {
rest::tickers_by_quote(self.http()?, quote_currencies).await
}
pub async fn year_candles(
&self,
market: &Market,
to: Option<crate::types::Timestamp>,
count: Option<u32>,
) -> Result<Vec<UpbitYearCandle>> {
rest::year_candles(self.http()?, market, to, count).await
}
pub async fn orderbook_instruments(
&self,
markets: &[Market],
) -> Result<Vec<UpbitOrderBookInstrument>> {
rest::orderbook_instruments(self.http()?, markets).await
}
pub async fn market_events(&self) -> Result<Vec<(Market, UpbitMarketEvent)>> {
rest::market_events(self.http()?).await
}
pub async fn subscribe_detailed(
&self,
subscription: &Subscription,
) -> Result<UpbitMarketStream> {
self.subscribe_detailed_with(subscription, &crate::client::default_stream_config())
.await
}
pub async fn subscribe_detailed_with(
&self,
subscription: &Subscription,
config: &StreamConfig,
) -> Result<UpbitMarketStream> {
let frame = stream::subscribe_frame(subscription, &ticket())?;
let session = ws::connect(
WsConnect {
url: self.region.websocket_url().to_string(),
headers: None,
subscribe: WsConnect::fixed(vec![frame]),
heartbeat: Some(stream::HEARTBEAT),
},
config,
)
.await?;
let close = session.close_handle();
let control = Arc::new(stream::SubscriptionControl::new(session.send_handle()));
self.active_subscriptions
.register(SubscriptionKey::from(subscription), &control);
Ok(UpbitMarketStream::new_with_close(
controlled_detailed_market_events(session, control, stream::DetailedDecoder::default()),
move || async move { close.close().await },
))
}
pub async fn subscribe_detailed_account(&self) -> Result<UpbitAccountStream> {
self.subscribe_detailed_account_with(&crate::client::default_stream_config())
.await
}
pub async fn subscribe_detailed_account_with(
&self,
config: &StreamConfig,
) -> Result<UpbitAccountStream> {
let credentials = self.credentials()?.clone();
let session = ws::connect(
WsConnect {
url: format!("{}/private", self.region.websocket_url()),
headers: Some(Box::new(move || {
Ok(vec![(
private::AUTHORIZATION.to_string(),
private::authorization(&credentials, "")?,
)])
})),
subscribe: WsConnect::fixed(vec![private::subscribe_frame(&ticket())?]),
heartbeat: Some(stream::HEARTBEAT),
},
config,
)
.await?;
let close = session.close_handle();
Ok(UpbitAccountStream::new_with_close(
events(
session,
private::detailed_account_events,
UpbitAccountStreamEvent::Reconnected,
),
move || async move { close.close().await },
))
}
pub async fn list_subscriptions(
&self,
subscription: &Subscription,
) -> Result<UpbitSubscriptionList> {
self.active_subscriptions
.control(subscription)?
.list_subscriptions()
.await
}
pub async fn test_order(&self, request: &OrderRequest) -> Result<Order> {
private::test_order(self.credentials()?, self.http()?, request).await
}
pub async fn test_order_detail(&self, request: &OrderRequest) -> Result<UpbitOrderResponse> {
private::test_order_detail(self.credentials()?, self.http()?, request).await
}
pub async fn place_order_detail(&self, request: &OrderRequest) -> Result<UpbitOrderResponse> {
private::place_order_detail(self.credentials()?, self.http()?, request).await
}
pub async fn cancel_order_detail(
&self,
market: &Market,
order_id: &str,
) -> Result<UpbitOrderResponse> {
private::cancel_order_detail(self.credentials()?, self.http()?, market, order_id).await
}
pub async fn cancel_order_by_client_id_detail(
&self,
market: &Market,
client_id: &str,
) -> Result<UpbitOrderResponse> {
private::cancel_order_by_client_id_detail(
self.credentials()?,
self.http()?,
market,
client_id,
)
.await
}
pub async fn orders_by_ids_detail(
&self,
request: &OrderLookupRequest,
) -> Result<Vec<UpbitOrderResponse>> {
private::orders_by_ids_detail(self.credentials()?, self.http()?, request).await
}
pub async fn cancel_orders_detail(
&self,
request: &CancelOrdersRequest,
) -> Result<UpbitCancelOrdersResponse> {
private::cancel_orders_detail(self.credentials()?, self.http()?, request).await
}
pub async fn order_detail(
&self,
request: &UpbitOrderDetailRequest,
) -> Result<UpbitOrderDetail> {
private::order_detail(self.credentials()?, self.http()?, request).await
}
pub async fn closed_orders(
&self,
request: &UpbitClosedOrdersRequest,
) -> Result<Vec<UpbitClosedOrder>> {
private::closed_orders(self.credentials()?, self.http()?, request).await
}
pub async fn deposit_info(&self, asset: &str, network: &Network) -> Result<UpbitDepositInfo> {
wallet::deposit_info(self.credentials()?, self.http()?, asset, network).await
}
pub async fn withdrawal_addresses(&self) -> Result<Vec<UpbitWithdrawalAddress>> {
wallet::withdrawal_addresses(self.credentials()?, self.http()?).await
}
pub async fn deposit_detail(
&self,
request: &TransferLookupRequest,
) -> Result<UpbitDepositResponse> {
wallet::deposit_detail(self.credentials()?, self.http()?, request).await
}
pub async fn withdrawal_detail(
&self,
request: &TransferLookupRequest,
) -> Result<UpbitWithdrawalResponse> {
wallet::withdrawal_detail(self.credentials()?, self.http()?, request).await
}
pub async fn cancel_withdrawal_detail(
&self,
withdrawal_id: &str,
) -> Result<UpbitCancelWithdrawalResponse> {
wallet::cancel_withdrawal_detail(self.credentials()?, self.http()?, withdrawal_id).await
}
pub async fn deposit_krw(&self, request: &UpbitKrwTransferRequest) -> Result<UpbitKrwDeposit> {
self.ensure_korea_wallet_region()?;
wallet::deposit_krw(self.credentials()?, self.http()?, request).await
}
pub async fn withdraw_krw(
&self,
request: &UpbitKrwTransferRequest,
) -> Result<UpbitKrwWithdrawal> {
self.ensure_korea_wallet_region()?;
wallet::withdraw_krw(self.credentials()?, self.http()?, request).await
}
pub async fn api_keys(&self) -> Result<Vec<UpbitApiKey>> {
self.ensure_korea_wallet_region()?;
wallet::api_keys(self.credentials()?, self.http()?).await
}
pub async fn list_pockets(&self) -> Result<Vec<UpbitPocket>> {
self.ensure_korea_pockets_region()?;
pockets::list(self.credentials()?, self.http()?).await
}
pub async fn list_pocket_api_keys(
&self,
request: &UpbitPocketApiKeysRequest,
) -> Result<Vec<UpbitPocketApiKeyGroup>> {
self.ensure_korea_pockets_region()?;
pockets::list_api_keys(self.credentials()?, self.http()?, request).await
}
pub async fn sub_pocket_balances(&self, pocket_uuid: &str) -> Result<Vec<UpbitPocketBalance>> {
self.ensure_korea_pockets_region()?;
pockets::balances(self.credentials()?, self.http()?, pocket_uuid).await
}
pub async fn universal_transfer(
&self,
request: &UpbitPocketUniversalTransferRequest,
) -> Result<UpbitPocketTransfer> {
self.ensure_korea_pockets_region()?;
pockets::universal_transfer(self.credentials()?, self.http()?, request).await
}
pub async fn universal_transfers(
&self,
request: &UpbitPocketTransferQuery,
) -> Result<Vec<UpbitPocketTransfer>> {
self.ensure_korea_pockets_region()?;
pockets::universal_transfers(self.credentials()?, self.http()?, request).await
}
pub async fn sub_pocket_transfer(
&self,
request: &UpbitPocketTransferRequest,
) -> Result<UpbitPocketTransfer> {
self.ensure_korea_pockets_region()?;
pockets::sub_pocket_transfer(self.credentials()?, self.http()?, request).await
}
pub async fn sub_pocket_transfers(
&self,
request: &UpbitPocketTransferQuery,
) -> Result<Vec<UpbitPocketTransfer>> {
self.ensure_korea_pockets_region()?;
pockets::sub_pocket_transfers(self.credentials()?, self.http()?, request).await
}
pub async fn travel_rule_vasps(&self) -> Result<Vec<UpbitTravelRuleVasp>> {
travel_rule::ensure_supported_region(self.region)?;
travel_rule::vasps(self.region, self.credentials()?, self.http()?).await
}
pub async fn verify_travel_rule_by_uuid(
&self,
deposit_uuid: &str,
vasp_uuid: &str,
) -> Result<UpbitTravelRuleVerification> {
travel_rule::ensure_supported_region(self.region)?;
travel_rule::verify_by_uuid(
self.region,
self.credentials()?,
self.http()?,
deposit_uuid,
vasp_uuid,
)
.await
}
pub async fn verify_travel_rule_by_txid(
&self,
txid: &str,
vasp_uuid: &str,
currency: &str,
net_type: &str,
) -> Result<UpbitTravelRuleVerification> {
travel_rule::ensure_supported_region(self.region)?;
travel_rule::verify_by_txid(
self.region,
self.credentials()?,
self.http()?,
txid,
vasp_uuid,
currency,
net_type,
)
.await
}
pub async fn batch_cancel_open_orders(
&self,
request: &UpbitBatchCancelRequest,
) -> Result<CancelOrdersResult> {
private::batch_cancel_open_orders(self.credentials()?, self.http()?, request).await
}
pub async fn cancel_and_new_order(
&self,
request: &UpbitCancelAndNewOrderRequest,
) -> Result<UpbitCancelAndNewOrderResult> {
private::cancel_and_new_order(self.credentials()?, self.http()?, request).await
}
pub async fn cancel_and_new_order_detail(
&self,
request: &UpbitCancelAndNewOrderRequest,
) -> Result<UpbitCancelAndNewOrderDetailResult> {
private::cancel_and_new_order_detail(self.credentials()?, self.http()?, request).await
}
pub(crate) fn is_authenticated(&self) -> bool {
self.credentials
.as_ref()
.is_some_and(|credentials| credentials.validate().is_ok())
}
fn http(&self) -> Result<&HttpTransport> {
self.http.as_ref().map_err(Clone::clone)
}
fn credentials(&self) -> Result<&UpbitCredentials> {
let credentials = self.credentials.as_ref().ok_or_else(|| {
Error::auth(
"this Upbit adapter has no credentials; add them with \
`UpbitAdapter::with_credentials`",
)
})?;
credentials.validate()?;
Ok(credentials)
}
fn ensure_korea_wallet_region(&self) -> Result<()> {
if self.region == UpbitRegion::Korea {
Ok(())
} else {
Err(Error::invalid_request(
"region",
"Upbit KRW transfers and API-key listing are available only in the Korea region",
))
}
}
fn ensure_korea_pockets_region(&self) -> Result<()> {
if self.region == UpbitRegion::Korea {
Ok(())
} else {
Err(Error::invalid_request(
"region",
"Upbit pocket APIs are available only in the Korea region",
))
}
}
fn validate_withdrawal_destination(&self, request: &WithdrawRequest) -> Result<()> {
if self.region == UpbitRegion::Indonesia
&& !matches!(
&request.destination,
TransferDestination::Exchange(destination)
if destination.exchange == Exchange::Upbit
)
{
return Err(Error::unsupported(
Feature::Withdrawals,
"upbit",
"Upbit Indonesia external withdrawals require beneficiary fields that are not yet represented by the common withdrawal request",
));
}
Ok(())
}
}
impl Default for UpbitAdapter {
fn default() -> Self {
Self::new()
}
}
impl Adapter for UpbitAdapter {
fn exchange(&self) -> Exchange {
Exchange::Upbit
}
fn supports(&self, feature: Feature) -> bool {
if feature.is_derivatives_only() {
return false;
}
if feature == Feature::TravelRule {
return matches!(self.region, UpbitRegion::Korea | UpbitRegion::Singapore)
&& self.is_authenticated();
}
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 {
let books = self
.order_books(std::slice::from_ref(&market), depth)
.await?;
rest::only(books, &market)
})
}
fn ticker(&self, market: &Market) -> BoxFuture<'_, Result<Ticker>> {
let market = market.clone();
Box::pin(async move {
let tickers = self.tickers(std::slice::from_ref(&market)).await?;
rest::only(tickers, &market)
})
}
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 frame = stream::subscribe_frame(subscription, &ticket());
let url = self.region.websocket_url().to_string();
let config = config.clone();
let active_subscriptions = Arc::clone(&self.active_subscriptions);
let subscription = SubscriptionKey::from(subscription);
Box::pin(async move {
let session = ws::connect(
WsConnect {
url,
headers: None,
subscribe: WsConnect::fixed(vec![frame?]),
heartbeat: Some(stream::HEARTBEAT),
},
&config,
)
.await?;
let close = session.close_handle();
let control = Arc::new(stream::SubscriptionControl::new(session.send_handle()));
active_subscriptions.register(subscription, &control);
let decoder = stream::Decoder::default();
Ok(MarketStream::new_with_close(
controlled_market_events(session, control, decoder),
move || async move { close.close().await },
))
})
}
fn balances(&self) -> BoxFuture<'_, Result<Vec<Balance>>> {
Box::pin(async move { private::balances(self.credentials()?, self.http()?).await })
}
fn order_rules(&self, market: &Market) -> BoxFuture<'_, Result<OrderRules>> {
let market = market.clone();
Box::pin(
async move { private::order_rules(self.credentials()?, self.http()?, &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.credentials()?, self.http()?, &asset).await },
)
}
fn deposit_addresses(&self) -> BoxFuture<'_, Result<Vec<DepositAddressEntry>>> {
Box::pin(async move { wallet::deposit_addresses(self.credentials()?, self.http()?).await })
}
fn deposit_address(
&self,
request: &DepositAddressRequest,
) -> BoxFuture<'_, Result<DepositAddress>> {
let request = request.clone();
Box::pin(async move {
wallet::deposit_address(self.credentials()?, self.http()?, &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.credentials()?, self.http()?, &request).await
})
}
fn prepare_withdrawal(
&self,
request: &WithdrawRequest,
) -> BoxFuture<'_, Result<WithdrawalQuote>> {
let request = request.clone();
Box::pin(async move {
self.validate_withdrawal_destination(&request)?;
wallet::prepare_withdrawal(self.credentials()?, self.http()?, &request).await
})
}
fn withdraw(&self, request: &WithdrawRequest) -> BoxFuture<'_, Result<Withdrawal>> {
let request = request.clone();
Box::pin(async move {
self.validate_withdrawal_destination(&request)?;
wallet::withdraw(self.credentials()?, self.http()?, &request).await
})
}
fn deposit(&self, request: &TransferLookupRequest) -> BoxFuture<'_, Result<Deposit>> {
let request = request.clone();
Box::pin(async move { wallet::deposit(self.credentials()?, self.http()?, &request).await })
}
fn withdrawal(&self, request: &TransferLookupRequest) -> BoxFuture<'_, Result<Withdrawal>> {
let request = request.clone();
Box::pin(
async move { wallet::withdrawal(self.credentials()?, self.http()?, &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.credentials()?, self.http()?, &withdrawal_id).await
})
}
fn deposits(&self, request: &TransferHistoryRequest) -> BoxFuture<'_, Result<Page<Deposit>>> {
let request = request.clone();
Box::pin(async move { wallet::deposits(self.credentials()?, self.http()?, &request).await })
}
fn withdrawals(
&self,
request: &TransferHistoryRequest,
) -> BoxFuture<'_, Result<Page<Withdrawal>>> {
let request = request.clone();
Box::pin(
async move { wallet::withdrawals(self.credentials()?, self.http()?, &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.credentials()?, self.http()?, 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.credentials()?, self.http()?, &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.credentials()?, self.http()?, &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.credentials()?, self.http()?, &request).await
})
}
fn order_history(&self, request: &OrderHistoryRequest) -> BoxFuture<'_, Result<Page<Order>>> {
let request = request.clone();
Box::pin(async move {
private::order_history(self.credentials()?, self.http()?, &request).await
})
}
fn place_order(&self, request: &OrderRequest) -> BoxFuture<'_, Result<Order>> {
let request = request.clone();
Box::pin(
async move { private::place_order(self.credentials()?, self.http()?, &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.credentials()?, self.http()?, &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.credentials()?,
self.http()?,
&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.credentials()?, self.http()?, &request).await
})
}
fn subscribe_account(&self, config: &StreamConfig) -> BoxFuture<'_, Result<AccountStream>> {
let url = format!("{}/private", self.region.websocket_url());
let config = config.clone();
Box::pin(async move {
let credentials = self.credentials()?.clone();
let session = ws::connect(
WsConnect {
url,
headers: Some(Box::new(move || {
Ok(vec![(
private::AUTHORIZATION.to_string(),
private::authorization(&credentials, "")?,
)])
})),
subscribe: WsConnect::fixed(vec![private::subscribe_frame(&ticket())?]),
heartbeat: Some(stream::HEARTBEAT),
},
&config,
)
.await?;
let close = session.close_handle();
Ok(AccountStream::new_with_close(
events(session, private::account_events, AccountEvent::Reconnected),
move || async move { close.close().await },
))
})
}
}
fn ticket() -> String {
uuid::Uuid::new_v4().to_string()
}
fn controlled_market_events(
session: WsSession,
control: Arc<stream::SubscriptionControl>,
mut decoder: stream::Decoder,
) -> impl Stream<Item = Result<MarketEvent>> + Send {
session.flat_map(move |item| {
let items = match item {
Ok(WsCommand::Text(text)) => {
if control.handle_frame(&text) {
Vec::new()
} else {
split(decoder.decode(&text))
}
}
Ok(WsCommand::Binary(bytes)) => match String::from_utf8(bytes) {
Ok(text) if control.handle_frame(&text) => Vec::new(),
Ok(text) => split(decoder.decode(&text)),
Err(err) => vec![Err(Error::decode(format!(
"upbit sent a frame that is not UTF-8: {err}"
)))],
},
Ok(WsCommand::Reconnected) => {
control.fail_pending();
vec![Ok(MarketEvent::Reconnected)]
}
Err(err) => {
control.fail_pending();
vec![Err(err)]
}
};
futures_util::stream::iter(items)
})
}
fn controlled_detailed_market_events(
session: WsSession,
control: Arc<stream::SubscriptionControl>,
mut decoder: stream::DetailedDecoder,
) -> impl Stream<Item = Result<UpbitMarketStreamEvent>> + Send {
session.flat_map(move |item| {
let items = match item {
Ok(WsCommand::Text(text)) => {
if control.handle_frame(&text) {
Vec::new()
} else {
split(decoder.decode(&text))
}
}
Ok(WsCommand::Binary(bytes)) => match String::from_utf8(bytes) {
Ok(text) if control.handle_frame(&text) => Vec::new(),
Ok(text) => split(decoder.decode(&text)),
Err(err) => vec![Err(Error::decode(format!(
"upbit sent a frame that is not UTF-8: {err}"
)))],
},
Ok(WsCommand::Reconnected) => {
control.fail_pending();
vec![Ok(UpbitMarketStreamEvent::Reconnected)]
}
Err(err) => {
control.fail_pending();
vec![Err(err)]
}
};
futures_util::stream::iter(items)
})
}
fn events<T: Clone + Send + 'static>(
session: WsSession,
mut decode: impl FnMut(&str) -> Result<Vec<T>> + Send + 'static,
reconnected: T,
) -> impl Stream<Item = Result<T>> + Send {
session.flat_map(move |item| {
let items = match item {
Ok(WsCommand::Text(text)) => split(decode(&text)),
Ok(WsCommand::Binary(bytes)) => match String::from_utf8(bytes) {
Ok(text) => split(decode(&text)),
Err(err) => vec![Err(Error::decode(format!(
"upbit sent a frame that is not UTF-8: {err}"
)))],
},
Ok(WsCommand::Reconnected) => vec![Ok(reconnected.clone())],
Err(err) => vec![Err(err)],
};
futures_util::stream::iter(items)
})
}
fn split<T>(decoded: Result<Vec<T>>) -> Vec<Result<T>> {
match decoded {
Ok(items) => items.into_iter().map(Ok).collect(),
Err(err) => vec![Err(err)],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_spot_exchange_never_claims_derivatives_features() {
let adapter = UpbitAdapter::new().with_credentials("access", "secret");
for feature in [
Feature::Positions,
Feature::Margin,
Feature::FundingRates,
Feature::FundingPayments,
Feature::MarginConfig,
Feature::ReduceOnlyOrders,
] {
assert!(!adapter.supports(feature), "{feature:?}");
}
}
#[test]
fn credentials_are_what_unlock_the_private_half() {
let public = UpbitAdapter::new();
let private = UpbitAdapter::new().with_credentials("access", "secret");
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:?}");
}
}
#[tokio::test]
async fn credentials_reject_blank_keys_and_accept_a_nonblank_pair() {
for (access_key, secret_key) in [
("", "secret"),
(" \t", "secret"),
("access", ""),
("access", " \n"),
] {
let adapter = UpbitAdapter::new().with_credentials(access_key, secret_key);
assert!(matches!(adapter.credentials(), Err(Error::Auth { .. })));
assert!(matches!(adapter.balances().await, Err(Error::Auth { .. })));
assert!(!adapter.is_authenticated());
}
let adapter = UpbitAdapter::new().with_credentials("access", "secret");
assert!(adapter.credentials().is_ok());
assert!(adapter.is_authenticated());
}
#[test]
fn public_market_data_works_without_credentials() {
let public = UpbitAdapter::new();
for feature in [
Feature::Markets,
Feature::Trades,
Feature::OrderBook,
Feature::Ticker,
Feature::Candles,
Feature::CandleStream,
] {
assert!(public.supports(feature), "{feature:?}");
}
}
#[tokio::test]
async fn list_subscriptions_never_opens_a_temporary_connection() {
let subscription = Subscription::new()
.market(Market::spot(Exchange::Upbit, "BTC", "KRW"))
.feed(crate::types::Feed::Ticker);
let error = UpbitAdapter::new()
.list_subscriptions(&subscription)
.await
.expect_err("a connection-scoped operation needs an active connection");
assert!(matches!(
error,
Error::InvalidRequest { field, .. } if field == "subscription"
));
}
#[test]
fn travel_rule_requires_a_supported_region_and_credentials() {
assert!(!UpbitAdapter::new().supports(Feature::TravelRule));
assert!(!UpbitAdapter::with_region(UpbitRegion::Singapore).supports(Feature::TravelRule));
assert!(
UpbitAdapter::new()
.with_credentials("access", "secret")
.supports(Feature::TravelRule)
);
assert!(
UpbitAdapter::with_region(UpbitRegion::Singapore)
.with_credentials("access", "secret")
.supports(Feature::TravelRule)
);
assert!(
!UpbitAdapter::with_region(UpbitRegion::Indonesia)
.with_credentials("access", "secret")
.supports(Feature::TravelRule)
);
}
#[tokio::test]
async fn korea_only_wallet_methods_reject_other_regions_before_credentials() {
let adapter = UpbitAdapter::with_region(UpbitRegion::Singapore);
let request = UpbitKrwTransferRequest::new(Decimal::ONE, UpbitKrwTwoFactorType::Kakao);
for result in [
adapter.deposit_krw(&request).await.map(|_| ()),
adapter.withdraw_krw(&request).await.map(|_| ()),
adapter.api_keys().await.map(|_| ()),
] {
assert!(
matches!(result, Err(Error::InvalidRequest { field, .. }) if field == "region")
);
}
}
#[tokio::test]
async fn korea_only_pocket_methods_reject_other_regions_before_credentials() {
let adapter = UpbitAdapter::with_region(UpbitRegion::Singapore);
let api_keys = UpbitPocketApiKeysRequest::new();
let universal = UpbitPocketUniversalTransferRequest::new("pocket-2", "XRP", Decimal::ONE);
let sub_pocket = UpbitPocketTransferRequest::new("pocket-2", "XRP", Decimal::ONE);
let history = UpbitPocketTransferQuery::new();
for result in [
adapter.list_pockets().await.map(|_| ()),
adapter.list_pocket_api_keys(&api_keys).await.map(|_| ()),
adapter.sub_pocket_balances("pocket-1").await.map(|_| ()),
adapter.universal_transfer(&universal).await.map(|_| ()),
adapter.universal_transfers(&history).await.map(|_| ()),
adapter.sub_pocket_transfer(&sub_pocket).await.map(|_| ()),
adapter.sub_pocket_transfers(&history).await.map(|_| ()),
] {
assert!(
matches!(result, Err(Error::InvalidRequest { field, .. }) if field == "region")
);
}
}
#[tokio::test]
async fn travel_rule_region_precedes_credential_validation() {
let error = UpbitAdapter::with_region(UpbitRegion::Indonesia)
.travel_rule_vasps()
.await
.expect_err("unsupported region must fail before credentials");
assert!(matches!(
error,
Error::Unsupported {
feature: Feature::TravelRule,
..
}
));
}
#[tokio::test]
async fn aggregated_order_books_fail_before_network_outside_korea() {
let singapore = UpbitAdapter::with_region(UpbitRegion::Singapore);
let market = Market::spot(Exchange::Upbit, "BTC", "SGD");
assert!(matches!(
singapore
.order_books_at_level(&[market], Decimal::ONE, Some(1))
.await,
Err(Error::Unsupported {
feature: Feature::OrderBook,
..
})
));
}
#[tokio::test]
async fn an_account_call_without_credentials_fails_before_the_network() {
let public = UpbitAdapter::new();
let market = Market::spot(Exchange::Upbit, "BTC", "KRW");
let order = crate::request::OrderRequest::market(
market.clone(),
crate::types::Side::Sell,
crate::types::Size::Base(rust_decimal::Decimal::ONE),
);
assert!(matches!(public.balances().await, Err(Error::Auth { .. })));
assert!(matches!(
public.open_orders(None).await,
Err(Error::Auth { .. })
));
assert!(matches!(
public.place_order(&order).await,
Err(Error::Auth { .. })
));
assert!(matches!(
public.test_order(&order).await,
Err(Error::Auth { .. })
));
assert!(matches!(
public
.order_detail(&UpbitOrderDetailRequest::by_uuid(market.clone(), "order-1"))
.await,
Err(Error::Auth { .. })
));
assert!(matches!(
public
.closed_orders(&UpbitClosedOrdersRequest::new().market(market.clone()))
.await,
Err(Error::Auth { .. })
));
assert!(matches!(
public
.cancel_and_new_order(&UpbitCancelAndNewOrderRequest::new(
UpbitOrderReference::uuid("order-1"),
UpbitCancelAndNewOrder::MarketSell {
volume: UpbitOrderVolume::RemainOnly,
},
))
.await,
Err(Error::Auth { .. })
));
assert!(matches!(
public
.deposit_info("BTC", &crate::types::Network::Bitcoin)
.await,
Err(Error::Auth { .. })
));
assert!(matches!(
public.withdrawal_addresses().await,
Err(Error::Auth { .. })
));
assert!(matches!(
public
.batch_cancel_open_orders(
&UpbitBatchCancelRequest::new(UpbitBatchCancelScope::All,)
)
.await,
Err(Error::Auth { .. })
));
assert!(matches!(
public.cancel_order(&market, "an-order").await,
Err(Error::Auth { .. })
));
assert!(matches!(
public.subscribe_account(&StreamConfig::default()).await,
Err(Error::Auth { .. })
));
}
#[tokio::test]
async fn the_derivatives_half_stays_at_the_trait_default() {
let adapter = UpbitAdapter::new().with_credentials("access", "secret");
let request =
crate::request::HistoryRequest::new(Market::perpetual(Exchange::Upbit, "BTC", "KRW"));
assert!(matches!(
adapter.positions(None).await,
Err(Error::Unsupported { .. })
));
assert!(matches!(
adapter.margin_summary().await,
Err(Error::Unsupported { .. })
));
assert!(matches!(
adapter.funding_rates(&request).await,
Err(Error::Unsupported { .. })
));
}
#[tokio::test]
async fn upbit_lists_no_derivatives_and_says_so_with_an_empty_answer() {
let markets = UpbitAdapter::new()
.markets(MarketKind::Perpetual)
.await
.expect("a listable kind");
assert!(markets.is_empty());
}
#[test]
fn a_frame_that_carries_no_events_yields_none_and_a_bad_one_yields_one_error() {
let mut decoder = stream::Decoder::default();
assert!(
decoder
.decode(r#"{"status":"UP"}"#)
.expect("a control frame")
.is_empty()
);
assert_eq!(split(decoder.decode("not json")).len(), 1);
assert_eq!(split(decoder.decode(r#"{"status":"UP"}"#)).len(), 0);
}
#[test]
fn each_region_is_a_separate_deployment() {
assert_eq!(UpbitAdapter::new().region(), UpbitRegion::Korea);
assert_ne!(
UpbitRegion::Korea.rest_base_url(),
UpbitRegion::Singapore.rest_base_url()
);
assert!(UpbitRegion::Thailand.websocket_url().starts_with("wss://"));
}
#[tokio::test]
async fn indonesia_external_withdrawal_fails_during_preparation_and_submission() {
use crate::types::{ChainDestination, Network};
use rust_decimal::Decimal;
let request = WithdrawRequest::new(
"BTC",
Network::Bitcoin,
Decimal::ONE,
TransferDestination::Chain(ChainDestination {
asset: "BTC".to_string(),
network: Network::Bitcoin,
address: "bc1destination".to_string(),
memo: None,
}),
);
let adapter = UpbitAdapter::with_region(UpbitRegion::Indonesia);
for result in [
adapter.prepare_withdrawal(&request).await.map(|_| ()),
adapter.withdraw(&request).await.map(|_| ()),
] {
assert!(matches!(
result,
Err(Error::Unsupported {
feature: Feature::Withdrawals,
..
})
));
}
}
}