Skip to content

Commit f164100

Browse files
authored
Merge pull request #86 from binance/rc-common-v1.2.0
2 parents a740878 + 981fa2a commit f164100

File tree

7 files changed

+113
-49
lines changed

7 files changed

+113
-49
lines changed

.github/workflows/release.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ jobs:
157157
158158
if [ -z "$LATEST_TAG" ]; then
159159
# No tags exist yet, start with v1.0.0
160-
NEW_VERSION="v1.1.0"
160+
NEW_VERSION="v1.0.0"
161161
else
162162
if [ "${{ matrix.client }}" == "common" ]; then
163163
CURRENT_VERSION=${LATEST_TAG#${{ matrix.client }}/v}

common/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
### Changelog
22

3+
## 1.2.0 - 2026-01-23
4+
5+
### Added (1)
6+
7+
- Added `Alpha` base url
8+
9+
### Changed (2)
10+
11+
- Updated `PrepareRequest` and `SendRequest` methods to include `signed` boolean parameter to indicate if the request requires signing.
12+
- Fixed Lock issue in `WebsocketStreams` `Unsubscribe` method by ensuring proper unlocking of mutex after stream removal.
13+
314
## 1.1.0 - 2026-01-13
415

516
- Updated `WebsocketStreams` `Subscribe` method to support `int32` format for `id` parameter

common/common/constants.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ const (
3939
// Algo API URLs
4040
const AlgoRestApiProdUrl = "https://api.binance.com"
4141

42+
// Alpha API URLs
43+
const AlphaRestApiProdUrl = "https://www.binance.com"
44+
4245
// C2C API URLs
4346
const C2CRestApiProdUrl = "https://api.binance.com"
4447

common/common/utils.go

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -415,8 +415,9 @@ func ParseRateLimitHeaders(header http.Header) ([]RateLimit, error) {
415415
// @param method The HTTP method (GET, POST, etc.).
416416
// @param queryParams The query parameters for the request.
417417
// @param cfg The configuration containing API keys, secrets, and other settings.
418+
// @param signed A boolean indicating whether the request requires signing.
418419
// @return The response from the REST API or an error if the request fails.
419-
func SendRequest[T any](ctx context.Context, path string, method string, queryParams url.Values, bodyParams interface{}, cfg *ConfigurationRestAPI) (*RestApiResponse[T], error) {
420+
func SendRequest[T any](ctx context.Context, path string, method string, queryParams url.Values, bodyParams interface{}, cfg *ConfigurationRestAPI, signed bool) (*RestApiResponse[T], error) {
420421
var (
421422
localVarHeaderParams = make(map[string]string)
422423
localVarHTTPContentTypes = []string{}
@@ -427,7 +428,7 @@ func SendRequest[T any](ctx context.Context, path string, method string, queryPa
427428
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
428429
}
429430

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

445446
httpClient := SetupProxy(cfg)
446-
447447
for attempt := 0; attempt <= retries; attempt++ {
448448
resp, err := httpClient.Do(req)
449449
if err != nil {
@@ -543,14 +543,16 @@ func SendRequest[T any](ctx context.Context, path string, method string, queryPa
543543
// @param headerParams A map of header parameters to include in the request.
544544
// @param queryParams The query parameters for the request.
545545
// @param c The configuration containing API keys, secrets, and other settings.
546+
// @param signed A boolean indicating whether the request requires signing.
546547
// @return The prepared HTTP request or an error if preparation fails.
547548
func PrepareRequest(
548549
ctx context.Context,
549550
path string, method string,
550551
headerParams map[string]string,
551552
queryParams url.Values,
552553
bodyParams interface{},
553-
c *ConfigurationRestAPI) (localVarRequest *http.Request, err error) {
554+
c *ConfigurationRestAPI,
555+
signed bool) (localVarRequest *http.Request, err error) {
554556

555557
reqURL, err := url.Parse(path)
556558
if err != nil {
@@ -608,34 +610,36 @@ func PrepareRequest(
608610
}
609611
}
610612

611-
if c.ApiSecret != "" {
612-
paramsToSign += "&timestamp=" + strconv.FormatInt(time.Now().UnixMilli(), 10)
613+
if signed && c != nil {
614+
if c.ApiSecret != "" {
615+
paramsToSign += "&timestamp=" + strconv.FormatInt(time.Now().UnixMilli(), 10)
613616

614-
signer := hmac.New(sha256.New, []byte(c.ApiSecret))
615-
signer.Write([]byte(paramsToSign))
616-
signature := signer.Sum(nil)
617-
if err != nil {
618-
return nil, err
617+
signer := hmac.New(sha256.New, []byte(c.ApiSecret))
618+
signer.Write([]byte(paramsToSign))
619+
signature := signer.Sum(nil)
620+
if err != nil {
621+
return nil, err
622+
}
623+
paramsToSign += "&signature=" + fmt.Sprintf("%x", signature)
619624
}
620-
paramsToSign += "&signature=" + fmt.Sprintf("%x", signature)
621-
}
622625

623-
if c.PrivateKey != "" {
624-
paramsToSign += "&timestamp=" + strconv.FormatInt(time.Now().UnixMilli(), 10)
626+
if c.PrivateKey != "" {
627+
paramsToSign += "&timestamp=" + strconv.FormatInt(time.Now().UnixMilli(), 10)
625628

626-
if c.Signer == nil {
627-
key, err := LoadPrivateKey(c.PrivateKey, c.PrivateKeyPassphrase)
629+
if c.Signer == nil {
630+
key, err := LoadPrivateKey(c.PrivateKey, c.PrivateKeyPassphrase)
631+
if err != nil {
632+
panic(err)
633+
}
634+
c.Signer = &cryptoSigner{s: key}
635+
}
636+
637+
signature, _ := c.Signer.Sign([]byte(paramsToSign))
628638
if err != nil {
629639
panic(err)
630640
}
631-
c.Signer = &cryptoSigner{s: key}
632-
}
633-
634-
signature, _ := c.Signer.Sign([]byte(paramsToSign))
635-
if err != nil {
636-
panic(err)
641+
paramsToSign += "&signature=" + base64.StdEncoding.EncodeToString(signature)
637642
}
638-
paramsToSign += "&signature=" + base64.StdEncoding.EncodeToString(signature)
639643
}
640644
reqURL.RawQuery = paramsToSign
641645

common/common/websocket.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,6 +1301,8 @@ func (w *WebsocketStreams) Unsubscribe(streams []string) error {
13011301

13021302
conn.mu.Lock()
13031303
delete(conn.StreamCallbackMap, stream)
1304+
conn.mu.Unlock()
1305+
13041306
log.Printf("Unsubscribed from stream %s", stream)
13051307
}
13061308

common/tests/unit/utils_test.go

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"github.com/binance/binance-connector-go/common/common"
2121
)
2222

23-
// Mock type implementing MappedNullable
2423
type mockMappedNullable struct {
2524
Data map[string]interface{}
2625
Err error
@@ -79,7 +78,7 @@ func TestParameterAddToHeaderOrQuery_StructMappedNullable(t *testing.T) {
7978
if result.Data["field1"] != "value1" {
8079
t.Errorf("Expected field1=value1, got %v", result.Data["field1"])
8180
}
82-
if result.Data["field2"] != float64(123) { // JSON numbers become float64
81+
if result.Data["field2"] != float64(123) {
8382
t.Errorf("Expected field2=123, got %v", result.Data["field2"])
8483
}
8584
}
@@ -349,7 +348,7 @@ func TestParseRateLimitHeaders_RetryAfter(t *testing.T) {
349348

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

394393
cfg := &common.ConfigurationRestAPI{}
395394

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

405+
func TestSendRequest_Signing(t *testing.T) {
406+
cfg := &common.ConfigurationRestAPI{
407+
ApiKey: "apikey123",
408+
ApiSecret: "secretkey456",
409+
}
410+
handler := func(w http.ResponseWriter, r *http.Request) {
411+
query := r.URL.Query()
412+
signature := query.Get("signature")
413+
if signature == "" {
414+
t.Errorf("Expected signature parameter to be set")
415+
}
416+
w.Header().Set("Content-Type", "application/json")
417+
w.WriteHeader(200)
418+
if _, err := fmt.Fprintln(w, `{"message":"signed"}`); err != nil {
419+
t.Fatal(err)
420+
}
421+
}
422+
server := httptest.NewServer(http.HandlerFunc(handler))
423+
defer server.Close()
424+
resp, err := common.SendRequest[SampleResponse](context.Background(), server.URL, "GET", url.Values{}, nil, cfg, true)
425+
if err != nil {
426+
t.Fatal(err)
427+
}
428+
if resp.Status != 200 || resp.Data.Message != "signed" {
429+
t.Errorf("Unexpected response: %+v", resp)
430+
}
431+
}
432+
406433
func TestSendRequest_RetryLogic(t *testing.T) {
407434
attempts := 0
408435
handler := func(w http.ResponseWriter, r *http.Request) {
@@ -417,12 +444,11 @@ func TestSendRequest_RetryLogic(t *testing.T) {
417444
Backoff: 0,
418445
}
419446

420-
// Mock retry logic
421447
originalShouldRetry = common.ShouldRetryRequest
422448
common.ShouldRetryRequest = mockShouldRetry
423449
defer func() { common.ShouldRetryRequest = originalShouldRetry }()
424450

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

446472
cfg := &common.ConfigurationRestAPI{}
447473

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

478504
cfg := &common.ConfigurationRestAPI{}
479505

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

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

537+
func TestPrepareRequest_Signing(t *testing.T) {
538+
cfg := &common.ConfigurationRestAPI{
539+
ApiKey: "apikey123",
540+
ApiSecret: "secretkey456",
541+
}
542+
query := url.Values{}
543+
query.Set("param", "value")
544+
545+
req, err := common.PrepareRequest(context.Background(), "https://example.com/test", "GET", map[string]string{}, query, nil, cfg, true)
546+
if err != nil {
547+
t.Fatal(err)
548+
}
549+
550+
if req.Header.Get("X-MBX-APIKEY") != "apikey123" {
551+
t.Errorf("Expected API key header set")
552+
}
553+
554+
if req.URL.Query().Get("param") != "value" {
555+
t.Errorf("Expected query param preserved")
556+
}
557+
558+
if req.URL.Query().Get("signature") == "" {
559+
t.Errorf("Expected signature parameter to be set")
560+
}
561+
}
562+
511563
func TestPrepareRequest_CustomHeadersAndCompression(t *testing.T) {
512564
cfg := &common.ConfigurationRestAPI{
513565
CustomHeaders: map[string]string{"X-Custom": "abc"},
514566
Compression: true,
515567
}
516-
req, err := common.PrepareRequest(context.Background(), "https://example.com", "GET", map[string]string{}, url.Values{}, nil, cfg)
568+
req, err := common.PrepareRequest(context.Background(), "https://example.com", "GET", map[string]string{}, url.Values{}, nil, cfg, false)
517569
if err != nil {
518570
t.Fatal(err)
519571
}
@@ -534,18 +586,14 @@ func TestPrepareRequest_WithURLValuesBody(t *testing.T) {
534586
body.Set("key2", "value2")
535587

536588
req, err := common.PrepareRequest(context.Background(), "https://example.com", "POST",
537-
map[string]string{}, url.Values{}, body, cfg)
589+
map[string]string{}, url.Values{}, body, cfg, false)
538590
if err != nil {
539591
t.Fatal(err)
540592
}
541593

542594
if req.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
543595
t.Errorf("Expected Content-Type header to be set")
544596
}
545-
546-
// Verify the body is included in paramsToSign
547-
// We can't directly check paramsToSign as it's not returned, but we can verify the behavior
548-
// by checking that the request would be properly signed if ApiSecret was set
549597
}
550598

551599
func TestPrepareRequest_WithMapStringStringBody(t *testing.T) {
@@ -556,7 +604,7 @@ func TestPrepareRequest_WithMapStringStringBody(t *testing.T) {
556604
}
557605

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

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

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

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

630678
req, err := common.PrepareRequest(context.Background(), "https://example.com", "POST",
631-
map[string]string{}, query, body, cfg)
679+
map[string]string{}, query, body, cfg, false)
632680
if err != nil {
633681
t.Fatal(err)
634682
}

common/tests/unit/websocket_test.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -734,10 +734,6 @@ func TestProcessMessage_ResponseMessageHandled(t *testing.T) {
734734
},
735735
}}
736736

737-
// doneChan := make(chan []byte, 1)
738-
// mockPendingMsgs := sync.Map{}
739-
// mockPendingMsgs.Store("0", doneChan)
740-
741737
conn := &common.WebSocketConnection{
742738
Id: "test-connection",
743739
Connected: common.OPEN,

0 commit comments

Comments
 (0)