Files
gocryptotrader/exchanges/itbit/itbit.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

444 lines
14 KiB
Go

package itbit
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"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/request"
)
const (
itbitAPIURL = "https://api.itbit.com/v1"
itbitAPIVersion = "1"
itbitMarkets = "markets"
itbitOrderbook = "order_book"
itbitTicker = "ticker"
itbitWallets = "wallets"
itbitBalances = "balances"
itbitTrades = "trades"
itbitFundingHistory = "funding_history"
itbitOrders = "orders"
itbitCryptoDeposits = "cryptocurrency_deposits"
itbitWalletTransfer = "wallet_transfers"
)
// ItBit is the overarching type across the ItBit package
type ItBit struct {
exchange.Base
}
// GetTicker returns ticker info for a specified market.
// currencyPair - example "XBTUSD" "XBTSGD" "XBTEUR"
func (i *ItBit) GetTicker(ctx context.Context, currencyPair string) (Ticker, error) {
var response Ticker
path := fmt.Sprintf("/%s/%s/%s", itbitMarkets, currencyPair, itbitTicker)
return response, i.SendHTTPRequest(ctx, exchange.RestSpot, path, &response)
}
// GetOrderbook returns full order book for the specified market.
// currencyPair - example "XBTUSD" "XBTSGD" "XBTEUR"
func (i *ItBit) GetOrderbook(ctx context.Context, currencyPair string) (*OrderbookResponse, error) {
response := OrderbookResponse{}
path := fmt.Sprintf("/%s/%s/%s", itbitMarkets, currencyPair, itbitOrderbook)
return &response, i.SendHTTPRequest(ctx, exchange.RestSpot, path, &response)
}
// GetTradeHistory returns recent trades for a specified market.
//
// currencyPair - example "XBTUSD" "XBTSGD" "XBTEUR"
// timestamp - matchNumber, only executions after this will be returned
func (i *ItBit) GetTradeHistory(ctx context.Context, currencyPair, tradeID string) (Trades, error) {
response := Trades{}
var req = itbitTrades
if tradeID != "" {
req += "?since=" + tradeID
}
path := fmt.Sprintf("/%s/%s/%s", itbitMarkets, currencyPair, req)
return response, i.SendHTTPRequest(ctx, exchange.RestSpot, path, &response)
}
// GetWallets returns information about all wallets associated with the account.
//
// params --
//
// page - [optional] page to return example 1. default 1
// perPage - [optional] items per page example 50, default 50 max 50
func (i *ItBit) GetWallets(ctx context.Context, params url.Values) ([]Wallet, error) {
creds, err := i.GetCredentials(ctx)
if err != nil {
return nil, err
}
var resp []Wallet
params.Set("userId", creds.ClientID)
path := fmt.Sprintf("/%s?%s", itbitWallets, params.Encode())
return resp, i.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp)
}
// CreateWallet creates a new wallet with a specified name.
func (i *ItBit) CreateWallet(ctx context.Context, walletName string) (Wallet, error) {
creds, err := i.GetCredentials(ctx)
if err != nil {
return Wallet{}, err
}
resp := Wallet{}
params := make(map[string]interface{})
params["userId"] = creds.ClientID
params["name"] = walletName
err = i.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, "/"+itbitWallets, params, &resp)
if err != nil {
return resp, err
}
if resp.Description != "" {
return resp, errors.New(resp.Description)
}
return resp, nil
}
// GetWallet returns wallet information by walletID
func (i *ItBit) GetWallet(ctx context.Context, walletID string) (Wallet, error) {
resp := Wallet{}
path := fmt.Sprintf("/%s/%s", itbitWallets, walletID)
err := i.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp)
if err != nil {
return resp, err
}
if resp.Description != "" {
return resp, errors.New(resp.Description)
}
return resp, nil
}
// GetWalletBalance returns balance information for a specific currency in a
// wallet.
func (i *ItBit) GetWalletBalance(ctx context.Context, walletID, currency string) (Balance, error) {
resp := Balance{}
path := fmt.Sprintf("/%s/%s/%s/%s", itbitWallets, walletID, itbitBalances, currency)
err := i.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp)
if err != nil {
return resp, err
}
if resp.Description != "" {
return resp, errors.New(resp.Description)
}
return resp, nil
}
// GetOrders returns active orders for itBit
// perPage defaults to & has a limit of 50
func (i *ItBit) GetOrders(ctx context.Context, walletID, symbol, status string, page, perPage int64) ([]Order, error) {
var resp []Order
params := make(map[string]interface{})
params["walletID"] = walletID
if symbol != "" {
params["instrument"] = symbol
}
if status != "" {
params["status"] = status
}
if page > 0 {
params["page"] = strconv.FormatInt(page, 10)
}
if perPage > 0 {
params["perPage"] = strconv.FormatInt(perPage, 10)
}
return resp, i.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, itbitOrders, params, &resp)
}
// GetWalletTrades returns all trades for a specified wallet.
func (i *ItBit) GetWalletTrades(ctx context.Context, walletID string, params url.Values) (Records, error) {
resp := Records{}
urlPath := fmt.Sprintf("/%s/%s/%s", itbitWallets, walletID, itbitTrades)
path := common.EncodeURLValues(urlPath, params)
err := i.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp)
if err != nil {
return resp, err
}
if resp.Description != "" {
return resp, errors.New(resp.Description)
}
return resp, nil
}
// GetFundingHistoryForWallet returns all funding history for a specified wallet.
func (i *ItBit) GetFundingHistoryForWallet(ctx context.Context, walletID string, params url.Values) (FundingRecords, error) {
resp := FundingRecords{}
urlPath := fmt.Sprintf("/%s/%s/%s", itbitWallets, walletID, itbitFundingHistory)
path := common.EncodeURLValues(urlPath, params)
err := i.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp)
if err != nil {
return resp, err
}
if resp.Description != "" {
return resp, errors.New(resp.Description)
}
return resp, nil
}
// PlaceOrder places a new order
func (i *ItBit) PlaceOrder(ctx context.Context, walletID, side, orderType, currency string, amount, price float64, instrument, clientRef string) (Order, error) {
resp := Order{}
path := fmt.Sprintf("/%s/%s/%s", itbitWallets, walletID, itbitOrders)
params := make(map[string]interface{})
params["side"] = side
params["type"] = orderType
params["currency"] = currency
params["amount"] = strconv.FormatFloat(amount, 'f', -1, 64)
params["price"] = strconv.FormatFloat(price, 'f', -1, 64)
params["instrument"] = instrument
if clientRef != "" {
params["clientOrderIdentifier"] = clientRef
}
err := i.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, params, &resp)
if err != nil {
return resp, err
}
if resp.Description != "" {
return resp, errors.New(resp.Description)
}
return resp, nil
}
// GetOrder returns an order by id.
func (i *ItBit) GetOrder(ctx context.Context, walletID string, params url.Values) (Order, error) {
resp := Order{}
urlPath := fmt.Sprintf("/%s/%s/%s", itbitWallets, walletID, itbitOrders)
path := common.EncodeURLValues(urlPath, params)
err := i.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp)
if err != nil {
return resp, err
}
if resp.Description != "" {
return resp, errors.New(resp.Description)
}
return resp, nil
}
// CancelExistingOrder cancels and open order. *This is not a guarantee that the
// order has been cancelled!*
func (i *ItBit) CancelExistingOrder(ctx context.Context, walletID, orderID string) error {
path := fmt.Sprintf("/%s/%s/%s/%s", itbitWallets, walletID, itbitOrders, orderID)
return i.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, path, nil, nil)
}
// GetCryptoDepositAddress returns a deposit address to send cryptocurrency to.
func (i *ItBit) GetCryptoDepositAddress(ctx context.Context, walletID, currency string) (CryptoCurrencyDeposit, error) {
resp := CryptoCurrencyDeposit{}
path := fmt.Sprintf("/%s/%s/%s", itbitWallets, walletID, itbitCryptoDeposits)
params := make(map[string]interface{})
params["currency"] = currency
err := i.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, params, &resp)
if err != nil {
return resp, err
}
if resp.Description != "" {
return resp, errors.New(resp.Description)
}
return resp, nil
}
// WalletTransfer transfers funds between wallets.
func (i *ItBit) WalletTransfer(ctx context.Context, walletID, sourceWallet, destWallet string, amount float64, currency string) (WalletTransfer, error) {
resp := WalletTransfer{}
path := fmt.Sprintf("/%s/%s/%s", itbitWallets, walletID, itbitWalletTransfer)
params := make(map[string]interface{})
params["sourceWalletId"] = sourceWallet
params["destinationWalletId"] = destWallet
params["amount"] = strconv.FormatFloat(amount, 'f', -1, 64)
params["currencyCode"] = currency
err := i.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, params, &resp)
if err != nil {
return resp, err
}
if resp.Description != "" {
return resp, errors.New(resp.Description)
}
return resp, nil
}
// SendHTTPRequest sends an unauthenticated HTTP request
func (i *ItBit) SendHTTPRequest(ctx context.Context, ep exchange.URL, path string, result interface{}) error {
endpoint, err := i.API.Endpoints.GetURL(ep)
if err != nil {
return err
}
item := &request.Item{
Method: http.MethodGet,
Path: endpoint + path,
Result: result,
Verbose: i.Verbose,
HTTPDebugging: i.HTTPDebugging,
HTTPRecording: i.HTTPRecording,
}
return i.SendPayload(ctx, request.Unset, func() (*request.Item, error) {
return item, nil
}, request.UnauthenticatedRequest)
}
// SendAuthenticatedHTTPRequest sends an authenticated request to itBit
func (i *ItBit) SendAuthenticatedHTTPRequest(ctx context.Context, ep exchange.URL, method, path string, params map[string]interface{}, result interface{}) error {
creds, err := i.GetCredentials(ctx)
if err != nil {
return err
}
endpoint, err := i.API.Endpoints.GetURL(ep)
if err != nil {
return err
}
req := make(map[string]interface{})
urlPath := endpoint + path
for key, value := range params {
req[key] = value
}
PayloadJSON := []byte("")
if params != nil {
PayloadJSON, err = json.Marshal(req)
if err != nil {
return err
}
}
var intermediary json.RawMessage
err = i.SendPayload(ctx, request.Unset, func() (*request.Item, error) {
n := i.Requester.GetNonce(true).String()
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
var message []byte
message, err = json.Marshal([]string{method, urlPath, string(PayloadJSON), n, timestamp})
if err != nil {
return nil, err
}
var hash []byte
hash, err = crypto.GetSHA256([]byte(n + string(message)))
if err != nil {
return nil, err
}
var hmac []byte
hmac, err = crypto.GetHMAC(crypto.HashSHA512,
[]byte(urlPath+string(hash)),
[]byte(creds.Secret))
if err != nil {
return nil, err
}
signature := crypto.Base64Encode(hmac)
headers := make(map[string]string)
headers["Authorization"] = creds.ClientID + ":" + signature
headers["X-Auth-Timestamp"] = timestamp
headers["X-Auth-Nonce"] = n
headers["Content-Type"] = "application/json"
return &request.Item{
Method: method,
Path: urlPath,
Headers: headers,
Body: bytes.NewBuffer(PayloadJSON),
Result: &intermediary,
NonceEnabled: true,
Verbose: i.Verbose,
HTTPDebugging: i.HTTPDebugging,
HTTPRecording: i.HTTPRecording,
}, nil
}, request.AuthenticatedRequest)
if err != nil {
return err
}
errCheck := struct {
Code int `json:"code"`
Description string `json:"description"`
RequestID string `json:"requestId"`
}{}
err = json.Unmarshal(intermediary, &errCheck)
if err == nil {
if errCheck.Code != 0 || errCheck.Description != "" {
return fmt.Errorf("sendAuthRequest error code: %d description: %s",
errCheck.Code,
errCheck.Description)
}
}
return json.Unmarshal(intermediary, result)
}
// GetFee returns an estimate of fee based on type of transaction
func (i *ItBit) GetFee(feeBuilder *exchange.FeeBuilder) (float64, error) {
var fee float64
switch feeBuilder.FeeType {
case exchange.CryptocurrencyTradeFee:
fee = calculateTradingFee(feeBuilder.PurchasePrice, feeBuilder.Amount, feeBuilder.IsMaker)
case exchange.InternationalBankWithdrawalFee:
fee = getInternationalBankWithdrawalFee(feeBuilder.FiatCurrency, feeBuilder.BankTransactionType)
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.0035 * price * amount
}
func calculateTradingFee(purchasePrice, amount float64, isMaker bool) float64 {
// TODO: Itbit has volume discounts, but not API endpoint to get the exact volume numbers
// When support is added, this needs to be updated to calculate the accurate volume fee
feePercent := 0.0035
if isMaker {
feePercent = -0.0003
}
return feePercent * purchasePrice * amount
}
func getInternationalBankWithdrawalFee(c currency.Code, bankTransactionType exchange.InternationalBankTransactionType) float64 {
var fee float64
if (bankTransactionType == exchange.Swift ||
bankTransactionType == exchange.WireTransfer) &&
c.Equal(currency.USD) {
fee = 40
} else if (bankTransactionType == exchange.SEPA ||
bankTransactionType == exchange.WireTransfer) &&
c == currency.EUR {
fee = 1
}
return fee
}