Skip to content

Commit

Permalink
add pionex client sdk
Browse files Browse the repository at this point in the history
  • Loading branch information
dasbd72 committed Mar 1, 2024
1 parent 7be27be commit efaa099
Show file tree
Hide file tree
Showing 10 changed files with 401 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ use (
./manager
./max
./okx
./pionex
)
141 changes: 141 additions & 0 deletions pionex/client.go
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))
}
35 changes: 35 additions & 0 deletions pionex/client_test.go
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)
}
})
}
}
23 changes: 23 additions & 0 deletions pionex/errors.go
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
}
84 changes: 84 additions & 0 deletions pionex/errors_test.go
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)
}
})
}
}
3 changes: 3 additions & 0 deletions pionex/go.mod
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
50 changes: 50 additions & 0 deletions pionex/request.go
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)
6 changes: 6 additions & 0 deletions pionex/response.go
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
}
12 changes: 12 additions & 0 deletions pionex/time.go
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)
}
Loading

0 comments on commit efaa099

Please sign in to comment.