use std::collections::HashMap;
use chrono::serde as chrono_serde;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::common::*;
use crate::v2::rest::api_impl::*;
pub use crate::v2::rest::public::RespVIPLevel;
#[derive(Serialize, Debug)]
pub struct GetProfile {}
impl_api!(GetProfile => RespProfile : auth GET, "/api/v2/members/profile");
#[derive(Serialize, Debug)]
pub struct GetProfileAndAccount {}
impl_api!(GetProfileAndAccount => RespProfile : auth GET, "/api/v2/members/me");
#[derive(Serialize, Debug)]
pub struct GetAccountVIPLevel {}
impl_api!(GetAccountVIPLevel => RespAccountVIPInfo : auth GET, "/api/v2/members/vip_level");
#[derive(Serialize, Debug)]
pub struct GetAccountOfCurrency {
#[serde(skip)]
pub path_currency: String,
}
impl_api!(GetAccountOfCurrency => RespAccountCurrencyInfo : auth GET, dynamic params {
api_url!(dynamic "/api/v2/members/accounts/{}", params.path_currency)
});
#[derive(Serialize, Debug)]
pub struct GetInternalTransfers {
#[serde(skip_serializing_if = "Option::is_none")]
pub currency: Option<String>,
pub side: InternalTransferSide,
#[serde(
rename = "from",
with = "chrono_serde::ts_seconds_option",
skip_serializing_if = "Option::is_none"
)]
pub from_timestamp: Option<DateTime>,
#[serde(
rename = "to",
with = "chrono_serde::ts_seconds_option",
skip_serializing_if = "Option::is_none"
)]
pub to_timestamp: Option<DateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pagination: Option<bool>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub page_params: Option<PageParams>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<u64>,
}
impl_api!(GetInternalTransfers => Vec<RespInternalTransferRecord> : auth GET, "/api/v2/internal_transfers");
#[derive(Serialize, Debug)]
pub struct GetInternalTransferByUUID {
pub uuid: String,
}
impl_api!(GetInternalTransferByUUID => RespInternalTransferRecord : auth GET, "/api/v2/internal_transfer");
#[derive(Serialize, Debug)]
pub struct GetRewards {
#[serde(skip_serializing_if = "Option::is_none")]
pub currency: Option<String>,
#[serde(
rename = "from",
with = "chrono_serde::ts_seconds_option",
skip_serializing_if = "Option::is_none"
)]
pub from_timestamp: Option<DateTime>,
#[serde(
rename = "to",
with = "chrono_serde::ts_seconds_option",
skip_serializing_if = "Option::is_none"
)]
pub to_timestamp: Option<DateTime>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub pagination: Option<bool>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub page_params: Option<PageParams>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<u64>,
}
impl_api!(GetRewards => Vec<RewardRecord> : auth GET, "/api/v2/rewards");
#[derive(Serialize, Debug)]
pub struct GetRewardsOfType {
#[serde(skip)]
pub reward_type: RewardType,
#[serde(flatten)]
pub detail: GetRewards,
}
impl_api!(GetRewardsOfType => Vec<RewardRecord> : auth GET, dynamic params {
let mut reward_str = String::with_capacity(18);
for (i, ch) in format!("{:?}", params.reward_type).char_indices() {
if i > 0 && ch.is_uppercase() {
reward_str.push('_');
}
reward_str.push(ch.to_ascii_lowercase());
}
api_url!(dynamic "/api/v2/rewards/{}", reward_str)
});
#[derive(Serialize, Debug)]
pub struct GetSavingInterestHistory {
pub currency: String,
#[serde(
rename = "from",
with = "chrono_serde::ts_seconds_option",
skip_serializing_if = "Option::is_none"
)]
pub from_timestamp: Option<DateTime>,
#[serde(
rename = "to",
with = "chrono_serde::ts_seconds_option",
skip_serializing_if = "Option::is_none"
)]
pub to_timestamp: Option<DateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pagination: Option<bool>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub page_params: Option<PageParams>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<u64>,
}
impl_api!(GetSavingInterestHistory => Vec<RewardRecord> : auth GET, "/api/v2/yields");
#[derive(Serialize, Debug)]
pub struct GetMaxRewardsYesterday {}
impl_api!(GetMaxRewardsYesterday => RespMAXReward : auth GET, "/api/v2/max_rewards/yesterday");
#[derive(Deserialize, Eq, PartialEq, Debug, Default)]
#[serde(default)]
pub struct RespProfile {
pub sn: String,
pub name: String,
pub email: String,
pub language: String,
pub country_code: String,
pub phone_set: Option<bool>,
pub phone_number: String,
pub phone_contact_approved: Option<bool>,
pub status: AccountStatus,
pub profile_verified: Option<bool>,
pub kyc_approved: Option<bool>,
pub kyc_state: String,
pub any_kyc_rejected: Option<bool>,
pub agreement_checked: Option<bool>,
pub level: Option<u8>,
pub vip_level: Option<u8>,
pub member_type: MemberType,
pub bank: Option<BankInfo>,
pub referral_code: String,
pub birthday: Option<String>,
pub gender: Gender,
pub nationality: Option<String>,
pub identity_type: Option<String>,
pub identity_number: Option<String>,
pub individual_verified: Option<bool>,
pub invoice_carrier_id: Option<String>,
pub invoice_carrier_type: Option<String>,
pub is_deleted: Option<bool>,
pub is_frozen: Option<bool>,
pub is_activated: Option<bool>,
pub is_corporate: Option<bool>,
pub current_two_factor_type: Option<String>,
pub locked_status_of_2fa: Option<String>,
pub documents: Option<HashMap<String, String>>,
pub supplemental_document_type: Option<String>,
pub user_agreement_checked: Option<bool>,
pub user_agreement_version: Option<String>,
pub withdrawable: Option<bool>,
pub accounts: Option<Vec<RespAccountCurrencyInfo>>,
}
#[derive(Deserialize, Eq, PartialEq, Default, Debug)]
pub struct RespAccountVIPInfo {
#[serde(rename = "current_vip_level")]
current: RespVIPLevel,
#[serde(rename = "next_vip_level")]
next: RespVIPLevel,
}
#[derive(Deserialize, Eq, PartialEq, Default, Debug)]
pub struct RespAccountCurrencyInfo {
pub currency: String,
pub balance: Decimal,
pub locked: Decimal,
#[serde(rename = "type")]
pub wallet_type: String,
pub fiat_currency: Option<String>,
pub fiat_balance: Option<Decimal>,
}
#[derive(Deserialize, Eq, PartialEq, Default, Debug)]
pub struct RespInternalTransferRecord {
pub uuid: String,
pub currency: String,
pub amount: Decimal,
#[serde(with = "chrono_serde::ts_seconds_option")]
pub created_at: Option<DateTime>,
pub state: String,
pub from_member: String,
pub to_member: String,
}
#[derive(Deserialize, Eq, PartialEq, Default, Debug)]
pub struct RespMAXReward {
pub trading_reward: Decimal,
pub holding_reward: Decimal,
}
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "snake_case")]
pub enum RewardType {
MiningReward,
HoldingReward,
TradingReward,
Commission,
AirdropReward,
RedemptionReward,
VipRebate,
SavingsInterest,
Unknown,
}
impl RewardType {
pub fn is_unknown(&self) -> bool {
self == &Self::Unknown
}
}
impl Default for RewardType {
fn default() -> Self {
Self::Unknown
}
}
#[derive(Deserialize, Eq, PartialEq, Debug)]
#[serde(rename_all = "lowercase")]
pub enum AccountStatus {
Inactivated,
Activated,
Frozen,
Unknown,
}
impl AccountStatus {
pub fn is_unknown(&self) -> bool {
self == &Self::Unknown
}
}
impl Default for AccountStatus {
fn default() -> Self {
Self::Unknown
}
}
#[derive(Deserialize, Eq, PartialEq, Debug)]
pub enum MemberType {
#[serde(rename = "type_guest")]
Guest,
#[serde(rename = "type_coin")]
Coin,
#[serde(rename = "type_twd")]
TWD,
Unknown,
}
impl MemberType {
pub fn is_unknown(&self) -> bool {
self == &Self::Unknown
}
}
impl Default for MemberType {
fn default() -> Self {
Self::Unknown
}
}
#[derive(Deserialize, Eq, PartialEq, Default, Debug)]
#[serde(default)]
pub struct BankInfo {
pub bank_code: String,
pub bank_name: String,
pub branch: String,
pub bank_branch_name: String,
pub name: Option<String>,
pub account: String,
pub state: String,
pub reject_reason: Option<String>,
pub intra_bank: Option<bool>,
pub bank_branch_active: Option<bool>,
}
#[derive(Deserialize, Eq, PartialEq, Debug)]
pub enum Gender {
#[serde(rename = "M")]
Male,
#[serde(rename = "F")]
Female,
#[serde(rename = "C")]
Corporation,
Unknown,
}
impl Gender {
pub fn is_unknown(&self) -> bool {
self == &Self::Unknown
}
}
impl Default for Gender {
fn default() -> Self {
Self::Unknown
}
}
#[derive(Serialize, Copy, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "lowercase")]
pub enum InternalTransferSide {
In,
Out,
}
#[derive(Deserialize, Eq, PartialEq, Default, Debug)]
pub struct RewardRecord {
pub uuid: String,
#[serde(rename = "type")]
pub reward_type: RewardType,
pub currency: String,
pub amount: Decimal,
#[serde(with = "chrono_serde::ts_seconds_option")]
pub created_at: Option<DateTime>,
pub state: String,
pub note: String,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::test_util::*;
use chrono::{TimeZone, Utc};
use rust_decimal_macros::dec;
use surf::Client as HTTPClient;
use surf_vcr::VcrMode;
async fn create_client(cassette: &'static str) -> HTTPClient {
let mut path_builder = test_resource_path();
path_builder.push("rest");
path_builder.push("private");
path_builder.push("misc");
path_builder.push(cassette);
create_test_recording_client(VcrMode::Replay, path_builder.as_path().to_str().unwrap())
.await
}
#[async_std::test]
async fn get_profile() {
let params = GetProfile {};
let resp = create_client("get_profile.yaml")
.await
.send(params.to_request(&TEST_CREDENTIALS))
.await
.expect("Error while sending request");
let result = GetProfile::read_response(resp.into()).await;
let profile: RespProfile = result.expect("failed to parse result");
assert_eq!(
profile,
RespProfile {
sn: "(test erased sn)".into(),
name: "John Doe".into(),
email: "(test erased email)".to_string(),
language: "en".into(),
country_code: "886".into(),
phone_set: None,
phone_number: "227221314".into(),
phone_contact_approved: None,
status: AccountStatus::Activated,
profile_verified: Some(true),
kyc_approved: None,
kyc_state: "verified".into(),
any_kyc_rejected: Some(false),
agreement_checked: Some(true),
level: Some(2),
vip_level: Some(0),
member_type: MemberType::TWD,
bank: Some(BankInfo {
bank_code: "808".into(),
bank_name: "玉山商業銀行".into(),
branch: "8080000".into(),
bank_branch_name: "某某分行".into(),
name: None,
account: "0000000000000".into(),
state: "verified".into(),
reject_reason: None,
intra_bank: None,
bank_branch_active: None
}),
referral_code: "58b11077".into(),
birthday: Some("198****-29".into()),
gender: Gender::Male,
nationality: Some("TW".into()),
identity_type: Some("taiwan_id".into()),
identity_number: Some("A12****789".into()),
individual_verified: None,
invoice_carrier_id: Some("/123ABCD".into()),
invoice_carrier_type: Some("0a0000".into()),
is_deleted: None,
is_frozen: None,
is_activated: None,
is_corporate: None,
current_two_factor_type: Some("app".into()),
locked_status_of_2fa: None,
documents: Some(HashMap::from([
("photo_id_front_state".into(), "verified".into()),
("photo_id_back_state".into(), "verified".into()),
("cellphone_bill_state".into(), "verified".into()),
("selfie_with_id_state".into(), "verified".into()),
])),
supplemental_document_type: Some("health_id_card".into()),
user_agreement_checked: None,
user_agreement_version: None,
withdrawable: None,
accounts: None,
}
);
}
#[async_std::test]
async fn get_profile_and_account() {
let params = GetProfileAndAccount {};
let resp = create_client("get_profile_and_account.yaml")
.await
.send(params.to_request(&TEST_CREDENTIALS))
.await
.expect("Error while sending request");
let result = GetProfileAndAccount::read_response(resp.into()).await;
let profile: RespProfile = result.expect("failed to parse result");
assert_eq!(
profile,
RespProfile {
sn: "(test erased sn)".into(),
name: "John Doe".into(),
email: "(test erased email)".to_string(),
language: "en".into(),
country_code: "886".into(),
phone_set: Some(true),
phone_number: "227221314".into(),
phone_contact_approved: None,
status: AccountStatus::Unknown,
profile_verified: Some(true),
kyc_approved: Some(true),
kyc_state: "verified".into(),
any_kyc_rejected: Some(false),
agreement_checked: None,
level: Some(2),
vip_level: Some(0),
member_type: MemberType::TWD,
bank: Some(BankInfo {
bank_code: "808".into(),
bank_name: "玉山商業銀行".into(),
branch: "8080000".into(),
bank_branch_name: "某某分行".into(),
name: None,
account: "0000000000000".into(),
state: "verified".into(),
reject_reason: None,
intra_bank: None,
bank_branch_active: None
}),
referral_code: "58b11077".into(),
birthday: Some("1985-02-29".into()),
gender: Gender::Male,
nationality: Some("TW".into()),
identity_type: Some("taiwan_id".into()),
identity_number: Some("A123456789".into()),
individual_verified: None,
invoice_carrier_id: Some("/123ABCD".into()),
invoice_carrier_type: Some("0a0000".into()),
is_deleted: Some(false),
is_frozen: Some(false),
is_activated: Some(true),
is_corporate: Some(false),
current_two_factor_type: None,
locked_status_of_2fa: None,
documents: Some(HashMap::from([
("photo_id_front_state".into(), "verified".into()),
("photo_id_back_state".into(), "verified".into()),
("cellphone_bill_state".into(), "verified".into()),
("selfie_with_id_state".into(), "verified".into()),
])),
supplemental_document_type: Some("health_id_card".into()),
user_agreement_checked: Some(true),
user_agreement_version: Some("5.1".into()),
withdrawable: Some(true),
accounts: Some(Vec::new()),
}
);
}
#[async_std::test]
async fn get_vip_level() {
let params = GetAccountVIPLevel {};
let resp = create_client("get_vip_level.yaml")
.await
.send(params.to_request(&TEST_CREDENTIALS))
.await
.expect("Error while sending request");
let result = GetAccountVIPLevel::read_response(resp.into()).await;
let level_info: RespAccountVIPInfo = result.expect("failed to parse result");
assert_eq!(
level_info,
RespAccountVIPInfo {
current: RespVIPLevel {
level: 0,
minimum_trading_volume: dec!(0),
minimum_staking_volume: dec!(0),
maker_fee: dec!(0.00045),
taker_fee: dec!(0.0015),
},
next: RespVIPLevel {
level: 1,
minimum_trading_volume: dec!(3000000),
minimum_staking_volume: dec!(500),
maker_fee: dec!(0.00035999999999999997),
taker_fee: dec!(0.00135),
},
}
);
}
#[async_std::test]
async fn get_account_of_currency() {
let params = GetAccountOfCurrency {
path_currency: "doge".into(),
};
let resp = create_client("get_account_of_currency.yaml")
.await
.send(params.to_request(&TEST_CREDENTIALS))
.await
.expect("Error while sending request");
let result = GetAccountOfCurrency::read_response(resp.into()).await;
let account_currency: RespAccountCurrencyInfo = result.expect("failed to parse result");
assert_eq!(
account_currency,
RespAccountCurrencyInfo {
currency: "doge".into(),
balance: dec!(10000.25),
locked: dec!(0.0),
wallet_type: "exchange".into(),
fiat_currency: None,
fiat_balance: None,
}
);
}
#[async_std::test]
async fn get_internal_transfers() {
let params = GetInternalTransfers {
currency: Some("max".into()),
side: InternalTransferSide::In,
from_timestamp: None,
to_timestamp: None,
pagination: None,
page_params: None,
offset: None,
};
let resp = create_client("get_internal_transfers.yaml")
.await
.send(params.to_request(&TEST_CREDENTIALS))
.await
.expect("Error while sending request");
let result = GetInternalTransfers::read_response(resp.into()).await;
let transfer_history: Vec<RespInternalTransferRecord> =
result.expect("failed to parse result");
assert_eq!(
transfer_history,
vec![RespInternalTransferRecord {
uuid: "(test erased uuid)".into(),
currency: "max".into(),
amount: dec!(1.0),
created_at: Some(Utc.timestamp(1605265665, 0)),
state: "done".into(),
from_member: "(test erased from_member)".into(),
to_member: "(test erased to_member)".into()
}]
);
}
#[async_std::test]
async fn get_transfers_by_uuid() {
let params = GetInternalTransferByUUID {
uuid: "2011131107100357467635".into(),
};
let resp = create_client("get_transfers_by_uuid.yaml")
.await
.send(params.to_request(&TEST_CREDENTIALS))
.await
.expect("Error while sending request");
let result = GetInternalTransferByUUID::read_response(resp.into()).await;
let record: RespInternalTransferRecord = result.expect("failed to parse result");
assert_eq!(
record,
RespInternalTransferRecord {
uuid: "(test erased uuid)".into(),
currency: "max".into(),
amount: dec!(1.0),
created_at: Some(Utc.timestamp(1605265665, 0)),
state: "done".into(),
from_member: "(test erased from_member)".into(),
to_member: "(test erased to_member)".into()
}
);
}
#[async_std::test]
async fn get_rewards() {
let params = GetRewards {
currency: Some("max".into()),
from_timestamp: Some(Utc.timestamp(1637316000, 0)),
to_timestamp: None,
pagination: None,
page_params: None,
offset: None,
};
let resp = create_client("get_rewards.yaml")
.await
.send(params.to_request(&TEST_CREDENTIALS))
.await
.expect("Error while sending request");
let result = GetRewards::read_response(resp.into()).await;
let reward_history: Vec<RewardRecord> = result.expect("failed to parse result");
assert_eq!(
reward_history,
vec![RewardRecord {
uuid: "(test erased uuid)".into(),
reward_type: RewardType::HoldingReward,
currency: "max".into(),
amount: dec!(6.21724144),
created_at: Some(Utc.timestamp(1637346829, 0)),
state: "done".into(),
note: "(test erased note)".into()
}]
);
}
#[async_std::test]
async fn get_rewards_of_type() {
let params = GetRewardsOfType {
reward_type: RewardType::HoldingReward,
detail: GetRewards {
currency: Some("max".into()),
from_timestamp: Some(Utc.timestamp(1637316000, 0)),
to_timestamp: None,
pagination: None,
page_params: None,
offset: None,
},
};
let resp = create_client("get_rewards_of_type.yaml")
.await
.send(params.to_request(&TEST_CREDENTIALS))
.await
.expect("Error while sending request");
let result = GetRewardsOfType::read_response(resp.into()).await;
let reward_history: Vec<RewardRecord> = result.expect("failed to parse result");
assert_eq!(
reward_history,
vec![RewardRecord {
uuid: "(test erased uuid)".into(),
reward_type: RewardType::HoldingReward,
currency: "max".into(),
amount: dec!(6.21724144),
created_at: Some(Utc.timestamp(1637346829, 0)),
state: "done".into(),
note: "(test erased note)".into()
}]
);
}
#[async_std::test]
async fn get_saving_interest_history() {
let params = GetSavingInterestHistory {
currency: "usdt".to_string(),
from_timestamp: Some(Utc.timestamp(1634724000, 0)),
to_timestamp: None,
pagination: None,
page_params: None,
offset: None,
};
let resp = create_client("get_saving_interest_history.yaml")
.await
.send(params.to_request(&TEST_CREDENTIALS))
.await
.expect("Error while sending request");
let result = GetSavingInterestHistory::read_response(resp.into()).await;
let interest_history: Vec<RewardRecord> = result.expect("failed to parse result");
assert_eq!(
interest_history,
vec![
RewardRecord {
uuid: "(test erased uuid)".to_string(),
reward_type: RewardType::SavingsInterest,
currency: "usdt".to_string(),
amount: dec!(0.00005154),
created_at: Some(Utc.timestamp(1635711201, 0)),
state: "done".to_string(),
note: "(test erased note)".to_string()
},
RewardRecord {
uuid: "(test erased uuid)".to_string(),
reward_type: RewardType::SavingsInterest,
currency: "usdt".to_string(),
amount: dec!(0.03194253),
created_at: Some(Utc.timestamp(1634760738, 0)),
state: "done".to_string(),
note: "(test erased note)".to_string()
}
]
);
}
#[async_std::test]
async fn get_max_rewards_yesterday() {
let params = GetMaxRewardsYesterday {};
let resp = create_client("get_max_rewards_yesterday.yaml")
.await
.send(params.to_request(&TEST_CREDENTIALS))
.await
.expect("Error while sending request");
let result = GetMaxRewardsYesterday::read_response(resp.into()).await;
let reward_info: RespMAXReward = result.expect("failed to parse result");
assert_eq!(
reward_info,
RespMAXReward {
trading_reward: dec!(0.0),
holding_reward: dec!(6.21724144),
}
);
}
}