From 69dbde7e74cbab26abdc413d851debe251cc0aed Mon Sep 17 00:00:00 2001 From: wt5RM2 Date: Thu, 19 Jul 2018 16:41:46 +0300 Subject: [PATCH 1/3] Modify nonce for value from documentation --- .gitignore | 7 +++++++ utils/nonce.go | 7 ++++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..2e782d933 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln \ No newline at end of file diff --git a/utils/nonce.go b/utils/nonce.go index 13af9bde8..b3792cb70 100644 --- a/utils/nonce.go +++ b/utils/nonce.go @@ -1,6 +1,7 @@ package utils import ( + "fmt" "strconv" "sync/atomic" "time" @@ -33,10 +34,10 @@ func NewEpochNonceGenerator() *EpochNonceGenerator { // v1 support -var nonce uint64 +var nonce string func init() { - nonce = uint64(time.Now().Unix()) * 1000 + nonce = fmt.Sprintf("%v", time.Now().Unix()*10000) } // GetNonce is a naive nonce producer that takes the current Unix nano epoch @@ -45,5 +46,5 @@ func init() { // key and as such needs to be synchronised with other instances using the same // key in order to avoid race conditions. func GetNonce() string { - return strconv.FormatUint(atomic.AddUint64(&nonce, 1), 10) + return nonce } From 40d85aadedbd7517ef00bc16526dcb90ec89c49c Mon Sep 17 00:00:00 2001 From: wt5RM2 Date: Thu, 19 Jul 2018 18:08:13 +0300 Subject: [PATCH 2/3] Refactore code to rewiev 19.07.2018 --- .gitignore | 2 +- tests/integration/v2/api_test.go | 49 - tests/integration/v2/assert.go | 150 -- tests/integration/v2/bitfinex_test.go | 21 - tests/integration/v2/incrementing_nonce.go | 16 - tests/integration/v2/listener.go | 366 ----- .../v2/live_websocket_private_test.go | 95 -- .../v2/live_websocket_public_test.go | 315 ---- tests/integration/v2/mock_async.go | 102 -- tests/integration/v2/mock_ws_private_test.go | 441 ----- tests/integration/v2/mock_ws_public_test.go | 84 - tests/integration/v2/reconnect_test.go | 431 ----- tests/integration/v2/test_ws_service.go | 218 --- utils/nonce.go | 9 +- v2/convert.go | 82 - v2/pairs.go | 32 - v2/rest/book.go | 43 - v2/rest/book_test.go | 30 - v2/rest/client.go | 250 --- v2/rest/orders.go | 93 -- v2/rest/orders_test.go | 67 - v2/rest/platform_status.go | 22 - v2/rest/positions.go | 31 - v2/rest/trades.go | 38 - v2/rest/transport.go | 82 - v2/types.go | 1423 ----------------- v2/websocket/api.go | 98 -- v2/websocket/channels.go | 430 ----- v2/websocket/client.go | 498 ------ v2/websocket/events.go | 175 -- v2/websocket/factories.go | 130 -- v2/websocket/parameters.go | 35 - v2/websocket/subscriptions.go | 283 ---- v2/websocket/transport.go | 165 -- 34 files changed, 3 insertions(+), 6303 deletions(-) delete mode 100644 tests/integration/v2/api_test.go delete mode 100644 tests/integration/v2/assert.go delete mode 100644 tests/integration/v2/bitfinex_test.go delete mode 100644 tests/integration/v2/incrementing_nonce.go delete mode 100644 tests/integration/v2/listener.go delete mode 100644 tests/integration/v2/live_websocket_private_test.go delete mode 100644 tests/integration/v2/live_websocket_public_test.go delete mode 100644 tests/integration/v2/mock_async.go delete mode 100644 tests/integration/v2/mock_ws_private_test.go delete mode 100644 tests/integration/v2/mock_ws_public_test.go delete mode 100644 tests/integration/v2/reconnect_test.go delete mode 100644 tests/integration/v2/test_ws_service.go delete mode 100644 v2/convert.go delete mode 100644 v2/pairs.go delete mode 100644 v2/rest/book.go delete mode 100644 v2/rest/book_test.go delete mode 100644 v2/rest/client.go delete mode 100644 v2/rest/orders.go delete mode 100644 v2/rest/orders_test.go delete mode 100644 v2/rest/platform_status.go delete mode 100644 v2/rest/positions.go delete mode 100644 v2/rest/trades.go delete mode 100644 v2/rest/transport.go delete mode 100644 v2/types.go delete mode 100644 v2/websocket/api.go delete mode 100644 v2/websocket/channels.go delete mode 100644 v2/websocket/client.go delete mode 100644 v2/websocket/events.go delete mode 100644 v2/websocket/factories.go delete mode 100644 v2/websocket/parameters.go delete mode 100644 v2/websocket/subscriptions.go delete mode 100644 v2/websocket/transport.go diff --git a/.gitignore b/.gitignore index 2e782d933..64adc1ee5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ *.suo *.ntvs* *.njsproj -*.sln \ No newline at end of file +*.sln diff --git a/tests/integration/v2/api_test.go b/tests/integration/v2/api_test.go deleted file mode 100644 index ad164678e..000000000 --- a/tests/integration/v2/api_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package tests - -/* -import ( - "context" - "github.com/bitfinexcom/bitfinex-api-go/v2/websocket" - "testing" - "time" -) - -// the following test is used to run the API against - -func TestAPI(t *testing.T) { - // create transport & nonce mocks - - // create client - params := websocket.NewDefaultParameters() - params.URL = "wss://dev-prdn.bitfinex.com:2997/ws/2" - ws := websocket.NewWithParams(params) //.Credentials("U83q9jkML2GVj1fVxFJOAXQeDGaXIzeZ6PwNPQLEXt4", "77SWIRggvw0rCOJUgk9GVcxbldjTxOJP5WLCjWBFIVc") - - // setup listener - listener := newListener() - listener.run(ws.Listen()) - - // set ws options - //ws.SetReadTimeout(time.Second * 2) - ws.Connect() - defer ws.Close() - - // begin test - //ev, err := listener.nextAuthEvent() - //if err != nil { - // t.Fatal(err) - //} - //assert(t, &websocket.AuthEvent{Event: "auth", Status: "OK"}, ev) - - - tradeSubID, err := ws.SubscribeTrades(context.Background(), "tBTCUSD") - if err != nil { - t.Fatal(err) - } - t.Logf("trade sub ID: %s", tradeSubID) - - //bookSubID, err := ws.SubscribeBook(context.Background(), "tBTCUSD", websocket.Precision0, websocket.FrequencyRealtime) - //t.Logf("book sub ID: %s", bookSubID) - - time.Sleep(time.Second * 15) -} -*/ diff --git a/tests/integration/v2/assert.go b/tests/integration/v2/assert.go deleted file mode 100644 index 4bf6dcf0a..000000000 --- a/tests/integration/v2/assert.go +++ /dev/null @@ -1,150 +0,0 @@ -package tests - -import ( - "bytes" - "reflect" - "testing" -) - -func isZeroOfUnderlyingType(x interface{}) bool { - if x == nil { - return true - } - if _, ok := x.([]string); ok { - return true - } - return x == reflect.Zero(reflect.TypeOf(x)).Interface() -} - -func objEq(expected, actual interface{}) bool { - - if expected == nil || actual == nil { - return expected == actual - } - if exp, ok := expected.([]byte); ok { - act, ok := actual.([]byte) - if !ok { - return false - } else if exp == nil || act == nil { - return exp == nil && act == nil - } - return bytes.Equal(exp, act) - } - return reflect.DeepEqual(expected, actual) - -} - -func assertSlice(t *testing.T, expected, actual interface{}) { - if objEq(expected, actual) { - t.Logf("%s OK", reflect.TypeOf(actual)) - return - } - - actualType := reflect.TypeOf(actual) - if actualType == nil { - t.Fatal() - } - expectedValue := reflect.ValueOf(expected) - if expectedValue.IsValid() && expectedValue.Type().ConvertibleTo(actualType) { - // Attempt comparison after type conversion - if reflect.DeepEqual(expectedValue.Convert(actualType).Interface(), actual) { - t.Logf("%s OK", reflect.TypeOf(actual)) - return - } - } - - t.Fatalf("FAIL %s: expected %#v, got %#v", reflect.TypeOf(expected), expected, actual) -} - -func isPrimitive(exp interface{}) bool { - t := reflect.TypeOf(exp) - switch t.Kind() { - case reflect.Interface: - return false - case reflect.Struct: - return false - case reflect.Array: - return false - case reflect.Func: - return false - case reflect.Map: - return false - case reflect.Ptr: - return false - case reflect.Slice: - return false - case reflect.UnsafePointer: - return false - default: - return true - } -} - -// does not work on slices -func assert(t *testing.T, expected interface{}, actual interface{}) { - - prexp := reflect.ValueOf(expected) - pract := reflect.ValueOf(actual) - - if isPrimitive(actual) { - if expected != actual { - t.Fatalf("expected %#v, got %#v", expected, actual) - } - t.Logf("OK %s", reflect.TypeOf(expected).Name()) - return - } - - if pract.IsNil() { - t.Errorf("nil actual value: %#v", actual) - t.Fail() - return - } - - exp := prexp.Elem() - act := pract.Elem() - - if !exp.IsValid() { - t.Errorf("reflected expectation not valid (%#v)", expected) - t.Fail() - } - - if exp.Type() != act.Type() { - t.Errorf("expected type %s, got %s", exp.Type(), act.Type()) - t.Fail() - } - - for i := 0; i < exp.NumField(); i++ { - expValueField := exp.Field(i) - expTypeField := exp.Type().Field(i) - - actValueField := act.Field(i) - actTypeField := act.Type().Field(i) - - if expTypeField.Name != actTypeField.Name { - t.Errorf("expected type %s, got %s", expTypeField.Name, actTypeField.Name) - t.Errorf("%#v", actual) - t.Fail() - } - if isZeroOfUnderlyingType(expValueField.Interface()) { - continue - } - if !isZeroOfUnderlyingType(expValueField.Interface()) && isZeroOfUnderlyingType(actValueField.Interface()) { - t.Errorf("expected %s, but was empty", expTypeField.Name) - t.Errorf("%#v", actual) - t.Fail() - return - } - assert(t, expValueField.Interface(), actValueField.Interface()) - /* - if expValueField.Interface() != actValueField.Interface() { - t.Errorf("expected %s %#v, got %#v", expTypeField.Name, expValueField.Interface(), actValueField.Interface()) - t.Fail() - } - */ - } - if t.Failed() { - t.Logf("FAIL %s", exp.Type().Name()) - return - } - t.Logf("OK %s", exp.Type().Name()) -} diff --git a/tests/integration/v2/bitfinex_test.go b/tests/integration/v2/bitfinex_test.go deleted file mode 100644 index 0a0d4dd44..000000000 --- a/tests/integration/v2/bitfinex_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package tests - -import ( - "log" - "os" -) - -var ( - auth = false - key = os.Getenv("BFX_API_KEY") - secret = os.Getenv("BFX_API_SECRET") -) - -func init() { - - if key != "" && secret != "" { - auth = true - } else { - log.Println("No authentication credentials provided so running only public tests.") - } -} diff --git a/tests/integration/v2/incrementing_nonce.go b/tests/integration/v2/incrementing_nonce.go deleted file mode 100644 index 044c14d6f..000000000 --- a/tests/integration/v2/incrementing_nonce.go +++ /dev/null @@ -1,16 +0,0 @@ -package tests - -import ( - "fmt" -) - -// IncrementingNonceGenerator starts at nonce1 and increments each by +1: nonce1, nonce2, ..., nonceN -type IncrementingNonceGenerator struct { - nonce int -} - -// GetNonce returns an incrementing nonce value. -func (m *IncrementingNonceGenerator) GetNonce() string { - m.nonce++ - return fmt.Sprintf("nonce%d", m.nonce) -} diff --git a/tests/integration/v2/listener.go b/tests/integration/v2/listener.go deleted file mode 100644 index 7b2c7963b..000000000 --- a/tests/integration/v2/listener.go +++ /dev/null @@ -1,366 +0,0 @@ -package tests - -import ( - "errors" - "log" - "time" - - bitfinex "github.com/bitfinexcom/bitfinex-api-go/v2" - "github.com/bitfinexcom/bitfinex-api-go/v2/websocket" -) - -type listener struct { - infoEvents chan *websocket.InfoEvent - authEvents chan *websocket.AuthEvent - ticks chan *bitfinex.Ticker - subscriptionEvents chan *websocket.SubscribeEvent - unsubscriptionEvents chan *websocket.UnsubscribeEvent - walletUpdates chan *bitfinex.WalletUpdate - balanceUpdates chan *bitfinex.BalanceUpdate - walletSnapshot chan *bitfinex.WalletSnapshot - positionSnapshot chan *bitfinex.PositionSnapshot - notifications chan *bitfinex.Notification - positions chan *bitfinex.PositionUpdate - tradeUpdates chan *bitfinex.TradeExecutionUpdate - tradeExecutions chan *bitfinex.TradeExecution - cancels chan *bitfinex.OrderCancel - marginBase chan *bitfinex.MarginInfoBase - marginUpdate chan *bitfinex.MarginInfoUpdate - funding chan *bitfinex.FundingInfo - orderNew chan *bitfinex.OrderNew - errors chan error -} - -func newListener() *listener { - return &listener{ - infoEvents: make(chan *websocket.InfoEvent, 10), - authEvents: make(chan *websocket.AuthEvent, 10), - ticks: make(chan *bitfinex.Ticker, 10), - subscriptionEvents: make(chan *websocket.SubscribeEvent, 10), - unsubscriptionEvents: make(chan *websocket.UnsubscribeEvent, 10), - walletUpdates: make(chan *bitfinex.WalletUpdate, 10), - balanceUpdates: make(chan *bitfinex.BalanceUpdate, 10), - walletSnapshot: make(chan *bitfinex.WalletSnapshot, 10), - positionSnapshot: make(chan *bitfinex.PositionSnapshot, 10), - errors: make(chan error, 10), - notifications: make(chan *bitfinex.Notification, 10), - positions: make(chan *bitfinex.PositionUpdate, 10), - tradeUpdates: make(chan *bitfinex.TradeExecutionUpdate, 10), - tradeExecutions: make(chan *bitfinex.TradeExecution, 10), - cancels: make(chan *bitfinex.OrderCancel, 10), - marginBase: make(chan *bitfinex.MarginInfoBase, 10), - marginUpdate: make(chan *bitfinex.MarginInfoUpdate, 10), - orderNew: make(chan *bitfinex.OrderNew, 10), - funding: make(chan *bitfinex.FundingInfo, 10), - } -} - -func (l *listener) nextInfoEvent() (*websocket.InfoEvent, error) { - timeout := make(chan bool) - go func() { - time.Sleep(time.Second * 2) - close(timeout) - }() - select { - case ev := <-l.infoEvents: - return ev, nil - case <-timeout: - return nil, errors.New("timed out waiting for InfoEvent") - } -} - -func (l *listener) nextAuthEvent() (*websocket.AuthEvent, error) { - timeout := make(chan bool) - go func() { - time.Sleep(time.Second * 2) - close(timeout) - }() - select { - case ev := <-l.authEvents: - return ev, nil - case <-timeout: - return nil, errors.New("timed out waiting for AuthEvent") - } -} - -func (l *listener) nextWalletUpdate() (*bitfinex.WalletUpdate, error) { - timeout := make(chan bool) - go func() { - time.Sleep(time.Second * 2) - close(timeout) - }() - select { - case ev := <-l.walletUpdates: - return ev, nil - case <-timeout: - return nil, errors.New("timed out waiting for WalletUpdate") - } -} - -func (l *listener) nextBalanceUpdate() (*bitfinex.BalanceUpdate, error) { - timeout := make(chan bool) - go func() { - time.Sleep(time.Second * 2) - close(timeout) - }() - select { - case ev := <-l.balanceUpdates: - return ev, nil - case <-timeout: - return nil, errors.New("timed out waiting for BalanceUpdate") - } -} - -func (l *listener) nextWalletSnapshot() (*bitfinex.WalletSnapshot, error) { - timeout := make(chan bool) - go func() { - time.Sleep(time.Second * 2) - close(timeout) - }() - select { - case ev := <-l.walletSnapshot: - return ev, nil - case <-timeout: - return nil, errors.New("timed out waiting for WalletSnapshot") - } -} - -func (l *listener) nextPositionSnapshot() (*bitfinex.PositionSnapshot, error) { - timeout := make(chan bool) - go func() { - time.Sleep(time.Second * 2) - close(timeout) - }() - select { - case ev := <-l.positionSnapshot: - return ev, nil - case <-timeout: - return nil, errors.New("timed out waiting for PositionSnapshot") - } -} - -func (l *listener) nextSubscriptionEvent() (*websocket.SubscribeEvent, error) { - timeout := make(chan bool) - go func() { - time.Sleep(time.Second * 2) - close(timeout) - }() - select { - case ev := <-l.subscriptionEvents: - return ev, nil - case <-timeout: - return nil, errors.New("timed out waiting for SubscribeEvent") - } -} - -func (l *listener) nextUnsubscriptionEvent() (*websocket.UnsubscribeEvent, error) { - timeout := make(chan bool) - go func() { - time.Sleep(time.Second * 2) - close(timeout) - }() - select { - case ev := <-l.unsubscriptionEvents: - return ev, nil - case <-timeout: - return nil, errors.New("timed out waiting for UnsubscribeEvent") - } -} - -func (l *listener) nextTick() (*bitfinex.Ticker, error) { - timeout := make(chan bool) - go func() { - time.Sleep(time.Second * 2) - close(timeout) - }() - select { - case ev := <-l.ticks: - return ev, nil - case <-timeout: - return nil, errors.New("timed out waiting for Ticker") - } -} - -func (l *listener) nextNotification() (*bitfinex.Notification, error) { - timeout := make(chan bool) - go func() { - time.Sleep(time.Second * 2) - close(timeout) - }() - select { - case ev := <-l.notifications: - return ev, nil - case <-timeout: - return nil, errors.New("timed out waiting for Notification") - } -} - -func (l *listener) nextTradeExecution() (*bitfinex.TradeExecution, error) { - timeout := make(chan bool) - go func() { - time.Sleep(time.Second * 2) - close(timeout) - }() - select { - case ev := <-l.tradeExecutions: - return ev, nil - case <-timeout: - return nil, errors.New("timed out waiting for TradeExecution") - } -} - -func (l *listener) nextPositionUpdate() (*bitfinex.PositionUpdate, error) { - timeout := make(chan bool) - go func() { - time.Sleep(time.Second * 2) - close(timeout) - }() - select { - case ev := <-l.positions: - return ev, nil - case <-timeout: - return nil, errors.New("timed out waiting for PositionUpdate") - } -} - -func (l *listener) nextTradeUpdate() (*bitfinex.TradeExecutionUpdate, error) { - timeout := make(chan bool) - go func() { - time.Sleep(time.Second * 2) - close(timeout) - }() - select { - case ev := <-l.tradeUpdates: - return ev, nil - case <-timeout: - return nil, errors.New("timed out waiting for TradeUpdate") - } -} - -func (l *listener) nextOrderCancel() (*bitfinex.OrderCancel, error) { - timeout := make(chan bool) - go func() { - time.Sleep(time.Second * 2) - close(timeout) - }() - select { - case ev := <-l.cancels: - return ev, nil - case <-timeout: - return nil, errors.New("timed out waiting for OrderCancel") - } -} - -func (l *listener) nextMarginInfoBase() (*bitfinex.MarginInfoBase, error) { - timeout := make(chan bool) - go func() { - time.Sleep(time.Second * 2) - close(timeout) - }() - select { - case ev := <-l.marginBase: - return ev, nil - case <-timeout: - return nil, errors.New("timed out waiting for MarginInfoBase") - } -} - -func (l *listener) nextMarginInfoUpdate() (*bitfinex.MarginInfoUpdate, error) { - timeout := make(chan bool) - go func() { - time.Sleep(time.Second * 2) - close(timeout) - }() - select { - case ev := <-l.marginUpdate: - return ev, nil - case <-timeout: - return nil, errors.New("timed out waiting for MarginInfoUpdate") - } -} - -func (l *listener) nextFundingInfo() (*bitfinex.FundingInfo, error) { - timeout := make(chan bool) - go func() { - time.Sleep(time.Second * 2) - close(timeout) - }() - select { - case ev := <-l.funding: - return ev, nil - case <-timeout: - return nil, errors.New("timed out waiting for FundingInfo") - } -} - -func (l *listener) nextOrderNew() (*bitfinex.OrderNew, error) { - timeout := make(chan bool) - go func() { - time.Sleep(time.Second * 2) - close(timeout) - }() - select { - case ev := <-l.orderNew: - return ev, nil - case <-timeout: - return nil, errors.New("timed out waiting for OrderNew") - } -} - -// strongly types messages and places them into a channel -func (l *listener) run(ch <-chan interface{}) { - go func() { - for { - select { - case msg := <-ch: - if msg == nil { - return - } - // remove threading guarantees when mulitplexing into channels - log.Printf("[DEBUG] WsService -> WsClient: %#v", msg) - switch msg.(type) { - case error: - l.errors <- msg.(error) - case *bitfinex.Ticker: - l.ticks <- msg.(*bitfinex.Ticker) - case *websocket.InfoEvent: - l.infoEvents <- msg.(*websocket.InfoEvent) - case *websocket.SubscribeEvent: - l.subscriptionEvents <- msg.(*websocket.SubscribeEvent) - case *websocket.UnsubscribeEvent: - l.unsubscriptionEvents <- msg.(*websocket.UnsubscribeEvent) - case *websocket.AuthEvent: - l.authEvents <- msg.(*websocket.AuthEvent) - case *bitfinex.WalletUpdate: - l.walletUpdates <- msg.(*bitfinex.WalletUpdate) - case *bitfinex.BalanceUpdate: - l.balanceUpdates <- msg.(*bitfinex.BalanceUpdate) - case *bitfinex.Notification: - l.notifications <- msg.(*bitfinex.Notification) - case *bitfinex.TradeExecutionUpdate: - l.tradeUpdates <- msg.(*bitfinex.TradeExecutionUpdate) - case *bitfinex.TradeExecution: - l.tradeExecutions <- msg.(*bitfinex.TradeExecution) - case *bitfinex.PositionUpdate: - l.positions <- msg.(*bitfinex.PositionUpdate) - case *bitfinex.OrderCancel: - l.cancels <- msg.(*bitfinex.OrderCancel) - case *bitfinex.MarginInfoBase: - l.marginBase <- msg.(*bitfinex.MarginInfoBase) - case *bitfinex.MarginInfoUpdate: - l.marginUpdate <- msg.(*bitfinex.MarginInfoUpdate) - case *bitfinex.OrderNew: - l.orderNew <- msg.(*bitfinex.OrderNew) - case *bitfinex.FundingInfo: - l.funding <- msg.(*bitfinex.FundingInfo) - case *bitfinex.PositionSnapshot: - l.positionSnapshot <- msg.(*bitfinex.PositionSnapshot) - case *bitfinex.WalletSnapshot: - l.walletSnapshot <- msg.(*bitfinex.WalletSnapshot) - default: - log.Printf("COULD NOT TYPE MSG ^") - } - } - } - }() -} diff --git a/tests/integration/v2/live_websocket_private_test.go b/tests/integration/v2/live_websocket_private_test.go deleted file mode 100644 index 7f19a91e9..000000000 --- a/tests/integration/v2/live_websocket_private_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package tests - -import ( - "context" - "sync" - "testing" - "time" - - "github.com/bitfinexcom/bitfinex-api-go/v2" - "github.com/bitfinexcom/bitfinex-api-go/v2/websocket" -) - -func TestWebsocketOrder(t *testing.T) { - if !auth { - t.Skip("no credentials, skipping order creation") - } - - wg := sync.WaitGroup{} - wg.Add(1) // 1. Authentication event - - c := websocket.New().Credentials(key, secret) - - errch := make(chan error) - - err := c.Connect() - if err != nil { - t.Fatalf("connecting to websocket service: %s", err) - } - defer c.Close() - - go func() { - for ev := range c.Listen() { - switch e := ev.(type) { - case *bitfinex.Notification: - if e.Status == "ERROR" && e.Type == "on-req" { - t.Errorf("failed to create order: %s", e.Text) - } - case *bitfinex.OrderNew: - wg.Done() - case *bitfinex.OrderCancel: - wg.Done() - case error: - t.Logf("Listen() error: %s", ev) - errch <- ev.(error) - wg.Done() - } - } - }() - /* - err = c.Authenticate(context.Background()) - if err != nil { - t.Fatalf("authenticating with websocket service: %s", err) - } - if err := wait(&wg, errch, 2*time.Second); err != nil { - t.Fatalf("failed to authenticate with websocket service: %s", err) - } - */ - wg.Add(1) - n := time.Now() - cid := n.Unix() - cidDate := n.Format("2006-01-02") - o := &bitfinex.OrderNewRequest{ - CID: cid, - Type: bitfinex.OrderTypeExchangeLimit, - Symbol: bitfinex.TradingPrefix + bitfinex.BTCUSD, - Amount: 1.0, - Price: 28.5, - } - - ctx, cxl1 := context.WithTimeout(context.Background(), 2*time.Second) - defer cxl1() - err = c.SubmitOrder(ctx, o) - if err != nil { - t.Fatalf("failed to send OrderNewRequest: %s", err) - } - if err := wait(&wg, errch, 2*time.Second); err != nil { - t.Fatalf("failed to create order: %s", err) - } - - oc := &bitfinex.OrderCancelRequest{ - CID: cid, - CIDDate: cidDate, - } - - wg.Add(1) - ctx, cxl2 := context.WithTimeout(context.Background(), 2*time.Second) - defer cxl2() - err = c.SubmitCancel(ctx, oc) - if err != nil { - t.Fatalf("failed to send OrderCancelRequest: %s", err) - } - if err := wait(&wg, errch, 2*time.Second); err != nil { - t.Fatalf("failed to cancel order: %s", err) - } -} diff --git a/tests/integration/v2/live_websocket_public_test.go b/tests/integration/v2/live_websocket_public_test.go deleted file mode 100644 index 0dc013339..000000000 --- a/tests/integration/v2/live_websocket_public_test.go +++ /dev/null @@ -1,315 +0,0 @@ -package tests - -import ( - "context" - "fmt" - "log" - "sync" - "testing" - "time" - - "github.com/bitfinexcom/bitfinex-api-go/v2" - "github.com/bitfinexcom/bitfinex-api-go/v2/websocket" -) - -// wait2 will wait for at least "count" messages on channel "ch" within time "t", or return an error -func wait2(ch <-chan interface{}, count int, bc <-chan error, t time.Duration) error { - c := make(chan interface{}) - go func() { - <-ch - close(c) - }() - select { - case <-bc: - return fmt.Errorf("transport closed while waiting") - case <-c: - return nil // normal - case <-time.After(t): - return fmt.Errorf("timed out waiting") - } - return nil -} - -func wait(wg *sync.WaitGroup, bc <-chan error, to time.Duration) error { - c := make(chan struct{}) - go func() { - defer close(c) - wg.Wait() - }() - select { - case <-bc: - return fmt.Errorf("websocket closed while waiting") // timed out - case <-c: - return nil // completed normally - case <-time.After(to): - return fmt.Errorf("timed out waiting") // timed out - } -} - -func TestPublicTicker(t *testing.T) { - c := websocket.New() - - err := c.Connect() - if err != nil { - t.Fatal("Error connecting to web socket : ", err) - } - defer c.Close() - - subs := make(chan interface{}, 10) - unsubs := make(chan interface{}, 10) - infos := make(chan interface{}, 10) - tick := make(chan interface{}, 100) - - errch := make(chan error) - go func() { - for { - select { - case msg := <-c.Listen(): - if msg == nil { - return - } - log.Printf("recv msg: %#v", msg) - switch m := msg.(type) { - case error: - errch <- msg.(error) - case *websocket.UnsubscribeEvent: - unsubs <- m - case *websocket.SubscribeEvent: - subs <- m - case *websocket.InfoEvent: - infos <- m - case *bitfinex.TickerSnapshot: - tick <- m - case *bitfinex.Ticker: - tick <- m - default: - t.Logf("test recv: %#v", msg) - } - } - } - }() - - ctx, cxl := context.WithTimeout(context.Background(), time.Second*5) - defer cxl() - id, err := c.SubscribeTicker(ctx, bitfinex.TradingPrefix+bitfinex.BTCUSD) - if err != nil { - t.Fatal(err) - } - - if err := wait2(tick, 1, errch, 2*time.Second); err != nil { - t.Fatalf("failed to receive ticker message from websocket: %s", err) - } - - err = c.Unsubscribe(ctx, id) - if err != nil { - t.Fatal(err) - } - - if err := wait2(unsubs, 1, errch, 2*time.Second); err != nil { - t.Errorf("failed to receive unsubscribe message from websocket: %s", err) - } -} - -func TestPublicTrades(t *testing.T) { - c := websocket.New() - wg := sync.WaitGroup{} - wg.Add(3) // 1. Info with version, 2. Subscription event, 3. 3 x data message - - err := c.Connect() - if err != nil { - t.Fatal("Error connecting to web socket : ", err) - } - defer c.Close() - - subs := make(chan interface{}, 10) - unsubs := make(chan interface{}, 10) - infos := make(chan interface{}, 10) - trades := make(chan interface{}, 100) - - errch := make(chan error) - go func() { - for { - select { - case msg := <-c.Listen(): - if msg == nil { - return - } - log.Printf("recv msg: %#v", msg) - switch m := msg.(type) { - case error: - errch <- msg.(error) - case *websocket.UnsubscribeEvent: - unsubs <- m - case *websocket.SubscribeEvent: - subs <- m - case *websocket.InfoEvent: - infos <- m - case *bitfinex.TradeExecutionUpdateSnapshot: - trades <- m - case *bitfinex.Trade: - trades <- m - case *bitfinex.TradeExecutionUpdate: - trades <- m - case *bitfinex.TradeExecution: - trades <- m - case *bitfinex.TradeSnapshot: - trades <- m - default: - t.Logf("test recv: %#v", msg) - } - } - } - }() - - ctx, cxl := context.WithTimeout(context.Background(), time.Second*5) - defer cxl() - id, err := c.SubscribeTrades(ctx, bitfinex.TradingPrefix+bitfinex.BTCUSD) - if err != nil { - t.Fatal(err) - } - - if err := wait2(trades, 1, errch, 2*time.Second); err != nil { - t.Errorf("failed to receive trade message from websocket: %s", err) - } - - err = c.Unsubscribe(ctx, id) - if err != nil { - t.Fatal(err) - } - - if err := wait2(unsubs, 1, errch, 2*time.Second); err != nil { - t.Errorf("failed to receive unsubscribe message from websocket: %s", err) - } -} - -func TestPublicBooks(t *testing.T) { - c := websocket.New() - wg := sync.WaitGroup{} - wg.Add(3) // 1. Info with version, 2. Subscription event, 3. data message - - err := c.Connect() - if err != nil { - t.Fatal("Error connecting to web socket : ", err) - } - defer c.Close() - - subs := make(chan interface{}, 10) - unsubs := make(chan interface{}, 10) - infos := make(chan interface{}, 10) - books := make(chan interface{}, 100) - - errch := make(chan error) - go func() { - for { - select { - case msg := <-c.Listen(): - if msg == nil { - return - } - log.Printf("recv msg: %#v", msg) - switch m := msg.(type) { - case error: - errch <- msg.(error) - case *websocket.UnsubscribeEvent: - unsubs <- m - case *websocket.SubscribeEvent: - subs <- m - case *websocket.InfoEvent: - infos <- m - case *bitfinex.BookUpdateSnapshot: - books <- m - case *bitfinex.BookUpdate: - books <- m - default: - t.Logf("test recv: %#v", msg) - } - } - } - }() - - ctx, cxl := context.WithTimeout(context.Background(), time.Second*5) - defer cxl() - id, err := c.SubscribeBook(ctx, bitfinex.TradingPrefix+bitfinex.BTCUSD, bitfinex.Precision0, bitfinex.FrequencyRealtime, 1) - if err != nil { - t.Fatal(err) - } - - if err := wait2(books, 1, errch, 5*time.Second); err != nil { - t.Fatalf("failed to receive book update message from websocket: %s", err) - } - - err = c.Unsubscribe(ctx, id) - if err != nil { - t.Fatal(err) - } - - if err := wait2(unsubs, 1, errch, 5*time.Second); err != nil { - t.Errorf("failed to receive unsubscribe message from websocket: %s", err) - } -} - -func TestPublicCandles(t *testing.T) { - c := websocket.New() - wg := sync.WaitGroup{} - wg.Add(3) // 1. Info with version, 2. Subscription event, 3. data message - - err := c.Connect() - if err != nil { - t.Fatal("Error connecting to web socket : ", err) - } - defer c.Close() - - subs := make(chan interface{}, 10) - unsubs := make(chan interface{}, 10) - infos := make(chan interface{}, 10) - candles := make(chan interface{}, 100) - - errch := make(chan error) - go func() { - for { - select { - case msg := <-c.Listen(): - if msg == nil { - return - } - log.Printf("recv msg: %#v", msg) - switch m := msg.(type) { - case error: - errch <- msg.(error) - case *websocket.UnsubscribeEvent: - unsubs <- m - case *websocket.SubscribeEvent: - subs <- m - case *websocket.InfoEvent: - infos <- m - case *bitfinex.Candle: - candles <- m - case *bitfinex.CandleSnapshot: - candles <- m - default: - t.Logf("test recv: %#v", msg) - } - } - } - }() - - ctx, cxl := context.WithTimeout(context.Background(), time.Second*5) - defer cxl() - id, err := c.SubscribeCandles(ctx, bitfinex.TradingPrefix+bitfinex.BTCUSD, bitfinex.OneMonth) - if err != nil { - t.Fatal(err) - } - - if err := wait2(candles, 1, errch, 2*time.Second); err != nil { - t.Errorf("failed to receive a candle message from websocket: %s", err) - } - - err = c.Unsubscribe(ctx, id) - if err != nil { - t.Fatal(err) - } - - if err := wait2(unsubs, 1, errch, 2*time.Second); err != nil { - t.Errorf("failed to receive an unsubscribe message from websocket: %s", err) - } -} diff --git a/tests/integration/v2/mock_async.go b/tests/integration/v2/mock_async.go deleted file mode 100644 index 4e2066a61..000000000 --- a/tests/integration/v2/mock_async.go +++ /dev/null @@ -1,102 +0,0 @@ -package tests - -import ( - "context" - "errors" - "fmt" - "log" - "sync" - "time" - - "github.com/bitfinexcom/bitfinex-api-go/v2/websocket" -) - -// does not work for reconnect tests -type TestAsyncFactory struct { - Async websocket.Asynchronous -} - -func (t *TestAsyncFactory) Create() websocket.Asynchronous { - return t.Async -} - -func newTestAsyncFactory(async websocket.Asynchronous) websocket.AsynchronousFactory { - return &TestAsyncFactory{Async: async} -} - -type TestAsync struct { - done chan error - bridge chan []byte - connected bool - Sent []interface{} - mutex sync.Mutex -} - -func (t *TestAsync) SentCount() int { - t.mutex.Lock() - defer t.mutex.Unlock() - return len(t.Sent) -} - -func (t *TestAsync) waitForMessage(num int) error { - seconds := 4 - loops := 20 - delay := time.Duration(float64(time.Second) * float64(seconds) / float64(loops)) - for i := 0; i < loops; i++ { - t.mutex.Lock() - len := len(t.Sent) - t.mutex.Unlock() - if num+1 <= len { - return nil - } - time.Sleep(delay) - } - return fmt.Errorf("did not send a message in pos %d", num) -} - -func (t *TestAsync) Connect() error { - t.connected = true - return nil -} - -func (t *TestAsync) Send(ctx context.Context, msg interface{}) error { - if !t.connected { - return errors.New("must connect before sending") - } - t.mutex.Lock() - defer t.mutex.Unlock() - t.Sent = append(t.Sent, msg) - return nil -} - -func (t *TestAsync) DumpSentMessages() { - for i, msg := range t.Sent { - log.Printf("%2d: %#v", i, msg) - } -} - -func (t *TestAsync) Listen() <-chan []byte { - return t.bridge -} - -func (t *TestAsync) Publish(raw string) { - t.bridge <- []byte(raw) -} - -func (t *TestAsync) Close() { - close(t.bridge) - close(t.done) -} - -func (t *TestAsync) Done() <-chan error { - return t.done -} - -func newTestAsync() *TestAsync { - return &TestAsync{ - bridge: make(chan []byte), - connected: false, - Sent: make([]interface{}, 0), - done: make(chan error), - } -} diff --git a/tests/integration/v2/mock_ws_private_test.go b/tests/integration/v2/mock_ws_private_test.go deleted file mode 100644 index e7cd64ffd..000000000 --- a/tests/integration/v2/mock_ws_private_test.go +++ /dev/null @@ -1,441 +0,0 @@ -package tests - -import ( - "context" - "testing" - - bitfinex "github.com/bitfinexcom/bitfinex-api-go/v2" - "github.com/bitfinexcom/bitfinex-api-go/v2/websocket" -) - -func TestAuthentication(t *testing.T) { - // create transport & nonce mocks - async := newTestAsync() - nonce := &IncrementingNonceGenerator{} - - // create client - ws := websocket.NewWithAsyncFactoryNonce(newTestAsyncFactory(async), nonce).Credentials("apiKeyABC", "apiSecretXYZ") - - // setup listener - listener := newListener() - listener.run(ws.Listen()) - - // set ws options - ws.Connect() - defer ws.Close() - - // begin test - async.Publish(`{"event":"info","version":2}`) - ev, err := listener.nextInfoEvent() - if err != nil { - t.Fatal(err) - } - assert(t, &websocket.InfoEvent{Version: 2}, ev) - - // assert outgoing auth request - if err := async.waitForMessage(0); err != nil { - t.Fatal(err.Error()) - } - assert(t, &websocket.SubscriptionRequest{SubID: "nonce1", Event: "auth", APIKey: "apiKeyABC"}, async.Sent[0].(*websocket.SubscriptionRequest)) - - // auth ack - async.Publish(`{"event":"auth","status":"OK","chanId":0,"userId":1,"subId":"nonce1","auth_id":"valid-auth-guid","caps":{"orders":{"read":1,"write":0},"account":{"read":1,"write":0},"funding":{"read":1,"write":0},"history":{"read":1,"write":0},"wallets":{"read":1,"write":0},"withdraw":{"read":0,"write":0},"positions":{"read":1,"write":0}}}`) - - // assert incoming auth ack - av, err := listener.nextAuthEvent() - if err != nil { - t.Fatal(err) - } - assert(t, &websocket.AuthEvent{Status: "OK", SubID: "nonce1", ChanID: 0}, av) -} - -func TestWalletBalanceUpdates(t *testing.T) { - // create transport & nonce mocks - async := newTestAsync() - nonce := &IncrementingNonceGenerator{} - - // create client - ws := websocket.NewWithAsyncFactoryNonce(newTestAsyncFactory(async), nonce).Credentials("apiKeyABC", "apiSecretXYZ") - - // setup listener - listener := newListener() - listener.run(ws.Listen()) - - // set ws options - //ws.SetReadTimeout(time.Second * 2) - ws.Connect() - defer ws.Close() - - // begin test--authentication assertions in TestAuthentication - async.Publish(`{"event":"info","version":2}`) - // eat event - _, err := listener.nextInfoEvent() - if err != nil { - t.Fatal(err) - } - - // auth ack - async.Publish(`{"event":"auth","status":"OK","chanId":0,"userId":1,"subId":"nonce1","auth_id":"valid-auth-guid","caps":{"orders":{"read":1,"write":0},"account":{"read":1,"write":0},"funding":{"read":1,"write":0},"history":{"read":1,"write":0},"wallets":{"read":1,"write":0},"withdraw":{"read":0,"write":0},"positions":{"read":1,"write":0}}}`) - - // eat event - _, err = listener.nextAuthEvent() - if err != nil { - t.Fatal(err) - } - - // publish account info post auth ack - async.Publish(`[0,"wu",["exchange","BTC",30,0,30]]`) - async.Publish(`[0,"wu",["exchange","USD",80000,0,80000]]`) - async.Publish(`[0,"wu",["exchange","ETH",100,0,100]]`) - async.Publish(`[0,"wu",["margin","BTC",10,0,10]]`) - async.Publish(`[0,"wu",["funding","BTC",10,0,10]]`) - async.Publish(`[0,"wu",["funding","USD",10000,0,10000]]`) - async.Publish(`[0,"wu",["margin","USD",10000,0,10000]]`) - async.Publish(`[0,"bu",[147260,147260]]`) - - // assert incoming wallet & balance updates - wu, err := listener.nextWalletUpdate() - if err != nil { - t.Fatal(err) - } - assert(t, &bitfinex.WalletUpdate{Type: "exchange", Currency: "BTC", Balance: 30, BalanceAvailable: 30}, wu) - wu, _ = listener.nextWalletUpdate() - assert(t, &bitfinex.WalletUpdate{Type: "exchange", Currency: "USD", Balance: 80000, BalanceAvailable: 80000}, wu) - wu, _ = listener.nextWalletUpdate() - assert(t, &bitfinex.WalletUpdate{Type: "exchange", Currency: "ETH", Balance: 100, BalanceAvailable: 100}, wu) - wu, _ = listener.nextWalletUpdate() - assert(t, &bitfinex.WalletUpdate{Type: "margin", Currency: "BTC", Balance: 10, BalanceAvailable: 10}, wu) - wu, _ = listener.nextWalletUpdate() - assert(t, &bitfinex.WalletUpdate{Type: "funding", Currency: "BTC", Balance: 10, BalanceAvailable: 10}, wu) - wu, _ = listener.nextWalletUpdate() - assert(t, &bitfinex.WalletUpdate{Type: "funding", Currency: "USD", Balance: 10000, BalanceAvailable: 10000}, wu) - wu, _ = listener.nextWalletUpdate() - assert(t, &bitfinex.WalletUpdate{Type: "margin", Currency: "USD", Balance: 10000, BalanceAvailable: 10000}, wu) - bu, err := listener.nextBalanceUpdate() - if err != nil { - t.Fatal(err) - } - // total aum, net aum - assert(t, &bitfinex.BalanceUpdate{TotalAUM: 147260, NetAUM: 147260}, bu) -} - -func TestNewOrder(t *testing.T) { - // create transport & nonce mocks - async := newTestAsync() - nonce := &IncrementingNonceGenerator{} - - // create client - ws := websocket.NewWithAsyncFactoryNonce(newTestAsyncFactory(async), nonce).Credentials("apiKeyABC", "apiSecretXYZ") - - // setup listener - listener := newListener() - listener.run(ws.Listen()) - - // set ws options - //ws.SetReadTimeout(time.Second * 2) - ws.Connect() - defer ws.Close() - - // begin test - async.Publish(`{"event":"info","version":2}`) - _, err := listener.nextInfoEvent() - if err != nil { - t.Fatal(err) - } - - // initial logon info--Authentication & WalletUpdate assertions in prior tests - async.Publish(`{"event":"auth","status":"OK","chanId":0,"userId":1,"subId":"nonce1","auth_id":"valid-auth-guid","caps":{"orders":{"read":1,"write":0},"account":{"read":1,"write":0},"funding":{"read":1,"write":0},"history":{"read":1,"write":0},"wallets":{"read":1,"write":0},"withdraw":{"read":0,"write":0},"positions":{"read":1,"write":0}}}`) - async.Publish(`[0,"wu",["exchange","BTC",30,0,30]]`) - async.Publish(`[0,"wu",["exchange","USD",80000,0,80000]]`) - async.Publish(`[0,"wu",["exchange","ETH",100,0,100]]`) - async.Publish(`[0,"wu",["margin","BTC",10,0,10]]`) - async.Publish(`[0,"wu",["funding","BTC",10,0,10]]`) - async.Publish(`[0,"wu",["funding","USD",10000,0,10000]]`) - async.Publish(`[0,"wu",["margin","USD",10000,0,10000]]`) - async.Publish(`[0,"bu",[147260,147260]]`) - - // submit order - err = ws.SubmitOrder(context.Background(), &bitfinex.OrderNewRequest{ - Symbol: "tBTCUSD", - CID: 123, - Amount: -0.456, - }) - if err != nil { - t.Fatal(err) - } - - // assert outgoing order request - if len(async.Sent) <= 1 { - t.Fatalf("expected >1 sent messages, got %d", len(async.Sent)) - } - assert(t, &bitfinex.OrderNewRequest{Symbol: "tBTCUSD", CID: 123, Amount: -0.456}, async.Sent[1].(*bitfinex.OrderNewRequest)) - - // order ack - async.Publish(`[0,"n",[null,"on-req",null,null,[1234567,null,123,"tBTCUSD",null,null,1,1,"MARKET",null,null,null,null,null,null,null,915.5,null,null,null,null,null,null,0,null,null],null,"SUCCESS","Submitting market buy order for 1.0 BTC."]]`) - - // assert order ack notification - not, err := listener.nextNotification() - if err != nil { - t.Fatal(err) - } - assert(t, &bitfinex.Notification{Type: "on-req", NotifyInfo: &bitfinex.OrderNew{ID: 1234567, CID: 123, Symbol: "tBTCUSD", Amount: 1, AmountOrig: 1, Type: "MARKET", Price: 915.5}}, not) -} - -func TestFills(t *testing.T) { - // create transport & nonce mocks - async := newTestAsync() - nonce := &IncrementingNonceGenerator{} - - // create client - ws := websocket.NewWithAsyncFactoryNonce(newTestAsyncFactory(async), nonce).Credentials("apiKeyABC", "apiSecretXYZ") - - // setup listener - listener := newListener() - listener.run(ws.Listen()) - - // set ws options - //ws.SetReadTimeout(time.Second * 2) - ws.Connect() - defer ws.Close() - - // begin test - async.Publish(`{"event":"info","version":2}`) - _, err := listener.nextInfoEvent() - if err != nil { - t.Fatal(err) - } - - // initial logon info - async.Publish(`{"event":"auth","status":"OK","chanId":0,"userId":1,"subId":"nonce1","auth_id":"valid-auth-guid","caps":{"orders":{"read":1,"write":0},"account":{"read":1,"write":0},"funding":{"read":1,"write":0},"history":{"read":1,"write":0},"wallets":{"read":1,"write":0},"withdraw":{"read":0,"write":0},"positions":{"read":1,"write":0}}}`) - async.Publish(`[0,"ps",[["tBTCUSD","ACTIVE",7,916.52002351,0,0,null,null,null,null]]]`) - async.Publish(`[0,"ws",[["exchange","BTC",30,0,null],["exchange","USD",80000,0,null],["exchange","ETH",100,0,null],["margin","BTC",10,0,null],["margin","USD",9987.16871968,0,null],["funding","BTC",10,0,null],["funding","USD",10000,0,null]]]`) - // consume & assert snapshots - ps, err := listener.nextPositionSnapshot() - if err != nil { - t.Fatal(err) - } - eps := make([]*bitfinex.Position, 1) - eps[0] = &bitfinex.Position{ - Symbol: "tBTCUSD", - Status: "ACTIVE", - Amount: 7, - BasePrice: 916.52002351, - } - snap := &bitfinex.PositionSnapshot{ - Snapshot: eps, - } - assertSlice(t, snap, ps) - w, err := listener.nextWalletSnapshot() - if err != nil { - t.Fatal(err) - } - ews := make([]*bitfinex.Wallet, 7) - ews[0] = &bitfinex.Wallet{Type: "exchange", Currency: "BTC", Balance: 30} - ews[1] = &bitfinex.Wallet{Type: "exchange", Currency: "USD", Balance: 80000} - ews[2] = &bitfinex.Wallet{Type: "exchange", Currency: "ETH", Balance: 100} - ews[3] = &bitfinex.Wallet{Type: "margin", Currency: "BTC", Balance: 10} - ews[4] = &bitfinex.Wallet{Type: "margin", Currency: "USD", Balance: 9987.16871968} - ews[5] = &bitfinex.Wallet{Type: "funding", Currency: "BTC", Balance: 10} - ews[6] = &bitfinex.Wallet{Type: "funding", Currency: "USD", Balance: 10000} - wsnap := &bitfinex.WalletSnapshot{ - Snapshot: ews, - } - assertSlice(t, wsnap, w) - - // submit order - err = ws.SubmitOrder(context.Background(), &bitfinex.OrderNewRequest{ - Symbol: "tBTCUSD", - CID: 123, - Amount: -0.456, - }) - if err != nil { - t.Fatal(err) - } - - // order ack - async.Publish(`[0,"n",[null,"on-req",null,null,[1234567,null,123,"tBTCUSD",null,null,1,1,"MARKET",null,null,null,null,null,null,null,915.5,null,null,null,null,null,null,0,null,null],null,"SUCCESS","Submitting market buy order for 1.0 BTC."]]`) - - // assert order ack notification--Authentication, WalletUpdate, order acknowledgement assertions in prior tests - _, err = listener.nextNotification() - if err != nil { - t.Fatal(err) - } - - // <..crossing orders generates a fill..> - // partial fills--position updates - async.Publish(`[0,"pu",["tBTCUSD","ACTIVE",0.21679716,915.9,0,0,null,null,null,null]]`) - async.Publish(`[0,"pu",["tBTCUSD","ACTIVE",1,916.13496085,0,0,null,null,null,null]]`) - pu, err := listener.nextPositionUpdate() - if err != nil { - t.Fatal(err) - } - assert(t, &bitfinex.PositionUpdate{Symbol: "tBTCUSD", Status: "ACTIVE", Amount: 0.21679716, BasePrice: 915.9}, pu) - pu, _ = listener.nextPositionUpdate() - assert(t, &bitfinex.PositionUpdate{Symbol: "tBTCUSD", Status: "ACTIVE", Amount: 1, BasePrice: 916.13496085}, pu) - - // full fill--order terminal state message - async.Publish(`[0,"oc",[1234567,0,123,"tBTCUSD",1514909325236,1514909325631,0,1,"MARKET",null,null,null,0,"EXECUTED @ 916.2(0.78): was PARTIALLY FILLED @ 915.9(0.22)",null,null,915.5,916.13496085,null,null,null,null,null,0,0,0]]`) - oc, err := listener.nextOrderCancel() - if err != nil { - t.Fatal(err) - } - assert(t, &bitfinex.OrderCancel{ID: 1234567, CID: 123, Symbol: "tBTCUSD", MTSCreated: 1514909325236, MTSUpdated: 1514909325631, Amount: 0, AmountOrig: 1, Type: "MARKET", Status: "EXECUTED @ 916.2(0.78): was PARTIALLY FILLED @ 915.9(0.22)", Price: 915.5, PriceAvg: 916.13496085}, oc) - - // fills--trade executions - async.Publish(`[0,"te",[1,"tBTCUSD",1514909325593,1234567,0.21679716,915.9,null,null,-1]]`) - async.Publish(`[0,"te",[2,"tBTCUSD",1514909325597,1234567,0.78320284,916.2,null,null,-1]]`) - te, err := listener.nextTradeExecution() - assert(t, &bitfinex.TradeExecution{ID: 1, MTS: 1514909325593, Amount: 0.21679716, Price: 915.9}, te) - te, _ = listener.nextTradeExecution() - assert(t, &bitfinex.TradeExecution{ID: 2, MTS: 1514909325597, Amount: 0.78320284, Price: 916.2}, te) - - // fills--trade updates - async.Publish(`[0,"tu",[1,"tBTCUSD",1514909325593,1234567,0.21679716,915.9,"MARKET",915.5,-1,-0.39712904,"USD"]]`) - async.Publish(`[0,"tu",[2,"tBTCUSD",1514909325597,1234567,0.78320284,916.2,"MARKET",915.5,-1,-1.43514088,"USD"]]`) - tu, err := listener.nextTradeUpdate() - if err != nil { - t.Fatal(err) - } - assert(t, &bitfinex.TradeExecutionUpdate{ID: 1, Pair: "tBTCUSD", MTS: 1514909325593, ExecAmount: 0.21679716, ExecPrice: 915.9, OrderType: "MARKET", OrderPrice: 915.5, OrderID: 1234567, Maker: -1, Fee: -0.39712904, FeeCurrency: "USD"}, tu) - tu, _ = listener.nextTradeUpdate() - assert(t, &bitfinex.TradeExecutionUpdate{ID: 2, Pair: "tBTCUSD", MTS: 1514909325597, ExecAmount: 0.78320284, ExecPrice: 916.2, OrderType: "MARKET", OrderPrice: 915.5, OrderID: 1234567, Maker: -1, Fee: -1.43514088, FeeCurrency: "USD"}, tu) - - // fills--wallet updates from fee deduction - async.Publish(`[0,"wu",["margin","USD",9999.60287096,0,null]]`) - async.Publish(`[0,"wu",["margin","USD",9998.16773008,0,null]]`) - wu, err := listener.nextWalletUpdate() - if err != nil { - t.Fatal(err) - } - assert(t, &bitfinex.WalletUpdate{Type: "margin", Currency: "USD", Balance: 9999.60287096}, wu) - wu, _ = listener.nextWalletUpdate() - assert(t, &bitfinex.WalletUpdate{Type: "margin", Currency: "USD", Balance: 9998.16773008}, wu) - - // margin info update for executed trades - async.Publish(`[0,"miu",["base",[-2.76536085,0,19150.16773008,19147.40236923]]]`) - async.Publish(`[0,"miu",["sym","tBTCUSD",[60162.93960325,61088.2924336,60162.93960325,60162.93960325,null,null,null,null]]]`) - mb, err := listener.nextMarginInfoBase() - if err != nil { - t.Fatal(err) - } - assert(t, &bitfinex.MarginInfoBase{}, mb) - mu, err := listener.nextMarginInfoUpdate() - if err != nil { - t.Fatal(err) - } - assert(t, &bitfinex.MarginInfoUpdate{}, mu) - - // position update for executed trades - async.Publish(`[0,"pu",["tBTCUSD","ACTIVE",1,916.13496085,0,0,-2.76536085,-0.30185082,0,43.7962]]`) - pu, _ = listener.nextPositionUpdate() - assert(t, &bitfinex.PositionUpdate{Symbol: "tBTCUSD", Status: "ACTIVE", Amount: 1, BasePrice: 916.13496085, ProfitLoss: -2.76536085, ProfitLossPercentage: -0.30185082, Leverage: 43.7962}, pu) - - // wallet margin update for executed trades - async.Publish(`[0,"wu",["margin","BTC",10,0,10]]`) - async.Publish(`[0,"wu",["margin","USD",9998.16773008,0,9998.16773008]]`) - wu, _ = listener.nextWalletUpdate() - assert(t, &bitfinex.WalletUpdate{Type: "margin", Currency: "BTC", Balance: 10, BalanceAvailable: 10}, wu) - wu, _ = listener.nextWalletUpdate() - assert(t, &bitfinex.WalletUpdate{Type: "margin", Currency: "USD", Balance: 9998.16773008, BalanceAvailable: 9998.16773008}, wu) - - // funding update for executed trades - async.Publish(`[0,"fiu",["sym","ftBTCUSD",[0,0,0,0]]]`) - fi, err := listener.nextFundingInfo() - if err != nil { - t.Fatal(err) - } - assert(t, &bitfinex.FundingInfo{Symbol: "ftBTCUSD"}, fi) -} - -func TestCancel(t *testing.T) { - // create transport & nonce mocks - async := newTestAsync() - nonce := &IncrementingNonceGenerator{} - - // create client - ws := websocket.NewWithAsyncFactoryNonce(newTestAsyncFactory(async), nonce).Credentials("apiKeyABC", "apiSecretXYZ") - - // setup listener - listener := newListener() - listener.run(ws.Listen()) - - // set ws options - //ws.SetReadTimeout(time.Second * 2) - ws.Connect() - defer ws.Close() - - // begin test - async.Publish(`{"event":"info","version":2}`) - _, err := listener.nextInfoEvent() - if err != nil { - t.Fatal(err) - } - - // initial logon info--Authentication & WalletUpdate assertions in prior tests - async.Publish(`{"event":"auth","status":"OK","chanId":0,"userId":1,"subId":"nonce1","auth_id":"valid-auth-guid","caps":{"orders":{"read":1,"write":0},"account":{"read":1,"write":0},"funding":{"read":1,"write":0},"history":{"read":1,"write":0},"wallets":{"read":1,"write":0},"withdraw":{"read":0,"write":0},"positions":{"read":1,"write":0}}}`) - async.Publish(`[0,"ps",[["tBTCUSD","ACTIVE",7,916.52002351,0,0,null,null,null,null]]]`) - async.Publish(`[0,"ws",[["exchange","BTC",30,0,null],["exchange","USD",80000,0,null],["exchange","ETH",100,0,null],["margin","BTC",10,0,null],["margin","USD",9987.16871968,0,null],["funding","BTC",10,0,null],["funding","USD",10000,0,null]]]`) - // consume & assert snapshots - listener.nextPositionSnapshot() - listener.nextWalletSnapshot() - - // submit order - err = ws.SubmitOrder(context.Background(), &bitfinex.OrderNewRequest{ - Symbol: "tBTCUSD", - CID: 123, - Amount: -0.456, - Type: "LIMIT", - Price: 900.0, - }) - if err != nil { - t.Fatal(err) - } - - // assert outgoing order request - if len(async.Sent) <= 1 { - t.Fatalf("expected >1 sent messages, got %d", len(async.Sent)) - } - assert(t, &bitfinex.OrderNewRequest{Symbol: "tBTCUSD", CID: 123, Amount: -0.456, Type: "LIMIT", Price: 900.0}, async.Sent[1].(*bitfinex.OrderNewRequest)) - - // order pending new - async.Publish(`[0,"n",[null,"on-req",null,null,[1234567,null,123,"tBTCUSD",null,null,1,1,"LIMIT",null,null,null,null,null,null,null,900,null,null,null,null,null,null,0,null,null],null,"SUCCESS","Submitting limit buy order for 1.0 BTC."]]`) - // order working--limit order - async.Publish(`[0,"on",[1234567,0,123,"tBTCUSD",1515179518260,1515179518315,1,1,"LIMIT",null,null,null,0,"ACTIVE",null,null,900,0,null,null,null,null,null,0,0,0]]`) - - // eat order ack notification - listener.nextNotification() - - on, err := listener.nextOrderNew() - if err != nil { - t.Fatal(err) - } - - // assert order new update - assert(t, &bitfinex.OrderNew{ID: 1234567, CID: 123, Symbol: "tBTCUSD", MTSCreated: 1515179518260, MTSUpdated: 1515179518315, Type: "LIMIT", Amount: 1, AmountOrig: 1, Status: "ACTIVE", Price: 900.0}, on) - - // publish cancel request - req := &bitfinex.OrderCancelRequest{ID: on.ID} - pre := async.SentCount() - err = ws.SubmitCancel(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if err := async.waitForMessage(pre); err != nil { - t.Fatal(err.Error()) - } - // assert sent message - assert(t, req, async.Sent[pre].(*bitfinex.OrderCancelRequest)) - - // cancel ack notify - async.Publish(`[0,"n",[null,"oc-req",null,null,[1149686139,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0,null,null],null,"SUCCESS","Submitted for cancellation; waiting for confirmation (ID: 1149686139)."]]`) - - // cancel confirm - async.Publish(`[0,"oc",[1234567,0,123,"tBTCUSD",1515179518260,1515179520203,1,1,"LIMIT",null,null,null,0,"CANCELED",null,null,900,0,null,null,null,null,null,0,0,0]]`) - - // assert cancel ack - oc, err := listener.nextOrderCancel() - if err != nil { - t.Fatal(err) - } - assert(t, &bitfinex.OrderCancel{ID: 1234567, CID: 123, Symbol: "tBTCUSD", MTSCreated: 1515179518260, MTSUpdated: 1515179520203, Type: "LIMIT", Status: "CANCELED", Price: 900.0, Amount: 1, AmountOrig: 1}, oc) -} diff --git a/tests/integration/v2/mock_ws_public_test.go b/tests/integration/v2/mock_ws_public_test.go deleted file mode 100644 index 353d9814e..000000000 --- a/tests/integration/v2/mock_ws_public_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package tests - -import ( - "context" - "testing" - - bitfinex "github.com/bitfinexcom/bitfinex-api-go/v2" - "github.com/bitfinexcom/bitfinex-api-go/v2/websocket" -) - -// method of testing with mocked endpoints -func TestTicker(t *testing.T) { - // create transport & nonce mocks - async := newTestAsync() - nonce := &IncrementingNonceGenerator{} - - // create client - ws := websocket.NewWithAsyncFactoryNonce(newTestAsyncFactory(async), nonce) - - // setup listener - listener := newListener() - listener.run(ws.Listen()) - - // set ws options - ws.Connect() - defer ws.Close() - - // info welcome msg - async.Publish(`{"event":"info","version":2}`) - ev, err := listener.nextInfoEvent() - if err != nil { - t.Fatal(err) - } - assert(t, &websocket.InfoEvent{Version: 2}, ev) - - // subscribe - id, err := ws.SubscribeTicker(context.Background(), "tBTCUSD") - if err != nil { - t.Fatal(err) - } - - // subscribe ack - async.Publish(`{"event":"subscribed","channel":"ticker","chanId":5,"symbol":"tBTCUSD","subId":"nonce1","pair":"BTCUSD"}`) - sub, err := listener.nextSubscriptionEvent() - if err != nil { - t.Fatal(err) - } - assert(t, &websocket.SubscribeEvent{ - SubID: "nonce1", - Channel: "ticker", - ChanID: 5, - Symbol: "tBTCUSD", - Pair: "BTCUSD", - }, sub) - - // tick data - async.Publish(`[5,[14957,68.17328796,14958,55.29588132,-659,-0.0422,14971,53723.08813995,16494,14454]]`) - tick, err := listener.nextTick() - if err != nil { - t.Fatal(err) - } - assert(t, &bitfinex.Ticker{ - Symbol: "tBTCUSD", - Bid: 14957, - Ask: 14958, - BidSize: 68.17328796, - AskSize: 55.29588132, - DailyChange: -659, - DailyChangePerc: -0.0422, - LastPrice: 14971, - Volume: 53723.08813995, - High: 16494, - Low: 14454, - }, tick) - - // unsubscribe - ws.Unsubscribe(context.Background(), id) - async.Publish(`{"event":"unsubscribed","chanId":5,"status":"OK"}`) - unsub, err := listener.nextUnsubscriptionEvent() - if err != nil { - t.Fatal(err) - } - assert(t, &websocket.UnsubscribeEvent{ChanID: 5, Status: "OK"}, unsub) -} diff --git a/tests/integration/v2/reconnect_test.go b/tests/integration/v2/reconnect_test.go deleted file mode 100644 index d3285f402..000000000 --- a/tests/integration/v2/reconnect_test.go +++ /dev/null @@ -1,431 +0,0 @@ -package tests - -import ( - "context" - "fmt" - "log" - "testing" - "time" - - "github.com/bitfinexcom/bitfinex-api-go/v2" - "github.com/bitfinexcom/bitfinex-api-go/v2/websocket" -) - -var ( - wsPort = 4001 - wsService *TestWsService - apiRecv *listener - apiClient *websocket.Client -) - -func assertDisconnect(maxWait time.Duration, client *websocket.Client) error { - loops := 5 - delay := maxWait / time.Duration(loops) - for i := 0; i < loops; i++ { - if !client.IsConnected() { - return nil - } - time.Sleep(delay) - } - return fmt.Errorf("peer did not disconnect in %s", maxWait.String()) -} - -// convienence -func newTestParams(wsPort int) *websocket.Parameters { - p := websocket.NewDefaultParameters() - p.HeartbeatTimeout = time.Second * 4 - p.ShutdownTimeout = time.Second * 4 - p.URL = fmt.Sprintf("ws://localhost:%d", wsPort) - p.AutoReconnect = true - p.ReconnectInterval = time.Millisecond * 250 // first reconnect is instant, won't need to wait on this - return p -} - -func setup(t *testing.T, hbTimeout time.Duration, autoReconnect, auth bool) { - if wsService != nil { - wsService.Stop() - } - if apiClient != nil { - apiClient.Close() - } - - time.Sleep(time.Millisecond * 250) - - wsService = NewTestWsService(wsPort) - wsService.PublishOnConnect(`{"event":"info","version":2}`) - err := wsService.Start() - if err != nil { - t.Fatal(err) - } - - // create client - params := newTestParams(wsPort) - params.HeartbeatTimeout = hbTimeout - params.AutoReconnect = autoReconnect - factory := websocket.NewWebsocketAsynchronousFactory(params) - nonce := &IncrementingNonceGenerator{} - apiClient = websocket.NewWithParamsAsyncFactoryNonce(params, factory, nonce) - if auth { - apiClient.Credentials("apiKey1", "apiSecret1") - } - - // setup listener - // listener closes when apiClient is closed - apiRecv = newListener() - apiRecv.run(apiClient.Listen()) - - // set ws options - apiClient.Connect() - - if err := wsService.WaitForClientCount(1); err != nil { - t.Fatal(err) - } -} - -func TestReconnectResubscribeWithAuth(t *testing.T) { - // create transport & nonce mocks - setup(t, time.Second*10, true, true) - - // begin test - infoEv, err := apiRecv.nextInfoEvent() - if err != nil { - t.Fatal(err) - } - expInfoEv := websocket.InfoEvent{ - Version: 2, - } - assert(t, &expInfoEv, infoEv) - - msg, err := wsService.WaitForMessage(0, 0) - if err != nil { - t.Fatal(err) - } - if `{"subId":"nonce1","event":"auth","apiKey":"apiKey1","authSig":"6e7e3ab737bac9d36b6c3170356c9483edb0079cb65a2afa81efa9a6b906e0c3aeb16b574a44073dff4c0f604adbdd7d","authPayload":"AUTHnonce1","authNonce":"nonce1"}` != msg { - t.Fatalf("[1] did not expect to receive msg: %s", msg) - } - wsService.Broadcast(`{"event":"auth","status":"OK","chanId":0,"userId":1,"subId":"nonce1","auth_id":"valid-auth-guid","caps":{"orders":{"read":1,"write":0},"account":{"read":1,"write":0},"funding":{"read":1,"write":0},"history":{"read":1,"write":0},"wallets":{"read":1,"write":0},"withdraw":{"read":0,"write":0},"positions":{"read":1,"write":0}}}`) - authEv, err := apiRecv.nextAuthEvent() - if err != nil { - t.Fatal(err) - } - expAuthEv := websocket.AuthEvent{ - Event: "auth", - Status: "OK", - ChanID: 0, - UserID: 1, - SubID: "nonce1", - AuthID: "valid-auth-guid", - } - assert(t, &expAuthEv, authEv) - - // subscriptions - // trade sub - _, err = apiClient.SubscribeTrades(context.Background(), "tBTCUSD") - if err != nil { - t.Fatal(err) - } - msg, err = wsService.WaitForMessage(0, 1) - if err != nil { - t.Fatal(err) - } - if `{"subId":"nonce2","event":"subscribe","channel":"trades","symbol":"tBTCUSD"}` != msg { - t.Fatalf("[2] did not expect to receive: %s", msg) - } - wsService.Broadcast(`{"event":"subscribed","channel":"trades","chanId":5,"symbol":"tBTCUSD","subId":"nonce2","pair":"BTCUSD"}`) - tradeSub, err := apiRecv.nextSubscriptionEvent() - if err != nil { - t.Fatal(err) - } - expTradeSub := websocket.SubscribeEvent{ - Symbol: "tBTCUSD", - SubID: "nonce2", - Channel: "trades", - } - assert(t, &expTradeSub, tradeSub) - - // book sub - _, err = apiClient.SubscribeBook(context.Background(), "tBTCUSD", bitfinex.Precision0, bitfinex.FrequencyRealtime, 25) - if err != nil { - t.Fatal(err) - } - msg, err = wsService.WaitForMessage(0, 2) - if err != nil { - t.Fatal(err) - } - if `{"subId":"nonce3","event":"subscribe","channel":"book","symbol":"tBTCUSD","prec":"P0","freq":"F0","len":"25"}` != msg { - t.Fatalf("[3] did not expect to receive: %s", msg) - } - wsService.Broadcast(`{"event":"subscribed","channel":"book","chanId":8,"symbol":"tBTCUSD","subId":"nonce3","pair":"BTCUSD","prec":"P0","freq":"F0","len":"25"}`) - bookSub, err := apiRecv.nextSubscriptionEvent() - if err != nil { - t.Fatal(err) - } - expBookSub := websocket.SubscribeEvent{ - Symbol: "tBTCUSD", - SubID: "nonce3", - Channel: "book", - Frequency: string(bitfinex.FrequencyRealtime), - Precision: string(bitfinex.Precision0), - } - assert(t, &expBookSub, bookSub) - - // abrupt disconnect - wsService.Stop() - - now := time.Now() - // wait for client disconnect to start reconnect looping - err = assertDisconnect(time.Second*10, apiClient) - if err != nil { - t.Fatal(err) - } - diff := time.Now().Sub(now) - t.Logf("client disconnect detected in %s", diff.String()) - - // recreate service - wsService = NewTestWsService(wsPort) - // fresh service, no clients - if wsService.TotalClientCount() != 0 { - t.Fatalf("total client count %d, expected non-zero", wsService.TotalClientCount()) - } - wsService.Start() - if err := wsService.WaitForClientCount(1); err != nil { - t.Fatal(err) - } - wsService.Broadcast(`{"event":"info","version":2}`) - infoEv, err = apiRecv.nextInfoEvent() - if err != nil { - t.Fatal(err) - } - assert(t, &expInfoEv, infoEv) - - // assert authentication again - msg, err = wsService.WaitForMessage(0, 0) - if err != nil { - t.Fatal(err) - } - if `{"subId":"nonce4","event":"auth","apiKey":"apiKey1","authSig":"3e424670c0fa4dcb293eea38b9fe62cca49cacc595da01a493d6b9328517a5c940b22141fecf16f653c2662b298238f4","authPayload":"AUTHnonce4","authNonce":"nonce4"}` != msg { - t.Fatalf("[4] did not expect to receive msg: %s", msg) - } - wsService.Broadcast(`{"event":"auth","status":"OK","chanId":0,"userId":1,"subId":"nonce4","auth_id":"valid-auth-guid","caps":{"orders":{"read":1,"write":0},"account":{"read":1,"write":0},"funding":{"read":1,"write":0},"history":{"read":1,"write":0},"wallets":{"read":1,"write":0},"withdraw":{"read":0,"write":0},"positions":{"read":1,"write":0}}}`) - authEv, err = apiRecv.nextAuthEvent() - if err != nil { - t.Fatal(err) - } - expAuthEv = websocket.AuthEvent{ - Event: "auth", - Status: "OK", - ChanID: 0, - UserID: 1, - SubID: "nonce4", - AuthID: "valid-auth-guid", - } - assert(t, &expAuthEv, authEv) - - // ensure client automatically resubscribes - msg, err = wsService.WaitForMessage(0, 1) - if err != nil { - t.Fatal(err) - } - if `{"subId":"nonce5","event":"subscribe","channel":"trades","symbol":"tBTCUSD"}` != msg { - t.Fatalf("[6] did not expect to receive: %s", msg) - } - wsService.Broadcast(`{"event":"subscribed","channel":"trades","chanId":5,"symbol":"tBTCUSD","subId":"nonce5","pair":"BTCUSD"}`) - tradeSub, err = apiRecv.nextSubscriptionEvent() - if err != nil { - t.Fatal(err) - } - expTradeSub = websocket.SubscribeEvent{ - Symbol: "tBTCUSD", - SubID: "nonce5", - Channel: "trades", - } - assert(t, &expTradeSub, tradeSub) - msg, err = wsService.WaitForMessage(0, 2) - if err != nil { - t.Fatal(err) - } - if `{"subId":"nonce6","event":"subscribe","channel":"book","symbol":"tBTCUSD","prec":"P0","freq":"F0","len":"25"}` != msg { - t.Fatalf("[5] did not expect to receive: %s", msg) - } - wsService.Broadcast(`{"event":"subscribed","channel":"book","chanId":8,"symbol":"tBTCUSD","subId":"nonce6","pair":"BTCUSD","prec":"P0","freq":"F0","len":"25"}`) - bookSub, err = apiRecv.nextSubscriptionEvent() - if err != nil { - t.Fatal(err) - } - expBookSub = websocket.SubscribeEvent{ - Symbol: "tBTCUSD", - SubID: "nonce6", - Channel: "book", - Frequency: string(bitfinex.FrequencyRealtime), - Precision: string(bitfinex.Precision0), - Len: "25", - } - assert(t, &expBookSub, bookSub) - - // API client thinks it's connected - if !apiClient.IsConnected() { - t.Fatal("not reconnected to websocket") - } -} - -func TestHeartbeatTimeoutNoReconnect(t *testing.T) { - // create transport & nonce mocks - setup(t, time.Second, false, false) - - // begin test - msg, err := apiRecv.nextInfoEvent() - if err != nil { - t.Fatal(err) - } - infoEv := websocket.InfoEvent{ - Version: 2, - } - assert(t, &infoEv, msg) - - _, err = apiClient.SubscribeTicker(context.Background(), "tBTCUSD") - if err != nil { - t.Fatal(err) - } - wsService.Broadcast(`{"event":"subscribed","channel":"ticker","chanId":5,"symbol":"tBTCUSD","subId":"nonce1","pair":"BTCUSD"}`) - - // expect timeout channel heartbeat - time.Sleep(time.Second * 2) - - if apiClient.IsConnected() { - t.Fatal("API client still connected, expected heartbeat disconnect") - } -} - -// also tests resubscribes -func TestHeartbeatTimeoutReconnect(t *testing.T) { - // create transport & nonce mocks - setup(t, time.Second, true, false) - - // begin test - // info msg automatically sends - msg, err := apiRecv.nextInfoEvent() - if err != nil { - t.Fatal(err) - } - infoEv := websocket.InfoEvent{ - Version: 2, - } - assert(t, &infoEv, msg) - - // use ticker sub to check for reconnect - _, err = apiClient.SubscribeTicker(context.Background(), "tBTCUSD") - if err != nil { - t.Fatal(err) - } - m, err := wsService.WaitForMessage(0, 0) - if err != nil { - t.Fatal(err) - } - if `{"subId":"nonce1","event":"subscribe","channel":"ticker","symbol":"tBTCUSD"}` != m { - t.Fatalf("[1] did not expect to receive: %s", m) - } - wsService.Broadcast(`{"event":"subscribed","channel":"ticker","chanId":5,"symbol":"tBTCUSD","subId":"nonce1","pair":"BTCUSD"}`) - tickerSub, err := apiRecv.nextSubscriptionEvent() - if err != nil { - t.Fatal(err) - } - expTickerSub := websocket.SubscribeEvent{ - Symbol: "tBTCUSD", - SubID: "nonce1", - Channel: "ticker", - } - assert(t, &expTickerSub, tickerSub) - - // expect timeout channel heartbeat - time.Sleep(time.Second * 2) - - // check reconnect subscriptions - m, err = wsService.WaitForMessage(0, 0) - if err != nil { - t.Fatal(err) - } - if `{"subId":"nonce2","event":"subscribe","channel":"ticker","symbol":"tBTCUSD"}` != m { - t.Fatalf("[2] did not expect to receive: %s", m) - } - wsService.Broadcast(`{"event":"subscribed","channel":"ticker","chanId":5,"symbol":"tBTCUSD","subId":"nonce2","pair":"BTCUSD"}`) - tickerSub, err = apiRecv.nextSubscriptionEvent() - if err != nil { - t.Fatal(err) - } - expTickerSub = websocket.SubscribeEvent{ - Symbol: "tBTCUSD", - SubID: "nonce2", - Channel: "ticker", - } - assert(t, &expTickerSub, tickerSub) -} - -func TestHeartbeatNoTimeoutData(t *testing.T) { - // create transport & nonce mocks - setup(t, time.Second, true, false) - - // begin test - // info msg automatically sends - msg, err := apiRecv.nextInfoEvent() - if err != nil { - t.Fatal(err) - } - infoEv := websocket.InfoEvent{ - Version: 2, - } - assert(t, &infoEv, msg) - - // use ticker sub to check for reconnect - _, err = apiClient.SubscribeTicker(context.Background(), "tBTCUSD") - if err != nil { - t.Fatal(err) - } - m, err := wsService.WaitForMessage(0, 0) - if err != nil { - t.Fatal(err) - } - if `{"subId":"nonce1","event":"subscribe","channel":"ticker","symbol":"tBTCUSD"}` != m { - t.Fatalf("[1] did not expect to receive: %s", m) - } - wsService.Broadcast(`{"event":"subscribed","channel":"ticker","chanId":5,"symbol":"tBTCUSD","subId":"nonce1","pair":"BTCUSD"}`) - tickerSub, err := apiRecv.nextSubscriptionEvent() - if err != nil { - t.Fatal(err) - } - expTickerSub := websocket.SubscribeEvent{ - Symbol: "tBTCUSD", - SubID: "nonce1", - Channel: "ticker", - } - assert(t, &expTickerSub, tickerSub) - - // would normally timeout here, but we can publish data to prevent - // the 1 second timeout - for i := 0; i < 8; i++ { - wsService.Broadcast(`[5,[14957,68.17328796,14958,55.29588132,-659,-0.0422,14971,53723.08813995,16494,14454]]`) - time.Sleep(time.Millisecond * 250) - } - - tick, err := apiRecv.nextTick() - if err != nil { - log.Fatal(err) - } - expTicker := bitfinex.Ticker{ - Symbol: "tBTCUSD", - Bid: 14957, - BidSize: 68.17328796, - Ask: 14958, - AskSize: 55.29588132, - DailyChange: -659, - DailyChangePerc: -0.0422, - LastPrice: 14971, - Volume: 53723.08813995, - High: 16494, - Low: 14454, - } - assert(t, &expTicker, tick) - - if !apiClient.IsConnected() { - t.Fatal("expected client connected, client has disconnected") - } -} diff --git a/tests/integration/v2/test_ws_service.go b/tests/integration/v2/test_ws_service.go deleted file mode 100644 index 45c15a1de..000000000 --- a/tests/integration/v2/test_ws_service.go +++ /dev/null @@ -1,218 +0,0 @@ -package tests - -import ( - "bytes" - "fmt" - "log" - "net" - "net/http" - "sync" - "time" - - "github.com/gorilla/websocket" -) - -var upgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, -} - -type client struct { - parent *TestWsService - *websocket.Conn - send chan []byte - received []string - lock sync.Mutex -} - -func (c *client) writePump() { - for msg := range c.send { - err := c.Conn.WriteMessage(websocket.TextMessage, msg) - if err != nil { - log.Printf("could not send message (%s) to client: %s", string(msg), err.Error()) - continue - } - } -} - -func (c *client) readPump() { - defer func() { - c.parent.unregister <- c - c.Conn.Close() - }() - for { - _, message, err := c.Conn.ReadMessage() - if err != nil { - if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { - log.Printf("error: %v", err) - } - log.Printf("test ws service drop client: %s", err.Error()) - break - } - message = bytes.TrimSpace(bytes.Replace(message, []byte("\n"), []byte(" "), -1)) - c.lock.Lock() - log.Printf("[DEBUG] WsClient -> WsService: %s", string(message)) - c.received = append(c.received, string(message)) - c.lock.Unlock() - } -} - -type TestWsService struct { - clients map[*client]bool - listener net.Listener - port int - - register chan *client - unregister chan *client - broadcast chan []byte - totalClients int - - publishOnConnect string -} - -func (s *TestWsService) WaitForClientCount(count int) error { - loops := 80 - delay := time.Millisecond * 50 - for i := 0; i < loops; i++ { - if s.totalClients == count { - return nil - } - time.Sleep(delay) - } - return fmt.Errorf("client peer #%d did not connect", count) -} - -func (s *TestWsService) TotalClientCount() int { - return s.totalClients -} - -func (s *TestWsService) PublishOnConnect(msg string) { - s.publishOnConnect = msg -} - -func NewTestWsService(port int) *TestWsService { - return &TestWsService{ - port: port, - clients: make(map[*client]bool), - register: make(chan *client), - unregister: make(chan *client), - broadcast: make(chan []byte), - } -} - -// Broadcast sends a message to all connected clients. -func (s *TestWsService) Broadcast(msg string) { - s.broadcast <- []byte(msg) -} - -// ReceivedCount starts indexing clients at position 0. -func (s *TestWsService) ReceivedCount(clientNum int) int { - i := 0 - for client := range s.clients { - if i == clientNum { - client.lock.Lock() - defer client.lock.Unlock() - return len(client.received) - } - i++ - } - return 0 -} - -// Received starts indexing clients and message positions at position 0. -func (s *TestWsService) Received(clientNum int, msgNum int) (string, error) { - var client *client - i := 0 - for client = range s.clients { - if i == clientNum { - break - } - i++ - } - if client != nil { - client.lock.Lock() - defer client.lock.Unlock() - if len(client.received) > msgNum { - return string(client.received[msgNum]), nil - } - return "", fmt.Errorf("could not find message index %d, %d messages exist", msgNum, len(client.received)) - } - return "", fmt.Errorf("could not find client %d", clientNum) -} - -func (s *TestWsService) WaitForMessage(clientNum int, msgNum int) (string, error) { - loops := 80 - delay := time.Millisecond * 50 - var msg string - var err error - for i := 0; i < loops; i++ { - msg, err = s.Received(clientNum, msgNum) - if err != nil { - time.Sleep(delay) - } else { - return msg, nil - } - } - return "", err -} - -func (s *TestWsService) ServeHTTP(w http.ResponseWriter, r *http.Request) { - s.serveWs(w, r) -} - -func (s *TestWsService) Stop() { - s.listener.Close() // stop listening to http - for c := range s.clients { - c.Close() - } -} - -func (s *TestWsService) Start() error { - go s.loop() - l, err := net.Listen("tcp", fmt.Sprintf(":%d", s.port)) - if err != nil { - return err - } - s.listener = l - go http.Serve(s.listener, s) - return nil -} - -func (s *TestWsService) serveWs(w http.ResponseWriter, r *http.Request) { - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - log.Print(err) - return - } - s.totalClients++ - client := &client{parent: s, Conn: conn, send: make(chan []byte, 256), received: make([]string, 0)} - go client.writePump() - go client.readPump() - s.clients[client] = true - if s.publishOnConnect != "" { - s.Broadcast(s.publishOnConnect) - } -} - -func (s *TestWsService) loop() { - for { - select { - case client := <-s.register: - s.clients[client] = true - case client := <-s.unregister: - if _, ok := s.clients[client]; ok { - delete(s.clients, client) - close(client.send) - } - case msg := <-s.broadcast: - for client := range s.clients { - select { - case client.send <- msg: - default: // send failure - close(client.send) - delete(s.clients, client) - } - } - } - } -} diff --git a/utils/nonce.go b/utils/nonce.go index b3792cb70..5734dab43 100644 --- a/utils/nonce.go +++ b/utils/nonce.go @@ -33,12 +33,7 @@ func NewEpochNonceGenerator() *EpochNonceGenerator { } // v1 support - -var nonce string - -func init() { - nonce = fmt.Sprintf("%v", time.Now().Unix()*10000) -} +const multiplier = 10000 // GetNonce is a naive nonce producer that takes the current Unix nano epoch // and counts upwards. @@ -46,5 +41,5 @@ func init() { // key and as such needs to be synchronised with other instances using the same // key in order to avoid race conditions. func GetNonce() string { - return nonce + return fmt.Sprintf("%v", time.Now().Unix() * multiplier) } diff --git a/v2/convert.go b/v2/convert.go deleted file mode 100644 index b95a03558..000000000 --- a/v2/convert.go +++ /dev/null @@ -1,82 +0,0 @@ -package bitfinex - -import ( - "fmt" -) - -func F64Slice(in []interface{}) ([]float64, error) { - var ret []float64 - for _, e := range in { - if item, ok := e.(float64); ok { - ret = append(ret, item) - } else { - return nil, fmt.Errorf("expected slice of float64 but got: %v", in) - } - } - - return ret, nil -} - -func i64ValOrZero(i interface{}) int64 { - if r, ok := i.(float64); ok { - return int64(r) - } - return 0 -} - -func iValOrZero(i interface{}) int { - if r, ok := i.(float64); ok { - return int(r) - } - return 0 -} - -func i64pValOrNil(i interface{}) *int64 { - if i == nil { - return nil - } - - if r, ok := i.(int64); ok { - return &r - } - return nil -} - -func ui64ValOrZero(i interface{}) uint64 { - if r, ok := i.(float64); ok { - return uint64(r) - } - return 0 -} - -func f64ValOrZero(i interface{}) float64 { - if r, ok := i.(float64); ok { - return r - } - return 0.0 -} - -func f64pValOrNil(i interface{}) *float64 { - if i == nil { - return nil - } - - if r, ok := i.(float64); ok { - return &r - } - return nil -} - -func bValOrFalse(i interface{}) bool { - if r, ok := i.(bool); ok { - return r - } - return false -} - -func sValOrEmpty(i interface{}) string { - if r, ok := i.(string); ok { - return r - } - return "" -} diff --git a/v2/pairs.go b/v2/pairs.go deleted file mode 100644 index d09d164ec..000000000 --- a/v2/pairs.go +++ /dev/null @@ -1,32 +0,0 @@ -package bitfinex - -// Available pairs -const ( - BTCUSD = "BTCUSD" - LTCUSD = "LTCUSD" - LTCBTC = "LTCBTC" - ETHUSD = "ETHUSD" - ETHBTC = "ETHBTC" - ETCUSD = "ETCUSD" - ETCBTC = "ETCBTC" - BFXUSD = "BFXUSD" - BFXBTC = "BFXBTC" - ZECUSD = "ZECUSD" - ZECBTC = "ZECBTC" - XMRUSD = "XMRUSD" - XMRBTC = "XMRBTC" - RRTUSD = "RRTUSD" - RRTBTC = "RRTBTC" - XRPUSD = "XRPUSD" - XRPBTC = "XRPBTC" - EOSETH = "EOSETH" - EOSUSD = "EOSUSD" - EOSBTC = "EOSBTC" - IOTUSD = "IOTUSD" - IOTBTC = "IOTBTC" - IOTETH = "IOTETH" - BCCBTC = "BCCBTC" - BCUBTC = "BCUBTC" - BCCUSD = "BCCUSD" - BCUUSD = "BCUUSD" -) diff --git a/v2/rest/book.go b/v2/rest/book.go deleted file mode 100644 index 5374a7e32..000000000 --- a/v2/rest/book.go +++ /dev/null @@ -1,43 +0,0 @@ -package rest - -import ( - "github.com/bitfinexcom/bitfinex-api-go/v2" - "net/url" - "path" - "strconv" -) - -type BookService struct { - Synchronous -} - -func (b *BookService) All(symbol string, precision bitfinex.BookPrecision, priceLevels int) (*bitfinex.BookUpdateSnapshot, error) { - req := NewRequestWithMethod(path.Join("book", symbol, string(precision)), "GET") - req.Params = make(url.Values) - req.Params.Add("len", strconv.Itoa(priceLevels)) - raw, err := b.Request(req) - - if err != nil { - return nil, err - } - - data := make([][]float64, 0, len(raw)) - for _, ifacearr := range raw { - if arr, ok := ifacearr.([]interface{}); ok { - sub := make([]float64, 0, len(arr)) - for _, iface := range arr { - if flt, ok := iface.(float64); ok { - sub = append(sub, flt) - } - } - data = append(data, sub) - } - } - - book, err := bitfinex.NewBookUpdateSnapshotFromRaw(symbol, string(precision), data) - if err != nil { - return nil, err - } - - return book, nil -} diff --git a/v2/rest/book_test.go b/v2/rest/book_test.go deleted file mode 100644 index 47bda2468..000000000 --- a/v2/rest/book_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package rest - -import ( - "bytes" - "github.com/bitfinexcom/bitfinex-api-go/v2" - "io/ioutil" - "net/http" - "testing" -) - -func TestBookAll(t *testing.T) { - httpDo := func(_ *http.Client, req *http.Request) (*http.Response, error) { - msg := `[[10579,1,0.0329596],[10578,1,0.11030234],[10577,2,0.11890895],[10576,2,1.0427],[10574,2,0.98962806],[10573,1,0.9443],[10572,1,0.06824617],[10571,1,0.42609023],[10570,1,0.002],[10569,2,0.99085269],[10568,3,2.1616],[10567,1,0.49990559],[10566,1,0.5],[10565,1,0.5413],[10564,2,0.99990599],[10563,2,0.28270321],[10561,2,0.99896343],[10560,1,0.498983],[10559,3,1.43741793],[10558,4,1.17],[10557,2,2.42],[10556,3,4.25833255],[10555,4,6.472],[10554,1,0.2],[10553,2,0.06940968],[10580,3,-4.5235],[10581,1,-0.9452],[10584,2,-0.46850263],[10585,1,-0.01],[10586,2,-0.93153],[10587,3,-0.82382839],[10589,2,-0.56565545],[10590,2,-0.43420271],[10592,1,-0.1],[10593,3,-2.1],[10594,3,-19.47635006],[10595,4,-7.352],[10596,1,-1.5],[10597,1,-4.5],[10598,1,-2.96],[10600,3,-0.41500001],[10601,1,-0.02835606],[10606,3,-0.28310301],[10607,2,-0.99729895],[10608,3,-0.25],[10609,3,-2.04831264],[10610,1,-0.05],[10613,2,-2],[10614,1,-0.3],[10615,1,-0.002]]` - resp := http.Response{ - Body: ioutil.NopCloser(bytes.NewBufferString(msg)), - StatusCode: 200, - } - return &resp, nil - } - - book, err := NewClientWithHttpDo(httpDo).Book.All("tBTCUSD", bitfinex.Precision0, 25) - - if err != nil { - t.Fatal(err) - } - - if len(book.Snapshot) != 50 { - t.Fatalf("expected 50 book update entries in snapshot, but got %d", len(book.Snapshot)) - } -} diff --git a/v2/rest/client.go b/v2/rest/client.go deleted file mode 100644 index 640691198..000000000 --- a/v2/rest/client.go +++ /dev/null @@ -1,250 +0,0 @@ -package rest - -import ( - "crypto/hmac" - "crypto/sha512" - "encoding/hex" - "encoding/json" - "fmt" - "github.com/bitfinexcom/bitfinex-api-go/utils" - "io" - "io/ioutil" - "net/http" - "net/url" -) - -var productionBaseURL = "https://api.bitfinex.com/v2/" - -type requestFactory interface { - NewAuthenticatedRequestWithData(refURL string, data map[string]interface{}) (Request, error) - NewAuthenticatedRequest(refURL string) (Request, error) -} - -type Synchronous interface { - Request(request Request) ([]interface{}, error) -} - -type Client struct { - // base members for synchronous API - apiKey string - apiSecret string - nonce utils.NonceGenerator - - // service providers - Orders OrderService - Positions PositionService - Trades TradeService - Platform PlatformService - Book BookService - - Synchronous -} - -func NewClient() *Client { - return NewClientWithURLNonce(productionBaseURL, utils.NewEpochNonceGenerator()) -} - -func NewClientWithURLNonce(url string, nonce utils.NonceGenerator) *Client { - httpDo := func(c *http.Client, req *http.Request) (*http.Response, error) { - return c.Do(req) - } - return NewClientWithURLHttpDoNonce(url, httpDo, nonce) -} - -func NewClientWithHttpDo(httpDo func(c *http.Client, r *http.Request) (*http.Response, error)) *Client { - return NewClientWithURLHttpDo(productionBaseURL, httpDo) -} - -func NewClientWithURLHttpDo(base string, httpDo func(c *http.Client, r *http.Request) (*http.Response, error)) *Client { - return NewClientWithURLHttpDoNonce(base, httpDo, utils.NewEpochNonceGenerator()) -} - -func NewClientWithURLHttpDoNonce(base string, httpDo func(c *http.Client, r *http.Request) (*http.Response, error), nonce utils.NonceGenerator) *Client { - url, _ := url.Parse(base) - sync := &HttpTransport{ - BaseURL: url, - httpDo: httpDo, - HTTPClient: http.DefaultClient, - } - return NewClientWithSynchronousNonce(sync, nonce) -} - -func NewClientWithURL(url string) *Client { - httpDo := func(c *http.Client, req *http.Request) (*http.Response, error) { - return c.Do(req) - } - return NewClientWithURLHttpDo(url, httpDo) -} - -func NewClientWithSynchronousNonce(sync Synchronous, nonce utils.NonceGenerator) *Client { - return NewClientWithSynchronousURLNonce(sync, productionBaseURL, nonce) -} - -// mock me in tests -func NewClientWithSynchronousURLNonce(sync Synchronous, url string, nonce utils.NonceGenerator) *Client { - c := &Client{ - Synchronous: sync, - nonce: nonce, - } - c.Orders = OrderService{Synchronous: c, requestFactory: c} - c.Book = BookService{Synchronous: c} - c.Trades = TradeService{Synchronous: c, requestFactory: c} - c.Platform = PlatformService{Synchronous: c} - c.Positions = PositionService{Synchronous: c, requestFactory: c} - return c -} - -func (c *Client) Credentials(key string, secret string) *Client { - c.apiKey = key - c.apiSecret = secret - return c -} - -// Request is a wrapper for standard http.Request. Default method is POST with no data. -type Request struct { - RefURL string // ref url - Data map[string]interface{} // body data - Method string // http method - Params url.Values // query parameters - Headers map[string]string -} - -// Response is a wrapper for standard http.Response and provides more methods. -type Response struct { - Response *http.Response - Body []byte -} - -func (c *Client) sign(msg string) string { - sig := hmac.New(sha512.New384, []byte(c.apiSecret)) - sig.Write([]byte(msg)) - return hex.EncodeToString(sig.Sum(nil)) -} - -func (c *Client) NewAuthenticatedRequest(refURL string) (Request, error) { - return c.NewAuthenticatedRequestWithData(refURL, nil) -} - -func (c *Client) NewAuthenticatedRequestWithData(refURL string, data map[string]interface{}) (Request, error) { - authURL := "auth/r/" + refURL - req := NewRequestWithData(authURL, data) - nonce := c.nonce.GetNonce() - b, err := json.Marshal(data) - if err != nil { - return Request{}, err - } - msg := "/api/v2/" + authURL + nonce - if data != nil { - msg += string(b) - } else { - msg += "{}" - } - req.Headers["Content-Type"] = "application/json" - req.Headers["Accept"] = "application/json" - req.Headers["bfx-nonce"] = nonce - req.Headers["bfx-signature"] = c.sign(msg) - req.Headers["bfx-apikey"] = c.apiKey - return req, nil -} - -func NewRequest(refURL string) Request { - return NewRequestWithDataMethod(refURL, nil, "POST") -} - -func NewRequestWithMethod(refURL string, method string) Request { - return NewRequestWithDataMethod(refURL, nil, method) -} - -func NewRequestWithData(refURL string, data map[string]interface{}) Request { - return NewRequestWithDataMethod(refURL, data, "POST") -} - -func NewRequestWithDataMethod(refURL string, data map[string]interface{}, method string) Request { - return Request{ - RefURL: refURL, - Data: data, - Method: method, - Headers: make(map[string]string), - } -} - -// newResponse creates new wrapper. -func newResponse(r *http.Response) *Response { - // Use a LimitReader of arbitrary size (here ~8.39MB) to prevent us from - // reading overly large response bodies. - lr := io.LimitReader(r.Body, 8388608) - body, err := ioutil.ReadAll(lr) - if err != nil { - body = []byte(`Error reading body:` + err.Error()) - } - - return &Response{r, body} -} - -// String converts response body to string. -// An empty string will be returned if error. -func (r *Response) String() string { - return string(r.Body) -} - -// checkResponse checks response status code and response -// for errors. -func checkResponse(r *Response) error { - if c := r.Response.StatusCode; 200 <= c && c <= 299 { - return nil - } - - var raw []interface{} - // Try to decode error message - errorResponse := &ErrorResponse{Response: r} - err := json.Unmarshal(r.Body, &raw) - if err != nil { - errorResponse.Message = "Error decoding response error message. " + - "Please see response body for more information." - return errorResponse - } - - if len(raw) < 3 { - errorResponse.Message = fmt.Sprintf("Expected response to have three elements but got %#v", raw) - return errorResponse - } - - if str, ok := raw[0].(string); !ok || str != "error" { - errorResponse.Message = fmt.Sprintf("Expected first element to be \"error\" but got %#v", raw) - return errorResponse - } - - code, ok := raw[1].(float64) - if !ok { - errorResponse.Message = fmt.Sprintf("Expected second element to be error code but got %#v", raw) - return errorResponse - } - errorResponse.Code = int(code) - - msg, ok := raw[2].(string) - if !ok { - errorResponse.Message = fmt.Sprintf("Expected third element to be error message but got %#v", raw) - return errorResponse - } - errorResponse.Message = msg - - return errorResponse -} - -// In case if API will wrong response code -// ErrorResponse will be returned to caller -type ErrorResponse struct { - Response *Response - Message string `json:"message"` - Code int `json:"code"` -} - -func (r *ErrorResponse) Error() string { - return fmt.Sprintf("%v %v: %d %v (%d)", - r.Response.Response.Request.Method, - r.Response.Response.Request.URL, - r.Response.Response.StatusCode, - r.Message, - r.Code, - ) -} diff --git a/v2/rest/orders.go b/v2/rest/orders.go deleted file mode 100644 index b9b908c31..000000000 --- a/v2/rest/orders.go +++ /dev/null @@ -1,93 +0,0 @@ -package rest - -import ( - "fmt" - "github.com/bitfinexcom/bitfinex-api-go/v2" - "path" -) - -// OrderService manages data flow for the Order API endpoint -type OrderService struct { - requestFactory - Synchronous -} - -// All returns all orders for the authenticated account. -func (s *OrderService) All(symbol string) (*bitfinex.OrderSnapshot, error) { - req, err := s.requestFactory.NewAuthenticatedRequest(path.Join("orders", symbol)) - if err != nil { - return nil, err - } - raw, err := s.Request(req) - if err != nil { - return nil, err - } - - os, err := bitfinex.NewOrderSnapshotFromRaw(raw) - if err != nil { - return nil, err - } - - return os, nil -} - -// Status retrieves the given order from the API. This is just a wrapper around -// the All() method, since the API does not provide lookup for a single Order. -func (s *OrderService) Status(orderID int64) (o *bitfinex.Order, err error) { - os, err := s.All("") - - if err != nil { - return o, err - } - - if len(os.Snapshot) == 0 { - return o, bitfinex.ErrNotFound - } - - for _, e := range os.Snapshot { - if e.ID == orderID { - return e, nil - } - } - - return o, bitfinex.ErrNotFound -} - -// All returns all orders for the authenticated account. -func (s *OrderService) History(symbol string) (*bitfinex.OrderSnapshot, error) { - if symbol == "" { - return nil, fmt.Errorf("symbol cannot be empty") - } - req, err := s.requestFactory.NewAuthenticatedRequest(path.Join("orders", symbol, "hist")) - if err != nil { - return nil, err - } - raw, err := s.Request(req) - if err != nil { - return nil, err - } - - os, err := bitfinex.NewOrderSnapshotFromRaw(raw) - if err != nil { - return nil, err - } - - return os, nil -} - -// OrderTrades returns a set of executed trades related to an order. -func (s *OrderService) OrderTrades(symbol string, orderID int64) (*bitfinex.TradeExecutionUpdateSnapshot, error) { - if symbol == "" { - return nil, fmt.Errorf("symbol cannot be empty") - } - key := fmt.Sprintf("%s:%d", symbol, orderID) - req, err := s.requestFactory.NewAuthenticatedRequest(path.Join("order", key, "trades")) - if err != nil { - return nil, err - } - raw, err := s.Request(req) - if err != nil { - return nil, err - } - return bitfinex.NewTradeExecutionUpdateSnapshotFromRaw(raw) -} diff --git a/v2/rest/orders_test.go b/v2/rest/orders_test.go deleted file mode 100644 index e04b6a9a4..000000000 --- a/v2/rest/orders_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package rest - -import ( - "bytes" - "io/ioutil" - "net/http" - "testing" - - "github.com/bitfinexcom/bitfinex-api-go/v2" -) - -func TestOrdersAll(t *testing.T) { - httpDo := func(_ *http.Client, req *http.Request) (*http.Response, error) { - msg := ` - [ - [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], - [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], - [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] - ]` - resp := http.Response{ - Body: ioutil.NopCloser(bytes.NewBufferString(msg)), - StatusCode: 200, - } - return &resp, nil - } - - orders, err := NewClientWithHttpDo(httpDo).Orders.All("") - - if err != nil { - t.Error(err) - } - - if len(orders.Snapshot) != 3 { - t.Fatalf("expected three orders but got %d", len(orders.Snapshot)) - } -} - -func TestOrdersHistory(t *testing.T) { - httpDo := func(_ *http.Client, req *http.Request) (*http.Response, error) { - msg := ` - [ - [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], - [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], - [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] - ]` - resp := http.Response{ - Body: ioutil.NopCloser(bytes.NewBufferString(msg)), - StatusCode: 200, - } - return &resp, nil - } - - orders, err := NewClientWithHttpDo(httpDo).Orders.History(bitfinex.TradingPrefix + bitfinex.IOTBTC) - - if err != nil { - t.Error(err) - } - - if len(orders.Snapshot) != 3 { - t.Errorf("expected three orders but got %d", len(orders.Snapshot)) - } - - _, err = NewClient().Orders.History("") - if err == nil { - t.Errorf("expected error when supplying empty symbol but got none") - } -} diff --git a/v2/rest/platform_status.go b/v2/rest/platform_status.go deleted file mode 100644 index 8d31ee58c..000000000 --- a/v2/rest/platform_status.go +++ /dev/null @@ -1,22 +0,0 @@ -package rest - -type PlatformService struct { - Synchronous -} - -// Status indicates whether the platform is currently operative or not. -func (p *PlatformService) Status() (bool, error) { - raw, err := p.Request(NewRequestWithMethod("platform/status", "GET")) - - if err != nil { - return false, err - } - /* - // raw is an interface type, but we only care about len & index 0 - s := make([]int, len(raw)) - for i, v := range raw { - s[i] = v.(int) - } - */ - return len(raw) > 0 && raw[0].(float64) == 1, nil -} diff --git a/v2/rest/positions.go b/v2/rest/positions.go deleted file mode 100644 index 1eff6fabf..000000000 --- a/v2/rest/positions.go +++ /dev/null @@ -1,31 +0,0 @@ -package rest - -import ( - "github.com/bitfinexcom/bitfinex-api-go/v2" -) - -// PositionService manages the Position endpoint. -type PositionService struct { - requestFactory - Synchronous -} - -// All returns all positions for the authenticated account. -func (s *PositionService) All() (*bitfinex.PositionSnapshot, error) { - req, err := s.requestFactory.NewAuthenticatedRequest("positions") - if err != nil { - return nil, err - } - raw, err := s.Request(req) - - if err != nil { - return nil, err - } - - os, err := bitfinex.NewPositionSnapshotFromRaw(raw) - if err != nil { - return nil, err - } - - return os, nil -} diff --git a/v2/rest/trades.go b/v2/rest/trades.go deleted file mode 100644 index 238d4097a..000000000 --- a/v2/rest/trades.go +++ /dev/null @@ -1,38 +0,0 @@ -package rest - -import ( - "github.com/bitfinexcom/bitfinex-api-go/v2" - "path" -) - -// TradeService manages the Trade endpoint. -type TradeService struct { - requestFactory - Synchronous -} - -// All returns all orders for the authenticated account. -func (s *TradeService) All(symbol string) (*bitfinex.TradeSnapshot, error) { - req, err := s.requestFactory.NewAuthenticatedRequestWithData(path.Join("trades", symbol, "hist"), map[string]interface{}{"start": nil, "end": nil, "limit": nil}) - if err != nil { - return nil, err - } - raw, err := s.Request(req) - - if err != nil { - return nil, err - } - - dat := make([][]float64, 0) - for _, r := range raw { - if f, ok := r.([]float64); ok { - dat = append(dat, f) - } - } - - os, err := bitfinex.NewTradeSnapshotFromRaw(symbol, dat) - if err != nil { - return nil, err - } - return os, nil -} diff --git a/v2/rest/transport.go b/v2/rest/transport.go deleted file mode 100644 index d91efb429..000000000 --- a/v2/rest/transport.go +++ /dev/null @@ -1,82 +0,0 @@ -package rest - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - "net/url" -) - -type HttpTransport struct { - BaseURL *url.URL - HTTPClient *http.Client - httpDo func(c *http.Client, req *http.Request) (*http.Response, error) -} - -func (h HttpTransport) Request(req Request) ([]interface{}, error) { - var raw []interface{} - - rel, err := url.Parse(req.RefURL) - if err != nil { - return nil, err - } - if req.Params != nil { - rel.RawQuery = req.Params.Encode() - } - if req.Data == nil { - req.Data = map[string]interface{}{} - } - - b, err := json.Marshal(req.Data) - if err != nil { - return nil, err - } - - body := bytes.NewReader(b) - - u := h.BaseURL.ResolveReference(rel) - httpReq, err := http.NewRequest(req.Method, u.String(), body) - for k, v := range req.Headers { - httpReq.Header.Add(k, v) - } - if err != nil { - return nil, err - } - - resp, err := h.do(httpReq, &raw) - if err != nil { - if resp != nil { - return nil, fmt.Errorf("%v", err) - } else { - return nil, fmt.Errorf("could not parse response: %s", resp.Response.Status) - } - - } - - return raw, nil -} - -// Do executes API request created by NewRequest method or custom *http.Request. -func (h HttpTransport) do(req *http.Request, v interface{}) (*Response, error) { - resp, err := h.httpDo(h.HTTPClient, req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - response := newResponse(resp) - err = checkResponse(response) - if err != nil { - return response, err - } - - if v != nil { - err = json.Unmarshal(response.Body, v) - if err != nil { - return response, err - } - } - - return response, nil -} diff --git a/v2/types.go b/v2/types.go deleted file mode 100644 index 781495e6b..000000000 --- a/v2/types.go +++ /dev/null @@ -1,1423 +0,0 @@ -package bitfinex - -import ( - "encoding/json" - "errors" - "fmt" - "log" - "math" -) - -// Prefixes for available pairs -const ( - FundingPrefix = "f" - TradingPrefix = "t" -) - -var ( - ErrNotFound = errors.New("not found") -) - -// Candle resolutions -const ( - OneMinute CandleResolution = "1m" - FiveMinutes CandleResolution = "5m" - FifteenMinutes CandleResolution = "15m" - ThirtyMinutes CandleResolution = "30m" - OneHour CandleResolution = "1h" - ThreeHours CandleResolution = "3h" - SixHours CandleResolution = "6h" - TwelveHours CandleResolution = "12h" - OneDay CandleResolution = "1D" - OneWeek CandleResolution = "7D" - TwoWeeks CandleResolution = "14D" - OneMonth CandleResolution = "1M" -) - -func CandleResolutionFromString(str string) (CandleResolution, error) { - switch str { - case string(OneMinute): - return OneMinute, nil - case string(FiveMinutes): - return FiveMinutes, nil - case string(FifteenMinutes): - return FifteenMinutes, nil - case string(ThirtyMinutes): - return ThirtyMinutes, nil - case string(OneHour): - return OneHour, nil - case string(ThreeHours): - return ThreeHours, nil - case string(SixHours): - return SixHours, nil - case string(TwelveHours): - return TwelveHours, nil - case string(OneDay): - return OneDay, nil - case string(OneWeek): - return OneWeek, nil - case string(TwoWeeks): - return TwoWeeks, nil - case string(OneMonth): - return OneMonth, nil - } - return OneMinute, fmt.Errorf("could not convert string to resolution: %s", str) -} - -// private type--cannot instantiate. -type candleResolution string - -// CandleResolution provides a typed set of resolutions for candle subscriptions. -type CandleResolution candleResolution - -// Order sides -const ( - Bid OrderSide = 1 - Ask OrderSide = 2 -) - -type orderSide byte - -// OrderSide provides a typed set of order sides. -type OrderSide orderSide - -// Book precision levels -const ( - // Aggregate precision levels - Precision0 BookPrecision = "P0" - Precision2 BookPrecision = "P2" - Precision1 BookPrecision = "P1" - Precision3 BookPrecision = "P3" - // Raw precision - PrecisionRawBook BookPrecision = "R0" -) - -// private type -type bookPrecision string - -// BookPrecision provides a typed book precision level. -type BookPrecision bookPrecision - -const ( - // FrequencyRealtime book frequency gives updates as they occur in real-time. - FrequencyRealtime BookFrequency = "F0" - // FrequencyTwoPerSecond delivers two book updates per second. - FrequencyTwoPerSecond BookFrequency = "F1" - // PriceLevelDefault provides a constant default price level for book subscriptions. - PriceLevelDefault int = 25 -) - -type bookFrequency string - -// BookFrequency provides a typed book frequency. -type BookFrequency bookFrequency - -const ( - OrderFlagHidden int = 64 - OrderFlagClose = 512 - OrderFlagPostOnly = 4096 - OrderFlagOCO = 16384 -) - -// OrderNewRequest represents an order to be posted to the bitfinex websocket -// service. -type OrderNewRequest struct { - GID int64 `json:"gid"` - CID int64 `json:"cid"` - Type string `json:"type"` - Symbol string `json:"symbol"` - Amount float64 `json:"amount,string"` - Price float64 `json:"price,string"` - PriceTrailing float64 `json:"price_trailing,string,omitempty"` - PriceAuxLimit float64 `json:"price_aux_limit,string,omitempty"` - Hidden bool `json:"hidden,omitempty"` - PostOnly bool `json:"postonly,omitempty"` -} - -// MarshalJSON converts the order object into the format required by the bitfinex -// websocket service. -func (o *OrderNewRequest) MarshalJSON() ([]byte, error) { - aux := struct { - GID int64 `json:"gid"` - CID int64 `json:"cid"` - Type string `json:"type"` - Symbol string `json:"symbol"` - Amount float64 `json:"amount,string"` - Price float64 `json:"price,string"` - PriceTrailing float64 `json:"price_trailing,string,omitempty"` - PriceAuxLimit float64 `json:"price_aux_limit,string,omitempty"` - Flags int `json:"flags,omitempty"` - }{ - GID: o.GID, - CID: o.CID, - Type: o.Type, - Symbol: o.Symbol, - Amount: o.Amount, - Price: o.Price, - PriceTrailing: o.PriceTrailing, - PriceAuxLimit: o.PriceAuxLimit, - } - - if o.Hidden { - aux.Flags = aux.Flags + OrderFlagHidden - } - - if o.PostOnly { - aux.Flags = aux.Flags + OrderFlagPostOnly - } - - body := []interface{}{0, "on", nil, aux} - return json.Marshal(&body) -} - -// OrderCancelRequest represents an order cancel request. -// An order can be cancelled using the internal ID or a -// combination of Client ID (CID) and the daten for the given -// CID. -type OrderCancelRequest struct { - ID int64 `json:"id,omitempty"` - CID int64 `json:"cid,omitempty"` - CIDDate string `json:"cid_date,omitempty"` -} - -// MarshalJSON converts the order cancel object into the format required by the -// bitfinex websocket service. -func (o *OrderCancelRequest) MarshalJSON() ([]byte, error) { - aux := struct { - ID int64 `json:"id,omitempty"` - CID int64 `json:"cid,omitempty"` - CIDDate string `json:"cid_date,omitempty"` - }{ - ID: o.ID, - CID: o.CID, - CIDDate: o.CIDDate, - } - - body := []interface{}{0, "oc", nil, aux} - return json.Marshal(&body) -} - -// TODO: MultiOrderCancelRequest represents an order cancel request. - -type Heartbeat struct { - //ChannelIDs []int64 -} - -// OrderType represents the types orders the bitfinex platform can handle. -type OrderType string - -const ( - OrderTypeMarket = "MARKET" - OrderTypeExchangeMarket = "EXCHANGE MARKET" - OrderTypeLimit = "LIMIT" - OrderTypeExchangeLimit = "EXCHANGE LIMIT" - OrderTypeStop = "STOP" - OrderTypeExchangeStop = "EXCHANGE STOP" - OrderTypeTrailingStop = "TRAILING STOP" - OrderTypeExchangeTrailingStop = "EXCHANGE TRAILING STOP" - OrderTypeFOK = "FOK" - OrderTypeExchangeFOK = "EXCHANGE FOK" - OrderTypeStopLimit = "STOP LIMIT" - OrderTypeExchangeStopLimit = "EXCHANGE STOP LIMIT" -) - -// OrderStatus represents the possible statuses an order can be in. -type OrderStatus string - -const ( - OrderStatusActive OrderStatus = "ACTIVE" - OrderStatusExecuted OrderStatus = "EXECUTED" - OrderStatusPartiallyFilled OrderStatus = "PARTIALLY FILLED" - OrderStatusCanceled OrderStatus = "CANCELED" -) - -// Order as returned from the bitfinex websocket service. -type Order struct { - ID int64 - GID int64 - CID int64 - Symbol string - MTSCreated int64 - MTSUpdated int64 - Amount float64 - AmountOrig float64 - Type string - TypePrev string - Flags int64 - Status OrderStatus - Price float64 - PriceAvg float64 - PriceTrailing float64 - PriceAuxLimit float64 - Notify bool - Hidden bool - PlacedID int64 -} - -// NewOrderFromRaw takes the raw list of values as returned from the websocket -// service and tries to convert it into an Order. -func NewOrderFromRaw(raw []interface{}) (o *Order, err error) { - if len(raw) == 12 { - o = &Order{ - ID: int64(f64ValOrZero(raw[0])), - Symbol: sValOrEmpty(raw[1]), - Amount: f64ValOrZero(raw[2]), - AmountOrig: f64ValOrZero(raw[3]), - Type: sValOrEmpty(raw[4]), - Status: OrderStatus(sValOrEmpty(raw[5])), - Price: f64ValOrZero(raw[6]), - PriceAvg: f64ValOrZero(raw[7]), - MTSUpdated: i64ValOrZero(raw[8]), - // 3 trailing zeroes, what do they map to? - } - } else if len(raw) < 26 { - return o, fmt.Errorf("data slice too short for order: %#v", raw) - } else { - // TODO: API docs say ID, GID, CID, MTS_CREATE, MTS_UPDATE are int but API returns float - o = &Order{ - ID: int64(f64ValOrZero(raw[0])), - GID: int64(f64ValOrZero(raw[1])), - CID: int64(f64ValOrZero(raw[2])), - Symbol: sValOrEmpty(raw[3]), - MTSCreated: int64(f64ValOrZero(raw[4])), - MTSUpdated: int64(f64ValOrZero(raw[5])), - Amount: f64ValOrZero(raw[6]), - AmountOrig: f64ValOrZero(raw[7]), - Type: sValOrEmpty(raw[8]), - TypePrev: sValOrEmpty(raw[9]), - Flags: i64ValOrZero(raw[12]), - Status: OrderStatus(sValOrEmpty(raw[13])), - Price: f64ValOrZero(raw[16]), - PriceAvg: f64ValOrZero(raw[17]), - PriceTrailing: f64ValOrZero(raw[18]), - PriceAuxLimit: f64ValOrZero(raw[19]), - Notify: bValOrFalse(raw[23]), - Hidden: bValOrFalse(raw[24]), - PlacedID: i64ValOrZero(raw[25]), - } - } - - return -} - -// OrderSnapshotFromRaw takes a raw list of values as returned from the websocket -// service and tries to convert it into an OrderSnapshot. -func NewOrderSnapshotFromRaw(raw []interface{}) (s *OrderSnapshot, err error) { - if len(raw) == 0 { - return - } - - os := make([]*Order, 0) - switch raw[0].(type) { - case []interface{}: - for _, v := range raw { - if l, ok := v.([]interface{}); ok { - o, err := NewOrderFromRaw(l) - if err != nil { - return s, err - } - os = append(os, o) - } - } - default: - return s, fmt.Errorf("not an order snapshot") - } - s = &OrderSnapshot{Snapshot: os} - - return -} - -// OrderSnapshot is a collection of Orders that would usually be sent on -// inital connection. -type OrderSnapshot struct { - Snapshot []*Order -} - -// OrderUpdate is an Order that gets sent out after every change to an -// order. -type OrderUpdate Order - -// OrderNew gets sent out after an Order was created successfully. -type OrderNew Order - -// OrderCancel gets sent out after an Order was cancelled successfully. -type OrderCancel Order - -type PositionStatus string - -const ( - PositionStatusActive PositionStatus = "ACTIVE" - PositionStatusClosed PositionStatus = "CLOSED" -) - -type Position struct { - Symbol string - Status PositionStatus - Amount float64 - BasePrice float64 - MarginFunding float64 - MarginFundingType int64 - ProfitLoss float64 - ProfitLossPercentage float64 - LiquidationPrice float64 - Leverage float64 -} - -func NewPositionFromRaw(raw []interface{}) (o *Position, err error) { - if len(raw) == 6 { - o = &Position{ - Symbol: sValOrEmpty(raw[0]), - Status: PositionStatus(sValOrEmpty(raw[1])), - Amount: f64ValOrZero(raw[2]), - BasePrice: f64ValOrZero(raw[3]), - MarginFunding: f64ValOrZero(raw[4]), - MarginFundingType: i64ValOrZero(raw[5]), - } - } else if len(raw) < 10 { - return o, fmt.Errorf("data slice too short for position: %#v", raw) - } else { - o = &Position{ - Symbol: sValOrEmpty(raw[0]), - Status: PositionStatus(sValOrEmpty(raw[1])), - Amount: f64ValOrZero(raw[2]), - BasePrice: f64ValOrZero(raw[3]), - MarginFunding: f64ValOrZero(raw[4]), - MarginFundingType: i64ValOrZero(raw[5]), - ProfitLoss: f64ValOrZero(raw[6]), - ProfitLossPercentage: f64ValOrZero(raw[7]), - LiquidationPrice: f64ValOrZero(raw[8]), - Leverage: f64ValOrZero(raw[9]), - } - } - return -} - -type PositionSnapshot struct { - Snapshot []*Position -} -type PositionNew Position -type PositionUpdate Position -type PositionCancel Position - -func NewPositionSnapshotFromRaw(raw []interface{}) (s *PositionSnapshot, err error) { - if len(raw) == 0 { - return - } - - ps := make([]*Position, 0) - switch raw[0].(type) { - case []interface{}: - for _, v := range raw { - if l, ok := v.([]interface{}); ok { - p, err := NewPositionFromRaw(l) - if err != nil { - return s, err - } - ps = append(ps, p) - } - } - default: - return s, fmt.Errorf("not a position snapshot") - } - s = &PositionSnapshot{Snapshot: ps} - - return -} - -// Trade represents a trade on the public data feed. -type Trade struct { - Pair string - ID int64 - MTS int64 - Amount float64 - Price float64 - Side OrderSide -} - -func NewTradeFromRaw(pair string, raw []interface{}) (o *Trade, err error) { - if len(raw) < 4 { - return o, fmt.Errorf("data slice too short for trade: %#v", raw) - } - - amt := f64ValOrZero(raw[2]) - var side OrderSide - if amt > 0 { - side = Bid - } else { - side = Ask - } - - o = &Trade{ - Pair: pair, - ID: i64ValOrZero(raw[0]), - MTS: i64ValOrZero(raw[1]), - Amount: math.Abs(amt), - Price: f64ValOrZero(raw[3]), - Side: side, - } - - return -} - -type TradeSnapshot struct { - Snapshot []*Trade -} - -func NewTradeSnapshotFromRaw(pair string, raw [][]float64) (*TradeSnapshot, error) { - if len(raw) <= 0 { - return nil, fmt.Errorf("data slice is too short for trade snapshot: %#v", raw) - } - snapshot := make([]*Trade, 0) - for _, flt := range raw { - t, err := NewTradeFromRaw(pair, ToInterface(flt)) - if err == nil { - snapshot = append(snapshot, t) - } - } - - return &TradeSnapshot{Snapshot: snapshot}, nil -} - -// TradeExecutionUpdate represents a full update to a trade on the private data feed. Following a TradeExecution, -// TradeExecutionUpdates include additional details, e.g. the trade's execution ID (TradeID). -type TradeExecutionUpdate struct { - ID int64 - Pair string - MTS int64 - OrderID int64 - ExecAmount float64 - ExecPrice float64 - OrderType string - OrderPrice float64 - Maker int - Fee float64 - FeeCurrency string -} - -// public trade update just looks like a trade -func NewTradeExecutionUpdateFromRaw(raw []interface{}) (o *TradeExecutionUpdate, err error) { - if len(raw) == 4 { - o = &TradeExecutionUpdate{ - ID: i64ValOrZero(raw[0]), - MTS: i64ValOrZero(raw[1]), - ExecAmount: f64ValOrZero(raw[2]), - ExecPrice: f64ValOrZero(raw[3]), - } - return - } - if len(raw) == 11 { - o = &TradeExecutionUpdate{ - ID: i64ValOrZero(raw[0]), - Pair: sValOrEmpty(raw[1]), - MTS: i64ValOrZero(raw[2]), - OrderID: i64ValOrZero(raw[3]), - ExecAmount: f64ValOrZero(raw[4]), - ExecPrice: f64ValOrZero(raw[5]), - OrderType: sValOrEmpty(raw[6]), - OrderPrice: f64ValOrZero(raw[7]), - Maker: iValOrZero(raw[8]), - Fee: f64ValOrZero(raw[9]), - FeeCurrency: sValOrEmpty(raw[10]), - } - return - } - return o, fmt.Errorf("data slice too short for trade update: %#v", raw) -} - -type TradeExecutionUpdateSnapshot struct { - Snapshot []*TradeExecutionUpdate -} -type HistoricalTradeSnapshot TradeExecutionUpdateSnapshot - -func NewTradeExecutionUpdateSnapshotFromRaw(raw []interface{}) (s *TradeExecutionUpdateSnapshot, err error) { - if len(raw) == 0 { - return - } - ts := make([]*TradeExecutionUpdate, 0) - switch raw[0].(type) { - case []interface{}: - for _, v := range raw { - if l, ok := v.([]interface{}); ok { - t, err := NewTradeExecutionUpdateFromRaw(l) - if err != nil { - return s, err - } - ts = append(ts, t) - } - } - default: - return s, fmt.Errorf("not a trade snapshot: %#v", raw) - } - s = &TradeExecutionUpdateSnapshot{Snapshot: ts} - - return -} - -// TradeExecution represents the first message receievd for a trade on the private data feed. -type TradeExecution struct { - ID int64 - Pair string - MTS int64 - OrderID int64 - Amount float64 - Price float64 - OrderType string - OrderPrice float64 - Maker int -} - -func NewTradeExecutionFromRaw(raw []interface{}) (o *TradeExecution, err error) { - if len(raw) < 6 { - log.Printf("[ERROR] not enough members (%d, need at least 6) for trade execution: %#v", raw) - return o, fmt.Errorf("data slice too short for trade execution: %#v", raw) - } - - // trade executions sometimes omit order type, price, and maker flag - o = &TradeExecution{ - ID: i64ValOrZero(raw[0]), - Pair: sValOrEmpty(raw[1]), - MTS: i64ValOrZero(raw[2]), - OrderID: i64ValOrZero(raw[3]), - Amount: f64ValOrZero(raw[4]), - Price: f64ValOrZero(raw[5]), - } - - if len(raw) >= 9 { - o.OrderType = sValOrEmpty(raw[6]) - o.OrderPrice = f64ValOrZero(raw[7]) - o.Maker = iValOrZero(raw[8]) - } - - return -} - -type Wallet struct { - Type string - Currency string - Balance float64 - UnsettledInterest float64 - BalanceAvailable float64 -} - -func NewWalletFromRaw(raw []interface{}) (o *Wallet, err error) { - if len(raw) == 4 { - o = &Wallet{ - Type: sValOrEmpty(raw[0]), - Currency: sValOrEmpty(raw[1]), - Balance: f64ValOrZero(raw[2]), - UnsettledInterest: f64ValOrZero(raw[3]), - } - } else if len(raw) < 5 { - return o, fmt.Errorf("data slice too short for wallet: %#v", raw) - } else { - o = &Wallet{ - Type: sValOrEmpty(raw[0]), - Currency: sValOrEmpty(raw[1]), - Balance: f64ValOrZero(raw[2]), - UnsettledInterest: f64ValOrZero(raw[3]), - BalanceAvailable: f64ValOrZero(raw[4]), - } - } - return -} - -type WalletUpdate Wallet -type WalletSnapshot struct { - Snapshot []*Wallet -} - -func NewWalletSnapshotFromRaw(raw []interface{}) (s *WalletSnapshot, err error) { - if len(raw) == 0 { - return - } - - ws := make([]*Wallet, 0) - switch raw[0].(type) { - case []interface{}: - for _, v := range raw { - if l, ok := v.([]interface{}); ok { - o, err := NewWalletFromRaw(l) - if err != nil { - return s, err - } - ws = append(ws, o) - } - } - default: - return s, fmt.Errorf("not an wallet snapshot") - } - s = &WalletSnapshot{Snapshot: ws} - - return -} - -type BalanceInfo struct { - TotalAUM float64 - NetAUM float64 - /*WalletType string - Currency string*/ -} - -func NewBalanceInfoFromRaw(raw []interface{}) (o *BalanceInfo, err error) { - if len(raw) < 2 { - return o, fmt.Errorf("data slice too short for balance info: %#v", raw) - } - - o = &BalanceInfo{ - TotalAUM: f64ValOrZero(raw[0]), - NetAUM: f64ValOrZero(raw[1]), - /*WalletType: sValOrEmpty(raw[2]), - Currency: sValOrEmpty(raw[3]),*/ - } - - return -} - -type BalanceUpdate BalanceInfo - -// marginInfoFromRaw returns either a MarginInfoBase or MarginInfoUpdate, since -// the Margin Info is split up into a base and per symbol parts. -func NewMarginInfoFromRaw(raw []interface{}) (o interface{}, err error) { - if len(raw) < 2 { - return o, fmt.Errorf("data slice too short for margin info base: %#v", raw) - } - - typ, ok := raw[0].(string) - if !ok { - return o, fmt.Errorf("expected margin info type in first position for margin info but got %#v", raw) - } - - if len(raw) == 2 && typ == "base" { // This should be ["base", [...]] - data, ok := raw[1].([]interface{}) - if !ok { - return o, fmt.Errorf("expected margin info array in second position for margin info but got %#v", raw) - } - - return NewMarginInfoBaseFromRaw(data) - } else if len(raw) == 3 && typ == "sym" { // This should be ["sym", SYMBOL, [...]] - symbol, ok := raw[1].(string) - if !ok { - return o, fmt.Errorf("expected margin info symbol in second position for margin info update but got %#v", raw) - } - - data, ok := raw[2].([]interface{}) - if !ok { - return o, fmt.Errorf("expected margin info array in third position for margin info update but got %#v", raw) - } - - return NewMarginInfoUpdateFromRaw(symbol, data) - } - - return nil, fmt.Errorf("invalid margin info type in %#v", raw) -} - -type MarginInfoUpdate struct { - Symbol string - TradableBalance float64 -} - -func NewMarginInfoUpdateFromRaw(symbol string, raw []interface{}) (o *MarginInfoUpdate, err error) { - if len(raw) < 1 { - return o, fmt.Errorf("data slice too short for margin info update: %#v", raw) - } - - o = &MarginInfoUpdate{ - Symbol: symbol, - TradableBalance: f64ValOrZero(raw[0]), - } - - return -} - -type MarginInfoBase struct { - UserProfitLoss float64 - UserSwaps float64 - MarginBalance float64 - MarginNet float64 -} - -func NewMarginInfoBaseFromRaw(raw []interface{}) (o *MarginInfoBase, err error) { - if len(raw) < 4 { - return o, fmt.Errorf("data slice too short for margin info base: %#v", raw) - } - - o = &MarginInfoBase{ - UserProfitLoss: f64ValOrZero(raw[0]), - UserSwaps: f64ValOrZero(raw[1]), - MarginBalance: f64ValOrZero(raw[2]), - MarginNet: f64ValOrZero(raw[3]), - } - - return -} - -type FundingInfo struct { - Symbol string - YieldLoan float64 - YieldLend float64 - DurationLoan float64 - DurationLend float64 -} - -func NewFundingInfoFromRaw(raw []interface{}) (o *FundingInfo, err error) { - if len(raw) < 3 { // "sym", symbol, data - return o, fmt.Errorf("data slice too short for funding info: %#v", raw) - } - - sym, ok := raw[1].(string) - if !ok { - return o, fmt.Errorf("expected symbol in second position of funding info: %v", raw) - } - - data, ok := raw[2].([]interface{}) - if !ok { - return o, fmt.Errorf("expected list in third position of funding info: %v", raw) - } - - if len(data) < 4 { - return o, fmt.Errorf("data too short: %#v", data) - } - - o = &FundingInfo{ - Symbol: sym, - YieldLoan: f64ValOrZero(data[0]), - YieldLend: f64ValOrZero(data[1]), - DurationLoan: f64ValOrZero(data[2]), - DurationLend: f64ValOrZero(data[3]), - } - - return -} - -type OfferStatus string - -const ( - OfferStatusActive OfferStatus = "ACTIVE" - OfferStatusExecuted OfferStatus = "EXECUTED" - OfferStatusPartiallyFilled OfferStatus = "PARTIALLY FILLED" - OfferStatusCanceled OfferStatus = "CANCELED" -) - -type Offer struct { - ID int64 - Symbol string - MTSCreated int64 - MTSUpdated int64 - Amout float64 - AmountOrig float64 - Type string - Flags interface{} - Status OfferStatus - Rate float64 - Period int64 - Notify bool - Hidden bool - Insure bool - Renew bool - RateReal float64 -} - -func NewOfferFromRaw(raw []interface{}) (o *Offer, err error) { - if len(raw) < 21 { - return o, fmt.Errorf("data slice too short for offer: %#v", raw) - } - - o = &Offer{ - ID: i64ValOrZero(raw[0]), - Symbol: sValOrEmpty(raw[1]), - MTSCreated: i64ValOrZero(raw[2]), - MTSUpdated: i64ValOrZero(raw[3]), - Amout: f64ValOrZero(raw[4]), - AmountOrig: f64ValOrZero(raw[5]), - Type: sValOrEmpty(raw[6]), - Flags: raw[9], - Status: OfferStatus(sValOrEmpty(raw[10])), - Rate: f64ValOrZero(raw[14]), - Period: i64ValOrZero(raw[15]), - Notify: bValOrFalse(raw[16]), - Hidden: bValOrFalse(raw[17]), - Insure: bValOrFalse(raw[18]), - Renew: bValOrFalse(raw[19]), - RateReal: f64ValOrZero(raw[20]), - } - - return -} - -type FundingOfferNew Offer -type FundingOfferUpdate Offer -type FundingOfferCancel Offer -type FundingOfferSnapshot struct { - Snapshot []*Offer -} - -func NewFundingOfferSnapshotFromRaw(raw []interface{}) (snap *FundingOfferSnapshot, err error) { - if len(raw) == 0 { - return - } - - fos := make([]*Offer, 0) - switch raw[0].(type) { - case []interface{}: - for _, v := range raw { - if l, ok := v.([]interface{}); ok { - o, err := NewOfferFromRaw(l) - if err != nil { - return snap, err - } - fos = append(fos, o) - } - } - default: - return snap, fmt.Errorf("not a funding offer snapshot") - } - - snap = &FundingOfferSnapshot{ - Snapshot: fos, - } - - return -} - -type HistoricalOffer Offer - -type CreditStatus string - -const ( - CreditStatusActive CreditStatus = "ACTIVE" - CreditStatusExecuted CreditStatus = "EXECUTED" - CreditStatusPartiallyFilled CreditStatus = "PARTIALLY FILLED" - CreditStatusCanceled CreditStatus = "CANCELED" -) - -type Credit struct { - ID int64 - Symbol string - Side string - MTSCreated int64 - MTSUpdated int64 - Amout float64 - Flags interface{} - Status CreditStatus - Rate float64 - Period int64 - MTSOpened int64 - MTSLastPayout int64 - Notify bool - Hidden bool - Insure bool - Renew bool - RateReal float64 - NoClose bool - PositionPair string -} - -func NewCreditFromRaw(raw []interface{}) (o *Credit, err error) { - if len(raw) < 22 { - return o, fmt.Errorf("data slice too short for offer: %#v", raw) - } - - o = &Credit{ - ID: i64ValOrZero(raw[0]), - Symbol: sValOrEmpty(raw[1]), - Side: sValOrEmpty(raw[2]), - MTSCreated: i64ValOrZero(raw[3]), - MTSUpdated: i64ValOrZero(raw[4]), - Amout: f64ValOrZero(raw[5]), - Flags: raw[6], - Status: CreditStatus(sValOrEmpty(raw[7])), - Rate: f64ValOrZero(raw[11]), - Period: i64ValOrZero(raw[12]), - MTSOpened: i64ValOrZero(raw[13]), - MTSLastPayout: i64ValOrZero(raw[14]), - Notify: bValOrFalse(raw[15]), - Hidden: bValOrFalse(raw[16]), - Insure: bValOrFalse(raw[17]), - Renew: bValOrFalse(raw[18]), - RateReal: f64ValOrZero(raw[19]), - NoClose: bValOrFalse(raw[20]), - PositionPair: sValOrEmpty(raw[21]), - } - - return -} - -type HistoricalCredit Credit -type FundingCreditNew Credit -type FundingCreditUpdate Credit -type FundingCreditCancel Credit - -type FundingCreditSnapshot struct { - Snapshot []*Credit -} - -func NewFundingCreditSnapshotFromRaw(raw []interface{}) (snap *FundingCreditSnapshot, err error) { - if len(raw) == 0 { - return - } - - fcs := make([]*Credit, 0) - switch raw[0].(type) { - case []interface{}: - for _, v := range raw { - if l, ok := v.([]interface{}); ok { - o, err := NewCreditFromRaw(l) - if err != nil { - return snap, err - } - fcs = append(fcs, o) - } - } - default: - return snap, fmt.Errorf("not a funding credit snapshot") - } - snap = &FundingCreditSnapshot{ - Snapshot: fcs, - } - - return -} - -type LoanStatus string - -const ( - LoanStatusActive LoanStatus = "ACTIVE" - LoanStatusExecuted LoanStatus = "EXECUTED" - LoanStatusPartiallyFilled LoanStatus = "PARTIALLY FILLED" - LoanStatusCanceled LoanStatus = "CANCELED" -) - -type Loan struct { - ID int64 - Symbol string - Side string - MTSCreated int64 - MTSUpdated int64 - Amout float64 - Flags interface{} - Status LoanStatus - Rate float64 - Period int64 - MTSOpened int64 - MTSLastPayout int64 - Notify bool - Hidden bool - Insure bool - Renew bool - RateReal float64 - NoClose bool -} - -func NewLoanFromRaw(raw []interface{}) (o *Loan, err error) { - if len(raw) < 21 { - return o, fmt.Errorf("data slice too short (len=%d) for loan: %#v", len(raw), raw) - } - - o = &Loan{ - ID: i64ValOrZero(raw[0]), - Symbol: sValOrEmpty(raw[1]), - Side: sValOrEmpty(raw[2]), - MTSCreated: i64ValOrZero(raw[3]), - MTSUpdated: i64ValOrZero(raw[4]), - Amout: f64ValOrZero(raw[5]), - Flags: raw[6], - Status: LoanStatus(sValOrEmpty(raw[7])), - Rate: f64ValOrZero(raw[11]), - Period: i64ValOrZero(raw[12]), - MTSOpened: i64ValOrZero(raw[13]), - MTSLastPayout: i64ValOrZero(raw[14]), - Notify: bValOrFalse(raw[15]), - Hidden: bValOrFalse(raw[16]), - Insure: bValOrFalse(raw[17]), - Renew: bValOrFalse(raw[18]), - RateReal: f64ValOrZero(raw[19]), - NoClose: bValOrFalse(raw[20]), - } - - return o, nil -} - -type HistoricalLoan Loan -type FundingLoanNew Loan -type FundingLoanUpdate Loan -type FundingLoanCancel Loan - -type FundingLoanSnapshot struct { - Snapshot []*Loan -} - -func NewFundingLoanSnapshotFromRaw(raw []interface{}) (snap *FundingLoanSnapshot, err error) { - if len(raw) == 0 { - return - } - - fls := make([]*Loan, 0) - switch raw[0].(type) { - case []interface{}: - for _, v := range raw { - if l, ok := v.([]interface{}); ok { - o, err := NewLoanFromRaw(l) - if err != nil { - return snap, err - } - fls = append(fls, o) - } - } - default: - return snap, fmt.Errorf("not a funding loan snapshot") - } - snap = &FundingLoanSnapshot{ - Snapshot: fls, - } - - return -} - -type FundingTrade struct { - ID int64 - Symbol string - MTSCreated int64 - OfferID int64 - Amount float64 - Rate float64 - Period int64 - Maker int64 -} - -func NewFundingTradeFromRaw(raw []interface{}) (o *FundingTrade, err error) { - if len(raw) < 8 { - return o, fmt.Errorf("data slice too short for funding trade: %#v", raw) - } - - o = &FundingTrade{ - ID: i64ValOrZero(raw[0]), - Symbol: sValOrEmpty(raw[1]), - MTSCreated: i64ValOrZero(raw[2]), - OfferID: i64ValOrZero(raw[3]), - Amount: f64ValOrZero(raw[4]), - Rate: f64ValOrZero(raw[5]), - Period: i64ValOrZero(raw[6]), - Maker: i64ValOrZero(raw[7]), - } - - return -} - -type FundingTradeExecution FundingTrade -type FundingTradeUpdate FundingTrade -type FundingTradeSnapshot struct { - Snapshot []*FundingTrade -} -type HistoricalFundingTradeSnapshot FundingTradeSnapshot - -func NewFundingTradeSnapshotFromRaw(raw []interface{}) (snap *FundingTradeSnapshot, err error) { - if len(raw) == 0 { - return - } - - fts := make([]*FundingTrade, 0) - switch raw[0].(type) { - case []interface{}: - for _, v := range raw { - if l, ok := v.([]interface{}); ok { - o, err := NewFundingTradeFromRaw(l) - if err != nil { - return snap, err - } - fts = append(fts, o) - } - } - default: - return snap, fmt.Errorf("not a funding trade snapshot") - } - snap = &FundingTradeSnapshot{ - Snapshot: fts, - } - - return -} - -type Notification struct { - MTS int64 - Type string - MessageID int64 - NotifyInfo interface{} - Code int64 - Status string - Text string -} - -func NewNotificationFromRaw(raw []interface{}) (o *Notification, err error) { - if len(raw) < 8 { - return o, fmt.Errorf("data slice too short for notification: %#v", raw) - } - - o = &Notification{ - MTS: i64ValOrZero(raw[0]), - Type: sValOrEmpty(raw[1]), - MessageID: i64ValOrZero(raw[2]), - //NotifyInfo: raw[4], - Code: i64ValOrZero(raw[5]), - Status: sValOrEmpty(raw[6]), - Text: sValOrEmpty(raw[7]), - } - - // raw[4] = notify info - var nraw []interface{} - if raw[4] != nil { - nraw = raw[4].([]interface{}) - switch o.Type { - case "on-req": - on, err := NewOrderFromRaw(nraw) - if err != nil { - return o, err - } - orderNew := OrderNew(*on) - o.NotifyInfo = &orderNew - case "oc-req": - oc, err := NewOrderFromRaw(nraw) - if err != nil { - return o, err - } - orderCancel := OrderCancel(*oc) - o.NotifyInfo = &orderCancel - case "fon-req": - fon, err := NewOfferFromRaw(nraw) - if err != nil { - return o, err - } - fundingOffer := FundingOfferNew(*fon) - o.NotifyInfo = &fundingOffer - case "foc-req": - foc, err := NewOfferFromRaw(nraw) - if err != nil { - return o, err - } - fundingOffer := FundingOfferCancel(*foc) - o.NotifyInfo = &fundingOffer - case "uca": - o.NotifyInfo = raw[4] - } - } - - return -} - -type Ticker struct { - Symbol string - Bid float64 - BidPeriod int64 - BidSize float64 - Ask float64 - AskPeriod int64 - AskSize float64 - DailyChange float64 - DailyChangePerc float64 - LastPrice float64 - Volume float64 - High float64 - Low float64 -} - -type TickerUpdate Ticker -type TickerSnapshot struct { - Snapshot []*Ticker -} - -func NewTickerSnapshotFromRaw(symbol string, raw [][]float64) (*TickerSnapshot, error) { - if len(raw) <= 0 { - return nil, fmt.Errorf("data slice too short for ticker snapshot: %#v", raw) - } - snap := make([]*Ticker, 0) - for _, f := range raw { - c, err := NewTickerFromRaw(symbol, ToInterface(f)) - if err == nil { - snap = append(snap, c) - } - } - return &TickerSnapshot{Snapshot: snap}, nil -} - -func NewTickerFromRaw(symbol string, raw []interface{}) (t *Ticker, err error) { - if len(raw) < 10 { - return t, fmt.Errorf("data slice too short for ticker, expected %d got %d: %#v", 10, len(raw), raw) - } - - t = &Ticker{ - Symbol: symbol, - Bid: f64ValOrZero(raw[0]), - BidSize: f64ValOrZero(raw[1]), - Ask: f64ValOrZero(raw[2]), - AskSize: f64ValOrZero(raw[3]), - DailyChange: f64ValOrZero(raw[4]), - DailyChangePerc: f64ValOrZero(raw[5]), - LastPrice: f64ValOrZero(raw[6]), - Volume: f64ValOrZero(raw[7]), - High: f64ValOrZero(raw[8]), - Low: f64ValOrZero(raw[9]), - } - - return -} - -type bookAction byte - -// BookAction represents a new/update or removal for a book entry. -type BookAction bookAction - -const ( - //BookUpdateEntry represents a new or updated book entry. - BookUpdateEntry BookAction = 0 - //BookRemoveEntry represents a removal of a book entry. - BookRemoveEntry BookAction = 1 -) - -// BookUpdate represents an order book price update. -type BookUpdate struct { - ID int64 // the book update ID, optional - Symbol string // book symbol - Price float64 // updated price - Count int64 // updated count, optional - Amount float64 // updated amount - Side OrderSide // side - Action BookAction // action (add/remove) -} - -type BookUpdateSnapshot struct { - Snapshot []*BookUpdate -} - -func NewBookUpdateSnapshotFromRaw(symbol, precision string, raw [][]float64) (*BookUpdateSnapshot, error) { - if len(raw) <= 0 { - return nil, fmt.Errorf("data slice too short for book snapshot: %#v", raw) - } - snap := make([]*BookUpdate, 0) - for _, f := range raw { - b, err := NewBookUpdateFromRaw(symbol, precision, ToInterface(f)) - if err == nil { - snap = append(snap, b) - } - } - return &BookUpdateSnapshot{Snapshot: snap}, nil -} - -func IsRawBook(precision string) bool { - return precision == "R0" -} - -// NewBookUpdateFromRaw creates a new book update object from raw data. Precision determines how -// to interpret the side (baked into Count versus Amount) -// raw book updates [ID, price, qty], aggregated book updates [price, amount, count] -func NewBookUpdateFromRaw(symbol, precision string, data []interface{}) (b *BookUpdate, err error) { - if len(data) < 3 { - return b, fmt.Errorf("data slice too short for book update, expected %d got %d: %#v", 5, len(data), data) - } - var px float64 - var id, cnt int64 - amt := f64ValOrZero(data[2]) - - var side OrderSide - var actionCtrl float64 - if IsRawBook(precision) { - // [ID, price, amount] - id = i64ValOrZero(data[0]) - px = f64ValOrZero(data[1]) - actionCtrl = px - } else { - // [price, amount, count] - px = f64ValOrZero(data[0]) - cnt = i64ValOrZero(data[1]) - actionCtrl = float64(cnt) - } - - if amt > 0 { - side = Bid - } else { - side = Ask - } - - var action BookAction - if actionCtrl <= 0 { - action = BookRemoveEntry - } else { - action = BookUpdateEntry - } - - b = &BookUpdate{ - Symbol: symbol, - Price: math.Abs(px), - Count: cnt, - Amount: math.Abs(amt), - Side: side, - Action: action, - ID: id, - } - - return -} - -type Candle struct { - Symbol string - Resolution CandleResolution - MTS int64 - Open float64 - Close float64 - High float64 - Low float64 - Volume float64 -} - -type CandleSnapshot struct { - Snapshot []*Candle -} - -func ToFloat64Slice(slice []interface{}) []float64 { - data := make([]float64, 0, len(slice)) - for _, i := range slice { - if f, ok := i.(float64); ok { - data = append(data, f) - } - } - return data -} - -func ToInterface(flt []float64) []interface{} { - data := make([]interface{}, len(flt)) - for j, f := range flt { - data[j] = f - } - return data -} - -func NewCandleSnapshotFromRaw(symbol string, resolution CandleResolution, raw [][]float64) (*CandleSnapshot, error) { - if len(raw) <= 0 { - return nil, fmt.Errorf("data slice too short for candle snapshot: %#v", raw) - } - snap := make([]*Candle, 0) - for _, f := range raw { - c, err := NewCandleFromRaw(symbol, resolution, ToInterface(f)) - if err == nil { - snap = append(snap, c) - } - } - return &CandleSnapshot{Snapshot: snap}, nil -} - -func NewCandleFromRaw(symbol string, resolution CandleResolution, raw []interface{}) (c *Candle, err error) { - if len(raw) < 6 { - return c, fmt.Errorf("data slice too short for candle, expected %d got %d: %#v", 6, len(raw), raw) - } - - c = &Candle{ - Symbol: symbol, - Resolution: resolution, - MTS: i64ValOrZero(raw[0]), - Open: f64ValOrZero(raw[1]), - Close: f64ValOrZero(raw[2]), - High: f64ValOrZero(raw[3]), - Low: f64ValOrZero(raw[4]), - Volume: f64ValOrZero(raw[5]), - } - - return -} diff --git a/v2/websocket/api.go b/v2/websocket/api.go deleted file mode 100644 index 35975bf37..000000000 --- a/v2/websocket/api.go +++ /dev/null @@ -1,98 +0,0 @@ -package websocket - -import ( - "context" - "fmt" - - "github.com/bitfinexcom/bitfinex-api-go/v2" -) - -// API for end-users to interact with Bitfinex. - -// Send publishes a generic message to the Bitfinex API. -func (c *Client) Send(ctx context.Context, msg interface{}) error { - return c.asynchronous.Send(ctx, msg) -} - -// Subscribe sends a subscription request to the Bitfinex API and tracks the subscription status by ID. -func (c *Client) Subscribe(ctx context.Context, req *SubscriptionRequest) (string, error) { - c.subscriptions.add(req) - err := c.asynchronous.Send(ctx, req) - if err != nil { - // propagate send error - return "", err - } - return req.SubID, nil -} - -// SubscribeTicker sends a subscription request for the ticker. -func (c *Client) SubscribeTicker(ctx context.Context, symbol string) (string, error) { - req := &SubscriptionRequest{ - SubID: c.nonce.GetNonce(), - Event: EventSubscribe, - Channel: ChanTicker, - Symbol: symbol, - } - return c.Subscribe(ctx, req) -} - -// SubscribeTrades sends a subscription request for the trade feed. -func (c *Client) SubscribeTrades(ctx context.Context, symbol string) (string, error) { - req := &SubscriptionRequest{ - SubID: c.nonce.GetNonce(), - Event: EventSubscribe, - Channel: ChanTrades, - Symbol: symbol, - } - return c.Subscribe(ctx, req) -} - -// SubscribeBook sends a subscription request for market data for a given symbol, at a given frequency, with a given precision, returning no more than priceLevels price entries. -// Default values are Precision0, Frequency0, and priceLevels=25. -func (c *Client) SubscribeBook(ctx context.Context, symbol string, precision bitfinex.BookPrecision, frequency bitfinex.BookFrequency, priceLevel int) (string, error) { - if priceLevel < 0 { - return "", fmt.Errorf("negative price levels not supported: %d", priceLevel) - } - req := &SubscriptionRequest{ - SubID: c.nonce.GetNonce(), - Event: EventSubscribe, - Channel: ChanBook, - Symbol: symbol, - Precision: string(precision), - Len: fmt.Sprintf("%d", priceLevel), // needed for R0? - } - if !bitfinex.IsRawBook(string(precision)) { - req.Frequency = string(frequency) - } - return c.Subscribe(ctx, req) -} - -// SubscribeCandles sends a subscription request for OHLC candles. -func (c *Client) SubscribeCandles(ctx context.Context, symbol string, resolution bitfinex.CandleResolution) (string, error) { - req := &SubscriptionRequest{ - SubID: c.nonce.GetNonce(), - Event: EventSubscribe, - Channel: ChanCandles, - Key: fmt.Sprintf("trade:%s:%s", resolution, symbol), - } - return c.Subscribe(ctx, req) -} - -// SubmitOrder sends an order request. -func (c *Client) SubmitOrder(ctx context.Context, order *bitfinex.OrderNewRequest) error { - return c.asynchronous.Send(ctx, order) -} - -// SubmitCancel sends a cancel request. -func (c *Client) SubmitCancel(ctx context.Context, cancel *bitfinex.OrderCancelRequest) error { - return c.asynchronous.Send(ctx, cancel) -} - -// LookupSubscription looks up a subscription request by ID -func (c *Client) LookupSubscription(subID string) (*SubscriptionRequest, error) { - s, err := c.subscriptions.lookupBySubscriptionID(subID) - if err != nil { - return nil, err - } - return s.Request, nil -} diff --git a/v2/websocket/channels.go b/v2/websocket/channels.go deleted file mode 100644 index c932aaf01..000000000 --- a/v2/websocket/channels.go +++ /dev/null @@ -1,430 +0,0 @@ -package websocket - -import ( - "encoding/json" - "fmt" - "log" - - "github.com/bitfinexcom/bitfinex-api-go/v2" -) - -func (c *Client) handleChannel(msg []byte) error { - var raw []interface{} - err := json.Unmarshal(msg, &raw) - if err != nil { - return err - } else if len(raw) < 2 { - return nil - } - - chID, ok := raw[0].(float64) - if !ok { - return fmt.Errorf("expected message to start with a channel id but got %#v instead", raw[0]) - } - - chanID := int64(chID) - sub, err := c.subscriptions.lookupByChannelID(chanID) - if err != nil { - // no subscribed channel for message - return err - } - c.subscriptions.heartbeat(chanID) - if sub.Public { - switch data := raw[1].(type) { - case string: - switch data { - case "hb": - // no-op, already updated heartbeat timeout from this event - return nil - default: - body := raw[2].([]interface{}) - return c.handlePublicChannel(chanID, sub.Request.Channel, data, body) - } - case []interface{}: - return c.handlePublicChannel(chanID, sub.Request.Channel, "", data) - } - } else { - return c.handlePrivateChannel(raw) - } - return nil -} - -func (c *Client) handlePublicChannel(chanID int64, channel, objType string, data []interface{}) error { - // unauthenticated data slice - // returns interface{} (which is really [][]float64) - obj, err := c.processDataSlice(data) - if err != nil { - return err - } - // public data is returned as raw interface arrays, use a factory to convert to raw type & publish - if factory, ok := c.factories[channel]; ok { - flt := obj.([][]float64) - if len(flt) == 1 { - // single item - arr := make([]interface{}, len(flt[0])) - for i, ft := range flt[0] { - arr[i] = ft - } - msg, err := factory.Build(chanID, objType, arr) - if err != nil { - return err - } - if msg != nil { - c.listener <- msg - } - } else if len(flt) > 1 { - msg, err := factory.BuildSnapshot(chanID, flt) - if err != nil { - return err - } - if msg != nil { - c.listener <- msg - } - } - } else { - // factory lookup error - return fmt.Errorf("could not find public factory for %s channel", channel) - } - return nil -} - -func (c *Client) handlePrivateChannel(raw []interface{}) error { - // authenticated data slice, or a heartbeat - if raw[1].(string) == "hb" { - chanID, ok := raw[0].(float64) - if !ok { - log.Printf("could not find chanID: %#v", raw) - return nil - } - c.handleHeartbeat(int64(chanID)) - } else { - // raw[2] is data slice - // authenticated snapshots? - if len(raw) > 2 { - if arr, ok := raw[2].([]interface{}); ok { - obj, err := c.handlePrivateDataMessage(raw[1].(string), arr) - if err != nil { - return err - } - // private data is returned as strongly typed data, publish directly - if obj != nil { - c.listener <- obj - } - } - } - } - return nil -} - -func (c *Client) handleHeartbeat(chanID int64) { - c.subscriptions.heartbeat(chanID) -} - -type unsubscribeMsg struct { - Event string `json:"event"` - ChanID int64 `json:"chanId"` -} - -func (c *Client) processDataSlice(data []interface{}) (interface{}, error) { - if len(data) == 0 { - return nil, fmt.Errorf("unexpected data slice: %v", data) - } - - var items [][]float64 - switch data[0].(type) { - case []interface{}: // [][]float64 - for _, e := range data { - if s, ok := e.([]interface{}); ok { - item, err := bitfinex.F64Slice(s) - if err != nil { - return nil, err - } - items = append(items, item) - } else { - return nil, fmt.Errorf("expected slice of float64 slices but got: %v", data) - } - } - case float64: // []float64 - item, err := bitfinex.F64Slice(data) - if err != nil { - return nil, err - } - items = append(items, item) - default: - return nil, fmt.Errorf("unexpected data slice: %v", data) - } - - return items, nil -} - -// public msg: [ChanID, [Data]] -// hb (both): [ChanID, "hb"] -// private update msg: [ChanID, "type", [Data]] -// private snapshot msg: [ChanID, "type", [[Data]]] -func (c *Client) handlePrivateDataMessage(term string, data []interface{}) (ms interface{}, err error) { - if len(data) == 0 { - // empty data msg - return nil, nil - } - - if term == "hb" { // Heartbeat - // TODO: Consider adding a switch to enable/disable passing these along. - return &bitfinex.Heartbeat{}, nil - } - /* - list, ok := data[2].([]interface{}) - if !ok { - return ms, fmt.Errorf("expected data list in third position but got %#v in %#v", data[2], data) - } - */ - ms = c.convertRaw(term, data) - - return -} - -// convertRaw takes a term and the raw data attached to it to try and convert that -// untyped list into a proper type. -func (c *Client) convertRaw(term string, raw []interface{}) interface{} { - // The things you do to get proper types. - switch term { - case "bu": - o, err := bitfinex.NewBalanceInfoFromRaw(raw) - if err != nil { - return err - } - bu := bitfinex.BalanceUpdate(*o) - return &bu - case "ps": - o, err := bitfinex.NewPositionSnapshotFromRaw(raw) - if err != nil { - return err - } - return o - case "pn": - o, err := bitfinex.NewPositionFromRaw(raw) - if err != nil { - return err - } - pn := bitfinex.PositionNew(*o) - return &pn - case "pu": - o, err := bitfinex.NewPositionFromRaw(raw) - if err != nil { - return err - } - pu := bitfinex.PositionUpdate(*o) - return &pu - case "pc": - o, err := bitfinex.NewPositionFromRaw(raw) - if err != nil { - return err - } - pc := bitfinex.PositionCancel(*o) - return &pc - case "ws": - o, err := bitfinex.NewWalletSnapshotFromRaw(raw) - if err != nil { - return err - } - return o - case "wu": - o, err := bitfinex.NewWalletFromRaw(raw) - if err != nil { - return err - } - wu := bitfinex.WalletUpdate(*o) - return &wu - case "os": - o, err := bitfinex.NewOrderSnapshotFromRaw(raw) - if err != nil { - return err - } - return o - case "on": - o, err := bitfinex.NewOrderFromRaw(raw) - if err != nil { - return err - } - on := bitfinex.OrderNew(*o) - return &on - case "ou": - o, err := bitfinex.NewOrderFromRaw(raw) - if err != nil { - return err - } - ou := bitfinex.OrderUpdate(*o) - return &ou - case "oc": - o, err := bitfinex.NewOrderFromRaw(raw) - if err != nil { - return err - } - oc := bitfinex.OrderCancel(*o) - return &oc - case "hts": - o, err := bitfinex.NewTradeExecutionUpdateSnapshotFromRaw(raw) - if err != nil { - return err - } - hts := bitfinex.HistoricalTradeSnapshot(*o) - return &hts - case "te": - o, err := bitfinex.NewTradeExecutionFromRaw(raw) - if err != nil { - return err - } - return o - case "tu": - tu, err := bitfinex.NewTradeExecutionUpdateFromRaw(raw) - if err != nil { - return err - } - return tu - case "fte": - o, err := bitfinex.NewFundingTradeFromRaw(raw) - if err != nil { - return err - } - fte := bitfinex.FundingTradeExecution(*o) - return &fte - case "ftu": - o, err := bitfinex.NewFundingTradeFromRaw(raw) - if err != nil { - return err - } - ftu := bitfinex.FundingTradeUpdate(*o) - return &ftu - case "hfts": - o, err := bitfinex.NewFundingTradeSnapshotFromRaw(raw) - if err != nil { - return err - } - nfts := bitfinex.HistoricalFundingTradeSnapshot(*o) - return &nfts - case "n": - o, err := bitfinex.NewNotificationFromRaw(raw) - if err != nil { - return err - } - return o - case "fos": - o, err := bitfinex.NewFundingOfferSnapshotFromRaw(raw) - if err != nil { - return err - } - return o - case "fon": - o, err := bitfinex.NewOfferFromRaw(raw) - if err != nil { - return err - } - fon := bitfinex.FundingOfferNew(*o) - return &fon - case "fou": - o, err := bitfinex.NewOfferFromRaw(raw) - if err != nil { - return err - } - fou := bitfinex.FundingOfferUpdate(*o) - return &fou - case "foc": - o, err := bitfinex.NewOfferFromRaw(raw) - if err != nil { - return err - } - foc := bitfinex.FundingOfferCancel(*o) - return &foc - case "fiu": - o, err := bitfinex.NewFundingInfoFromRaw(raw) - if err != nil { - return err - } - return o - case "fcs": - o, err := bitfinex.NewFundingCreditSnapshotFromRaw(raw) - if err != nil { - return err - } - return o - case "fcn": - o, err := bitfinex.NewCreditFromRaw(raw) - if err != nil { - return err - } - fcn := bitfinex.FundingCreditNew(*o) - return &fcn - case "fcu": - o, err := bitfinex.NewCreditFromRaw(raw) - if err != nil { - return err - } - fcu := bitfinex.FundingCreditUpdate(*o) - return &fcu - case "fcc": - o, err := bitfinex.NewCreditFromRaw(raw) - if err != nil { - return err - } - fcc := bitfinex.FundingCreditCancel(*o) - return &fcc - case "fls": - o, err := bitfinex.NewFundingLoanSnapshotFromRaw(raw) - if err != nil { - return err - } - return o - case "fln": - o, err := bitfinex.NewLoanFromRaw(raw) - if err != nil { - return err - } - fln := bitfinex.FundingLoanNew(*o) - return &fln - case "flu": - o, err := bitfinex.NewLoanFromRaw(raw) - if err != nil { - return err - } - flu := bitfinex.FundingLoanUpdate(*o) - return &flu - case "flc": - o, err := bitfinex.NewLoanFromRaw(raw) - if err != nil { - return err - } - flc := bitfinex.FundingLoanCancel(*o) - return &flc - //case "uac": - case "hb": - return &bitfinex.Heartbeat{} - case "ats": - // TODO: Is not in documentation, so figure out what it is. - return nil - case "oc-req": - // TODO - return nil - case "on-req": - // TODO - return nil - case "mis": // Should not be sent anymore as of 2017-04-01 - return nil - case "miu": - o, err := bitfinex.NewMarginInfoFromRaw(raw) - if err != nil { - return err - } - // return a strongly typed reference, rather than dereference a generic interface - // too bad golang doesn't inherit an interface's underlying type when creating a reference to the interface - if base, ok := o.(*bitfinex.MarginInfoBase); ok { - return base - } - if update, ok := o.(*bitfinex.MarginInfoUpdate); ok { - return update - } - return o // better than nothing - default: - log.Printf("unhandled channel data, term: %s", term) - } - - return fmt.Errorf("term %q not recognized", term) -} diff --git a/v2/websocket/client.go b/v2/websocket/client.go deleted file mode 100644 index f79c7f848..000000000 --- a/v2/websocket/client.go +++ /dev/null @@ -1,498 +0,0 @@ -package websocket - -import ( - "bytes" - "context" - "fmt" - "log" - "strings" - "sync" - "time" - "unicode" - - "github.com/bitfinexcom/bitfinex-api-go/utils" - - "crypto/hmac" - "crypto/sha512" - "encoding/hex" - - "github.com/bitfinexcom/bitfinex-api-go/v2" -) - -var productionBaseURL = "wss://api.bitfinex.com/ws/2" - -// ws-specific errors -var ( - ErrWSNotConnected = fmt.Errorf("websocket connection not established") - ErrWSAlreadyConnected = fmt.Errorf("websocket connection already established") -) - -// Available channels -const ( - ChanBook = "book" - ChanTrades = "trades" - ChanTicker = "ticker" - ChanCandles = "candles" -) - -// Events -const ( - EventSubscribe = "subscribe" - EventUnsubscribe = "unsubscribe" - EventPing = "ping" -) - -// Authentication states -const ( - NoAuthentication AuthState = 0 - PendingAuthentication AuthState = 1 - SuccessfulAuthentication AuthState = 2 - RejectedAuthentication AuthState = 3 -) - -// private type--cannot instantiate. -type authState byte - -// AuthState provides a typed authentication state. -type AuthState authState // prevent user construction of authStates - -// DMSCancelOnDisconnect cancels session orders on disconnect. -const DMSCancelOnDisconnect int = 4 - -// Asynchronous interface decouples the underlying transport from API logic. -type Asynchronous interface { - Connect() error - Send(ctx context.Context, msg interface{}) error - Listen() <-chan []byte - Close() - Done() <-chan error -} - -// AsynchronousFactory provides an interface to re-create asynchronous transports during reconnect events. -type AsynchronousFactory interface { - Create() Asynchronous -} - -// WebsocketAsynchronousFactory creates a websocket-based asynchronous transport. -type WebsocketAsynchronousFactory struct { - parameters *Parameters -} - -// NewWebsocketAsynchronousFactory creates a new websocket factory with a given URL. -func NewWebsocketAsynchronousFactory(parameters *Parameters) AsynchronousFactory { - return &WebsocketAsynchronousFactory{ - parameters: parameters, - } -} - -// Create returns a new websocket transport. -func (w *WebsocketAsynchronousFactory) Create() Asynchronous { - return newWs(w.parameters.URL, w.parameters.LogTransport) -} - -// Client provides a unified interface for users to interact with the Bitfinex V2 Websocket API. -type Client struct { - asyncFactory AsynchronousFactory // for re-creating transport during reconnects - - timeout int64 // read timeout - apiKey string - apiSecret string - cancelOnDisconnect bool - Authentication AuthState - asynchronous Asynchronous - nonce utils.NonceGenerator - isConnected bool - terminal bool - resetSubscriptions []*subscription - init bool - - // connection & operational behavior - parameters *Parameters - - // subscription manager - subscriptions *subscriptions - factories map[string]messageFactory - - // close signal sent to user on shutdown - shutdown chan bool - - // downstream listener channel to deliver API objects - listener chan interface{} -} - -// Credentials assigns authentication credentials to a connection request. -func (c *Client) Credentials(key string, secret string) *Client { - c.apiKey = key - c.apiSecret = secret - return c -} - -// CancelOnDisconnect ensures all orders will be canceled if this API session is disconnected. -func (c *Client) CancelOnDisconnect(cxl bool) *Client { - c.cancelOnDisconnect = cxl - return c -} - -func (c *Client) sign(msg string) string { - sig := hmac.New(sha512.New384, []byte(c.apiSecret)) - sig.Write([]byte(msg)) - return hex.EncodeToString(sig.Sum(nil)) -} - -func (c *Client) registerFactory(channel string, factory messageFactory) { - c.factories[channel] = factory -} - -// New creates a default client. -func New() *Client { - return NewWithParams(NewDefaultParameters()) -} - -// NewWithAsyncFactory creates a new default client with a given asynchronous transport factory interface. -func NewWithAsyncFactory(async AsynchronousFactory) *Client { - return NewWithParamsAsyncFactory(NewDefaultParameters(), async) -} - -// NewWithParams creates a new default client with a given set of parameters. -func NewWithParams(params *Parameters) *Client { - return NewWithParamsAsyncFactory(params, NewWebsocketAsynchronousFactory(params)) -} - -// NewWithAsyncFactoryNonce creates a new default client with a given asynchronous transport factory and nonce generator. -func NewWithAsyncFactoryNonce(async AsynchronousFactory, nonce utils.NonceGenerator) *Client { - return NewWithParamsAsyncFactoryNonce(NewDefaultParameters(), async, nonce) -} - -// NewWithParamsNonce creates a new default client with a given set of parameters and nonce generator. -func NewWithParamsNonce(params *Parameters, nonce utils.NonceGenerator) *Client { - return NewWithParamsAsyncFactoryNonce(params, NewWebsocketAsynchronousFactory(params), nonce) -} - -// NewWithParamsAsyncFactory creates a new default client with a given set of parameters and asynchronous transport factory interface. -func NewWithParamsAsyncFactory(params *Parameters, async AsynchronousFactory) *Client { - return NewWithParamsAsyncFactoryNonce(params, async, utils.NewEpochNonceGenerator()) -} - -// NewWithParamsAsyncFactoryNonce creates a new client with a given set of parameters, asynchronous transport factory, and nonce generator interfaces. -func NewWithParamsAsyncFactoryNonce(params *Parameters, async AsynchronousFactory, nonce utils.NonceGenerator) *Client { - c := &Client{ - asyncFactory: async, - Authentication: NoAuthentication, - factories: make(map[string]messageFactory), - subscriptions: newSubscriptions(params.HeartbeatTimeout), - nonce: nonce, - isConnected: false, - parameters: params, - listener: make(chan interface{}), - terminal: false, - } - c.registerPublicFactories() - return c -} - -func extractSymbolResolutionFromKey(subscription string) (symbol string, resolution bitfinex.CandleResolution, err error) { - var res, sym string - str := strings.Split(subscription, ":") - if len(str) < 3 { - return "", resolution, fmt.Errorf("could not convert symbol resolution for %s: len %d", subscription, len(str)) - } - res = str[1] - sym = str[2] - resolution, err = bitfinex.CandleResolutionFromString(res) - if err != nil { - return "", resolution, err - } - return sym, resolution, nil -} - -func (c *Client) registerPublicFactories() { - c.registerFactory(ChanTicker, newTickerFactory(c.subscriptions)) - c.registerFactory(ChanTrades, newTradeFactory(c.subscriptions)) - c.registerFactory(ChanBook, newBookFactory(c.subscriptions)) - c.registerFactory(ChanCandles, newCandlesFactory(c.subscriptions)) -} - -// IsConnected returns true if the underlying asynchronous transport is connected to an endpoint. -func (c *Client) IsConnected() bool { - return c.isConnected -} - -func (c *Client) listenDisconnect() { - select { - case e := <-c.asynchronous.Done(): // transport shutdown - if e != nil { - log.Printf("socket disconnect: %s", e.Error()) - } - c.isConnected = false - c.reconnect(e) - case e := <-c.subscriptions.ListenDisconnect(): // subscription heartbeat timeout - if e != nil { - log.Printf("heartbeat disconnect: %s", e.Error()) - } - c.isConnected = false - if e != nil { - c.closeAsyncAndWait(c.parameters.ShutdownTimeout) - c.reconnect(e) - } - case <-c.shutdown: // normal shutdown - c.isConnected = false - } -} - -func (c *Client) dumpParams() { - log.Print("----Bitfinex Client Parameters----") - log.Printf("AutoReconnect=%t", c.parameters.AutoReconnect) - log.Printf("ReconnectInterval=%s", c.parameters.ReconnectInterval) - log.Printf("ReconnectAttempts=%d", c.parameters.ReconnectAttempts) - log.Printf("ShutdownTimeout=%s", c.parameters.ShutdownTimeout) - log.Printf("ResubscribeOnReconnect=%t", c.parameters.ResubscribeOnReconnect) - log.Printf("HeartbeatTimeout=%s", c.parameters.HeartbeatTimeout) - log.Printf("URL=%s", c.parameters.URL) -} - -// Connect to the Bitfinex API, this should only be called once. -func (c *Client) Connect() error { - c.dumpParams() - c.reset() - return c.connect() -} - -// reset assumes transport has already died or been closed -func (c *Client) reset() { - subs := c.subscriptions.Reset() - if subs != nil { - c.resetSubscriptions = subs - } - c.shutdown = make(chan bool) - c.init = true - c.asynchronous = c.asyncFactory.Create() - // wait for shutdown signals from child & caller - go c.listenDisconnect() - // listen to data from async - go c.listenUpstream() -} - -func (c *Client) connect() error { - err := c.asynchronous.Connect() - if err == nil { - c.isConnected = true - } - return err -} - -func (c *Client) reconnect(err error) error { - if c.terminal { - c.exit(err) - return err - } - if !c.parameters.AutoReconnect { - err := fmt.Errorf("AutoReconnect setting is disabled, do not reconnect: %s", err.Error()) - c.exit(err) - return err - } - for ; c.parameters.reconnectTry < c.parameters.ReconnectAttempts; c.parameters.reconnectTry++ { - log.Printf("waiting %s until reconnect...", c.parameters.ReconnectInterval) - time.Sleep(c.parameters.ReconnectInterval) - log.Printf("reconnect attempt %d/%d", c.parameters.reconnectTry+1, c.parameters.ReconnectAttempts) - c.reset() - err = c.connect() - if err == nil { - log.Print("reconnect OK") - c.parameters.reconnectTry = 0 - return nil - } - log.Printf("reconnect failed: %s", err.Error()) - } - if err != nil { - log.Printf("could not reconnect: %s", err.Error()) - } - return c.exit(err) -} - -func (c *Client) exit(err error) error { - c.terminal = true - c.close(err) - return err -} - -// start this goroutine before connecting, but this should die during a connection failure -func (c *Client) listenUpstream() { - for { - select { - case <-c.shutdown: - return // only exit point - case msg := <-c.asynchronous.Listen(): - if msg != nil { - // Errors here should be non critical so we just log them. - err := c.handleMessage(msg) - if err != nil { - log.Printf("[WARN]: %s\n", err) - } - } - } - } -} - -// terminal, unrecoverable state. called after async is closed. -func (c *Client) close(e error) { - if c.listener != nil { - if e != nil { - c.listener <- e - } - close(c.listener) - } - // shutdowns goroutines - close(c.shutdown) -} - -func (c *Client) closeAsyncAndWait(t time.Duration) { - if !c.init { - return - } - timeout := make(chan bool) - wg := sync.WaitGroup{} - wg.Add(1) - go func() { - select { - case <-c.asynchronous.Done(): // will this work? - wg.Done() - case <-timeout: - log.Print("blocking async shutdown timed out") - wg.Done() - } - }() - c.asynchronous.Close() - go func() { - time.Sleep(t) - close(timeout) - }() - wg.Wait() -} - -// Listen provides an atomic interface for receiving API messages. -// When a websocket connection is terminated, the publisher channel will close. -func (c *Client) Listen() <-chan interface{} { - return c.listener -} - -// Close provides an interface for a user initiated shutdown. -// Close will close the Done() channel. -func (c *Client) Close() { - c.terminal = true - c.closeAsyncAndWait(c.parameters.ShutdownTimeout) - c.subscriptions.Close() - - // clean shutdown waits on shutdown channel, which is triggered by cascading resource - // cleanups after a closed asynchronous transport - timeout := make(chan bool) - go func() { - time.Sleep(c.parameters.ShutdownTimeout) - close(timeout) - }() - select { - case <-c.shutdown: - return // successful cleanup - case <-timeout: - log.Print("shutdown timed out") - return - } -} - -func (c *Client) handleMessage(msg []byte) error { - t := bytes.TrimLeftFunc(msg, unicode.IsSpace) - err := error(nil) - // either a channel data array or an event object, raw json encoding - if bytes.HasPrefix(t, []byte("[")) { - err = c.handleChannel(msg) - } else if bytes.HasPrefix(t, []byte("{")) { - err = c.handleEvent(msg) - } else { - return fmt.Errorf("unexpected message: %s", msg) - } - return err -} - -func (c *Client) sendUnsubscribeMessage(ctx context.Context, chanID int64) error { - return c.asynchronous.Send(ctx, unsubscribeMsg{Event: "unsubscribe", ChanID: chanID}) -} - -func (c *Client) checkResubscription() { - if c.parameters.ResubscribeOnReconnect && c.resetSubscriptions != nil { - for _, sub := range c.resetSubscriptions { - if sub.Request.Event == "auth" { - continue - } - sub.Request.SubID = c.nonce.GetNonce() // new nonce - log.Printf("resubscribing to %s with nonce %s", sub.Request.String(), sub.Request.SubID) - _, err := c.Subscribe(context.Background(), sub.Request) - if err != nil { - log.Printf("could not resubscribe: %s", err.Error()) - } - } - } -} - -// called when an info event is received -func (c *Client) handleOpen() { - if c.hasCredentials() { - c.authenticate(context.Background()) - } else { - c.checkResubscription() - } -} - -// called when an auth event is received -func (c *Client) handleAuthAck(auth *AuthEvent) { - if c.Authentication == SuccessfulAuthentication { - err := c.subscriptions.activate(auth.SubID, auth.ChanID) - if err != nil { - log.Printf("could not activate auth subscription: %s", err.Error()) - } - c.checkResubscription() - } else { - log.Print("authentication failed") - } -} - -func (c *Client) hasCredentials() bool { - return c.apiKey != "" && c.apiSecret != "" -} - -// Unsubscribe looks up an existing subscription by ID and sends an unsubscribe request. -func (c *Client) Unsubscribe(ctx context.Context, id string) error { - sub, err := c.subscriptions.lookupBySubscriptionID(id) - if err != nil { - return err - } - // sub is removed from manager on ack from API - return c.sendUnsubscribeMessage(ctx, sub.ChanID) -} - -// Authenticate creates the payload for the authentication request and sends it -// to the API. The filters will be applied to the authenticated channel, i.e. -// only subscribe to the filtered messages. -func (c *Client) authenticate(ctx context.Context, filter ...string) error { - nonce := c.nonce.GetNonce() - - payload := "AUTH" + nonce - s := &SubscriptionRequest{ - Event: "auth", - APIKey: c.apiKey, - AuthSig: c.sign(payload), - AuthPayload: payload, - AuthNonce: nonce, - Filter: filter, - SubID: nonce, - } - if c.cancelOnDisconnect { - s.DMS = DMSCancelOnDisconnect - } - c.subscriptions.add(s) - - if err := c.asynchronous.Send(ctx, s); err != nil { - return err - } - c.Authentication = PendingAuthentication - - return nil -} diff --git a/v2/websocket/events.go b/v2/websocket/events.go deleted file mode 100644 index 21b6b6736..000000000 --- a/v2/websocket/events.go +++ /dev/null @@ -1,175 +0,0 @@ -package websocket - -import ( - "encoding/json" - "fmt" -) - -type eventType struct { - Event string `json:"event"` -} - -type InfoEvent struct { - Version float64 `json:"version"` -} - -type RawEvent struct { - Data interface{} -} - -func newPingEvent() *eventType { - return &eventType{ - Event: EventPing, - } -} - -type AuthEvent struct { - Event string `json:"event"` - Status string `json:"status"` - ChanID int64 `json:"chanId,omitempty"` - UserID int64 `json:"userId,omitempty"` - SubID string `json:"subId"` - AuthID string `json:"auth_id,omitempty"` - Message string `json:"msg,omitempty"` - Caps Capabilities `json:"caps"` -} - -type Capability struct { - Read int `json:"read"` - Write int `json:"write"` -} - -type Capabilities struct { - Orders Capability `json:"orders"` - Account Capability `json:"account"` - Funding Capability `json:"funding"` - History Capability `json:"history"` - Wallets Capability `json:"wallets"` - Withdraw Capability `json:"withdraw"` - Positions Capability `json:"positions"` -} - -// error codes pulled from v2 docs & API usage -const ( - ErrorCodeUnknownEvent int = 10000 - ErrorCodeUnknownPair = 10001 - ErrorCodeUnknownBookPrecision = 10011 - ErrorCodeUnknownBookLength = 10012 - ErrorCodeSubscriptionFailed = 10300 - ErrorCodeAlreadySubscribed = 10301 - ErrorCodeUnknownChannel = 10302 - ErrorCodeUnsubscribeFailed = 10400 - ErrorCodeNotSubscribed = 10401 -) - -type ErrorEvent struct { - Code int `json:"code"` - Message string `json:"msg"` - - // also contain members related to subscription reject - SubID string `json:"subId"` - Channel string `json:"channel"` - ChanID int64 `json:"chanId"` - Symbol string `json:"symbol"` - Precision string `json:"prec,omitempty"` - Frequency string `json:"freq,omitempty"` - Key string `json:"key,omitempty"` - Len string `json:"len,omitempty"` - Pair string `json:"pair"` -} - -type UnsubscribeEvent struct { - Status string `json:"status"` - ChanID int64 `json:"chanId"` -} - -type SubscribeEvent struct { - SubID string `json:"subId"` - Channel string `json:"channel"` - ChanID int64 `json:"chanId"` - Symbol string `json:"symbol"` - Precision string `json:"prec,omitempty"` - Frequency string `json:"freq,omitempty"` - Key string `json:"key,omitempty"` - Len string `json:"len,omitempty"` - Pair string `json:"pair"` -} - -type ConfEvent struct { - Flags int `json:"flags"` -} - -// onEvent handles all the event messages and connects SubID and ChannelID. -func (c *Client) handleEvent(msg []byte) error { - event := &eventType{} - err := json.Unmarshal(msg, event) - if err != nil { - return err - } - //var e interface{} - switch event.Event { - case "info": - i := InfoEvent{} - err = json.Unmarshal(msg, &i) - if err != nil { - return err - } - c.handleOpen() - c.listener <- &i - case "auth": - a := AuthEvent{} - err = json.Unmarshal(msg, &a) - if err != nil { - return err - } - if a.Status != "" && a.Status == "OK" { - c.Authentication = SuccessfulAuthentication - } else { - c.Authentication = RejectedAuthentication - } - c.handleAuthAck(&a) - c.listener <- &a - return nil - case "subscribed": - s := SubscribeEvent{} - err = json.Unmarshal(msg, &s) - if err != nil { - return err - } - err = c.subscriptions.activate(s.SubID, s.ChanID) - if err != nil { - return err - } - c.listener <- &s - return nil - case "unsubscribed": - s := UnsubscribeEvent{} - err = json.Unmarshal(msg, &s) - if err != nil { - return err - } - c.subscriptions.removeByChannelID(s.ChanID) - c.listener <- &s - case "error": - er := ErrorEvent{} - err = json.Unmarshal(msg, &er) - if err != nil { - return err - } - c.listener <- &er - case "conf": - ec := ConfEvent{} - err = json.Unmarshal(msg, &ec) - if err != nil { - return err - } - c.listener <- &ec - default: - return fmt.Errorf("unknown event: %s", msg) // TODO: or just log? - } - - //err = json.Unmarshal(msg, &e) - //TODO raw message isn't ever published - - return err -} diff --git a/v2/websocket/factories.go b/v2/websocket/factories.go deleted file mode 100644 index d186772a5..000000000 --- a/v2/websocket/factories.go +++ /dev/null @@ -1,130 +0,0 @@ -package websocket - -import ( - "github.com/bitfinexcom/bitfinex-api-go/v2" -) - -type messageFactory interface { - Build(chanID int64, objType string, raw []interface{}) (interface{}, error) - BuildSnapshot(chanID int64, raw [][]float64) (interface{}, error) -} - -type TickerFactory struct { - *subscriptions -} - -func newTickerFactory(subs *subscriptions) *TickerFactory { - return &TickerFactory{ - subscriptions: subs, - } -} - -func (f *TickerFactory) Build(chanID int64, objType string, raw []interface{}) (interface{}, error) { - sub, err := f.subscriptions.lookupByChannelID(chanID) - if err == nil { - tick, err := bitfinex.NewTickerFromRaw(sub.Request.Symbol, raw) - return tick, err - } - return nil, err -} - -func (f *TickerFactory) BuildSnapshot(chanID int64, raw [][]float64) (interface{}, error) { - sub, err := f.subscriptions.lookupByChannelID(chanID) - if err == nil { - return bitfinex.NewTickerSnapshotFromRaw(sub.Request.Symbol, raw) - } - return nil, err -} - -type TradeFactory struct { - *subscriptions -} - -func newTradeFactory(subs *subscriptions) *TradeFactory { - return &TradeFactory{ - subscriptions: subs, - } -} - -func (f *TradeFactory) Build(chanID int64, objType string, raw []interface{}) (interface{}, error) { - sub, err := f.subscriptions.lookupByChannelID(chanID) - if "tu" == objType { - return nil, nil // do not process TradeUpdate messages on public feed, only need to process TradeExecution (first copy seen) - } - if err == nil { - trade, err := bitfinex.NewTradeFromRaw(sub.Request.Symbol, raw) - return trade, err - } - return nil, err -} - -func (f *TradeFactory) BuildSnapshot(chanID int64, raw [][]float64) (interface{}, error) { - sub, err := f.subscriptions.lookupByChannelID(chanID) - if err == nil { - return bitfinex.NewTradeSnapshotFromRaw(sub.Request.Symbol, raw) - } - return nil, err -} - -type BookFactory struct { - *subscriptions -} - -func newBookFactory(subs *subscriptions) *BookFactory { - return &BookFactory{ - subscriptions: subs, - } -} - -func (f *BookFactory) Build(chanID int64, objType string, raw []interface{}) (interface{}, error) { - sub, err := f.subscriptions.lookupByChannelID(chanID) - if err == nil { - update, err := bitfinex.NewBookUpdateFromRaw(sub.Request.Symbol, sub.Request.Precision, raw) - return update, err - } - return nil, err -} - -func (f *BookFactory) BuildSnapshot(chanID int64, raw [][]float64) (interface{}, error) { - sub, err := f.subscriptions.lookupByChannelID(chanID) - if err == nil { - return bitfinex.NewBookUpdateSnapshotFromRaw(sub.Request.Symbol, sub.Request.Precision, raw) - } - return nil, err -} - -type CandlesFactory struct { - *subscriptions -} - -func newCandlesFactory(subs *subscriptions) *CandlesFactory { - return &CandlesFactory{ - subscriptions: subs, - } -} - -func (f *CandlesFactory) Build(chanID int64, objType string, raw []interface{}) (interface{}, error) { - sub, err := f.subscriptions.lookupByChannelID(chanID) - if err != nil { - return nil, err - } - sym, res, err := extractSymbolResolutionFromKey(sub.Request.Key) - if err != nil { - return nil, err - } - candle, err := bitfinex.NewCandleFromRaw(sym, res, raw) - return candle, err -} - -func (f *CandlesFactory) BuildSnapshot(chanID int64, raw [][]float64) (interface{}, error) { - sub, err := f.subscriptions.lookupByChannelID(chanID) - if err != nil { - return nil, err - } - sym, res, err := extractSymbolResolutionFromKey(sub.Request.Key) - if err != nil { - return nil, err - } - snap, err := bitfinex.NewCandleSnapshotFromRaw(sym, res, raw) - return snap, err -} diff --git a/v2/websocket/parameters.go b/v2/websocket/parameters.go deleted file mode 100644 index 9f652e54c..000000000 --- a/v2/websocket/parameters.go +++ /dev/null @@ -1,35 +0,0 @@ -package websocket - -import ( - "time" -) - -// Parameters defines adapter behavior. -type Parameters struct { - AutoReconnect bool - ReconnectInterval time.Duration - ReconnectAttempts int - reconnectTry int - ShutdownTimeout time.Duration - - ResubscribeOnReconnect bool - - HeartbeatTimeout time.Duration - LogTransport bool - - URL string -} - -func NewDefaultParameters() *Parameters { - return &Parameters{ - AutoReconnect: true, - ReconnectInterval: time.Second * 3, - reconnectTry: 0, - ReconnectAttempts: 5, - URL: productionBaseURL, - ShutdownTimeout: time.Second * 5, - ResubscribeOnReconnect: true, - HeartbeatTimeout: time.Second * 10, // HB = 5s - LogTransport: false, // log transport send/recv - } -} diff --git a/v2/websocket/subscriptions.go b/v2/websocket/subscriptions.go deleted file mode 100644 index 78b9895d2..000000000 --- a/v2/websocket/subscriptions.go +++ /dev/null @@ -1,283 +0,0 @@ -package websocket - -import ( - "fmt" - "log" - "sort" - "strings" - "sync" - "time" -) - -type SubscriptionRequest struct { - SubID string `json:"subId"` - Event string `json:"event"` - - // authenticated - APIKey string `json:"apiKey,omitempty"` - AuthSig string `json:"authSig,omitempty"` - AuthPayload string `json:"authPayload,omitempty"` - AuthNonce string `json:"authNonce,omitempty"` - Filter []string `json:"filter,omitempty"` - DMS int `json:"dms,omitempty"` // dead man switch - - // unauthenticated - Channel string `json:"channel,omitempty"` - Symbol string `json:"symbol,omitempty"` - Precision string `json:"prec,omitempty"` - Frequency string `json:"freq,omitempty"` - Key string `json:"key,omitempty"` - Len string `json:"len,omitempty"` - Pair string `json:"pair,omitempty"` -} - -func (s *SubscriptionRequest) String() string { - if s.Key == "" && s.Channel != "" && s.Symbol != "" { - return fmt.Sprintf("%s %s", s.Channel, s.Symbol) - } - if s.Channel != "" && s.Symbol != "" && s.Precision != "" && s.Frequency != "" { - return fmt.Sprintf("%s %s %s %s", s.Channel, s.Symbol, s.Precision, s.Frequency) - } - if s.Channel != "" && s.Symbol != "" { - return fmt.Sprintf("%s %s", s.Channel, s.Symbol) - } - return "" -} - -type UnsubscribeRequest struct { - Event string `json:"event"` - ChanID int64 `json:"chanId"` -} - -type subscription struct { - ChanID int64 - pending bool - Public bool - - Request *SubscriptionRequest - - hbDeadline time.Time -} - -func isPublic(request *SubscriptionRequest) bool { - switch request.Channel { - case ChanBook: - return true - case ChanCandles: - return true - case ChanTicker: - return true - case ChanTrades: - return true - } - return false -} - -func newSubscription(request *SubscriptionRequest) *subscription { - return &subscription{ - ChanID: -1, - Request: request, - pending: true, - Public: isPublic(request), - } -} - -func (s subscription) SubID() string { - return s.Request.SubID -} - -func (s subscription) Pending() bool { - return s.pending -} - -func newSubscriptions(heartbeatTimeout time.Duration) *subscriptions { - subs := &subscriptions{ - subsBySubID: make(map[string]*subscription), - subsByChanID: make(map[int64]*subscription), - hbTimeout: heartbeatTimeout, - hbShutdown: make(chan struct{}), - hbDisconnect: make(chan error), - hbSleep: heartbeatTimeout / time.Duration(4), - } - go subs.control() - return subs -} - -type heartbeat struct { - ChanID int64 - *time.Time -} - -type subscriptions struct { - lock sync.Mutex - - subsBySubID map[string]*subscription // subscription map indexed by subscription ID - subsByChanID map[int64]*subscription // subscription map indexed by channel ID - - hbActive bool - hbDisconnect chan error // disconnect parent due to heartbeat timeout - hbTimeout time.Duration - hbSleep time.Duration - hbShutdown chan struct{} -} - -// SubscriptionSet is a typed version of an array of subscription pointers, intended to meet the sortable interface. -// We need to sort Reset()'s return values for tests with more than 1 subscription (range map order is undefined) -type SubscriptionSet []*subscription - -func (s SubscriptionSet) Len() int { - return len(s) -} -func (s SubscriptionSet) Less(i, j int) bool { - return strings.Compare(s[i].SubID(), s[j].SubID()) < 0 -} -func (s SubscriptionSet) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} - -func (s *subscriptions) heartbeat(chanID int64) { - s.lock.Lock() - defer s.lock.Unlock() - if sub, ok := s.subsByChanID[chanID]; ok { - sub.hbDeadline = time.Now().Add(s.hbTimeout) - } -} - -func (s *subscriptions) sweep(exp time.Time) error { - s.lock.Lock() - defer s.lock.Unlock() - if !s.hbActive { - return nil - } - for _, sub := range s.subsByChanID { - if exp.After(sub.hbDeadline) { - s.hbActive = false - return fmt.Errorf("heartbeat disconnect on channel %d expired at %s (%s timeout)", sub.ChanID, sub.hbDeadline, s.hbTimeout) - } - } - return nil -} - -func (s *subscriptions) control() { - for { - select { - case <-s.hbShutdown: - return - default: - } - if err := s.sweep(time.Now()); err != nil { - s.hbDisconnect <- err - } - time.Sleep(s.hbSleep) - } -} - -// Close is terminal. Do not call heartbeat after close. -func (s *subscriptions) Close() { - s.reset() - close(s.hbShutdown) -} - -func (s *subscriptions) reset() []*subscription { - s.lock.Lock() - var subs []*subscription - if len(s.subsBySubID) > 0 { - subs = make([]*subscription, 0, len(s.subsBySubID)) - for _, sub := range s.subsBySubID { - subs = append(subs, sub) - } - sort.Sort(SubscriptionSet(subs)) - } - s.lock.Unlock() - return subs -} - -// Reset clears all subscriptions from the currently managed list, and returns -// a slice of the existing subscriptions prior to reset. Returns nil if no subscriptions exist. -func (s *subscriptions) Reset() []*subscription { - subs := s.reset() - s.lock.Lock() - s.hbActive = false - s.subsBySubID = make(map[string]*subscription) - s.subsByChanID = make(map[int64]*subscription) - s.lock.Unlock() - return subs -} - -// ListenDisconnect returns an error channel which receives a message when a heartbeat has expired a channel. -func (s *subscriptions) ListenDisconnect() <-chan error { - return s.hbDisconnect -} - -func (s *subscriptions) add(sub *SubscriptionRequest) *subscription { - s.lock.Lock() - defer s.lock.Unlock() - subscription := newSubscription(sub) - s.subsBySubID[sub.SubID] = subscription - return subscription -} - -func (s *subscriptions) removeByChannelID(chanID int64) error { - s.lock.Lock() - defer s.lock.Unlock() - sub, ok := s.subsByChanID[chanID] - if !ok { - return fmt.Errorf("could not find channel ID %d", chanID) - } - delete(s.subsByChanID, chanID) - if _, ok = s.subsBySubID[sub.SubID()]; ok { - delete(s.subsBySubID, sub.SubID()) - } - return nil -} - -func (s *subscriptions) removeBySubscriptionID(subID string) error { - s.lock.Lock() - defer s.lock.Unlock() - sub, ok := s.subsBySubID[subID] - if !ok { - return fmt.Errorf("could not find subscription ID %s to remove", subID) - } - // exists, remove both indices - delete(s.subsBySubID, subID) - if _, ok = s.subsByChanID[sub.ChanID]; ok { - delete(s.subsByChanID, sub.ChanID) - } - return nil -} - -func (s *subscriptions) activate(subID string, chanID int64) error { - s.lock.Lock() - defer s.lock.Unlock() - if sub, ok := s.subsBySubID[subID]; ok { - if chanID != 0 { - //log.Printf("%#v", sub.Request) - log.Printf("activated subscription %s %s for channel %d", sub.Request.Channel, sub.Request.Symbol, chanID) - } - sub.pending = false - sub.ChanID = chanID - sub.hbDeadline = time.Now().Add(s.hbTimeout) - s.subsByChanID[chanID] = sub - s.hbActive = true - return nil - } - return fmt.Errorf("could not find subscription ID %s to activate", subID) -} - -func (s *subscriptions) lookupByChannelID(chanID int64) (*subscription, error) { - s.lock.Lock() - defer s.lock.Unlock() - if sub, ok := s.subsByChanID[chanID]; ok { - return sub, nil - } - return nil, fmt.Errorf("could not find subscription for channel ID %d", chanID) -} - -func (s *subscriptions) lookupBySubscriptionID(subID string) (*subscription, error) { - s.lock.Lock() - defer s.lock.Unlock() - if sub, ok := s.subsBySubID[subID]; ok { - return sub, nil - } - return nil, fmt.Errorf("could not find subscription ID %s", subID) -} diff --git a/v2/websocket/transport.go b/v2/websocket/transport.go deleted file mode 100644 index 70c66e8f5..000000000 --- a/v2/websocket/transport.go +++ /dev/null @@ -1,165 +0,0 @@ -package websocket - -import ( - "context" - "crypto/tls" - "encoding/json" - "fmt" - "log" - "net" - "net/http" - "sync" - - "github.com/gorilla/websocket" -) - -func newWs(baseURL string, logTransport bool) *ws { - return &ws{ - BaseURL: baseURL, - downstream: make(chan []byte), - shutdown: make(chan struct{}), - finished: make(chan error), - logTransport: logTransport, - } -} - -type ws struct { - ws *websocket.Conn - wsLock sync.Mutex - BaseURL string - TLSSkipVerify bool - downstream chan []byte - userShutdown bool - logTransport bool - - shutdown chan struct{} // signal to kill looping goroutines - finished chan error // signal to parent with error, if applicable -} - -func (w *ws) Connect() error { - if w.ws != nil { - return nil // no op - } - w.wsLock.Lock() - defer w.wsLock.Unlock() - w.userShutdown = false - var d = websocket.Dialer{ - Subprotocols: []string{"p1", "p2"}, - ReadBufferSize: 1024, - WriteBufferSize: 1024, - Proxy: http.ProxyFromEnvironment, - } - - d.TLSClientConfig = &tls.Config{InsecureSkipVerify: w.TLSSkipVerify} - - log.Printf("connecting ws to %s", w.BaseURL) - ws, resp, err := d.Dial(w.BaseURL, nil) - if err != nil { - if err == websocket.ErrBadHandshake { - log.Printf("bad handshake: status code %d", resp.StatusCode) - } - return err - } - w.ws = ws - go w.listenWs() - return nil -} - -// Send marshals the given interface and then sends it to the API. This method -// can block so specify a context with timeout if you don't want to wait for too -// long. -func (w *ws) Send(ctx context.Context, msg interface{}) error { - if w.ws == nil { - return ErrWSNotConnected - } - - bs, err := json.Marshal(msg) - if err != nil { - return err - } - - select { - case <-ctx.Done(): - return ctx.Err() - case <-w.shutdown: // abrupt ws shutdown - return fmt.Errorf("websocket connection closed") - default: - } - - w.wsLock.Lock() - defer w.wsLock.Unlock() - if w.logTransport { - log.Printf("ws->srv: %s", string(bs)) - } - err = w.ws.WriteMessage(websocket.TextMessage, bs) - if err != nil { - w.cleanup(err) - return err - } - - return nil -} - -func (w *ws) Done() <-chan error { - return w.finished -} - -// listen on ws & fwd to listen() -func (w *ws) listenWs() { - for { - if w.ws == nil { - return - } - - select { - case <-w.shutdown: // external shutdown request - return - default: - } - - _, msg, err := w.ws.ReadMessage() - if err != nil { - if cl, ok := err.(*websocket.CloseError); ok { - log.Printf("close error code: %d", cl.Code) - } - // a read during normal shutdown results in an OpError: op on closed connection - if _, ok := err.(*net.OpError); ok { - // general read error on a closed network connection, OK - w.cleanup(nil) - return - } - w.cleanup(err) - return - } - if w.logTransport { - log.Printf("srv->ws: %s", string(msg)) - } - w.downstream <- msg - } -} - -func (w *ws) Listen() <-chan []byte { - return w.downstream -} - -func (w *ws) cleanup(err error) { - close(w.downstream) // shut down caller's listen channel - close(w.shutdown) // signal to kill goroutines - if err != nil && !w.userShutdown { - w.finished <- err - } - close(w.finished) // signal to parent listeners -} - -// Close the websocket connection -func (w *ws) Close() { - w.wsLock.Lock() - w.userShutdown = true - if w.ws != nil { - if err := w.ws.Close(); err != nil { // will trigger cleanup() - log.Printf("[INFO]: error closing websocket: %s", err) - } - w.ws = nil - } - w.wsLock.Unlock() -} From e6cca0601103dee3d4cc2e90b63a58a8350944ab Mon Sep 17 00:00:00 2001 From: wt5RM2 Date: Fri, 20 Jul 2018 09:23:04 +0300 Subject: [PATCH 3/3] Delete examples and helper for v2 --- examples/v2/book-feed/main.go | 45 ------------------------- examples/v2/rest-orders/main.go | 60 --------------------------------- examples/v2/trade-feed/main.go | 44 ------------------------ examples/v2/ws-book/README.md | 4 --- examples/v2/ws-book/main.go | 45 ------------------------- examples/v2/ws-private/main.go | 41 ---------------------- utils/nonce.go | 27 --------------- 7 files changed, 266 deletions(-) delete mode 100644 examples/v2/book-feed/main.go delete mode 100644 examples/v2/rest-orders/main.go delete mode 100644 examples/v2/trade-feed/main.go delete mode 100644 examples/v2/ws-book/README.md delete mode 100644 examples/v2/ws-book/main.go delete mode 100644 examples/v2/ws-private/main.go diff --git a/examples/v2/book-feed/main.go b/examples/v2/book-feed/main.go deleted file mode 100644 index a8f2a39e1..000000000 --- a/examples/v2/book-feed/main.go +++ /dev/null @@ -1,45 +0,0 @@ -package main - -import ( - "context" - "github.com/bitfinexcom/bitfinex-api-go/v2" - "github.com/bitfinexcom/bitfinex-api-go/v2/websocket" - "log" - "net/http" - _ "net/http/pprof" - "os" - "os/signal" -) - -func main() { - client := websocket.New() - err := client.Connect() - if err != nil { - log.Printf("could not connect: %s", err.Error()) - return - } - go func() { - for msg := range client.Listen() { - log.Printf("recv: %#v", msg) - if _, ok := msg.(*websocket.InfoEvent); ok { - _, err := client.SubscribeBook(context.Background(), "BTCUSD", bitfinex.Precision0, bitfinex.FrequencyRealtime, 1) - if err != nil { - log.Printf("could not subscribe to book: %s", err.Error()) - } - } - } - }() - done := make(chan bool, 1) - interrupt := make(chan os.Signal) - signal.Notify(interrupt, os.Interrupt, os.Kill) - go func() { - log.Println(http.ListenAndServe("localhost:6060", nil)) - }() - go func() { - <-interrupt - client.Close() - done <- true - os.Exit(0) - }() - <-done -} diff --git a/examples/v2/rest-orders/main.go b/examples/v2/rest-orders/main.go deleted file mode 100644 index 8ab981371..000000000 --- a/examples/v2/rest-orders/main.go +++ /dev/null @@ -1,60 +0,0 @@ -package main - -import ( - "flag" - "log" - "os" - "strconv" - - "github.com/bitfinexcom/bitfinex-api-go/v2" - "github.com/bitfinexcom/bitfinex-api-go/v2/rest" -) - -var ( - orderid = flag.String("id", "", "lookup trades for an order ID") - api = flag.String("api", "https://api.bitfinex.com/v2/", "v2 REST API URL") -) - -// Set BFX_APIKEY and BFX_SECRET as : -// -// export BFX_API_KEY=YOUR_API_KEY -// export BFX_API_SECRET=YOUR_API_SECRET -// -// you can obtain it from https://www.bitfinex.com/api - -func main() { - flag.Parse() - - key := os.Getenv("BFX_API_KEY") - secret := os.Getenv("BFX_API_SECRET") - c := rest.NewClientWithURL(*api).Credentials(key, secret) - - available, err := c.Platform.Status() - if err != nil { - log.Fatalf("getting status: %s", err) - } - - if !available { - log.Fatalf("API not available") - } - - if *orderid != "" { - ordid, err := strconv.ParseInt(*orderid, 10, 64) - if err != nil { - log.Fatal(err) - } - os, err := c.Orders.OrderTrades(bitfinex.TradingPrefix+bitfinex.BTCUSD, ordid) - if err != nil { - log.Fatalf("getting order trades: %s", err) - } - - log.Printf("order trades: %#v\n", os) - } else { - os, err := c.Orders.History(bitfinex.TradingPrefix + bitfinex.BTCUSD) - if err != nil { - log.Fatalf("getting orders: %s", err) - } - - log.Printf("orders: %#v\n", os) - } -} diff --git a/examples/v2/trade-feed/main.go b/examples/v2/trade-feed/main.go deleted file mode 100644 index 36e7fa0a1..000000000 --- a/examples/v2/trade-feed/main.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "context" - "github.com/bitfinexcom/bitfinex-api-go/v2/websocket" - "log" - "net/http" - _ "net/http/pprof" - "os" - "os/signal" -) - -func main() { - client := websocket.New() - err := client.Connect() - if err != nil { - log.Printf("could not connect: %s", err.Error()) - return - } - go func() { - for msg := range client.Listen() { - log.Printf("recv: %#v", msg) - if _, ok := msg.(*websocket.InfoEvent); ok { - _, err := client.SubscribeTrades(context.Background(), "tBTCUSD") - if err != nil { - log.Printf("could not subscribe to trades: %s", err.Error()) - } - } - } - }() - done := make(chan bool, 1) - interrupt := make(chan os.Signal) - signal.Notify(interrupt, os.Interrupt, os.Kill) - go func() { - log.Println(http.ListenAndServe("localhost:6060", nil)) - }() - go func() { - <-interrupt - client.Close() - done <- true - os.Exit(0) - }() - <-done -} diff --git a/examples/v2/ws-book/README.md b/examples/v2/ws-book/README.md deleted file mode 100644 index 0687943ba..000000000 --- a/examples/v2/ws-book/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Websocket public book - -This is a simple example showing off how to connect to the websocket service -and subscribe to one of the public ticker channels. diff --git a/examples/v2/ws-book/main.go b/examples/v2/ws-book/main.go deleted file mode 100644 index 6b64ba24f..000000000 --- a/examples/v2/ws-book/main.go +++ /dev/null @@ -1,45 +0,0 @@ -package main - -import ( - "context" - "log" - "time" - - "github.com/bitfinexcom/bitfinex-api-go/v2" - "github.com/bitfinexcom/bitfinex-api-go/v2/websocket" -) - -func main() { - c := websocket.New() - - err := c.Connect() - if err != nil { - log.Fatal("Error connecting to web socket : ", err) - } - - // subscribe to BTCUSD book - ctx, cxl1 := context.WithTimeout(context.Background(), time.Second*5) - defer cxl1() - _, err = c.SubscribeBook(ctx, bitfinex.TradingPrefix+bitfinex.BTCUSD, bitfinex.Precision0, bitfinex.FrequencyRealtime, 25) - if err != nil { - log.Fatal(err) - } - - // subscribe to BTCUSD trades - ctx, cxl2 := context.WithTimeout(context.Background(), time.Second*5) - defer cxl2() - _, err = c.SubscribeTrades(ctx, bitfinex.TradingPrefix+bitfinex.BTCUSD) - if err != nil { - log.Fatal(err) - } - - for obj := range c.Listen() { - switch obj.(type) { - case error: - log.Printf("channel closed: %s", obj) - break - default: - } - log.Printf("MSG RECV: %#v", obj) - } -} diff --git a/examples/v2/ws-private/main.go b/examples/v2/ws-private/main.go deleted file mode 100644 index 7043c5968..000000000 --- a/examples/v2/ws-private/main.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -import ( - "log" - "os" - "time" - - "github.com/bitfinexcom/bitfinex-api-go/v2/websocket" -) - -// Set BFX_APIKEY and BFX_SECRET as : -// -// export BFX_API_KEY=YOUR_API_KEY -// export BFX_API_SECRET=YOUR_API_SECRET -// -// you can obtain it from https://www.bitfinex.com/api - -func main() { - - uri := os.Getenv("BFX_API_URI") - key := os.Getenv("BFX_API_KEY") - secret := os.Getenv("BFX_API_SECRET") - p := websocket.NewDefaultParameters() - p.URL = uri - c := websocket.NewWithParams(p).Credentials(key, secret) - - err := c.Connect() - if err != nil { - log.Fatalf("connecting authenticated websocket: %s", err) - } - go func() { - for msg := range c.Listen() { - log.Printf("MSG RECV: %#v", msg) - } - }() - - //ctx, _ := context.WithTimeout(context.Background(), time.Second*1) - //c.Authenticate(ctx) - - time.Sleep(time.Second * 10) -} diff --git a/utils/nonce.go b/utils/nonce.go index 5734dab43..1703d75d3 100644 --- a/utils/nonce.go +++ b/utils/nonce.go @@ -2,36 +2,9 @@ package utils import ( "fmt" - "strconv" - "sync/atomic" "time" ) -// v2 types - -type NonceGenerator interface { - GetNonce() string -} - -type EpochNonceGenerator struct { - nonce uint64 -} - -// GetNonce is a naive nonce producer that takes the current Unix nano epoch -// and counts upwards. -// This is a naive approach because the nonce bound to the currently used API -// key and as such needs to be synchronised with other instances using the same -// key in order to avoid race conditions. -func (u *EpochNonceGenerator) GetNonce() string { - return strconv.FormatUint(atomic.AddUint64(&u.nonce, 1), 10) -} - -func NewEpochNonceGenerator() *EpochNonceGenerator { - return &EpochNonceGenerator{ - nonce: uint64(time.Now().Unix()) * 1000, - } -} - // v1 support const multiplier = 10000