Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ jobs:

if [ -z "$LATEST_TAG" ]; then
# No tags exist yet, start with v1.0.0
NEW_VERSION="v1.1.0"
NEW_VERSION="v1.0.0"
else
if [ "${{ matrix.client }}" == "common" ]; then
CURRENT_VERSION=${LATEST_TAG#${{ matrix.client }}/v}
Expand Down
11 changes: 11 additions & 0 deletions common/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
### Changelog

## 1.2.0 - 2026-01-23

### Added (1)

- Added `Alpha` base url

### Changed (2)

- Updated `PrepareRequest` and `SendRequest` methods to include `signed` boolean parameter to indicate if the request requires signing.
- Fixed Lock issue in `WebsocketStreams` `Unsubscribe` method by ensuring proper unlocking of mutex after stream removal.

## 1.1.0 - 2026-01-13

- Updated `WebsocketStreams` `Subscribe` method to support `int32` format for `id` parameter
Expand Down
3 changes: 3 additions & 0 deletions common/common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ const (
// Algo API URLs
const AlgoRestApiProdUrl = "https://api.binance.com"

// Alpha API URLs
const AlphaRestApiProdUrl = "https://www.binance.com"

// C2C API URLs
const C2CRestApiProdUrl = "https://api.binance.com"

Expand Down
52 changes: 28 additions & 24 deletions common/common/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,8 +415,9 @@ func ParseRateLimitHeaders(header http.Header) ([]RateLimit, error) {
// @param method The HTTP method (GET, POST, etc.).
// @param queryParams The query parameters for the request.
// @param cfg The configuration containing API keys, secrets, and other settings.
// @param signed A boolean indicating whether the request requires signing.
// @return The response from the REST API or an error if the request fails.
func SendRequest[T any](ctx context.Context, path string, method string, queryParams url.Values, bodyParams interface{}, cfg *ConfigurationRestAPI) (*RestApiResponse[T], error) {
func SendRequest[T any](ctx context.Context, path string, method string, queryParams url.Values, bodyParams interface{}, cfg *ConfigurationRestAPI, signed bool) (*RestApiResponse[T], error) {
var (
localVarHeaderParams = make(map[string]string)
localVarHTTPContentTypes = []string{}
Expand All @@ -427,7 +428,7 @@ func SendRequest[T any](ctx context.Context, path string, method string, queryPa
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}

req, err := PrepareRequest(ctx, path, method, localVarHeaderParams, queryParams, bodyParams, cfg)
req, err := PrepareRequest(ctx, path, method, localVarHeaderParams, queryParams, bodyParams, cfg, signed)
if err != nil {
return &RestApiResponse[T]{}, err
}
Expand All @@ -443,7 +444,6 @@ func SendRequest[T any](ctx context.Context, path string, method string, queryPa
var lastErr error

httpClient := SetupProxy(cfg)

for attempt := 0; attempt <= retries; attempt++ {
resp, err := httpClient.Do(req)
if err != nil {
Expand Down Expand Up @@ -543,14 +543,16 @@ func SendRequest[T any](ctx context.Context, path string, method string, queryPa
// @param headerParams A map of header parameters to include in the request.
// @param queryParams The query parameters for the request.
// @param c The configuration containing API keys, secrets, and other settings.
// @param signed A boolean indicating whether the request requires signing.
// @return The prepared HTTP request or an error if preparation fails.
func PrepareRequest(
ctx context.Context,
path string, method string,
headerParams map[string]string,
queryParams url.Values,
bodyParams interface{},
c *ConfigurationRestAPI) (localVarRequest *http.Request, err error) {
c *ConfigurationRestAPI,
signed bool) (localVarRequest *http.Request, err error) {

reqURL, err := url.Parse(path)
if err != nil {
Expand Down Expand Up @@ -608,34 +610,36 @@ func PrepareRequest(
}
}

if c.ApiSecret != "" {
paramsToSign += "&timestamp=" + strconv.FormatInt(time.Now().UnixMilli(), 10)
if signed && c != nil {
if c.ApiSecret != "" {
paramsToSign += "&timestamp=" + strconv.FormatInt(time.Now().UnixMilli(), 10)

signer := hmac.New(sha256.New, []byte(c.ApiSecret))
signer.Write([]byte(paramsToSign))
signature := signer.Sum(nil)
if err != nil {
return nil, err
signer := hmac.New(sha256.New, []byte(c.ApiSecret))
signer.Write([]byte(paramsToSign))
signature := signer.Sum(nil)
if err != nil {
return nil, err
}
paramsToSign += "&signature=" + fmt.Sprintf("%x", signature)
}
paramsToSign += "&signature=" + fmt.Sprintf("%x", signature)
}

if c.PrivateKey != "" {
paramsToSign += "&timestamp=" + strconv.FormatInt(time.Now().UnixMilli(), 10)
if c.PrivateKey != "" {
paramsToSign += "&timestamp=" + strconv.FormatInt(time.Now().UnixMilli(), 10)

if c.Signer == nil {
key, err := LoadPrivateKey(c.PrivateKey, c.PrivateKeyPassphrase)
if c.Signer == nil {
key, err := LoadPrivateKey(c.PrivateKey, c.PrivateKeyPassphrase)
if err != nil {
panic(err)
}
c.Signer = &cryptoSigner{s: key}
}

signature, _ := c.Signer.Sign([]byte(paramsToSign))
if err != nil {
panic(err)
}
c.Signer = &cryptoSigner{s: key}
}

signature, _ := c.Signer.Sign([]byte(paramsToSign))
if err != nil {
panic(err)
paramsToSign += "&signature=" + base64.StdEncoding.EncodeToString(signature)
}
paramsToSign += "&signature=" + base64.StdEncoding.EncodeToString(signature)
}
reqURL.RawQuery = paramsToSign

Expand Down
2 changes: 2 additions & 0 deletions common/common/websocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -1301,6 +1301,8 @@ func (w *WebsocketStreams) Unsubscribe(streams []string) error {

conn.mu.Lock()
delete(conn.StreamCallbackMap, stream)
conn.mu.Unlock()

log.Printf("Unsubscribed from stream %s", stream)
}

Expand Down
88 changes: 68 additions & 20 deletions common/tests/unit/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"github.com/binance/binance-connector-go/common/common"
)

// Mock type implementing MappedNullable
type mockMappedNullable struct {
Data map[string]interface{}
Err error
Expand Down Expand Up @@ -79,7 +78,7 @@ func TestParameterAddToHeaderOrQuery_StructMappedNullable(t *testing.T) {
if result.Data["field1"] != "value1" {
t.Errorf("Expected field1=value1, got %v", result.Data["field1"])
}
if result.Data["field2"] != float64(123) { // JSON numbers become float64
if result.Data["field2"] != float64(123) {
t.Errorf("Expected field2=123, got %v", result.Data["field2"])
}
}
Expand Down Expand Up @@ -349,7 +348,7 @@ func TestParseRateLimitHeaders_RetryAfter(t *testing.T) {

func TestParseRateLimitHeaders_InvalidCount(t *testing.T) {
h := http.Header{}
h.Set("X-MBX-USED-WEIGHT-1m", "abc") // invalid number
h.Set("X-MBX-USED-WEIGHT-1m", "abc")
rates, err := common.ParseRateLimitHeaders(h)
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -393,7 +392,7 @@ func TestSendRequest_Success(t *testing.T) {

cfg := &common.ConfigurationRestAPI{}

resp, err := common.SendRequest[SampleResponse](context.Background(), server.URL, "GET", url.Values{}, nil, cfg)
resp, err := common.SendRequest[SampleResponse](context.Background(), server.URL, "GET", url.Values{}, nil, cfg, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -403,6 +402,34 @@ func TestSendRequest_Success(t *testing.T) {
}
}

func TestSendRequest_Signing(t *testing.T) {
cfg := &common.ConfigurationRestAPI{
ApiKey: "apikey123",
ApiSecret: "secretkey456",
}
handler := func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
signature := query.Get("signature")
if signature == "" {
t.Errorf("Expected signature parameter to be set")
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
if _, err := fmt.Fprintln(w, `{"message":"signed"}`); err != nil {
t.Fatal(err)
}
}
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()
resp, err := common.SendRequest[SampleResponse](context.Background(), server.URL, "GET", url.Values{}, nil, cfg, true)
if err != nil {
t.Fatal(err)
}
if resp.Status != 200 || resp.Data.Message != "signed" {
t.Errorf("Unexpected response: %+v", resp)
}
}

func TestSendRequest_RetryLogic(t *testing.T) {
attempts := 0
handler := func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -417,12 +444,11 @@ func TestSendRequest_RetryLogic(t *testing.T) {
Backoff: 0,
}

// Mock retry logic
originalShouldRetry = common.ShouldRetryRequest
common.ShouldRetryRequest = mockShouldRetry
defer func() { common.ShouldRetryRequest = originalShouldRetry }()

resp, err := common.SendRequest[SampleResponse](context.Background(), server.URL, "GET", url.Values{}, nil, cfg)
resp, err := common.SendRequest[SampleResponse](context.Background(), server.URL, "GET", url.Values{}, nil, cfg, false)
if err == nil {
t.Fatalf("Expected error for server failure")
}
Expand All @@ -445,7 +471,7 @@ func TestSendRequest_HTTPErrorStatus(t *testing.T) {

cfg := &common.ConfigurationRestAPI{}

resp, err := common.SendRequest[SampleResponse](context.Background(), server.URL, "GET", url.Values{}, nil, cfg)
resp, err := common.SendRequest[SampleResponse](context.Background(), server.URL, "GET", url.Values{}, nil, cfg, false)
if err == nil {
t.Fatalf("Expected error for 404")
}
Expand Down Expand Up @@ -477,7 +503,7 @@ func TestSendRequest_ContentEncodingGzip(t *testing.T) {

cfg := &common.ConfigurationRestAPI{}

resp, err := common.SendRequest[SampleResponse](context.Background(), server.URL, "GET", url.Values{}, nil, cfg)
resp, err := common.SendRequest[SampleResponse](context.Background(), server.URL, "GET", url.Values{}, nil, cfg, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -494,7 +520,7 @@ func TestPrepareRequest_BasicHeadersAndQuery(t *testing.T) {
query := url.Values{}
query.Set("param", "value")

req, err := common.PrepareRequest(context.Background(), "https://example.com/test", "GET", map[string]string{}, query, nil, cfg)
req, err := common.PrepareRequest(context.Background(), "https://example.com/test", "GET", map[string]string{}, query, nil, cfg, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -508,12 +534,38 @@ func TestPrepareRequest_BasicHeadersAndQuery(t *testing.T) {
}
}

func TestPrepareRequest_Signing(t *testing.T) {
cfg := &common.ConfigurationRestAPI{
ApiKey: "apikey123",
ApiSecret: "secretkey456",
}
query := url.Values{}
query.Set("param", "value")

req, err := common.PrepareRequest(context.Background(), "https://example.com/test", "GET", map[string]string{}, query, nil, cfg, true)
if err != nil {
t.Fatal(err)
}

if req.Header.Get("X-MBX-APIKEY") != "apikey123" {
t.Errorf("Expected API key header set")
}

if req.URL.Query().Get("param") != "value" {
t.Errorf("Expected query param preserved")
}

if req.URL.Query().Get("signature") == "" {
t.Errorf("Expected signature parameter to be set")
}
}

func TestPrepareRequest_CustomHeadersAndCompression(t *testing.T) {
cfg := &common.ConfigurationRestAPI{
CustomHeaders: map[string]string{"X-Custom": "abc"},
Compression: true,
}
req, err := common.PrepareRequest(context.Background(), "https://example.com", "GET", map[string]string{}, url.Values{}, nil, cfg)
req, err := common.PrepareRequest(context.Background(), "https://example.com", "GET", map[string]string{}, url.Values{}, nil, cfg, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -534,18 +586,14 @@ func TestPrepareRequest_WithURLValuesBody(t *testing.T) {
body.Set("key2", "value2")

req, err := common.PrepareRequest(context.Background(), "https://example.com", "POST",
map[string]string{}, url.Values{}, body, cfg)
map[string]string{}, url.Values{}, body, cfg, false)
if err != nil {
t.Fatal(err)
}

if req.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
t.Errorf("Expected Content-Type header to be set")
}

// Verify the body is included in paramsToSign
// We can't directly check paramsToSign as it's not returned, but we can verify the behavior
// by checking that the request would be properly signed if ApiSecret was set
}

func TestPrepareRequest_WithMapStringStringBody(t *testing.T) {
Expand All @@ -556,7 +604,7 @@ func TestPrepareRequest_WithMapStringStringBody(t *testing.T) {
}

req, err := common.PrepareRequest(context.Background(), "https://example.com", "POST",
map[string]string{}, url.Values{}, body, cfg)
map[string]string{}, url.Values{}, body, cfg, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -575,7 +623,7 @@ func TestPrepareRequest_WithMapStringInterfaceBody(t *testing.T) {
}

req, err := common.PrepareRequest(context.Background(), "https://example.com", "POST",
map[string]string{}, url.Values{}, body, cfg)
map[string]string{}, url.Values{}, body, cfg, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -596,7 +644,7 @@ func TestPrepareRequest_WithJSONBody(t *testing.T) {
}

req, err := common.PrepareRequest(context.Background(), "https://example.com", "POST",
map[string]string{}, url.Values{}, body, cfg)
map[string]string{}, url.Values{}, body, cfg, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -610,7 +658,7 @@ func TestPrepareRequest_WithNilBody(t *testing.T) {
cfg := &common.ConfigurationRestAPI{}

req, err := common.PrepareRequest(context.Background(), "https://example.com", "GET",
map[string]string{}, url.Values{}, nil, cfg)
map[string]string{}, url.Values{}, nil, cfg, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -628,7 +676,7 @@ func TestPrepareRequest_WithQueryAndBody(t *testing.T) {
body.Set("bodyParam", "bodyValue")

req, err := common.PrepareRequest(context.Background(), "https://example.com", "POST",
map[string]string{}, query, body, cfg)
map[string]string{}, query, body, cfg, false)
if err != nil {
t.Fatal(err)
}
Expand Down
4 changes: 0 additions & 4 deletions common/tests/unit/websocket_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -734,10 +734,6 @@ func TestProcessMessage_ResponseMessageHandled(t *testing.T) {
},
}}

// doneChan := make(chan []byte, 1)
// mockPendingMsgs := sync.Map{}
// mockPendingMsgs.Store("0", doneChan)

conn := &common.WebSocketConnection{
Id: "test-connection",
Connected: common.OPEN,
Expand Down