2023-07-21 06:55:27 +00:00
|
|
|
package bybitapi
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"crypto/hmac"
|
|
|
|
"crypto/sha256"
|
2023-07-25 13:30:49 +00:00
|
|
|
"encoding/hex"
|
2023-07-21 06:55:27 +00:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strconv"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/c9s/requestgen"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
|
|
|
|
"github.com/c9s/bbgo/pkg/types"
|
|
|
|
)
|
|
|
|
|
2023-08-02 08:57:30 +00:00
|
|
|
const (
|
|
|
|
defaultHTTPTimeout = time.Second * 15
|
|
|
|
|
|
|
|
RestBaseURL = "https://api.bybit.com"
|
|
|
|
WsSpotPublicSpotUrl = "wss://stream.bybit.com/v5/public/spot"
|
|
|
|
WsSpotPrivateUrl = "wss://stream.bybit.com/v5/private"
|
|
|
|
)
|
2023-07-21 06:55:27 +00:00
|
|
|
|
|
|
|
// defaultRequestWindowMilliseconds specify how long an HTTP request is valid. It is also used to prevent replay attacks.
|
2023-07-25 13:30:49 +00:00
|
|
|
var defaultRequestWindowMilliseconds = fmt.Sprintf("%d", 5*time.Second.Milliseconds())
|
2023-07-21 06:55:27 +00:00
|
|
|
|
|
|
|
type RestClient struct {
|
|
|
|
requestgen.BaseAPIClient
|
|
|
|
|
|
|
|
key, secret string
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewClient() (*RestClient, error) {
|
|
|
|
u, err := url.Parse(RestBaseURL)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &RestClient{
|
|
|
|
BaseAPIClient: requestgen.BaseAPIClient{
|
|
|
|
BaseURL: u,
|
|
|
|
HttpClient: &http.Client{
|
|
|
|
Timeout: defaultHTTPTimeout,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *RestClient) Auth(key, secret string) {
|
|
|
|
c.key = key
|
|
|
|
// pragma: allowlist secret
|
|
|
|
c.secret = secret
|
|
|
|
}
|
|
|
|
|
|
|
|
// newAuthenticatedRequest creates new http request for authenticated routes.
|
|
|
|
func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) {
|
|
|
|
if len(c.key) == 0 {
|
|
|
|
return nil, errors.New("empty api key")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(c.secret) == 0 {
|
|
|
|
return nil, errors.New("empty api secret")
|
|
|
|
}
|
|
|
|
|
|
|
|
rel, err := url.Parse(refURL)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if params != nil {
|
|
|
|
rel.RawQuery = params.Encode()
|
|
|
|
}
|
|
|
|
|
|
|
|
pathURL := c.BaseURL.ResolveReference(rel)
|
|
|
|
path := pathURL.Path
|
|
|
|
if rel.RawQuery != "" {
|
|
|
|
path += "?" + rel.RawQuery
|
|
|
|
}
|
|
|
|
|
|
|
|
t := time.Now().In(time.UTC)
|
|
|
|
timestamp := strconv.FormatInt(t.UnixMilli(), 10)
|
|
|
|
|
|
|
|
body, err := castPayload(payload)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var signKey string
|
|
|
|
switch method {
|
|
|
|
case http.MethodPost:
|
|
|
|
signKey = timestamp + c.key + defaultRequestWindowMilliseconds + string(body)
|
|
|
|
case http.MethodGet:
|
|
|
|
signKey = timestamp + c.key + defaultRequestWindowMilliseconds + rel.RawQuery
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("unexpected method: %s", method)
|
|
|
|
}
|
|
|
|
|
|
|
|
// See https://bybit-exchange.github.io/docs/v5/guide#create-a-request
|
|
|
|
//
|
|
|
|
// 1. timestamp + API key + (recv_window) + (queryString | jsonBodyString)
|
|
|
|
// 2. Use the HMAC_SHA256 or RSA_SHA256 algorithm to sign the string in step 1, and convert it to a hex
|
|
|
|
// string (HMAC_SHA256) / base64 (RSA_SHA256) to obtain the sign parameter.
|
|
|
|
// 3. Append the sign parameter to request header, and send the HTTP request.
|
2023-08-07 06:55:09 +00:00
|
|
|
signature := Sign(signKey, c.secret)
|
2023-07-21 06:55:27 +00:00
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Add("Content-Type", "application/json")
|
|
|
|
req.Header.Add("X-BAPI-API-KEY", c.key)
|
|
|
|
req.Header.Add("X-BAPI-TIMESTAMP", timestamp)
|
|
|
|
req.Header.Add("X-BAPI-SIGN", signature)
|
|
|
|
req.Header.Add("X-BAPI-RECV-WINDOW", defaultRequestWindowMilliseconds)
|
|
|
|
return req, nil
|
|
|
|
}
|
|
|
|
|
2023-08-07 06:55:09 +00:00
|
|
|
func Sign(payload string, secret string) string {
|
2023-07-21 06:55:27 +00:00
|
|
|
var sig = hmac.New(sha256.New, []byte(secret))
|
|
|
|
_, err := sig.Write([]byte(payload))
|
|
|
|
if err != nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2023-07-25 13:30:49 +00:00
|
|
|
return hex.EncodeToString(sig.Sum(nil))
|
2023-07-21 06:55:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func castPayload(payload interface{}) ([]byte, error) {
|
|
|
|
if payload == nil {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
switch v := payload.(type) {
|
|
|
|
case string:
|
|
|
|
return []byte(v), nil
|
|
|
|
|
|
|
|
case []byte:
|
|
|
|
return v, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
return json.Marshal(payload)
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
sample:
|
|
|
|
|
|
|
|
{
|
|
|
|
"retCode": 0,
|
|
|
|
"retMsg": "OK",
|
|
|
|
"result": {
|
|
|
|
},
|
|
|
|
"retExtInfo": {},
|
|
|
|
"time": 1671017382656
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
|
|
|
|
type APIResponse struct {
|
2023-11-08 14:43:01 +00:00
|
|
|
// Success/Error code
|
|
|
|
RetCode uint `json:"retCode"`
|
|
|
|
// Success/Error msg. OK, success, SUCCESS indicate a successful response
|
|
|
|
RetMsg string `json:"retMsg"`
|
|
|
|
// Business data result
|
|
|
|
Result json.RawMessage `json:"result"`
|
|
|
|
// Extend info. Most of the time, it is {}
|
2023-07-24 15:27:43 +00:00
|
|
|
RetExtInfo json.RawMessage `json:"retExtInfo"`
|
|
|
|
// Time is current timestamp (ms)
|
|
|
|
Time types.MillisecondTimestamp `json:"time"`
|
2023-07-21 06:55:27 +00:00
|
|
|
}
|
2023-11-08 14:43:01 +00:00
|
|
|
|
|
|
|
func (a APIResponse) Validate() error {
|
|
|
|
if a.RetCode != 0 {
|
|
|
|
return a.Error()
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a APIResponse) Error() error {
|
|
|
|
return fmt.Errorf("retCode: %d, retMsg: %s, retExtInfo: %q, time: %s", a.RetCode, a.RetMsg, a.RetExtInfo, a.Time)
|
|
|
|
}
|