From 19b02e129bf89094d4b686680e9268741b276dfd Mon Sep 17 00:00:00 2001 From: wuhewuhe Date: Mon, 30 Oct 2023 19:26:54 +0100 Subject: [PATCH] http public request --- .idea/.gitignore | 8 ++ .idea/bybit.go.api.iml | 9 ++ .idea/modules.xml | 8 ++ .idea/vcs.xml | 7 ++ bybit_api_client.go | 186 +++++++++++++++++++++++++++++++++ consts.go | 5 + examples/app.go | 7 ++ examples/market/server_time.go | 23 ++++ go.mod | 8 ++ go.sum | 3 + handlers/errors.go | 20 ++++ market.go | 23 ++++ request.go | 76 ++++++++++++++ 13 files changed, 383 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/bybit.go.api.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 bybit_api_client.go create mode 100644 consts.go create mode 100644 examples/app.go create mode 100644 examples/market/server_time.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 handlers/errors.go create mode 100644 market.go create mode 100644 request.go diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..1c2fda5 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/bybit.go.api.iml b/.idea/bybit.go.api.iml new file mode 100644 index 0000000..338a266 --- /dev/null +++ b/.idea/bybit.go.api.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..7288a09 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..f245aa7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/bybit_api_client.go b/bybit_api_client.go new file mode 100644 index 0000000..15e0d8d --- /dev/null +++ b/bybit_api_client.go @@ -0,0 +1,186 @@ +package bybit + +import ( + "bybit.go.api/handlers" + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "time" +) + +// TimeInForceType define time in force type of order +type TimeInForceType string + +// Client define API client +type Client struct { + APIKey string + SecretKey string + BaseURL string + HTTPClient *http.Client + Debug bool + Logger *log.Logger + do doFunc +} + +type doFunc func(req *http.Request) (*http.Response, error) + +// Globals +const ( + timestampKey = "X-BAPI-TIMESTAMP" + signatureKey = "X-BAPI-SIGN" + apiRequestKey = "X-BAPI-API-KEY" + recvWindowKey = "X-BAPI-RECV-WINDOW" + signTypeKey = "2" +) + +func currentTimestamp() int64 { + return FormatTimestamp(time.Now()) +} + +// FormatTimestamp formats a time into Unix timestamp in milliseconds, as requested by Bybit. +func FormatTimestamp(t time.Time) int64 { + return t.UnixNano() / int64(time.Millisecond) +} + +func PrettyPrint(i interface{}) string { + s, _ := json.MarshalIndent(i, "", "\t") + return string(s) +} + +func (c *Client) debug(format string, v ...interface{}) { + if c.Debug { + c.Logger.Printf(format, v...) + } +} + +// NewClient Create client function for initialising new Bybit client +func NewClient(apiKey string, secretKey string, baseURL ...string) *Client { + url := "https://api.bybit.com" + + if len(baseURL) > 0 { + url = baseURL[0] + } + + return &Client{ + APIKey: apiKey, + SecretKey: secretKey, + BaseURL: url, + HTTPClient: http.DefaultClient, + Logger: log.New(os.Stderr, Name, log.LstdFlags), + } +} + +func (c *Client) parseRequest(r *request, opts ...RequestOption) (err error) { + // set request options from user + for _, opt := range opts { + opt(r) + } + err = r.validate() + if err != nil { + return err + } + + fullURL := fmt.Sprintf("%s%s", c.BaseURL, r.endpoint) + + queryString := r.query.Encode() + body := &bytes.Buffer{} + bodyString := r.form.Encode() + header := http.Header{} + if r.header != nil { + header = r.header.Clone() + } + header.Set("User-Agent", fmt.Sprintf("%s/%s", Name, Version)) + if bodyString != "" { + header.Set("Content-Type", "application/json") + body = bytes.NewBufferString(bodyString) + } + if r.secType == secTypeSigned { + header.Set(signTypeKey, "2") + header.Set(apiRequestKey, c.APIKey) + header.Set(timestampKey, fmt.Sprintf("%d", currentTimestamp())) + if r.recvWindow == "" { + header.Set(recvWindowKey, "5000") + } else { + header.Set(recvWindowKey, r.recvWindow) + } + + var signatureBase string + if r.method == "GET" { + signatureBase = fmt.Sprintf("%d%s%s%s", FormatTimestamp(time.Now()), c.APIKey, r.recvWindow, queryString) + } else if r.method == "POST" { + signatureBase = fmt.Sprintf("%d%s%s%s", FormatTimestamp(time.Now()), c.APIKey, r.recvWindow, bodyString) + } + + mac := hmac.New(sha256.New, []byte(c.SecretKey)) + _, err = mac.Write([]byte(signatureBase)) + if err != nil { + return err + } + signatureValue := fmt.Sprintf("%x", mac.Sum(nil)) + header.Set(signatureKey, signatureValue) + } + if queryString != "" { + fullURL = fmt.Sprintf("%s?%s", fullURL, queryString) + } + // c.debug("full url: %s, body: %s", fullURL, bodyString) + r.fullURL = fullURL + r.header = header + r.body = body + return nil +} + +func (c *Client) callAPI(ctx context.Context, r *request, opts ...RequestOption) (data []byte, err error) { + err = c.parseRequest(r, opts...) + req, err := http.NewRequest(r.method, r.fullURL, r.body) + if err != nil { + return []byte{}, err + } + req = req.WithContext(ctx) + req.Header = r.header + // c.debug("request: %#v", req) + f := c.do + if f == nil { + f = c.HTTPClient.Do + } + res, err := f(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 returned error if the original error was nil and an + // error occurred while closing the body. + if err == nil && cerr != nil { + err = cerr + } + }() + // c.debug("response: %#v", res) + c.debug("response body: %s", string(data)) + // c.debug("response status code: %d", res.StatusCode) + + if res.StatusCode >= http.StatusBadRequest { + apiErr := new(handlers.APIError) + e := json.Unmarshal(data, apiErr) + if e != nil { + // c.debug("failed to unmarshal json: %s", e) + } + return nil, apiErr + } + return data, nil +} + +// NewServerTimeService Market Endpoints +func (c *Client) NewServerTimeService() *ServerTime { + return &ServerTime{c: c} +} diff --git a/consts.go b/consts.go new file mode 100644 index 0000000..cdbd73c --- /dev/null +++ b/consts.go @@ -0,0 +1,5 @@ +package bybit + +const Name = "bybit.api.go" + +const Version = "1.0.0" diff --git a/examples/app.go b/examples/app.go new file mode 100644 index 0000000..c048119 --- /dev/null +++ b/examples/app.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("hello world") +} diff --git a/examples/market/server_time.go b/examples/market/server_time.go new file mode 100644 index 0000000..77d1d5a --- /dev/null +++ b/examples/market/server_time.go @@ -0,0 +1,23 @@ +package main + +import ( + bybit "bybit.go.api" + "context" + "fmt" +) + +func main() { + ServerTime() +} + +func ServerTime() { + + client := bybit.NewClient("", "") + + // set to debug mode + client.Debug = true + + // NewServerTimeService + serverTime := client.NewServerTimeService().Do(context.Background()) + fmt.Println(bybit.PrettyPrint(serverTime)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..914a29e --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module bybit.go.api + +go 1.21 + +require ( + github.com/bitly/go-simplejson v0.5.1 + github.com/gorilla/websocket v1.5.0 + ) \ No newline at end of file diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a746f7a --- /dev/null +++ b/go.sum @@ -0,0 +1,3 @@ +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= diff --git a/handlers/errors.go b/handlers/errors.go new file mode 100644 index 0000000..a2ab602 --- /dev/null +++ b/handlers/errors.go @@ -0,0 +1,20 @@ +package handlers + +import "fmt" + +// APIError define API error when response status is 4xx or 5xx +type APIError struct { + Code int64 `json:"retCode"` + Message string `json:"retMsg"` +} + +// Error return error code and message +func (e APIError) Error() string { + return fmt.Sprintf(" code=%d, msg=%s", e.Code, e.Message) +} + +// IsAPIError check if e is an API error +func IsAPIError(e error) bool { + _, ok := e.(*APIError) + return ok +} diff --git a/market.go b/market.go new file mode 100644 index 0000000..448335b --- /dev/null +++ b/market.go @@ -0,0 +1,23 @@ +package bybit + +import ( + "context" + "net/http" +) + +// Binance Check Server Time endpoint (GET /v5/market/time) +type ServerTime struct { + c *Client +} + +// Send the request +func (s *ServerTime) Do(ctx context.Context, opts ...RequestOption) (res []byte) { + r := &request{ + method: http.MethodGet, + endpoint: "/v5/market/time", + secType: secTypeNone, + } + data, _ := s.c.callAPI(ctx, r, opts...) + res = data + return res +} diff --git a/request.go b/request.go new file mode 100644 index 0000000..015bf7d --- /dev/null +++ b/request.go @@ -0,0 +1,76 @@ +package bybit + +import ( + "fmt" + "io" + "net/http" + "net/url" +) + +type secType int + +const ( + secTypeNone secType = iota + secTypeSigned // private request +) + +type params map[string]interface{} + +// request define an API request +type request struct { + method string + endpoint string + query url.Values + form url.Values + recvWindow string + secType secType + header http.Header + body io.Reader + fullURL string +} + +// addParam add param with key/value to query string +func (r *request) addParam(key string, value interface{}) *request { + if r.query == nil { + r.query = url.Values{} + } + r.query.Add(key, fmt.Sprintf("%v", value)) + return r +} + +// setParam set param with key/value to query string +func (r *request) setParam(key string, value interface{}) *request { + if r.query == nil { + r.query = url.Values{} + } + r.query.Set(key, fmt.Sprintf("%v", value)) + return r +} + +// setParams set params with key/values to query string +func (r *request) setParams(m params) *request { + for k, v := range m { + r.setParam(k, v) + } + return r +} + +func (r *request) validate() (err error) { + if r.query == nil { + r.query = url.Values{} + } + if r.form == nil { + r.form = url.Values{} + } + return nil +} + +// Append `WithRecvWindow(insert_recvwindow)` to request to modify the default recvWindow value +func WithRecvWindow(recvWindow string) RequestOption { + return func(r *request) { + r.recvWindow = recvWindow + } +} + +// RequestOption define option type for request +type RequestOption func(*request)