diff --git a/v2/futures/client_ws.go b/v2/common/websocket/client.go similarity index 75% rename from v2/futures/client_ws.go rename to v2/common/websocket/client.go index 9a93268c..e5b40590 100644 --- a/v2/futures/client_ws.go +++ b/v2/common/websocket/client.go @@ -1,4 +1,4 @@ -package futures +package websocket import ( "context" @@ -13,11 +13,9 @@ import ( "github.com/gorilla/websocket" "github.com/jpillora/backoff" - - "github.com/adshao/go-binance/v2/common" ) -//go:generate mockgen -source client_ws.go -destination mock/client_ws.go -package mock +//go:generate mockgen -source client.go -destination mock/client.go -package mock const ( // reconnectMinInterval define reconnect min interval @@ -33,6 +31,9 @@ var ( // ErrorWsIdAlreadySent defines that request with the same id was already sent ErrorWsIdAlreadySent = errors.New("ws error: request with same id already sent") + + // KeepAlivePingDeadline defines deadline to send ping frame + KeepAlivePingDeadline = 10 * time.Second ) // messageId define id field of request/response @@ -40,15 +41,11 @@ type messageId struct { Id string `json:"id"` } -// ClientWs define API websocket client -type ClientWs struct { - APIKey string - SecretKey string +// client define API websocket client +type client struct { Debug bool - KeyType string - TimeOffset int64 logger *log.Logger - conn wsConnection + conn Connection connMu sync.Mutex reconnectSignal chan struct{} connectionEstablishedSignal chan struct{} @@ -58,18 +55,15 @@ type ClientWs struct { reconnectCount int64 } -func (c *ClientWs) debug(format string, v ...interface{}) { +func (c *client) debug(format string, v ...interface{}) { if c.Debug { c.logger.Println(fmt.Sprintf(format, v...)) } } -// NewClientWs init ClientWs -func NewClientWs(conn wsConnection, apiKey, secretKey string) (*ClientWs, error) { - client := &ClientWs{ - APIKey: apiKey, - SecretKey: secretKey, - KeyType: common.KeyTypeHmac, +// NewClient init client +func NewClient(conn Connection) (Client, error) { + client := &client{ logger: log.New(os.Stderr, "Binance-golang ", log.LstdFlags), conn: conn, connMu: sync.Mutex{}, @@ -86,21 +80,17 @@ func NewClientWs(conn wsConnection, apiKey, secretKey string) (*ClientWs, error) return client, nil } -type wsClient interface { +type Client interface { Write(id string, data []byte) error WriteSync(id string, data []byte, timeout time.Duration) ([]byte, error) GetReadChannel() <-chan []byte GetReadErrorChannel() <-chan error - GetApiKey() string - GetSecretKey() string - GetTimeOffset() int64 - GetKeyType() string GetReconnectCount() int64 Wait(timeout time.Duration) } // Write sends data into websocket connection -func (c *ClientWs) Write(id string, data []byte) error { +func (c *client) Write(id string, data []byte) error { c.connMu.Lock() defer c.connMu.Unlock() @@ -120,7 +110,7 @@ func (c *ClientWs) Write(id string, data []byte) error { // WriteSync sends data to the websocket connection and waits for a response synchronously // Should be used separately from the asynchronous Write method (do not send anything in parallel) -func (c *ClientWs) WriteSync(id string, data []byte, timeout time.Duration) ([]byte, error) { +func (c *client) WriteSync(id string, data []byte, timeout time.Duration) ([]byte, error) { c.connMu.Lock() defer c.connMu.Unlock() @@ -157,36 +147,20 @@ func (c *ClientWs) WriteSync(id string, data []byte, timeout time.Duration) ([]b } } -func (c *ClientWs) GetReadChannel() <-chan []byte { +func (c *client) GetReadChannel() <-chan []byte { return c.readC } -func (c *ClientWs) GetReadErrorChannel() <-chan error { +func (c *client) GetReadErrorChannel() <-chan error { return c.readErrChan } -func (c *ClientWs) GetApiKey() string { - return c.APIKey -} - -func (c *ClientWs) GetSecretKey() string { - return c.SecretKey -} - -func (c *ClientWs) GetTimeOffset() int64 { - return c.TimeOffset -} - -func (c *ClientWs) GetKeyType() string { - return c.KeyType -} - -func (c *ClientWs) Wait(timeout time.Duration) { +func (c *client) Wait(timeout time.Duration) { c.wait(timeout) } // read data from connection -func (c *ClientWs) read() { +func (c *client) read() { defer func() { // reading from closed connection 1000 times caused panic // prevent panic for any case @@ -231,7 +205,7 @@ func (c *ClientWs) read() { // wait until all responses received // make sure that you are not sending requests -func (c *ClientWs) wait(timeout time.Duration) { +func (c *client) wait(timeout time.Duration) { doneC := make(chan struct{}) ctx, cancel := context.WithCancel(context.Background()) @@ -260,7 +234,7 @@ func (c *ClientWs) wait(timeout time.Duration) { } // handleReconnect waits for reconnect signal and starts reconnect -func (c *ClientWs) handleReconnect() { +func (c *client) handleReconnect() { for _ = range c.reconnectSignal { c.debug("reconnect: received signal") @@ -285,10 +259,10 @@ func (c *ClientWs) handleReconnect() { } // startReconnect starts reconnect loop with increasing delay -func (c *ClientWs) startReconnect(b *backoff.Backoff) *connection { +func (c *client) startReconnect(b *backoff.Backoff) Connection { for { atomic.AddInt64(&c.reconnectCount, 1) - conn, err := newConnection() + conn, err := c.conn.RestoreConnection() if err != nil { delay := b.Duration() c.debug("reconnect: error while reconnecting. try in %s", delay.Round(time.Millisecond)) @@ -301,7 +275,9 @@ func (c *ClientWs) startReconnect(b *backoff.Backoff) *connection { } // GetReconnectCount returns reconnect counter value -func (c *ClientWs) GetReconnectCount() int64 { return atomic.LoadInt64(&c.reconnectCount) } +func (c *client) GetReconnectCount() int64 { + return atomic.LoadInt64(&c.reconnectCount) +} // NewRequestList creates request list func NewRequestList() RequestList { @@ -356,37 +332,47 @@ func (l *RequestList) IsAlreadyInList(id string) bool { return false } -// constructor for connection -func newConnection() (*connection, error) { - conn, err := WsApiInitReadWriteConn() +// NewConnection constructor for connection +func NewConnection( + initUnderlyingWsConnFn func() (*websocket.Conn, error), + isKeepAliveNeeded bool, + keepaliveTimeout time.Duration, +) (Connection, error) { + underlyingWsConn, err := initUnderlyingWsConnFn() if err != nil { return nil, err } wsConn := &connection{ - conn: conn, - connectionMu: sync.Mutex{}, - lastResponseMu: sync.Mutex{}, + conn: underlyingWsConn, + connectionMu: sync.Mutex{}, + lastResponseMu: sync.Mutex{}, + initUnderlyingWsConnFn: initUnderlyingWsConnFn, + keepaliveTimeout: keepaliveTimeout, } - if WebsocketKeepalive { - go wsConn.keepAlive(WebsocketTimeoutReadWriteConnection) + if isKeepAliveNeeded { + go wsConn.keepAlive(keepaliveTimeout) } return wsConn, nil } -// instance of single connection with keepalive handler +// connection is an instance of single ws connection with keepalive handler type connection struct { - conn *websocket.Conn - connectionMu sync.Mutex - lastResponse time.Time - lastResponseMu sync.Mutex + conn *websocket.Conn + connectionMu sync.Mutex + lastResponse time.Time + lastResponseMu sync.Mutex + initUnderlyingWsConnFn func() (*websocket.Conn, error) + keepaliveTimeout time.Duration + isKeepAliveNeeded bool } -type wsConnection interface { +type Connection interface { WriteMessage(messageType int, data []byte) error ReadMessage() (messageType int, p []byte, err error) + RestoreConnection() (Connection, error) } // WriteMessage is a thread-safe method for conn.WriteMessage @@ -401,6 +387,11 @@ func (c *connection) ReadMessage() (int, []byte, error) { return c.conn.ReadMessage() } +// RestoreConnection recreates ws connection with the same underlying connection callback and keepalive timeout +func (c *connection) RestoreConnection() (Connection, error) { + return NewConnection(c.initUnderlyingWsConnFn, c.isKeepAliveNeeded, c.keepaliveTimeout) +} + // keepAlive handles ping-pong for connection func (c *connection) keepAlive(timeout time.Duration) { ticker := time.NewTicker(timeout) @@ -455,7 +446,7 @@ func (c *connection) ping() error { c.connectionMu.Lock() defer c.connectionMu.Unlock() - deadline := time.Now().Add(10 * time.Second) + deadline := time.Now().Add(KeepAlivePingDeadline) err := c.conn.WriteControl(websocket.PingMessage, []byte{}, deadline) if err != nil { return err @@ -463,33 +454,3 @@ func (c *connection) ping() error { return nil } - -// NewOrderPlaceWsService init OrderPlaceWsService -func NewOrderPlaceWsService(apiKey, secretKey string) (*OrderPlaceWsService, error) { - conn, err := newConnection() - if err != nil { - return nil, err - } - - client, err := NewClientWs(conn, apiKey, secretKey) - if err != nil { - return nil, err - } - - return &OrderPlaceWsService{c: client}, nil -} - -// NewOrderCancelWsService init OrderCancelWsService -func NewOrderCancelWsService(apiKey, secretKey string) (*OrderCancelWsService, error) { - conn, err := newConnection() - if err != nil { - return nil, err - } - - client, err := NewClientWs(conn, apiKey, secretKey) - if err != nil { - return nil, err - } - - return &OrderCancelWsService{c: client}, nil -} diff --git a/v2/futures/client_ws_test.go b/v2/common/websocket/client_test.go similarity index 81% rename from v2/futures/client_ws_test.go rename to v2/common/websocket/client_test.go index 50d1d7dc..491a7b3a 100644 --- a/v2/futures/client_ws_test.go +++ b/v2/common/websocket/client_test.go @@ -1,4 +1,4 @@ -package futures +package websocket import ( "context" @@ -14,22 +14,28 @@ import ( "github.com/stretchr/testify/suite" ) -func (s *clientWsTestSuite) SetupTest() { +type testApiRequest struct { + Id string `json:"id"` + Method string `json:"method"` + Params map[string]interface{} `json:"params"` +} + +func (s *clientTestSuite) SetupTest() { s.apiKey = "dummyApiKey" s.secretKey = "dummySecretKey" } -type clientWsTestSuite struct { +type clientTestSuite struct { suite.Suite apiKey string secretKey string } -func TestClientWs(t *testing.T) { - suite.Run(t, new(clientWsTestSuite)) +func TestClient(t *testing.T) { + suite.Run(t, new(clientTestSuite)) } -func (s *clientWsTestSuite) TestReadWriteSync() { +func (s *clientTestSuite) TestReadWriteSync() { stopCh := make(chan struct{}) go func() { startWsTestServer(stopCh) @@ -38,13 +44,23 @@ func (s *clientWsTestSuite) TestReadWriteSync() { stopCh <- struct{}{} }() - useLocalhost = true - WebsocketKeepalive = true + conn, err := NewConnection(func() (*websocket.Conn, error) { + Dialer := websocket.Dialer{ + Proxy: http.ProxyFromEnvironment, + HandshakeTimeout: 45 * time.Second, + EnableCompression: false, + } + + c, _, err := Dialer.Dial("ws://localhost:8080/ws", nil) + if err != nil { + return nil, err + } - conn, err := newConnection() + return c, nil + }, true, 10*time.Second) s.Require().NoError(err) - client, err := NewClientWs(conn, s.apiKey, s.secretKey) + client, err := NewClient(conn) s.Require().NoError(err) tests := []struct { @@ -58,7 +74,7 @@ func (s *clientWsTestSuite) TestReadWriteSync() { s.Require().NoError(err) requestID := id.String() - req := WsApiRequest{ + req := testApiRequest{ Id: requestID, Method: "some-method", Params: map[string]interface{}{}, @@ -66,7 +82,7 @@ func (s *clientWsTestSuite) TestReadWriteSync() { reqRaw, err := json.Marshal(req) s.Require().NoError(err) - responseRaw, err := client.WriteSync(requestID, reqRaw, WriteSyncWsTimeout) + responseRaw, err := client.WriteSync(requestID, reqRaw, 5*time.Second) s.Require().NoError(err) s.Require().Equal(reqRaw, responseRaw) }, @@ -78,7 +94,7 @@ func (s *clientWsTestSuite) TestReadWriteSync() { s.Require().NoError(err) requestID := id.String() - req := WsApiRequest{ + req := testApiRequest{ Id: "some-other-request-id", Method: "some-method", Params: map[string]interface{}{}, @@ -89,7 +105,7 @@ func (s *clientWsTestSuite) TestReadWriteSync() { err = client.Write(requestID, reqRaw) s.Require().NoError(err) - req = WsApiRequest{ + req = testApiRequest{ Id: requestID, Method: "some-method", Params: map[string]interface{}{}, @@ -97,7 +113,7 @@ func (s *clientWsTestSuite) TestReadWriteSync() { reqRaw, err = json.Marshal(req) s.Require().NoError(err) - responseRaw, err := client.WriteSync(requestID, reqRaw, WriteSyncWsTimeout) + responseRaw, err := client.WriteSync(requestID, reqRaw, 5*time.Second) s.Require().NoError(err) s.Require().Equal(reqRaw, responseRaw) }, @@ -109,7 +125,7 @@ func (s *clientWsTestSuite) TestReadWriteSync() { s.Require().NoError(err) requestID := id.String() - req := WsApiRequest{ + req := testApiRequest{ Id: requestID, Method: "some-method", Params: map[string]interface{}{ @@ -131,7 +147,7 @@ func (s *clientWsTestSuite) TestReadWriteSync() { s.Require().NoError(err) requestID := id.String() - req := WsApiRequest{ + req := testApiRequest{ Id: requestID, Method: "some-method", Params: map[string]interface{}{}, @@ -197,7 +213,7 @@ func wsHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Received message: %s\n", message) - req := WsApiRequest{} + req := testApiRequest{} if err := json.Unmarshal(message, &req); err != nil { log.Println("Error unmarshalling message:", err) continue diff --git a/v2/common/websocket/mock/client.go b/v2/common/websocket/mock/client.go new file mode 100644 index 00000000..1e05558e --- /dev/null +++ b/v2/common/websocket/mock/client.go @@ -0,0 +1,187 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: client.go + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + time "time" + + websocket "github.com/adshao/go-binance/v2/common/websocket" + gomock "github.com/golang/mock/gomock" +) + +// MockClient is a mock of Client interface. +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient. +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance. +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// GetReadChannel mocks base method. +func (m *MockClient) GetReadChannel() <-chan []byte { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetReadChannel") + ret0, _ := ret[0].(<-chan []byte) + return ret0 +} + +// GetReadChannel indicates an expected call of GetReadChannel. +func (mr *MockClientMockRecorder) GetReadChannel() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReadChannel", reflect.TypeOf((*MockClient)(nil).GetReadChannel)) +} + +// GetReadErrorChannel mocks base method. +func (m *MockClient) GetReadErrorChannel() <-chan error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetReadErrorChannel") + ret0, _ := ret[0].(<-chan error) + return ret0 +} + +// GetReadErrorChannel indicates an expected call of GetReadErrorChannel. +func (mr *MockClientMockRecorder) GetReadErrorChannel() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReadErrorChannel", reflect.TypeOf((*MockClient)(nil).GetReadErrorChannel)) +} + +// GetReconnectCount mocks base method. +func (m *MockClient) GetReconnectCount() int64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetReconnectCount") + ret0, _ := ret[0].(int64) + return ret0 +} + +// GetReconnectCount indicates an expected call of GetReconnectCount. +func (mr *MockClientMockRecorder) GetReconnectCount() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReconnectCount", reflect.TypeOf((*MockClient)(nil).GetReconnectCount)) +} + +// Wait mocks base method. +func (m *MockClient) Wait(timeout time.Duration) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Wait", timeout) +} + +// Wait indicates an expected call of Wait. +func (mr *MockClientMockRecorder) Wait(timeout interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wait", reflect.TypeOf((*MockClient)(nil).Wait), timeout) +} + +// Write mocks base method. +func (m *MockClient) Write(id string, data []byte) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Write", id, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// Write indicates an expected call of Write. +func (mr *MockClientMockRecorder) Write(id, data interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockClient)(nil).Write), id, data) +} + +// WriteSync mocks base method. +func (m *MockClient) WriteSync(id string, data []byte, timeout time.Duration) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WriteSync", id, data, timeout) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// WriteSync indicates an expected call of WriteSync. +func (mr *MockClientMockRecorder) WriteSync(id, data, timeout interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteSync", reflect.TypeOf((*MockClient)(nil).WriteSync), id, data, timeout) +} + +// MockConnection is a mock of Connection interface. +type MockConnection struct { + ctrl *gomock.Controller + recorder *MockConnectionMockRecorder +} + +// MockConnectionMockRecorder is the mock recorder for MockConnection. +type MockConnectionMockRecorder struct { + mock *MockConnection +} + +// NewMockConnection creates a new mock instance. +func NewMockConnection(ctrl *gomock.Controller) *MockConnection { + mock := &MockConnection{ctrl: ctrl} + mock.recorder = &MockConnectionMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockConnection) EXPECT() *MockConnectionMockRecorder { + return m.recorder +} + +// ReadMessage mocks base method. +func (m *MockConnection) ReadMessage() (int, []byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReadMessage") + ret0, _ := ret[0].(int) + ret1, _ := ret[1].([]byte) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// ReadMessage indicates an expected call of ReadMessage. +func (mr *MockConnectionMockRecorder) ReadMessage() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadMessage", reflect.TypeOf((*MockConnection)(nil).ReadMessage)) +} + +// RestoreConnection mocks base method. +func (m *MockConnection) RestoreConnection() (websocket.Connection, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RestoreConnection") + ret0, _ := ret[0].(websocket.Connection) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RestoreConnection indicates an expected call of RestoreConnection. +func (mr *MockConnectionMockRecorder) RestoreConnection() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestoreConnection", reflect.TypeOf((*MockConnection)(nil).RestoreConnection)) +} + +// WriteMessage mocks base method. +func (m *MockConnection) WriteMessage(messageType int, data []byte) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WriteMessage", messageType, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// WriteMessage indicates an expected call of WriteMessage. +func (mr *MockConnectionMockRecorder) WriteMessage(messageType, data interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteMessage", reflect.TypeOf((*MockConnection)(nil).WriteMessage), messageType, data) +} diff --git a/v2/common/websocket/types.go b/v2/common/websocket/types.go new file mode 100644 index 00000000..d4d219b1 --- /dev/null +++ b/v2/common/websocket/types.go @@ -0,0 +1,139 @@ +package websocket + +import ( + "encoding/json" + "errors" + "fmt" + "net/url" + "time" + + "github.com/adshao/go-binance/v2/common" +) + +// WsApiMethodType define method name for websocket API +type WsApiMethodType string + +// WsApiRequest define common websocket API request +type WsApiRequest struct { + Id string `json:"id"` + Method WsApiMethodType `json:"method"` + Params map[string]interface{} `json:"params"` +} + +var ( + // WriteSyncWsTimeout defines timeout for WriteSync method of client_ws + WriteSyncWsTimeout = 5 * time.Second +) + +const ( + // apiKey define key for websocket API parameters + apiKey = "apiKey" + + // timestampKey define key for websocket API parameters + timestampKey = "timestamp" + + // signatureKey define key for websocket API parameters + signatureKey = "signature" + + // SPOT + + // OrderPlaceSpotWsApiMethod define method for creation order via websocket API + OrderPlaceSpotWsApiMethod WsApiMethodType = "order.place" + + // FUTURES + + // OrderPlaceFuturesWsApiMethod define method for creation order via websocket API + OrderPlaceFuturesWsApiMethod WsApiMethodType = "order.place" + + // CancelFuturesWsApiMethod define method for cancel order via websocket API + CancelFuturesWsApiMethod WsApiMethodType = "order.cancel" +) + +var ( + // ErrorRequestIDNotSet defines that request ID is not set + ErrorRequestIDNotSet = errors.New("ws service: request id is not set") + + // ErrorApiKeyIsNotSet defines that ApiKey is not set + ErrorApiKeyIsNotSet = errors.New("ws service: api key is not set") + + // ErrorSecretKeyIsNotSet defines that SecretKey is not set + ErrorSecretKeyIsNotSet = errors.New("ws service: secret key is not set") +) + +func NewRequestData( + requestID string, + apiKey string, + secretKey string, + timeOffset int64, + keyType string, +) RequestData { + return RequestData{ + requestID: requestID, + apiKey: apiKey, + secretKey: secretKey, + timeOffset: timeOffset, + keyType: keyType, + } +} + +type RequestData struct { + requestID string + apiKey string + secretKey string + timeOffset int64 + keyType string +} + +// CreateRequest creates signed ws request +func CreateRequest(reqData RequestData, method WsApiMethodType, params map[string]interface{}) ([]byte, error) { + if reqData.requestID == "" { + return nil, ErrorRequestIDNotSet + } + + if reqData.apiKey == "" { + return nil, ErrorApiKeyIsNotSet + } + + if reqData.secretKey == "" { + return nil, ErrorSecretKeyIsNotSet + } + + params[apiKey] = reqData.apiKey + params[timestampKey] = timestamp(reqData.timeOffset) + + sf, err := common.SignFunc(reqData.keyType) + if err != nil { + return nil, err + } + signature, err := sf(reqData.secretKey, encodeParams(params)) + if err != nil { + return nil, err + } + params[signatureKey] = signature + + req := WsApiRequest{ + Id: reqData.requestID, + Method: method, + Params: params, + } + + rawData, err := json.Marshal(req) + if err != nil { + return nil, err + } + + return rawData, nil +} + +// encode encodes the parameters to a URL encoded string +func encodeParams(p map[string]interface{}) string { + queryValues := url.Values{} + for key, value := range p { + queryValues.Add(key, fmt.Sprintf("%v", value)) + } + return queryValues.Encode() +} + +func timestamp(offsetMilli int64) int64 { + return time.Now().UnixMilli() - offsetMilli +} diff --git a/v2/futures/order_cancel_service_ws.go b/v2/futures/order_cancel_service_ws.go index 9d7d4a39..7ff7eb68 100644 --- a/v2/futures/order_cancel_service_ws.go +++ b/v2/futures/order_cancel_service_ws.go @@ -5,6 +5,7 @@ import ( "time" "github.com/adshao/go-binance/v2/common" + "github.com/adshao/go-binance/v2/common/websocket" ) // NewOrderCancelRequest init OrderCancelRequest @@ -71,12 +72,46 @@ type OrderCancelWsResponse struct { // OrderCancelWsService cancel order type OrderCancelWsService struct { - c wsClient + c websocket.Client + ApiKey string + SecretKey string + KeyType string + TimeOffset int64 +} + +// NewOrderCancelWsService init OrderCancelWsService +func NewOrderCancelWsService(apiKey, secretKey string) (*OrderCancelWsService, error) { + conn, err := websocket.NewConnection(WsApiInitReadWriteConn, WebsocketKeepalive, WebsocketTimeoutReadWriteConnection) + if err != nil { + return nil, err + } + + client, err := websocket.NewClient(conn) + if err != nil { + return nil, err + } + + return &OrderCancelWsService{ + c: client, + ApiKey: apiKey, + SecretKey: secretKey, + KeyType: common.KeyTypeHmac, + }, nil } // Do - sends 'order.cancel' request func (s *OrderCancelWsService) Do(requestID string, request *OrderCancelRequest) error { - rawData, err := createWsRequest(requestID, s.c, CancelWsApiMethod, request.buildParams()) + rawData, err := websocket.CreateRequest( + websocket.NewRequestData( + requestID, + s.ApiKey, + s.SecretKey, + s.TimeOffset, + s.KeyType, + ), + websocket.CancelFuturesWsApiMethod, + request.buildParams(), + ) if err != nil { return err } @@ -90,12 +125,22 @@ func (s *OrderCancelWsService) Do(requestID string, request *OrderCancelRequest) // SyncDo - sends 'order.cancel' request and receives response func (s *OrderCancelWsService) SyncDo(requestID string, request *OrderCancelRequest) (*OrderCancelWsResponse, error) { - rawData, err := createWsRequest(requestID, s.c, CancelWsApiMethod, request.buildParams()) + rawData, err := websocket.CreateRequest( + websocket.NewRequestData( + requestID, + s.ApiKey, + s.SecretKey, + s.TimeOffset, + s.KeyType, + ), + websocket.CancelFuturesWsApiMethod, + request.buildParams(), + ) if err != nil { return nil, err } - response, err := s.c.WriteSync(requestID, rawData, WriteSyncWsTimeout) + response, err := s.c.WriteSync(requestID, rawData, websocket.WriteSyncWsTimeout) if err != nil { return nil, err } diff --git a/v2/futures/order_cancel_service_ws_test.go b/v2/futures/order_cancel_service_ws_test.go index 1019a6ea..c62fd6e9 100644 --- a/v2/futures/order_cancel_service_ws_test.go +++ b/v2/futures/order_cancel_service_ws_test.go @@ -5,7 +5,8 @@ import ( "fmt" "testing" - "github.com/adshao/go-binance/v2/futures/mock" + "github.com/adshao/go-binance/v2/common/websocket" + "github.com/adshao/go-binance/v2/common/websocket/mock" "github.com/golang/mock/gomock" "github.com/stretchr/testify/suite" ) @@ -19,10 +20,13 @@ func (s *orderCancelServiceWsTestSuite) SetupTest() { s.requestID = "e2a85d9f-07a5-4f94-8d5f-789dc3deb098" s.ctrl = gomock.NewController(s.T()) - s.client = mock.NewMockwsClient(s.ctrl) + s.client = mock.NewMockClient(s.ctrl) s.orderCancel = &OrderCancelWsService{ - c: s.client, + c: s.client, + ApiKey: s.apiKey, + SecretKey: s.secretKey, + KeyType: s.signedKey, } s.orderCancelRequest = NewOrderCancelRequest().OrigClientOrderID(s.requestID) @@ -40,7 +44,7 @@ type orderCancelServiceWsTestSuite struct { timeOffset int64 ctrl *gomock.Controller - client *mock.MockwsClient + client *mock.MockClient requestID string @@ -53,7 +57,7 @@ func TestOrderCancelServiceWs(t *testing.T) { } func (s *orderCancelServiceWsTestSuite) TestOrderCancel() { - s.expectCalls(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) + s.reset(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) s.client.EXPECT().Write(s.requestID, gomock.Any()).Return(nil).AnyTimes() @@ -62,34 +66,34 @@ func (s *orderCancelServiceWsTestSuite) TestOrderCancel() { } func (s *orderCancelServiceWsTestSuite) TestOrderCancel_EmptyRequestID() { - s.expectCalls(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) + s.reset(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) s.client.EXPECT().Write(gomock.Any(), gomock.Any()).Return(nil).Times(0) err := s.orderCancel.Do("", s.orderCancelRequest) - s.ErrorIs(err, ErrorRequestIDNotSet) + s.ErrorIs(err, websocket.ErrorRequestIDNotSet) } func (s *orderCancelServiceWsTestSuite) TestOrderCancel_EmptyApiKey() { - s.expectCalls("", s.secretKey, s.signedKey, s.timeOffset) + s.reset("", s.secretKey, s.signedKey, s.timeOffset) s.client.EXPECT().Write(s.requestID, gomock.Any()).Return(nil).Times(0) err := s.orderCancel.Do(s.requestID, s.orderCancelRequest) - s.ErrorIs(err, ErrorApiKeyIsNotSet) + s.ErrorIs(err, websocket.ErrorApiKeyIsNotSet) } func (s *orderCancelServiceWsTestSuite) TestOrderCancel_EmptySecretKey() { - s.expectCalls(s.apiKey, "", s.signedKey, s.timeOffset) + s.reset(s.apiKey, "", s.signedKey, s.timeOffset) s.client.EXPECT().Write(s.requestID, gomock.Any()).Return(nil).Times(0) err := s.orderCancel.Do(s.requestID, s.orderCancelRequest) - s.ErrorIs(err, ErrorSecretKeyIsNotSet) + s.ErrorIs(err, websocket.ErrorSecretKeyIsNotSet) } func (s *orderCancelServiceWsTestSuite) TestOrderCancel_EmptySignKeyType() { - s.expectCalls(s.apiKey, s.secretKey, "", s.timeOffset) + s.reset(s.apiKey, s.secretKey, "", s.timeOffset) s.client.EXPECT().Write(s.requestID, gomock.Any()).Return(nil).Times(0) @@ -98,7 +102,7 @@ func (s *orderCancelServiceWsTestSuite) TestOrderCancel_EmptySignKeyType() { } func (s *orderCancelServiceWsTestSuite) TestOrderCancelSync() { - s.expectCalls(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) + s.reset(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) orderCancelResponse := OrderCancelWsResponse{ Id: s.requestID, @@ -122,7 +126,7 @@ func (s *orderCancelServiceWsTestSuite) TestOrderCancelSync() { } func (s *orderCancelServiceWsTestSuite) TestOrderCancelSync_EmptyRequestID() { - s.expectCalls(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) + s.reset(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) s.client.EXPECT(). WriteSync(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("write sync: error")).Times(0) @@ -130,33 +134,33 @@ func (s *orderCancelServiceWsTestSuite) TestOrderCancelSync_EmptyRequestID() { req := s.orderCancelRequest response, err := s.orderCancel.SyncDo("", req) s.Nil(response) - s.ErrorIs(err, ErrorRequestIDNotSet) + s.ErrorIs(err, websocket.ErrorRequestIDNotSet) } func (s *orderCancelServiceWsTestSuite) TestOrderCancelSync_EmptyApiKey() { - s.expectCalls("", s.secretKey, s.signedKey, s.timeOffset) + s.reset("", s.secretKey, s.signedKey, s.timeOffset) s.client.EXPECT(). WriteSync(s.requestID, gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("write sync: error")).Times(0) response, err := s.orderCancel.SyncDo(s.requestID, s.orderCancelRequest) s.Nil(response) - s.ErrorIs(err, ErrorApiKeyIsNotSet) + s.ErrorIs(err, websocket.ErrorApiKeyIsNotSet) } func (s *orderCancelServiceWsTestSuite) TestOrderCancelSync_EmptySecretKey() { - s.expectCalls(s.apiKey, "", s.signedKey, s.timeOffset) + s.reset(s.apiKey, "", s.signedKey, s.timeOffset) s.client.EXPECT(). WriteSync(s.requestID, gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("write sync: error")).Times(0) response, err := s.orderCancel.SyncDo(s.requestID, s.orderCancelRequest) s.Nil(response) - s.ErrorIs(err, ErrorSecretKeyIsNotSet) + s.ErrorIs(err, websocket.ErrorSecretKeyIsNotSet) } func (s *orderCancelServiceWsTestSuite) TestOrderCancelSync_EmptySignKeyType() { - s.expectCalls(s.apiKey, s.secretKey, "", s.timeOffset) + s.reset(s.apiKey, s.secretKey, "", s.timeOffset) s.client.EXPECT(). WriteSync(s.requestID, gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("write sync: error")).Times(0) @@ -166,9 +170,12 @@ func (s *orderCancelServiceWsTestSuite) TestOrderCancelSync_EmptySignKeyType() { s.Error(err) } -func (s *orderCancelServiceWsTestSuite) expectCalls(apiKey, secretKey, signKeyType string, timeOffset int64) { - s.client.EXPECT().GetApiKey().Return(apiKey).AnyTimes() - s.client.EXPECT().GetSecretKey().Return(secretKey).AnyTimes() - s.client.EXPECT().GetKeyType().Return(signKeyType).AnyTimes() - s.client.EXPECT().GetTimeOffset().Return(timeOffset).AnyTimes() +func (s *orderCancelServiceWsTestSuite) reset(apiKey, secretKey, signKeyType string, timeOffset int64) { + s.orderCancel = &OrderCancelWsService{ + c: s.client, + ApiKey: apiKey, + SecretKey: secretKey, + KeyType: signKeyType, + TimeOffset: timeOffset, + } } diff --git a/v2/futures/order_place_service_ws.go b/v2/futures/order_place_service_ws.go index 759bca51..165ced4d 100644 --- a/v2/futures/order_place_service_ws.go +++ b/v2/futures/order_place_service_ws.go @@ -5,12 +5,36 @@ import ( "time" "github.com/adshao/go-binance/v2/common" + "github.com/adshao/go-binance/v2/common/websocket" ) // OrderPlaceWsService creates order type OrderPlaceWsService struct { - c wsClient - signFn func(string, string) (*string, error) + c websocket.Client + ApiKey string + SecretKey string + KeyType string + TimeOffset int64 +} + +// NewOrderPlaceWsService init OrderPlaceWsService +func NewOrderPlaceWsService(apiKey, secretKey string) (*OrderPlaceWsService, error) { + conn, err := websocket.NewConnection(WsApiInitReadWriteConn, WebsocketKeepalive, WebsocketTimeoutReadWriteConnection) + if err != nil { + return nil, err + } + + client, err := websocket.NewClient(conn) + if err != nil { + return nil, err + } + + return &OrderPlaceWsService{ + c: client, + ApiKey: apiKey, + SecretKey: secretKey, + KeyType: common.KeyTypeHmac, + }, nil } // OrderPlaceWsRequest parameters for 'order.place' websocket API @@ -199,7 +223,17 @@ func (s *OrderPlaceWsRequest) buildParams() params { // Do - sends 'order.place' request func (s *OrderPlaceWsService) Do(requestID string, request *OrderPlaceWsRequest) error { - rawData, err := createWsRequest(requestID, s.c, OrderPlaceWsApiMethod, request.buildParams()) + rawData, err := websocket.CreateRequest( + websocket.NewRequestData( + requestID, + s.ApiKey, + s.SecretKey, + s.TimeOffset, + s.KeyType, + ), + websocket.OrderPlaceFuturesWsApiMethod, + request.buildParams(), + ) if err != nil { return err } @@ -213,12 +247,22 @@ func (s *OrderPlaceWsService) Do(requestID string, request *OrderPlaceWsRequest) // SyncDo - sends 'order.place' request and receives response func (s *OrderPlaceWsService) SyncDo(requestID string, request *OrderPlaceWsRequest) (*CreateOrderWsResponse, error) { - rawData, err := createWsRequest(requestID, s.c, OrderPlaceWsApiMethod, request.buildParams()) + rawData, err := websocket.CreateRequest( + websocket.NewRequestData( + requestID, + s.ApiKey, + s.SecretKey, + s.TimeOffset, + s.KeyType, + ), + websocket.OrderPlaceFuturesWsApiMethod, + request.buildParams(), + ) if err != nil { return nil, err } - response, err := s.c.WriteSync(requestID, rawData, WriteSyncWsTimeout) + response, err := s.c.WriteSync(requestID, rawData, websocket.WriteSyncWsTimeout) if err != nil { return nil, err } diff --git a/v2/futures/order_place_service_ws_test.go b/v2/futures/order_place_service_ws_test.go index 0e425568..3a2bd2d3 100644 --- a/v2/futures/order_place_service_ws_test.go +++ b/v2/futures/order_place_service_ws_test.go @@ -5,7 +5,8 @@ import ( "fmt" "testing" - "github.com/adshao/go-binance/v2/futures/mock" + "github.com/adshao/go-binance/v2/common/websocket" + "github.com/adshao/go-binance/v2/common/websocket/mock" "github.com/golang/mock/gomock" "github.com/stretchr/testify/suite" ) @@ -27,10 +28,13 @@ func (s *orderPlaceServiceWsTestSuite) SetupTest() { s.newClientOrderID = "testOrder" s.ctrl = gomock.NewController(s.T()) - s.client = mock.NewMockwsClient(s.ctrl) + s.client = mock.NewMockClient(s.ctrl) s.orderPlace = &OrderPlaceWsService{ - c: s.client, + c: s.client, + ApiKey: s.apiKey, + SecretKey: s.secretKey, + KeyType: s.signedKey, } s.orderPlaceRequest = NewOrderPlaceWsRequest(). @@ -55,7 +59,7 @@ type orderPlaceServiceWsTestSuite struct { timeOffset int64 ctrl *gomock.Controller - client *mock.MockwsClient + client *mock.MockClient requestID string symbol string @@ -75,7 +79,7 @@ func TestOrderPlaceServiceWsPlace(t *testing.T) { } func (s *orderPlaceServiceWsTestSuite) TestOrderPlace() { - s.expectCalls(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) + s.reset(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) s.client.EXPECT().Write(s.requestID, gomock.Any()).Return(nil).AnyTimes() @@ -84,34 +88,34 @@ func (s *orderPlaceServiceWsTestSuite) TestOrderPlace() { } func (s *orderPlaceServiceWsTestSuite) TestOrderPlace_EmptyRequestID() { - s.expectCalls(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) + s.reset(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) s.client.EXPECT().Write(gomock.Any(), gomock.Any()).Return(nil).Times(0) err := s.orderPlace.Do("", s.orderPlaceRequest) - s.ErrorIs(err, ErrorRequestIDNotSet) + s.ErrorIs(err, websocket.ErrorRequestIDNotSet) } func (s *orderPlaceServiceWsTestSuite) TestOrderPlace_EmptyApiKey() { - s.expectCalls("", s.secretKey, s.signedKey, s.timeOffset) + s.reset("", s.secretKey, s.signedKey, s.timeOffset) s.client.EXPECT().Write(s.requestID, gomock.Any()).Return(nil).Times(0) err := s.orderPlace.Do(s.requestID, s.orderPlaceRequest) - s.ErrorIs(err, ErrorApiKeyIsNotSet) + s.ErrorIs(err, websocket.ErrorApiKeyIsNotSet) } func (s *orderPlaceServiceWsTestSuite) TestOrderPlace_EmptySecretKey() { - s.expectCalls(s.apiKey, "", s.signedKey, s.timeOffset) + s.reset(s.apiKey, "", s.signedKey, s.timeOffset) s.client.EXPECT().Write(s.requestID, gomock.Any()).Return(nil).Times(0) err := s.orderPlace.Do(s.requestID, s.orderPlaceRequest) - s.ErrorIs(err, ErrorSecretKeyIsNotSet) + s.ErrorIs(err, websocket.ErrorSecretKeyIsNotSet) } func (s *orderPlaceServiceWsTestSuite) TestOrderPlace_EmptySignKeyType() { - s.expectCalls(s.apiKey, s.secretKey, "", s.timeOffset) + s.reset(s.apiKey, s.secretKey, "", s.timeOffset) s.client.EXPECT().Write(s.requestID, gomock.Any()).Return(nil).Times(0) @@ -120,7 +124,7 @@ func (s *orderPlaceServiceWsTestSuite) TestOrderPlace_EmptySignKeyType() { } func (s *orderPlaceServiceWsTestSuite) TestOrderPlaceSync() { - s.expectCalls(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) + s.reset(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) orderPlaceResponse := CreateOrderWsResponse{ Id: s.requestID, @@ -153,7 +157,7 @@ func (s *orderPlaceServiceWsTestSuite) TestOrderPlaceSync() { } func (s *orderPlaceServiceWsTestSuite) TestOrderPlaceSync_EmptyRequestID() { - s.expectCalls(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) + s.reset(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) s.client.EXPECT(). WriteSync(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("write sync: error")).Times(0) @@ -161,33 +165,33 @@ func (s *orderPlaceServiceWsTestSuite) TestOrderPlaceSync_EmptyRequestID() { req := s.orderPlaceRequest response, err := s.orderPlace.SyncDo("", req) s.Nil(response) - s.ErrorIs(err, ErrorRequestIDNotSet) + s.ErrorIs(err, websocket.ErrorRequestIDNotSet) } func (s *orderPlaceServiceWsTestSuite) TestOrderPlaceSync_EmptyApiKey() { - s.expectCalls("", s.secretKey, s.signedKey, s.timeOffset) + s.reset("", s.secretKey, s.signedKey, s.timeOffset) s.client.EXPECT(). WriteSync(s.requestID, gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("write sync: error")).Times(0) response, err := s.orderPlace.SyncDo(s.requestID, s.orderPlaceRequest) s.Nil(response) - s.ErrorIs(err, ErrorApiKeyIsNotSet) + s.ErrorIs(err, websocket.ErrorApiKeyIsNotSet) } func (s *orderPlaceServiceWsTestSuite) TestOrderPlaceSync_EmptySecretKey() { - s.expectCalls(s.apiKey, "", s.signedKey, s.timeOffset) + s.reset(s.apiKey, "", s.signedKey, s.timeOffset) s.client.EXPECT(). WriteSync(s.requestID, gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("write sync: error")).Times(0) response, err := s.orderPlace.SyncDo(s.requestID, s.orderPlaceRequest) s.Nil(response) - s.ErrorIs(err, ErrorSecretKeyIsNotSet) + s.ErrorIs(err, websocket.ErrorSecretKeyIsNotSet) } func (s *orderPlaceServiceWsTestSuite) TestOrderPlaceSync_EmptySignKeyType() { - s.expectCalls(s.apiKey, s.secretKey, "", s.timeOffset) + s.reset(s.apiKey, s.secretKey, "", s.timeOffset) s.client.EXPECT(). WriteSync(s.requestID, gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("write sync: error")).Times(0) @@ -197,9 +201,12 @@ func (s *orderPlaceServiceWsTestSuite) TestOrderPlaceSync_EmptySignKeyType() { s.Error(err) } -func (s *orderPlaceServiceWsTestSuite) expectCalls(apiKey, secretKey, signKeyType string, timeOffset int64) { - s.client.EXPECT().GetApiKey().Return(apiKey).AnyTimes() - s.client.EXPECT().GetSecretKey().Return(secretKey).AnyTimes() - s.client.EXPECT().GetKeyType().Return(signKeyType).AnyTimes() - s.client.EXPECT().GetTimeOffset().Return(timeOffset).AnyTimes() +func (s *orderPlaceServiceWsTestSuite) reset(apiKey, secretKey, signKeyType string, timeOffset int64) { + s.orderPlace = &OrderPlaceWsService{ + c: s.client, + ApiKey: apiKey, + SecretKey: secretKey, + KeyType: signKeyType, + TimeOffset: timeOffset, + } } diff --git a/v2/futures/request.go b/v2/futures/request.go index 0d218c21..396dee6a 100644 --- a/v2/futures/request.go +++ b/v2/futures/request.go @@ -17,15 +17,6 @@ const ( type params map[string]interface{} -// encode encodes the parameters to a URL encoded string -func (p *params) encode() string { - queryValues := url.Values{} - for key, value := range *p { - queryValues.Add(key, fmt.Sprintf("%v", value)) - } - return queryValues.Encode() -} - // request define an API request type request struct { method string diff --git a/v2/futures/websocket_api_types.go b/v2/futures/websocket_api_types.go deleted file mode 100644 index 64432405..00000000 --- a/v2/futures/websocket_api_types.go +++ /dev/null @@ -1,85 +0,0 @@ -package futures - -import ( - "encoding/json" - "errors" - "time" - - "github.com/adshao/go-binance/v2/common" -) - -// WsApiMethodType define method name for websocket API -type WsApiMethodType string - -// WsApiRequest define common websocket API request -type WsApiRequest struct { - Id string `json:"id"` - Method WsApiMethodType `json:"method"` - Params params `json:"params"` -} - -const ( - // apiKey define key for websocket API parameters - apiKey = "apiKey" - - // OrderPlaceWsApiMethod define method for creation order via websocket API - OrderPlaceWsApiMethod WsApiMethodType = "order.place" - - // CancelWsApiMethod define method for cancel order via websocket API - CancelWsApiMethod WsApiMethodType = "order.cancel" - - // WriteSyncWsTimeout defines timeout for WriteSync method of client_ws - WriteSyncWsTimeout = 5 * time.Second -) - -var ( - // ErrorRequestIDNotSet defines that request ID is not set - ErrorRequestIDNotSet = errors.New("ws service: request id is not set") - - // ErrorApiKeyIsNotSet defines that ApiKey is not set - ErrorApiKeyIsNotSet = errors.New("ws service: api key is not set") - - // ErrorSecretKeyIsNotSet defines that SecretKey is not set - ErrorSecretKeyIsNotSet = errors.New("ws service: secret key is not set") -) - -// createWsRequest creates signed ws request -func createWsRequest(requestID string, client wsClient, method WsApiMethodType, params params) ([]byte, error) { - if requestID == "" { - return nil, ErrorRequestIDNotSet - } - - if client.GetApiKey() == "" { - return nil, ErrorApiKeyIsNotSet - } - - if client.GetSecretKey() == "" { - return nil, ErrorSecretKeyIsNotSet - } - - params[apiKey] = client.GetApiKey() - params[timestampKey] = currentTimestamp() - client.GetTimeOffset() - - sf, err := common.SignFunc(client.GetKeyType()) - if err != nil { - return nil, err - } - signature, err := sf(client.GetSecretKey(), params.encode()) - if err != nil { - return nil, err - } - params[signatureKey] = signature - - req := WsApiRequest{ - Id: requestID, - Method: method, - Params: params, - } - - rawData, err := json.Marshal(req) - if err != nil { - return nil, err - } - - return rawData, nil -} diff --git a/v2/futures/websocket_service.go b/v2/futures/websocket_service.go index a8704742..b208060e 100644 --- a/v2/futures/websocket_service.go +++ b/v2/futures/websocket_service.go @@ -4,21 +4,21 @@ import ( "encoding/json" "errors" "fmt" - "github.com/bitly/go-simplejson" - "github.com/gorilla/websocket" "strings" "time" + + "github.com/bitly/go-simplejson" + "github.com/gorilla/websocket" ) // Endpoints -const ( - baseWsMainUrl = "wss://fstream.binance.com/ws" - baseWsTestnetUrl = "wss://stream.binancefuture.com/ws" - baseCombinedMainURL = "wss://fstream.binance.com/stream?streams=" - baseCombinedTestnetURL = "wss://stream.binancefuture.com/stream?streams=" +var ( + BaseWsMainUrl = "wss://fstream.binance.com/ws" + BaseWsTestnetUrl = "wss://stream.binancefuture.com/ws" + BaseCombinedMainURL = "wss://fstream.binance.com/stream?streams=" + BaseCombinedTestnetURL = "wss://stream.binancefuture.com/stream?streams=" BaseWsApiMainURL = "wss://ws-fapi.binance.com/ws-fapi/v1" BaseWsApiTestnetURL = "wss://testnet.binancefuture.com/ws-fapi/v1" - localhostWsApiURL = "ws://localhost:8080/ws" ) var ( @@ -28,12 +28,10 @@ var ( WebsocketKeepalive = false // UseTestnet switch all the WS streams from production to the testnet UseTestnet = false - ProxyUrl = "" // WebsocketTimeoutReadWriteConnection is an interval for sending ping/pong messages if WebsocketKeepalive is enabled // using for websocket API (read/write) WebsocketTimeoutReadWriteConnection = time.Second * 10 - // useLocalhost switch all the WS streams from production to localhost testing - useLocalhost = false + ProxyUrl = "" ) func getWsProxyUrl() *string { @@ -50,17 +48,17 @@ func SetWsProxyUrl(url string) { // getWsEndpoint return the base endpoint of the WS according the UseTestnet flag func getWsEndpoint() string { if UseTestnet { - return baseWsTestnetUrl + return BaseWsTestnetUrl } - return baseWsMainUrl + return BaseWsMainUrl } // getCombinedEndpoint return the base endpoint of the combined stream according the UseTestnet flag func getCombinedEndpoint() string { if UseTestnet { - return baseCombinedTestnetURL + return BaseCombinedTestnetURL } - return baseCombinedMainURL + return BaseCombinedMainURL } // WsAggTradeEvent define websocket aggTrde event. @@ -1215,9 +1213,5 @@ func getWsApiEndpoint() string { return BaseWsApiTestnetURL } - if useLocalhost { - return localhostWsApiURL - } - return BaseWsApiMainURL } diff --git a/v2/order_service_ws_create.go b/v2/order_service_ws_create.go new file mode 100644 index 00000000..9940b035 --- /dev/null +++ b/v2/order_service_ws_create.go @@ -0,0 +1,284 @@ +package binance + +import ( + "time" + + "github.com/adshao/go-binance/v2/common" + "github.com/adshao/go-binance/v2/common/websocket" +) + +// OrderCreateWsService creates order +type OrderCreateWsService struct { + c websocket.Client + ApiKey string + SecretKey string + KeyType string + TimeOffset int64 +} + +// NewOrderCreateWsService init OrderCreateWsService +func NewOrderCreateWsService(apiKey, secretKey string) (*OrderCreateWsService, error) { + conn, err := websocket.NewConnection(WsApiInitReadWriteConn, WebsocketKeepalive, WebsocketTimeoutReadWriteConnection) + if err != nil { + return nil, err + } + + client, err := websocket.NewClient(conn) + if err != nil { + return nil, err + } + + return &OrderCreateWsService{ + c: client, + ApiKey: apiKey, + SecretKey: secretKey, + KeyType: common.KeyTypeHmac, + }, nil +} + +// OrderCreateWsRequest parameters for 'order.place' websocket API +type OrderCreateWsRequest struct { + symbol string + side SideType + orderType OrderType + timeInForce *TimeInForceType + quantity string + price *string + newClientOrderID *string + stopPrice *string + newOrderRespType NewOrderRespType + quoteOrderQty *string + trailingDelta *int64 + icebergQty *string + strategyId *uint64 + strategyType *uint32 + recvWindow *uint16 +} + +// NewOrderCreateWsRequest init OrderCreateWsRequest +func NewOrderCreateWsRequest() *OrderCreateWsRequest { + return &OrderCreateWsRequest{} +} + +// buildParams builds params +func (s *OrderCreateWsRequest) buildParams() params { + m := params{ + "symbol": s.symbol, + "side": s.side, + "type": s.orderType, + "newOrderRespType": s.newOrderRespType, + } + if s.quantity != "" { + m["quantity"] = s.quantity + } + if s.timeInForce != nil { + m["timeInForce"] = *s.timeInForce + } + if s.price != nil { + m["price"] = *s.price + } + if s.newClientOrderID != nil { + m["newClientOrderId"] = *s.newClientOrderID + } + if s.stopPrice != nil { + m["stopPrice"] = *s.stopPrice + } + if s.quoteOrderQty != nil { + m["quoteOrderQty"] = *s.quoteOrderQty + } + if s.trailingDelta != nil { + m["trailingDelta"] = *s.trailingDelta + } + if s.icebergQty != nil { + m["icebergQty"] = *s.icebergQty + } + if s.strategyId != nil { + m["strategyId"] = *s.strategyId + } + if s.strategyType != nil { + m["strategyType"] = *s.strategyType + } + if s.recvWindow != nil { + m["recvWindow"] = *s.recvWindow + } + return m +} + +// Do - sends 'order.place' request +func (s *OrderCreateWsService) Do(requestID string, request *OrderCreateWsRequest) error { + rawData, err := websocket.CreateRequest( + websocket.NewRequestData( + requestID, + s.ApiKey, + s.SecretKey, + s.TimeOffset, + s.KeyType, + ), + websocket.OrderPlaceSpotWsApiMethod, + request.buildParams(), + ) + if err != nil { + return err + } + + if err := s.c.Write(requestID, rawData); err != nil { + return err + } + + return nil +} + +// SyncDo - sends 'order.place' request and receives response +func (s *OrderCreateWsService) SyncDo(requestID string, request *OrderCreateWsRequest) (*CreateOrderWsResponse, error) { + rawData, err := websocket.CreateRequest( + websocket.NewRequestData( + requestID, + s.ApiKey, + s.SecretKey, + s.TimeOffset, + s.KeyType, + ), + websocket.OrderPlaceSpotWsApiMethod, + request.buildParams(), + ) + if err != nil { + return nil, err + } + + response, err := s.c.WriteSync(requestID, rawData, websocket.WriteSyncWsTimeout) + if err != nil { + return nil, err + } + + createOrderWsResponse := &CreateOrderWsResponse{} + if err := json.Unmarshal(response, createOrderWsResponse); err != nil { + return nil, err + } + + return createOrderWsResponse, nil +} + +// ReceiveAllDataBeforeStop waits until all responses will be received from websocket until timeout expired +func (s *OrderCreateWsService) ReceiveAllDataBeforeStop(timeout time.Duration) { + s.c.Wait(timeout) +} + +// GetReadChannel returns channel with API response data (including API errors) +func (s *OrderCreateWsService) GetReadChannel() <-chan []byte { + return s.c.GetReadChannel() +} + +// GetReadErrorChannel returns channel with errors which are occurred while reading websocket connection +func (s *OrderCreateWsService) GetReadErrorChannel() <-chan error { + return s.c.GetReadErrorChannel() +} + +// GetReconnectCount returns count of reconnect attempts by client +func (s *OrderCreateWsService) GetReconnectCount() int64 { + return s.c.GetReconnectCount() +} + +// Symbol set symbol +func (s *OrderCreateWsRequest) Symbol(symbol string) *OrderCreateWsRequest { + s.symbol = symbol + return s +} + +// Side set side +func (s *OrderCreateWsRequest) Side(side SideType) *OrderCreateWsRequest { + s.side = side + return s +} + +// Type set type +func (s *OrderCreateWsRequest) Type(orderType OrderType) *OrderCreateWsRequest { + s.orderType = orderType + return s +} + +// TimeInForce set timeInForce +func (s *OrderCreateWsRequest) TimeInForce(timeInForce TimeInForceType) *OrderCreateWsRequest { + s.timeInForce = &timeInForce + return s +} + +// Quantity set quantity +func (s *OrderCreateWsRequest) Quantity(quantity string) *OrderCreateWsRequest { + s.quantity = quantity + return s +} + +// Price set price +func (s *OrderCreateWsRequest) Price(price string) *OrderCreateWsRequest { + s.price = &price + return s +} + +// NewClientOrderID set newClientOrderID +func (s *OrderCreateWsRequest) NewClientOrderID(newClientOrderID string) *OrderCreateWsRequest { + s.newClientOrderID = &newClientOrderID + return s +} + +// StopPrice set stopPrice +func (s *OrderCreateWsRequest) StopPrice(stopPrice string) *OrderCreateWsRequest { + s.stopPrice = &stopPrice + return s +} + +// RecvWindow set recvWindow +func (s *OrderCreateWsRequest) RecvWindow(recvWindow uint16) *OrderCreateWsRequest { + s.recvWindow = &recvWindow + return s +} + +// StrategyType set strategyType +func (s *OrderCreateWsRequest) StrategyType(strategyType uint32) *OrderCreateWsRequest { + s.strategyType = &strategyType + return s +} + +// StrategyId set strategyId +func (s *OrderCreateWsRequest) StrategyId(strategyId uint64) *OrderCreateWsRequest { + s.strategyId = &strategyId + return s +} + +// IcebergQty set icebergQty +func (s *OrderCreateWsRequest) IcebergQty(icebergQty string) *OrderCreateWsRequest { + s.icebergQty = &icebergQty + return s +} + +// TrailingDelta set trailingDelta +func (s *OrderCreateWsRequest) TrailingDelta(trailingDelta int64) *OrderCreateWsRequest { + s.trailingDelta = &trailingDelta + return s +} + +// QuoteOrderQty set quoteOrderQty +func (s *OrderCreateWsRequest) QuoteOrderQty(quoteOrderQty string) *OrderCreateWsRequest { + s.quoteOrderQty = "eOrderQty + return s +} + +// NewOrderRespType set newOrderRespType +func (s *OrderCreateWsRequest) NewOrderRespType(newOrderRespType NewOrderRespType) *OrderCreateWsRequest { + s.newOrderRespType = newOrderRespType + return s +} + +// CreateOrderResult define order creation result +type CreateOrderResult struct { + CreateOrderResponse +} + +// CreateOrderWsResponse define 'order.place' websocket API response +type CreateOrderWsResponse struct { + Id string `json:"id"` + Status int `json:"status"` + Result CreateOrderResult `json:"result"` + + // error response + Error *common.APIError `json:"error,omitempty"` +} diff --git a/v2/order_service_ws_create_test.go b/v2/order_service_ws_create_test.go new file mode 100644 index 00000000..9a37fbef --- /dev/null +++ b/v2/order_service_ws_create_test.go @@ -0,0 +1,211 @@ +package binance + +import ( + "fmt" + "testing" + + "github.com/adshao/go-binance/v2/common/websocket" + "github.com/adshao/go-binance/v2/common/websocket/mock" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/suite" +) + +func (s *orderPlaceServiceWsTestSuite) SetupTest() { + s.apiKey = "dummyApiKey" + s.secretKey = "dummySecretKey" + s.signedKey = "HMAC" + s.timeOffset = 0 + + s.requestID = "e2a85d9f-07a5-4f94-8d5f-789dc3deb098" + + s.symbol = "BTCUSDT" + s.side = SideTypeSell + s.orderType = OrderTypeLimit + s.timeInForce = TimeInForceTypeGTC + s.quantity = "0.1001" + s.price = "50000" + s.newClientOrderID = "testOrder" + + s.ctrl = gomock.NewController(s.T()) + s.client = mock.NewMockClient(s.ctrl) + + s.orderPlace = &OrderCreateWsService{ + c: s.client, + ApiKey: s.apiKey, + SecretKey: s.secretKey, + KeyType: s.signedKey, + } + + s.orderPlaceRequest = NewOrderCreateWsRequest(). + Symbol(s.symbol). + Side(s.side). + Type(s.orderType). + TimeInForce(s.timeInForce). + Quantity(s.quantity). + Price(s.price). + NewClientOrderID(s.newClientOrderID) +} + +func (s *orderPlaceServiceWsTestSuite) TearDownTest() { + s.ctrl.Finish() +} + +type orderPlaceServiceWsTestSuite struct { + suite.Suite + apiKey string + secretKey string + signedKey string + timeOffset int64 + + ctrl *gomock.Controller + client *mock.MockClient + + requestID string + symbol string + side SideType + orderType OrderType + timeInForce TimeInForceType + quantity string + price string + newClientOrderID string + + orderPlace *OrderCreateWsService + orderPlaceRequest *OrderCreateWsRequest +} + +func TestOrderPlaceServiceWsPlace(t *testing.T) { + suite.Run(t, new(orderPlaceServiceWsTestSuite)) +} + +func (s *orderPlaceServiceWsTestSuite) TestOrderPlace() { + s.reset(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) + + s.client.EXPECT().Write(s.requestID, gomock.Any()).Return(nil).AnyTimes() + + err := s.orderPlace.Do(s.requestID, s.orderPlaceRequest) + s.NoError(err) +} + +func (s *orderPlaceServiceWsTestSuite) TestOrderPlace_EmptyRequestID() { + s.reset(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) + + s.client.EXPECT().Write(gomock.Any(), gomock.Any()).Return(nil).Times(0) + + err := s.orderPlace.Do("", s.orderPlaceRequest) + s.ErrorIs(err, websocket.ErrorRequestIDNotSet) +} + +func (s *orderPlaceServiceWsTestSuite) TestOrderPlace_EmptyApiKey() { + s.reset("", s.secretKey, s.signedKey, s.timeOffset) + + s.client.EXPECT().Write(s.requestID, gomock.Any()).Return(nil).Times(0) + + err := s.orderPlace.Do(s.requestID, s.orderPlaceRequest) + s.ErrorIs(err, websocket.ErrorApiKeyIsNotSet) +} + +func (s *orderPlaceServiceWsTestSuite) TestOrderPlace_EmptySecretKey() { + s.reset(s.apiKey, "", s.signedKey, s.timeOffset) + + s.client.EXPECT().Write(s.requestID, gomock.Any()).Return(nil).Times(0) + + err := s.orderPlace.Do(s.requestID, s.orderPlaceRequest) + s.ErrorIs(err, websocket.ErrorSecretKeyIsNotSet) +} + +func (s *orderPlaceServiceWsTestSuite) TestOrderPlace_EmptySignKeyType() { + s.reset(s.apiKey, s.secretKey, "", s.timeOffset) + + s.client.EXPECT().Write(s.requestID, gomock.Any()).Return(nil).Times(0) + + err := s.orderPlace.Do(s.requestID, s.orderPlaceRequest) + s.Error(err) +} + +func (s *orderPlaceServiceWsTestSuite) TestOrderPlaceSync() { + s.reset(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) + + orderPlaceResponse := CreateOrderWsResponse{ + Id: s.requestID, + Status: 200, + Result: CreateOrderResult{ + CreateOrderResponse{ + Symbol: s.symbol, + OrderID: 0, + ClientOrderID: s.newClientOrderID, + Price: s.price, + TimeInForce: s.timeInForce, + Type: s.orderType, + Side: s.side, + }, + }, + } + + rawResponseData, err := json.Marshal(orderPlaceResponse) + s.NoError(err) + + s.client.EXPECT().WriteSync(s.requestID, gomock.Any(), gomock.Any()).Return(rawResponseData, nil).Times(1) + + req := s.orderPlaceRequest + response, err := s.orderPlace.SyncDo(s.requestID, req) + s.Require().NoError(err) + s.Equal(*req.newClientOrderID, response.Result.ClientOrderID) + s.Equal(req.symbol, response.Result.Symbol) + s.Equal(req.orderType, response.Result.Type) + s.Equal(*req.price, response.Result.Price) +} + +func (s *orderPlaceServiceWsTestSuite) TestOrderPlaceSync_EmptyRequestID() { + s.reset(s.apiKey, s.secretKey, s.signedKey, s.timeOffset) + + s.client.EXPECT(). + WriteSync(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("write sync: error")).Times(0) + + req := s.orderPlaceRequest + response, err := s.orderPlace.SyncDo("", req) + s.Nil(response) + s.ErrorIs(err, websocket.ErrorRequestIDNotSet) +} + +func (s *orderPlaceServiceWsTestSuite) TestOrderPlaceSync_EmptyApiKey() { + s.reset("", s.secretKey, s.signedKey, s.timeOffset) + + s.client.EXPECT(). + WriteSync(s.requestID, gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("write sync: error")).Times(0) + + response, err := s.orderPlace.SyncDo(s.requestID, s.orderPlaceRequest) + s.Nil(response) + s.ErrorIs(err, websocket.ErrorApiKeyIsNotSet) +} + +func (s *orderPlaceServiceWsTestSuite) TestOrderPlaceSync_EmptySecretKey() { + s.reset(s.apiKey, "", s.signedKey, s.timeOffset) + + s.client.EXPECT(). + WriteSync(s.requestID, gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("write sync: error")).Times(0) + + response, err := s.orderPlace.SyncDo(s.requestID, s.orderPlaceRequest) + s.Nil(response) + s.ErrorIs(err, websocket.ErrorSecretKeyIsNotSet) +} + +func (s *orderPlaceServiceWsTestSuite) TestOrderPlaceSync_EmptySignKeyType() { + s.reset(s.apiKey, s.secretKey, "", s.timeOffset) + + s.client.EXPECT(). + WriteSync(s.requestID, gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("write sync: error")).Times(0) + + response, err := s.orderPlace.SyncDo(s.requestID, s.orderPlaceRequest) + s.Nil(response) + s.Error(err) +} + +func (s *orderPlaceServiceWsTestSuite) reset(apiKey, secretKey, signKeyType string, timeOffset int64) { + s.orderPlace = &OrderCreateWsService{ + c: s.client, + ApiKey: apiKey, + SecretKey: secretKey, + KeyType: signKeyType, + TimeOffset: timeOffset, + } +} diff --git a/v2/websocket.go b/v2/websocket.go index dbfaab72..26b68d40 100644 --- a/v2/websocket.go +++ b/v2/websocket.go @@ -108,3 +108,27 @@ func keepAlive(c *websocket.Conn, timeout time.Duration) { } }() } + +var WsGetReadWriteConnection = func(cfg *WsConfig) (*websocket.Conn, error) { + proxy := http.ProxyFromEnvironment + if cfg.Proxy != nil { + u, err := url.Parse(*cfg.Proxy) + if err != nil { + return nil, err + } + proxy = http.ProxyURL(u) + } + + Dialer := websocket.Dialer{ + Proxy: proxy, + HandshakeTimeout: 45 * time.Second, + EnableCompression: false, + } + + c, _, err := Dialer.Dial(cfg.Endpoint, nil) + if err != nil { + return nil, err + } + + return c, nil +} diff --git a/v2/websocket_service.go b/v2/websocket_service.go index ed31481e..3b215a2e 100644 --- a/v2/websocket_service.go +++ b/v2/websocket_service.go @@ -5,6 +5,8 @@ import ( "fmt" "strings" "time" + + "github.com/gorilla/websocket" ) var ( @@ -13,12 +15,18 @@ var ( BaseWsTestnetURL = "wss://testnet.binance.vision/ws" BaseCombinedMainURL = "wss://stream.binance.com:9443/stream?streams=" BaseCombinedTestnetURL = "wss://testnet.binance.vision/stream?streams=" + BaseWsApiMainURL = "wss://ws-api.binance.com:443/ws-api/v3" + BaseWsApiTestnetURL = "wss://testnet.binance.vision/ws-api/v3" // WebsocketTimeout is an interval for sending ping/pong messages if WebsocketKeepalive is enabled WebsocketTimeout = time.Second * 60 + // WebsocketKeepalive enables sending ping/pong messages to check the connection stability WebsocketKeepalive = false - ProxyUrl = "" + // WebsocketTimeoutReadWriteConnection is an interval for sending ping/pong messages if WebsocketKeepalive is enabled + // using for websocket API (read/write) + WebsocketTimeoutReadWriteConnection = time.Second * 10 + ProxyUrl = "" ) func getWsProxyUrl() *string { @@ -851,3 +859,22 @@ func WsAllBookTickerServe(handler WsBookTickerHandler, errHandler ErrHandler) (d } return wsServe(cfg, wsHandler, errHandler) } + +// WsApiInitReadWriteConn create and serve connection +func WsApiInitReadWriteConn() (*websocket.Conn, error) { + cfg := newWsConfig(getWsApiEndpoint()) + conn, err := WsGetReadWriteConnection(cfg) + if err != nil { + return nil, err + } + + return conn, err +} + +// getWsApiEndpoint return the base endpoint of the API WS according the UseTestnet flag +func getWsApiEndpoint() string { + if UseTestnet { + return BaseWsApiTestnetURL + } + return BaseWsApiMainURL +}