package btcmarkets import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strconv" "strings" "time" "github.com/thrasher-corp/gocryptotrader/common" "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/order" "github.com/thrasher-corp/gocryptotrader/exchanges/request" ) var ( errInvalidAmount = errors.New("cannot be less than or equal to zero") errIDRequired = errors.New("id is required") ) const ( btcMarketsAPIURL = "https://api.btcmarkets.net" btcMarketsAPIVersion = "/v3" // UnAuthenticated EPs btcMarketsAllMarkets = "/markets/" btcMarketsGetTicker = "/ticker/" btcMarketsGetTrades = "/trades?" btcMarketOrderBook = "/orderbook?" btcMarketsCandles = "/candles?" btcMarketsTickers = "tickers?" btcMarketsMultipleOrderbooks = "orderbooks?" btcMarketsGetTime = "/time" btcMarketsWithdrawalFees = "/withdrawal-fees" btcMarketsUnauthPath = btcMarketsAPIURL + btcMarketsAPIVersion + btcMarketsAllMarkets // Authenticated EPs btcMarketsAccountBalance = "/accounts/me/balances" btcMarketsTradingFees = "/accounts/me/trading-fees" btcMarketsTransactions = "/accounts/me/transactions" btcMarketsOrders = "/orders" btcMarketsTradeHistory = "/trades" btcMarketsWithdrawals = "/withdrawals" btcMarketsDeposits = "/deposits" btcMarketsTransfers = "/transfers" btcMarketsAddresses = "/addresses" btcMarketsAssets = "/assets" btcMarketsReports = "/reports" btcMarketsBatchOrders = "/batchorders" orderFailed = "Failed" orderPartiallyCancelled = "Partially Cancelled" orderCancelled = "Cancelled" orderFullyMatched = "Fully Matched" orderPartiallyMatched = "Partially Matched" orderPlaced = "Placed" orderAccepted = "Accepted" ask = "ask" // order types limit = "Limit" market = "Market" stopLimit = "Stop Limit" stop = "Stop" takeProfit = "Take Profit" // order sides askSide = "Ask" bidSide = "Bid" // time in force immediateOrCancel = "IOC" fillOrKill = "FOK" subscribe = "subscribe" fundChange = "fundChange" orderChange = "orderChange" heartbeat = "heartbeat" tick = "tick" wsOB = "orderbookUpdate" tradeEndPoint = "trade" // Subscription management when connection and subscription established addSubscription = "addSubscription" removeSubscription = "removeSubscription" clientType = "api" ) // BTCMarkets is the overarching type across the BTCMarkets package type BTCMarkets struct { exchange.Base } // GetMarkets returns the BTCMarkets instruments func (b *BTCMarkets) GetMarkets(ctx context.Context) ([]Market, error) { var resp []Market return resp, b.SendHTTPRequest(ctx, btcMarketsUnauthPath, &resp) } // GetTicker returns a ticker // symbol - example "btc" or "ltc" func (b *BTCMarkets) GetTicker(ctx context.Context, marketID string) (Ticker, error) { var tick Ticker return tick, b.SendHTTPRequest(ctx, btcMarketsUnauthPath+marketID+btcMarketsGetTicker, &tick) } // GetTrades returns executed trades on the exchange func (b *BTCMarkets) GetTrades(ctx context.Context, marketID string, before, after, limit int64) ([]Trade, error) { if (before > 0) && (after >= 0) { return nil, errors.New("BTCMarkets only supports either before or after, not both") } var trades []Trade params := url.Values{} if before > 0 { params.Set("before", strconv.FormatInt(before, 10)) } if after > 0 { params.Set("after", strconv.FormatInt(after, 10)) } if limit > 0 { params.Set("limit", strconv.FormatInt(limit, 10)) } return trades, b.SendHTTPRequest(ctx, btcMarketsUnauthPath+marketID+btcMarketsGetTrades+params.Encode(), &trades) } // GetOrderbook returns current orderbook. // levels are: // 0 - Returns the top bids and ask orders only. // 1 - Returns top 50 bids and asks. // 2 - Returns full orderbook. WARNING: This is cached every 10 seconds. func (b *BTCMarkets) GetOrderbook(ctx context.Context, marketID string, level int64) (*Orderbook, error) { params := url.Values{} if level != 0 { params.Set("level", strconv.FormatInt(level, 10)) } var temp tempOrderbook err := b.SendHTTPRequest(ctx, btcMarketsUnauthPath+marketID+btcMarketOrderBook+params.Encode(), &temp) if err != nil { return nil, err } orderbook := Orderbook{ MarketID: temp.MarketID, SnapshotID: temp.SnapshotID, Bids: make([]OBData, len(temp.Bids)), Asks: make([]OBData, len(temp.Asks)), } for x := range temp.Asks { price, err := strconv.ParseFloat(temp.Asks[x][0], 64) if err != nil { return nil, err } amount, err := strconv.ParseFloat(temp.Asks[x][1], 64) if err != nil { return nil, err } orderbook.Asks[x] = OBData{ Price: price, Volume: amount, } } for a := range temp.Bids { price, err := strconv.ParseFloat(temp.Bids[a][0], 64) if err != nil { return nil, err } amount, err := strconv.ParseFloat(temp.Bids[a][1], 64) if err != nil { return nil, err } orderbook.Bids[a] = OBData{ Price: price, Volume: amount, } } return &orderbook, nil } // GetMarketCandles gets candles for specified currency pair func (b *BTCMarkets) GetMarketCandles(ctx context.Context, marketID, timeWindow string, from, to time.Time, before, after, limit int64) (out CandleResponse, err error) { if (before > 0) && (after >= 0) { return out, errors.New("BTCMarkets only supports either before or after, not both") } params := url.Values{} params.Set("timeWindow", timeWindow) if from.After(to) && !to.IsZero() { return out, errors.New("start time cannot be after end time") } if !from.IsZero() { params.Set("from", from.UTC().Format(time.RFC3339)) } if !to.IsZero() { params.Set("to", to.UTC().Format(time.RFC3339)) } if before > 0 { params.Set("before", strconv.FormatInt(before, 10)) } if after >= 0 { params.Set("after", strconv.FormatInt(after, 10)) } if limit > 0 { params.Set("limit", strconv.FormatInt(limit, 10)) } return out, b.SendHTTPRequest(ctx, btcMarketsUnauthPath+marketID+btcMarketsCandles+params.Encode(), &out) } // GetTickers gets multiple tickers func (b *BTCMarkets) GetTickers(ctx context.Context, marketIDs currency.Pairs) ([]Ticker, error) { var tickers []Ticker params := url.Values{} for x := range marketIDs { params.Add("marketId", marketIDs[x].String()) } return tickers, b.SendHTTPRequest(ctx, btcMarketsUnauthPath+btcMarketsTickers+params.Encode(), &tickers) } // GetMultipleOrderbooks gets orderbooks func (b *BTCMarkets) GetMultipleOrderbooks(ctx context.Context, marketIDs []string) ([]Orderbook, error) { var temp []tempOrderbook params := url.Values{} for x := range marketIDs { params.Add("marketId", marketIDs[x]) } err := b.SendHTTPRequest(ctx, btcMarketsUnauthPath+btcMarketsMultipleOrderbooks+params.Encode(), &temp) if err != nil { return nil, err } orderbooks := make([]Orderbook, 0, len(marketIDs)) for i := range temp { var tempOB Orderbook var price, volume float64 tempOB.MarketID = temp[i].MarketID tempOB.SnapshotID = temp[i].SnapshotID tempOB.Asks = make([]OBData, len(temp[i].Asks)) tempOB.Bids = make([]OBData, len(temp[i].Bids)) for a := range temp[i].Asks { volume, err = strconv.ParseFloat(temp[i].Asks[a][1], 64) if err != nil { return orderbooks, err } price, err = strconv.ParseFloat(temp[i].Asks[a][0], 64) if err != nil { return orderbooks, err } tempOB.Asks[a] = OBData{Price: price, Volume: volume} } for y := range temp[i].Bids { volume, err = strconv.ParseFloat(temp[i].Bids[y][1], 64) if err != nil { return orderbooks, err } price, err = strconv.ParseFloat(temp[i].Bids[y][0], 64) if err != nil { return orderbooks, err } tempOB.Bids[y] = OBData{Price: price, Volume: volume} } orderbooks = append(orderbooks, tempOB) } return orderbooks, nil } // GetCurrentServerTime gets time from btcmarkets func (b *BTCMarkets) GetCurrentServerTime(ctx context.Context) (time.Time, error) { var resp TimeResp return resp.Time, b.SendHTTPRequest(ctx, btcMarketsAPIURL+btcMarketsAPIVersion+btcMarketsGetTime, &resp) } // GetAccountBalance returns the full account balance func (b *BTCMarkets) GetAccountBalance(ctx context.Context) ([]AccountData, error) { var resp []AccountData return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet, btcMarketsAccountBalance, nil, &resp, request.Auth) } // GetTradingFees returns trading fees for all pairs based on trading activity func (b *BTCMarkets) GetTradingFees(ctx context.Context) (TradingFeeResponse, error) { var resp TradingFeeResponse return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet, btcMarketsTradingFees, nil, &resp, request.Auth) } // GetTradeHistory returns trade history func (b *BTCMarkets) GetTradeHistory(ctx context.Context, marketID, orderID string, before, after, limit int64) ([]TradeHistoryData, error) { if (before > 0) && (after >= 0) { return nil, errors.New("BTCMarkets only supports either before or after, not both") } var resp []TradeHistoryData params := url.Values{} if marketID != "" { params.Set("marketId", marketID) } if orderID != "" { params.Set("orderId", orderID) } if before > 0 { params.Set("before", strconv.FormatInt(before, 10)) } if after >= 0 { params.Set("after", strconv.FormatInt(after, 10)) } if limit > 0 { params.Set("limit", strconv.FormatInt(limit, 10)) } return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet, common.EncodeURLValues(btcMarketsTradeHistory, params), nil, &resp, request.Auth) } // GetTradeByID returns the singular trade of the ID given func (b *BTCMarkets) GetTradeByID(ctx context.Context, id string) (TradeHistoryData, error) { var resp TradeHistoryData return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet, btcMarketsTradeHistory+"/"+id, nil, &resp, request.Auth) } // formatOrderType conforms order type to the exchange acceptable order type // strings func (b *BTCMarkets) formatOrderType(o order.Type) (string, error) { switch o { case order.Limit: return limit, nil case order.Market: return market, nil case order.StopLimit: return stopLimit, nil case order.Stop: return stop, nil case order.TakeProfit: return takeProfit, nil default: return "", fmt.Errorf("%s %s %w", b.Name, o, order.ErrTypeIsInvalid) } } // formatOrderSide conforms order side to the exchange acceptable order side // strings func (b *BTCMarkets) formatOrderSide(o order.Side) (string, error) { switch o { case order.Ask: return askSide, nil case order.Bid: return bidSide, nil default: return "", fmt.Errorf("%s %s %w", b.Name, o, order.ErrSideIsInvalid) } } // getTimeInForce returns a string depending on the options in order.Submit func (b *BTCMarkets) getTimeInForce(s *order.Submit) string { if s.ImmediateOrCancel { return immediateOrCancel } if s.FillOrKill { return fillOrKill } return "" // GTC (good till cancelled, default value) } // NewOrder requests a new order and returns an ID func (b *BTCMarkets) NewOrder(ctx context.Context, price, amount, triggerPrice, targetAmount float64, marketID, orderType, side, timeInForce, selfTrade, clientOrderID string, postOnly bool) (OrderData, error) { req := make(map[string]interface{}) req["marketId"] = marketID if price != 0 { req["price"] = strconv.FormatFloat(price, 'f', -1, 64) } req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64) req["type"] = orderType req["side"] = side if orderType == stopLimit || orderType == takeProfit || orderType == stop { req["triggerPrice"] = strconv.FormatFloat(triggerPrice, 'f', -1, 64) } if targetAmount > 0 { req["targetAmount"] = strconv.FormatFloat(targetAmount, 'f', -1, 64) } if timeInForce != "" { req["timeInForce"] = timeInForce } if postOnly { req["postOnly"] = postOnly } if selfTrade != "" { req["selfTrade"] = selfTrade } if clientOrderID != "" { req["clientOrderID"] = clientOrderID } var resp OrderData return resp, b.SendAuthenticatedRequest(ctx, http.MethodPost, btcMarketsOrders, req, &resp, orderFunc) } // GetOrders returns current order information on the exchange func (b *BTCMarkets) GetOrders(ctx context.Context, marketID string, before, after, limit int64, openOnly bool) ([]OrderData, error) { if (before > 0) && (after >= 0) { return nil, errors.New("BTCMarkets only supports either before or after, not both") } var resp []OrderData params := url.Values{} if marketID != "" { params.Set("marketId", marketID) } if before > 0 { params.Set("before", strconv.FormatInt(before, 10)) } if after >= 0 { params.Set("after", strconv.FormatInt(after, 10)) } if limit > 0 { params.Set("limit", strconv.FormatInt(limit, 10)) } if openOnly { params.Set("status", "open") } return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet, common.EncodeURLValues(btcMarketsOrders, params), nil, &resp, request.Auth) } // CancelAllOpenOrdersByPairs cancels all open orders unless pairs are specified func (b *BTCMarkets) CancelAllOpenOrdersByPairs(ctx context.Context, marketIDs []string) ([]CancelOrderResp, error) { var resp []CancelOrderResp req := make(map[string]interface{}) if len(marketIDs) > 0 { var strTemp strings.Builder for x := range marketIDs { strTemp.WriteString("marketId=" + marketIDs[x] + "&") } req["marketId"] = strTemp.String()[:strTemp.Len()-1] } return resp, b.SendAuthenticatedRequest(ctx, http.MethodDelete, btcMarketsOrders, req, &resp, request.Auth) } // FetchOrder finds order based on the provided id func (b *BTCMarkets) FetchOrder(ctx context.Context, id string) (*OrderData, error) { var resp *OrderData return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet, btcMarketsOrders+"/"+id, nil, &resp, request.Auth) } // RemoveOrder removes a given order func (b *BTCMarkets) RemoveOrder(ctx context.Context, id string) (CancelOrderResp, error) { var resp CancelOrderResp return resp, b.SendAuthenticatedRequest(ctx, http.MethodDelete, btcMarketsOrders+"/"+id, nil, &resp, request.Auth) } // ReplaceOrder cancels an order and then places a new order. func (b *BTCMarkets) ReplaceOrder(ctx context.Context, id, clientOrderID string, price, amount float64) (*OrderData, error) { if price <= 0 { return nil, fmt.Errorf("price %w", errInvalidAmount) } if amount <= 0 { return nil, fmt.Errorf("amount %w", errInvalidAmount) } if id == "" { return nil, errIDRequired } req := make(map[string]interface{}, 3) req["price"] = strconv.FormatFloat(price, 'f', -1, 64) req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64) if clientOrderID != "" { req["clientOrderId"] = clientOrderID } var resp *OrderData return resp, b.SendAuthenticatedRequest(ctx, http.MethodPut, btcMarketsOrders+"/"+id, req, &resp, request.Auth) } // ListWithdrawals lists the withdrawal history func (b *BTCMarkets) ListWithdrawals(ctx context.Context, before, after, limit int64) ([]TransferData, error) { if (before > 0) && (after >= 0) { return nil, errors.New("BTCMarkets only supports either before or after, not both") } var resp []TransferData params := url.Values{} if before > 0 { params.Set("before", strconv.FormatInt(before, 10)) } if after >= 0 { params.Set("after", strconv.FormatInt(after, 10)) } if limit > 0 { params.Set("limit", strconv.FormatInt(limit, 10)) } return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet, common.EncodeURLValues(btcMarketsWithdrawals, params), nil, &resp, request.Auth) } // GetWithdrawal gets withdrawawl info for a given id func (b *BTCMarkets) GetWithdrawal(ctx context.Context, id string) (TransferData, error) { var resp TransferData if id == "" { return resp, errors.New("id cannot be an empty string") } return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet, btcMarketsWithdrawals+"/"+id, nil, &resp, request.Auth) } // ListDeposits lists the deposit history func (b *BTCMarkets) ListDeposits(ctx context.Context, before, after, limit int64) ([]TransferData, error) { if (before > 0) && (after >= 0) { return nil, errors.New("BTCMarkets only supports either before or after, not both") } var resp []TransferData params := url.Values{} if before > 0 { params.Set("before", strconv.FormatInt(before, 10)) } if after >= 0 { params.Set("after", strconv.FormatInt(after, 10)) } if limit > 0 { params.Set("limit", strconv.FormatInt(limit, 10)) } return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet, common.EncodeURLValues(btcMarketsDeposits, params), nil, &resp, request.Auth) } // GetDeposit gets deposit info for a given ID func (b *BTCMarkets) GetDeposit(ctx context.Context, id string) (TransferData, error) { var resp TransferData return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet, btcMarketsDeposits+"/"+id, nil, &resp, request.Auth) } // ListTransfers lists the past asset transfers func (b *BTCMarkets) ListTransfers(ctx context.Context, before, after, limit int64) ([]TransferData, error) { if (before > 0) && (after >= 0) { return nil, errors.New("BTCMarkets only supports either before or after, not both") } var resp []TransferData params := url.Values{} if before > 0 { params.Set("before", strconv.FormatInt(before, 10)) } if after >= 0 { params.Set("after", strconv.FormatInt(after, 10)) } if limit > 0 { params.Set("limit", strconv.FormatInt(limit, 10)) } return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet, common.EncodeURLValues(btcMarketsTransfers, params), nil, &resp, request.Auth) } // GetTransfer gets asset transfer info for a given ID func (b *BTCMarkets) GetTransfer(ctx context.Context, id string) (TransferData, error) { var resp TransferData return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet, btcMarketsTransfers+"/"+id, nil, &resp, request.Auth) } // FetchDepositAddress gets deposit address for the given asset func (b *BTCMarkets) FetchDepositAddress(ctx context.Context, curr currency.Code, before, after, limit int64) (*DepositAddress, error) { var resp DepositAddress if (before > 0) && (after >= 0) { return nil, errors.New("BTCMarkets only supports either before or after, not both") } params := url.Values{} params.Set("assetName", curr.Upper().String()) if before > 0 { params.Set("before", strconv.FormatInt(before, 10)) } if after >= 0 { params.Set("after", strconv.FormatInt(after, 10)) } if limit > 0 { params.Set("limit", strconv.FormatInt(limit, 10)) } if err := b.SendAuthenticatedRequest(ctx, http.MethodGet, common.EncodeURLValues(btcMarketsAddresses, params), nil, &resp, request.Auth); err != nil { return nil, err } if curr == currency.XRP { splitStr := "?dt=" if !strings.Contains(resp.Address, splitStr) { return nil, errors.New("unable to find split string for XRP") } splitter := strings.Split(resp.Address, splitStr) resp.Address = splitter[0] resp.Tag = splitter[1] } return &resp, nil } // GetWithdrawalFees gets withdrawal fees for all assets func (b *BTCMarkets) GetWithdrawalFees(ctx context.Context) ([]WithdrawalFeeData, error) { var resp []WithdrawalFeeData return resp, b.SendHTTPRequest(ctx, btcMarketsAPIURL+btcMarketsAPIVersion+btcMarketsWithdrawalFees, &resp) } // ListAssets lists all available assets func (b *BTCMarkets) ListAssets(ctx context.Context) ([]AssetData, error) { var resp []AssetData return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet, btcMarketsAssets, nil, &resp, request.Auth) } // GetTransactions gets trading fees func (b *BTCMarkets) GetTransactions(ctx context.Context, assetName string, before, after, limit int64) ([]TransactionData, error) { if (before > 0) && (after >= 0) { return nil, errors.New("BTCMarkets only supports either before or after, not both") } var resp []TransactionData params := url.Values{} if assetName != "" { params.Set("assetName", assetName) } if before > 0 { params.Set("before", strconv.FormatInt(before, 10)) } if after >= 0 { params.Set("after", strconv.FormatInt(after, 10)) } if limit > 0 { params.Set("limit", strconv.FormatInt(limit, 10)) } return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet, common.EncodeURLValues(btcMarketsTransactions, params), nil, &resp, request.Auth) } // CreateNewReport creates a new report func (b *BTCMarkets) CreateNewReport(ctx context.Context, reportType, format string) (CreateReportResp, error) { var resp CreateReportResp req := make(map[string]interface{}) req["type"] = reportType req["format"] = format return resp, b.SendAuthenticatedRequest(ctx, http.MethodPost, btcMarketsReports, req, &resp, newReportFunc) } // GetReport finds details bout a past report func (b *BTCMarkets) GetReport(ctx context.Context, reportID string) (ReportData, error) { var resp ReportData return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet, btcMarketsReports+"/"+reportID, nil, &resp, request.Auth) } // RequestWithdraw requests withdrawals func (b *BTCMarkets) RequestWithdraw(ctx context.Context, assetName string, amount float64, toAddress, accountName, accountNumber, bsbNumber, bankName string) (TransferData, error) { var resp TransferData req := make(map[string]interface{}) req["assetName"] = assetName req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64) if assetName != "AUD" { req["toAddress"] = toAddress } else { if accountName != "" { req["accountName"] = accountName } if accountNumber != "" { req["accountNumber"] = accountNumber } if bsbNumber != "" { req["bsbNumber"] = bsbNumber } if bankName != "" { req["bankName"] = bankName } } return resp, b.SendAuthenticatedRequest(ctx, http.MethodPost, btcMarketsWithdrawals, req, &resp, withdrawFunc) } // BatchPlaceCancelOrders places and cancels batch orders func (b *BTCMarkets) BatchPlaceCancelOrders(ctx context.Context, cancelOrders []CancelBatch, placeOrders []PlaceBatch) (*BatchPlaceCancelResponse, error) { numActions := len(cancelOrders) + len(placeOrders) if numActions > 4 { return nil, errors.New("BTCMarkets can only handle 4 orders at a time") } orderRequests := make([]interface{}, numActions) for x := range cancelOrders { orderRequests[x] = CancelOrderMethod{CancelOrder: cancelOrders[x]} } for y := range placeOrders { if placeOrders[y].ClientOrderID == "" { return nil, errors.New("placeorders must have ClientOrderID filled") } orderRequests[y] = PlaceOrderMethod{PlaceOrder: placeOrders[y]} } var resp BatchPlaceCancelResponse return &resp, b.SendAuthenticatedRequest(ctx, http.MethodPost, btcMarketsBatchOrders, orderRequests, &resp, batchFunc) } // GetBatchTrades gets batch trades func (b *BTCMarkets) GetBatchTrades(ctx context.Context, ids []string) (BatchTradeResponse, error) { var resp BatchTradeResponse if len(ids) > 50 { return resp, errors.New("batchtrades can only handle 50 ids at a time") } marketIDs := strings.Join(ids, ",") return resp, b.SendAuthenticatedRequest(ctx, http.MethodGet, btcMarketsBatchOrders+"/"+marketIDs, nil, &resp, request.Auth) } // CancelBatch cancels given ids func (b *BTCMarkets) CancelBatch(ctx context.Context, ids []string) (BatchCancelResponse, error) { var resp BatchCancelResponse marketIDs := strings.Join(ids, ",") return resp, b.SendAuthenticatedRequest(ctx, http.MethodDelete, btcMarketsBatchOrders+"/"+marketIDs, nil, &resp, batchFunc) } // SendHTTPRequest sends an unauthenticated HTTP request func (b *BTCMarkets) SendHTTPRequest(ctx context.Context, path string, result interface{}) error { item := &request.Item{ Method: http.MethodGet, Path: path, Result: result, Verbose: b.Verbose, HTTPDebugging: b.HTTPDebugging, HTTPRecording: b.HTTPRecording, } return b.SendPayload(ctx, request.Unset, func() (*request.Item, error) { return item, nil }) } // SendAuthenticatedRequest sends an authenticated HTTP request func (b *BTCMarkets) SendAuthenticatedRequest(ctx context.Context, method, path string, data, result interface{}, f request.EndpointLimit) (err error) { creds, err := b.GetCredentials(ctx) if err != nil { return err } newRequest := func() (*request.Item, error) { strTime := strconv.FormatInt(time.Now().UnixMilli(), 10) var body io.Reader var payload, hmac []byte switch data.(type) { case map[string]interface{}, []interface{}: payload, err = json.Marshal(data) if err != nil { return nil, err } body = bytes.NewBuffer(payload) strMsg := method + btcMarketsAPIVersion + path + strTime + string(payload) hmac, err = crypto.GetHMAC(crypto.HashSHA512, []byte(strMsg), []byte(creds.Secret)) if err != nil { return nil, err } default: strArray := strings.Split(path, "?") hmac, err = crypto.GetHMAC(crypto.HashSHA512, []byte(method+btcMarketsAPIVersion+strArray[0]+strTime), []byte(creds.Secret)) if err != nil { return nil, err } } headers := make(map[string]string) headers["Accept"] = "application/json" headers["Accept-Charset"] = "UTF-8" headers["Content-Type"] = "application/json" headers["BM-AUTH-APIKEY"] = creds.Key headers["BM-AUTH-TIMESTAMP"] = strTime headers["BM-AUTH-SIGNATURE"] = crypto.Base64Encode(hmac) return &request.Item{ Method: method, Path: btcMarketsAPIURL + btcMarketsAPIVersion + path, Headers: headers, Body: body, Result: result, AuthRequest: true, Verbose: b.Verbose, HTTPDebugging: b.HTTPDebugging, HTTPRecording: b.HTTPRecording, }, nil } return b.SendPayload(ctx, f, newRequest) } // GetFee returns an estimate of fee based on type of transaction func (b *BTCMarkets) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) { var fee float64 switch feeBuilder.FeeType { case exchange.CryptocurrencyTradeFee: temp, err := b.GetTradingFees(ctx) if err != nil { return fee, err } for x := range temp.FeeByMarkets { p, err := currency.NewPairFromString(temp.FeeByMarkets[x].MarketID) if err != nil { return 0, err } if p == feeBuilder.Pair { fee = temp.FeeByMarkets[x].MakerFeeRate if !feeBuilder.IsMaker { fee = temp.FeeByMarkets[x].TakerFeeRate } } } case exchange.CryptocurrencyWithdrawalFee: temp, err := b.GetWithdrawalFees(ctx) if err != nil { return fee, err } for x := range temp { if currency.NewCode(temp[x].AssetName) == feeBuilder.Pair.Base { fee = temp[x].Fee * feeBuilder.PurchasePrice * feeBuilder.Amount } } case exchange.InternationalBankWithdrawalFee: return 0, errors.New("international bank withdrawals are not supported") case exchange.OfflineTradeFee: fee = getOfflineTradeFee(feeBuilder) } if fee < 0 { fee = 0 } return fee, nil } // getOfflineTradeFee calculates the worst case-scenario trading fee func getOfflineTradeFee(feeBuilder *exchange.FeeBuilder) float64 { switch { case feeBuilder.Pair.IsCryptoPair(): return 0.002 * feeBuilder.PurchasePrice * feeBuilder.Amount default: return 0.0085 * feeBuilder.PurchasePrice * feeBuilder.Amount } }