github.com/bitfinexcom/bitfinex-api-go@v0.0.0-20210608095005-9e0b26f200fb/v2/rest/client.go (about)

     1  package rest
     2  
     3  import (
     4  	"crypto/hmac"
     5  	"crypto/sha512"
     6  	"encoding/hex"
     7  	"encoding/json"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"net/http"
    12  	"net/url"
    13  
    14  	"github.com/bitfinexcom/bitfinex-api-go/pkg/models/common"
    15  	"github.com/bitfinexcom/bitfinex-api-go/pkg/utils"
    16  )
    17  
    18  var productionBaseURL = "https://api-pub.bitfinex.com/v2/"
    19  
    20  type requestFactory interface {
    21  	NewAuthenticatedRequestWithData(permissionType common.PermissionType, refURL string, data map[string]interface{}) (Request, error)
    22  	NewAuthenticatedRequestWithBytes(permissionType common.PermissionType, refURL string, data []byte) (Request, error)
    23  	NewAuthenticatedRequest(permissionType common.PermissionType, refURL string) (Request, error)
    24  }
    25  
    26  type Synchronous interface {
    27  	Request(request Request) ([]interface{}, error)
    28  }
    29  
    30  type Client struct {
    31  	// base members for synchronous API
    32  	apiKey    string
    33  	apiSecret string
    34  	nonce     utils.NonceGenerator
    35  
    36  	// service providers
    37  	Candles        CandleService
    38  	Orders         OrderService
    39  	Positions      PositionService
    40  	Trades         TradeService
    41  	Tickers        TickerService
    42  	TickersHistory TickerHistoryService
    43  	Currencies     CurrenciesService
    44  	Platform       PlatformService
    45  	Book           BookService
    46  	Wallet         WalletService
    47  	Ledgers        LedgerService
    48  	Stats          StatsService
    49  	Status         StatusService
    50  	Derivatives    DerivativesService
    51  	Funding        FundingService
    52  	Pulse          PulseService
    53  	Invoice        InvoiceService
    54  	Market         MarketService
    55  
    56  	Synchronous
    57  }
    58  
    59  // Create a new Rest client
    60  func NewClient() *Client {
    61  	return NewClientWithURLNonce(productionBaseURL, utils.NewEpochNonceGenerator())
    62  }
    63  
    64  // Create a new Rest client with a custom nonce generator
    65  func NewClientWithURLNonce(url string, nonce utils.NonceGenerator) *Client {
    66  	httpDo := func(c *http.Client, req *http.Request) (*http.Response, error) {
    67  		return c.Do(req)
    68  	}
    69  	return NewClientWithURLHttpDoNonce(url, httpDo, nonce)
    70  }
    71  
    72  // Create a new Rest client with a custom http handler
    73  func NewClientWithHttpDo(httpDo func(c *http.Client, r *http.Request) (*http.Response, error)) *Client {
    74  	return NewClientWithURLHttpDo(productionBaseURL, httpDo)
    75  }
    76  
    77  // Create a new Rest client with a custom base url and HTTP handler
    78  func NewClientWithURLHttpDo(base string, httpDo func(c *http.Client, r *http.Request) (*http.Response, error)) *Client {
    79  	return NewClientWithURLHttpDoNonce(base, httpDo, utils.NewEpochNonceGenerator())
    80  }
    81  
    82  // Create a new Rest client with a custom base url, HTTP handler and none generator
    83  func NewClientWithURLHttpDoNonce(base string, httpDo func(c *http.Client, r *http.Request) (*http.Response, error), nonce utils.NonceGenerator) *Client {
    84  	url, _ := url.Parse(base)
    85  	sync := &HttpTransport{
    86  		BaseURL:    url,
    87  		httpDo:     httpDo,
    88  		HTTPClient: http.DefaultClient,
    89  	}
    90  	return NewClientWithSynchronousNonce(sync, nonce)
    91  }
    92  
    93  // Create a new Rest client with a custom base url
    94  func NewClientWithURL(url string) *Client {
    95  	httpDo := func(c *http.Client, req *http.Request) (*http.Response, error) {
    96  		return c.Do(req)
    97  	}
    98  	return NewClientWithURLHttpDo(url, httpDo)
    99  }
   100  
   101  // Create a new Rest client with a synchronous HTTP handler and a custom nonce generaotr
   102  func NewClientWithSynchronousNonce(sync Synchronous, nonce utils.NonceGenerator) *Client {
   103  	return NewClientWithSynchronousURLNonce(sync, productionBaseURL, nonce)
   104  }
   105  
   106  // Create a new Rest client with a synchronous HTTP handler and a custom base url and nonce generator
   107  func NewClientWithSynchronousURLNonce(sync Synchronous, url string, nonce utils.NonceGenerator) *Client {
   108  	c := &Client{
   109  		Synchronous: sync,
   110  		nonce:       nonce,
   111  	}
   112  	c.Orders = OrderService{Synchronous: c, requestFactory: c}
   113  	c.Book = BookService{Synchronous: c}
   114  	c.Candles = CandleService{Synchronous: c}
   115  	c.Trades = TradeService{Synchronous: c, requestFactory: c}
   116  	c.Tickers = TickerService{Synchronous: c, requestFactory: c}
   117  	c.TickersHistory = TickerHistoryService{Synchronous: c, requestFactory: c}
   118  	c.Currencies = CurrenciesService{Synchronous: c, requestFactory: c}
   119  	c.Platform = PlatformService{Synchronous: c}
   120  	c.Positions = PositionService{Synchronous: c, requestFactory: c}
   121  	c.Wallet = WalletService{Synchronous: c, requestFactory: c}
   122  	c.Ledgers = LedgerService{Synchronous: c, requestFactory: c}
   123  	c.Stats = StatsService{Synchronous: c, requestFactory: c}
   124  	c.Status = StatusService{Synchronous: c, requestFactory: c}
   125  	c.Derivatives = DerivativesService{Synchronous: c, requestFactory: c}
   126  	c.Funding = FundingService{Synchronous: c, requestFactory: c}
   127  	c.Pulse = PulseService{Synchronous: c, requestFactory: c}
   128  	c.Invoice = InvoiceService{Synchronous: c, requestFactory: c}
   129  	c.Market = MarketService{Synchronous: c, requestFactory: c}
   130  	return c
   131  }
   132  
   133  // Set the clients credentials in order to make authenticated requests
   134  func (c *Client) Credentials(key string, secret string) *Client {
   135  	c.apiKey = key
   136  	c.apiSecret = secret
   137  	return c
   138  }
   139  
   140  // Request is a wrapper for standard http.Request.  Default method is POST with no data.
   141  type Request struct {
   142  	RefURL  string     // ref url
   143  	Data    []byte     // body data
   144  	Method  string     // http method
   145  	Params  url.Values // query parameters
   146  	Headers map[string]string
   147  }
   148  
   149  // Response is a wrapper for standard http.Response and provides more methods.
   150  type Response struct {
   151  	Response *http.Response
   152  	Body     []byte
   153  }
   154  
   155  func (c *Client) sign(msg string) (string, error) {
   156  	sig := hmac.New(sha512.New384, []byte(c.apiSecret))
   157  	_, err := sig.Write([]byte(msg))
   158  	if err != nil {
   159  		return "", nil
   160  	}
   161  	return hex.EncodeToString(sig.Sum(nil)), nil
   162  }
   163  
   164  // Create a new authenticated GET request with the given permission type and endpoint url
   165  // For example permissionType = "r" and refUrl = "/orders" then the target endpoint will be
   166  // https://api.bitfinex.com/v2/auth/r/orders/:Symbol
   167  func (c *Client) NewAuthenticatedRequest(permissionType common.PermissionType, refURL string) (Request, error) {
   168  	return c.NewAuthenticatedRequestWithBytes(permissionType, refURL, []byte("{}"))
   169  }
   170  
   171  // Create a new authenticated POST request with the given permission type,endpoint url and data (bytes) as the body
   172  // For example permissionType = "r" and refUrl = "/orders" then the target endpoint will be
   173  // https://api.bitfinex.com/v2/auth/r/orders/:Symbol
   174  func (c *Client) NewAuthenticatedRequestWithBytes(permissionType common.PermissionType, refURL string, data []byte) (Request, error) {
   175  	authURL := fmt.Sprintf("auth/%s/%s", string(permissionType), refURL)
   176  	req := NewRequestWithBytes(authURL, data)
   177  	nonce := c.nonce.GetNonce()
   178  	msg := "/api/v2/" + authURL + nonce + string(data)
   179  	sig, err := c.sign(msg)
   180  	if err != nil {
   181  		return Request{}, err
   182  	}
   183  	req.Headers["Content-Type"] = "application/json"
   184  	req.Headers["Accept"] = "application/json"
   185  	req.Headers["bfx-nonce"] = nonce
   186  	req.Headers["bfx-signature"] = sig
   187  	req.Headers["bfx-apikey"] = c.apiKey
   188  	return req, nil
   189  }
   190  
   191  // Create a new authenticated POST request with the given permission type,endpoint url and data (map[string]interface{}) as the body
   192  // For example permissionType = "r" and refUrl = "/orders" then the target endpoint will be
   193  // https://api.bitfinex.com/v2/auth/r/orders/:Symbol
   194  func (c *Client) NewAuthenticatedRequestWithData(permissionType common.PermissionType, refURL string, data map[string]interface{}) (Request, error) {
   195  	b, err := json.Marshal(data)
   196  	if err != nil {
   197  		return Request{}, err
   198  	}
   199  	return c.NewAuthenticatedRequestWithBytes(permissionType, refURL, b)
   200  }
   201  
   202  // Create new POST request with an empty body as payload
   203  func NewRequest(refURL string) Request {
   204  	return NewRequestWithDataMethod(refURL, []byte("{}"), "POST")
   205  }
   206  
   207  // Create a new request with the given method (POST | GET)
   208  func NewRequestWithMethod(refURL string, method string) Request {
   209  	return NewRequestWithDataMethod(refURL, []byte("{}"), method)
   210  }
   211  
   212  // Create a new POST request with the given bytes as body
   213  func NewRequestWithBytes(refURL string, data []byte) Request {
   214  	return NewRequestWithDataMethod(refURL, data, "POST")
   215  }
   216  
   217  // Create a new POST request with the given data (map[string]interface{}) as body
   218  func NewRequestWithData(refURL string, data map[string]interface{}) (Request, error) {
   219  	b, err := json.Marshal(data)
   220  	if err != nil {
   221  		return Request{}, err
   222  	}
   223  	return NewRequestWithDataMethod(refURL, b, "POST"), nil
   224  }
   225  
   226  // Create a new request with a given method (POST | GET) with bytes as body
   227  func NewRequestWithDataMethod(refURL string, data []byte, method string) Request {
   228  	return Request{
   229  		RefURL:  refURL,
   230  		Data:    data,
   231  		Method:  method,
   232  		Headers: make(map[string]string),
   233  	}
   234  }
   235  
   236  // newResponse creates new wrapper.
   237  func newResponse(r *http.Response) *Response {
   238  	// Use a LimitReader of arbitrary size (here ~8.39MB) to prevent us from
   239  	// reading overly large response bodies.
   240  	lr := io.LimitReader(r.Body, 8388608)
   241  	body, err := ioutil.ReadAll(lr)
   242  	if err != nil {
   243  		body = []byte(`Error reading body:` + err.Error())
   244  	}
   245  
   246  	return &Response{r, body}
   247  }
   248  
   249  // String converts response body to string.
   250  // An empty string will be returned if error.
   251  func (r *Response) String() string {
   252  	return string(r.Body)
   253  }
   254  
   255  // checkResponse checks response status code and response
   256  // for errors.
   257  func checkResponse(r *Response) error {
   258  	if c := r.Response.StatusCode; c >= 200 && c <= 299 {
   259  		return nil
   260  	}
   261  
   262  	var raw []interface{}
   263  	// Try to decode error message
   264  	errorResponse := &ErrorResponse{Response: r}
   265  	err := json.Unmarshal(r.Body, &raw)
   266  	if err != nil {
   267  		errorResponse.Message = "Error decoding response error message. " +
   268  			"Please see response body for more information."
   269  		return errorResponse
   270  	}
   271  
   272  	if len(raw) < 3 {
   273  		errorResponse.Message = fmt.Sprintf("Expected response to have three elements but got %#v", raw)
   274  		return errorResponse
   275  	}
   276  
   277  	if str, ok := raw[0].(string); !ok || str != "error" {
   278  		errorResponse.Message = fmt.Sprintf("Expected first element to be \"error\" but got %#v", raw)
   279  		return errorResponse
   280  	}
   281  
   282  	code, ok := raw[1].(float64)
   283  	if !ok {
   284  		errorResponse.Message = fmt.Sprintf("Expected second element to be error code but got %#v", raw)
   285  		return errorResponse
   286  	}
   287  	errorResponse.Code = int(code)
   288  
   289  	msg, ok := raw[2].(string)
   290  	if !ok {
   291  		errorResponse.Message = fmt.Sprintf("Expected third element to be error message but got %#v", raw)
   292  		return errorResponse
   293  	}
   294  	errorResponse.Message = msg
   295  
   296  	return errorResponse
   297  }
   298  
   299  // In case if API will wrong response code
   300  // ErrorResponse will be returned to caller
   301  type ErrorResponse struct {
   302  	Response *Response
   303  	Message  string `json:"message"`
   304  	Code     int    `json:"code"`
   305  }
   306  
   307  func (r *ErrorResponse) Error() string {
   308  	return fmt.Sprintf("%v %v: %d %v (%d)",
   309  		r.Response.Response.Request.Method,
   310  		r.Response.Response.Request.URL,
   311  		r.Response.Response.StatusCode,
   312  		r.Message,
   313  		r.Code,
   314  	)
   315  }