-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
401 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,4 +6,5 @@ use ( | |
./manager | ||
./max | ||
./okx | ||
./pionex | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
package pionex | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"crypto/hmac" | ||
"crypto/sha256" | ||
"encoding/hex" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
"os" | ||
) | ||
|
||
const ( | ||
baseAPIMainURL = "https://api.pionex.com" | ||
|
||
timestampKey = "timestamp" | ||
|
||
apiKeyHeader = "PIONEX-KEY" | ||
signatureHeader = "PIONEX-SIGNATURE" | ||
) | ||
|
||
type Client struct { | ||
apiKey string | ||
apiSecret string | ||
apiEndpoint string | ||
httpClient *http.Client | ||
} | ||
|
||
type Client_builder struct { | ||
APIKey string | ||
APISecret string | ||
APIEndpoint string | ||
HTTPClient *http.Client | ||
} | ||
|
||
func (b Client_builder) Build() *Client { | ||
if b.APIKey == "" { | ||
b.APIKey = os.Getenv("PIONEX_API_KEY") | ||
} | ||
if b.APISecret == "" { | ||
b.APISecret = os.Getenv("PIONEX_API_SECRET") | ||
} | ||
if b.APIKey == "" || b.APISecret == "" { | ||
panic("API key and secret are required") | ||
} | ||
if b.APIEndpoint == "" { | ||
b.APIEndpoint = baseAPIMainURL | ||
} | ||
if b.HTTPClient == nil { | ||
b.HTTPClient = http.DefaultClient | ||
} | ||
return &Client{ | ||
apiKey: b.APIKey, | ||
apiSecret: b.APISecret, | ||
apiEndpoint: b.APIEndpoint, | ||
httpClient: b.HTTPClient, | ||
} | ||
} | ||
|
||
func (c *Client) CallAPI(ctx context.Context, r *Request, opts ...RequestOption) (data []byte, err error) { | ||
// set request options from user | ||
for _, opt := range opts { | ||
opt(r) | ||
} | ||
|
||
path := r.endpoint | ||
query := url.Values{} | ||
header := http.Header{} | ||
body := "" | ||
// set request parameters | ||
if r.method == http.MethodGet { | ||
for k, v := range r.params { | ||
query.Add(k, fmt.Sprintf("%v", v)) | ||
} | ||
} else { | ||
b, err := json.Marshal(r.params) | ||
if err != nil { | ||
return nil, err | ||
} | ||
body = string(b) | ||
if body == "{}" { | ||
body = "" | ||
} | ||
header.Add("Content-Type", "application/json") | ||
} | ||
if r.secType == SecTypePrivate { | ||
query.Add(timestampKey, fmt.Sprintf("%d", currentTimestamp())) | ||
} | ||
if len(query) > 0 { | ||
path += "?" + query.Encode() | ||
} | ||
if r.secType == SecTypePrivate { | ||
header.Set(apiKeyHeader, c.apiKey) | ||
header.Set(signatureHeader, sign(c.apiSecret, r.method+path+body)) | ||
} | ||
|
||
// create request | ||
var req *http.Request | ||
var res *http.Response | ||
req, err = http.NewRequestWithContext(ctx, r.method, fmt.Sprintf("%s%s", c.apiEndpoint, path), bytes.NewBuffer([]byte(body))) | ||
if err != nil { | ||
return nil, err | ||
} | ||
req.Header = header | ||
res, err = c.httpClient.Do(req) | ||
if err != nil { | ||
return []byte{}, err | ||
} | ||
data, err = io.ReadAll(res.Body) | ||
if err != nil { | ||
return []byte{}, err | ||
} | ||
defer func() { | ||
cerr := res.Body.Close() | ||
// Only overwrite the retured error if the original error was nil and an | ||
// error occurred while closing the body. | ||
if err == nil && cerr != nil { | ||
err = cerr | ||
} | ||
}() | ||
|
||
if res.StatusCode >= http.StatusBadRequest { | ||
apiErr := &APIError{} | ||
e := json.Unmarshal(data, apiErr) | ||
if e != nil { | ||
fmt.Fprintf(os.Stderr, "failed to unmarshal json: %s", e) | ||
} | ||
return nil, apiErr | ||
} | ||
return data, nil | ||
} | ||
|
||
func sign(secret, message string) string { | ||
mac := hmac.New(sha256.New, []byte(secret)) | ||
mac.Write([]byte(message)) | ||
return hex.EncodeToString(mac.Sum(nil)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package pionex | ||
|
||
import "testing" | ||
|
||
func Test_sign(t *testing.T) { | ||
type args struct { | ||
secret string | ||
message string | ||
} | ||
tests := []struct { | ||
name string | ||
args args | ||
want string | ||
}{ | ||
{ | ||
name: "empty", | ||
want: "b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad", | ||
}, | ||
{ | ||
name: "non-empty", | ||
args: args{ | ||
"sec", | ||
`{"msg":"test"}`, | ||
}, | ||
want: "23aa07ef2fc07cd18c30a180feea514b0f2717499cc070d7715c9c1a0e4e7c7a", | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
if got := sign(tt.args.secret, tt.args.message); got != tt.want { | ||
t.Errorf("sign() = %v, want %v", got, tt.want) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package pionex | ||
|
||
import ( | ||
"fmt" | ||
) | ||
|
||
// APIError define API error when response status is 4xx or 5xx | ||
type APIError struct { | ||
BaseResponse | ||
Code string `json:"code"` | ||
Message string `json:"message"` | ||
} | ||
|
||
// Error return error code and message | ||
func (e APIError) Error() string { | ||
return fmt.Sprintf("<APIError> code=%s, message=%s", e.Code, e.Message) | ||
} | ||
|
||
// IsAPIError check if e is an API error | ||
func IsAPIError(e error) bool { | ||
_, ok := e.(*APIError) | ||
return ok | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package pionex | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
) | ||
|
||
func TestAPIError_Error(t *testing.T) { | ||
type fields struct { | ||
BaseResponse BaseResponse | ||
Code string | ||
Message string | ||
} | ||
tests := []struct { | ||
name string | ||
fields fields | ||
want string | ||
}{ | ||
{ | ||
name: "empty", | ||
want: "<APIError> code=, message=", | ||
}, | ||
{ | ||
name: "non-empty", | ||
fields: fields{ | ||
Code: "APIKEY_LOST", | ||
Message: "test", | ||
}, | ||
want: "<APIError> code=APIKEY_LOST, message=test", | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
e := APIError{ | ||
BaseResponse: tt.fields.BaseResponse, | ||
Code: tt.fields.Code, | ||
Message: tt.fields.Message, | ||
} | ||
if got := e.Error(); got != tt.want { | ||
t.Errorf("APIError.Error() = %v, want %v", got, tt.want) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestIsAPIError(t *testing.T) { | ||
type args struct { | ||
e error | ||
} | ||
tests := []struct { | ||
name string | ||
args args | ||
want bool | ||
}{ | ||
{ | ||
name: "nil", | ||
args: args{ | ||
e: nil, | ||
}, | ||
want: false, | ||
}, | ||
{ | ||
name: "APIError", | ||
args: args{ | ||
e: &APIError{}, | ||
}, | ||
want: true, | ||
}, | ||
{ | ||
name: "other", | ||
args: args{ | ||
fmt.Errorf("test"), | ||
}, | ||
want: false, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
if got := IsAPIError(tt.args.e); got != tt.want { | ||
t.Errorf("IsAPIError() = %v, want %v", got, tt.want) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module github.com/dasbd72/go-exchange-sdk/pionex | ||
|
||
go 1.22.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package pionex | ||
|
||
import "net/http" | ||
|
||
type SecType int | ||
|
||
const ( | ||
// SecTypePublic defines no security type | ||
SecTypePublic SecType = iota | ||
// SecTypePrivate defines signature required: | ||
// | ||
// timestamp(query) defines the current timestamp in milliseconds | ||
// | ||
// PIONEX-SIGNATURE(header) defines the signature of the request | ||
SecTypePrivate | ||
) | ||
|
||
// Request define an API request, build with Request_builder | ||
type Request struct { | ||
method string | ||
endpoint string | ||
secType SecType | ||
params map[string]interface{} | ||
} | ||
|
||
// Request_builder define a builder for Request | ||
type Request_builder struct { | ||
Method string | ||
Endpoint string | ||
SecType SecType | ||
Params map[string]interface{} | ||
} | ||
|
||
func (b Request_builder) Build() *Request { | ||
if b.Method == "" { | ||
b.Method = http.MethodGet | ||
} | ||
if b.Params == nil { | ||
b.Params = map[string]interface{}{} | ||
} | ||
return &Request{ | ||
method: b.Method, | ||
endpoint: b.Endpoint, | ||
secType: b.SecType, | ||
params: b.Params, | ||
} | ||
} | ||
|
||
// RequestOption define option type for request | ||
type RequestOption func(*Request) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package pionex | ||
|
||
type BaseResponse struct { | ||
Result bool `json:"result"` // Response result | ||
Timestamp int64 `json:"timestamp"` // Response timestamp in milliseconds | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package pionex | ||
|
||
import "time" | ||
|
||
func currentTimestamp() int64 { | ||
return formatTimestamp(time.Now()) | ||
} | ||
|
||
// formatTimestamp formats a time into Unix timestamp in milliseconds, as requested by Binance. | ||
func formatTimestamp(t time.Time) int64 { | ||
return t.UnixNano() / int64(time.Millisecond) | ||
} |
Oops, something went wrong.