package bitstamp import ( "bytes" "encoding/json" "errors" "fmt" "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/websocket/wshandler" log "github.com/thrasher-corp/gocryptotrader/logger" ) const ( bitstampAPIURL = "https://www.bitstamp.net/api" bitstampAPIVersion = "2" bitstampAPITicker = "ticker" bitstampAPITickerHourly = "ticker_hour" bitstampAPIOrderbook = "order_book" bitstampAPITransactions = "transactions" bitstampAPIEURUSD = "eur_usd" bitstampAPIBalance = "balance" bitstampAPIUserTransactions = "user_transactions" bitstampAPIOpenOrders = "open_orders" bitstampAPIOrderStatus = "order_status" bitstampAPICancelOrder = "cancel_order" bitstampAPICancelAllOrders = "cancel_all_orders" bitstampAPIMarket = "market" bitstampAPIWithdrawalRequests = "withdrawal_requests" bitstampAPIOpenWithdrawal = "withdrawal/open" bitstampAPIBitcoinWithdrawal = "bitcoin_withdrawal" bitstampAPILTCWithdrawal = "ltc_withdrawal" bitstampAPIETHWithdrawal = "eth_withdrawal" bitstampAPIBCHWithdrawal = "bch_withdrawal" bitstampAPIBitcoinDeposit = "bitcoin_deposit_address" bitstampAPILitecoinDeposit = "ltc_address" bitstampAPIEthereumDeposit = "eth_address" bitstampAPIBitcoinCashDeposit = "bch_address" bitstampAPIUnconfirmedBitcoin = "unconfirmed_btc" bitstampAPITransferToMain = "transfer-to-main" bitstampAPITransferFromMain = "transfer-from-main" bitstampAPIXrpWithdrawal = "xrp_withdrawal" bitstampAPIXrpDeposit = "xrp_address" bitstampAPIReturnType = "string" bitstampAPITradingPairsInfo = "trading-pairs-info" bitstampAuthRate = 8000 bitstampUnauthRate = 8000 bitstampTimeLayout = "2006-1-2 15:04:05" ) // Bitstamp is the overarching type across the bitstamp package type Bitstamp struct { exchange.Base WebsocketConn *wshandler.WebsocketConnection } // GetFee returns an estimate of fee based on type of transaction func (b *Bitstamp) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) { var fee float64 switch feeBuilder.FeeType { case exchange.CryptocurrencyTradeFee: balance, err := b.GetBalance() if err != nil { return 0, err } fee = b.CalculateTradingFee(feeBuilder.Pair.Base, feeBuilder.Pair.Quote, feeBuilder.PurchasePrice, feeBuilder.Amount, balance) case exchange.CyptocurrencyDepositFee: fee = 0 case exchange.InternationalBankDepositFee: fee = getInternationalBankDepositFee(feeBuilder.Amount) case exchange.InternationalBankWithdrawalFee: fee = getInternationalBankWithdrawalFee(feeBuilder.Amount) 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.0025 * price * amount } // getInternationalBankWithdrawalFee returns international withdrawal fee func getInternationalBankWithdrawalFee(amount float64) float64 { fee := amount * 0.0009 if fee < 15 { return 15 } return fee } // getInternationalBankDepositFee returns international deposit fee func getInternationalBankDepositFee(amount float64) float64 { fee := amount * 0.0005 if fee < 7.5 { return 7.5 } if fee > 300 { return 300 } return fee } // CalculateTradingFee returns fee on a currency pair func (b *Bitstamp) CalculateTradingFee(base, quote currency.Code, purchasePrice, amount float64, balances Balances) float64 { var fee float64 if v, ok := balances[base.String()]; ok { switch quote { case currency.BTC: fee = v.BTCFee case currency.USD: fee = v.USDFee case currency.EUR: fee = v.EURFee } } return fee * purchasePrice * amount } // GetTicker returns ticker information func (b *Bitstamp) GetTicker(currency string, hourly bool) (Ticker, error) { response := Ticker{} tickerEndpoint := bitstampAPITicker if hourly { tickerEndpoint = bitstampAPITickerHourly } path := fmt.Sprintf( "%s/v%s/%s/%s/", b.API.Endpoints.URL, bitstampAPIVersion, tickerEndpoint, strings.ToLower(currency), ) return response, b.SendHTTPRequest(path, &response) } // GetOrderbook Returns a JSON dictionary with "bids" and "asks". Each is a list // of open orders and each order is represented as a list holding the price and // the amount. func (b *Bitstamp) GetOrderbook(currency string) (Orderbook, error) { type response struct { Timestamp int64 `json:"timestamp,string"` Bids [][]string `json:"bids"` Asks [][]string `json:"asks"` } resp := response{} path := fmt.Sprintf( "%s/v%s/%s/%s/", b.API.Endpoints.URL, bitstampAPIVersion, bitstampAPIOrderbook, strings.ToLower(currency), ) err := b.SendHTTPRequest(path, &resp) if err != nil { return Orderbook{}, err } orderbook := Orderbook{} orderbook.Timestamp = resp.Timestamp for _, x := range resp.Bids { price, err := strconv.ParseFloat(x[0], 64) if err != nil { log.Error(log.ExchangeSys, err) continue } amount, err := strconv.ParseFloat(x[1], 64) if err != nil { log.Error(log.ExchangeSys, err) continue } orderbook.Bids = append(orderbook.Bids, OrderbookBase{price, amount}) } for _, x := range resp.Asks { price, err := strconv.ParseFloat(x[0], 64) if err != nil { log.Error(log.ExchangeSys, err) continue } amount, err := strconv.ParseFloat(x[1], 64) if err != nil { log.Error(log.ExchangeSys, err) continue } orderbook.Asks = append(orderbook.Asks, OrderbookBase{price, amount}) } return orderbook, nil } // GetTradingPairs returns a list of trading pairs which Bitstamp // currently supports func (b *Bitstamp) GetTradingPairs() ([]TradingPair, error) { var result []TradingPair path := fmt.Sprintf("%s/v%s/%s", b.API.Endpoints.URL, bitstampAPIVersion, bitstampAPITradingPairsInfo) return result, b.SendHTTPRequest(path, &result) } // GetTransactions returns transaction information // value paramater ["time"] = "minute", "hour", "day" will collate your // response into time intervals. Implementation of value in test code. func (b *Bitstamp) GetTransactions(currencyPair string, values url.Values) ([]Transactions, error) { var transactions []Transactions path := common.EncodeURLValues( fmt.Sprintf( "%s/v%s/%s/%s/", b.API.Endpoints.URL, bitstampAPIVersion, bitstampAPITransactions, strings.ToLower(currencyPair), ), values, ) return transactions, b.SendHTTPRequest(path, &transactions) } // GetEURUSDConversionRate returns the conversion rate between Euro and USD func (b *Bitstamp) GetEURUSDConversionRate() (EURUSDConversionRate, error) { rate := EURUSDConversionRate{} path := fmt.Sprintf("%s/%s", b.API.Endpoints.URL, bitstampAPIEURUSD) return rate, b.SendHTTPRequest(path, &rate) } // GetBalance returns full balance of currency held on the exchange func (b *Bitstamp) GetBalance() (Balances, error) { var balance map[string]string err := b.SendAuthenticatedHTTPRequest(bitstampAPIBalance, true, nil, &balance) if err != nil { return nil, err } balances := make(map[string]Balance) for k := range balance { curr := k[0:3] _, ok := balances[strings.ToUpper(curr)] if !ok { avail, _ := strconv.ParseFloat(balance[curr+"_available"], 64) bal, _ := strconv.ParseFloat(balance[curr+"_balance"], 64) reserved, _ := strconv.ParseFloat(balance[curr+"_reserved"], 64) withdrawalFee, _ := strconv.ParseFloat(balance[curr+"_withdrawal_fee"], 64) currBalance := Balance{ Available: avail, Balance: bal, Reserved: reserved, WithdrawalFee: withdrawalFee, } switch strings.ToUpper(curr) { case currency.USD.String(): eurFee, _ := strconv.ParseFloat(balance[curr+"eur_fee"], 64) currBalance.EURFee = eurFee case currency.EUR.String(): usdFee, _ := strconv.ParseFloat(balance[curr+"usd_fee"], 64) currBalance.USDFee = usdFee default: btcFee, _ := strconv.ParseFloat(balance[curr+"btc_fee"], 64) currBalance.BTCFee = btcFee eurFee, _ := strconv.ParseFloat(balance[curr+"eur_fee"], 64) currBalance.EURFee = eurFee usdFee, _ := strconv.ParseFloat(balance[curr+"usd_fee"], 64) currBalance.USDFee = usdFee } balances[strings.ToUpper(curr)] = currBalance } } return balances, nil } // GetUserTransactions returns an array of transactions func (b *Bitstamp) GetUserTransactions(currencyPair string) ([]UserTransactions, error) { type Response struct { Date string `json:"datetime"` TransactionID int64 `json:"id"` Type int `json:"type,string"` USD interface{} `json:"usd"` EUR interface{} `json:"eur"` XRP interface{} `json:"xrp"` BTC interface{} `json:"btc"` BTCUSD interface{} `json:"btc_usd"` Fee float64 `json:"fee,string"` OrderID int64 `json:"order_id"` } var response []Response if currencyPair == "" { if err := b.SendAuthenticatedHTTPRequest(bitstampAPIUserTransactions, true, url.Values{}, &response); err != nil { return nil, err } } else { if err := b.SendAuthenticatedHTTPRequest(bitstampAPIUserTransactions+"/"+currencyPair, true, url.Values{}, &response); err != nil { return nil, err } } processNumber := func(i interface{}) float64 { switch t := i.(type) { case float64: return t case string: amt, _ := strconv.ParseFloat(t, 64) return amt default: return 0 } } var transactions []UserTransactions for x := range response { tx := UserTransactions{} tx.Date = response[x].Date tx.TransactionID = response[x].TransactionID tx.Type = response[x].Type tx.EUR = processNumber(response[x].EUR) tx.XRP = processNumber(response[x].XRP) tx.USD = processNumber(response[x].USD) tx.BTC = processNumber(response[x].BTC) tx.BTCUSD = processNumber(response[x].BTCUSD) tx.Fee = response[x].Fee tx.OrderID = response[x].OrderID transactions = append(transactions, tx) } return transactions, nil } // GetOpenOrders returns all open orders on the exchange func (b *Bitstamp) GetOpenOrders(currencyPair string) ([]Order, error) { var resp []Order path := fmt.Sprintf( "%s/%s", bitstampAPIOpenOrders, strings.ToLower(currencyPair), ) return resp, b.SendAuthenticatedHTTPRequest(path, true, nil, &resp) } // GetOrderStatus returns an the status of an order by its ID func (b *Bitstamp) GetOrderStatus(orderID int64) (OrderStatus, error) { resp := OrderStatus{} req := url.Values{} req.Add("id", strconv.FormatInt(orderID, 10)) return resp, b.SendAuthenticatedHTTPRequest(bitstampAPIOrderStatus, false, req, &resp) } // CancelExistingOrder cancels order by ID func (b *Bitstamp) CancelExistingOrder(orderID int64) (CancelOrder, error) { var req = url.Values{} req.Add("id", strconv.FormatInt(orderID, 10)) var result CancelOrder err := b.SendAuthenticatedHTTPRequest(bitstampAPICancelOrder, true, req, &result) if err != nil { return result, err } return result, nil } // CancelAllExistingOrders cancels all open orders on the exchange func (b *Bitstamp) CancelAllExistingOrders() (bool, error) { result := false return result, b.SendAuthenticatedHTTPRequest(bitstampAPICancelAllOrders, false, nil, &result) } // PlaceOrder places an order on the exchange. func (b *Bitstamp) PlaceOrder(currencyPair string, price, amount float64, buy, market bool) (Order, error) { var req = url.Values{} req.Add("amount", strconv.FormatFloat(amount, 'f', -1, 64)) req.Add("price", strconv.FormatFloat(price, 'f', -1, 64)) response := Order{} orderType := order.Buy.Lower() if !buy { orderType = order.Sell.Lower() } var path string if market { path = fmt.Sprintf("%s/%s/%s", orderType, bitstampAPIMarket, strings.ToLower(currencyPair)) } else { path = fmt.Sprintf("%s/%s", orderType, strings.ToLower(currencyPair)) } return response, b.SendAuthenticatedHTTPRequest(path, true, req, &response) } // GetWithdrawalRequests returns withdrawal requests for the account // timedelta - positive integer with max value 50000000 which returns requests // from number of seconds ago to now. func (b *Bitstamp) GetWithdrawalRequests(timedelta int64) ([]WithdrawalRequests, error) { var resp []WithdrawalRequests if timedelta > 50000000 || timedelta < 0 { return resp, errors.New("time delta exceeded, max: 50000000 min: 0") } value := url.Values{} value.Set("timedelta", strconv.FormatInt(timedelta, 10)) if timedelta == 0 { value = url.Values{} } return resp, b.SendAuthenticatedHTTPRequest(bitstampAPIWithdrawalRequests, false, value, &resp) } // CryptoWithdrawal withdraws a cryptocurrency into a supplied wallet, returns ID // amount - The amount you want withdrawn // address - The wallet address of the cryptocurrency // symbol - the type of crypto ie "ltc", "btc", "eth" // destTag - only for XRP default to "" // instant - only for bitcoins func (b *Bitstamp) CryptoWithdrawal(amount float64, address, symbol, destTag string, instant bool) (CryptoWithdrawalResponse, error) { var req = url.Values{} req.Add("amount", strconv.FormatFloat(amount, 'f', -1, 64)) req.Add("address", address) resp := CryptoWithdrawalResponse{} var endpoint string switch strings.ToLower(symbol) { case currency.BTC.Lower().String(): if instant { req.Add("instant", "1") } else { req.Add("instant", "0") } endpoint = bitstampAPIBitcoinWithdrawal case currency.LTC.Lower().String(): endpoint = bitstampAPILTCWithdrawal case currency.ETH.Lower().String(): endpoint = bitstampAPIETHWithdrawal case currency.XRP.Lower().String(): if destTag != "" { req.Add("destination_tag", destTag) } endpoint = bitstampAPIXrpWithdrawal case currency.BCH.Lower().String(): endpoint = bitstampAPIBCHWithdrawal default: return resp, errors.New("incorrect symbol") } return resp, b.SendAuthenticatedHTTPRequest(endpoint, false, req, &resp) } // OpenBankWithdrawal Opens a bank withdrawal request (SEPA or international) func (b *Bitstamp) OpenBankWithdrawal(amount float64, currency, name, iban, bic, address, postalCode, city, country, comment, withdrawalType string) (FIATWithdrawalResponse, error) { var req = url.Values{} req.Add("amount", strconv.FormatFloat(amount, 'f', -1, 64)) req.Add("account_currency", currency) req.Add("name", name) req.Add("iban", iban) req.Add("bic", bic) req.Add("address", address) req.Add("postal_code", postalCode) req.Add("city", city) req.Add("country", country) req.Add("type", withdrawalType) req.Add("comment", comment) resp := FIATWithdrawalResponse{} return resp, b.SendAuthenticatedHTTPRequest(bitstampAPIOpenWithdrawal, true, req, &resp) } // OpenInternationalBankWithdrawal Opens a bank withdrawal request (international) func (b *Bitstamp) OpenInternationalBankWithdrawal(amount float64, currency, name, iban, bic, address, postalCode, city, country, bankName, bankAddress, bankPostCode, bankCity, bankCountry, internationalCurrency, comment, withdrawalType string) (FIATWithdrawalResponse, error) { var req = url.Values{} req.Add("amount", strconv.FormatFloat(amount, 'f', -1, 64)) req.Add("account_currency", currency) req.Add("name", name) req.Add("iban", iban) req.Add("bic", bic) req.Add("address", address) req.Add("postal_code", postalCode) req.Add("city", city) req.Add("country", country) req.Add("type", withdrawalType) req.Add("comment", comment) req.Add("currency", internationalCurrency) req.Add("bank_name", bankName) req.Add("bank_address", bankAddress) req.Add("bank_postal_code", bankPostCode) req.Add("bank_city", bankCity) req.Add("bank_country", bankCountry) resp := FIATWithdrawalResponse{} return resp, b.SendAuthenticatedHTTPRequest(bitstampAPIOpenWithdrawal, true, req, &resp) } // GetCryptoDepositAddress returns a depositing address by crypto // crypto - example "btc", "ltc", "eth", "xrp" or "bch" func (b *Bitstamp) GetCryptoDepositAddress(crypto currency.Code) (string, error) { var resp string v2Resp := struct { Address string `json:"address"` }{} switch crypto { case currency.BTC: return resp, b.SendAuthenticatedHTTPRequest(bitstampAPIBitcoinDeposit, false, nil, &resp) case currency.LTC: return v2Resp.Address, b.SendAuthenticatedHTTPRequest(bitstampAPILitecoinDeposit, true, nil, &v2Resp) case currency.ETH: return v2Resp.Address, b.SendAuthenticatedHTTPRequest(bitstampAPIEthereumDeposit, true, nil, &v2Resp) case currency.XRP: return v2Resp.Address, b.SendAuthenticatedHTTPRequest(bitstampAPIXrpDeposit, true, nil, &v2Resp) case currency.BCH: return v2Resp.Address, b.SendAuthenticatedHTTPRequest(bitstampAPIBitcoinCashDeposit, true, nil, &v2Resp) default: return resp, fmt.Errorf("unsupported cryptocurrency string %s", crypto) } } // GetUnconfirmedBitcoinDeposits returns unconfirmed transactions func (b *Bitstamp) GetUnconfirmedBitcoinDeposits() ([]UnconfirmedBTCTransactions, error) { var response []UnconfirmedBTCTransactions return response, b.SendAuthenticatedHTTPRequest(bitstampAPIUnconfirmedBitcoin, false, nil, &response) } // TransferAccountBalance transfers funds from either a main or sub account // amount - to transfers // currency - which currency to transfer // subaccount - name of account // toMain - bool either to or from account func (b *Bitstamp) TransferAccountBalance(amount float64, currency, subAccount string, toMain bool) error { var req = url.Values{} req.Add("amount", strconv.FormatFloat(amount, 'f', -1, 64)) req.Add("currency", currency) if subAccount == "" { return errors.New("missing subAccount parameter") } req.Add("subAccount", subAccount) var path string if toMain { path = bitstampAPITransferToMain } else { path = bitstampAPITransferFromMain } var resp interface{} return b.SendAuthenticatedHTTPRequest(path, true, req, &resp) } // SendHTTPRequest sends an unauthenticated HTTP request func (b *Bitstamp) SendHTTPRequest(path string, result interface{}) error { return b.SendPayload(http.MethodGet, path, nil, nil, result, false, false, b.Verbose, b.HTTPDebugging, b.HTTPRecording) } // SendAuthenticatedHTTPRequest sends an authenticated request func (b *Bitstamp) SendAuthenticatedHTTPRequest(path string, v2 bool, values url.Values, result interface{}) error { if !b.AllowAuthenticatedRequest() { return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) } n := b.Requester.GetNonce(true).String() if values == nil { values = url.Values{} } values.Set("key", b.API.Credentials.Key) values.Set("nonce", n) hmac := crypto.GetHMAC(crypto.HashSHA256, []byte(n+b.API.Credentials.ClientID+b.API.Credentials.Key), []byte(b.API.Credentials.Secret)) values.Set("signature", strings.ToUpper(crypto.HexEncodeToString(hmac))) if v2 { path = fmt.Sprintf("%s/v%s/%s/", b.API.Endpoints.URL, bitstampAPIVersion, path) } else { path = fmt.Sprintf("%s/%s/", b.API.Endpoints.URL, path) } if b.Verbose { log.Debugf(log.ExchangeSys, "Sending POST request to "+path) } headers := make(map[string]string) headers["Content-Type"] = "application/x-www-form-urlencoded" encodedValues := values.Encode() readerValues := bytes.NewBufferString(encodedValues) interim := json.RawMessage{} errCap := struct { Error string `json:"error"` Status string `json:"status"` Reason interface{} `json:"reason"` }{} err := b.SendPayload(http.MethodPost, path, headers, readerValues, &interim, true, true, b.Verbose, b.HTTPDebugging, b.HTTPRecording) if err != nil { return err } if err := json.Unmarshal(interim, &errCap); err == nil { if errCap.Error != "" { return errors.New(errCap.Error) } if data, ok := errCap.Reason.(map[string][]string); ok { var details string for _, v := range data { details += strings.Join(v, "") } return errors.New(details) } if data, ok := errCap.Reason.(string); ok { return errors.New(data) } if errCap.Status != "" { return errors.New(errCap.Status) } } return json.Unmarshal(interim, result) } func parseTime(dateTime string) (time.Time, error) { return time.Parse(bitstampTimeLayout, dateTime) }