Huobi: Add deposit address and withdraw quota API support (#433)

* Add Huobi deposit and withdraw quota API support
Plus perform come code deduplication

* Go mod tidy
This commit is contained in:
Adrian Gallagher
2020-02-03 12:44:46 +11:00
committed by GitHub
parent a32d16e1f5
commit b8dfa443d3
6 changed files with 210 additions and 210 deletions

View File

@@ -24,8 +24,9 @@ import (
)
const (
huobiAPIURL = "https://api.huobi.pro"
huobiAPIVersion = "1"
huobiAPIURL = "https://api.huobi.pro"
huobiAPIVersion = "1"
huobiAPIVersion2 = "2"
huobiMarketHistoryKline = "market/history/kline"
huobiMarketDetail = "market/detail"
@@ -39,6 +40,8 @@ const (
huobiTimestamp = "common/timestamp"
huobiAccounts = "account/accounts"
huobiAccountBalance = "account/accounts/%s/balance"
huobiAccountDepositAddress = "account/deposit/address"
huobiAccountWithdrawQuota = "account/withdraw/quota"
huobiAggregatedBalance = "subuser/aggregate-balance"
huobiOrderPlace = "order/orders/place"
huobiOrderCancel = "order/orders/%s/submitcancel"
@@ -60,6 +63,8 @@ const (
huobiAuthRate = 100
huobiUnauthRate = 100
huobiStatusError = "error"
)
// HUOBI is the overarching type across this package
@@ -287,61 +292,38 @@ func (h *HUOBI) GetTimestamp() (int64, error) {
// GetAccounts returns the Huobi user accounts
func (h *HUOBI) GetAccounts() ([]Account, error) {
type response struct {
Response
AccountData []Account `json:"data"`
}
var result response
err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiAccounts, url.Values{}, nil, &result)
if result.ErrorMessage != "" {
return nil, errors.New(result.ErrorMessage)
}
return result.AccountData, err
result := struct {
Accounts []Account `json:"data"`
}{}
err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiAccounts, url.Values{}, nil, &result, false)
return result.Accounts, err
}
// GetAccountBalance returns the users Huobi account balance
func (h *HUOBI) GetAccountBalance(accountID string) ([]AccountBalanceDetail, error) {
type response struct {
Response
result := struct {
AccountBalanceData AccountBalance `json:"data"`
}
var result response
}{}
endpoint := fmt.Sprintf(huobiAccountBalance, accountID)
v := url.Values{}
v.Set("account-id", accountID)
err := h.SendAuthenticatedHTTPRequest(http.MethodGet, endpoint, v, nil, &result)
if result.ErrorMessage != "" {
return nil, errors.New(result.ErrorMessage)
}
err := h.SendAuthenticatedHTTPRequest(http.MethodGet, endpoint, v, nil, &result, false)
return result.AccountBalanceData.AccountBalanceDetails, err
}
// GetAggregatedBalance returns the balances of all the sub-account aggregated.
func (h *HUOBI) GetAggregatedBalance() ([]AggregatedBalance, error) {
type response struct {
Response
result := struct {
AggregatedBalances []AggregatedBalance `json:"data"`
}
var result response
}{}
err := h.SendAuthenticatedHTTPRequest(
http.MethodGet,
huobiAggregatedBalance,
nil,
nil,
&result,
false,
)
if result.ErrorMessage != "" {
return nil, errors.New(result.ErrorMessage)
}
return result.AggregatedBalances, err
}
@@ -370,35 +352,28 @@ func (h *HUOBI) SpotNewOrder(arg SpotNewOrderRequestParams) (int64, error) {
data.Source = arg.Source
}
type response struct {
Response
result := struct {
OrderID int64 `json:"data,string"`
}
var result response
err := h.SendAuthenticatedHTTPRequest(http.MethodPost, huobiOrderPlace, nil, data, &result)
if result.ErrorMessage != "" {
return 0, errors.New(result.ErrorMessage)
}
}{}
err := h.SendAuthenticatedHTTPRequest(
http.MethodPost,
huobiOrderPlace,
nil,
data,
&result,
false,
)
return result.OrderID, err
}
// CancelExistingOrder cancels an order on Huobi
func (h *HUOBI) CancelExistingOrder(orderID int64) (int64, error) {
type response struct {
Response
resp := struct {
OrderID int64 `json:"data,string"`
}
var result response
}{}
endpoint := fmt.Sprintf(huobiOrderCancel, strconv.FormatInt(orderID, 10))
err := h.SendAuthenticatedHTTPRequest(http.MethodPost, endpoint, url.Values{}, nil, &result)
if result.ErrorMessage != "" {
return 0, errors.New(result.ErrorMessage)
}
return result.OrderID, err
err := h.SendAuthenticatedHTTPRequest(http.MethodPost, endpoint, url.Values{}, nil, &resp, false)
return resp.OrderID, err
}
// CancelOrderBatch cancels a batch of orders -- to-do
@@ -409,7 +384,7 @@ func (h *HUOBI) CancelOrderBatch(_ []int64) ([]CancelOrderBatch, error) {
}
var result response
err := h.SendAuthenticatedHTTPRequest(http.MethodPost, huobiOrderCancelBatch, url.Values{}, nil, &result)
err := h.SendAuthenticatedHTTPRequest(http.MethodPost, huobiOrderCancelBatch, url.Values{}, nil, &result, false)
if result.ErrorMessage != "" {
return nil, errors.New(result.ErrorMessage)
@@ -432,7 +407,7 @@ func (h *HUOBI) CancelOpenOrdersBatch(accountID, symbol string) (CancelOpenOrder
Symbol: symbol,
}
err := h.SendAuthenticatedHTTPRequest(http.MethodPost, huobiBatchCancelOpenOrders, url.Values{}, data, &result)
err := h.SendAuthenticatedHTTPRequest(http.MethodPost, huobiBatchCancelOpenOrders, url.Values{}, data, &result, false)
if result.Data.FailedCount > 0 {
return result, fmt.Errorf("there were %v failed order cancellations", result.Data.FailedCount)
}
@@ -442,45 +417,35 @@ func (h *HUOBI) CancelOpenOrdersBatch(accountID, symbol string) (CancelOpenOrder
// GetOrder returns order information for the specified order
func (h *HUOBI) GetOrder(orderID int64) (OrderInfo, error) {
type response struct {
Response
resp := struct {
Order OrderInfo `json:"data"`
}
var result response
}{}
urlVal := url.Values{}
urlVal.Set("clientOrderId", strconv.FormatInt(orderID, 10))
err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiGetOrder, urlVal, nil, &result)
if result.ErrorMessage != "" {
return result.Order, errors.New(result.ErrorMessage)
}
return result.Order, err
err := h.SendAuthenticatedHTTPRequest(http.MethodGet,
huobiGetOrder,
urlVal,
nil,
&resp,
false)
return resp.Order, err
}
// GetOrderMatchResults returns matched order info for the specified order
func (h *HUOBI) GetOrderMatchResults(orderID int64) ([]OrderMatchInfo, error) {
type response struct {
Response
resp := struct {
Orders []OrderMatchInfo `json:"data"`
}
var result response
}{}
endpoint := fmt.Sprintf(huobiGetOrderMatch, strconv.FormatInt(orderID, 10))
err := h.SendAuthenticatedHTTPRequest(http.MethodGet, endpoint, url.Values{}, nil, &result)
if result.ErrorMessage != "" {
return nil, errors.New(result.ErrorMessage)
}
return result.Orders, err
err := h.SendAuthenticatedHTTPRequest(http.MethodGet, endpoint, url.Values{}, nil, &resp, false)
return resp.Orders, err
}
// GetOrders returns a list of orders
func (h *HUOBI) GetOrders(symbol, types, start, end, states, from, direct, size string) ([]OrderInfo, error) {
type response struct {
Response
resp := struct {
Orders []OrderInfo `json:"data"`
}
}{}
vals := url.Values{}
vals.Set("symbol", symbol)
@@ -510,21 +475,15 @@ func (h *HUOBI) GetOrders(symbol, types, start, end, states, from, direct, size
vals.Set("size", size)
}
var result response
err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiGetOrders, vals, nil, &result)
if result.ErrorMessage != "" {
return nil, errors.New(result.ErrorMessage)
}
return result.Orders, err
err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiGetOrders, vals, nil, &resp, false)
return resp.Orders, err
}
// GetOpenOrders returns a list of orders
func (h *HUOBI) GetOpenOrders(accountID, symbol, side string, size int64) ([]OrderInfo, error) {
type response struct {
Response
resp := struct {
Orders []OrderInfo `json:"data"`
}
}{}
vals := url.Values{}
vals.Set("symbol", symbol)
@@ -534,22 +493,15 @@ func (h *HUOBI) GetOpenOrders(accountID, symbol, side string, size int64) ([]Ord
}
vals.Set("size", strconv.FormatInt(size, 10))
var result response
err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiGetOpenOrders, vals, nil, &result)
if result.ErrorMessage != "" {
return nil, errors.New(result.ErrorMessage)
}
return result.Orders, err
err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiGetOpenOrders, vals, nil, &resp, false)
return resp.Orders, err
}
// GetOrdersMatch returns a list of matched orders
func (h *HUOBI) GetOrdersMatch(symbol, types, start, end, from, direct, size string) ([]OrderMatchInfo, error) {
type response struct {
Response
resp := struct {
Orders []OrderMatchInfo `json:"data"`
}
}{}
vals := url.Values{}
vals.Set("symbol", symbol)
@@ -578,13 +530,8 @@ func (h *HUOBI) GetOrdersMatch(symbol, types, start, end, from, direct, size str
vals.Set("size", size)
}
var result response
err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiGetOrdersMatch, vals, nil, &result)
if result.ErrorMessage != "" {
return nil, errors.New(result.ErrorMessage)
}
return result.Orders, err
err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiGetOrdersMatch, vals, nil, &resp, false)
return resp.Orders, err
}
// MarginTransfer transfers assets into or out of the margin account
@@ -604,18 +551,11 @@ func (h *HUOBI) MarginTransfer(symbol, currency string, amount float64, in bool)
path = huobiMarginTransferOut
}
type response struct {
Response
resp := struct {
TransferID int64 `json:"data"`
}
var result response
err := h.SendAuthenticatedHTTPRequest(http.MethodPost, path, nil, data, &result)
if result.ErrorMessage != "" {
return 0, errors.New(result.ErrorMessage)
}
return result.TransferID, err
}{}
err := h.SendAuthenticatedHTTPRequest(http.MethodPost, path, nil, data, &resp, false)
return resp.TransferID, err
}
// MarginOrder submits a margin order application
@@ -630,18 +570,11 @@ func (h *HUOBI) MarginOrder(symbol, currency string, amount float64) (int64, err
Amount: strconv.FormatFloat(amount, 'f', -1, 64),
}
type response struct {
Response
resp := struct {
MarginOrderID int64 `json:"data"`
}
var result response
err := h.SendAuthenticatedHTTPRequest(http.MethodPost, huobiMarginOrders, nil, data, &result)
if result.ErrorMessage != "" {
return 0, errors.New(result.ErrorMessage)
}
return result.MarginOrderID, err
}{}
err := h.SendAuthenticatedHTTPRequest(http.MethodPost, huobiMarginOrders, nil, data, &resp, false)
return resp.MarginOrderID, err
}
// MarginRepayment repays a margin amount for a margin ID
@@ -652,19 +585,13 @@ func (h *HUOBI) MarginRepayment(orderID int64, amount float64) (int64, error) {
Amount: strconv.FormatFloat(amount, 'f', -1, 64),
}
type response struct {
Response
resp := struct {
MarginOrderID int64 `json:"data"`
}
}{}
var result response
endpoint := fmt.Sprintf(huobiMarginRepay, strconv.FormatInt(orderID, 10))
err := h.SendAuthenticatedHTTPRequest(http.MethodPost, endpoint, nil, data, &result)
if result.ErrorMessage != "" {
return 0, errors.New(result.ErrorMessage)
}
return result.MarginOrderID, err
err := h.SendAuthenticatedHTTPRequest(http.MethodPost, endpoint, nil, data, &resp, false)
return resp.MarginOrderID, err
}
// GetMarginLoanOrders returns the margin loan orders
@@ -697,47 +624,31 @@ func (h *HUOBI) GetMarginLoanOrders(symbol, currency, start, end, states, from,
vals.Set("size", size)
}
type response struct {
Response
resp := struct {
MarginLoanOrders []MarginOrder `json:"data"`
}
var result response
err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiMarginLoanOrders, vals, nil, &result)
if result.ErrorMessage != "" {
return nil, errors.New(result.ErrorMessage)
}
return result.MarginLoanOrders, err
}{}
err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiMarginLoanOrders, vals, nil, &resp, false)
return resp.MarginLoanOrders, err
}
// GetMarginAccountBalance returns the margin account balances
func (h *HUOBI) GetMarginAccountBalance(symbol string) ([]MarginAccountBalance, error) {
type response struct {
Response
resp := struct {
Balances []MarginAccountBalance `json:"data"`
}
}{}
vals := url.Values{}
if symbol != "" {
vals.Set("symbol", symbol)
}
var result response
err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiMarginAccountBalance, vals, nil, &result)
if result.ErrorMessage != "" {
return nil, errors.New(result.ErrorMessage)
}
return result.Balances, err
err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiMarginAccountBalance, vals, nil, &resp, false)
return resp.Balances, err
}
// Withdraw withdraws the desired amount and currency
func (h *HUOBI) Withdraw(c currency.Code, address, addrTag string, amount, fee float64) (int64, error) {
type response struct {
Response
resp := struct {
WithdrawID int64 `json:"data"`
}
}{}
data := struct {
Address string `json:"address"`
@@ -759,33 +670,56 @@ func (h *HUOBI) Withdraw(c currency.Code, address, addrTag string, amount, fee f
data.AddrTag = addrTag
}
var result response
err := h.SendAuthenticatedHTTPRequest(http.MethodPost, huobiWithdrawCreate, nil, data, &result)
if result.ErrorMessage != "" {
return 0, errors.New(result.ErrorMessage)
}
return result.WithdrawID, err
err := h.SendAuthenticatedHTTPRequest(http.MethodPost, huobiWithdrawCreate, nil, data, &resp.WithdrawID, false)
return resp.WithdrawID, err
}
// CancelWithdraw cancels a withdraw request
func (h *HUOBI) CancelWithdraw(withdrawID int64) (int64, error) {
type response struct {
Response
resp := struct {
WithdrawID int64 `json:"data"`
}
}{}
vals := url.Values{}
vals.Set("withdraw-id", strconv.FormatInt(withdrawID, 10))
var result response
endpoint := fmt.Sprintf(huobiWithdrawCancel, strconv.FormatInt(withdrawID, 10))
err := h.SendAuthenticatedHTTPRequest(http.MethodPost, endpoint, vals, nil, &result)
err := h.SendAuthenticatedHTTPRequest(http.MethodPost, endpoint, vals, nil, &resp, false)
return resp.WithdrawID, err
}
if result.ErrorMessage != "" {
return 0, errors.New(result.ErrorMessage)
// QueryDepositAddress returns the deposit address for a specified currency
func (h *HUOBI) QueryDepositAddress(cryptocurrency string) (DepositAddress, error) {
resp := struct {
DepositAddress []DepositAddress `json:"data"`
}{}
vals := url.Values{}
vals.Set("currency", cryptocurrency)
err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiAccountDepositAddress, vals, nil, &resp, true)
if err != nil {
return DepositAddress{}, err
}
return result.WithdrawID, err
if len(resp.DepositAddress) == 0 {
return DepositAddress{}, errors.New("deposit address data isn't populated")
}
return resp.DepositAddress[0], nil
}
// QueryWithdrawQuotas returns the users cryptocurrency withdraw quotas
func (h *HUOBI) QueryWithdrawQuotas(cryptocurrency string) (WithdrawQuota, error) {
resp := struct {
WithdrawQuota WithdrawQuota `json:"data"`
}{}
vals := url.Values{}
vals.Set("currency", cryptocurrency)
err := h.SendAuthenticatedHTTPRequest(http.MethodGet, huobiAccountWithdrawQuota, vals, nil, &resp, true)
if err != nil {
return WithdrawQuota{}, err
}
return resp.WithdrawQuota, nil
}
// SendHTTPRequest sends an unauthenticated HTTP request
@@ -803,7 +737,7 @@ func (h *HUOBI) SendHTTPRequest(path string, result interface{}) error {
}
// SendAuthenticatedHTTPRequest sends authenticated requests to the HUOBI API
func (h *HUOBI) SendAuthenticatedHTTPRequest(method, endpoint string, values url.Values, data, result interface{}) error {
func (h *HUOBI) SendAuthenticatedHTTPRequest(method, endpoint string, values url.Values, data, result interface{}, isVersion2API bool) error {
if !h.AllowAuthenticatedRequest() {
return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, h.Name)
}
@@ -817,7 +751,12 @@ func (h *HUOBI) SendAuthenticatedHTTPRequest(method, endpoint string, values url
values.Set("SignatureVersion", "2")
values.Set("Timestamp", time.Now().UTC().Format("2006-01-02T15:04:05"))
endpoint = fmt.Sprintf("/v%s/%s", huobiAPIVersion, endpoint)
if isVersion2API {
endpoint = fmt.Sprintf("/v%s/%s", huobiAPIVersion2, endpoint)
} else {
endpoint = fmt.Sprintf("/v%s/%s", huobiAPIVersion, endpoint)
}
payload := fmt.Sprintf("%s\napi.huobi.pro\n%s\n%s",
method, endpoint, values.Encode())
@@ -864,26 +803,45 @@ func (h *HUOBI) SendAuthenticatedHTTPRequest(method, endpoint string, values url
urlPath := h.API.Endpoints.URL + common.EncodeURLValues(endpoint, values)
var body []byte
if data != nil {
encoded, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("%s unable to marshal data: %s", h.Name, err)
}
body = encoded
}
return h.SendPayload(method,
interim := json.RawMessage{}
err := h.SendPayload(method,
urlPath,
headers,
bytes.NewReader(body),
result,
bytes.NewBuffer(body),
&interim,
true,
false,
h.Verbose,
h.HTTPDebugging,
h.HTTPRecording)
if err != nil {
return err
}
if isVersion2API {
var errCap ResponseV2
if err = json.Unmarshal(interim, &errCap); err == nil {
if errCap.Code != 200 && errCap.Message != "" {
return errors.New(errCap.Message)
}
}
} else {
var errCap Response
if err = json.Unmarshal(interim, &errCap); err == nil {
if errCap.Status == huobiStatusError && errCap.ErrorMessage != "" {
return errors.New(errCap.ErrorMessage)
}
}
}
return json.Unmarshal(interim, result)
}
// GetFee returns an estimate of fee based on type of transaction

View File

@@ -634,10 +634,23 @@ func TestWithdrawInternationalBank(t *testing.T) {
}
}
func TestGetDepositAddress(t *testing.T) {
_, err := h.GetDepositAddress(currency.BTC, "")
if err == nil {
t.Error("GetDepositAddress() error cannot be nil")
func TestQueryDepositAddress(t *testing.T) {
_, err := h.QueryDepositAddress(currency.BTC.Lower().String())
if !areTestAPIKeysSet() && err == nil {
t.Error("Expecting an error when no keys are set")
}
if areTestAPIKeysSet() && err != nil {
t.Error(err)
}
}
func TestQueryWithdrawQuota(t *testing.T) {
_, err := h.QueryWithdrawQuotas(currency.BTC.Lower().String())
if !areTestAPIKeysSet() && err == nil {
t.Error("Expecting an error when no keys are set")
}
if areTestAPIKeysSet() && err != nil {
t.Error(err)
}
}

View File

@@ -9,6 +9,12 @@ type Response struct {
ErrorMessage string `json:"err-msg"`
}
// ResponseV2 stores the Huobi generic response info
type ResponseV2 struct {
Code int32 `json:"code"`
Message string `json:"message"`
}
// KlineItem stores a kline item
type KlineItem struct {
ID int64 `json:"id"`
@@ -246,6 +252,32 @@ type SpotNewOrderRequestParams struct {
Type SpotNewOrderRequestParamsType `json:"type"` // 订单类型, buy-market: 市价买, sell-market: 市价卖, buy-limit: 限价买, sell-limit: 限价卖
}
// DepositAddress stores the users deposit address info
type DepositAddress struct {
Currency string `json:"currency"`
Address string `json:"address"`
AddressTag string `json:"addressTag"`
Chain string `json:"chain"`
}
// ChainQuota stores the users currency chain quota
type ChainQuota struct {
Chain string `json:"chain"`
MaxWithdrawAmount float64 `json:"maxWithdrawAmt,string"`
WithdrawQuotaPerDay float64 `json:"withdrawQuotaPerDay,string"`
RemainingWithdrawQuotaPerDay float64 `json:"remainWithdrawQuotaPerDay,string"`
WithdrawQuotaPerYear float64 `json:"withdrawQuotaPerYear,string"`
RemainingWithdrawQuotaPerYear float64 `json:"remainWithdrawQuotaPerYear,string"`
WithdrawQuotaTotal float64 `json:"withdrawQuotaTotal,string"`
RemainingWithdrawQuotaTotal float64 `json:"remainWithdrawQuotaTotal,string"`
}
// WithdrawQuota stores the users withdraw quotas
type WithdrawQuota struct {
Currency string `json:"currency"`
Chains []ChainQuota `json:"chains"`
}
// SpotNewOrderRequestParamsType order type
type SpotNewOrderRequestParamsType string

View File

@@ -87,6 +87,7 @@ func (h *HUOBI) SetDefaults() {
CancelOrders: true,
CancelOrder: true,
SubmitOrder: true,
CryptoDeposit: true,
CryptoWithdrawal: true,
TradeFee: true,
},
@@ -652,7 +653,8 @@ func (h *HUOBI) GetOrderInfo(orderID string) (order.Detail, error) {
// GetDepositAddress returns a deposit address for a specified currency
func (h *HUOBI) GetDepositAddress(cryptocurrency currency.Code, accountID string) (string, error) {
return "", common.ErrFunctionNotSupported
resp, err := h.QueryDepositAddress(cryptocurrency.Lower().String())
return resp.Address, err
}
// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is