Files
gocryptotrader/exchanges/zb/zb.go
Scott fcc5ad4551 exchanges/qa: Add exchange wrapper testing suite (#1159)
* initial concept of a nice validation tester for exchanges

* adds some datahandler design

* expand testing

* more tests and fixes

* minor end of day fix for bithumb

* fixes implementation issues

* more test coverage and improvements, but not sure if i should continue

* fix more wrapper implementations

* adds error type, more fixes

* changes signature, fixes implementations

* fixes more wrapper implementations

* one more bit

* more cleanup

* WOW things work?

* lintle 1/1337

* mini bump

* fixes all linting

* neaten

* GetOrderInfo+ asset pair fixes+improvements

* adds new websocket test

* expand ws testing

* fix bug, expand tests, improve implementation

* code coverage of a lot of new codes

* fixes everything

* reverts accidental changes

* minor fixes from reviewing code

* removes Bitfinex cancelBatchOrder implementation

* fixes dumb baby typo for babies

* mini nit fixes

* so many nits to address

* addresses all the nits

* Titlecase

* switcheroo

* removes websocket testing for now

* fix appveyor, minor test fix

* fixes typo, re-kindles killed kode

* skip binance wrapper tests when running CI

* expired context, huobi okx fixes

* kodespull

* fix ordering

* time fix because why not

* fix exmo, others

* hopefully this fixes all of my life's problems

* last thing today

* huobi, more like hypotrophy

* golangci-lint, more like mypooroldknee-splint

* fix huobi times by removing them

* should fix okx currency issues

* blocks the application

* adds last little contingency for pairs

* addresses most nits and new problems

* lovely fixed before seeing why okx sucks

* fixes issues with okx websocket

* the classic receieieivaier

* lintle

* adds test and fixes existing tests

* expands error handling messages during setup

* fixes dumb okx bugs introduced

* quick fix for lint and exmo

* fixes nixes

* fix exmo deposit issue

* lint

* fixes issue with extra asset runs missing

* fix surprise race

* all the lint and merge fixes

* fixes surprise bugs in OKx

* fixes issues with times and chains

* fixing all the merge stuff

* merge fix

* rm logs and a panic potential

* lovely lint lament

* an easy demonstration of scenario, but not of initial purpose

* put it in the bin

* Revert "put it in the bin"

This reverts commit 15c6490f713233d43f10957367fcbf18e3818bdd.

* re-add after immediate error popup

* fix mini poor test design

* okx okay

* merge fixes

* fixes issues discovered in lovely test

* I FORGOT TO COMMIT THIS

* nit fixaroonaboo

* forgoetten test fix

* revert old okx asset intrument work

* fixes

* revert problems I didnt understand. update bybit

* fix merge bugs

* test cleanup

* further improvements

* reshuffle and lint

* rm redundant CI_TEST by rm the CI_TEST field that is redundant

* path fix

* move to its own section, dont run on 32 bit + appveyor

* lint

* fix lbank

* address nits

* let it rip

* fix failing test time range

* niteroo boogaloo

* mod tidy, use common.SimpleTimeFormat
2023-07-03 11:09:43 +10:00

593 lines
18 KiB
Go

package zb
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
)
const (
zbTradeURL = "https://api.zb.com"
zbMarketURL = "https://trade.zb.com/api"
zbAPIVersion = "v1"
zbData = "data"
zbAccountInfo = "getAccountInfo"
zbMarkets = "markets"
zbKline = "kline"
zbOrder = "order"
zbCancelOrder = "cancelOrder"
zbTicker = "ticker"
zbTrades = "trades"
zbTickers = "allTicker"
zbDepth = "depth"
zbUnfinishedOrdersIgnoreTradeType = "getUnfinishedOrdersIgnoreTradeType"
zbGetOrdersGet = "getOrders"
zbGetOrder = "getOrder"
zbWithdraw = "withdraw"
zbDepositAddress = "getUserAddress"
zbMultiChainDepositAddress = "getPayinAddress"
zbWithdrawalRecords = "getWithdrawRecord"
zbDepositRecords = "getChargeRecord"
)
// ZB is the overarching type across this package
// 47.91.169.147 api.zb.com
// 47.52.55.212 trade.zb.com
type ZB struct {
exchange.Base
}
// SpotNewOrder submits an order to ZB
func (z *ZB) SpotNewOrder(ctx context.Context, arg SpotNewOrderRequestParams) (int64, error) {
creds, err := z.GetCredentials(ctx)
if err != nil {
return 0, err
}
var result SpotNewOrderResponse
vals := url.Values{}
vals.Set("accesskey", creds.Key)
vals.Set("method", "order")
vals.Set("amount", strconv.FormatFloat(arg.Amount, 'f', -1, 64))
vals.Set("currency", arg.Symbol)
vals.Set("price", strconv.FormatFloat(arg.Price, 'f', -1, 64))
vals.Set("tradeType", string(arg.Type))
err = z.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpotSupplementary, http.MethodGet, vals, &result, request.Auth)
if err != nil {
return 0, err
}
if result.Code != 1000 {
return 0, fmt.Errorf("%w unsuccessful new order, message: %s code: %d", request.ErrAuthRequestFailed, result.Message, result.Code)
}
newOrderID, err := strconv.ParseInt(result.ID, 10, 64)
if err != nil {
return 0, err
}
return newOrderID, nil
}
// GetDepositRecords returns the deposit records
func (z *ZB) GetDepositRecords(ctx context.Context, arg *WalletRecordsRequest) (*DepositRecordsResponse, error) {
if arg == nil {
return nil, fmt.Errorf("%w WalletRecordsRequest", common.ErrNilPointer)
}
var resp DepositRecordsResponse
vals := url.Values{}
vals.Set("method", "getChargeRecord")
vals.Set("currency", arg.Currency.String())
if arg.PageSize > 0 {
vals.Set("pageIndex", strconv.FormatInt(arg.PageIndex, 10))
}
if arg.PageIndex > 0 {
vals.Set("pageSize", strconv.FormatInt(arg.PageSize, 10))
}
return &resp, z.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpotSupplementary, http.MethodGet, vals, &resp, request.Auth)
}
// GetWithdrawalRecords returns the withdrawal records
func (z *ZB) GetWithdrawalRecords(ctx context.Context, arg *WalletRecordsRequest) (*WithdrawalRecordsResponse, error) {
if arg == nil {
return nil, fmt.Errorf("%w WalletRecordsRequest", common.ErrNilPointer)
}
var resp WithdrawalRecordsResponse
vals := url.Values{}
vals.Set("method", "getWithdrawRecord")
vals.Set("currency", arg.Currency.String())
vals.Set("pageIndex", strconv.FormatInt(arg.PageIndex, 10))
vals.Set("pageSize", strconv.FormatInt(arg.PageSize, 10))
return &resp, z.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpotSupplementary, http.MethodGet, vals, &resp, request.Auth)
}
// CancelExistingOrder cancels an order
func (z *ZB) CancelExistingOrder(ctx context.Context, orderID int64, symbol string) error {
creds, err := z.GetCredentials(ctx)
if err != nil {
return err
}
type response struct {
Code int `json:"code"` // Result code
Message string `json:"message"` // Result Message
}
vals := url.Values{}
vals.Set("accesskey", creds.Key)
vals.Set("method", "cancelOrder")
vals.Set("id", strconv.FormatInt(orderID, 10))
vals.Set("currency", symbol)
var result response
err = z.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpotSupplementary, http.MethodGet, vals, &result, request.Auth)
if err != nil {
return err
}
if result.Code != 1000 {
return fmt.Errorf("%w %v", request.ErrAuthRequestFailed, result.Message)
}
return nil
}
// GetAccountInformation returns account information including coin information
// and pricing
func (z *ZB) GetAccountInformation(ctx context.Context) (AccountsResponse, error) {
creds, err := z.GetCredentials(ctx)
if err != nil {
return AccountsResponse{}, err
}
var result AccountsResponse
vals := url.Values{}
vals.Set("accesskey", creds.Key)
vals.Set("method", "getAccountInfo")
return result, z.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpotSupplementary, http.MethodGet, vals, &result, request.Auth)
}
// GetUnfinishedOrdersIgnoreTradeType returns unfinished orders
func (z *ZB) GetUnfinishedOrdersIgnoreTradeType(ctx context.Context, currency string, pageindex, pagesize int64) ([]Order, error) {
creds, err := z.GetCredentials(ctx)
if err != nil {
return nil, err
}
var result []Order
vals := url.Values{}
vals.Set("accesskey", creds.Key)
vals.Set("method", zbUnfinishedOrdersIgnoreTradeType)
vals.Set("currency", currency)
vals.Set("pageIndex", strconv.FormatInt(pageindex, 10))
vals.Set("pageSize", strconv.FormatInt(pagesize, 10))
return result, z.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpotSupplementary, http.MethodGet, vals, &result, request.Auth)
}
// GetOrders returns finished orders
func (z *ZB) GetOrders(ctx context.Context, currency string, pageindex, side int64) ([]Order, error) {
creds, err := z.GetCredentials(ctx)
if err != nil {
return nil, err
}
var response []Order
vals := url.Values{}
vals.Set("accesskey", creds.Key)
vals.Set("method", zbGetOrdersGet)
vals.Set("currency", currency)
vals.Set("pageIndex", strconv.FormatInt(pageindex, 10))
vals.Set("tradeType", strconv.FormatInt(side, 10))
return response, z.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpotSupplementary, http.MethodGet, vals, &response, request.Auth)
}
// GetSingleOrder Get single buy order or sell order
func (z *ZB) GetSingleOrder(ctx context.Context, orderID, customerOrderID string, currency currency.Pair) (*Order, error) {
creds, err := z.GetCredentials(ctx)
if err != nil {
return nil, err
}
var response Order
pFmt, err := z.GetPairFormat(asset.Spot, true)
if err != nil {
return nil, err
}
vals := url.Values{}
vals.Set("accesskey", creds.Key)
vals.Set("method", zbGetOrder)
vals.Set("currency", pFmt.Format(currency))
if orderID != "" {
vals.Set("id", orderID)
}
if customerOrderID != "" {
vals.Set("customerOrderId", customerOrderID)
}
return &response, z.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpotSupplementary, http.MethodGet, vals, &response, request.Auth)
}
// GetMarkets returns market information including pricing, symbols and
// each symbols decimal precision
func (z *ZB) GetMarkets(ctx context.Context) (map[string]MarketResponseItem, error) {
endpoint := fmt.Sprintf("/%s/%s/%s", zbData, zbAPIVersion, zbMarkets)
var res map[string]MarketResponseItem
err := z.SendHTTPRequest(ctx, exchange.RestSpot, endpoint, &res, request.UnAuth)
if err != nil {
return nil, err
}
return res, nil
}
// GetLatestSpotPrice returns latest spot price of symbol
//
// symbol: string of currency pair
// 获取最新价格
func (z *ZB) GetLatestSpotPrice(ctx context.Context, symbol string) (float64, error) {
res, err := z.GetTicker(ctx, symbol)
if err != nil {
return 0, err
}
return res.Ticker.Last, nil
}
// GetTicker returns a ticker for a given symbol
func (z *ZB) GetTicker(ctx context.Context, symbol string) (TickerResponse, error) {
urlPath := fmt.Sprintf("/%s/%s/%s?market=%s", zbData, zbAPIVersion, zbTicker, symbol)
var res TickerResponse
err := z.SendHTTPRequest(ctx, exchange.RestSpot, urlPath, &res, request.UnAuth)
return res, err
}
// GetTrades returns trades for a given symbol
func (z *ZB) GetTrades(ctx context.Context, symbol string) (TradeHistory, error) {
urlPath := fmt.Sprintf("/%s/%s/%s?market=%s", zbData, zbAPIVersion, zbTrades, symbol)
var res TradeHistory
err := z.SendHTTPRequest(ctx, exchange.RestSpot, urlPath, &res, request.UnAuth)
return res, err
}
// GetTickers returns ticker data for all supported symbols
func (z *ZB) GetTickers(ctx context.Context) (map[string]TickerChildResponse, error) {
urlPath := fmt.Sprintf("/%s/%s/%s", zbData, zbAPIVersion, zbTickers)
resp := make(map[string]TickerChildResponse)
err := z.SendHTTPRequest(ctx, exchange.RestSpot, urlPath, &resp, request.UnAuth)
return resp, err
}
// GetOrderbook returns the orderbook for a given symbol
func (z *ZB) GetOrderbook(ctx context.Context, symbol string) (*OrderbookResponse, error) {
urlPath := fmt.Sprintf("/%s/%s/%s?market=%s", zbData, zbAPIVersion, zbDepth, symbol)
var res OrderbookResponse
err := z.SendHTTPRequest(ctx, exchange.RestSpot, urlPath, &res, request.UnAuth)
if err != nil {
return nil, err
}
if len(res.Asks) == 0 {
return nil, errors.New("ZB GetOrderbook asks is empty")
}
if len(res.Bids) == 0 {
return nil, errors.New("ZB GetOrderbook bids is empty")
}
// reverse asks data
eLen := len(res.Asks)
var target int
for i := eLen/2 - 1; i >= 0; i-- {
target = eLen - 1 - i
(res.Asks)[i], (res.Asks)[target] = (res.Asks)[target], (res.Asks)[i]
}
return &res, nil
}
// GetSpotKline returns Kline data
func (z *ZB) GetSpotKline(ctx context.Context, arg KlinesRequestParams) (KLineResponse, error) {
vals := url.Values{}
vals.Set("type", arg.Type)
vals.Set("market", arg.Symbol)
if arg.Since > 0 {
vals.Set("since", strconv.FormatInt(arg.Since, 10))
}
if arg.Size != 0 {
vals.Set("size", fmt.Sprintf("%d", arg.Size))
}
urlPath := fmt.Sprintf("/%s/%s/%s?%s", zbData, zbAPIVersion, zbKline, vals.Encode())
var res KLineResponse
resp := struct {
Data [][]float64 `json:"data"`
MoneyType string `json:"moneyType"`
Symbol string `json:"symbol"`
}{}
err := z.SendHTTPRequest(ctx, exchange.RestSpot, urlPath, &resp, klineFunc)
if err != nil {
return res, err
}
if resp.Data == nil || resp.Symbol == "" || resp.MoneyType == "" {
return res, errors.New("GetSpotKline received empty data")
}
res.MoneyType = resp.MoneyType
res.Symbol = resp.Symbol
for x := range resp.Data {
if len(resp.Data[x]) < 6 {
return res, errors.New("unexpected kline data length")
}
timestamp, err := convert.TimeFromUnixTimestampFloat(resp.Data[x][0])
if err != nil {
return res, err
}
res.Data = append(res.Data, &KLineResponseData{
KlineTime: timestamp,
Open: resp.Data[x][1],
High: resp.Data[x][2],
Low: resp.Data[x][3],
Close: resp.Data[x][4],
Volume: resp.Data[x][5],
})
}
return res, nil
}
// GetCryptoAddress fetches and returns the deposit address
// NOTE - PLEASE BE AWARE THAT YOU NEED TO GENERATE A DEPOSIT ADDRESS VIA
// LOGGING IN AND NOT BY USING THIS ENDPOINT OTHERWISE THIS WILL GIVE YOU A
// GENERAL ERROR RESPONSE.
func (z *ZB) GetCryptoAddress(ctx context.Context, currency currency.Code) (*UserAddress, error) {
var resp UserAddress
vals := url.Values{}
vals.Set("method", zbDepositAddress)
vals.Set("currency", currency.Lower().String())
if err := z.SendAuthenticatedHTTPRequest(ctx,
exchange.RestSpotSupplementary,
http.MethodGet,
vals,
&resp,
request.Auth); err != nil {
return nil, err
}
if !resp.Message.IsSuccessful {
return nil, errors.New(resp.Message.Description)
}
if strings.Contains(resp.Message.Data.Address, "_") {
splitter := strings.Split(resp.Message.Data.Address, "_")
resp.Message.Data.Address, resp.Message.Data.Tag = splitter[0], splitter[1]
}
return &resp, nil
}
// GetMultiChainDepositAddress returns deposit addresses for a given currency
func (z *ZB) GetMultiChainDepositAddress(ctx context.Context, currency currency.Code) ([]MultiChainDepositAddress, error) {
var resp MultiChainDepositAddressResponse
vals := url.Values{}
vals.Set("method", zbMultiChainDepositAddress)
vals.Set("currency", currency.Lower().String())
if err := z.SendAuthenticatedHTTPRequest(ctx,
exchange.RestSpotSupplementary,
http.MethodGet,
vals,
&resp,
request.Auth); err != nil {
return nil, err
}
if !resp.Message.IsSuccessful {
return nil, errors.New(resp.Message.Description)
}
return resp.Message.Data, nil
}
// SendHTTPRequest sends an unauthenticated HTTP request
func (z *ZB) SendHTTPRequest(ctx context.Context, ep exchange.URL, path string, result interface{}, f request.EndpointLimit) error {
endpoint, err := z.API.Endpoints.GetURL(ep)
if err != nil {
return err
}
item := &request.Item{
Method: http.MethodGet,
Path: endpoint + path,
Result: result,
Verbose: z.Verbose,
HTTPDebugging: z.HTTPDebugging,
HTTPRecording: z.HTTPRecording,
}
return z.SendPayload(ctx, f, func() (*request.Item, error) {
return item, nil
}, request.UnauthenticatedRequest)
}
// SendAuthenticatedHTTPRequest sends authenticated requests to the zb API
func (z *ZB) SendAuthenticatedHTTPRequest(ctx context.Context, ep exchange.URL, httpMethod string, params url.Values, result interface{}, f request.EndpointLimit) error {
creds, err := z.GetCredentials(ctx)
if err != nil {
return err
}
endpoint, err := z.API.Endpoints.GetURL(ep)
if err != nil {
return err
}
params.Set("accesskey", creds.Key)
hex, err := crypto.Sha1ToHex(creds.Secret)
if err != nil {
return err
}
hmac, err := crypto.GetHMAC(crypto.HashMD5,
[]byte(params.Encode()),
[]byte(hex))
if err != nil {
return err
}
var intermediary json.RawMessage
newRequest := func() (*request.Item, error) {
params.Set("reqTime", strconv.FormatInt(time.Now().UnixMilli(), 10))
params.Set("sign", fmt.Sprintf("%x", hmac))
urlPath := fmt.Sprintf("%s/%s?%s",
endpoint,
params.Get("method"),
params.Encode())
return &request.Item{
Method: httpMethod,
Path: urlPath,
Result: &intermediary,
Verbose: z.Verbose,
HTTPDebugging: z.HTTPDebugging,
HTTPRecording: z.HTTPRecording,
}, nil
}
err = z.SendPayload(ctx, f, newRequest, request.AuthenticatedRequest)
if err != nil {
return err
}
errCap := struct {
Code int64 `json:"code"`
Message string `json:"message"`
}{}
err = json.Unmarshal(intermediary, &errCap)
if err == nil {
if errCap.Code > 1000 {
return fmt.Errorf("%w error code: %d error code message: %s error message: %s",
request.ErrAuthRequestFailed,
errCap.Code,
errorCode[errCap.Code],
errCap.Message)
}
}
return json.Unmarshal(intermediary, result)
}
// GetFee returns an estimate of fee based on type of transaction
func (z *ZB) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) {
var fee float64
switch feeBuilder.FeeType {
case exchange.CryptocurrencyTradeFee:
fee = calculateTradingFee(feeBuilder.PurchasePrice, feeBuilder.Amount)
case exchange.CryptocurrencyWithdrawalFee:
fee = getWithdrawalFee(feeBuilder.Pair.Base)
case exchange.OfflineTradeFee:
fee = getOfflineTradeFee(feeBuilder.PurchasePrice, feeBuilder.Amount)
}
if fee < 0 {
fee = 0
}
return fee, nil
}
// getOfflineTradeFee calculates the worst case-scenario trading fee
func getOfflineTradeFee(price, amount float64) float64 {
return 0.002 * price * amount
}
func calculateTradingFee(purchasePrice, amount float64) (fee float64) {
fee = 0.002
return fee * amount * purchasePrice
}
func getWithdrawalFee(c currency.Code) float64 {
return WithdrawalFees[c]
}
var errorCode = map[int64]string{
1000: "Successful call",
1001: "General error message",
1002: "internal error",
1003: "Verification failed",
1004: "Financial security password lock",
1005: "The fund security password is incorrect. Please confirm and re-enter.",
1006: "Real-name certification is awaiting review or review",
1009: "This interface is being maintained",
1010: "Not open yet",
1012: "Insufficient permissions",
1013: "Can not trade, if you have any questions, please contact online customer service",
1014: "Cannot be sold during the pre-sale period",
2002: "Insufficient balance in Bitcoin account",
2003: "Insufficient balance of Litecoin account",
2005: "Insufficient balance in Ethereum account",
2006: "Insufficient balance in ETC currency account",
2007: "Insufficient balance of BTS currency account",
2009: "Insufficient account balance",
3001: "Pending order not found",
3002: "Invalid amount",
3003: "Invalid quantity",
3004: "User does not exist",
3005: "Invalid parameter",
3006: "Invalid IP or inconsistent with the bound IP",
3007: "Request time has expired",
3008: "Transaction history not found",
4001: "API interface is locked",
4002: "Request too frequently",
}
// Withdraw transfers funds
func (z *ZB) Withdraw(ctx context.Context, currency, address, safepassword string, amount, fees float64, itransfer bool) (string, error) {
creds, err := z.GetCredentials(ctx)
if err != nil {
return "", err
}
type response struct {
Code int `json:"code"` // Result code
Message string `json:"message"` // Result Message
ID string `json:"id"` // Withdrawal ID
}
vals := url.Values{}
vals.Set("accesskey", creds.Key)
vals.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64))
vals.Set("currency", currency)
vals.Set("fees", strconv.FormatFloat(fees, 'f', -1, 64))
vals.Set("itransfer", strconv.FormatBool(itransfer))
vals.Set("method", "withdraw")
vals.Set("receiveAddr", address)
vals.Set("safePwd", safepassword)
var resp response
err = z.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpotSupplementary, http.MethodGet, vals, &resp, request.Auth)
if err != nil {
return "", err
}
if resp.Code != 1000 {
return "", fmt.Errorf("%w %v", request.ErrAuthRequestFailed, resp.Message)
}
return resp.ID, nil
}