Skip to content

Commit d926899

Browse files
committed
adds the first couple of services for v2 REST endpoints
this also includes a breaking change because the REST endpoints use nonces that are max 53bit, whereas before we used a 64bit number, so if you've used this lib before you might have to generate a new API key in order to get rid of the `nonce too small` error message
1 parent 3d070aa commit d926899

File tree

11 files changed

+332
-27
lines changed

11 files changed

+332
-27
lines changed

examples/v2/rest-orders/main.go

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"os"
6+
7+
"github.com/bitfinexcom/bitfinex-api-go/v2"
8+
)
9+
10+
// Set BFX_APIKEY and BFX_SECRET as :
11+
//
12+
// export BFX_API_KEY=YOUR_API_KEY
13+
// export BFX_API_SECRET=YOUR_API_SECRET
14+
//
15+
// you can obtain it from https://www.bitfinex.com/api
16+
17+
func main() {
18+
key := os.Getenv("BFX_API_KEY")
19+
secret := os.Getenv("BFX_API_SECRET")
20+
c := bitfinex.NewClient().Credentials(key, secret)
21+
22+
available, err := c.Platform.Status()
23+
if err != nil {
24+
log.Fatalf("getting status: %s", err)
25+
}
26+
27+
if !available {
28+
log.Fatalf("API not available")
29+
}
30+
31+
os, err := c.Orders.History(bitfinex.TradingPrefix + bitfinex.IOTBTC)
32+
if err != nil {
33+
log.Fatalf("getting orders: %s", err)
34+
}
35+
36+
log.Printf("orders: %#v\n", os)
37+
}

examples/v2/ws-private/main.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ func main() {
2626
log.Fatalf("connecting authenticated websocket: %s", err)
2727
}
2828

29-
c.Websocket.AttachEventHandler(func(_ context.Context, ev interface{}) {
29+
c.Websocket.AttachEventHandler(func(ev interface{}) {
3030
log.Printf("EVENT: %#v", ev)
3131
})
3232

33-
c.Websocket.AttachPrivateHandler(func(_ context.Context, msg interface{}) {
33+
c.Websocket.AttachPrivateHandler(func(msg interface{}) {
3434
log.Printf("PRIV MSG: %#v", msg)
3535
})
3636

utils/nonce.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
var nonce uint64
1010

1111
func init() {
12-
nonce = uint64(time.Now().UnixNano())
12+
nonce = uint64(time.Now().Unix()) * 1000
1313
}
1414

1515
// GetNonce is a naive nonce producer that takes the current Unix nano epoch

v2/client.go

+58-23
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
package bitfinex
33

44
import (
5+
"bytes"
56
"crypto/hmac"
67
"crypto/sha512"
7-
"encoding/base64"
88
"encoding/hex"
99
"encoding/json"
1010
"fmt"
@@ -18,7 +18,7 @@ import (
1818

1919
const (
2020
// BaseURL is the v2 REST endpoint.
21-
BaseURL = "https://api.bitfinex.com/v2"
21+
BaseURL = "https://api.bitfinex.com/v2/"
2222

2323
// WebSocketURL is the v2 Websocket endpoint.
2424
WebSocketURL = "wss://api.bitfinex.com/ws/2"
@@ -38,6 +38,10 @@ type Client struct {
3838

3939
// Services
4040
Websocket *bfxWebsocket
41+
Orders *OrderService
42+
Platform *PlatformService
43+
Positions *PositionService
44+
Trades *TradeService
4145
}
4246

4347
func NewClient() *Client {
@@ -50,12 +54,16 @@ func NewClientWithHTTP(h *http.Client) *Client {
5054
c := &Client{BaseURL: baseURL, HTTPClient: h}
5155

5256
c.Websocket = newBfxWebsocket(c, WebSocketURL)
57+
c.Orders = &OrderService{client: c}
58+
c.Platform = &PlatformService{client: c}
59+
c.Positions = &PositionService{client: c}
60+
c.Trades = &TradeService{client: c}
5361

5462
return c
5563
}
5664

5765
// NewRequest create new API request. Relative url can be provided in refURL.
58-
func (c *Client) newRequest(method string, refURL string, params url.Values) (*http.Request, error) {
66+
func (c *Client) newRequest(method string, refURL string, params url.Values, body io.Reader) (*http.Request, error) {
5967
rel, err := url.Parse(refURL)
6068
if err != nil {
6169
return nil, err
@@ -65,7 +73,7 @@ func (c *Client) newRequest(method string, refURL string, params url.Values) (*h
6573
}
6674

6775
u := c.BaseURL.ResolveReference(rel)
68-
req, err := http.NewRequest(method, u.String(), nil)
76+
req, err := http.NewRequest(method, u.String(), body)
6977

7078
if err != nil {
7179
return nil, err
@@ -76,39 +84,39 @@ func (c *Client) newRequest(method string, refURL string, params url.Values) (*h
7684

7785
// NewAuthenticatedRequest creates new http request for authenticated routes
7886
func (c *Client) newAuthenticatedRequest(m string, refURL string, data map[string]interface{}) (*http.Request, error) {
79-
req, err := c.newRequest(m, refURL, nil)
80-
if err != nil {
81-
return nil, err
82-
}
87+
refURL = "auth/r/" + refURL
8388

84-
payload := map[string]interface{}{
85-
"request": "/v2/" + refURL,
86-
"nonce": utils.GetNonce(),
89+
if data == nil {
90+
data = map[string]interface{}{}
8791
}
8892

89-
for k, v := range data {
90-
payload[k] = v
93+
b, err := json.Marshal(data)
94+
if err != nil {
95+
return nil, err
9196
}
9297

93-
payloadJSON, err := json.Marshal(payload)
98+
rd := bytes.NewReader(b)
99+
req, err := c.newRequest(m, refURL, nil, rd)
94100
if err != nil {
95101
return nil, err
96102
}
97103

98-
payloadEnc := base64.StdEncoding.EncodeToString(payloadJSON)
104+
nonce := utils.GetNonce()
105+
message := "/api/v2/" + refURL + nonce + string(b)
106+
sig := c.sign(message)
99107

100108
req.Header.Add("Content-Type", "application/json")
101109
req.Header.Add("Accept", "application/json")
102-
req.Header.Add("X-BFX-APIKEY", c.APIKey)
103-
req.Header.Add("X-BFX-PAYLOAD", payloadEnc)
104-
req.Header.Add("X-BFX-SIGNATURE", c.signPayload(payloadEnc))
110+
req.Header.Add("bfx-nonce", nonce)
111+
req.Header.Add("bfx-signature", sig)
112+
req.Header.Add("bfx-apikey", c.APIKey)
105113

106114
return req, nil
107115
}
108116

109-
func (c *Client) signPayload(payload string) string {
117+
func (c *Client) sign(msg string) string {
110118
sig := hmac.New(sha512.New384, []byte(c.APISecret))
111-
sig.Write([]byte(payload))
119+
sig.Write([]byte(msg))
112120
return hex.EncodeToString(sig.Sum(nil))
113121
}
114122

@@ -158,14 +166,40 @@ func checkResponse(r *Response) error {
158166
return nil
159167
}
160168

169+
var raw []interface{}
161170
// Try to decode error message
162171
errorResponse := &ErrorResponse{Response: r}
163-
err := json.Unmarshal(r.Body, errorResponse)
172+
err := json.Unmarshal(r.Body, &raw)
164173
if err != nil {
165174
errorResponse.Message = "Error decoding response error message. " +
166175
"Please see response body for more information."
176+
return errorResponse
177+
}
178+
179+
if len(raw) < 3 {
180+
errorResponse.Message = fmt.Sprintf("Expected response to have three elements but got %#v", raw)
181+
return errorResponse
167182
}
168183

184+
if str, ok := raw[0].(string); !ok || str != "error" {
185+
errorResponse.Message = fmt.Sprintf("Expected first element to be \"error\" but got %#v", raw)
186+
return errorResponse
187+
}
188+
189+
code, ok := raw[1].(float64)
190+
if !ok {
191+
errorResponse.Message = fmt.Sprintf("Expected second element to be error code but got %#v", raw)
192+
return errorResponse
193+
}
194+
errorResponse.Code = int(code)
195+
196+
msg, ok := raw[2].(string)
197+
if !ok {
198+
errorResponse.Message = fmt.Sprintf("Expected third element to be error message but got %#v", raw)
199+
return errorResponse
200+
}
201+
errorResponse.Message = msg
202+
169203
return errorResponse
170204
}
171205

@@ -174,14 +208,16 @@ func checkResponse(r *Response) error {
174208
type ErrorResponse struct {
175209
Response *Response
176210
Message string `json:"message"`
211+
Code int `json:"code"`
177212
}
178213

179214
func (r *ErrorResponse) Error() string {
180-
return fmt.Sprintf("%v %v: %d %v",
215+
return fmt.Sprintf("%v %v: %d %v (%d)",
181216
r.Response.Response.Request.Method,
182217
r.Response.Response.Request.URL,
183218
r.Response.Response.StatusCode,
184219
r.Message,
220+
r.Code,
185221
)
186222
}
187223

@@ -194,7 +230,6 @@ func (c *Client) do(req *http.Request, v interface{}) (*Response, error) {
194230
defer resp.Body.Close()
195231

196232
response := newResponse(resp)
197-
198233
err = checkResponse(response)
199234
if err != nil {
200235
return response, err

v2/errors.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package bitfinex
2+
3+
import (
4+
"errors"
5+
)
6+
7+
var (
8+
ErrNotFound = errors.New("not found")
9+
)

v2/orders.go

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package bitfinex
2+
3+
import (
4+
"fmt"
5+
"path"
6+
)
7+
8+
// OrderService manages the Order endpoint.
9+
type OrderService struct {
10+
client *Client
11+
}
12+
13+
// All returns all orders for the authenticated account.
14+
func (s *OrderService) All(symbol string) (OrderSnapshot, error) {
15+
req, err := s.client.newAuthenticatedRequest("POST", path.Join("orders", symbol), nil)
16+
if err != nil {
17+
return nil, err
18+
}
19+
20+
var raw []interface{}
21+
_, err = s.client.do(req, &raw)
22+
if err != nil {
23+
return nil, err
24+
}
25+
26+
os, err := orderSnapshotFromRaw(raw)
27+
if err != nil {
28+
return nil, err
29+
}
30+
31+
return os, nil
32+
}
33+
34+
// Status retrieves the given order from the API. This is just a wrapper around
35+
// the All() method, since the API does not provide lookup for a single Order.
36+
func (s *OrderService) Status(orderID int64) (o Order, err error) {
37+
os, err := s.All("")
38+
if err != nil {
39+
return o, err
40+
}
41+
if len(os) == 0 {
42+
return o, ErrNotFound
43+
}
44+
45+
for _, e := range os {
46+
if e.ID == orderID {
47+
return e, nil
48+
}
49+
}
50+
51+
return o, ErrNotFound
52+
}
53+
54+
// All returns all orders for the authenticated account.
55+
func (s *OrderService) History(symbol string) (OrderSnapshot, error) {
56+
if symbol == "" {
57+
return nil, fmt.Errorf("symbol cannot be empty")
58+
}
59+
60+
req, err := s.client.newAuthenticatedRequest("POST", path.Join("orders", symbol, "hist"), nil)
61+
if err != nil {
62+
return nil, err
63+
}
64+
65+
var raw []interface{}
66+
_, err = s.client.do(req, &raw)
67+
if err != nil {
68+
return nil, err
69+
}
70+
fmt.Printf("raw: %#v\n", raw)
71+
72+
os, err := orderSnapshotFromRaw(raw)
73+
if err != nil {
74+
return nil, err
75+
}
76+
77+
return os, nil
78+
}

v2/orders_test.go

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package bitfinex
2+
3+
import (
4+
"bytes"
5+
"io/ioutil"
6+
"net/http"
7+
"testing"
8+
)
9+
10+
func TestOrdersAll(t *testing.T) {
11+
httpDo = func(_ *http.Client, req *http.Request) (*http.Response, error) {
12+
msg := `
13+
[
14+
[4419360502,null,83283216761,"tIOTBTC",1508281683000,1508281731000,63938,63938,"EXCHANGE LIMIT",null,null,null,null,"CANCELED",null,null,0.0000843,0,0,0,null,null,null,0,0,null],
15+
[4419354239,null,83265164211,"tIOTBTC",1508281665000,1508281674000,63976,63976,"EXCHANGE LIMIT",null,null,null,null,"CANCELED",null,null,0.00008425,0,0,0,null,null,null,0,0,null],
16+
[4419339620,null,83217673277,"tIOTBTC",1508281618000,1508281653000,64014,64014,"EXCHANGE LIMIT",null,null,null,null,"CANCELED",null,null,0.0000842,0,0,0,null,null,null,0,0,null]
17+
]`
18+
resp := http.Response{
19+
Body: ioutil.NopCloser(bytes.NewBufferString(msg)),
20+
StatusCode: 200,
21+
}
22+
return &resp, nil
23+
}
24+
25+
orders, err := NewClient().Orders.All("")
26+
27+
if err != nil {
28+
t.Error(err)
29+
}
30+
31+
if len(orders) != 3 {
32+
t.Fatalf("expected three orders but got %d", len(orders))
33+
}
34+
}
35+
36+
func TestOrdersHistory(t *testing.T) {
37+
httpDo = func(_ *http.Client, req *http.Request) (*http.Response, error) {
38+
msg := `
39+
[
40+
[4419360502,null,83283216761,"tIOTBTC",1508281683000,1508281731000,63938,63938,"EXCHANGE LIMIT",null,null,null,null,"CANCELED",null,null,0.0000843,0,0,0,null,null,null,0,0,null],
41+
[4419354239,null,83265164211,"tIOTBTC",1508281665000,1508281674000,63976,63976,"EXCHANGE LIMIT",null,null,null,null,"CANCELED",null,null,0.00008425,0,0,0,null,null,null,0,0,null],
42+
[4419339620,null,83217673277,"tIOTBTC",1508281618000,1508281653000,64014,64014,"EXCHANGE LIMIT",null,null,null,null,"CANCELED",null,null,0.0000842,0,0,0,null,null,null,0,0,null]
43+
]`
44+
resp := http.Response{
45+
Body: ioutil.NopCloser(bytes.NewBufferString(msg)),
46+
StatusCode: 200,
47+
}
48+
return &resp, nil
49+
}
50+
51+
orders, err := NewClient().Orders.History(TradingPrefix + IOTBTC)
52+
53+
if err != nil {
54+
t.Error(err)
55+
}
56+
57+
if len(orders) != 3 {
58+
t.Errorf("expected three orders but got %d", len(orders))
59+
}
60+
61+
_, err = NewClient().Orders.History("")
62+
if err == nil {
63+
t.Errorf("expected error when supplying empty symbol but got none")
64+
}
65+
}

0 commit comments

Comments
 (0)