package zb import ( "encoding/json" "errors" "fmt" "net/http" "net/url" "strconv" "strings" "time" "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/websocket/wshandler" ) const ( zbTradeURL = "http://api.zb.cn/data" zbMarketURL = "https://trade.zb.cn/api" zbAPIVersion = "v1" zbAccountInfo = "getAccountInfo" zbMarkets = "markets" zbKline = "kline" zbOrder = "order" zbCancelOrder = "cancelOrder" zbTicker = "ticker" zbTickers = "allTicker" zbDepth = "depth" zbUnfinishedOrdersIgnoreTradeType = "getUnfinishedOrdersIgnoreTradeType" zbGetOrdersGet = "getOrders" zbWithdraw = "withdraw" zbDepositAddress = "getUserAddress" zbAuthRate = 100 zbUnauthRate = 100 ) // 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 { WebsocketConn *wshandler.WebsocketConnection exchange.Base } // SpotNewOrder submits an order to ZB func (z *ZB) SpotNewOrder(arg SpotNewOrderRequestParams) (int64, error) { var result SpotNewOrderResponse vals := url.Values{} vals.Set("accesskey", z.API.Credentials.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(http.MethodGet, vals, &result) if err != nil { return 0, err } if result.Code != 1000 { return 0, fmt.Errorf("unsuccessful new order, message: %s code: %d", result.Message, result.Code) } newOrderID, err := strconv.ParseInt(result.ID, 10, 64) if err != nil { return 0, err } return newOrderID, nil } // CancelExistingOrder cancels an order func (z *ZB) CancelExistingOrder(orderID int64, symbol string) error { type response struct { Code int `json:"code"` // Result code Message string `json:"message"` // Result Message } vals := url.Values{} vals.Set("accesskey", z.API.Credentials.Key) vals.Set("method", "cancelOrder") vals.Set("id", strconv.FormatInt(orderID, 10)) vals.Set("currency", symbol) var result response err := z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &result) if err != nil { return err } if result.Code != 1000 { return errors.New(result.Message) } return nil } // GetAccountInformation returns account information including coin information // and pricing func (z *ZB) GetAccountInformation() (AccountsResponse, error) { var result AccountsResponse vals := url.Values{} vals.Set("accesskey", z.API.Credentials.Key) vals.Set("method", "getAccountInfo") return result, z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &result) } // GetUnfinishedOrdersIgnoreTradeType returns unfinished orders func (z *ZB) GetUnfinishedOrdersIgnoreTradeType(currency string, pageindex, pagesize int64) ([]Order, error) { var result []Order vals := url.Values{} vals.Set("accesskey", z.API.Credentials.Key) vals.Set("method", zbUnfinishedOrdersIgnoreTradeType) vals.Set("currency", currency) vals.Set("pageIndex", strconv.FormatInt(pageindex, 10)) vals.Set("pageSize", strconv.FormatInt(pagesize, 10)) err := z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &result) return result, err } // GetOrders returns finished orders func (z *ZB) GetOrders(currency string, pageindex, side int64) ([]Order, error) { var response []Order vals := url.Values{} vals.Set("accesskey", z.API.Credentials.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(http.MethodGet, vals, &response) } // GetMarkets returns market information including pricing, symbols and // each symbols decimal precision func (z *ZB) GetMarkets() (map[string]MarketResponseItem, error) { endpoint := fmt.Sprintf("%s/%s/%s", z.API.Endpoints.URL, zbAPIVersion, zbMarkets) var res map[string]MarketResponseItem err := z.SendHTTPRequest(endpoint, &res) 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(symbol string) (float64, error) { res, err := z.GetTicker(symbol) if err != nil { return 0, err } return res.Ticker.Last, nil } // GetTicker returns a ticker for a given symbol func (z *ZB) GetTicker(symbol string) (TickerResponse, error) { urlPath := fmt.Sprintf("%s/%s/%s?market=%s", z.API.Endpoints.URL, zbAPIVersion, zbTicker, symbol) var res TickerResponse err := z.SendHTTPRequest(urlPath, &res) return res, err } // GetTickers returns ticker data for all supported symbols func (z *ZB) GetTickers() (map[string]TickerChildResponse, error) { urlPath := fmt.Sprintf("%s/%s/%s", z.API.Endpoints.URL, zbAPIVersion, zbTickers) resp := make(map[string]TickerChildResponse) err := z.SendHTTPRequest(urlPath, &resp) return resp, err } // GetOrderbook returns the orderbook for a given symbol func (z *ZB) GetOrderbook(symbol string) (OrderbookResponse, error) { urlPath := fmt.Sprintf("%s/%s/%s?market=%s", z.API.Endpoints.URL, zbAPIVersion, zbDepth, symbol) var res OrderbookResponse err := z.SendHTTPRequest(urlPath, &res) if err != nil { return res, err } if len(res.Asks) == 0 { return res, fmt.Errorf("ZB GetOrderbook asks is empty") } if len(res.Bids) == 0 { return res, fmt.Errorf("ZB GetOrderbook bids is empty") } // reverse asks data var data [][]float64 for x := len(res.Asks); x > 0; x-- { data = append(data, res.Asks[x-1]) } res.Asks = data return res, nil } // GetSpotKline returns Kline data func (z *ZB) GetSpotKline(arg KlinesRequestParams) (KLineResponse, error) { vals := url.Values{} vals.Set("type", string(arg.Type)) vals.Set("market", arg.Symbol) if arg.Since != "" { vals.Set("since", arg.Since) } if arg.Size != 0 { vals.Set("size", fmt.Sprintf("%d", arg.Size)) } urlPath := fmt.Sprintf("%s/%s/%s?%s", z.API.Endpoints.URL, zbAPIVersion, zbKline, vals.Encode()) var res KLineResponse var rawKlines map[string]interface{} err := z.SendHTTPRequest(urlPath, &rawKlines) if err != nil { return res, err } if rawKlines == nil || rawKlines["symbol"] == nil { return res, errors.New("zb GetSpotKline rawKlines is nil") } res.Symbol = rawKlines["symbol"].(string) res.MoneyType = rawKlines["moneyType"].(string) rawKlineDatasString, _ := json.Marshal(rawKlines["data"].([]interface{})) var rawKlineDatas [][]interface{} if err := json.Unmarshal(rawKlineDatasString, &rawKlineDatas); err != nil { return res, errors.New("zb rawKlines unmarshal failed") } for _, k := range rawKlineDatas { ot, err := convert.TimeFromUnixTimestampFloat(k[0]) if err != nil { return res, errors.New("zb cannot parse Kline.OpenTime") } res.Data = append(res.Data, &KLineResponseData{ ID: k[0].(float64), KlineTime: ot, Open: k[1].(float64), High: k[2].(float64), Low: k[3].(float64), Close: k[4].(float64), Volume: k[5].(float64), }) } 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(currency currency.Code) (UserAddress, error) { var resp UserAddress vals := url.Values{} vals.Set("method", zbDepositAddress) vals.Set("currency", currency.Lower().String()) return resp, z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &resp) } // SendHTTPRequest sends an unauthenticated HTTP request func (z *ZB) SendHTTPRequest(path string, result interface{}) error { return z.SendPayload(http.MethodGet, path, nil, nil, result, false, false, z.Verbose, z.HTTPDebugging, z.HTTPRecording) } // SendAuthenticatedHTTPRequest sends authenticated requests to the zb API func (z *ZB) SendAuthenticatedHTTPRequest(httpMethod string, params url.Values, result interface{}) error { if !z.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, z.Name) } params.Set("accesskey", z.API.Credentials.Key) hmac := crypto.GetHMAC(crypto.HashMD5, []byte(params.Encode()), []byte(crypto.Sha1ToHex(z.API.Credentials.Secret))) params.Set("reqTime", fmt.Sprintf("%d", convert.UnixMillis(time.Now()))) params.Set("sign", fmt.Sprintf("%x", hmac)) urlPath := fmt.Sprintf("%s/%s?%s", z.API.Endpoints.URLSecondary, params.Get("method"), params.Encode()) var intermediary json.RawMessage errCap := struct { Code int64 `json:"code"` Message string `json:"message"` }{} err := z.SendPayload(httpMethod, urlPath, nil, strings.NewReader(""), &intermediary, true, false, z.Verbose, z.HTTPDebugging, z.HTTPRecording) if err != nil { return err } err = json.Unmarshal(intermediary, &errCap) if err == nil { if errCap.Code > 1000 { return fmt.Errorf("sendAuthenticatedHTTPRequest error code: %d message %s", errCap.Code, errorCode[errCap.Code]) } } 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(currency, address, safepassword string, amount, fees float64, itransfer bool) (string, error) { 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", z.API.Credentials.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("recieveAddr", address) vals.Set("safePwd", safepassword) var resp response err := z.SendAuthenticatedHTTPRequest(http.MethodGet, vals, &resp) if err != nil { return "", err } if resp.Code != 1000 { return "", errors.New(resp.Message) } return resp.ID, nil }