package bithumb import ( "bytes" "encoding/json" "errors" "fmt" "net/url" "reflect" "strconv" "time" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" "github.com/thrasher-/gocryptotrader/currency/symbol" exchange "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/request" "github.com/thrasher-/gocryptotrader/exchanges/ticker" log "github.com/thrasher-/gocryptotrader/logger" ) const ( apiURL = "https://api.bithumb.com" noError = "0000" // Public API requestsPerSecondPublicAPI = 20 publicTicker = "/public/ticker/" publicOrderBook = "/public/orderbook/" publicTransactionHistory = "/public/transaction_history/" // Private API requestsPerSecondPrivateAPI = 10 privateAccInfo = "/info/account" privateAccBalance = "/info/balance" privateWalletAdd = "/info/wallet_address" privateTicker = "/info/ticker" privateOrders = "/info/orders" privateUserTrans = "/info/user_transactions" privatePlaceTrade = "/trade/place" privateOrderDetail = "/info/order_detail" privateCancelTrade = "/trade/cancel" privateBTCWithdraw = "/trade/btc_withdrawal" privateKRWDeposit = "/trade/krw_deposit" privateKRWWithdraw = "/trade/krw_withdrawal" privateMarketBuy = "/trade/market_buy" privateMarketSell = "/trade/market_sell" bithumbAuthRate = 10 bithumbUnauthRate = 20 ) // Bithumb is the overarching type across the Bithumb package type Bithumb struct { exchange.Base } // SetDefaults sets the basic defaults for Bithumb func (b *Bithumb) SetDefaults() { b.Name = "Bithumb" b.Enabled = false b.Verbose = false b.RESTPollingDelay = 10 b.APIWithdrawPermissions = exchange.AutoWithdrawCrypto | exchange.AutoWithdrawFiat b.RequestCurrencyPairFormat.Delimiter = "" b.RequestCurrencyPairFormat.Uppercase = true b.ConfigCurrencyPairFormat.Delimiter = "" b.ConfigCurrencyPairFormat.Uppercase = true b.ConfigCurrencyPairFormat.Index = "KRW" b.AssetTypes = []string{ticker.Spot} b.SupportsAutoPairUpdating = true b.SupportsRESTTickerBatching = true b.Requester = request.New(b.Name, request.NewRateLimit(time.Second, bithumbAuthRate), request.NewRateLimit(time.Second, bithumbUnauthRate), common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) b.APIUrlDefault = apiURL b.APIUrl = b.APIUrlDefault b.WebsocketInit() } // Setup takes in the supplied exchange configuration details and sets params func (b *Bithumb) Setup(exch config.ExchangeConfig) { if !exch.Enabled { b.SetEnabled(false) } else { b.Enabled = true b.AuthenticatedAPISupport = exch.AuthenticatedAPISupport b.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) b.SetHTTPClientTimeout(exch.HTTPTimeout) b.SetHTTPClientUserAgent(exch.HTTPUserAgent) b.RESTPollingDelay = exch.RESTPollingDelay b.Verbose = exch.Verbose b.Websocket.SetEnabled(exch.Websocket) b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") err := b.SetCurrencyPairFormat() if err != nil { log.Fatal(err) } err = b.SetAssetTypes() if err != nil { log.Fatal(err) } err = b.SetAutoPairDefaults() if err != nil { log.Fatal(err) } err = b.SetAPIURL(exch) if err != nil { log.Fatal(err) } err = b.SetClientProxyAddress(exch.ProxyAddress) if err != nil { log.Fatal(err) } } } // GetTradablePairs returns a list of tradable currencies func (b *Bithumb) GetTradablePairs() ([]string, error) { result, err := b.GetAllTickers() if err != nil { return nil, err } var currencies []string for x := range result { currencies = append(currencies, x) } return currencies, nil } // GetTicker returns ticker information // // symbol e.g. "btc" func (b *Bithumb) GetTicker(symbol string) (Ticker, error) { response := Ticker{} path := fmt.Sprintf("%s%s%s", b.APIUrl, publicTicker, common.StringToUpper(symbol)) err := b.SendHTTPRequest(path, &response) if err != nil { return response, err } if response.Status != noError { return response, errors.New(response.Message) } return response, nil } // GetAllTickers returns all ticker information func (b *Bithumb) GetAllTickers() (map[string]Ticker, error) { type Response struct { ActionStatus Data map[string]interface{} } response := Response{} path := fmt.Sprintf("%s%s%s", b.APIUrl, publicTicker, "all") err := b.SendHTTPRequest(path, &response) if err != nil { return nil, err } if response.Status != noError { return nil, errors.New(response.Message) } result := make(map[string]Ticker) for k, v := range response.Data { if k == "date" { continue } if reflect.TypeOf(v).String() != "map[string]interface {}" { continue } data := v.(map[string]interface{}) var t Ticker t.AveragePrice, _ = strconv.ParseFloat(data["average_price"].(string), 64) t.BuyPrice, _ = strconv.ParseFloat(data["buy_price"].(string), 64) t.ClosingPrice, _ = strconv.ParseFloat(data["closing_price"].(string), 64) t.MaxPrice, _ = strconv.ParseFloat(data["max_price"].(string), 64) t.MinPrice, _ = strconv.ParseFloat(data["min_price"].(string), 64) t.OpeningPrice, _ = strconv.ParseFloat(data["opening_price"].(string), 64) t.SellPrice, _ = strconv.ParseFloat(data["sell_price"].(string), 64) t.UnitsTraded, _ = strconv.ParseFloat(data["units_traded"].(string), 64) t.Volume1Day, _ = strconv.ParseFloat(data["volume_1day"].(string), 64) t.Volume7Day, _ = strconv.ParseFloat(data["volume_7day"].(string), 64) result[k] = t } return result, nil } // GetOrderBook returns current orderbook // // symbol e.g. "btc" func (b *Bithumb) GetOrderBook(symbol string) (Orderbook, error) { response := Orderbook{} path := fmt.Sprintf("%s%s%s", b.APIUrl, publicOrderBook, common.StringToUpper(symbol)) err := b.SendHTTPRequest(path, &response) if err != nil { return response, err } if response.Status != noError { return response, errors.New(response.Message) } return response, nil } // GetTransactionHistory returns recent transactions // // symbol e.g. "btc" func (b *Bithumb) GetTransactionHistory(symbol string) (TransactionHistory, error) { response := TransactionHistory{} path := fmt.Sprintf("%s%s%s", b.APIUrl, publicTransactionHistory, common.StringToUpper(symbol)) err := b.SendHTTPRequest(path, &response) if err != nil { return response, err } if response.Status != noError { return response, errors.New(response.Message) } return response, nil } // GetAccountInformation returns account information by singular currency func (b *Bithumb) GetAccountInformation(currency string) (Account, error) { response := Account{} val := url.Values{} if currency != "" { val.Set("currency", currency) } return response, b.SendAuthenticatedHTTPRequest(privateAccInfo, val, &response) } // GetAccountBalance returns customer wallet information func (b *Bithumb) GetAccountBalance(c string) (FullBalance, error) { var response Balance var fullBalance = FullBalance{ make(map[string]float64), make(map[string]float64), make(map[string]float64), make(map[string]float64), make(map[string]float64), } vals := url.Values{} if c != "" { vals.Set("currency", c) } err := b.SendAuthenticatedHTTPRequest(privateAccBalance, vals, &response) if err != nil { return fullBalance, err } // Added due to increasing of the usuable currencies on exchange, usually // without notificatation, so we dont need to update structs later on for tag, datum := range response.Data { splitTag := common.SplitStrings(tag, "_") c := splitTag[len(splitTag)-1] var val float64 if reflect.TypeOf(datum).String() != "float64" { val, err = strconv.ParseFloat(datum.(string), 64) if err != nil { return fullBalance, err } } else { val = datum.(float64) } switch splitTag[0] { case "available": fullBalance.Available[c] = val case "in": fullBalance.InUse[c] = val case "total": fullBalance.Total[c] = val case "misu": fullBalance.Misu[c] = val case "xcoin": fullBalance.Xcoin[c] = val default: return fullBalance, fmt.Errorf("GetAccountBalance error tag name %s unhandled", splitTag) } } return fullBalance, nil } // GetWalletAddress returns customer wallet address // // currency e.g. btc, ltc or "", will default to btc without currency specified func (b *Bithumb) GetWalletAddress(currency string) (WalletAddressRes, error) { response := WalletAddressRes{} params := url.Values{} params.Set("currency", common.StringToUpper(currency)) return response, b.SendAuthenticatedHTTPRequest(privateWalletAdd, params, &response) } // GetLastTransaction returns customer last transaction func (b *Bithumb) GetLastTransaction() (LastTransactionTicker, error) { response := LastTransactionTicker{} return response, b.SendAuthenticatedHTTPRequest(privateTicker, nil, &response) } // GetOrders returns order list // // orderID: order number registered for purchase/sales // transactionType: transaction type(bid : purchase, ask : sell) // count: Value : 1 ~1000 (default : 100) // after: YYYY-MM-DD hh:mm:ss's UNIX Timestamp // (2014-11-28 16:40:01 = 1417160401000) func (b *Bithumb) GetOrders(orderID, transactionType, count, after, currency string) (Orders, error) { response := Orders{} params := url.Values{} params.Set("order_id", orderID) params.Set("type", transactionType) params.Set("count", count) params.Set("after", after) params.Set("currency", common.StringToUpper(currency)) return response, b.SendAuthenticatedHTTPRequest(privateOrders, params, &response) } // GetUserTransactions returns customer transactions func (b *Bithumb) GetUserTransactions() (UserTransactions, error) { response := UserTransactions{} return response, b.SendAuthenticatedHTTPRequest(privateUserTrans, nil, &response) } // PlaceTrade executes a trade order // // orderCurrency: BTC, ETH, DASH, LTC, ETC, XRP, BCH, XMR, ZEC, QTUM, BTG, EOS // (default value: BTC) // transactionType: Transaction type(bid : purchase, ask : sales) // units: Order quantity // price: Transaction amount per currency func (b *Bithumb) PlaceTrade(orderCurrency, transactionType string, units float64, price int64) (OrderPlace, error) { response := OrderPlace{} params := url.Values{} params.Set("order_currency", common.StringToUpper(orderCurrency)) params.Set("Payment_currency", "KRW") params.Set("type", common.StringToUpper(transactionType)) params.Set("units", strconv.FormatFloat(units, 'f', -1, 64)) params.Set("price", strconv.FormatInt(price, 10)) return response, b.SendAuthenticatedHTTPRequest(privatePlaceTrade, params, &response) } // ModifyTrade modifies an order already on the exchange books func (b *Bithumb) ModifyTrade(orderID, orderCurrency, transactionType string, units float64, price int64) (OrderPlace, error) { response := OrderPlace{} params := url.Values{} params.Set("order_currency", common.StringToUpper(orderCurrency)) params.Set("Payment_currency", "KRW") params.Set("type", common.StringToUpper(transactionType)) params.Set("units", strconv.FormatFloat(units, 'f', -1, 64)) params.Set("price", strconv.FormatInt(price, 10)) params.Set("order_id", orderID) return response, b.SendAuthenticatedHTTPRequest(privatePlaceTrade, params, &response) } // GetOrderDetails returns specific order details // // orderID: Order number registered for purchase/sales // transactionType: Transaction type(bid : purchase, ask : sales) // currency: BTC, ETH, DASH, LTC, ETC, XRP, BCH, XMR, ZEC, QTUM, BTG, EOS // (default value: BTC) func (b *Bithumb) GetOrderDetails(orderID, transactionType, currency string) (OrderDetails, error) { response := OrderDetails{} params := url.Values{} params.Set("order_id", common.StringToUpper(orderID)) params.Set("type", common.StringToUpper(transactionType)) params.Set("currency", common.StringToUpper(currency)) return response, b.SendAuthenticatedHTTPRequest(privateOrderDetail, params, &response) } // CancelTrade cancels a customer purchase/sales transaction // transactionType: Transaction type(bid : purchase, ask : sales) // orderID: Order number registered for purchase/sales // currency: BTC, ETH, DASH, LTC, ETC, XRP, BCH, XMR, ZEC, QTUM, BTG, EOS // (default value: BTC) func (b *Bithumb) CancelTrade(transactionType, orderID, currency string) (ActionStatus, error) { response := ActionStatus{} params := url.Values{} params.Set("order_id", common.StringToUpper(orderID)) params.Set("type", common.StringToUpper(transactionType)) params.Set("currency", common.StringToUpper(currency)) return response, b.SendAuthenticatedHTTPRequest(privateCancelTrade, nil, &response) } // WithdrawCrypto withdraws a customer currency to an address // // address: Currency withdrawing address // destination: Currency withdrawal Destination Tag (when withdraw XRP) OR // Currency withdrawal Payment Id (when withdraw XMR) // currency: BTC, ETH, DASH, LTC, ETC, XRP, BCH, XMR, ZEC, QTUM // (default value: BTC) // units: Quantity to withdraw currency func (b *Bithumb) WithdrawCrypto(address, destination, currency string, units float64) (ActionStatus, error) { response := ActionStatus{} params := url.Values{} params.Set("address", address) if len(destination) > 0 { params.Set("destination", destination) } params.Set("currency", common.StringToUpper(currency)) params.Set("units", strconv.FormatFloat(units, 'f', -1, 64)) return response, b.SendAuthenticatedHTTPRequest(privateBTCWithdraw, params, &response) } // RequestKRWDepositDetails returns Bithumb banking details for deposit // information func (b *Bithumb) RequestKRWDepositDetails() (KRWDeposit, error) { response := KRWDeposit{} return response, b.SendAuthenticatedHTTPRequest(privateKRWDeposit, nil, &response) } // RequestKRWWithdraw allows a customer KRW withdrawal request // // bank: Bankcode with bank name e.g. (bankcode)_(bankname) // account: Withdrawing bank account number // price: Withdrawing amount func (b *Bithumb) RequestKRWWithdraw(bank, account string, price int64) (ActionStatus, error) { response := ActionStatus{} params := url.Values{} params.Set("bank", bank) params.Set("account", account) params.Set("price", strconv.FormatInt(price, 10)) return response, b.SendAuthenticatedHTTPRequest(privateKRWWithdraw, params, &response) } // MarketBuyOrder initiates a buy order through available order books // // currency: BTC, ETH, DASH, LTC, ETC, XRP, BCH, XMR, ZEC, QTUM, BTG, EOS // (default value: BTC) // units: Order quantity func (b *Bithumb) MarketBuyOrder(currency string, units float64) (MarketBuy, error) { response := MarketBuy{} params := url.Values{} params.Set("currency", common.StringToUpper(currency)) params.Set("units", strconv.FormatFloat(units, 'f', -1, 64)) return response, b.SendAuthenticatedHTTPRequest(privateMarketBuy, params, &response) } // MarketSellOrder initiates a sell order through available order books // // currency: BTC, ETH, DASH, LTC, ETC, XRP, BCH, XMR, ZEC, QTUM, BTG, EOS // (default value: BTC) // units: Order quantity func (b *Bithumb) MarketSellOrder(currency string, units float64) (MarketSell, error) { response := MarketSell{} params := url.Values{} params.Set("currency", common.StringToUpper(currency)) params.Set("units", strconv.FormatFloat(units, 'f', -1, 64)) return response, b.SendAuthenticatedHTTPRequest(privateMarketSell, params, &response) } // SendHTTPRequest sends an unauthenticated HTTP request func (b *Bithumb) SendHTTPRequest(path string, result interface{}) error { return b.SendPayload("GET", path, nil, nil, result, false, b.Verbose) } // SendAuthenticatedHTTPRequest sends an authenticated HTTP request to bithumb func (b *Bithumb) SendAuthenticatedHTTPRequest(path string, params url.Values, result interface{}) error { if !b.AuthenticatedAPISupport { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) } if params == nil { params = url.Values{} } if b.Nonce.Get() == 0 { b.Nonce.Set(time.Now().UnixNano() / int64(time.Millisecond)) } else { b.Nonce.Inc() } params.Set("endpoint", path) payload := params.Encode() hmacPayload := path + string(0) + payload + string(0) + b.Nonce.String() hmac := common.GetHMAC(common.HashSHA512, []byte(hmacPayload), []byte(b.APISecret)) hmacStr := common.HexEncodeToString(hmac) headers := make(map[string]string) headers["Api-Key"] = b.APIKey headers["Api-Sign"] = common.Base64Encode([]byte(hmacStr)) headers["Api-Nonce"] = b.Nonce.String() headers["Content-Type"] = "application/x-www-form-urlencoded" var intermediary json.RawMessage errCapture := struct { Status string `json:"status"` Message string `json:"message"` }{} err := b.SendPayload("POST", b.APIUrl+path, headers, bytes.NewBufferString(payload), &intermediary, true, b.Verbose) if err != nil { return err } err = common.JSONDecode(intermediary, &errCapture) if err == nil { if errCapture.Status != "" && errCapture.Status != "0000" { return fmt.Errorf("SendAuthenticatedHTTPRequest error Code:%s Message:%s", errCapture.Status, errCode[errCapture.Status]) } } return common.JSONDecode(intermediary, result) } // GetFee returns an estimate of fee based on type of transaction func (b *Bithumb) GetFee(feeBuilder exchange.FeeBuilder) (float64, error) { var fee float64 switch feeBuilder.FeeType { case exchange.CryptocurrencyTradeFee: fee = calculateTradingFee(feeBuilder.PurchasePrice, feeBuilder.Amount) case exchange.CyptocurrencyDepositFee: fee = getDepositFee(feeBuilder.FirstCurrency, feeBuilder.Amount) case exchange.CryptocurrencyWithdrawalFee: fee = getWithdrawalFee(feeBuilder.FirstCurrency) case exchange.InternationalBankWithdrawalFee: fee = getWithdrawalFee(feeBuilder.CurrencyItem) } if fee < 0 { fee = 0 } return fee, nil } // calculateTradingFee returns fee when performing a trade func calculateTradingFee(purchasePrice float64, amount float64) float64 { fee := 0.0015 return fee * amount * purchasePrice } // getDepositFee returns fee on a currency when depositing small amounts to bithumb func getDepositFee(currency string, amount float64) float64 { var fee float64 switch currency { case symbol.BTC: if amount <= 0.005 { fee = 0.001 } case symbol.LTC: if amount <= 0.3 { fee = 0.01 } case symbol.DASH: if amount <= 0.04 { fee = 0.01 } case symbol.BCH: if amount <= 0.03 { fee = 0.001 } case symbol.ZEC: if amount <= 0.02 { fee = 0.001 } case symbol.BTG: if amount <= 0.15 { fee = 0.001 } } return fee } // getWithdrawalFee returns fee on a currency when withdrawing out of bithumb func getWithdrawalFee(currency string) float64 { return WithdrawalFees[currency] } var errCode = map[string]string{ "5100": "Bad Request", "5200": "Not Member", "5300": "Invalid Apikey", "5302": "Method Not Allowed", "5400": "Database Fail", "5500": "Invalid Parameter", "5600": "CUSTOM NOTICE (상황별 에러 메시지 출력) usually means transaction not allowed", "5900": "Unknown Error", }