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 }